Interfaces

Interfaces in Java allow common functionality (methods) to be declared. Several classes may implement an interface and a class may implement more than one interface. Contrast this with subclassing where in Java a class can only extend a single superclass. Interfaces would not be useful if not for the fact that it is possible to declare reference variables of a an Interface type. Similar to classes, interfaces belong to packages and are defined in a file named <interface_name>.java. The structure of an interface file is as follows:

<package_declaration>
<imports>

<modifier> interface <interface_name> extends {

...

}

In contrast to classes, interfaces only declare methods, but typically do not provide an implementation (definition) for methods. Syntactically, a method declaration is the same as the definition except that the method body is ommitted. Since Java version 8 interfaces are allowed to implement instance (non-static) methods. Such methods are prefixed with the default keyword.

A class can implement zero or more interfaces. These are listed as part of the class declaration like this:

public class MyClass implements interface1, interface2, ... {
    ...
}

Say we have some classes whose instances allow us to access their fields through a method Object getFieldValue(String fieldName). Any class may implement this functionality indepenent of what superclass it is derived from. Thus, it may make sense to define this method in an interface.

In [1]:
package lecture;

public interface GenericFieldAccessor {
    
    // declare a method that every class implementing the interface must define
    public Object getFieldValue(String fieldName);
}
Out[1]:
lecture.GenericFieldAccessor

Now let's define some classes that implement that interface.

In [2]:
package lecture;

public class Person implements GenericFieldAccessor {
    
    private String name;
    private int age;
  
    public Person (String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public Object getFieldValue(String fieldName) {
        if (fieldName.equals("name")) {
            return name;
        }
        else if (fieldName.equals("age")) {
            return new Integer(age);
        }
        // here we would throw an exception to indicate to the caller that the field name does not exist
        return null;
    }
    
}
Out[2]:
lecture.Person
In [3]:
package lecture;

public class Car implements GenericFieldAccessor {
    
    private String brand;
    private String model;
    private int year;
    
    public Car (String brand, String model, int year) {
        this.brand = brand;
        this.model = model;
        this.year = year;
    }
    
    public Object getFieldValue(String fieldName) {
        if (fieldName.equals("brand")) {
            return brand;
        }
        if (fieldName.equals("model")) {
            return model;
        }
        if (fieldName.equals("year")) {
            return year;
        }
        return null;
    }
}
Out[3]:
lecture.Car

Now we can call the getFieldValue method from any object of a class that implements the GenericFieldAccessor interface:

In [4]:
import lecture.Person;
import lecture.Car;
import lecture.GenericFieldAccessor;

GenericFieldAccessor x = new Person("Peter", 59);
GenericFieldAccessor y = new Car("Porsche", "311", 1979);

System.out.println(String.format("Person x has name %s and age %d", x.getFieldValue("name"), x.getFieldValue("age")));
System.out.println(String.format("Car y has brand %s and model %s", y.getFieldValue("brand"), y.getFieldValue("model")));
Person x has name Peter and age 59
Car y has brand Porsche and model 311
Out[4]:
null

Inheritance

In Java, an interface can be inherited from another interface.

So far, GenericFieldAccessor is not that useful. First of a client still has to know exactly which fields a class provides. Futhermore, we still have to add this annoying boilerplate code to every class that implements the interface. We will solve the first problem now and come back to the second problem when we talk about reflection.

In [5]:
package lecture;

public interface GenericFieldAccessorPlus extends GenericFieldAccessor {
    
    public String[] getFieldNames();
}
Out[5]:
lecture.GenericFieldAccessorPlus
In [6]:
package lecture;

public class PersonPlus implements GenericFieldAccessorPlus {
    
    private String name;
    private int age;

    public PersonPlus (String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public Object getFieldValue(String fieldName) {
        if (fieldName.equals("name")) {
            return name;
        }
        else if (fieldName.equals("age")) {
            return new Integer(age);
        }
        // here we would throw an exception to indicate to the caller that the field name does not exist
        return null;
    }
 
    public String[] getFieldNames() {
        return new String[]{ "name" , "age"};
    }
    
}
Out[6]:
lecture.PersonPlus
In [8]:
import lecture.PersonPlus;
import lecture.GenericFieldAccessorPlus;

GenericFieldAccessorPlus x = new PersonPlus("Peter", 59);

for(String fieldName: x.getFieldNames()) {
    System.out.println(String.format("field <%s> has value <%s>", fieldName, x.getFieldValue(fieldName).toString()));
}
field <name> has value <Peter>
field <age> has value <59>
Out[8]:
null

Default methods in interfaces

As an example for default methods consider a scenario where we did created the GenericFieldAccessor interface and have already created several classes that implement this interface. Now if we want to extend this interface to add the getFieldNames method. If we do this, then we have to extend all classes implementing this interface with an implementation of this method. For better backward compatibility we could provide a default implementation to avoid having to change all of these classes.

In [14]:
package lecture;

public interface GenericFieldAccessorDefault extends GenericFieldAccessor {
    
