Object construction, the 'this' keyword, and inheritance mechanisms
package fr.tbr.exercises;
/**
* Example of a car, constructor with
* a "color" parameter
* @author Tom
*/
public class Car {
private String color;
public Car(String color) {
this.color = color;
}
}
this keyword.this allows to access to the current instance valuesthis keyword can be used in any
non-static methods
/**
* return the calculated interest on one year
* warning, this method updates the balance with the result of that computation
* @return the interest
*/
public double calculateInterest(){
double result = this.interestRate * this.balance;
this.balance += result; //equivalent to this.balance = this.balance + result
return result;
}
this keyword is
strongly recommended
IS-A relationship
public class Quadrilateral {
private double sideA;
private double sideB;
private double sideC;
private double sideD;
/*...*/
public double calculatePerimeter(){
return this.sideA + this.sideB
+ this.sideC + this.sideD;
}
}
public class Rectangle extends Quadrilateral{
public Rectangle(double height, double width)
{
super (height, height, width, width);
}
}
super keyword: it allows the access of the inherited Class (here the
Quadrilateral Class)
calculatePerimeter()
method again
In the geometry example found here,
add the Ellipse object and try to generalize the Circle class methods
super object methodsabstract classespublic abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
// Abstract method - no implementation
public abstract double calculateArea();
// Concrete method - has implementation
public String getColor() {
return this.color;
}
}
abstract keyword for the classpublic class Circle extends Shape {
private double radius;
public Circle(String color, double radius) {
super(color); // Call parent constructor
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
@Override annotation to indicate you're implementing an abstract methodinterfacesDrivable and Lockablepublic interface Drawable {
// All methods in interfaces are implicitly public and abstract
void draw();
double getArea();
// Since Java 8: default methods with implementation
default void display() {
System.out.println("Drawing shape with area: " + getArea());
}
}
interface keyword instead of classpublic abstractdefault methods (since Java 8) with implementationstatic methodspublic class Rectangle extends Shape implements Drawable, Comparable<Rectangle> {
private double width;
private double height;
public Rectangle(String color, double width, double height) {
super(color);
this.width = width;
this.height = height;
}
@Override
public double calculateArea() {
return width * height;
}
@Override
public void draw() {
System.out.println("Drawing a " + color + " rectangle");
}
@Override
public double getArea() {
return calculateArea();
}
@Override
public int compareTo(Rectangle other) {
return Double.compare(this.calculateArea(), other.calculateArea());
}
}
| Feature | Abstract Class | Interface |
|---|---|---|
| Inheritance | Can extend only one | Can implement multiple |
| Fields | Can have any fields | Only public static final |
| Methods | Abstract + concrete | Abstract + default + static |
| Constructor | Yes | No |
| Use case | IS-A relationship | CAN-DO behavior |
public class ShapeDemo {
public static void main(String[] args) {
// Polymorphism: treat all shapes uniformly
List<Shape> shapes = new ArrayList<>();
shapes.add(new Circle("Red", 5.0));
shapes.add(new Rectangle("Blue", 4.0, 6.0));
shapes.add(new Triangle("Green", 3.0, 4.0));
// Calculate total area using polymorphism
double totalArea = 0;
for (Shape shape : shapes) {
totalArea += shape.calculateArea(); // Calls correct method
System.out.println(shape.getClass().getSimpleName() +
" area: " + shape.calculateArea());
}
System.out.println("Total area: " + totalArea);
}
}
// Abstract base class
public abstract class Shape {
protected String color;
public Shape(String color) {
this.color = color;
}
public abstract double calculateArea();
public abstract double calculatePerimeter();
}
// Interface for drawable objects
public interface Drawable {
void draw();
default String getDescription() {
return "A drawable shape";
}
}
// Concrete implementation
public class Circle extends Shape implements Drawable, Comparable<Circle> {
private double radius;
public Circle(String color, double radius) {
super(color);
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
@Override
public double calculatePerimeter() {
return 2 * Math.PI * radius;
}
@Override
public void draw() {
System.out.println("Drawing a " + color + " circle with radius " + radius);
}
@Override
public int compareTo(Circle other) {
return Double.compare(this.radius, other.radius);
}
}
Create a complete geometry system with the following:
Shape class with:
color fieldcalculateArea() and calculatePerimeter()getColor()Drawable interface with a draw() methodCircle (radius)Rectangle (width, height)Triangle (base, height)ShapeManager class that:
Comparable to sort them by area
java.lang.Objectextends Object, Java does it automatically| Method | Purpose |
|---|---|
toString() |
Returns a string representation of the object |
equals(Object) |
Checks if two objects are logically equal |
hashCode() |
Returns an integer hash code for the object |
getClass() |
Returns the runtime class of the object |
toString() returns: ClassName@hexHashCodepublic class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
}
// Without overriding toString()
Person p = new Person("Alice", 25);
System.out.println(p); // Output: Person@1a2b3c4d (not helpful!)
toString() for meaningful output
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
// Now we get useful output
Person p = new Person("Alice", 25);
System.out.println(p); // Output: Person{name='Alice', age=25}
// toString() is called automatically in many situations:
System.out.println("User: " + p); // String concatenation
String.format("User: %s", p); // Format strings
== compares references (are they the same object in memory?)equals() compares values (are they logically equivalent?)Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);
Person p3 = p1;
// Reference comparison
System.out.println(p1 == p2); // false (different objects)
System.out.println(p1 == p3); // true (same reference)
// Without overriding equals(), it behaves like ==
System.out.println(p1.equals(p2)); // false (by default)
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object obj) {
// 1. Check if same reference
if (this == obj) return true;
// 2. Check if null or different class
if (obj == null || getClass() != obj.getClass()) return false;
// 3. Cast and compare fields
Person other = (Person) obj;
return age == other.age &&
Objects.equals(name, other.name);
}
}
// Now logical comparison works
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);
System.out.println(p1.equals(p2)); // true!
hashCode() returns an integer "fingerprint" of the objectequals(), they must have the same hashCode()HashMap, HashSet, etc.@Override
public int hashCode() {
return Objects.hash(name, age);
}
// Complete equals/hashCode contract:
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25);
System.out.println(p1.equals(p2)); // true
System.out.println(p1.hashCode() == p2.hashCode()); // MUST be true!
equals(), you must also override hashCode()
// Without proper hashCode(), HashSet doesn't work correctly!
Set<Person> people = new HashSet<>();
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25); // Same values as p1
people.add(p1);
people.add(p2); // Should be ignored (duplicate)
// Without hashCode() override:
System.out.println(people.size()); // 2 (WRONG! Both were added)
// With proper hashCode() override:
System.out.println(people.size()); // 1 (Correct! Duplicate detected)
The final keyword can be applied to:
| Applied to | Meaning |
|---|---|
| Variable | Cannot be reassigned (constant) |
| Method | Cannot be overridden in subclasses |
| Class | Cannot be extended (no subclasses) |
public class Circle {
// final static = class-level constant (by convention UPPER_CASE)
public static final double PI = 3.14159265359;
// final instance variable - must be initialized once
private final double radius;
public Circle(double radius) {
this.radius = radius; // OK: initialized in constructor
}
public void setRadius(double r) {
// this.radius = r; // ERROR: cannot reassign final variable
}
public double getArea() {
return PI * radius * radius;
}
}
// Usage
final int maxAttempts = 3;
// maxAttempts = 5; // ERROR: cannot reassign
// final method - cannot be overridden
public class BankAccount {
private double balance;
// Subclasses cannot change this security-critical method
public final void validateTransaction(double amount) {
if (amount <= 0) {
throw new IllegalArgumentException("Invalid amount");
}
}
}
// final class - cannot be extended
public final class SecurityUtils {
public static String hashPassword(String password) {
// ...
}
}
// This would cause a compile error:
// public class HackedSecurityUtils extends SecurityUtils { } // ERROR!
String is a final class in Java - you cannot extend it
A record is a concise way to create immutable data classes:
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
// + equals, hashCode, toString
}
public record Point(int x, int y) { }
// That's it! You get:
// - Constructor
// - x() and y() accessors
// - equals()
// - hashCode()
// - toString()
// Define a record
public record Customer(String name, String email, int age) { }
// Create instances
var customer = new Customer("Alice", "alice@email.com", 30);
// Access components (note: accessors don't use "get" prefix)
String name = customer.name(); // "Alice"
String email = customer.email(); // "alice@email.com"
// Automatic toString
System.out.println(customer);
// Output: Customer[name=Alice, email=alice@email.com, age=30]
// Automatic equals and hashCode
var customer2 = new Customer("Alice", "alice@email.com", 30);
System.out.println(customer.equals(customer2)); // true
public record BankAccount(String accountNumber, double balance) {
// Compact constructor for validation
public BankAccount {
if (balance < 0) {
throw new IllegalArgumentException("Balance cannot be negative");
}
accountNumber = accountNumber.toUpperCase(); // Normalize
}
// Custom methods are allowed
public BankAccount deposit(double amount) {
return new BankAccount(accountNumber, balance + amount);
}
// Static methods too
public static BankAccount empty(String accountNumber) {
return new BankAccount(accountNumber, 0.0);
}
}
| Use Records When | Use Classes When |
|---|---|
| Data is immutable (read-only) | State needs to change over time |
| Simple data carrier (DTO, value object) | Complex behavior and state management |
| You want automatic equals/hashCode | You need custom inheritance hierarchy |
| API response/request models | Domain entities with lifecycle |
// Perfect for records
record OrderItem(String productId, int quantity, double price) { }
record ApiResponse(int status, String message, Object data) { }
instanceof (Java 16)Combine type checking and casting in one step:
if (shape instanceof Circle) {
Circle c = (Circle) shape;
double area = c.getArea();
}
if (obj instanceof String) {
String s = (String) obj;
System.out.println(s.length());
}
if (shape instanceof Circle c) {
// c is already a Circle!
double area = c.getArea();
}
if (obj instanceof String s) {
// s is already a String!
System.out.println(s.length());
}
c, s) is automatically cast and in scope!
// Process different shape types
public double calculateArea(Shape shape) {
if (shape instanceof Circle c) {
return Math.PI * c.getRadius() * c.getRadius();
} else if (shape instanceof Rectangle r) {
return r.getWidth() * r.getHeight();
} else if (shape instanceof Triangle t) {
return 0.5 * t.getBase() * t.getHeight();
}
throw new IllegalArgumentException("Unknown shape");
}
// With negation
if (!(obj instanceof String s)) {
return; // Early exit
}
// s is in scope here!
System.out.println(s.toUpperCase());
Sealed classes restrict which classes can extend them:
// Only Circle, Rectangle, and Triangle can extend Shape
public sealed class Shape
permits Circle, Rectangle, Triangle {
// ...
}
public final class Circle extends Shape { } // Cannot be extended further
public final class Rectangle extends Shape { } // Cannot be extended further
public non-sealed class Triangle extends Shape { } // Open for extension
sealed - declares restricted inheritancepermits - lists allowed subclassesfinal, sealed, or non-sealedSealed classes enable exhaustive pattern matching:
public sealed interface Result permits Success, Failure { }
public record Success(String data) implements Result { }
public record Failure(String error) implements Result { }
// Compiler knows all possible cases!
String process(Result result) {
return switch (result) {
case Success s -> "Got: " + s.data();
case Failure f -> "Error: " + f.error();
// No default needed - compiler knows it's exhaustive!
};
}
| Feature | Java Version | Use Case |
|---|---|---|
| Records | 16 | Immutable data carriers (DTOs, value objects) |
| Pattern Matching instanceof | 16 | Cleaner type checks without explicit casts |
| Sealed Classes | 17 | Controlled inheritance hierarchies |
| Pattern Matching in Switch | 21 | Type-based branching with exhaustiveness |
// All together: Sealed record hierarchy with pattern matching
sealed interface PaymentResult permits Approved, Declined { }
record Approved(String transactionId) implements PaymentResult { }
record Declined(String reason) implements PaymentResult { }
Update your Shape classes to properly implement Object methods:
toString() in the Circle class to return:
"Circle[color=red, radius=5.0]"
equals() to compare circles by color and radiushashCode() to be consistent with equalsCircle c1 = new Circle("red", 5.0);
Circle c2 = new Circle("red", 5.0);
Circle c3 = new Circle("blue", 5.0);
System.out.println(c1); // Should print nicely
System.out.println(c1.equals(c2)); // Should be true
System.out.println(c1.equals(c3)); // Should be false
Set<Circle> circles = new HashSet<>();
circles.add(c1);
circles.add(c2); // Should not add (duplicate)
System.out.println(circles.size()); // Should be 1
toString(), equals(), hashCode()