7  Inheritance

A class can be declared a sub-type of another (base) class, the new (derived) class, implicitly contains (inherits), all the members of the class it inherits from.

The derived class can augment the base class structure with any additional member that it defines explicitly. Moreover, it Can override the definition of existing methods by providing its own implementation

The code of the new derived class consists only of the augmentations and overrides w.r.t. the base class.

Example base class

class Employee{
    String name;
    double wage;
    void incrementWage(){
    wage += 10;
  } 
  void print(){ 
    System.out.println(name);
  }
}

In Java the inheritance relation between two classes is defined in the declaration of the derived class, using the keyword extends followed by the name of the base class.

An example of class Manager that inherits from Employee to augment it:

class Manager extends Employee{
1    String managedUnit;
2    void changeUnit(String u){
    managerUnit = u;
  }

3  public void print(){
      System.out.println (name + ", manages " + managedUnit);
  }

}
1
a new attribute is added to those inherited from Employee
2
a new method is added to those inherited from Employee
3
method print() overrides the one defined in class Employee

With the above defintion, it is possible in any method to write:

1Manager m = new Manager();

2m.incrementWage();
3m.changeUnit("Section 21");

4m.print();
1
a new Manager object is created
2
even if the Manager class does not explicitly define an incrementWage() method it inherits it from Employee
3
method changeUnit() is not present in the base class but it has been added
4
the print() method is invoked, the version in Manager is used instead of the overridden one in Employee

7.1 Why inheritance

There are several motivation for using inheritance in OO languages.

The first and most obvious advantage of inheritance is reuse.

Often, a class consists of minor modifications of an already existing class, inheritance avoids code repetitions. As a consequence it enables localization of code, i.e. having the code specific to a given feature collected together. The results are:

  • fixing a bug in the base class automatically fixes it in all the subclasses;
  • adding a new functionality in the base class automatically adds it in the subclasses too;
  • less chances of different (and inconsistent) implementations of the same operation.

A second important aspect, deriving from the principle of substitution, is flexibility.

Often, a program needs to treat several objects belonging to different classes in the same way. Two properties of inheritance in OO languages make this easy and seamless:

  • polymorphism allows feeding algorithms with objects of different classes, provided they share a common base class;
  • dynamic binding allows accommodating different behavior behind the same interface.

7.1.1 Generalization

Inheritance implies a relation of generalization or specialization. Generalization when moving from derived classes to base class, specialization when moving in the opposite direction. Let us consider, for instance, the example UML diagram shown in fig

Figure 7.1: UML Class Diagram of Employee, Manager, CEO

The three classes, Employee, Manager, and CEO are linked by inheritance relationships. In particular Manager extends/inherits from Employee, and CEO extends/inherits from Manager. As a consequence the sets of objects instances of the three classes are related by subset relations as shown in Figure 7.2.

Figure 7.2: Inheritance and subsets

\[ \textsf{CEO} \subset \textsf{Manager} \subset \textsf{Employee} \]

In practice every CEO is also a manager and every manager is also an employee.

7.1.2 Inheritance graphs

Inheritance can exist on multiple leveles, as in Figure 7.1. Considering the classes as nodes and the extends relations as edges, we obtain a Directed Acyclic Graph (DAG).

In praticular when we consider a base class and all the classes that – either directly or indirectly – inherit from that class, the result is a tree graph that is usually called inheritance tree.

As a principle of good design it is important to remember that too deep inheritance trees might reduce the code understandability. In fact, to figure out the structure and behavior of a class in the tree, you need to look into each and every ancestor class

A commonly used measure to asses the complexity of an inheritance tree si the Depth of Inheritance Tree (\(DIT\)).

\(DIT\) is defined as the number of levels that exist below the root base class.

A general rule of thumb is to keep \(DIT \le 5\). Please note that this is an empirical limit that is not universally valid. It is useful, whenever the \(DIT\) in your code is above the threshold, to check the understandability of the code.

7.1.3 Terminology

The usual terminology adopted with respect to inheritance is:

  • Class one above: parent class
  • Class one below: child class
  • Class one or more above: Superclass, Ancestor class, Base class
  • Class one or more below: Subclass, Descendent class, Derived class

7.2 Polymorphism and Dynamic binding

7.2.1 Polymorphism

A reference of type \(T\) can point to – be dereferenced to – an object of type S if-and-only-if ether

  • \(S\) is equal to \(T\) or
  • \(S\) is a subclass of \(T\).

More formally: \[ \mathrm{type}( r ) = T \land \mathrm{deref}( r ) ~ \mathtt{instance_of} ~ S \iff S = T \lor T ~ \mathtt{extends} ~ S \]

where:

  • \(r\) is a reference
  • \(T\) and \(S\) are class types
  • \(\mathrm{type}\) is a function that links a reference to its type
  • \(\mathrm{deref}\) links a reference to the object it points to
  • \(\mathtt{instance_of}\) is the relationship between an object and its class
  • \(\mathtt{extends}\) is the relationship of inheritance between two classes

For instance, taking the classes defined above:

* Employee e;
1* e = new Employee();
2* e = new Manager();
1
usual way, reference type and object type are the same (Employee = Employee)
2
e is polymorphic and refers to an object of a derived class (Manager extends Employee)

The compiler treats indifferently objects of different classes, provided they derive from a common base class. Polymorphism can be leveraged, e.g., to collect objects that are instances of different classes into a single container:

Employee[] team = {
  new Manager("Mary Black",25000,"IT"),
  new Employee("John Smith",12000),
  new Employee("Jane Doe",12000),
  new CEO("Elma Moss",300000)
};

Polymorphism in a strongly typed language – such as Java – allows the compiler to perform strict static type checking.

The compiler performs a check on method invocation on the basis of the reference type, which is the only information available at compile time.

for(Employee it : team){      
  it.print();
}

The compiler checks whether type of it (i.e.Employee) provides the method print() with no arguments. If that is true the call is considered correct. In fact it can point to any subclass of Employee and all them will inherit that method.

7.2.2 Dynamic Binding

When talkin about OO in general terms we say that the the methods of a class define the messages that can be received by the objects of that class. Up to now – before inheritance – we mixed the concepts of message and method.

When we consider the possibility for a derived class to override a method defined in a base class, the association between the message (i.e. method invocation) and method (i.e. method actual execution) is not unique anymore.

The association between the message (invoked method) and method (execution) is performed by the JVM at run-time using the dynamic binding procedure.

Dynamic binding procedure:

  1. The JVM retrieves the effective class of the target object
  2. If that class defines or overrides the required method, that version of the method is executed
  3. Otherwise the parent class is considered and step 2 is repeated

