Modifiers

  • Class
    • Public: Accessible from any other class
    • Private: Only accessible from within the same class
    • Static: For nested class, doesn’t need parent to be instantiated for it to be instantiated. Both static & non-static can be accessed through Class only
    • Final: Class cannot be inherited from
    • Abstract: Class cannot be instantiated, has to be inherited
    • Protected: Accessible within the same package & by subclasses
    • (no modifier): Only accessible within the same package
  • Field
    • Public: Accessible from any other class
    • Private: Only accessible from within the same class
    • Static: Shared between all instances of the class. Can be accessed through Class or instance. Otherwise, accessed through instance only
    • Final: Value cannot be changed after initialisation (can only be assigned once)
    • Protected: Accessible within the same package & by subclasses
    • (no modifier): Only accessible within the same package
  • Method
    • Public: Accessible from any other class
    • Private: Only accessible from within the same class
    • Static: Can be called without creating an instance of the class
    • Final: Cannot be overridden in subclasses
    • Abstract: Implementation not provided, subclasses have to implement it
    • Protected: Accessible within the same package & by subclasses
    • (no modifier): Only accessible within the same package
  • Overriding method has to be same or more accessible: private < default < protected < public
  • Interface methods are by default public abstract

Overloading (not polymorphism)

Java classes can contain multiple methods with the same name, if they have different signatures, e.g.

class Circle {
	int r;
	// ...
	@Override
	public boolean equals(Object x) {
		return false;
	}
 
	public boolean equals(Circle c) {
		return c.r == this.r;
	}
}

Dynamic Binding

How do we know which method to call when we execute the program?

class ColouredCircle extends Circle {
	int colour;
	@Override
	public boolean equals(Object x) {
		return false;
	}
 
	public boolean equals(ColouredCircle c) {
		return super.equals(c) && this.colour = c.colour;
	}
}
 
ColouredCircle cc = new ColouredCircle(1,1);
Circle c = new ColouredCircle(1,2);
 
System.out.println(c.equals(cc)); // <- How does this line work
// prints true

1. At compile time

  • Identify the compile-time type of c (Circle)
  • Check the class Circle for methods called equals
  • Choose the most specific method descriptor that does not lead to a compilation error in this case Circle::equals(Circle)
    • since it is more specific than Circle::equals(Object)
    • does not lead to a compilation error when a ColouredCircle (compile-time type of cc) is passed to it
    • Search parents if required
  • Store this method descriptor in the compiled bytecode
  • (Perform type erasure if necessary)

2. At run time

  • Retrieve the method descriptor from step 1
  • Determine run-time type of c (ColouredCircle)
  • Look for an accessible method with a matching descriptor
    • First, look in the current class (ColouredCircle)
    • If not found, look up the parents until found (this will always find something, because the compiler got the method descriptor from a parent in the first place)
    • The first matching method will be executed (most specific signature after type erasure)

Most Specific Method Signature?

A method is more specific than a method if the arguments to can be passed to without compilation error. So hi(ColouredCircle) is more specific than hi(Circle)

Bridge Method

// Before
public class Node<T> {
    public T data;
    public Node(T data) { this.data = data; }
    public void setData(T data) {
        this.data = data;
    }
}
 
public class MyNode extends Node<Integer> {
    public void setData(Integer data) {
        super.setData(data);
    } // This intends to override setData
}
MyNode mn = new MyNode(1);
mn.setData("hello");
 
// After type erasure & bridge method
public class Node {
    public Object data;
    public Node(Object data) { this.data = data; }
    public void setData(Object data) {
        this.data = data;
    }
}
 
public class MyNode extends Node {
    public MyNode(Integer data) { super(data); }
    public void setData(Object data) {
        setData((Integer) data); // this refers to MyNode::setData
    } // BRIDGE METHOD
    public void setData(Integer data) {
        super.setData(data);
    }
}
  • A bridge method is generated when both:
    • A type extends/implements a parameterised type
    • Type erasure changes the signature of one or more inherited methods
  • The bridge method is generated right after type erasure
  • Note that in the example above
    • We expect the signatures before type erasure to match, since Node::setData(T) and T is integer, and MyNode::setData(Integer)
    • But the signatures after type erasure do not match: Node::setData(Object) vs MyNode::setData(Integer)
    • So, calling myNode.setData("hello") will work, because MyNode::setData is just overloading Node::setData .
    • The bridge method ensures the overriding works as expected

Java Types

Primitive types

  • byte short int long float double
  • char int

Compile-time vs Run-time types

Circle c = new ColouredCircle(...);

The compile-time type of c is Circle. This does not change. The run-time type of c is ColouredCircle, which can change, e.g. c = new Circle();

Variance of Types

  • Let be a complex type based on type . is:
    • Covariant if implies
    • Contravariant if implies
    • Invariant if is neither covariant nor contravariant: and have no relationship
  • Examples
    • Covariant: Java arrays are covariant (reference type arrays only, not primitive arrays)
    • Invariant: Java generics are invariant
