Object-oriented Programming (OOP)

Principles

Object vs. Value

Each type has either object or value semantics. This decision must be taken explicitly.

Value

A value describes abstract entities. Its identity is implicitly defined (e.g. 10, "B"). It has no lifecycle and an immutable state. The state can only be interpreted. Sharing causes no side effects and is thread-safe. Examples: integer, float, character, monetary value, account number, color, date

Object

An object models entities from the real world (i.e. phenomena in a problem domain / context). They can be distinctively referenced and shared (-> may lead to side effects). They have a lifecycle (creation, mutation, deletion). Examples: account, payment, person

Immutable Class in Java

An immutable class cannot have mutators (i.e. setters). The state can only be retrieved but not changed. Alternatively, copy-on-write can be used: All mutating operations create a copy of the object the mutation is applied to and return the copy.

  • Class must not be extensible: use public final class
  • All fields must be final to prohibit changes after initialization
  • All fields must be private
  • References to mutable objects are not available to other clients
  • If client provides objects, a copy must be made

The Java Record class can be used: public record Point (int x, int y) {}

Types and Classes

A type is an abstract description of properties of a set of objects. A class is the implementation of a type. (Object -> Class -> Interface -> Type). There are two different ways to perform type checking:

Static Type Checking (Explicit Typing)

The class / interface is a type. Thus is is an abstract description of the properties of an object. Variables can only be assigned along the class / interface hierarchy. Upcasting and downcasting is possible. In general, explicit typing improves readability, reliability and efficiency.

Dynamic Type Checking (Implicit Typing)

The type is implicitly defined by the protocol (i.e., set of messages understood by the object). It is more flexible than static type checking because the assignment compatibility is not restricted by type hierarchy. However, implicit typing reduces readability, reliability and efficiency. As an alternative to classes, prototypes and delegation can be used.

Subtype and Subclass

It is important to distinguish Subtyping and Subclassing.

Subtype (Is-a-Relationship)
T' is a subtype of T if T-objects can be replaced by T'-objects.
Subclass (Has-a-Relationship)
T' is a subclass of T if T' is derived from T but T-objects cannot be replaced by T'-objects.


Subtyping (Interface Inheritance)

Subtypes must fulfill the Principle of Substitutability (Liskov Substitution Principle). For this to hold, the subtype must provide at least the interface of its base type. It cannot be more restrictive than the base type. Furthermore, the preconditions must not be narrowed (i.e., requiring something more specific) and the postconditions must not be widened (i.e., returning something more generic). However, it can accept something more general and return something more specific. Additionally, subtypes must not throw new exceptions, unless the exception is a subtype of the exception thrown by the base type.

Subclassing (Implementation Inheritance)

There is little compliance between the base class and the subclass. The base class is just a minor detail of the subclass (mainly used for code-sharing purposes). Basically: I want to use the code of Class X, therefore I derive my Class Y from it but don't satisfy the contract. How to fix it: Use composition instead.

Java Naming Conventions

  • Classes: Nouns, first letter of each word is capitalized: ShadowBox
  • Interfaces: Start with I, first letter of each internal word is capitalized: IMouseListener
  • Methods: First letter lowercase, first letter of each internal word is capitalized: setCoordinate
  • Constants: All uppercase, words separated by "_": MAX_ITEMS

Type Checking

Static Type Checking

Static type checking detects type errors at compile time. It enables the generation of efficient target code (e.g., byte code). However, type errors can still occur at run-time (e.g., by type casting). Dynamic type checking provides flexibility at the expense of readability, reliability, and efficiency. The developer is invoking valid methods. Important: Dynamic type checking does not mean weak type checking. Because types are still checked at run-time whereas in weak type checking, the types are not checked anymore.

Static Type checking vs. Dynamic Binding
Type checking ensures that a message is understood by an object. Dynamic binding selects the appropriate method based on the dynamic type of the receiver.
Static Type checking vs. Inheritance
An object of Type T' can be assigned to a variable of static type T if T' is derived form T. The assignment compatibility is restricted by type (inheritance) hierarchy. Dynamic binding is only possible along the type hierarchy.

Type inference allows deriving the type of a variable from the context and thus enables static type checking. However, it might affect the readability.

Dynamic Type Checking

Dynamic type checking (dynamic typing) allows for dynamic bindings outside of the type hierarchy. The criterion to evaluate assignments is: Does an object understand a message? i.e. Looks like a duck, walks like a duck, quacks like a duck, thus it is a duck.

Covariance and Contravariance

Covariance

The type of a method parameter is narrowed in overriding method. The principle of substitutability is applied to method arguments. It is useful, but expensive to guarantee type safety at compile time. Java (and most other oo languages) do not support covariance.

