Skip to content

Basic OO programming concepts

Previously we've covered the historic journey from primitive or low-level programming languages, to advanced languages, notably java. We've briefly seen the motivation for information hiding, and why encapsulation and type safety are powerful concepts to render our implementations more robust. In this lecture we take a dive into the basic concepts of object-oriented programming, specifically into more details of the encapsulation philosophy and how inheritance interplays with type safety.

Lecture upshot

Encapsulation and inheritance both form the fabric for separation of concerns. By hiding implementation details within a class we can contain complexity. Inheritance allows restructuring of our code to reuse existing class properties (fields and functions) without code duplication.

Encapsulation and information hiding

In the last session we've briefly seen the relevance of keeping a class secret.

To play the devil's advocate, why would we not just make all fields public?

  • A lot less headache thinking about public / private.
  • Everything still works, we can access our data directly.
  • Our code is even shorter, we could argue this is more elegant than rows of boilerplate getters and setters.
Where's the issue ?

You cannot rely on function users that they correctly handle object internals. Most likely they have no bad intentions but simply are not familiar with the class internals. Code defensively, secure your object internals.

Example

A simple example are classes with basic restrictions and value ranges.

If we were to implement a classic dice, we would expect its value to always we in range 1-6.

However, if we have just public fields, there is absolutely no guarantee this constraint is respected.

public class Dice {

  public int value = 1;

  public Dice() {
  }
}

However, if we make the field private, we can implement basic sanity check into our setter:

public class Dice {

  private int value = 1;

  public Dice() {
  }

  public int getValue() {
    return value;
  }

  public void setValue(int value) {
    if (value > 6 || value < 1) {
      throw new RuntimeException("Value not allowed!");
    }
    this.value = value;
  }
}

Concealing complexity

Another motivation for restricting access to private information is to conceal private class information, i.e. implementation details.

  • If you read the time from your alarm clock, you just want the time - you're not interested in how the clock internally works.
  • The same holds for java classes.

Protecting deep references

Implementing getters the right way is sometimes less trivial than one might think.

  • Getters for primitive values are usually safe, no-one can tamper with the value stored in our class, using the getter.
  • Getters for objects however, are a common cause for class secret leaks. Here's an example:
public class University {

  // The list is private and only accessible via a getter so we might think this is all safe
  private List<Student> students;

  // However we can abuse the getter, because it returns a reference, not an object!
  public List<Student> getStudents() {
    return students;
  }
}

Illustration of objects in RAM versus variables in RAM:

There are a few ways to overcome the issue:

  • If we are the author of the returned class, we can make sure there's a read-only variant, using an interface or common base class (a bit more on that later today).
  • If it's a collection, we can use Collections.immutable... to create an immutable variant.
  • And last but not least, we can simply create a deep copy of the result, before returning a reference.

It has to be a deep copy

Copies alone are not enough, it has to be a deep copy, i.e. every object references within must be copied, too. Carefull with Arrays.clone() it only creates shallow copies.
Creating deep copies can be a bit of a hassle, but there are some tricks to get around (mentioned in class).

Classes, inheritance and polymorphism

Common to most Object-Oriented programming-languages is the concept of hierarchies.

  • Hierarchies of classes
  • Hierarchies of interfaces
  • Classes implementing interfaces
  • etc...

In the remainder we'll walk through they various scenarios and which purpose they serve.

Single inheritance

In the simplest case, one class can inherit properties or behaviour from exactly one other class.

We also call...

  • ... the class providing original properties and behaviour the super-class.
  • ... the class inheriting properties and behaviour the sub-class.

The interest of single inheritance is to define common behaviour in the super-class, and specialized behaviour in the sub-class.

Example and Visualization

All Ducks can swim() and quack():

classDiagram
    class Duck {
        <<Class>>
        +String swim()
        +String quack()
    }

For simplicity, let's assume all methods return a String vocalizing the sound of each method:

  • Ducks swim() implementation returns "splash splash splash"
  • Ducks quack() implementation returns "quack quack quack"
public class Duck {

  public String swim() {
    return "splash splash splash";
  }

