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
Listcontains other objects. - The
Listalways functions the same way, no matter what objects are stored inside. - However, the
Listrequires 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:
- The
<Apple>notation assigns theAppleclass to theListgeneric. From here on, onlyApples (or subclasses) can be added to theListobject.- Allowed:
apples.add(new Apple()) - Not allowed (generic mismatch):
apples.add(new Pair())
- Allowed:
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
Catapultmay only be loaded with what it was built for. (Imagine we have a catapult constructed forApples and then someone loads it withPairs !!) - When loaded, the catapult can be fired. The object is then nullified (sent to the moon, i.e. replaced by a
nullreference)
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 toFruit(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 theActionListenerinterface: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
returnstatement
- Must be wrapped in
- If there are parameters, they are noted within the brackets:
Examples
-
Simple lambda to print the received value to console:
-
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
Consumerinterface 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
forEachstatement the lambda has to be stored in aConsumerinterface!
- The
-
Example of storing a lambda in a custom interface:
- Interface:
- 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 theprintlnfunction 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::printlnto pin-point to theprintlnfunction 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
What does the API say ?
"(Streams are a) [...] sequence of elements supporting sequential and parallel aggregate operations."
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
664579primes below10 million. - It took about
1.4seconds to compute the result.
- It correctly determined there are
- We have a collection (
ArrayList list) that stores all values from1to15.- 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.3seconds - that's a drop to only21%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:
waittoo long: application is unresponsive.waittoo 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