gg = G: Indentation comp.nus.edu.sg/~cs2030/style/
Motivating Example
Instead of having so many “find volumes”, we create an Interface “shape” that is implemented by Circle and Rectangle.

Preview
If you don’t have the classes.
# vim
void main() {}
# compile
javac --enable-preview --release 21 L4.java
# You will get notes
# push into jshell
jshell Main.java
Interface (+ Circle + Rectangle)
A contract that tells you what service can be provided. An interface specifics methods that are to be defined in the implementation class. Interface methods are implicitly public and cannot be instantiated. Circle IS A shape.
interface Shape {
double getArea()
}
# Circlass class
class Circle implements Shape {
private double radius;
Circle(double radius) {
this.radius = radius;
}
double getArea() {
return Math.PI * radius * radius; // Area = πr²
}
}
# Rectangle class
class Rectangle implements Shape {
private double width;
private double height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
double getArea() {
return width * height; // Area = width * height
}
}
Polymorphism
Shape can take the form of a Circle, Rectangle, etc.
Shape shape = new Circle(1.0); // shape is a Circle Shape shape = new Rectangle(2.0, 3.0); // same variable, now a Rectangle
Why it’s useful
double findVolume(Shape shape, double height) {
return shape.getArea() * height;
}
Without polymorphism,
double findVolumeOfCircle(Circle c, double height) { ... }
double findVolumeOfRectangle(Rectangle r, double height) { ... }
// and so on for every shape...
Interface as a contract
3 parties are involved.
The Contract (Shape Interface)
Defines what must be provided, not how.
interface Shape { double getArea(); // every Shape MUST have this }
The implementers (Circle, Rectangle)
Classes that sign the contract and must fulfil it (MUST DEFINE THE METHOD)
class Circle implements Shape {
double radius;
@Override
public double getArea() {
return Math.PI * radius * radius; // their own implementation
}
}
class Rectangle implements Shape {
double width, height;
@Override
public double getArea() {
return width * height; // their own implementation
}
}
The Client (findVolume)
Code that uses the contract without caring about the implementer
double findVolume(Shape shape, double height) {
return shape.getArea() * height;
// only knows about Shape, not Circle or Rectangle
}
Your client only depends on your Shape, not the specific Class.

Implementing Multiple Interfaces
For example, we have Shape and Movable interfaces.
interface Shape {
double getArea(); // Circle must implement this
}
interface Movable {
Movable moveBy(double x, double y); // Circle must implement this too
}
If Circle implements both, Circle needs to define both methods
class Circle implements Shape, Movable {
public double getArea() { // fulfils Shape contract
return Math.PI * radius * radius;
}
public Circle moveBy(double x, double y) { // fulfils Movable contract
return new Circle(this.centre.moveBy(x, y), this.radius);
// creates a NEW circle at a shifted position
}
}
Usecases
1. Used as a Shape only
Shape s = new Circle(point, 1.0);
s.getArea(); // ✅ works
s.moveBy(1, 2); // ❌ can't -- Shape doesn't know about moveBy
2. Used as a Movable only
Movable m = new Circle(point, 1.0);
m.moveBy(1, 2); // ✅ works
m.getArea(); // ❌ can't -- Movable doesn't know about getArea
3. Used as a Circle (access everything)
Circle c = new Circle(point, 1.0);
c.getArea(); // ✅ works
c.moveBy(1, 2); // ✅ works
c.toString(); // ✅ works
4. Used as both at the same time
Circle c = new Circle(point, 1.0);
Shape s = c; // treat it as a Shape
Movable m = c; // same object, treat it as a Movable
s.getArea(); // ✅ works
m.moveBy(1, 2); // ✅ works
5. Passed into methods that expect either interface
// A method that only cares about area
double findVolume(Shape shape, double height) {
return shape.getArea() * height;
}
// A method that only cares about moving
Movable shift(Movable m) {
return m.moveBy(5, 5);
}
Circle c = new Circle(point, 1.0);
findVolume(c, 10.0); // ✅ works because Circle is-a Shape
shift(c); // ✅ works because Circle is-a Movable
Why? Circle implements Shape, Movable
6. Interface that combines both
interface MovableShape extends Shape, Movable {
// no new methods needed
// just combines both contracts
}
class Circle implements MovableShape {
public double getArea() { ... } // must implement
public Circle moveBy(double x, double y) { ... } // must implement
}
// Now you can use MovableShape as the type
static void moveAndPrintArea(MovableShape obj, double x, double y) {
System.out.println("Area: " + obj.getArea());
obj.moveBy(x, y);
}
Programming to Interfaces
| Declared type | Can call | Cannot call |
|---|---|---|
Shape | getArea() | moveBy() |
Movable | moveBy() | getArea() |
Circle | both | nothing |
Shape s = c;
s.getArea(); // ✅ Shape knows about getArea()
s.moveBy(1.0, 2.0); // ❌ ERROR - Shape doesn't know about moveBy()
Movable moveRightBy(Movable movable, double x) {
return movable.moveBy(x, 0.0);
}
moveRightBy(c, 1.0); // ✅ returns Circle @ (1.0, 0.0)
moveRightBy(c, 1.0).getArea(); // ❌ ERROR - Movable doesn't know about getArea()
Based on the type of the RETURNED object, it restricts the methods you can call.
Substitutability
variable_T = expression_S is only valid if S is substitutable for T or S <: T. That means that S is a “more specific” version of T.
Shape shape = new Circle(1.0); // ✅ Circle is substitutable for Shape
Circle circle = new Shape(); // ❌ Shape is NOT substitutable for Circle
Compile-Time vs Run-Time
Compile-Time controls what methods you can call based on the variable declared.
Circle circle = new FilledCircle(1.0, Color.BLUE);
circle.getArea(); // ✅ Circle has getArea()
circle.toString(); // ✅ Circle has toString()
circle.fillColor(Color.GREEN);
// ❌ COMPILE ERROR - Circle doesn't have fillColor()
// even though the actual object is a FilledCircle!
Java refuses to compile fillColor because at compile time, it only knows circle is a Circle — and Circle doesn’t have fillColor(). Java doesn’t look at what the actual object is at this stage.
Run-Time Type Controls Which Version Runs
Circle c1 = new Circle(1.0);
Circle c2 = new FilledCircle(1.0, Color.BLUE);
c1.toString(); // calls Circle's toString() → "Circle with radius 1.0"
c2.toString(); // calls FilledCircle's toString() → "FilledCircle with radius 1.0, color BLUE"
Same compile-time type (Circle), same method call (.toString()), but different results because the run-time types are different!
void foo(Circle circle) {
double area = circle.getArea();
// ✅ compiles - Circle has getArea()
Color color = circle.fillColor(Color.RED);
// ❌ COMPILE ERROR - Circle doesn't have fillColor()
}
Statics vs Dynamic binding
Circle circle = new FilledCircle(1.0, Color.BLUE);
circle.toString();
Step 1 — Static binding at compile time: “okay, Circle has a toString() with no arguments, that’s allowed ✅”
Step 2 — Dynamic binding at runtime: “the actual object is FilledCircle, so call FilledCircle’s toString() ✅”

Static binding picks the right method signature to call, based on arguments passed
circle.toString() // static binding picks toString()
circle.toString(5) // static binding picks toString(int n)
circle.toString("hi") // static binding picks toString(String s)
*What are method signatures? Name + Parameters
toString() // name: toString, params: none
toString(int n) // name: toString, params: int
toString(String s) // name: toString, params: String
toString(String s, int n) // name: toString, params: String and int*
Object Equality
Default behaviour of equals and ==
circle == circle // true — same object
circle == new Circle(1.0) // false — different object, even if same radius
circle.equals(new Circle(1.0)) // false — same problem
To fix this, we try overloaded equals
This compares by radius value instead of memory address.
boolean equals(Circle circle) {
return Math.abs(this.radius - circle.radius) < THRESHOLD;
}
c.equals(new Circle(1.0)) // true ✅
But, there’s a bug
Shape s = c;
s.equals(new Circle(1.0)) // false ❌ — shouldn't this be true?
s has a compile-time type of Shape. So static binding kicks in and looks for equals(Circle) on Shape — it doesn’t exist there. So it falls back to the default equals(Object) from Java, which compares memory addresses again.
Hence, we use an overriding equals method
Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj instanceof Circle circle) {
return Math.abs(this.radius - circle.radius) < THRESHOLD;
}
if (obj instanceof Square square) {
// compare by side length or whatever
}
if (obj instanceof Rectangle rect) {
// compare by area or whatever
}
return false;
}
Because the parameter is now Object (same as the original), this is a proper override. So dynamic binding kicks in — meaning regardless of whether the declared type is Shape or Circle, Java looks at the actual object and uses Circle’s version of equals.
Then inside the method, instanceof checks “is this Object actually a Circle at runtime?” — if yes, it type-casts it to Circle and does the radius comparison.
Concrete Class vs Interface vs Abstract Class
Concrete Class
A fully complete class. Has real data (fields) and real method implementations. You can create objects directly from it.
class Circle {
private double radius;
Circle(double radius) {
this.radius = radius;
}
double getArea() {
return Math.PI * radius * radius;
}
}
// can instantiate directly
Circle c = new Circle(1.0); // ✅
Interface
Just a contract — lists method signatures but no data, no implementations. Classes that implement it must provide the actual code.
interface Shape {
double getArea(); // no implementation
double getPerimeter(); // no implementation
}
class Circle implements Shape {
private double radius;
public double getArea() {
return Math.PI * radius * radius; // must implement
}
public double getPerimeter() {
return 2 * Math.PI * radius; // must implement
}
}
// cannot instantiate interface directly
Shape s = new Shape(); // ❌
Shape s = new Circle(1.0); // ✅
Abstract Class
The middle ground. Can have real data, some implemented methods, and some abstract methods left for subclasses to fill in. Can’t be instantiated because it’s incomplete.
abstract class FilledShape {
private Color color; // real data ✅
FilledShape(Color color) {
this.color = color;
}
abstract double getArea(); // no implementation — subclass must do it
Color getColor() { // real implementation ✅
return this.color;
}
}
class FilledCircle extends FilledShape {
private double radius;
FilledCircle(double radius, Color color) {
super(color);
this.radius = radius;
}
double getArea() { // fills in the blank
return Math.PI * radius * radius;
}
}
// cannot instantiate abstract class
FilledShape fs = new FilledShape(Color.BLUE); // ❌
FilledShape fs = new FilledCircle(1.0, Color.BLUE); // ✅