  public String quack() {
    return "quack quack quack";
  }
}

Now we're interested in a new kind of Ducks: RedheadDucks

  • RedheadDucks are Ducks, and almost behave identical like the existing Duck definition.
  • RedheadDuck, can swim() and quack(). The existing implementation is perfect.

We do not want to copy paste the existing Duck code, but since RedheadDucks are Ducks, we can just extend the existing implementation:

classDiagram
    class Duck {
        <<Class>>
        +String swim()
        +String quack()
    }

    class RedheadDuck {
        <<Class>>
    }

    Duck <|-- RedheadDuck : extends
  • The arrow with hollow triangle from RedheadDuck to Duck indicates that both methods are inherited.
  • The RedheadDuck class does not copy the method definitions:
class RedheadDuck extends Duck {

  // no methods implemented here
}
  • Nontheless these methods exist - they are inherited from the superclass. So on every RedheadDuck object created, we can call these methods :

    public static void main() {
      RedheadDuck redheadDuck = new RedheadDuck();
    
      // Can be called, and implicitly applies implementation of Duck superclass:
      System.out.println(redheadDuck.swim());
      System.out.println(redheadDuck.quack());
    }
    

Example taken from "Freeman & Freeman, Head First Design Patterns"

Multiple subclasses

The "is-a" relationship, although not commutative, can have parallel alternatives. That is, multiple sub-classes can share the same super-class, without interfering with one-another.

If that's the case, all sub-classes inherit the same methods from the super-class.

Example and Visualization

We can add an additional type of Ducks to our previous example: Mallards

  • Mallards, like RedheadDucks are Ducks and should inherit all common methods.
  • Once more, we do not want to copy paste code from the Duck class, so we just use a second extend relation:
  class Mallard extends Duck {

}
  • With now two types of Ducks, we can visualize the resulting class diagram as:
    classDiagram
        class Duck {
            <<Class>>
            +String swim()
            +String quack()
        }
    
        class RedheadDuck {
            <<Class>>
        }
    
        class Mallard {
            <<Class>>
        }
    
      Duck <|-- RedheadDuck : extends
      Duck <|-- Mallard : extends

Method override

  • So far our two Duck sub-classes inherit the exact behaviour from the Duck super-class.
  • But there is one issue: Mallards are in fact a species with an extremely loud quack... more like a "QUACK !!!".

I'm a loud mallard - QUACK !!!

  • But since our Mallard class extends Duck, it still has the default quack.
  • Luckily we can tweak the default behaviour by overriding specific methods.
  class Mallard extends Duck {

  // placing a method with identical signature overrides the inherited super-behaviour.
  // All other methods, e.g. `swim` remain unaffected.
  @Override
  public String quack() {
    return "QUACK !!!";
  }
}

We can also visualize the fact that a specific method was overridden:

classDiagram
  class Duck {
      <<Class>>
      +String swim()
      +String quack()
  }

  class RedheadDuck {
      <<Class>>
  }

  class Mallard {
      <<Class>>
      >>
      +String quack()
  }

  Duck <|-- RedheadDuck
  Duck <|-- Mallard

In UML, explicitly showing an inherited methods hints an Override implementation.

Polymorphism

Assume we have a little park with an array of 5 Ducks. The park also is supposed to have a method makeDuckConcernt() which lets every duck quack once.

public class Park {

  private Duck[] ducks;

  public String makeDuckConcert();
}
  • Unfortunately we do not know which kind of ducks are in the array.
  • It could be only classic Ducks. It could be Mallards. It could be RedheadDucks... or a mix of all.
  • So how would we implement the makeDuckConcert method, to make sure the right quacks are heard ?

Bad: Type checks

We could iterate over every element in the array, then check what type of duck it is, and then print the right quack.

// primitive version, using type checks
String makeDuckConcert() {

  // iterate over all ducks (whatever they exact subtype)
  for (int i = 0; i < ducks.length; i++) {

    // Check if it is a very loud duck
    if (ducks[i].getClass() == Mallard.class) {
      System.out.println("QUACK !!!");
    }

    // if it's not a Mallard, print the normal quack
    else {
      System.out.println("Quack");
    }
  }
}