Note that the procedure is guaranteed to terminate since the compiler checks the reference type class (a base of the actual one) defines the method.

Considering the following example:

for(Employee it : team){
  it.print();
}

Based on the effective class of the object referenced by it in each iteration, a different version of the method print() shall be executed. If the effective class is

  • Employee, the class defines the original version which is executed,
  • Manager, the class defines an override of the method which is executed,
  • CEO, the class does not define any matching method, thus the parent class Manager is considered, which defines an override that is executed.

The important consequence of the dynamic binding procedure is that several objects from different classes, sharing a common ancestor class can be treated uniformly. It is possible to write algorithms for the base class (using the relative methods) and apply them unmodified to any subclass.

7.2.3 Substitutability principle

In practice polymorphism and dynamic binding enable the substitutability principle (Liskov and Wing 1994).

We can formulate the principle as: if \(S\) is a subtype of \(T\), then objects of type \(T\) may be substituted with objects of type \(S\) without affecting the properties valid for the original objects.

This principle is also known as the Liskov Substitution Principle (LSP) from Barbara Liskov who first proposed it.

7.2.4 Inheritance vs. Duck typing

Dynamic binding can be applied in strict relation to inheritance or unrelated to it. This latter approach is used in other languages, e.g. Python.

The languages that use dynamic binding independently from inheritances are said to use Duck typing.

If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck

The compiler does not perform any type check for method invocation. The correctness of method invocation is checked at run-time only. Invocation is correct if the effective class of the target object provides the required method (directly or inherited).

In languages using duck typing dynamic binding can result into a run-time error. While languages using strict type checking guarantee dynamic binding procedure always succeed.

7.2.5 Override rules

A method override must use exactly the original (overridden) method signature.

Visibility cannot be restricted since it would affect the correctness of the dynamic binding procedure. Although the method override may widen visibility (e.g. from package to public).

When a slightly different method is defined in a derived class, the compiler (and JVM) will not considered it as an override and therefore it will not be considere by the dynamic binding procedure. As a result, minor mistakes in typing might jeopardize correct behavior at run-time.

The annotation@Override before a method informs the compiler that a method is intended as an override. Thus the compiler generates an error if it is not a correct override, i.e. if the signature does not match that of method in a superclass.

Example of improper override:

class MiddleManager extends Employee{
1  public void Print(){ ... }
}
1
the method is not an override since is stars with a capital letter while the original method in class Employee starts with a lowercase letter.

The result is that at run-time this variation will not be executed, and the version in the base class will be used instead.

The recommended way of defining an override is:

class MiddleManager extends Employee{
1    @Override
2    public void Print(){}
}
1
the @Override annotation informs the compiler that the following method is intedend to be an override
2
the compilers produces an error telling the method Print() should override a method present in a super class.

7.3 Casting

Java is a strongly typed programming language, i.e., each variable has a type and a variable can host only values of that type.

float f;
1f = 4.7;
2f = false;
Car c;   
3c = new Car();
4c = "car";
1
legal assignment floating point value to FP variable
2
illegal assignment: boolean value to FP variable
3
legal assignment: Car reference to Car variable
4
illegal assignment: string reference to Car variable

Cast with primitive types correspond to a type conversion, either explicit or implicit

int i = 42;
1float f = i;
2f = (float) i;
1
implicit cast: 2’s complement to floating point
2
explicit cast

7.3.1 Upcast

Assignment from a more specific type (subtype) to a more general type (supertype) is called an up-cast. The term derives from the convetion in class diagram (see Figure 7.1) of representing base classes at the top and derived classes at the bottom, therefore a conversion form a derived to a base class moves up.

Employee e = new Employee();
Manager m = new Manager();
Employee em = (Employee) m;

Given the set interpretation of inheritance we have that:

\[ \forall m \in \mathrm{Manager} : m \in \mathrm{Employee}\]

Therefore upcasts are always type-safe and are performed implicitly by the compiler. The cast (Employee) from the previous code be safely omitte:

Employee em = m;

It is importat to remember that reference type and object type are distinct concepts. When casts involve class types, we do not have any “conversion” of the value but just a change in reference type. In practice only the reference is affected while the pointed object remains totally unaltered. In the previous example the object referenced to by em continues to be of Manager type. Notably, in contrast, a primitive type cast involves a value conversion.

7.3.2 Downcast

Assignment from a more general type (supertype) to a more specific one (subtype) is called a down-cast.

Employee em = new ...() ;
Manager mm = (Manager)em  ; 

\[ \begin{array}{l} \exists em \in \mathrm{Employee} : em \notin \mathrm{Manager}\\ \end{array} \]

Since it is possible that te cast is not correct, no automatic conversion is provided by the compiler. Down-casts must be explicit, so that they force the programmer to take responsibility of checking the cast is valid.

To access a member defined in a class you need a reference of that class type – or any subclass –. Therefore when the starting point is a generic (base class) reference, a down-cast can be required to access specific (derived class) members:

1Employee emp = team[0];
2String d = emp.getDepartment();
3Manager mgr = (Manager)team[0];
4d = mgr.getDepartment();
1
simple assignment without casting
2
syntaxError: The method getDepartment() is undefined for the type Employee
3
down-cast to Manager, correctness guaranteed by the developer
4
correct

Warning: the compiler trusts any downcast written by the programmer. The JVM checks type consistency for all reference assignments, at run-time, it ensures that the class of the object must be equal to the class of the reference or to any of its subclasses.

1mgr = (Manager)team[1];
1
ClassCastException: Employee cannot be cast to Manager

To ensure downcast safety is is possible to use the the instanceof operator: aReference instanceof aClass evaluates to true if the object pointed to by the reference can be cast to the class, i.e. if the object belongs to the given class or to any of its subclasses.

Example instanceof

instanceof Employee Manager CEO
anEmployee true false false
aManager true true false
aCEO true true true

A safe way of rewriting the above code is:

if(team[1] instanceof Manager){
  mgr = (Manager)team[1];
}

The above code idiom is not easy to read and redundant, for this reason since Java 16 the Java language allows the pattern matching instanceof. The new syntax allows writing the check with instanceof followed by a safe downcast can be written in a more compact and efficient form as:

if(team[1]instanceofManager mgr){
    // mgr is in scope here
}

7.4 Inheritance and Visibility (scope)

The classes related by inheritance follow the usual rules concerning visibility an access. Therefore a derived class is not allowd to access a base class private members.

class Employee {
  private String name;
  private double wage;
}

class Manager extends Employee {
  void print() {
1    System.out.println  (  "  Manager  "  + name +   "     "   + wage);
  }
}
1
name and wage are not visible

Since it is quite common, for a derived class to require access to the base class members, specific visibility rules must be defined for such cases. For this reason an additional visibility level, marked by the modifier protected, is available.