// Covariant
Integer[] ints;
Object[] objects;
objects = ints; // no compiler error, Integer[] <: Object[] (arrays are covariant)
ints = objects; // error
 
// Invariant
Pair<Integer, Integer> ints = // ...
Pair<Object, Object> objects = // ...
objects = ints; // error
ints = objects; // error

How Generic Types Work

  1. Compiler checks types
  2. Compiler removes generics (Type Erasure), replaces them with either Object for unbound generic types or the first type bound for bound generic types
class Pair<S implements Abc & Def, T> {
	private S first;
	private T second;
 
	public Pair(S first, T second) {// ... }
	public S getFirst() { return this.first; }
	public T getSecond() { return this.second; }
}

After the compiler has checked the types, and is happy, this becomes:

class Pair {
	private Abc first;
	private Object second;
 
	public Pair(Abc first, Object second) {// ... }
	public Abc getFirst() { return this.first; }
	public Object getSecond() { return this.second; }
}

also,

Pair<String, Integer> p = new Pair<String, Integer>("hello", 1);
Integer i = p.getSecond();

becomes:

Pair p = new Pair("hello", 1);
Integer i = (Integer) p.getSecond();

Wildcard Types

  • See Wildcard Syntax
  • Upper-bound wildcard
    • A<? extends S> means the generic type parameter can be anything that is a subtype of S
    • For any type S, A<S> <: A<? extends S> (note S <: S)
    • Covariance: If S <: T , then A<? extends S> <: A<? extends T>
      • So, A<S> <: A<? extends T> , due to transitive property
  • Lower-bound wildcard
    • A<? super S> means the generic type parameter can be anything that is a supertype of S
    • For any type S , A<S> <: A<? super S> (note S <: S)
    • Contravariance: If S <: T , then A<? super T> <: A<? super S>
      • So, A<T> <: A<? super S> , due to transitive property
  • Unbounded wildcard
    • A<?> means the generic type parameter can be anything
    • A<?> is a supertype of all A<...>
  • mnemonic PECS: producer extends ; consumer super
    • In above example, in the copyFrom method, Container<? extends T> c
      • We call c.get() , where c is “producing” a value
      • So, we use extends
    • In the copyTo method, Container<? super T> c
      • We call c.set() , where c is “consuming” a value
      • So, we use super

Type Inference

// example class A
public <T extends GetAreable> T findLargest(Seq<? extends T> seq) 
// ...
Shape s = A.findLargest(new Seq<Circle>(0));

Sources of Type Constraints

  1. Type bound: <T extends GetAreable>T <: GetAreable
  2. Target typing: Shape s = ...T <: Shape
  3. Upper bound wildcard: Seq<? extends T>Seq<Circle> <: Seq<? extends T>Circle <: T

Inference

  1. Consider all type constraints, in solving for T
    1. T <: GetAreable
    2. T <: Shape
    3. Circle <: T (<: Object)
  2. Solve the constraints, if possible, else, compiler error. note Shape <: GetAreable 4. Circle <: T <: Shape
  3. Determine lower bound, that is the type of T , or use the rules: 2. Type1 <: T <: Type2T inferred as Type1 3. Type1 <: T (<: Object)T inferred as Type 1 4. T <: Type2T inferred as Type 2

Functional Interfaces

Capture

  • For outer class A, A.this is captured
  • For variables, they are captured directly
  • If the captured variables change Reference , then there will be compiler error (not final or effectively final)

Method Reference

Box::of            // x -> Box.of(x)
Box::new           // x -> new Box(x)
a::foo             // x -> a.foo(x), a is captured
A::foo             // (x, y) -> x.foo(y) or (x, y) -> A.foo(x,y)

Definition

  • Contains exactly one abstract method
  • Can have any number of default or static methods

Monad

  • Informally, a monad design will have these functions
    • flatMap: A function that
      • Takes another function that takes in an “unwrapped” value, and returns a “wrapped” value, after applying an operation
      • Unwraps the current object, and applies the input function to it, then consolidates the results into a wrapped value
      • M<T> -> (T -> M<U>) -> M<U>
      • Takes a wrapped gift, and instructions to create another wrapped gift from the first gifts contents. Unwraps the first gift, then follows the instructions to create a second gift.
    • unit/of: A function that takes in an “unwrapped” value, and wraps it, without additional context
  • Formally, where Monad is a type that is a monad, and monad is an instance of the type:
    • Left identity: Monad.of(a).flatMap(x -> f(x)) == f(a)
    • Right identity: monad.flatMap(x -> Monad.of(x)) == monad
    • Associative: monad.flatMap(x -> f(x)).flatMap(x -> g(x)) == monad.flatMap(x -> f(x).flatMap(y -> g(x)))

Stream

