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), and CTT of cc (ColouredCircle)
  • List down all the accessible equals methods from Circle
  • 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
    • note: <T> void fun(T) is always more general
  • (Perform type erasure if necessary)
  • Store this method descriptor in the compiled bytecode (store ERASED descriptor)

2. At run time

  • 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 descriptor 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 /* ERASED parent type */ data) {
        super.setData((Integer /* original Type param */) 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