The complete rules for visibily are:

  • public members are always accessible,
  • protect members dare accessible from classes in the same package and subclasses,
  • package members are accessible from classes in the same package,
  • private members are accessible only from within the declaring class.

In summary:

Method in the same class Method of other class in the same package Method of subclass Method of class in other package
private yes
package yes yes
protected yes yes yes
public yes yes yes yes

7.5 Inheritance and constructors

The construction of an instance of a derived class must take into consideration the inherited parts. Since each object “contains” an instance of the parent class – i.e., the inherited attributes–, this part must be initialized first so that the derived class-specific parts can give that part for granted.

The Java compiler automatically inserts a call to the default constructor – i.e. any constructor without arguments – of the parent class. The call is inserted as the first statement of each child class constructor. Execution of constructors proceeds top-down in the inheritance hierarchy. As a consequence, when a method of the child class is executed (constructor included), the super-class is completely initialized already

As an example, given the follwing constructions that print a message:

class Employee {

  Employee() {
    System.out.println("ctor Employee");
  }
}
class Manager extends Employee  { 
  Manager() {
    System.out.println("ctor Manager"); 
  }
}
class CEO extends Manager {
  CEO() {
    System.out.println("ctor CEO");
  }
}

The creation of a CEO object

CEO ceo = new CEO();

will produce the following outpu

ctorEmployee
ctorManager
ctorCEO

7.5.1 super (constructor)

The procedure described above works it there is a default constructor, defined explicitly or implicitly (provided by the compiler is no constructor is defined).

If a constructor with arguments is defined, the default constructor provided by the compiler “disappears”, as a consequence, the derived class constructor implicitly invokes the base class constructor which cannot be resolved

class Employee{
  private String name;
  private double wage;

1  Employee(String name, double wage){
    name = n;
    wage = w;
  }
}
1
an explicit constructor is defined, thus no default (void) constructor is not defined
1class Manager extends Employee{
}
1
Error: no matching constructor in class Employee, the default constuctor is not available

The child class constructor must call the right constructor of the parent class, explicitly. It possible to use super() to invoke the right constructor of parent class and pass the appropriate arguments. It must be the first statement in a child class’s constructor

Example:

class Manager extends Employee{
  private String unit;

  Manager(String n, double w, String u) {
1    super(n,w);
2    unit = u;
  }
}
1
the constructor of the super class is invoked with the two required arguments n and w
2
the inizialization of the specific attribute unit

When no super invocation is present in the constructor, the compiler automatically adds a super() that invokes the default constructor.

7.5.2 super (reference) example

When a method in a derived class overrides one in the base class, the latter is masked, i.e., the overridden method is invisible from the derived class as well as the users of the class. This might represent a problem if we wish to re-use the original overridden method from within the subclass.

To overcome this limitation, it is possible to use the super special reference that allow referring to members as defined in the super class.

class Employee{
  String name;
  public void print(){ 
    System.out.println  (name);
  }
}
class Manager extends Employee{
  private String unit;

  public void print(){
    super.print();
    System.out.println("\tmanages " + managedUnit);
  }
}

There are two special references always available in the methods of a class:

  • this references the current object
  • super references the current object as if it were a parent class instance

In conformace with the visibility rules, it is possible to redefine an attribute defined in the base class. The attribute defined in the derived class masks the one defined in the base class. To access the masked out attribute from the derived class, it is possible to use the super reference.

class Parent{
  protected int attr = 7;
}
class Child extends Parent {
1  protected String attr = "hello";
  void print(){
2    System.out.println( super.attr );
    System.out.println( attr );
  }  
}
1
the attr attribute mask the attribute with the same name in class Parent
2
the super reference allows accessin the masked attribute

As far as good design practices, while method override is extremely useful and widely adopted, attribute override is not a recommended practice as it can create confusion.

7.5.3 final inheritance

The keyword final applied to a method makes it not overridable by subclasses. It can be useful when methods must keep a predefined behavior, e.g. method provide basic service to other classes.

The keyword final applied to a class makes it not extensible. It can be useful the class behavior must be preserved because it is essential to many other classes. For instance, String as well all wrapper classes are defined as static because defining a sub-class (whose behavior may substantilly differ from the base class) and passing the relative instances instead of, e.g. a string, might afffect the behavior of other classes.

7.6 Class Object

in Java there is a class called “Object”, it look like an antinomy but is is not.

All classes are subtypes of java.lang.Object. Whenever a new class is defined without a base class – i.e. without the extends clause – the compiler automatically adds extends Object.

7.6.1 Why class Object

The first motivation to have a common base class to all classes defined in the language is generality. Any instance of any class can be seen as an Object instance, thus Object is the universal reference type, much like the void* in C.

Since each class is either directly or indirectly a subclass of Object, it is always possible to upcast any reference to Object.

As a consequence, we can collect heterogeneous objects into a single container using Object references

Object[] objects = {
          "First!",
          new Employee(),
          Integer.valueOf(42),
          22.0 / 7.0 // autoboxed to Double
};
for(Object obj: objects){
  System.out.println( obj );
}

Since Object references can point to objects but not to primitive types, wrappers must be used instead of primitive types.

A second motivation is to have a minimal univesal common behavior. Class Object defines some common operations which are inherited by all classes. Often, they are overridden in sub-classes.

The Object class defines the following methods:

  • toString(), returns string representation of the object
  • equals(), checks if two objects have same contents
  • hashCode(), returns a unique code
  • clone(), (protected) creates a copy of the current object
  • finalize(), (protected) invoked by garbage collector upon memory reclamation.

7.6.2 Object.toString()

The method toString() returns a string representing the object contents. The default (Object) implementation returns “ClassName@hashcode”, e.g. org.Employee@af9e22.

The typical implementation of the method returns the contents at – some – attributes. To avoid possible mistakes, it is recommended to us the @Override notation.

class Employee {
  // ...
  @Override
  public StringtoString(){
    return "Employee: " + name;
  }
}

More in general the toString() method is used whenever a string representation is required, e.g. when

  • an object is passed to a print method
  • an object is concatenated with a string

When invoking the System.out.print( Object ) method, it implicitly calls the toString() the argument to obtain the string representation to be printed. In fact, all the print or write methods call the valueOf() method of class String that is reported below. This approach is more robust than directly calling toString() since it avoids a NullPointerException that would occur if the object to be printed is null and returns simply "null" in such a case.

Listing 7.1: Implementation of the valueOf(Object) method in String
public static String valueOf(Object obj) {
  return (obj == null) ? "null" : obj.toString();
}

The same approach is taken when the string concatenation operator + is applied to an object: the same valueOf() method is called, providing a robust way of concatenating object references that might possibly be null.