  • reduce with 3 parameters: identity, combiner, accumulator
    • combiner & accumulator must be associative = f(f(x, y), z) = f(x, f(y, z))
    • comb & acc must be compatible: combiner.apply(u, accumulator.apply(identity, t)) = accumulator.apply(u, t)
    • combiner.apply(identity, i) must be equal to i

Nested Class Stuff

  • Can access fields and methods of container class, even private ones
    • Need to access using qualified this: Container.this.x
  • A private nested class can be returned, but not directly used (Container.Nested) outside of the container class

Parallel

  • Parallel: do multiple things at once
  • Concurrent: While waiting for one thing to complete, can do something else
  • All parallel programs are concurrent
  • Having multiple cores/processors is a prerequisite to running a program in parallel

CompletableFuture

Creation Methods

MethodDescription
new CompletableFuture<>()Creates an incomplete future that can be completed manually
completedFuture(value)Creates a future that’s already completed with the given value
runAsync(Runnable)Creates a future from a Runnable (returns void)
supplyAsync(Supplier)Creates a future from a Supplier (returns a value)

Completion Methods

MethodDescription
complete(value)Completes this future with the given value
completeExceptionally(ex)Completes this future with the given exception
cancel(boolean)Attempts to cancel execution
isDone()Returns true if completed in any way
isCompletedExceptionally()Returns true if completed exceptionally
isCancelled()Returns true if cancelled

Transformation Methods

MethodDescription
thenApply(Function)Transforms the result using the given function
thenApplyAsync(Function)Same as thenApply but executes asynchronously
thenAccept(Consumer)Consumes the result without returning a value
thenAcceptAsync(Consumer)Same as thenAccept but executes asynchronously
thenRun(Runnable)Executes the action after completion, ignoring the result
thenRunAsync(Runnable)Same as thenRun but executes asynchronously

Composition Methods

MethodDescription
thenCompose(Function)Chains with another CompletableFuture (flatMap)
thenComposeAsync(Function)Same as thenCompose but executes asynchronously
thenCombine(CF, BiFunction)Combines results of two futures using the given function
thenCombineAsync(CF, BiFunction)Same as thenCombine but executes asynchronously
thenAcceptBoth(CF, BiConsumer)Consumes results of two futures without returning a value
runAfterBoth(CF, Runnable)Runs action after both futures complete

Coordination Methods

MethodDescription
applyToEither(CF, Function)Applies function to result of whichever future completes first
acceptEither(CF, Consumer)Consumes result of whichever future completes first
runAfterEither(CF, Runnable)Runs action after either future completes
allOf(CF...)Returns a future that completes when all specified futures complete
anyOf(CF...)Returns a future that completes when any of the specified futures complete

Error Handling Methods

MethodDescription
exceptionally(Function)Returns alternate result if this future completes exceptionally
handle(BiFunction)Handles both success and failure cases
handleAsync(BiFunction)Same as handle but executes asynchronously
whenComplete(BiConsumer)Performs action when complete (with result or exception)
whenCompleteAsync(BiConsumer)Same as whenComplete but executes asynchronously

Result Retrieval Methods

MethodDescription
get()Waits if necessary for completion and returns result (blocking)
get(long, TimeUnit)Waits with timeout for completion and returns result (blocking)
join()Similar to get() but throws unchecked exceptions (blocking)
getNow(valueIfAbsent)Returns result or given value if not done (non-blocking)
orTimeout(long, TimeUnit)Completes exceptionally with TimeoutException after specified timeout
completeOnTimeout(value, long, TimeUnit)Completes with the given value after timeout
The async variants of these methods (with “Async” suffix) perform the operations on a different thread.

ForkJoin

Key Concepts

  • Work-Stealing Algorithm: Idle threads “steal” work from busy threads’ queues, ensuring efficient load balancing
    • New tasks (fork) are added to the head of the the threads’ deque
    • Tasks are stolen from the tail of other threads’ deque
  • ForkJoinTask: Abstract base class for fork/join tasks
    • RecursiveAction: For tasks that don’t return results
    • RecursiveTask: For tasks that return results

Implementation Pattern

class SumTask extends RecursiveTask<Long> {
    @Override
    protected Long compute() {
        if (taskIsSmallEnough()) {
            return computeDirectly();
        }
        
        // Split task
        SumTask left = new SumTask(leftPortion);
        SumTask right = new SumTask(rightPortion);
        
        right.fork();  // Execute asynchronously
        long leftResult = left.compute();  // Execute directly
        long rightResult = right.join();  // Wait for result
        
        return leftResult + rightResult;
    }
}

??

  • A is a necessary for B: not A → not B
  • A is sufficient for B: A → B
CS2030java.util
BooleanCondition<T>::testfunction.Predicate<T>::test
Producer<T>::producefunction.Supplier<T>::get
Consumer<T>::consumefunction.Consumer<T>::accept
Transformer<T, R>::transformfunction.Function<T, R>::apply
Transformer<T, T>::transformfunction.UnaryOp<T>::apply
Combiner<S, T, R>::combinefunction.BiFunction<S, T, R>::apply