Object-Oriented Programming with Java, part I + II

cc

This material is licensed under the Creative Commons BY-NC-SA license, which means that you can use it and distribute it freely so long as you do not erase the names of the original authors. If you make changes in the material and want to distribute this altered version of the material, you have to license it with a similar free license. The use of the material for commercial use is prohibited without a separate agreement.

Authors: Arto Hellas, Matti Luukkainen
Translators to English: Emilia Hjelm, Alex H. Virtanen, Matti Luukkainen, Virpi Sumu, Birunthan Mohanathas, Etiënne Goossens
Extra material added by: Etiënne Goossens, Maurice Snoeren, Johan Talboom

The course is maintained by Technische Informatica Breda


Many Interfaces, and Interface Flexibility

Last week we were introduced to interfaces. An interface defines one or more methods which have to be implemented in the class which implements the interface. The interfaces can be stored into packages like any other class. For instance, the interface Identifiable below is located in the package application.domain, and it defines that the classes which implement it have to implement the method public String getID().

package application.domain;

public interface Identifiable {
    String getID();
}

The class makes use of the interface through the keyword implements. The class Person, which implements the Idenfifiable interface. The getIDof Person class always returns the person ID.

package application.domain;

public class Person implements Identifiable {
    private String name;
    private String id;

    public Person(String name, String id) {
        this.name = name;
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public String getPersonID() {
        return this.id;
    }

    @Override
    public String getID() {
        return getPersonID();
    }

    @Override
    public String toString(){
        return this.name + " ID: " +this.id;
    }
}

An interface strength is that interfaces are also types. All the objects which are created from classes that implement an interface also have that interface’s type. This effictively helps us to build our applications.

We create the class Register, which we can use to search for people against their names. In addition to retrieve single people, Register provides a method to retrieve a list with all the people.

public class Register {
    private HashMap<String, Identifiable> registered;

    public Register() {
        this.registered = new HashMap<String, Identifiable>();
    }

    public void add(Identifiable toBeAdded) {
        this.registered.put(toBeAdded.getID(), toBeAdded);
    }

    public Identifiable get(String id) {
        return this.registered.get(id);
    }

    public List<Identifiable> getAll() {
        return new ArrayList<Identifiable>(registered.values());
    }
}

Using the register is easy.

Register personnel = new Register();
personnel.add( new Person("Pekka", "221078-123X") );
personnel.add( new Person("Jukka", "110956-326B") );

System.out.println( personnel.get("280283-111A") );

Person found = (Person) personnel.get("110956-326B");
System.out.println( found.getName() );

Because the people are recorded in the register as Identifiable, we have to change back their type if we want to deal with people through those methods which are not defined in the interface. This is what happens in the last two lines.

What about if we wanted an operation which returns the people recorded in our register sorted according to their ID?

One class can implement various different interfaces, and our Person class can implement Comparable in addition to Identifiable. When we implement various different interfaces, we separate them with a comma (public class ... implements FirstInterface, SecondInterface ...). When we implement many interfaces, we have to implement all the methods required by all the interfaces. Below, we implement the interface Comparable in the class Person.

package application.domain;

public class Person implements Identifiable, Comparable<Person> {
    private String name;
    private String id;

    public Person(String name, String id) {
        this.name = name;
        this.id = id;
    }

    public String getName() {
        return this.name;
    }

    public String getPersonID() {
        return this.id;
    }

    @Override
    public String getID() {
        return getPersonID();
    }

    @Override
    public int compareTo(Person another) {
        return this.getID().compareTo(another.getID());
    }
}

Now, we can add to the register method sortAndGetEverything:

    public List<Identifiable> sortAndGetEverything() {
        ArrayList<Identifiable> all = new ArrayList<Identifiable>(registered.values());
        Collections.sort(all);
        return all;
    }

However, we notice that our solution does not work. Because the people are recorded into the register as if their type was Identifiable, Person has to implement the interface Comparable<Identifiable> so that our register could sort people with its method Collections.sort(). This means we have to modify Person’s interface:

public class Person implements Identifiable, Comparable<Identifiable> {
    // ...

    @Override
    public int compareTo(Identifiable another) {
        return this.getID().compareTo(another.getID());
    }
}

Now our solution works!

Our Register is unaware of the real type of the objects we record. We can use the class Register to record objects of different types than Person, as long as the object class implements the interface Identifiable. For instance, below we use the register to manage shop sales:

public class Sale implements Identifiable {
    private String name;
    private String barcode;
    private int stockBalance;
    private int price;

    public Sale(String name, String barcode) {
        this.name = name;
        this.barcode = barcode;
    }