Note

It is a good design practice to override the toString() method for small objects whose representation can fit a limited number of characters. The string representation of an object is extremely useful during debugging. It can be printed or logged to keep track of what happens during execution. Moreover, most debuggers call the toString() method to show a representation of the objects.

7.6.3 Object.equals()

The method equals() is used to test the content equality of two objects. It is called on the first object to be compared while the second is passed as argument to the method: o1.equals(o2).

The default implementation (in class Object) compares references:

Listing 7.2: Implementation of the equals() method in Object
public boolean equals(Object other){
    return this == other;
}

When overridden, it must respect a contract, i.e. a set of properties:

  • Reflexive: x.equals(x) == true
  • Symmetric: x.equals(y) == y.equals(x)
  • Transitive: for any reference x, y and z:
    x.equals(y) && y.equals(z) \(\implies\) x.equals(z)
  • Consistent: multiple calls to x.equals(y) must consistently return the same value, this means that no information used in the comparison should be changed (immutables);
  • Robust: x.equals(null) == false

An example of equals() override

@Override
public boolean equals(Object o){
  if(o instanceof Employee other)){
    return this.name.equals(other.name);
  }
  return false;
}

The above implementation relays the check of equality among employees to a check of equality of their names, i.e. it calls the equals() method of class String. It satifies the equals contract, in details:

  • it is reflexive, symmetric, and transitive since String.equals() is reflexive, symmetric, and transitive,
  • it is consistent if the name attribute is immutable,
  • it is robust since null instanceof Employee == false.

7.6.4 Object.hashCode()

The hashCode() method returns a (fairly) unique code for the object that can be used as index in hash tables or as a quick approximation for equality.

The default implementation (Object) converts the internal address of the object into an integer. It must be overridden to return codes that are function of the object state.

When overridden it must comply with the hashCode() contract

  • Consistent: hashCode() must return the same value, if no information used in equals() is modified.
  • Equal compliant:
    • x.equals(y) \(\implies\) x.hashCode() == y.hashCode()
    • If two objects are not equal, then calling hashCode() may return distinct values, producing distinct values for not equal objects may improve the performance of hash tables

hashCode() vs. equals()

Condition Required Not Required (but allowed)
x.equals(y) == true x.hashCode()== y.hashCode()
x.hashCode() == y.hashCode() x.equals(y)== true
x.equals(y) == false -
x.hashCode() != y.hashCode() x.equals(y)== false

7.6.5 Variable arguments - example

The Object class is used as the universal reference type when passing a variable list of arguments with the ... syntax. In such cases the list of arguments is available in the method as an Object[].

Example of variable arguments list:

static void printList(String pre, Object... args){
  System.out.print  (pre + " ");
  boolean first = true;
  for(Object o :args){
    if(!first){
      System.out.print(", ");
    }else{
      first = false;
    }
    System.out.print(o);
  }
  System.out.println  ();
}

public static void main(String[]   args  ) {
    printList("List:", "A", 'b', 123,   "  hi\!  "  );
}

7.6.6 Array Covariance

The usage of the Object reference as a generic reference type for elements in arrays is based on the assumption of co-variance of arrays.

Given that we have two classes – \(T\) and \(S\) – related by inheritance, \(T~\mathtt{extends}~S\), what is the relationship among the arrays having element of the two types? There are three possibilities:

\[ T~\mathtt{extends}~S \implies \begin{cases} T[]~\mathtt{extends}~S[] & \mathsf{covariant}\\ S[]~\mathtt{extends}~T[] & \mathsf{contravariant}\\ - & \mathsf{invariant}\\ \end{cases} \]

The choice made in Java is to make arrays covariant, therefore e.g. String[] extends Object[]. Covariance is ideal when reading content of arrays but is may create problems when writing. For instance, the following code is correct according to the Java compiler:

String[] names = {"one","two","three"};
1Object[] objects = names;
2objects[0] = Integer.valueOf(1);
3String s = names[0];
1
due to covariance of arrays this is considered an up-cast
2
this is a regular up-cast, but produces an ArrayStoreException error
3
if the previous asignment were correct this would be an error

In practice whan assigning to an element of an array, the JVM checks the compatibility of the value with the type of the elements.

While the choice of making arrays covariant allows writing code that is unsafe at run-time when writing in the array, it allows a great flexibility since it permits defining general methods accepting Object[].

7.7 Abstract classes

Often, a superclass is used to define common behavior for children classes. In this case some methods may have no obvious meaningful implementation in the superclass.

Abstract classes can leave the behavior partially unspecified, i.e. some method – abstract – can provide no body. Since they are incomplete, abstract classes cannot be instantiated.

A class can be declared abstract using the abstract modifier. The same modifier can be used in front of the methods that are left unimplemented.

public abstract class Shape {
  private int color ;
  public void setColor ( int color ){
    this.color  = color ;
  }

  // to be implemented in child classes
  public abstract void draw();
}

In general when a class represent an abstract concept and no implementation can be devised for a method, declaring it abstract is much better than writing a warning message, such as: System.err.println("Sorry, don’t know how to draw this shape");

Then the classes extending the abstract class must provide an implementation to the abstract methods.

public class Circle extends Shape{
  public void draw(){
    // body goes here
  }
}
1Shape a = new Shape();
2Shape a = new Circle();
1
since Shape is abstract is cannot be instantiated, this is an error
2
class Circle is concrete, therefore it can be instantiated

The abstract modifier marks the method as non-complete / undefined The modifier must be applied to all incomplete method and to the class A class must be declared abstract if any of its methods is abstract

A class that extends an abstract class should implement (i.e. override) all the base class abstract methods If any abstract method is not implemented, then the class must be declared abstract itself.

7.7.1 Template Method Pattern

Example: Sorter

Without the use of abstract

public class Sorter {
  public void sort(Object v[]){
    for(int i=1; i<v.length; ++i){
      for(int j=0; j<v.length-i; ++j){
        if(compare(v[j],v[j\+1])>0){ 
          Object o=v[j];
          v[j]=v[j+1]; v[j+1]=o;
        }
      }
    }
  }

  protected int compare(Object a, Object b){
    System.err.println  ("Someone forgot about the compare() method!");
    return 0;   // why not 42?
  }
}

What else could we do here?

public abstract class Sorter {
  public void sort(Object v[]){
    for(int i=1; i<v.length; ++i){
      for(int j=0; j<v.length-i; ++j){
        if(compare(v[j],v[j\+1])>0){ 
          Object o=v[j];
          v[j]=v[j+1]; v[j+1]=o;
        }
      }
    }
  }

  protected abstract int compare(Object a, Object b);
}