    // declare a method that every class implementing the interface must define
    public default Object getFieldNames() throws UnsupportedOperationException {
        throw new UnsupportedOperationException("not implemented by this class");
    }
}
Out[14]:
lecture.GenericFieldAccessorDefault
In [15]:
package lecture;

public class PersonDefault implements GenericFieldAccessorDefault {
    
    private String name;
    private int age;
  
    public PersonDefault (String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public Object getFieldValue(String fieldName) {
        if (fieldName.equals("name")) {
            return name;
        }
        else if (fieldName.equals("age")) {
            return new Integer(age);
        }
        // here we would throw an exception to indicate to the caller that the field name does not exist
        return null;
    }
    
}
Out[15]:
lecture.PersonDefault
In [16]:
import lecture.GenericFieldAccessorDefault;
import lecture.PersonDefault;
    
GenericFieldAccessorDefault g = new PersonDefault("Peter", 13);
g.getFieldNames();
ERROR: java.lang.UnsupportedOperationException: not implemented by this class

Abstract Classes

Abstract classes are used to implement common functionality in a class hierarchy that does not result in a fully functional class. An abstract class can mix fully implemented methods and abstract methods that are declared (like in an interface) but not implemented. It is not possible to create instances of an abstract class. A non-abstract class extending an abstract class has to provide implementations for all of its abstract methods.

Differences between abstract classes and interfaces:

  • Interfaces can only define static fields
  • Interfaces allow for multiple inheritence

Nested Classes

A nested class is a class which is defined in the top-level block of another class. Nested (and only nested) classes can be static. A typical use case for nested classes are helper classes that are strongly associated with the class utilizing them. For instance, the ListCell of list class would be a good example. Nested classes are referenced using their enclosing class like this: OuterClass.InnerClass. Instances of a non-static nested class can only be created using an instance of the enclosing class (called the outer class). The syntax for doing this outside of the enclosing classs is:

OuterClass x = new OuterClass();
OuterClass.InnerClass inner  = x.new InnerClass();

Within the enclosing class it instances are just created using new, e.g., new InnerClass(). For static nested classes this is not required and the syntax to instantiate them is:

OuterClass.StaticInnerClass inner = new OuterClass.StaticInnerClass();
In [9]:
package lecture;

// List with nested class for list cells
public class List {
    
    public class ListCell {
        public String val;
        public ListCell next;
        
        public ListCell(String val) {
            this.val = val;
            this.next = null;
        }
    }
    
    private ListCell head;
    private ListCell tail;
    private int size;
    
    public List () {
        size = 0;
        head = null;
        tail = null;
    }
    
    public void append(String val) {
        ListCell newTail = new ListCell(val);

        if (head == null) {
            head = newTail;
            tail = newTail;    
        }
        else {
            tail.next = newTail;
            tail = newTail;
        }
        size++;
    }
    
    public String get(int pos) {
        ListCell cur = head;
        if (pos < 0 || pos >= size)
            throw new IndexOutOfBoundsException();
        
        for(int i = 0; i < pos; i++)
            cur = cur.next;
        
        return cur.val;
    }
    
    public String toString() {
        StringBuilder b = new StringBuilder();
        ListCell cur = head;
        
        b.append('[');
        while(cur != null) {
            b.append(cur.val);
            if (cur.next != null)
                b.append(", ");
            cur = cur.next;
        }
        b.append(']');
        return b.toString();
    }
    
}
Out[9]:
lecture.List
In [10]:
import lecture.List;

List a = new List();
a.append("A");
a.append("B");
a.append("C");

// create a list cell outside of the list class (just for illustration)
a.new ListCell("test");

return a.toString();
Out[10]:
[A, B, C]

Anonymous classes

Sometimes a class will only be instantiated once. For instance, when using a comparator to sort a collection, there may only be the need to instantiate this class once and use this single instance for sorting collections of this type. Creating a separate .java file for such a class may be overkill. Java supports this use case through anonymous classes which defined as part of a new statement.

In [1]:
import java.util.Comparator;
import java.util.ArrayList;
import java.util.Collections;

ArrayList<String> a = new ArrayList<String> ();
a.add("abcD");
a.add("agbA");
a.add("asdasC");
a.add("asdasdB");

Comparator<String> lastCharComp = new Comparator<String> () {
    
    public int compare(String o1, String o2) {
        char last1 = o1.charAt(o1.length() - 1);
        char last2 = o2.charAt(o2.length() - 1);
        
        if (last1 < last2)
            return -1;
        if (last1 > last2)
            return 1;
        return 0;
    }
    
};

Collections.sort(a, lastCharComp);
return a;
Out[1]:
[agbA, asdasdB, asdasC, abcD]