Skip to content

Advanced OO programming concepts

In this last lecture on object-oriented concepts, we'll take a closer look at advanced concepts, notably generics, lambdas & streams, inversion of control and reflection.

Lecture upshot

Generics are for consistent handling of unknown types. Lambdas and streams are a functional-programming oriented alternative to loops, inversion of control reduces hard coded dependencies, reflection allows changing code properties at runtime.

Generics

Often times you want to use (or design) a class working with some other class - but when coding you don't know what that other class is, and you don't care, as long as it is used consistently.

A first example are Lists:

  • A List contains other objects.
  • The List always functions the same way, no matter what objects are stored inside.
  • However, the List requires all stored objects to be of the same type (or subtype).
List Status
List of Apples OK
List of Pairs OK
List of Apples and Pairs Not OK

But how can a List enforce consistency checks, if it can work with either object Apples or Pairs ?

Generics

Generics are placeholders for classes. Any class may take their place, but if so the class must be used consistenly accross all generic occurrences.

Collections

  • Most likely you have already used generics, when initializing a collection.
  • For example when instantiating a List, you're using the notation:
List<Apple> apples = new LinkedList<>();
  • The <Apple> notation assigns the Apple class to the List generic. From here on, only Apples (or subclasses) can be added to the List object.
    • Allowed: apples.add(new Apple())
    • Not allowed (generic mismatch): apples.add(new Pair())

What about the <>

In earlier java versions, one had to match the variable's generic on the object's generic, i.e. List<Apple> apples = new LinkedList<Apple>(). However, since the consistency is implicit later java versions switched to the diamond operator <>, for brevity.

Maps

  • Maps are an example of collections with multi-generics.
  • Maps are essentially like phone books, the list from (unique) keys to values:
  • Example:
Professor Office
Assi 4620
Kavanagh 4330
Mili 4340
Schiedermeier 4440
Stiévenart 4735
What role do generics play for a map, and what types should be associated for this example ?

The map requires two generics, one to ensure a consistent type is used for the professor names, and another to ensure a consistent type is used for their offices. In this example it could be new HashMap<String, Integer>().

Defining generics

  • So far we've only seen how to use existing classes with generic contraints.
  • Now let's take a look at how to write a custom new class applying a generic constraint.

Generic catapult

Imagine we have a Catapult class which can be charged with objects, and then sends them flying.

Some constraints apply:

  • Once initialized the Catapult may only be loaded with what it was built for. (Imagine we have a catapult constructed for Apples and then someone loads it with Pairs !!)
  • When loaded, the catapult can be fired. The object is then nullified (sent to the moon, i.e. replaced by a null reference)

Implementation:

// <T> represents a generic. Whatever class T will be, it has to be consistent
// across all later occurrences in this code.
public class Catapult<T> {

    private T charge;

    public void load(T charge) {
        this.charge = charge;
    }

    public T unload() {
        T chargeBuffer = charge;
        charge = null;
        return chargeBuffer;
    }

    public void fire() {
        System.out.println("Sending charge (" + charge.toString() + ") to the moon");
        charge = null;
    }
}

Generic extension fruit-catapult

A more advanced example is the FruitCatapult. It works similar to the regular catapult, but can accustom two charges - but they both have to be the same Fruit.

Illustration:

List Status
Two Apples OK
Two Pairs OK
One Apple and one Pair Not OK

Luckily Apples and Pairs follow a strict Fruit hierarchy:

  classDiagram
    class Fruit {
        <<Interface>>
    }

    class Apple {
        <<Class>>
    }

    class Pair {
        <<Class>>
    }

    Fruit <|.. Apple: implements
    Fruit <|.. Pair: implements
This seems contrived, why not just use Fruit instead of <T extends Fruit> ?

While the catapult would then be restricted to Fruits, it could be charged with different fruits at once.

Code:

// Catapult works with anything implementing Fruit
// Note: For generics "extends" covers also interfaces !
public class FruitCatapult<T extends Fruit> {