Example: StringSorter

public class StringSorter extends Sorter{
    @Override
  int compare(Object a, Object b){
    String sa = (String)a;
    String sb = (String)b;
    return sa.compareTo(sb);
  }
}

Sample usage:

 Sorter ssrt= new StringSorter();
 String[] v={"g","t","h","n","j","k"};
 ssrt.sort(v);

Template Method Example

  • Context:
    • An algorithm/behavior has a stable core and several variation at given points
  • Problem
    • You have to implement/maintain several almost identical pieces of code

See slide deck on design patterns

7.7.2 Composite Pattern

Abstract Expression Tree

(img/J03-JavaInheritance13.wmf)

Expression Tree example

 Expression e =

   new Operation('\*',

                    new Value(6),

                    new Operation('\+',

                                new Value(4),

                                new Value(3)));

 System.out.println(e.formula() \+ " = "    

                                 \+e.eval());
public abstract class Expression{

  public abstract double eval();

  public abstract String formula();

}

 public class Value extends Expression{

      private double value;

      public Value(double v){ value = v; }

_@Override_

  public double eval() { return value; }

_@Override_

  public String formula() {

    return   String.valueOf  (value);

  }   }
 public class Operation extends Expression{

   private char op;

      private Expression left, right;

   public Addition(char o, Expression l,

                           Expression r){

          op = o; left=l; right=r;

      }

  public double eval() {

   switch(op){

    case ’\+: return   left.eval  ()\+  right.eval  ();

}

  }

  public String formula() {

        return "(" \+   left.formula  () \+ " " \+ op \+ 

           " " \+   right.formula  () \+ ")";

}    }

Composite Pattern

  • Context:
    • You need to represent part-whole hierarchies of objects
  • Problem
    • Clients need to access a unique interface
    • There are structural difference between composite objects and individual objects.

7.8 Interfaces

A Java interface is a Special type of class where:

  • Methods are implicitly abstract (no body)
  • Attributes are implicitly static and final
  • Members are implicitly public

It is defined with the keyword interface instead of class. Cannot be instantiated like abstract classes, i.e. no new. Can be used as a type for references

Interface example

public interface Expression{
  double eval();
  String formula();
}

When a class implements (in place of extends) an interface, must override all the interface methods – unless the class is declared abstract –.

public class Value implements Expression {
  @Override 
  void value(){}

  @Override 
  void formula(){}
}

Warning

In object-oriented jargon the general term interface is used to indicate the set of publicly available methods, or a subset, when talking about many interfaces. A Java interface, is a distinct though related concept. When a class implements an interface the methods defined in the interface constitute an interface of the class.

7.8.1 Interfaces and inheritance:

An interface:

  • cannot extend a class
  • can extend many interfaces

A class:

  • can extend a single class (exactly one, if not explicit it is Object)
    • Single inheritance
  • implement multiple interfaces
    • Multiple inheritance
class Person extends Employee implements Cloneable, Comparable {}

Inheritance Classes & Interfaces

Summary of inheritance between classes and interfaces.
Class Interface
Class
extends
(exactly one)
implements
(up to many)
Interface
X extends
(up to many)

7.8.2 Anonymous Classes

  • Interfaces can be used to instantiate anonymous local inner classes.

An anonymous inner class is defined within a method code, uses a variation of the new operator and provides the implementation of the interfaces method:

Iface obj = new Iface(){
  public void method(){  …  }
};

7.8.3 Static methods in interfaces

Interfaces can host static methods with the following restriction:

  • Cannot refer to instance methods (without a reference), like in regular classes;
  • Cannot change static attributes since they are final by default in interfaces;
  • Can be overridden, since Java 8

7.8.4 Default methods

There is a specific type of methods in interfaces that can hava an implementatin, such methods are called default methods. This kind of methods has been available since Java 8.

The default methods:

  • Can refer to arguments and other methods
  • Cannot refer to non-static attributes, since they are unknown to the interface
  • Can be overridden in implementing classes as any regular method

The main motivation for default methods is to provide the capability of injecting a specific behavior inside interfaces that would otherwise be pure declarations. This is similar to regular methods in classes, but unlike classes, they leverage multiple inheritance.

The specific use case that led to the definition of default methods is the need to enhance classe that implement pre-existing interfaces – from previou versions of Java – by adding new functionality and still ensuring compatibility with existing code written for older versions of those interfaces.

In fact, default methods let multiple inheritance, which was banned entering the Java language from the main door of classes, re-enter through the window of interfaces.

Therefore default methods may lead to conflicts among methods.

Let us considera a fictious example with two interfaces:

interface If1{
    int m1(inti);
    default void m2(){
            // do something...
    }
}
interface If2 {
  int m3(inti);
    default void m2() {
            // do something...
    }
}

And a class the implements both interaces:

class Cls implements If1, If2 {
  @Override
  public int m3(inti) { /* .. */ }

  @Override
  public int m1(inti) { /* .. */ }
}

The class has a duplicate default methods named m2 without arguments that are inherited from the interfaces If1 and If2.

To resolve the conflit, the class must provide its own version of the two methdos that takes precedence over the default implementations found in the two base interfaces:

class Cls implements If1, If2 {
  @Override
  public int m3(inti) { /* .. */ }

  @Override
  public int m1(inti) { /* .. */ }

  @Override
  public void m2() { /* .. */ }
}

7.8.5 Static interface attributes

Attributes in interfaces can be exclusively static and are automatically considered final, i.e. they only can define constants.

Due to the multiple inheritance of interfaces it is possible to encounter a conflicts among static attributes defined in different interfaces.

interface If1 {
  static int CONST=1;
  int m1(int i);
}
interface If2 {
    static int CONST=2;
        int m2(int i);
}
class Cls implements If1, If2 {
  @Override
  public int m1(int i) { return -1; }

  @Override
  public int m2(int i) {
1    return CONST ;
  } 
}
1
the field CONST is ambiguous since it matches in both interfaces!

The solution is to disambiguate using the interface name:

class Cls implements If1, If2 {
  @Override
  public int m1(int i) { return -1; }

  @Override
  public int m2(int i) {
    return If2.CONST ;
  } 
}

7.8.6 Functional interface

An interface containing only one regular method – i.e. excluding default and static methods – is called a functional interface.

In a functional interface the method’s semantics is purely functional, that is the result of the method is based solely on the arguments.

Functional interfaces can be used to perform behavioral parameterization.

A set of predefined functional interfaces are declared in the package java.util.function. There are specific variation for different primitive types as well as the generics version (see Generics).

The functional interfaces that work with Object are:

  • Consumer
    • void accept(Object value)
  • Supplier
    • Object get()
  • Predicate
    • boolean test(Object value)
  • Function
    • Object apply(Object value)
  • BiFunction
    • Object apply(Object l, Object r)

