Modifiers

Class

  • Public: Accessible from any other class
  • Private: Only accessible from within the same class
  • Static: Can only be used in nested classes
  • Final: Class cannot be inherited from
  • Abstract: Class cannot be instantiated, has to be inherited

Variable (Field)

  • Public: Accessible from any other class
  • Private: Only accessible from within the same class
  • Static: Shared between all instances of the class
  • Final: Value cannot be changed after initialisation (can only be assigned once)

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

Polymorphism

Overloading

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 signature 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
  • 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 MyNode(Integer data) { super(data); }
    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

Syntax

General Stuff

// Point.java
public class Point {
	int x;
	int y;
	public Point(int x, int y) {
		this.x = x;
		this.y = y;
	}
}
// Shape.java
abstract class Shape implements GetAreable, Comparable { // comparable not actually implemented, just an example
	private int numAxesOfSymmetry;
 
	public boolean isSymmetric() {
		return numAxesOfSymmetry > 0;
	}
}
// Circle.java
public class Circle extends Shape {
	Point centre;
	int radius;
	public Circle(Point centre, int radius) {
		this.centre = centre;
		this.radius = radius;
	}
 
	@Override
	public double getArea() {
		return Math.PI * Math.pow(r, 2);
	}
	
	@Override
	public boolean equals(Object x) {
		return false;
	}
 
	public boolean equals(Circle c) {
		return this.centre.equals(c.centre) && this.radius == c.radius;
	}
}
// ColouredCircle.java
public class ColouredCircle extends Circle {
	String colour;
	public ColouredCircle(Point centre, int radius, String colour) {
		super(centre, radius);
		this.colour = colour;
	}
	
	@Override
	public boolean equals(ColouredCircle c) {
		return super.equals(c) && this.colour == c.colour;
	}
}
// GetAreable.java
public interface GetAreable {
	double getArea();
}
// Main.java
public class Main {
	public static void main(String[] args) {
		Point origin = new Point(0,0);
		Shape[] shapes = {
			new Circle(origin, 1), 
			new ColouredCircle(origin, 1, "green") }
		// or Shape[] shapes = new Shape[2] for empty array
		// shapes.length is the array length
		for (Shape shape : shapes) {
			System.out.println(shape.getArea());
		}
	}
}

Exceptions

// MyFileNotFoundException.java
// This is a checked exception, which means we have to handle it with a try catch, or declare it with a throws statement. It is checked at compile time.
// This is used when the exception is caused by something out of our control e.g. a file not existing, web request fail etc.
class MyFileNotFoundException extends Exception {
	public MyFileNotFoundException(String message) {
		super(message);
	}
 
	@Override
	public String getMessage() {
		return "File not found! Message: " + super.getMessage();
	}
}
// ArrayIndexException.java
// This is an unchecked exception, which means we do not need to handle it in the code.
// This is used for programmer error e.g. null pointer exception, array index out of bounds
class ArrayIndexException extends RuntimeException {
}
class Example {
	public String readFile(String path) throws MyFileNotFoundException {
		// ...
		throw new MyFileNotFoundException("file " + path + " not found!");
	}
 
	public String readFiles(String[] paths) {
		String result = "";
		for (String path : paths) {
			try {
				result += this.readFile(path)
			} catch (MyFileNotFoundException e) {
				System.out.println(e.getMessage());
			} finally {
				// this code is always run
				System.out.println("done with a file!");
			}
		}
	}
}

Generic Types

Generic Class

// Pair.java
public class Pair<S, T> { 
// convention: type parameters are single capital letters
	private S first;
	private T second;
 
	public Pair(S first, T second) {
		this.first = first;
		this.second = second;
	}
 
	public S getFirst() {
		return this.first;
	}
 
	public T getSecond() {
		return this.second;
	}
}
 
// note: generic types need to be reference types, not primatives
// does not work, int is not a reference type:
Pair<int, String> a = new Pair<int, String>(123, "Hello"); 
// works:
Pair<Integer, String> a = new Pair<Integer, String>(123, "Hello");
// syntax to use the type parameters of the variable:
Pair<Integer, String> a = new Pair<>(123, "Hello"); // <> diamond operator is equivalent to above
 
// Generic types can be bound, 
public class Pair<S extends Abc, T> {// ... }
// Abc can be a class or interface. This just means that S should be a subtype of Abc
// Multiple bounds
public class Pair<S extends Abc & Def, T> {// ... }

Generic Method

// Contains.java
public class Contains {
	public static <T> boolean contains(T[] items, T target) {
		for (T item : items) {
			if (item.equals(target)) {
				return true;
			}
		}
		return false;
	}
}
 
// application
Integer[] ints = new Integer[] {1,2,3};
Contains.<Integer>contains(ints, 2);

Generics cannot be used with arrays, due to type erasure

new Pair<String, Integer>[2]; // error
class Abc<S, T> {
	private S[] arr; // ok
	// ...
	new Pair<S, T>[2]; // error
	this.arr = new S[2]; // error
	this.arr = (S[]) new Object[2]; // ok, but produces warning
	// to suppress this warning:
	@SuppressWarnings("unchecked") // be careful of how items can be added to the array
	S[] a = (S[]) new Object[2];
	this.arr = a;
}

Wildcards

class Shape {}
class Circle extends Shape {}
 
class Container<T> {
	private T item;
 
	public Container(T item) {
		this.item = item;
	}
	
	public T get() {
	    return item;
	}
	
	public void set(T item) {
	    this.item = item;
	}
 
	// upper bound wildcard
	// we want to allow copying from a container that contains anything that is a subtype of T
	public void copyFrom(Container<? extends T> c) {
	    this.item = c.get();
	}
 
	// lower bound wildcard
	// we want to allow copying to a container that contains anything that is a supertype of T
	public void copyTo(Container<? super T> c) {
	    c.set(this.item);
	}
}
 
// demo
Shape s = new Shape();
Circle c = new Circle();
        
Container<Shape> sc = new Container<>(s);
Container<Circle> cc = new Container<>(c);
        
sc.copyFrom(cc); // ok
cc.copyFrom(sc); // error
        
cc.copyTo(sc); // ok
sc.copyTo(cc); // error
@SuppressWarnings("unchecked")
Queue<Passenger>[] q = (Queue<Passenger>[]) new Queue<?>[nStops];
this.queues = q; // Queue<Passenger> <: Queue<?>

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 <: Type2 → T inferred as Type1 3. Type1 <: T (<: Object) → T inferred as Type 1 4. T <: Type2 → T inferred as Type 2