Java · lesson 14 of 66
Beginner
17- 1 Introduction to Java
- 2 Setting Up Java
- 3 Data Types in Java
- 4 Operators in Java
- 5 Variables and Operators in Java
- 6 Control Flow in Java
- 7 Arrays in Java
- 8 Functions (Methods) in Java
- 9 Strings in Java
- 10 Methods in Java
- 11 Java OOP — Complete Guide
- 12 Introduction to Object-Oriented Programming in Java
- 13 Encapsulation in Java
- 14 Inheritance in Java
- 15 Polymorphism in Java
- 17 Abstraction in Java
- 18 Error Types in Java
Intermediate
38- 16 Regular Expressions in Java
- 19 Interfaces in Java — A Deep Dive
- 20 Abstract Classes in Java
- 21 Method Overloading in Java
- 22 Composition vs Inheritance in Java
- 23 Generics in Java
- 27 Exception Handling in Java
- 28 Java Streams API
- 29 Collections Framework in Java
- 30 Optional in Java
- 31 File Handling in Java
- 32 Records in Java
- 33 Java 8 Features
- 34 Sealed Classes and Interfaces in Java
- 35 Multithreading in Java
- 37 JDBC — Database Access in Java
- 39 Maven — Java Build and Dependency Management
- 42 Gradle — Modern Java Build Tool
- 44 Unit Testing with JUnit 5 and Mockito
- 45 Logging in Java with SLF4J and Logback
- 49 Spring Boot — Introduction
- 50 Dependency Injection in Spring Boot
- 51 Java Project Ideas
- 52 Building REST APIs with Spring Boot
- 53 Validation in Spring Boot
- 54 Spring Data JPA — Database Access
- 55 Exception Handling in Spring Boot
- 56 Spring Security — Authentication and Authorization
- 57 JWT Authentication in Spring Boot
- 58 Dockerizing Spring Boot Applications
- 59 Java Enums: Beyond Simple Constants
- 60 Inner and Anonymous Classes in Java
- 61 Java Generics: Bounded Types, Wildcards, and Generic Methods
- 62 Java Date/Time API: LocalDate, LocalDateTime, and Duration
- 63 Java NIO.2: Paths, Files, and WatchService
- 64 Java Annotations: Custom Types and Meta-Information
- 65 Java Reflection: Dynamic Type Inspection and Method Invocation
- 66 Java Localization: ResourceBundle and Locale Support
Advanced
11Inheritance in Java
Learn how to build class hierarchies using extends, super, method overriding, and the Liskov Substitution Principle with real Java examples.
By SkillByExample · Updated
Inheritance lets a subclass acquire the fields and methods of a superclass and then add to
or change that behaviour. It models “is-a” relationships: a Car is a Vehicle. Each example is
a single file run with the JDK 21 source launcher (java File.java).
extends and super(…)
A subclass constructor must call a superclass constructor first, so the inherited part of the object is initialised before the subclass adds its own.
public class CarDemo {
public static void main(String[] args) {
Car car = new Car("Toyota", 2023, 4);
car.accelerate(60);
car.openTrunk();
System.out.println(car.info());
}
}
class Vehicle {
protected final String brand;
protected final int year;
protected double speed;
Vehicle(String brand, int year) {
this.brand = brand;
this.year = year;
System.out.println("Vehicle constructor");
}
void accelerate(double amount) { speed += amount; System.out.printf("%s at %.0f km/h%n", brand, speed); }
String info() { return year + " " + brand; }
}
class Car extends Vehicle {
private final int doors;
Car(String brand, int year, int doors) {
super(brand, year);
this.doors = doors;
System.out.println("Car constructor");
}
void openTrunk() { System.out.println("Trunk opened"); }
@Override String info() { return super.info() + ", " + doors + " doors"; }
}
$ java CarDemo.java
Vehicle constructor
Car constructor
Toyota at 60 km/h
Trunk opened
2023 Toyota, 4 doors
Car never declares accelerate or brand, yet uses both. Car.info() calls super.info() and
appends to it instead of duplicating the formatting.
Forgetting the parent constructor
If the superclass has no no-argument constructor, the subclass must call one explicitly.
public class NoSuper {
public static void main(String[] args) {
new Truck();
}
}
class Vehicle {
Vehicle(String brand) { }
}
class Truck extends Vehicle {
Truck() {
System.out.println("building a truck");
}
}
$ java NoSuper.java
NoSuper.java:12: error: constructor Vehicle in class Vehicle cannot be applied to given types;
Truck() {
^
required: String
found: no arguments
reason: actual and formal argument lists differ in length
1 error
error: compilation failed
Without an explicit call, the compiler inserts super() — and Vehicle has no such constructor.
Adding super("Volvo"); as the first line of Truck() fixes it.
Overriding and dynamic dispatch
An overriding method replaces the parent’s version for instances of the subclass. Which version runs is decided by the object’s runtime class, even when the call comes from the parent’s code.
public class Shapes {
public static void main(String[] args) {
Shape[] shapes = { new Shape(), new Circle(1), new Rect(4, 6) };
for (Shape s : shapes) System.out.println(s.describe());
}
}
class Shape {
double area() { return 0; }
String describe() { return getClass().getSimpleName() + " with area " + String.format("%.2f", area()); }
}
class Circle extends Shape {
private final double r;
Circle(double r) { this.r = r; }
@Override double area() { return Math.PI * r * r; }
}
class Rect extends Shape {
private final double w, h;
Rect(double w, double h) { this.w = w; this.h = h; }
@Override double area() { return w * h; }
}
$ java Shapes.java
Shape with area 0.00
Circle with area 3.14
Rect with area 24.00
describe() is written once in Shape, but its call to area() reaches the subclass version
because the array elements really are a Circle and a Rect. @Override makes the compiler
check that a matching parent method exists, so a typo like aera() is an error instead of a
silent new method.
Multi-level hierarchies
Each level can call the level above with super. Keep hierarchies shallow; two or three levels
is plenty.
public class Electric {
public static void main(String[] args) {
ElectricCar tesla = new ElectricCar("Tesla", 0.90);
tesla.accelerate(100);
tesla.accelerate(900);
System.out.println(tesla.info());
}
}
class Vehicle {
protected final String brand; protected double speed;
Vehicle(String brand) { this.brand = brand; }
void accelerate(double amount) { speed += amount; System.out.printf("%s at %.0f km/h%n", brand, speed); }
String info() { return brand + " at " + speed + " km/h"; }
}
class Car extends Vehicle {
Car(String brand) { super(brand); }
@Override String info() { return "Car: " + super.info(); }
}
class ElectricCar extends Car {
private double battery; // 0.0 to 1.0
ElectricCar(String brand, double battery) { super(brand); this.battery = battery; }
@Override void accelerate(double amount) {
double cost = amount * 0.001; // 0.1% per km/h
if (cost > battery) { System.out.println("Not enough battery for +" + amount); return; }
battery -= cost;
super.accelerate(amount);
}
@Override String info() { return super.info() + String.format(", battery %.0f%%", battery * 100); }
}
$ java Electric.java
Tesla at 100 km/h
Not enough battery for +900.0
Car: Tesla at 100.0 km/h, battery 80%
ElectricCar.accelerate adds a check, then delegates to Vehicle.accelerate for the speed
update. info() is built from all three levels: Car adds the prefix, Vehicle the speed, and
ElectricCar the battery.
final: no subclasses, no overrides
public class FinalDemo {
public static void main(String[] args) {
System.out.println(new Point3D(1, 2, 3));
}
}
final class Point {
final double x, y;
Point(double x, double y) { this.x = x; this.y = y; }
}
class Point3D extends Point {
final double z;
Point3D(double x, double y, double z) { super(x, y); this.z = z; }
}
$ java FinalDemo.java
FinalDemo.java:12: error: cannot inherit from final Point
class Point3D extends Point {
^
1 error
error: compilation failed
A final class guarantees no subclass can add mutable state or change behaviour — String and
Integer are final for this reason. A final method can’t be overridden, but its class can
still be extended.
Inherited from Object: toString, equals, hashCode
Every class extends java.lang.Object. Its defaults print a hash and compare identity, which is
rarely what a value class wants — and equals without hashCode breaks hash collections.
import java.util.*;
public class ObjectMethods {
public static void main(String[] args) {
Set<Product> set = new HashSet<>();
set.add(new Product("Laptop", 999));
Product same = new Product("Laptop", 999);
System.out.println("equals: " + same.equals(set.iterator().next()));
System.out.println("contains: " + set.contains(same));
Set<FixedProduct> fixed = new HashSet<>(List.of(new FixedProduct("Laptop", 999)));
System.out.println("fixed contains: " + fixed.contains(new FixedProduct("Laptop", 999)) + " " + fixed);
}
}
class Product {
final String name; final int price;
Product(String name, int price) { this.name = name; this.price = price; }
@Override public boolean equals(Object o) {
return o instanceof Product p && p.name.equals(name) && p.price == price;
}
}
class FixedProduct extends Product {
FixedProduct(String name, int price) { super(name, price); }
@Override public int hashCode() { return Objects.hash(name, price); }
@Override public String toString() { return "Product(" + name + ", $" + price + ")"; }
}
$ java ObjectMethods.java
equals: true
contains: false
fixed contains: true [Product(Laptop, $999)]
The two Products are equal, yet the set says it doesn’t contain one: HashSet checked the bucket
for same’s identity-based hash code, found nothing there, and never called equals. Overriding
hashCode consistently fixes it. A record generates all three methods for you.
Liskov Substitution Principle
A subclass must work anywhere its parent is expected. Square extends Rectangle looks natural
but breaks code written for rectangles:
public class Liskov {
static void stretch(Rectangle r) {
r.setWidth(5);
r.setHeight(4);
System.out.println(r.getClass().getSimpleName() + ": expected area 20, got " + r.area());
}
public static void main(String[] args) {
stretch(new Rectangle());
stretch(new Square());
}
}
class Rectangle {
protected int w, h;
void setWidth(int w) { this.w = w; }
void setHeight(int h) { this.h = h; }
int area() { return w * h; }
}
class Square extends Rectangle {
@Override void setWidth(int w) { this.w = w; this.h = w; }
@Override void setHeight(int h) { this.w = h; this.h = h; }
}
$ java Liskov.java
Rectangle: expected area 20, got 20
Square: expected area 20, got 16
Square keeps its own rule (equal sides) by breaking Rectangle’s (width and height are
independent). Code that is correct for every Rectangle now fails for one subclass. When a
subclass must weaken the parent’s guarantees, it shouldn’t be a subclass.
Practice
- Add a
Motorcycle extends Vehiclewith awheelie()method. Store one in aVehiclevariable and callaccelerateandwheelie. Why doesn’tv.wheelie()compile, and how do you callwheeliesafely?
Solution
public class Bikes {
public static void main(String[] args) {
Vehicle v = new Motorcycle("Ducati");
v.accelerate(40);
if (v instanceof Motorcycle m) m.wheelie();
}
}
class Vehicle {
protected final String brand; protected double speed;
Vehicle(String brand) { this.brand = brand; }
void accelerate(double amount) { speed += amount; System.out.println(brand + " at " + speed); }
}
class Motorcycle extends Vehicle {
Motorcycle(String brand) { super(brand); }
void wheelie() { System.out.println(brand + " pulls a wheelie"); }
}
$ java Bikes.java
Ducati at 40.0
Ducati pulls a wheelie
v.wheelie() wouldn’t compile: the compiler only knows the static type Vehicle, which has no
such method. The pattern-matching instanceof checks the runtime type and gives a Motorcycle
variable in one step.
- Make
Shape.describe()finaland try to override it inCircle. What happens?
Solution
public class FinalMethod {
public static void main(String[] args) {
System.out.println(new Circle().describe());
}
}
class Shape {
double area() { return 0; }
final String describe() { return "area " + area(); }
}
class Circle extends Shape {
@Override double area() { return 3.14; }
@Override String describe() { return "a circle"; }
}
$ java FinalMethod.java
FinalMethod.java:14: error: describe() in Circle cannot override describe() in Shape
@Override String describe() { return "a circle"; }
^
overridden method is final
1 error
error: compilation failed
area() can still be overridden; only describe() is locked. This is how a parent fixes an
algorithm while leaving steps open.
- Replace
Product/FixedProductwith arecord Product(String name, int price)and repeat theHashSet.containstest.
Solution
import java.util.HashSet;
import java.util.Set;
public class RecordProduct {
record Product(String name, int price) {}
public static void main(String[] args) {
Set<Product> set = new HashSet<>();
set.add(new Product("Laptop", 999));
System.out.println(set.contains(new Product("Laptop", 999)) + " " + set);
}
}
$ java RecordProduct.java
true [Product[name=Laptop, price=999]]
Records generate equals, hashCode and toString from their components, so they are always
consistent. Records are implicitly final, so they cannot be extended either.
- Fix the Liskov problem by giving
RectangleandSquarea common interfaceShapewitharea()and no setters, and make both immutable.
Solution
import java.util.List;
public class LiskovFixed {
interface Shape { int area(); }
record Rectangle(int w, int h) implements Shape { public int area() { return w * h; } }
record Square(int side) implements Shape { public int area() { return side * side; } }
public static void main(String[] args) {
List<Shape> shapes = List.of(new Rectangle(5, 4), new Square(4));
for (Shape s : shapes) System.out.println(s + " area " + s.area());
}
}
$ java LiskovFixed.java
Rectangle[w=5, h=4] area 20
Square[side=4] area 16
Neither type promises independent setters any more, so neither can break the other’s contract.