Summary
Dynamic type checking
- check if the run-time type of x is a subtype of y
java
x instanceof y
true if
Final
- prevents user from overriding a method
java
class Parent {
final void func() { ...
}
}
class Child extends Parent {
@Override
void func() { ... // not possible to override
}
}
Specific
- method M is more specific than a method N if the arguments to M can be passed to N without compilation error
Concept
Overriding
- replacing parent methods, with the same method signature, in the child class
- return type of the overriding method should be a subtype of the return type of the overridden method
@Override
pre-processor statement allows javac to check that the function actually overrides something
Overloading
- multiple functions with the same name but different method signatures
- when invoked the most specific implementation is used
Overriding allows for run-time polymorphism
Overloading allows for compile-time polymorphism
Dynamic binding
- find most specific method descriptor based on CTT
- invoke the method, with the same descriptor based on RTT
- class methods do not support dynamic binding
java
class A {
boolean contains(Object[] array, Object obj) {
for (Object curr : array) { // compile-time curr is Object
if (curr.equals(obj)) { // invokes the equal function based on the run-time type of curr
return true;
}
}
return false;
}
}
- at compile-time, CTT(
curr
) = Object, search for the most specificObject.equals()
method that accepts CTT(obj
) = Object, store the method descriptor ->Object::equals(Object)
- at run-time, determine RTT(
curr
), look for the first accessible method that matches the descriptor, traverse up the class hierachy until found
Application
Object methods that are usually overriden
String toString()
- what’s printed upon instantiation in jshell
boolean equal(Object obj)
- what it means for another object to be equal to this one
- one of the only places where type casting is acceptable
3D and 2D point
java
class Point2D {
private double x;
private double y;
public Point2D(double x, double y) {
this.x = x;
this.y = y;
}
public Point2D(Point2D p) { // overloaded constructor
this.x = p.x;
this.y = p.y;
}
@Override
public String toString() { // overriding the base Object toString method
return "x=" + this.x + ", y=" + this.y;
}
public boolean equal(Object obj) {
if (obj instanceof Point2D) {
Point2D p = (Point2D) obj; // typecast after checking
return this.x == p.x && this.y == p.y;
} else {
return false;
}
}
}
class Point3D extends Point2D {
private double z;
public Point3D(double x, double y, double z) {
super(x, y); // construct the parent 2D point
this.z = z;
}
public Point3D(Point2D p, double z) { // overloaded constructor
super(p);
this.z = z;
}
@Override
public String toString() { // overriding the parent toString method
return super.toString() + ", z=" + this.z; // invoking the parent toString method
}
@Override
public boolean equal(Object obj) {
if (obj instanceof Point3D) { // check subtype first
Point3D p = (Point3D) obj; // type cast after checking
return super.equal(p) && this.z == p.z; // works since
} else if (obj instanceof Point2D) {
Point2D p = (Point2D) obj;
return super.equal(p) && this.z == 0;
} else {
return false;
}
}
}
Runtime type check
java
class A {}
class B extends A {}
A a = new A();
B b = new B();
A ab = new B();
b instanceof A // true RTT(b) = B <: A
b instanceof B // true RTT(b) = B <: B
a instanceof A // true RTT(a) = A <: A
a instanceof B // false RTT(a) = A </: B
ab instanceof A // true RTT(ab) = B <: A
ab instanceof B // true RTT(ab) = B <: B
Dynamic binding
java
public class Point {
private double x;
private double y;
public Point(double x, double y) {
this.x = x;
this.y = y;
}
public boolean equals(Point p) {
return this.x == p.x && this.y == p.y;
}
}
public class Circle {
private Point centre;
private int radius;
public Circle(Point centre, int radius) {
this.centre = centre;
this.radius = radius;
}
@Override
public boolean equals(Object obj) {
System.out.println("equals(Object) called");
if (obj == this) {
return true;
}
if (obj instanceof Circle) {
Circle circle = (Circle) obj;
return (circle.centre.equals(centre) && circle.radius == radius);
} else {
return false;
}
}
public boolean equals(Circle circle) {
System.out.println("equals(Circle) called");
return circle.centre.equals(centre) && circle.radius == radius;
}
}
Circle c1 = new Circle(new Point(0, 0), 10);
Circle c2 = new Circle(new Point(0, 0), 10);
Object o1 = c1;
Object o2 = c2;
o1.equals(o2); // compile-time: looks for Object::equal(Object), run-time: finds Circle.equal(Object), passes o2
o1.equals((Circle) o2); //* compile time: since Circle<:Object, looks for Object::equal(Object), run-time: finds Circle.equal(Object), passes (Circle) o2
o1.equals(c2); // compile time: since Circle<:Object, looks for Object::equal(Object), run-time: finds Circle.equal(Object), passes c2
c1.equals(o2); // compile-time: looks for Circle::equal(Object), run-time: finds Circle.equal(Object), passes o2
c1.equals((Circle) o2); //* compile-time: looks for Circle::equal(Circle), run-time: finds Circle.equal(Circle), passes (Circle) o2
c1.equals(c2); // compile time: looks for Circle::equal(Circle), run-time: finds Circle.equal(Circle), passes c2