Note that the above versions of the methods in the interfaces are simplified versions. The actual ones use Generics

There are also primitive-specific functions, the int versions are:

  • IntFunction
    • Object apply(int value)
  • IntConsumer
    • void accept(int value)
  • IntPredicate
    • boolean test(int value)
  • IntSupplier
    • int getAsInt()
  • IntBinaryOperator
    • int applyAsInt(int left, int right)

7.9 Interface Usage Patterns

Interces serve several different purpose in Java programs:

  • Allows alternative implementations
    • Define a common “interface
  • Provide a common behavior
    • Define method(s) to be called by algorithms
  • Enable behavioral parameterization
    • Encapsulate behavior in an object parameter
  • Enable communication decoupling
    • Define a set of callback method(s)
  • Allow class flagging

7.9.1 Alternative implementations

  • Context
    • The same module can be implemented in different ways by distinct classes with varying:
      • Storage type or strategy
      • Processing
  • Problem
    • The classes should be interchangeable
  • Solution
    • An interface defines methods with a well-defined semantics and functional specification
    • Distinct classes can implement it

Example: Complex numbers

public interface Complex {
  double real();
  double im();
  double mod();
  double arg();
}

Can be implemented using, e.g., either cartesian or polar coordinates

class ComplexRect implements Complex   {
  private double   im  , re;
  public ComplexRect(double re, double im) {
    this.im = im; this.re = re; }

  @Override public doublereal() { return re; }

  @Override public doubleim() { return im ; }

  @Override public doublearg() { return Math.atan2(im, re); }

  @Override public doublemod() { return Math.sqrt  (re * re + im * im  ); }
}
class ComplexPolar implements Complex {
    private double mod, arg;
    public ComplexPolar  (double   mod  , double   arg  ) {
                   this.mod = mod; this.arg = arg; }

  @Override public doublereal() { return mod * cos(arg); }

  @Override public doubleim() { return mod * sin(arg); }

  @Override public doublemod() { return mod; }

  @Override public doublearg() { return arg; }
}

Sample usage

Complex c1 = new ComplexRect(4,3);

System.out.println  (c1 +
" ->   Module   " + c1.mod() + 
"    argument  : " + c1.arg());

Complex c2 = newComplexPolar(5,0.6435);
System.out.println  (c2 + 
" -> Real " + c1.real() + 
"    Imaginary  : " + c1.im());

Default methods can be useful to implements operation that are independent of the specific implmentation, example:

public interface Complex {
  //...
  default boolean isReal(){
        return im()==0;
  }
}

Interfaces can become the façade for alternative implementations. If alternatives are known in advance, it is possible to define a few static methods that can serve as factory methods. In this case the concrete implementations of the interface can even remain hidden withing a package.

public interface Complex {
  // ...
  static Complex fromRect(double re, double   im  ){
      return new ComplexRect(  re,im  );
  }
  static Complex fromPolar(double mod, double arg){
      return new ComplexPolar( mod,arg );
  }
}

Sample usage:

Complex c1 = Complex.fromRect(4,3);

System.out.println  (c1 +
" ->   Module   " + c1.mod() + 
"    argument  : " + c1.arg());

Complex c2 = Complex.fromPolar(5,0.6435);
System.out.println  (c2 + 
" -> Real " + c1.real() + 
"    Imaginary  : " + c1.im());

7.9.2 Common behavior

  • Context
    • An algorithm requires its data to provide a predefined set of common operations
  • Problem
    • The algorithm must work on diverse classes
  • Solution
    • An interface defines the required methods
    • Classes implement the interface and provide methods that are used by the algorithm

7.9.2.1 Common behavior: sorting

An meaingful example of common behavior is represented by sorting algorithms. Class java.utils.Arrays provides the static method sort() that is able to sort the element if an array.

Sorting primitive types can be carried on by specialized version of the sorting algorithm:

int[] v = {7, 2, 5, 1, 8, 5};
Arrays.sort(v);

When sorting object arrays, the algorithm requires a means to compare two objects. This can be achived through an interface providing a method for that purpose.

This is goal of the standard interface java.lang.Comparable. It is defined (roughly) as:

public interface Comparable{ 
    int compareTo(Object obj);
}