Contravariance

The type of a method parameter is widened in overriding method. Contravariant method parameter does not override in Java (just overloading). Contravariant return type is not allowed in Java.

Types of Inheritance

  • Specialization: Derive concrete subclass form concrete base class
  • Specification: Concrete subclass implements abstract base class
  • Restriction: Subclass restricts the base class interface (DON'T!)
  • Combination: Multiple Inheritance
  • Construction: Implementation inheritance (DON'T)

Multiple Inheritance

Multiple inheritance leads to a more complex language (syntax and semantics), more complex class hierarchies. They are often used instead of composition. However, they tend to mess up the class hierarchy (postponing clean redesign). Additionally, they can lead to naming conflicts (IF C inherits from A and B and both have the same method m())

Mixin Classes

Mixin classes are designed for adding behavior. They do not define any state. They may contain both abstract and concrete methods. Java 8 provides default methods in interfaces, thus they can be used to add behavior.

Design for Inheritance

By default inheritance is possible for each class. However, this violates encapsulation if the base class is not designed for it.

Guidelines for Designing for Inheritance

  • Members (fields) should be private by default
  • Constructors must not invoke overridable methods
  • Prohibit subclassing if class cannot be safely subclassed (e.g. final keyword in Java)

Java Interfaces

An interface is a "class" which declares only abstract method. It defines a type -> most abstract type specification. State is not allowed (except constants). Since Java 8 default implementations can be provided. However, implementation of default methods cannot be factorized (Everything within the interface is public, if I want to factorize something, I don't want it to be public.)

A class can inherit from 0-1 classes and implement 0-n interfaces:

public interface Scalable {
  void drawScaled(Graphics g, int scale);
}
public class ScalableBox extends Box implements Scalable {
  void drawScaled(Graphics g, int scale) { ... };  
}
public class ScaledView extends View {
  void addScaledShape(Scalable scalable) { ... };
}
Important: Use interfaces for static type declarations whenever possible!

Functional Interfaces

Interface which contains exactly one abstract method (and 0..n default methods). It specifies type of a lambda expression. It may be annotated with @FunctionalInterface.

@FunctionalInterface
interface Converter<F, T> {
  T convert (F from);
}
...
Converter<String, Integer> stringToInt = (from) -> Integer.valueOf(from);
Converter<String, Float) stringToFloat = Float::valueOf
Integer i = stringToInt.convert("4711");
Float f = stringToFloat.convert("4711.3412");

Tagging (or marker) Interface

Interface used for marking a class to be eligible for being processed by some other class. It consists of an empty interface (no method declarations).

Constant Interface

They are used for namespace convenience. However, when used an implementation detail becomes a part of the class' exported API. Since Java 5 static import should be used.

Groovy Traits

Traits may define state and behavior. They allow lightweight multiple inheritance ("interfaces on steroid"). They can be used for behavior composition or run-time implementation of interfaces. Naming conflict resolution (diamond problem) is handled such that the last trait wins and the state is shared if inherited more than once.

class Person {
  String firstName = "Jonathan"String lastName = "Pine"
  String name()return "$firstName $lastName" }
}
trait FirstName { 
  String firstName() { 
    return firstName 
  } 
}
FirstName fn = new Person() as FirstName
assert fn.firstName() == "Jonathan"

Abstract Classes

Abstract classes are used to define common structural and functional properties. Compared to interfaces they can define additional state. They are designed for subtyping. Abstract classes do not allow the creation of instances but they can define constructors.

Abstract enable factorization: Common properties of subclasses can be defined in the abstract class. Inheritance can then be used to adapt / implement these properties. A system and its interfaces can be defined using abstract classes / interfaces on which the implementation can be based.

They facilitate the implementation of concrete classes. Several concrete methods can be based on a few abstract methods (e.g. comparison of objects, all methods can be based on "equals" and "greater than"). The more abstract methods an abstract class has, the less attractive it is for inheritance. Thus good abstract classes contain a lot of concrete methods and only a small amount of abstract methods.

Abstract Classes and Methods in Java

An abstract class is explicitly defined as abstract. It can contain 0..n abstract methods and instance / class variables may be provided. Abstract methods only define the signature. The implementation is left to derived classes. Abstract methods can only be used on abstract classes.

public abstract class Shape { // <-- abstract classprivate Rectangle coordinates;
  public abstract void draw(Graphics g); // <-- abstract methodpublic void setCoordinates(Rectangle r) { coordinates = r; }
  public Rectangle getCoordinates() { return coordinates; }
}

Abstract Classes and Interfaces

In general interfaces should be as small as possible. Methods of an interface are abstract (except default methods). Implementing class must thus implement all non-default methods of its interfaces. Small interfaces ease implementability and substitutability and encourage reuse.

Abstract classes and interfaces can be combined. An abstract class "implements" an interface. The methods are not abstract but their implementation is empty (in the interface). A client may then either implement the interface (and is forced to implement all methods) or inherit from abstract class (override only required (abstract) methods).

Since Java 8 the combination interface / abstract class has been mostly superseded by interfaces with default methods.

public interface MouseListener extends EventListener {
  void mouseClicked(MouseEvent e);
  void mouseEntered(MouseEvent e);
  void mouseExited(MouseEvent e);
  void mousePressed(MouseEvent e);
  void mouseReleased(MouseEvent e);
}

If a client implements this interface, then all methods must be implemented. However, in most cases the client only cares about a subset of these methods. By providing an abstract class that implements the interface and inheriting from that class the client can only implement the methods needed:

public abstract class MouseAdapter implements MouseListener {
  public void mouseClicked(MouseEvent e) {};
  public void mouseEntered(MouseEvent e) {};
  public void mouseExited(MouseEvent e) {};
  public void mousePressed(MouseEvent e) {};
  public void mouseReleased(MouseEvent e) {};
}
public class CustomMouseAdapter extends MouseAdapter {
  public void mouseClicked(MouseEvent e) {
    Logger.log("Mouse Clicked");
  }
}

Adding abstract methods break the client (because all clients need to implement it). This is similar to adding a method to an interface without a default implementation. This can be a major roadblock for evolving libraries.

It is generally recommended to develop against interfaces / abstract classes. The interfaces protect the class from changes since they have no implementation (stay consistent over time). Variables and parameters should be declared using interfaces / abstract classes.

A key concept for reusable design (e.g. in frameworks) is the abstract coupling between classes. Instance variables are typed by interface (preferred) or abstract class. It then refers to a concrete instance at run-time (implementation of derived class).

Information Hiding

The goal is to abstract from the implementation to minimize coupling between classes (i.e., i do not want to care about the implementation when building a client; I can replace it if necessary). To achieve this restricted access rights are required. Java distinguishes between clients and derived classes with public vs. protected. Classes within a subsystem can be restricted with public vs. package private.

Accessors should be used both by clients and derived classes. They can be used to hide the implementation (representation) of state. They minimize coupling and allow replacement of state implementation. However, they are more verbose and inefficient.

Inheritance violates information hiding because proper overriding requires knowledge about implementation details of base class.

public MyList {
  public void add(Object e) { ... }
  public void addAll(Object[] a) { ... }
}
public class InstrumentedList extends MyList {
  private int addCounter = 0;
  public int getAddCounter() { return addCounter; }
  
  @Overridepublic void add(Object e) { addCounter++; super.add(e); }
  
  @Override
  public void addAll(Object[] a) { addCounter += a.length; super.addAll(e) }
}
InstrumentedList v = new InstrumentedList();
v.addAll(new String[]{"a","b","c"});

// assert fails if MyList::addAll uses MyList::add
assert v.getAddCounter() == 3;


Abstract State

Another problem is that the state implementation cannot be changed by derived classes. An abstract state can be defined by specifying abstract accessors in the abstract base class. The implementation of the state can then be done in the derived class (incl. concrete accessors).

public abstract class AbstractButton {
  public abstract String getLabel();
  public abstract void setLabel(String label);
  // ...
}
public class Button extends AbstractButton {
  private String label;
  public String getLabel() { return label; }
  public void setLabel(String label) { this.label = label; }
}

Adaptability vs. Unforeseen Changes

In general non-generic (simple) solutions favour time and money at the expense of adaptability. In contrast, generic (complex) solutions favour adaptability at the expense of time and money. The challenge is to find the sweet spot between these two extremes.

public interface IShape {
  float area();
}
public class Rectangle implements IShape {
  private Point origin;
  private Point corner;
  public Point getOrigin() { ... }
  public Point getCorner() { ... }
  public float area() { ... }
}
public class Circle implements IShape {
  private Point center;
  private int radius;
  public Point getCenter() { ... }
  public int getRadius() { ... }
  public float area() { ... }
}

What do we do if there is a new requirement: Clients need center of Shape?

Option A: Extend Interface and implementing classes

Only possible if interfaces / classes may be changed. However, libraries usually do not allow for changes.

public interface IShape {
  float area();
  Point center();
}


Option B: Add extension outside of interface / class

Open up the abstraction and deal with concrete implementations instead. This does not cover new interface implementations (e.g. Triangle). However, an Exception could be thrown if new implementations are provided:

public static Point center(IShape s) {
  if (s instanceof Circle) {
    return ((Circle)s).getCenter();
  } else if (s instanceof Rectangle) {
    return // compute center based on origin and corner
  } else {
    throw new IllegalArgumentException();
  }
}


Option C: Seal interface

The interface can be sealed to prevent new implementations of IShape. Thus, clients can safely deal with existing implementations:

public sealed interface class IShape permits Circle, Rectangle {
  float area();
}
public static Point center(IShape s) {
  return switch(s) {
    case Circle c -> c.getCenter();
    case Rectangle r -> // compute center based on origin and corner
  }
}

Generic Classes

Generics allow to abstract over types. Thus they can be used whenever an implementation only differs by type. A class specifies formal type parameters and the actual type parameters are provided upon instantiation / inheritance. They are a programming language feature and are supported by most statically typed programming languages.

public class ArrayList<E> implements List<E> {
  public void add(E object) {};
  public E get(int index) {};
}
List<Button> buttonList = newArrayList<Button>();
buttonList.add(new Button());
buttonList.add(new Integer()); // -> compile-time error
Button b1 = buttonList.get(0); 
Integer b2 = buttonList.get(1); // -> compile-time error

In Java generic classes are compatible with non-generic classes. If the type parameter is omitted, then "Object" is assumed. However, Java base types (primitives) are not allowed for actual type parameters (e.g. List<int>). Additionally, Java also provides generic methods:

public static <T> boolean contains(T[] array, T object) { ... }

Generic classes increase the reliability (by avoiding downcasts) and readability. Furthermore, autoboxing / -unboxing complements the feature nicely:

// without autoboxing/-unboxing
List<Integer> numbers = new ArrayList<Integer>();
numbers.add(new Integer(2));
int val = numbers.get(0).intValue();
// with autoboxing/-unboxing
numbers.add(2)
int val = numbers.get(1)

However, the increase the complexity of the language (syntax and above all semantics).

Inheritance and Generic Classes

The genericity of type parameters can be restricted to enforce that actual type parameters conform to a specific type:

public class NumberList<T extends Number> {
  public void add(T object) {};
  public T get (int index) {};
}
NumberList<Double> numberList = new NumberList<Double>();
NumberList<String> stringList = new NumberList<String>(); // compile-time error!!

To increase flexibility and reliability the combination of inheritance and generics is desirable even if it leads to more complex syntax and semantics.

When designing the API of a class we want to acheive flexibility, type safety and readability. The API should be expressive and self-documenting and the client should not care about typing challenges.

public void printAll(List<Object> l) {
  for (Object o: l) { System.out.println(o);} // hardly usable
}
public void printAll(List<?> l) {
  for (Object o: l) { System.out.println(o);} // flexible
}


Case Study: API design of a generic class "Stack"

public class Stack<E> {
  public Stack();
  public void push(E e):
  public void E pop();
  public boolean isEmpty();
}
// additional method for stack (inflexible)
public void pushAll(Iterable<E> src) {
  for (E e : src) push(e);
}
// usage
Stack<Number> numberStack = new Stack<Number>();
Iterable<Integer> integers = ...;
numberStack.pushAll(integers); 
// compile time error 
// because Iterable<Integer> is not a subtype of Iterable<Number>

It is not possible to use a more specific type. However, by using the extends keyword, the method can be modified to allow more generic types:

// additional method for stack (flexible and safe)
public void pushAll(Iterable<? extends E> src) {
  for (E e : src) push(e);
}

In a similar fashion we might want to add a method that allows us to pop all elements inside a collection:

// additional method for stack (inflexible)
public void popAll(Collection<E> dst) {
  while(!isEmpty()) dst.add(pop());
}
// usage
Stack<Number> numberStack = new Stack<Number>();
Collection<Object> objects = ...;
numberStack.popAll(objects); // compile-time error
// because we cannot use a more generic version

To allow a more generic type we can use the super keyword:

// additional method for stack (flexible and safe)
public void popAll(Collection<? super E> dst) {
  while (!isEmpty()) dst.add(pop());
}


When to use Extends / Super?

PECS: producer extends, consumer super

It helps to think about whether a parameter is produced or consumed to figure out which keyword to use. A producer is allowed to produce something more specific, hence extends, a consumer is allowed to accept something more general, hence super. In popAll() we are consuming (using, writing) the Collection but we're not reading it. Thus, we can also accept something more generic. In pushAll we are producing (reading) but not consuming it. Thus, we can also accept something more specific. If we're both consuming and producing then no keyword can be used. If we are neither producing nor consuming then the ? type parameter can be used.