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
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>
.
- All IntelliJ tips can be found here
- Implement all abstract methods Let us suppose that your program contains the interface
Interface
, and you are building the classClass
which implements the>interface. It will be annoying to write the declaration raws of all the interface methods.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!
- Clean and Build Sometimes, IntelliJ may get confused and try to run a code version without noticing all the corrected changes made to it. Usually you notice it because something “strange” happens. Usually, you can fix the problem by using Rebuild operation. The operation is found in the Build menu. Rebuild deletes the translated versions of the code and generates a new translation.
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 packagemoving.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 interfaceThing
.Add the method
public String getName()
to Item, and replace the methodpublic String toString()
so that it returns strings which follow the pattern “name (volume dm^3)”. Item should now work like the followingThing 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 classItem
; the item natural order must be ascending against volume. When you have implemented the interfaceComparable
, the sort method of classCollection
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 packagemoving.domain
. At first, implement the following method for yourBox
:
- 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 interfaceThing
to the box. If it does not fit in the box, the method returnsfalse
, otherwisetrue
. The box must store the things into a list.Also, make your
Box
implement the interfaceThing
. The methodgetVolume
has to return the current volume of the things inside the box.Exercise interfaces2-1.4: Packing Items
Implement the class
Packer
in the packagemoving.logic
. The constructor of the classPacker
is given the parameterint 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 classBox
, for instance; this would help printing the content of the box.