: Simplified Comparable interface. {#lst-comparable-simplified}

The method compareTo returns:

  • <0 if this precedes obj
  • =0 if this equals obj
  • >0 if this follows obj

Note: the above definition is a simplified version, the actual declaration uses generics.

Example of Comparable implementation

public class Student implements Comparable{
  int id;

  Student(int id){ this.id =id; }

  @Override
  public int compareTo(Object o){
    Student other = (Student)o;
    return this.id - other.id;
  }
}

7.9.2.2 Common behavior: iteration

A second example of common behavior found in the standard Java libraries is the iteration on container objects.

The solution is based on the Iterator Pattern.

  • Context
    • A collection of objects must be iterated
  • Problem
    • Multiple concurrent iterations are possible
    • The internal storage must not be exposed
  • Solution
    • Provide an iterator object, attached to the collection, that can be advanced independently

It is more complex since includes two distinct interfaces:

The java.lang.Iterable interface

java{#lst-iterable-simplified .java lst-cap="Simplified version of Iterable."}public interfaceIterator{ public interface Iterable{ Iterator iterator(); }

The method iterator() returns a object whose class must implement the Iterator interface.

The java.util.Iterator interface is defined as:

java{#lst-iterator-simplified .java lst-cap="Simplified version of Iterator."}public interfaceIterator{ boolean hasNext(); Object next(); }

The semantics of the Iterator is as follows:

  • Initially the iterator is positioned before the first element
  • hasNext() checks whether a next element is present
  • next() returns the next element and advances by one position

Note: the above definition is a simplified version, the actual declaration uses generics.

The combination of the two interfaces allow writing a loop on the elements of a container:

Iterator it = seq.iterator()
while( it.hasNext() ){
  Object element = it.next();
  System.out.println(element);
}

In fact, any class implementing Iterable can be the target of a for-each construct, which uses the methods in the Iterator interface interface to iterate over the elements. Thus the loop above can be rewritten into an equivalent using the for-each construc:

for(Object element : seq  ){
  System.out.println(element);
}

The following class implements Iterable to allow interating on a sequence of random values.

Listing 7.3: Iterable class RandomSeq
class RandomSeq implements Iterable {
  private double[] values;
  public RandomSeq(int n){
    values = new double[n];
    for(int i=0; i<n; ++i)
      values[i] = Math.random();
  }

  public Iterator iterator() {
    return new Iterator(){
      private int next=0;

      public boolean hasNext() { 
              return next < values.length; }

      public Object next() { 
              return Double.valueOf( values[next++]);}
    };
  }
}

The above class can be used to iterate on the values with a for-each statement:

RandomSeq seq = new RandomSeq(10);
for(Object e : seq){
    double v = ((Double)e).doubleValue();
    System.out.println(v);
}

7.9.3 Behavioral parameterization

  • Context
    • A generic algorithm is fully defined but a few given core operations that vary often
  • Problem
    • Multiple implementations with significant code repetitions
    • Complex conditional structures
  • Solution
    • The operations are defined in interfaces
    • Objects implementing the interface are used to parameterize the algorithm

A behavioral parameters is an object that is intended to provide a behavior that can be plugged into an existing algorithm. Usually it has no state but only operations. Most often the behavioral parameter implements a functional interface.

7.9.3.1 Strategy Pattern

  • Context
    • Many classes or algorithm has a stable core and several behavioural variations
      • The detailed operation performed may vary
  • Problem
    • Several different implementations for the variations are needed
    • Usage of multiple conditional constructs would tangle up the code
  • Solution
    • Embed each variation inside a strategy object passed as a parameter to the algorithm
    • The strategy object’s class implements an interface providing the operations required by the algorithm

For instance, a behavioral parameter can implements the standard functional interface java.util.function.Consumer

Listing 7.4: Simplified version of Consumer.
public interface Consumer{
  void accept(Object o);
}

It can be used to accept and process objects from an an array.

static void forEach(Object[] v, Consumer c){
  for(Object o : v){
    c.accept  (o);
  }
}
String[] v = {"A", "B", "C", "D"};  
Consumer printer = new Consumer(){
  @Override
  public void accept(Object o){
    System.out.println(o);
  }
};
forEach(v, printer);

7.9.3.2 Comparator

The limitation of the sorting solution that uses the common behavior approach with the interface Comparable is that the method defines one specific criterion for ordering.

The solution is to used an approach based on the Strategy pattern, implemented by an overload of the sort() method that leverages the java.util.Comparator interface.

Listing 7.5: Simplified version of Comparator.
public interface Comparator{
  int compare(Object a, Object b);
}

The semantics of the compare() method is similar to that of the compareTo() method, it returns:

  • a negative integer if a precedes b
  • 0, if a equals b
  • a positive integer if a succeeds b

Note: the above definition is a simplified version, the actual declaration uses generics.

public class StudentCmp implements Comparator{
  public int compare(Object a, Object b){
    Student sa = (Student)a;
    Student sb = (Student)b; 
    return a.id – b.id  ;
  }
}

The comparator can be used in the following way

Student[] sv = {new Student(11), 
                           new Student(3),
                           new Student(7)}; 

Arrays.sort(sv, new Comparator(){
  public int compare(Object a, Object b){
    Student sa = (Student)a;
    Student sb = (Student)b; 
    return a.id - b.id;
    }
});

7.9.4 Communication decoupling

When dealing with complex object-oriented designs, separating senders and receivers of messages is important to:

  • Reduce code coupling
  • Improve reusability
  • Enforce layering and structure

7.9.4.1 Observer - Observable

Observer Pattern

  • Context:
    • The change in one object may trigger operations in one or more other objects
  • Problem
    • High coupling
    • Number and type of objects to be notified may not be known in advance
  • Solution
    • Define a base Subject class that provides methods to
      • Manage observers registrations
      • Send notifications
    • Define a standard Observer interface with a method that receives the notifications

The pair of the Observer interface and Observable class allow a standardized interaction between an objects that needs to notify one or more other objects. They are both defined in package java.util.

  • Class Observable manages:
    • registration of interested observers by means of method addObserver()
    • sending the notification of the status change to the observer(s) together with additional information concerning the status (event object).
  • Interface Observer allows:
    • Receiving standardized notification of the observer change of state through method update() that accepts two arguments:
      • Observable object that originated the notification
      • Additional information (the event object)

Sending a notification from an observable element involves two steps: * record the fact the the status of the observable has changed, by means of method setChanged() , * send the actual notification and provide additional information (the event object), by means of method notifyObservers()

Warning: Observer-Observable have been deprecated in Java 9 because they exhibit a few limitations.

7.9.5 Flagging interface idiom

  • Context:
    • A set of classes is treated similarly but a subset must be treated differently
  • Problem:
    • Different objects must be identified at run-time
    • Adding a flag attribute would impact all classes
  • Solution:
    • Let different classes implement an emptyflagginginterface
    • Check at run-time using instanceof

7.9.5.1 Interface Cloneable

In Java implementing Cloneable flags as safe making a field-for-field copy of instances. The Object.clone() method, if the class is flagged, makes a field-for-field copy of the object. Otherwise it throws a CloneNotSupportedException error.

By convention, classes that implement this interface should override Object.clone(). The override should get a copy using super.clone() and possibly modify fields on the clone object before returning it.

7.10 Lambda Functions and Methods References

The definition of anonymous inner classes is very common when using functional interfaces in Java idiomatic ways.

Let us conside, for instance the Consumer interface reported in Listing 7.4. The code required to instantiate an anonymous implementation is the following:

1Consumer printer =
2new Consumer(){
3  public void accept(Object o){
4    System.out.println(o);
  }
};
1
a declaration of the Consumer reference
2
the new statement with the same interface name as in the declaration
3
the functional method declaration (same as in the original interface)
4
the method body, i.e. actual implementation of the method.

The only fragments of code really useful are the declaration and the method body, all the rest is just boilerplate code. The boilerplate code has several drawbacks:

  • requires time to be written,
  • can contain mistakes,
  • makes the code harder to read.

To reduce the boilerplate code to implement functional interfaces since Java 8, two new constructs have been introduce.

The lambda expressions that let the previous code be reduced to:

Consumer printer =
    o -> System.out.println(o);

The method references that let the previous code be reduced to:

Consumer printer =
    System.out::println;

7.10.1 Lambda expressions

A lambda expression is equivalent to the definition of anonymous inner class for functional interfaces. The Lambda expression syntax

parameters -> body

Where:

  • the parameters can be

    • None: ()
    • One: e.g., x or (x)
    • Two or more: (x, y) must be enclosed in parentheses

    The parameter types can be omitted, in that case they are inferred from assignee reference type

  • the body

    • Expression: e.g. x + y
    • Code Block: e.g. { return x + y; }

The parameter types are usually omitted. Compiler can infer the correct type from the context, specifically the assignee reference type. They match the parameter types of the the functional interface only method

As an example, it is possible to write a comparator with a lambda expression:

Arrays.sort( sv, 
  (a,b)-> ((Student)a).id - ((Student)b).id
);

Vs.

Arrays.sort(sv, new Comparator(){
  public int compare(Object a, Object b){
      return ((Student)a).id - ((Student)b).id; 
}});

7.10.2 Method reference

A method reference is a compact representation of functional interface that relays the invocation to single method.

Method reference syntax

Container :: methodName

There are several different types of method references based on the type of the container and the type of method

Kind Example
Static method Class::staticMethodName
Instance method of a given object object::instanceMethodName
Instance method of an object of a given type Type::methodName
Constructor Class::new

7.10.2.1 Static method reference

Like a C function, the parameters of the functional method are mapped to the arguments of the static method.

For instance, using the standard DoubleBinaryOperator interface:

interface DoubleBinaryOperator{
  double applyAsDouble(double a, double b);
}

It is possible to define a functiona interface implementation that takes the maximum of two numbers:

DoubleBinaryOperator combine = Math::max;
double d=combine.applyAsDouble(1.0, 3.1);

The alternative implementation as a lambda expression is:

DoubleBinaryOperator combine = (a,b) -> Math.max(a,b)
double d=combine.applyAsDouble(1.0, 3.1);

7.10.2.2 Instance method of object reference

The functional method arguments are mapped to the arguments of the target method of the object.

For instance using the following functional interface:

interface IntToCharFunction{
  char apply(int value);
}

We can define a functional interface implementation that convers a decimal digit to an hexadecimal digit:

String hexDigits = "0123456789ABCDEF";
IntToCharFunction hex = hexDigits::charAt;
System.out.println  ("Hex for 10 : " +  hex.apply(10));

The alternative implementation as a lambda expression is:

String hexDigits = "0123456789ABCDEF";
IntToCharFunction hex = ch -> hexDigits.charAt(ch);
System.out.println  ("Hex for 10 : " +  hex.apply(10));

7.10.2.3 Instance method reference

The functional method arguments are mapped as follows:

  • the first argument is mapped to the target object on which the method is invoked
  • the possible remaining arguments are mapped to the target method arguments

For instance using the following functional interface:

interface StringToIntFunction {
    int apply(String s);
}

We can define a functional interface implementation that returns the length of a string:

StringToIntFunction len = String::length;
for(String s : words){
    System.out.println(len.apply(s));
}

The equivalent using lambda expression would be:

StringToIntFunction len = s -> s.length();
for(String s : words){
    System.out.println(len.apply(s));
}

7.10.2.4 Constructor reference

The functional method arguments are mapped to the arguments of the target class constructor. The return is a new object of the given type.

For instance using the following functional interface

interface IntegerBuilder {
        Integer build(int value);  
}

It is possible to create a functional implementation that converts an int to an Integer

IntegerBuilder builder = Integer::new;
Integer i = builder.build(1);

The equivalent code using a lambda expression is

IntegerBuilder builder = i -> new Integer(i);
Integer i = builder.build(1);

7.10.3 Lambda Implementation

Lambda expression are implemented through method references. For each expression, a new method named lambda$# is created, the body contains the lambda code. The method is added to the declaring class. The compiler places a reference to that method in place of the lambda.

Lambda vs. Method reference

A method reference allows direct call.

For insance the followig code calls directly the method()

Runnable methRef = Class::method;
methRef.run();

and is exactly equivalente to a direct call:

method();

A lambda expression makes an indirect call, through the fictious method that is generated by the compiler:

Runnable lambda = ()->method();
lambda.run()

Calls the method lambda$0() that is defined as:

void lamda$0(){ method(); }

Which, in turn, calls the target method(). In this case if we look at the call stack we can observe:

  • the caller method
  • the lambda expression method
  • the callee method

In general method references are preferrable because they are more efficient.

7.11 Practical Design Reflections

7.11.1 Inheritance vs. composition

  • Reuse can be achieved via: *Inheritance
    • The reusing class inherits reused members that are available as own members
    • Clients can invoke directly inherited methods *Composition
    • The reusing class has the reused methods available in an included object (attribute)
    • Clients invoke new methods that delegate requests to the included object

(img/J03-JavaInheritance20.wmf)

(img/J03-JavaInheritance21.wmf)

Observer w/Inheritance

(img/J03-JavaInheritance22.wmf)

Observer w/Composition

(img/J03-JavaInheritance23.wmf)

Observer subject w/inheritance

public class Subject extends Observable{
  String  prop="ini";
  public void setProp(String val){
    setChanged();
    property =val;
    notifyObservers("theProp");
  }
}

Observer subject w/composition

public class Subject {

  PropertyChangeSupportpcs = newPropertyChangeSupport(this);
  String  prop="ini";

  public voidsetProp(String val) {
    String old = prop;
    property =val;
    pcs.firePropertyChange("theProp",old,val);
  }

  // delegation:

  public void addObs(PropertyChangeListener l){
    pcs.addPropertyChangeListener("theProp",l);
  }
}

Observer with inheritance

public class Concerned implementsObserver{
  @Override
  public void update(Observable src, Object arg) {
    System.out.println("Variation of " + arg);
    }
}

Observer with composition

public class Concerned implements PropertyChangeListener{
  @Override
  public voidpropertyChange( PropertyChangeEvent evt) {
    System.out.println("Variation of " + evt.getPropertyName());
  }
}

7.11.2 Algorithm variability

  • Common behavior idiom
    • The variability is bound to the type of objects processed by the algorithm
    • Behavior is implicit in the data classes
    • Less flexibility
  • Strategy pattern
    • The variability is implemented through behavioral objects (strategies)
    • Behavior is explicitly defined upon invocation
    • More flexibility

Sorting flexibility

class Student implements Comparable { //…
  public int compareTo(Object s){
    return id - ((Student)s).id;
  }
}
Arrays.sort(students); // <- implicit

Comparable provides an explicit strategy

// explicit strategy
Arrays.sort(students,(a,b)->{ // ascending
   return ((Student)a).id - ((Student)b).id;
});

Arrays.sort(students,(a,b)->{ // descending
   return ((Student)b).id - ((Student)a).id;
});

7.12 Wrap-up

  • Inheritance
    • Objects defined as sub-types of already existing objects. They share the parent data/methods without having to re-implement
  • Specialization
    • Child class augments parent (e.g. adds an attribute/method)
  • Overriding
    • Child class redefines parent method
  • Implementation/reification
    • Child class provides the actual behaviour of a parent method
  • Polymorphism
    • The same message can produce different behavior depending on the actual type of the receiver objects (late binding of message/method)
  • Interfaces provide a mechanism for
    • Constraining alternative implementations
    • Defining a common behavior
    • Behavioral parameterization
  • Functional interfaces and lambda simplify the syntax for behavioral parameterization