    public String getID() {
        return barcode;
    }

    // ...
}

Register products = new Register();
products.add( new Product("milk", "11111111") );
products.add( new Product("yogurt", "11111112") );
products.add( new Product("cheese", "11111113") );

System.out.println( products.get("99999999") );

Product product = (Product)products.get("11111112");
product.increaseStock(100);
product.changePrice(23);

The class Register is quite universal now that it is not dependent on concrete classes. Whatever class which implements Identifiable is compatible with Register. However, the method sortAndGetEverything can only work if we implement the interface Comparable<Identifiable>.

IntelliJ Tips

However it is possible to ask IntelliJ to fill in the method bodies automatically. When you have defined the interface a class should implement, i.e. when you have written

public class Class implements Interface {
}

IntelliJ paints the class name red. If you go to lamp icon on the left corner of the raw, click, and choose Implement all abstract methods, the method bodies will appear in your code!

Exercise interfaces2-1: Moving

Before moving, you pack your things and put them into boxes trying to keep the number of boxes needed as small as possible. In this exercise we simulate packing things into boxes. Each thing has a volume, and boxes have got a maximum capacity.

Exercise interfaces2-1.1: Things and Items

The removers will later on move your things to a track (which is not implemented here); therefore, we first implement the interface Thing, which represents all things and boxes.

The Thing interface has to determine the method int getVolume(), which is needed to understand the size of a thing. Implement the interface Thing in the package moving.domain.

Next, implement the class Item in the package moving.domain. The class receives the item name (String) and volume (int) as parameter. The class has to implement the interface Thing.

Add the method public String getName() to Item, and replace the method public String toString() so that it returns strings which follow the pattern “name (volume dm^3)”. Item should now work like the following

Thing item = new Item("toothbrash", 2);
System.out.println(item);
toothbrash (2 dm^3)

Exercise interfaces2-1.2: Comparable Item

When we pack our items into boxes, we want to start in order from the first items. Implement the interface Comparable in the class Item; the item natural order must be ascending against volume. When you have implemented the interface Comparable, the sort method of class Collection has to work in the following way:

List<Item> items = new ArrayList<Item>();
items.add(new Item("passport", 2));
items.add(new Item("toothbrash", 1));
items.add(new Item("circular saw", 100));

Collections.sort(items);
System.out.println(items);
[toothbrash (1 dm^3), passport (2 dm^3), circular saw (100 dm^3)]

Exercise interfaces2-1.3: Moving Box

Implement now the class Box in the package moving.domain. At first, implement the following method for your Box:

  • the constructor public Box(int maximumCapacity) receives the box maximum capacity as parameter;
  • the method public boolean addThing(Thing thing) adds an item which implements the interface Thing to the box. If it does not fit in the box, the method returns false, otherwise true. The box must store the things into a list.

Also, make your Box implement the interface Thing. The method getVolume has to return the current volume of the things inside the box.

Exercise interfaces2-1.4: Packing Items

Implement the class Packer in the package moving.logic. The constructor of the class Packer is given the parameter int boxesVolume, which determines how big boxes the packer should use.

Afterwards, implement the method public List<Box> packThings(List<Thing> things), which packs things into boxes.

The method should move all the things in the parameter list into boxes, and these boxes should be contained by the list the method returns. You don’t need to pay attention to such situations where the things are bigger than the boxes used by the packer. The tests do not check the way the packer makes use of the moving boxes.

The example below shows how our packer should work:

// the things we want to pack
List<Thing> things = new ArrayList<Thing>();
things.add(new Item("passport", 2));
things.add(new Item("toothbrash", 1));
things.add(new Item("book", 4));
things.add(new Item("circular saw", 8));

// we create a packer which uses boxes whose valume is 10
Packer packer = new Packer(10);

// we ask our packer to pack things into boxes
List<Box> boxes = packer.packThings( things );

System.out.println("number of boxes: "+boxes.size());

for (Box box : boxes) {
    System.out.println("  things in the box: "+box.getVolume()+" dm^3");
}

Prints:

number of boxes: 2
 things in the box: 7 dm^3
 things in the box: 8 dm^3

The packer has packed the things into two boxes, the first box has the firts three things, whose total volume was 7, and the last thing in the list – the circular saw, whose volume was 8 – has gone to the third box. The tests do not set a limit to the number of boxes used by the packer; each thing could have been packed into a different box, and the output would have been:

number of boxes: 4
 things in the box: 2 dm^3
 things in the box: 1 dm^3
 things in the box: 4 dm^3
 things in the box: 8 dm^3

Note: to help testing, it would be convinient to create the method toString for the class Box, for instance; this would help printing the content of the box.