Although this solution works, can you spot 2 issues ?

What's the issue with above code

We reimplemented the quacking behaviour, we need concrete knowledge on all possibly existing duck types.

Good: Delegation

We can do way better !

  • Every duck knows itself how to quack - we do not need to look up the type
    • Rather than quacking based on what duck it is, let's just ask every duck to quack.
    • The duck can itself apply the matching behaviour.
// improved version, using polymorphism
String makeDuckConcert() {
  for (int i = 0; i < ducks.length; i++) {
    // Just delegate - let the duck itself decide on how to quack.
    ducks[i].quack();
  }
}

Why does this even work ?

  • All ducks are guaranteed to have an implementation of quack()
  • When we call quack(), this invokes the implementation of the concrete object duck type - we are ignorant to which precisely it is, but the object knows!

The technique is called polymorphism. We play on the fact that there are implementations who each bring their own implementation and just delegate the actual execution down to the individual implementations.

What are the ingredients for a polymorphism soup ?

1) A common method. 2) Overriding implementations. 3) Calling the common method on the super-type.

Polymorphism in other languages

Java has a strong type system, i.e. the compiler only considers types compatible if explicitly declared.

  • In the previous examples, we were able to assign a RedheadDuck instance to a Duck variable.
  • This was only allowed, because we explicitly declared that RedheadDuck extends Duck.

Not all languages apply a strong type system, e.g. JavaScript does not.

  • Polymorphism is still possible, i.e. we can still delegate concrete method behaviour to the object instance.
  • How's that possible ? JavaScript (and other non-strongly typed languages) apply a Duck-Type system

Duck Typing

The informal explanation of Duck typing is: "If it swims like a duck and quacks like a duck it probably is a duck"
More formal: Object compatibility is determined based on the available methods, rather than an explicitly declared hierarchy.
In JavaScript: You create an array of objects (that may be compatible or not), then call the desired method (that may be available or not). You'll only see at runtime if it works out the intended way.

Types, interfaces and abstract classes

Strong type systems

  • Strict object-oriented languages apply a strong type system, that is, the compiler checks at compile time if variable assignments are possible.
    • Strong type systems are a security mechanism to prevent programming errors. If the programmer attempts to store something in an incompatible variable, chances are high the programmer made a mistake. The type-system ensures the error can be fixed before program execution, instead of leading to a program crash, or worse (unpredictable behaviour)
    • Some languages at once allow type declarations, but only for decorative purposes mostly serve a decorative purposes, e.g. type hints in python do not prevent incompatible attributions.
  • For example in Java, it is not possible to assign a String value to an int field:
    // Not allowed, only int values can be assigned.
    int myMagicNumber = "42";
    

Subtypes and compatibility

  • The extends keyword, describing singe inheritance is the verbal equivalent of an "is a" relationship.
  • The keyword allows enlarging type compatibility:
    • If every RedheadDuck is-a Duck, we can store it in a Duck variable:
    • Code example:
        // This is ok, because every RedheadDuck is a Duck
        Duck duck = new RedheadDuck();
      

extends is non-commutative

Class inheritance is not commutative, that means while every object of the sub-class can be assigned to a variable of the super-class, this does not work the other way round!

Non-commutative example :

  • Every RedheadDuck is a Duck
  • But not every Duck is a RedheadDuck !
// This is not ok, because not every Duck is a RedheadDuck
RedheadDuck redheadDuck = new Duck();
What exactly happens if we attempt to store the supertype instance in the subtype variable ?

We will receive a compiler error, because the is a relationship only works the other way around:
error: incompatible types: Duck cannot be converted to RedheadDuck

Abstract classes

  • Previously our super-class (Duck) was a standard java class.
  • It was absolutely possible to instantiate new objects, and call the provided methods, with:
void initializeSuperClass() {
  Duck duck = new Duck();
  System.out.println(duck.swim());
  System.out.println(duck.quack());
}
  • However, if we're fully honest, no animal is ever just a Duck. In real life, every animal is a certain species ( alas sub-class) of Duck. We've seen:
    • Mallard
    • RedheadDuck
  • So how can we prevent the instantiation of pure Ducks, and restrict objects to concrete species (Mallard, RedheadDuck) ?

Abstract classes

The abstract keyword signals that a class cannot be instantiated. It can only serve as super-class for other classes, but there can be no instances of the abstract class.

Abstract class example

For our previous hierarchy, abstract is a good fit:

  • An abstract Duck super-class prevents the instantiation of Duck objects (Mallards and RedheadDucks are not affected).
  • Sub-classes still inherit all implemented methods, i.e. Mallard and RedheadDuck have a default swim() and quack() behaviour.

Code wise, this translates to:

public abstract class Duck {

  public String swim() {
    return "splash splash splash";
  }

  public String quack() {
    return "quack quack quack";
  }
}

The unified modelling language (UML) also marks abstract classes with a corresponding stereotype:

  classDiagram
    class Duck {
        <<Abstract>>
        +String swim()
        +String quack()
    }

    class RedheadDuck {
        <<Class>>
    }

    class Mallard {
        <<Class>>
    }

    Duck <|-- RedheadDuck : extends
    Duck <|-- Mallard : extends

What's the consequence, code wise ?

  • The Duck class can no longer be instantiated.
  • RedheadDucks and Mallards can be instantiated (and assigned to Duck variables) like before.
void initializationExample() {

  // This does not work ! Duck is abstract and cannot be instantiated
  Duck duck = new Duck();

  // But this does work: The sub-classes are not abstract and can be instantiated. The still inherit a method implementation from the super-class:
  Duck mallard = new Mallard();
  Duck redheadDuck = new redheadDuck();
  mallard.quack();
  mallard.swim();
  redheadDuck.quack();
  redheadDuck.swim();
}

Multi-inheritance

  • Inheritance is often not a matter of just one super-class-sub-class pair, but a longer chain.
  • For instance, we can imagine that Ducks, since they are Birds should also have a fly() method.
    • The fly() method should not be implemented in Duck, because other non-Duck birds can fly, too.
classDiagram
    class Bird {
        <<Abstract>>
        +String fly()
    }

    class Duck {
        <<Abstract>>
        +String swim()
        +String quack()
    }

    class RedheadDuck {
        <<Class>>
    }

    class Mallard {
        <<Class>>
        >>
    }

    Bird <|-- Duck : extends
    Duck <|-- RedheadDuck : extends
    Duck <|-- Mallard : extends

Inheritance is transitive

With Duck extending Bird and RedheadDuck+Mallard extending Duck, the fly() method is inherited by all instances of RedheadDuck and Mallard. Inheritance is transitive.

A convenient side effect is that we can now add as many new Duck sub-types as we want, they'll always be able to fly(), swim() and quack().

What must be added to Duck, to inherit from Bird as super-class

extends Bird ! So the full class declaration is now public abstract class Duck extends Bird

The diamond problem

  • So far we've only considered cases, where a class inherits (extends) a single super-class.
  • But we could contemplate and what should happen when a class inherits from multiple super-classes:
classDiagram
    class Vehicle {
        <<Abstract>>
        +String accelerate()
        +String slowDown()
    }

    class Electric {
        <<Abstract>>
        +String accelerate()
        +String chargeBattery()
    }

    class Combustion {
        <<Abstract>>
        +String accelerate()
        +String fuelUp()
    }

    class Hybrid {
        <<Class>>
    }

    Vehicle <|-- Electric : extends
    Vehicle <|-- Combustion : extends
    Electric <|-- Hybrid : extends
    Combustion <|-- Hybrid : extends

Here we can argue that the Electric and Combustion class each provide their own accelerate() method, for they internally use different energy sources to put the vehicle on speed.

Why is it called the diamond problem ?

The resulting class hierarchy, as displayed in the UML diagram shows two alternative extension paths. The issue is that Hybrid now inherits two conflicting accelerate() implementations. Which one should win? There's no right answer, therefore some languages e.g. Java forbid double (or more) inheritance altogether. In java, it is not possible to extend more than one direct super class.