    private T charge1;
    private T charge2;

    public void loadFirst(T charge1) {
        this.charge1 = charge1;
    }

    public void loadSecond(T charge2) {
        this.charge2 = charge2;
    }

    public T unloadFirst() {
        T chargeBuffer = charge1;
        charge1 = null;
        return chargeBuffer;
    }

    public T unloadSecond() {
        T chargeBuffer = charge2;
        charge2 = null;
        return chargeBuffer;
    }

    public void fire() {
        System.out.println("Sending both charges to the moon");
        charge1 = null;
        charge2 = null;
    }
}

Generics only know extends, no implements

The class declaration is a bit counter-intuitive: Fruit is an interface, so why is it T extends Fruit ? For generics the syntax is slightly different: Whether interface of super-class, in a generic definition it is always extends.

Generic hierarchies

We can also assume out catapult to be branded to any subclass of Fruit, while requiring the subclass to be used consistently across methods: This is achieved with a generic: <T extends Fruit>

classDiagram
    class Fruit
    class Apple
    class Pear
    class Banana

    Fruit <|-- Apple
    Fruit <|-- Pear
    Fruit <|-- Banana

T can now be either an Apple, Pair, or Banana, but whatever it is, it must be used consistently!
For our FruitCatapult, it would mean once we place a certain kind of fruit in the catapult, all catapult methods are likewise branded to this kind of fruit:

/**
 * This last example shows how to use a specific extension consistently.
 * If we used just "Fruit" instead of a generic, we could not enforce consistency across methods.
 * Using T extends Fruit ensures:
 *  * It is a Fruit
 *  * It is the same fruit extension across all methods.
 */
public class FruitCatapult<T extends Fruit> {

  private T charge1;
  private T charge2;

  public void loadFirst(T charge1) {
    this.charge1 = charge1;
  }

  public void loadSecond(T charge2) {
    this.charge2 = charge2;
  }

  public T unloadFirst() {
    T chargeBuffer = charge1;
    charge1 = null;
    return chargeBuffer;
  }

  public T unloadSecond() {
    T chargeBuffer = charge2;
    charge2 = null;
    return chargeBuffer;
  }

  public void fire() {
    System.out.println("Sending both charges to the moon");
    charge1 = null;
    charge2 = null;
  }
}

Generic upper bounds

Sometimes we want to add a constraint, while keeping flexibility. This is where the ? notation comes in:

<? extends Fruit> stands for: I don't know what it is, but it is a subclass of Fruit.

  • Consequently, when accessing the ? element, we know it can be assigned to Fruit (it's a subclass)
  • But we cannot feed in a specific type, because we don't know what specific subclass it is!

Example:

// Here we create a list, that is specific for a Fruit subclass (Apples, Pairs, Bananas)
// But we do NOT specificy which exact subclass it is:
List<? extends Fruit> fruits = new ArrayList<Apple>();

// Consequently, when retrieving elements, we can store them in the (superclass) Fruit variable
Fruit f = fruits.get(0); // OK, we know it’s a Fruit

// But we can NOT add specific subclass elements, because we don't know which exact subclass is used.
fruits.add(new Apple()); // not possible

Lambdas

  • So far we've made a strict separation between functionality (methods), and data (values, objects).
  • Whenever we wanted to access a certain functionality, we did so by passing through a class.
  • In this section we're looking a lambdas, a java mechanism to treat functions like data.

What are lambdas

Lambdas are a compactly defined anonymous functions, i.e. functions that can be either executed in place, or stored and passed as a variable.

Motivation

  • There are situations in which you want to treat functions as data, i.e. pass functions around.

  • An example is a button listener:

    • When "programing" the listener, you assign an event to a function.
    • The listener definition requires a function as input argument.
  • Previously you've solved this with anonymous inner classes:

    public static void main(String[] args) {
    
      // Creating a java swing button.
      JButton button = new JButton();
    
      // Programming button behaviour by using an anonymous inner class
      button.addActionListener(new ActionListener() {
        @Override
        public void actionPerformed(ActionEvent e) {
    
          System.out.println("The button was pressed.");
        }
      });
    }
    
  • The anonymous class is the body of new ActionListener(){...}, it implicitly creates an anonymous subclass to the ActionListener interface:

    classDiagram
        class ActionListener {
            <<Interface>>
            +actionPerformed()
        }
    
        class AnonymousClass {
            <<Class>>
            +actionPerformed()
        }
    
        ActionListener <|.. AnonymousClass: "implements"

So far so good, but...

The downside with anonymous inner classes is their verbosity. A lot of code for something rather simple: wrapping up a function to be executed.

Syntax

  • Like any java function, a lambda maps from zero to many input values to one output value.
  • The general syntax is: () -> result
    • If there are parameters, they are noted within the brackets: (param1, param2, ...) -> result
    • If the result is not a single statement (e.g. call of existing function), the result function:
      • Must be wrapped in {...}
      • Must contain a return statement

Examples

  • Simple lambda to print the received value to console:

    void main() {
        // Source: https://www.w3schools.com/java/java_lambda.asp
        ArrayList<Integer> numbers = new ArrayList<Integer>();
        numbers.add(5);
        numbers.add(9);
        numbers.add(8);
        numbers.add(1);
        numbers.forEach((n) -> System.out.println(n));
    }
    
  • Storing lambda in a variable, using function as data:

    void main() {
    
      // Defining lambda and storing it in variable
      Consumer<Integer> printToConsole = (n) -> System.out.println(n);
    
      // Using the lambda on a collection
      ArrayList<Integer> numbers = new ArrayList<Integer>();
      numbers.add(5);
      numbers.add(9);
      numbers.add(8);
      numbers.add(1);
      numbers.forEach(printToConsole);
    }
    

Caveats

The previous example looks a bit contrived. Why did we use the Consumer interface to store our lambda ?

  • Lambdas can be stored under any interfaces, under the premise that it is a functional interface, i.e. the interface has only one public and non-static method.
    • The Consumer interface happens to be a pragmatic choice, as it is a preexisting java-provided interface.
    • We can absolutely define our own interface for the purpose of storing our lambda.
    • However, the interface we use must match its usage, so for usage in a forEach statement the lambda has to be stored in a Consumer interface!
  • Example of storing a lambda in a custom interface:

    • Interface:
    public interface StringLambda {
    
      String modify(String s);
    }
    
    • Lambda usage:
      void main() {
    
        // Define lambdas and store them in custom interface
        StringLambda repeat = (s) -> s + " " + s;
        StringLambda reverse = (s) -> new StringBuilder(s).reverse().toString();
    
        // Here we pass our lambdas as parameters
        printModified("Hello", repeat);
        printModified("Hello", reverse);
      }
    
      void printModified(String str, StringLambda stringLambda) {
        System.out.println(stringLambda.modify(str));
      }
    

Method reference

  • In some cases you just want to pass an existing function, as a lambda.
  • We've already seen a way to do this, e.g: (s) -> System.out.println(s) is a way to expose the println function as lambda.
  • However, this example is a bit contrived, technically we're just wrapping an existing function.
  • A more elegant path is to directly expose the existing function, using the method reference operator. We can simply use System.out::println to pin-point to the println function as lambda. String::foo

Code example:

void methodReferenceOperator() {
    ArrayList<Integer> numbers = new ArrayList<>();
    numbers.add(5);
    numbers.add(9);
    numbers.add(8);
    numbers.add(1);
    numbers.forEach(System.out::println);
}

Streams

  • Java is (mostly) an imperative language, i.e. code is executed line-by-line.
  • Nonetheless, it is possible to tackle specific problems with a dedicated functional programming concept: Streams

Illustrations

Let's unravel what the formal definition means in practice !

  • Usually you'd process data, e.g. a collection element by element.
  • For example if we want to count how many primes are in a given range:
void main() {
    List<Integer> list = new ArrayList<>();
    for (int i = 1; i <= 10000000; i++) {
        list.add(i);
    }

    // Checking iteratively for primes, count
    int result = 0;
    for (Integer i : list) {
        if (isPrime(i)) {
            result++;
        }
    }
    System.out.println(result);
}

Let's recap:

  • The code worked:
    • It correctly determined there are 664579 primes below 10 million.
    • It took about 1.4 seconds to compute the result.
  • We have a collection (ArrayList list) that stores all values from 1 to 15.
    • The collections stores data.
    • The collection does not operate on data (we need an extra loop to do so).

Now let's see how a stream version would work:

  • If we had a stream instead, we could directly perform aggregate operations on the elements, notably:
    • Mapping (transforming elements)
    • Reducing (filtering / grouping elements)

Map-Reduce

These two ingredients are all that's required for Map&Reduce style data processing - a powerful programming paradigm to obtain massive efficiency gains, through parallel processing.

  • Here's a stream version that does the same as the initial loop:
    • Data is initially converted to a stream.
    • Elements are mapped to integer values (map)
    • Elements are filtered (reduce)
    • Elements are counted (reduce)
void main() {
    List<Integer> list = new ArrayList<>();
    for (int i = 1; i <= 15; i++) {
        list.add(i);
    }

    System.out.println(
            // convert to stream
            list.stream()
                    // map
                    .mapToInt(Integer::intValue)
                    // reduce
                    .filter(i -> isPrime(i))
                    // reduce
                    .count());
}

The stream version of our code is a bit faster, it computes the same result in just 1.2 seconds.

Streams and lambdas

Lambdas are an essential tool for map & reduce:

  • Mapping: We can use a lambda to efficiently define what is value is transformed to
  • Reducing: We can use a lambda to efficiently define which conditions apply for filtering

Streams and threading

Now is a good time to wonder why the performance gain was not an order of magnitude higher.

  • We've correctly implemented our algorithm as map-reduce variant using a stream and lambdas.
  • We've only observed a 14% performance increase: Not bad, but not groundbreaking.
  • The reason is we're not yet unleashing the full potential of streams.

Let's revisit the initial stream definition:

  • It says: Streams support "[...]sequential and parallel aggregate operations".
  • So far we're only using sequential operations ! We have one stream, and it being processed element by element!

Luckily there's a simple fix, we can use the parallelStream() method:

void main() {
    List<Integer> list = new ArrayList<>();
    for (int i = 1; i <= 15; i++) {
        list.add(i);
    }

    System.out.println(
            // convert to PARALLEL stream
            list.parallelStream()
                    // map
                    .mapToInt(Integer::intValue)
                    //reduce
                    .filter(i -> isPrime(i))
                    //reduce
                    .count());
}

With parallelStream() we advised the JVM to distribute the prime-checking of stream elements on all available CPU cores.

  • Depending on your machine this entails massive performance gains.
  • The 8 core machine used by the prof measures 0.3 seconds - that's a drop to only 21% of the original time needed!

A parallel solution isn't always more performative

Distribution the processing of individual stream elements over multiple cores still requires some background scheduling by the JVM, alas there is some overhead. When working with small streams, a parallel solution is not necessarily faster, as the scheduling overhead may outweigh the performance gains from parallel CPU workers. In our example the parallel approach is only faster when searching beyond roughly 1 million.

Inversion of control

Inversion of control is an umbrella term for multiple programming context where the developer deliberately hands over control to an external entity.

  • It often goes hand in hand with declarative context, i.e. the developer only declares what is requested, not how it is reached. The developer cedes control over the specific mechanism for reaching the overall goal.
  • Also referred to as the "Hollywood principle": "Don't call us, we call you !"

The Hollywood Principle

The naming comes from a stereotypical situation in movie castings. Many actors apply for a role, and all of them eager to know as soon as possible whether they got the job repeatedly call the studio to ask for news. Obviously this is a waste of resource, as the studio can also simply call the actors, once a decision has been made. The applicants are to accept that the situation is out of their control, and they should not attempt to interfere with the process.

Observer pattern

The movie casting scenario can be translated one-to-one to a programming concept. Managing an external event, e.g. the click of a UI button, or a status change on a remote server.

Keeping control

Polling is a vivid example of keeping control (where you shouldn't).

  • Whether something is to happen on a button click, or a status change on a server, the most naive implementation is to repeatedly check in an infinite loop:
flowchart TD
    Start([Start])
    CheckStatus[Check resource status]
    IsReady{Event happened?}
    Wait[Wait for a while]
    Process[Do something]
    End([End])
    Start --> CheckStatus
    CheckStatus --> IsReady
    IsReady -- Yes --> Process
    IsReady -- No --> Wait --> CheckStatus
    Process --> End

Issues:

  • Most of the time there's no news.
  • Timing is critical:
    • wait too long: application is unresponsive.
    • wait too short: application consumes too much CPU

Ceding control

The common "inversion of control" answer to this situation is to abandon the polling loop for an observer pattern, i.e. registering a handler for the event of interest.

import java.beans.EventHandler;

public static void main() {

    // Create a handler that embodies a specific behaviour.
    EventHandler handler = new EventHandler() {
        @Override
        public void onEvent() {
            System.out.println("The button was clicked!");
        }
    };

    // Tell resource to notify the handler, when event happened.
    Resource resource = ...
    resource.registerEventHandler(handler);
}

Dependency injection

Another "inversion of control" example is "dependency injection".

We will learn more about dependency injection in an upcoming lecture.

Reflection

Reflection is the practice of changing class properties at runtime.

  • Usually all class properties (constructur, fields, methods, etc...) are defined in source code, and transformed to bytecode by the compiler. At we commonly take compiled classes as set-in stone, i.e. the perfectly reflect what has been translated from in source code.
  • Reflection refers to changing classes after compilation, that is, we can change fields, or modify method access at runtime.
  • Java has a built-in capacity for reflection, i.e. no additional libraries are needed to apply reflection.

Examples

In the following we'll take a look at two examples of how reflection can be used to modify class properties at runtime.

Parameter modification

  • Let's assume a simple class, representing a student's graded Exam.
  • We can imagine as requirement that once the object created, the grade cannot be changed.
  • One way to implement such a class is by using a private field, with only getter access:
/**
 * Represents a graded exam. We assume a grade can never be changed.
 */
public class Exam {

    // private field for grade. Is only exposed via getter.
    private final char grade;

    /**
     * Exam constructor. This is the only way of setting the grade.
     * @param grade
     */
    Exam(char grade) {
        this.grade = grade;
    }

    /**
     * Getter for grade.
     * @return the grade as character from A-F.
     */
    public char getGrade() {
        return grade;
    }
}

Pretty solid, one might say. However, as reflection allows modifying the class at runtime, we can change any field to "accessible" (equivalent to public) and change the value anyway:

  public static void main(String[] args) throws Exception {

    // Create an immutable exam object and print the initial status.
    Exam exam = new Exam('C');
    System.out.println("Original grade: " + exam.getGrade());

    // Access the private field 'grade' (change to public)
    Field gradeField = Exam.class.getDeclaredField("grade");
    gradeField.setAccessible(true); // bypass private access

    // Modify the field
    gradeField.setChar(exam, 'A');

    System.out.println("Modified grade: " + exam.getGrade());
}

Final adds additional protection

The recent java versions have added additional security to prevent access on class-internal fields if needed. When a field is final, additional security mechanisms prevent from naive access via reflection.

Objenesis

  • A second (and a bit evil) example is a small library called Objenesis.
  • Objenesis has one job: Create objects of classes.
Why would anybody need that

Some libraries / frameworks impose their own way for obtaining specific classes. With Objenesis you can outsmart their "inversion of control" and effectively take back control on when and how to create any object you desire.

  • Let's assume we have something that cannot be instantiated:
/**
 * We assume this class is provided as is, we have no write access (e.g. it comes from a maven repo,
 * as JAR).
 */
class UnconstrucableBlob {

    /**
     * Private constructor. This is nasty, because how do we ever create an object of this blob?
     */
    private UnconstrucableBlob() {
    }

    /**
     * Calling the helloWorld method requires an instance. But how to create one if the constructor is
     * private ?
     */
    public void helloWorld() {
        System.out.println("You've done the impossible - your blob is alive!");
    }
}

That means, we can not simply call the constructor (it is private):

    public static void main(String[] args) {

    // This will not work (constructor is private)
    UnconstrucableBlob blob = new UnconstrucableBlob();
}

However, we can use Objenesis:

main() {
    Objenesis objenesis = new ObjenesisStd();
    UnconstrucableBlob blob =
            objenesis.getInstantiatorOf(UnconstrucableBlob.class).newInstance();
    blob.helloWorld();
}
How does Objenesis create the new blob instance ?

With reflection. The library creates a new constructor, or changes the existing one to public.

Annotations

  • Reflection is often used in combination with annotations.
  • Java annotations are short flags which can be added to:
    • Class definitions
    • Variable definitions
    • Method definitions
    • Field definitions
  • (Most) annotations are ignored by the compiler, i.e. they have no direct associated behaviour.
  • E.g. we can come up with our own annotation "@Toto", and add it to a class, without breaking anything.
    • The compiler will simply ignore the annotation.

Custom annotations

Defining a new annotation is straightforward:

import java.lang.annotation.*;

// We need this annotation to survive the compile process. Retention ensures so.
@Retention(RetentionPolicy.RUNTIME)
// We need our annotation to be placeable exclusively in front of class fields
@Target(ElementType.FIELD)
public @interface GradeInfo {

    // The fields within this annotation definition define what payload can be added.
    String value() default "N/A";    // Optional parameter

    boolean required() default false;
}

We can now use our own custom annotation at code level, to annotate class fields:

public class Exam {


    // We want to set a default value to every midterm exam grade, in case not initialized, using reflection.
    // To do so, we add an annotation to the grade field.
    @GradeInfo(value = "midterm", required = true)
    private char grade;

    public Exam(char grade) {
        this.grade = grade;
    }
}

Finally, verifying annotations at runtime is done using reflection:

  public static void main(String[] args) throws NoSuchFieldException {

    // Use reflection to look up annotation information at runtime:
    Field gradeField = Exam.class.getDeclaredField("grade");

    // Note: No objects are required to inspect class attributes
    if (gradeField.isAnnotationPresent(GradeInfo.class)) {
        GradeInfo annotation = gradeField.getAnnotation(GradeInfo.class);
        System.out.println("GradeInfo value: " + annotation.value());
        System.out.println("Is required: " + annotation.required());
    }
}

Annotations and reflection

Annotations are often used in combination with reflection, to reduce boilerplace code.

Not all annotations indicate reflection though, to give to examples:

Framework / Library Uses annotations Modifies code at
Lombok Yes Compile time (APT)
Spring Core Yes Runtime (with reflection)

Bean container management is an advanced form of dependency injection. With spring core, almost all classes (Beans) are initiated, managed, and destroyed by an external framework e.g. Spring Core. This notably refers to their wiring, which is configured via an explicit configuration file, or syntax conventions. The main interest is to keep the code clean of anything solely needed for class instantiation (the code should only contain actual logic, not boilerplate code needed for preparing classes).

Closing thoughts

Reflection is a powerful tool, but as always, with great power comes great responsibility.

  • Reflection is notoriously hard to debug, because the code you're inspecting (source code) is not the code you're debugging (code modified at runtime by reflection).
  • It is easy to write code nobody understands anymore, use reflection cautiously.

Think twice

Whenever you're tempted to use reflection, think twice if is really needed, and if the benefits outweigh potential drawbacks on code comprehensiveness.

Literature

Inspiration and further reads for the curious minds:

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