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 M is more specific than a method N if the arguments to M can be passed to N without compilation error. So hi(ColouredCircle) is more specific than hi(Circle)
Bridge Method
// Beforepublic 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 methodpublic 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 C(T) be a complex type based on type T . C is:
Covariant if S<:T implies C(S)<:C(T)
Contravariant if S<:T implies C(T)<:C(S)
Invariant if C is neither covariant nor contravariant: C(S) and C(T) have no relationship
Examples
Covariant: Java arrays are covariant (reference type arrays only, not primitive arrays)
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();
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 Apublic <T extends GetAreable> T findLargest(Seq<? extends T> seq) // ...Shape s = A.findLargest(new Seq<Circle>(0));
Sources of Type Constraints
Type bound:<T extends GetAreable> → T <: GetAreable
Target typing:Shape s = ... → T <: Shape
Upper bound wildcard:Seq<? extends T> → Seq<Circle> <: Seq<? extends T> → Circle <: T
Inference
Consider all type constraints, in solving for T
T <: GetAreable
T <: Shape
Circle <: T (<: Object)
Solve the constraints, if possible, else, compiler error. note Shape <: GetAreable
4. Circle <: T <: Shape
Determine lower bound, that is the type of T , or use the rules:
2. Type1 <: T <: Type2 → T inferred as Type1
3. Type1 <: T (<: Object) → T inferred as Type 1
4. T <: Type2 → T 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 capturedA::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
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
Method
Description
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
Method
Description
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
Method
Description
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
Method
Description
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
Method
Description
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
Method
Description
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
Method
Description
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; }}