Interfaces

  • With the "diamond problem", we've seen how multi-inheritance, i.e. one class having several direct super-classes easily leads to conflicts.
  • By consequence many OO-languages, notably Java, do not allow multi-inheritance.
  • However, there are good reasons to require classes to provide specific methods.
  • This is possible with Java interfaces:
    • Similar to class inheritance, a java class can implements a given interface.
    • With the implements keyword used, the class must provide all methods "mentioned" in the interface.
    • The interface does not provide an actual implementation, only method signatures.

Examples for interfaces in real life:

  • Electrical outlets: Whatever electric device you design, it better match the specifications. (And from travelling we all know about the frustrations of having devices that do not match the interface specification.)
  • Vehicle pedals: Clutch left, breaks in the middle, gas on the right - better not throw a car on the market with other assignments. Bad things will happen.

Interfaces are contracts

Interfaces provide no implementation, but are technical contract. Whatever device (or class) claims to implement an interface, the developer must make sure the interface specifications are respected.

Example and illustration

In UML, interfaces are decorated with the <<Interface>> stereotype, and implementing classes are connected with a dashed arrow:

classDiagram
    class Vehicle {
        <<Interface>>
        +String accelerate()
        +String slowDown()
    }

    class Electric {
        <<Interface>>
        +String chargeBattery()
    }

    class Combustion {
        <<Interface>>
        +String fuelUp()
    }

    class Hybrid {
        <<Class>>
        +String accelerate()
        +String slowDown()
        +String chargeBattery()
        +String fuelUp()
    }

    Vehicle <|-- Electric : extends
    Vehicle <|-- Combustion : extends
    Electric <|.. Hybrid : implements
    Combustion <|.. Hybrid : implements

To reach double interface implementation, the direct parents can be simply listed with the class definition:

public class Hybrid implements Electric, Combustion {

  @Override
  public String accelerate() {
    ...
  }

  @Override
  public String slowDown() {
    ...
  }

  @Override
  public String chargeBattery() {
    ...
  }

  @Override
  public String fuelUp() {
    ...
  }

Note how the "Audi" class lists both methods, although already defined in the interface. This is because the interface does not provide an implementation, i.e. these methods **must ** be implemented in the Audi class. There is no default behaviour to inherit as it was the case with extension of a super-class.

Extends vs implements

In the above example we also have an implicit hierarchy of interfaces, i.e. Electric and Combustion both extends the base Vehicle interface. Initially there can be a little confusion with when to use implements and when to use extends, but there is an easy rule to memorize: "Extends works only for identical concepts"

Super concept Sub concept Keyword
Class Class extends
Interface Interface extends
Interface Class implements
Class Interface (*)
What to use for (*) ?

Nothing! An interface contractually defines the methods to be implemented by a class, it cannot inherit from a class. Eliminate this combination, it makes no sense.

Restricted keyword combinations

Class inheritance and interface extensions are a powerful fabric for many object-oriented concepts. However, some keyword combinations are restricted, as they would inherently lead to conceptual conflicts:

No private interface methods

  • Sometimes we'd like to force implementations to contain a private method, as useful helper for whatever the implementation.
  • Unfortunately this goes against the motivation of interfaces: private methods are implementation internals, and interfaces are a contract for black-box interactions.
  • In fact, it is not even necessary to place the public keyword in front of java interface methods: All interface methods are by default public.

A little caveat: interfaces can actually contain private methods, however these can only be called by default / static methods and are never inherited.

No static methods in abstract classes

  • When implementing a common abstract class, we'd sometimes like to provide some static code, available for all instances, for example a built in singleton pattern.
  • Unfortunately this is an inherent contradition:
    • abstract for a class means: A subclass is required for this functionality to exist
    • static for a method means: This functionality can be accessed, even if no entity exists.
  • So in combination we at once say something must not exist and at once attribute functionality to what must not exist.

Literature

Inspiration and further reads for the curious minds:

Here's the link to proceed to the lab unit: Lab 03