Intermediate

Object-oriented Programming in Java

Java is an object-oriented language, and to be effective in Java, it is essential to understand OOP — how it works, and more importantly, the fundamental ideas behind it.

Java version: Java SE 17 (LTS) — content applicable to Java 8+


Table of Contents

  1. Course Overview
  2. Object-oriented programming approach
  1. Working with objects
  1. Define your own classes
  1. Hide information — Encapsulation
  1. Design with abstraction and encapsulation
  1. The Legacy
  1. Polymorphism
  1. Interfaces
  1. Design with inheritance and polymorphism
  1. The static keyword
  1. Training conclusion

1. Course Overview

Welcome to this training on object-oriented programming (OOP) in Java, presented by Paolo Perrotta. Java is an object-oriented language, and to be effective in Java, it is essential to understand OOP — how it works, and more importantly, the fundamental ideas behind it.

Training objectives

This training aims to enable you to master the fundamental concepts of OOP in Java. We start with the basics — objects and classes — and then progress towards the four fundamental pillars of object-oriented programming:

  1. Abstraction
  2. Encapsulation
  3. Inheritance
  4. Polymorphism

Along the way, we will explore many Java features:

  • object references
  • access modifiers
  • The interfaces
  • And many more

Prerequisites

To follow this training, the following knowledge is sufficient:

  • Importing packages
  • Calling methods
  • Basic structures: if, loops
  • Be able to write a small program with a few methods and variables

It is not necessary to know the objects or classes in advance.

Final objective

At the end of this training, you will know enough OOP to start working on a real Java system in production.


2. Approach to object oriented programming

Total module duration: 13m 49s

Welcome to the OOP

Before diving into the content, a version check is in order. This training is based on the latest LTS (Long Term Support) version of Java. LTS stands for long-term support. Almost everything covered in this training also applies to older versions. Java 8 is used as a reference: everything presented is compatible with Java 8 and later versions, unless a newer feature is explicitly mentioned.

Setting OOP is difficult. Academic definitions exist but are not very useful for a beginner. Instead of a formal definition, the chosen approach is to give a high-level description of object-oriented programming in Java, a view from 10,000 meters above sea level.

OOP in 6 minutes

The example of the alarm management application will be the central example of the entire training. Let’s imagine we’re building an application that monitors an industrial environment and triggers alarms to signal when something needs attention. You can configure these alarms, place them on a dashboard, etc.

The basic idea of ​​object-oriented programming: you build your program using objects, and objects are like variables, except they’re smarter.

In an alarm management application, we would have objects representing alarms. Each object is like a record — it contains:

  • fields (data): a message string describing the alarm (for example, “temperature too high”), a Boolean indicating if the alarm is active
  • methods (data operations): an operation to view the alarm, an operation to save a phone number for notifications, etc.

An object is a bundle of data and operations on that data.

Java also has the concept of class (class). A class is like a blueprint for objects. From an Alarm class, you can create as many alarms as you want. They all share the same structure and operations, but they are separate objects with their own data.

The four pillars of OOP that this training will cover:

  • Abstraction: define the types adapted to our domain
  • Encapsulation: hide the implementation
  • Inheritance: create class hierarchies
  • Polymorphism: write generic code that works with multiple types

The object-oriented debate

The developer community is divided on OOP in Java. Several positions coexist:

  1. Java does OOP correctly, as it is supposed to be done.
  2. Java is not OOP enough — it remains too procedural, it does not go far enough in that direction.
  3. Java misses the point of OOP — it focuses on parts of the OOP that are not so important and neglects more important parts.
  4. OOP is simply not a good idea — some developers prefer functional programming, from which Java has borrowed some ideas over the years.

This debate exists, all these positions have a certain legitimacy. But this training is not about debates — you learn the basics, and you will form your own opinion with experience.

Mechanics and design

There are two facets of object-oriented programming:

  1. The mechanics: Java features that support the fundamental ideas of OOP — this is primarily what this training covers.
  2. Design: use these features to write good code. This is the creative part of programming. Design is always important, and even more so in OOP, because OOP provides many new tools and using them is not always intuitive.

For example, for encapsulation, there are Java keywords to decide whether a method is part of an object’s interface or implementation. But beyond these keywords, you have to learn when it makes sense to make something visible or not. This is the design side.

In this training, we mainly focus on the technical side, with some design guidelines to avoid classic beginner mistakes.

Content of this training

Structure module by module:

  1. Modules 1-2: Introduction
  2. Modules 3-6: Abstraction and encapsulation (working with objects, defining classes, hiding information, design)
  3. Modules 7-10: Inheritance and polymorphism (inheritance, polymorphism, abstract classes, interfaces, design)
  4. Module 11: The static keyword — the least OOP of Java features
  5. Module 12: Conclusion

3. Working with objects

Total module duration: 30m 23s

Use objects

Creating an object and calling a method on it may seem simple, but it involves several pitfalls that are worth examining in depth.

Creation of an object:

Alarm alarm = new Alarm("Temperature too high");
  • We use new, the class name, and parentheses
  • The variable must have a type — in this case, the class of the object
  • With recent versions of Java, we can use var instead of the class name
var alarm = new Alarm("Temperature too high");  // Java 10+
  • Parentheses are used to pass arguments to the constructor — here, a message

What we can do with an object:

  1. Access its fields: alarm.active
  2. Call its methods: alarm.turnOn()

Concrete example (module 3, UsingObjects.java file):

package com.pluralsight.alarms;

public class UsingObjects {
    public static void main(String[] args) {
        Alarm alarm = new Alarm("Temperature too high");
        System.out.println(alarm.active);
        System.out.println(alarm.message);
        System.out.println(alarm.getReport());
        alarm.turnOn();
        System.out.println(alarm.active);
        System.out.println(alarm.getReport());
    }
}

The Alarm class of this module:

package com.pluralsight.alarms;

public class Alarm {
    final String message;
    boolean active;

    Alarm(String message) {
        this.message = message;
    }

    void turnOn() {
        active = true;
    }

    void turnOff() {
        active = false;
    }

    String getReport() {
        return getReport(false);
    }

    private String getReport(boolean uppercase) {
        if (active) {
            if (uppercase)
                return message.toUpperCase();
            else
                return message;
        } else
            return "";
    }

    void sendReport() {
        System.out.println(getReport(true));
    }
}

Things under the hood

There is a fundamental difference between primitive type variables and object type variables in Java:

Primitive variable:

int myInt = 42;

Java allocates memory for the variable (4 bytes for an int). The value 42 is written directly to this memory. It’s like a box with a value inside.

Object variable:

Alarm alarm = new Alarm("");

Java creates the object in a special area of ​​memory called the heap. The amount of memory allocated depends on the class. Java then returns a reference (reference) to this object, and we store this reference in the variable.

Key point: Object variables contain references to objects, not the objects themselves.

Comparison table:

PrimitivesObjects
Stored valueThe value directly (ex: 42)A reference to the object
LocationStackHeap (heap)
InitializationLiteral (ex: 42, true)Via new
Default0, false, etc.null

More about references

Instructor Analogy: The heap is like a big lake. When you create an object, it’s like putting a boat in the middle of the lake. The keyword new creates the boat and also throws a rope towards the shore. This rope is the reference. We grab the rope and connect it to a pole in the ground — this pole is the variable, with the same type as the object.

We never interact directly with the object — we always use it through the reference. Another analogy: the reference is like a remote control for the object. We never touch the object itself, we control it remotely via the reference.

Consequences of object variables being references:

  1. References can be null
  2. There may be aliasing (several variables pointing to the same object)
  3. Distinction between identity and equality of objects
  4. final references do not mean the object is immutable

The dreaded NullPointerException

First consequence of references: a reference may not have a value — it may reference… nothing. This is the special value null.

Primitive variables always have a value: an int is always an integer (even if it is 0), a char is always unicode. They cannot be “nothing”. In contrast, an object reference can be null.

Alarm alarm = null;  // Cette référence ne pointe vers aucun objet
alarm.turnOn();      // NullPointerException !

When we try to call a method or access a field on a null reference, we get a NullPointerException — probably the most common exception in Java code.

NullPointerException occurs for several less obvious reasons:

  • Many methods in Java libraries can return either an object or null
  • Object fields have null as default value (when it is an object type)

Good news with Java 17: the error messages are much more precise and indicate exactly which reference was null.

Object aliasing

package com.pluralsight.alarms;

public class Aliasing {
    public static void main(String[] args) {
        Alarm alarm1 = new Alarm("");
        System.out.println(alarm1.active);  // false
        Alarm alarm2 = alarm1;              // On copie la RÉFÉRENCE, pas l'objet !
        alarm2.turnOn();
        System.out.println(alarm2.active);  // true
        System.out.println(alarm1.active);  // true aussi ! C'est le même objet
    }
}

Explanation: When we assign alarm1 to alarm2, we copy the reference value. Both variables now point to the same object in the heap. This is called aliasing — having multiple aliases (names) for the same object. If we modify the object via one or the other of the references, both will “see” the modification.

This phenomenon does not occur with primitives:

package com.pluralsight.alarms;

public class NoAliasingForPrimitives {
    public static void main(String[] args) {
        int integer1 = 41;
        int integer2 = integer1;  // On copie la VALEUR
        integer1++;
        System.out.println(integer1);  // 42
        System.out.println(integer2);  // 41 — pas affecté !
    }
}

With primitives, the value is copied, not a reference. Modifying integer1 does not affect integer2.

Passing objects to methods

Aliasing also occurs each time we pass an object to a method:

static void doStuff(Alarm alarm) {
    alarm.active = true;  // Modifie l'objet original !
}

public static void main(String[] args) {
    Alarm alarm = new Alarm("Test");
    System.out.println(alarm.active);  // false
    doStuff(alarm);
    System.out.println(alarm.active);  // true — modifié par doStuff !
}

What’s happening: When we call doStuff(alarm), the reference is copied into the alarm parameter of the method. We now have two references to the same object. When the method modifies the object, the original reference also sees the change.

It’s inherent in Java, you can’t avoid it. This is simply how parameter passing in Java works for objects.

Identity and equality

A classic trap for Java beginners:

package com.pluralsight.alarms;

public class EqualityAndIdentity {
    public static void main(String[] args) {
        String s1 = "test string";
        String s2 = "test string";
        System.out.println(s1.equals(s2));  // true (égalité de contenu)
        System.out.println(s1 == s2);       // true aussi à cause du string interning !
    }
}

Warning: == compares references (the identity), not the contents of the objects.

ConceptDescriptionOperator
IdentityBoth variables point to the same object in memory==
EqualityBoth objects have the same content/value.equals()

Classic example that traps beginners:

String a = new String("hello");
String b = new String("hello");
System.out.println(a == b);      // false ! Deux objets distincts en mémoire
System.out.println(a.equals(b)); // true  — même contenu

Rule: Never use == to compare String or other objects if you want to compare their value. Use .equals().

Note: The result of == may vary between classes. Some classes implement equals() to check content equality, others do not. Consult the documentation.

Summary of identity and equality

  • Two objects are identical if they are literally the same object in memory. Two references point to the same object.
  • Two objects are equal if they contain the same data (eg: two String with the same sequence of characters).
  • == verifies identity. Do not use it to check the equality of objects.
  • equals() has different behavior depending on the class. Sometimes it’s a comparison of identity, sometimes a comparison of equality. Check the documentation.
  • For primitives there is no concept of identity, only equality. == still works correctly.

The meaning of constants

The final keyword in Java declares that a variable cannot be reassigned:

final int MY_CONST = 42;
// MY_CONST = 50;  // Erreur de compilation !

But what about objects?

package com.pluralsight.alarms;

public class FinalDoesNotMeanImmutable {
    public static void main(String[] args) {
        final Alarm alarm = new Alarm("");  // La RÉFÉRENCE est finale
        System.out.println(alarm.active);  // false
        alarm.turnOn();                    // On peut encore modifier l'objet !
        System.out.println(alarm.active);  // true
        // alarm = new Alarm("autre");     // Erreur ! On ne peut pas réassigner la référence
    }
}

Important distinction:

  • A final reference: the reference cannot be reassigned (point to another object)
  • An immutable object (immutable): you cannot change the internal state of the object

final on a reference ≠ immutable object

String in Java are immutable: if we “modify” a String, we actually obtain a new String.

To make an object immutable, you must design it that way when creating the class.

Autoboxing

Java has always maintained a distinction between primitive types and objects. But there is a feature that makes this distinction less visible: autoboxing.

Sometimes we have a primitive and we want an object instead. Classic example: storing an int in a list (which can only contain objects).

Java offers wrapper classes for each primitive type:

PrimitiveWrapper class
intInteger
booleanBoolean
doubleDouble
floatFloat
longLong
charCharacter
package com.pluralsight.alarms;

import java.util.ArrayList;
import java.util.List;

public class AutoboxingAndUnboxing {
    public static void main(String[] args) {
        int aPrimitive = 42;

        List<Object> myList = new ArrayList<>();
        myList.add(aPrimitive);           // Autoboxing : int → Integer automatiquement

        Integer anObject = aPrimitive;    // Autoboxing automatique
        int anotherPrimitive = anObject;  // Unboxing automatique : Integer → int
        System.out.println(anotherPrimitive);  // 42
    }
}

autoboxing (automatic primitive → wrapper conversion) and unboxing (reverse) are managed automatically by Java. No need to write new Integer(42) or anObject.intValue() explicitly anymore.

Module 3 Summary

This module covered several important consequences of object variables containing references:

  1. References can be null → source of NullPointerException
  2. Aliasing: several variables can reference the same object. This happens every time an object is passed to a method.
  3. Identity vs equality: == compares references (identity). equals() compares the contents (depending on the implementation).
  4. Constants and immutability: a final reference cannot be reassigned, but the pointed object can still be modified (unless it is immutable).
  5. Autoboxing/unboxing: Java automatically converts between primitives and their corresponding wrapper classes.

4. Define your own classes

Total module duration: 31m 18s

Define a class

Client code first approach: Instead of first defining a class and then using it, one can write client code that uses it, then define the class for that code to work. This is a technique used by experienced developers.

Basic syntax:

class Alarm {
    // Corps de la classe
}
  • The keyword class followed by the name
  • The body in braces {}
  • Naming convention: class names use CamelCase (also called UpperCamelCase or PascalCase): first letters of each word capitalized — Alarm, HighVisibilityAlarm, NullPointerException
  • This convention is very important in Java. It resembles the humps of a camel, hence the name “Camel case”.
  • In a real project, the class is in its own file with the same name as the class + .java extension: Alarm.java

Declare a field

To add data to a class, declare fields:

class Alarm {
    boolean active;  // Field déclaré dans la classe
}

Naming convention for fields, methods and variables: lowerCamelCase (first letters in lower case) — active, getMessage, snoozeUntil.

Class names tend to be nouns (things), method names tend to be verbs (actions).

If you create several alarms, each has its own active field, with its own value (true or false).

Define method

Anatomy of a method:

void turnOn() {
    active = true;
}
  • Return type: the type of the returned value (String, int, boolean, etc.) or void if the method returns nothing
  • Noun: in lowerCamelCase, typically a verb
  • Arguments: in parentheses
  • Body: the code in braces

The keyword this references the current object. It is optional in most cases, but may be necessary to remove ambiguity between a field and a parameter of the same name.

Overloading: several methods can have the same name if they have different argument lists:

String getReport() {
    return getReport(false);  // Appelle la version avec argument
}

String getReport(boolean uppercase) {
    if (active) {
        if (uppercase)
            return message.toUpperCase();
        else
            return message;
    } else
        return "";
}

Java determines which version to call by looking at the arguments passed.

A development story

This section simulates the reality of software development: the client or team leader arrives with urgent requests in the middle of work in progress.

New features requested:

  1. An alarm must contain a message describing the alarm (eg: “Temperature too high”)
  2. A getReport method returning the message if the alarm is active, an empty string otherwise — with option to display in uppercase
  3. A sendReport (placeholder) method which simply displays the report in uppercase on the screen, pending a real implementation

Add functionality to the class

package com.pluralsight.alarms;

class Alarm {
    boolean active;
    final String message;

    Alarm(String message) {
        this.message = message;
    }

    void turnOn() {
        active = true;
    }

    void turnOff() {
        active = false;
    }

    String getReport() {
        return getReport(false);
    }

    String getReport(boolean uppercase) {
        if (active) {
            if (uppercase)
                return message.toUpperCase();
            else
                return message;
        } else
            return "";
    }

    void sendReport() {
        System.out.println(getReport(true));
    }
}

And the client program:

package com.pluralsight.alarms;

public class Program {
    public static void main(String[] args) {
        Alarm alarm = new Alarm("Temperature too high");
        alarm.turnOn();
        alarm.sendReport();
    }
}

Null strikes again

Problem: if you create an Alarm without a message, the message field is null by default. When getReport tries to call message.toUpperCase(), we get a NullPointerException.

Why? When Java allocates memory for an object, it initializes all fields to 0 (equivalent to false for boolean, and null for object references).

Alarm alarm = new Alarm();       // Si on avait un constructeur sans argument
alarm.sendReport();              // NullPointerException : message est null !

Java 17 gives more precise error messages:

Cannot invoke "String.toUpperCase()" because "<local1>.message" is null

This behavior is due to the fact that fields of type object (like String) are themselves references, and their default value is therefore null.

Initialize fields

Options to avoid the problem:

  1. Default value at declaration:
String message = "No message set";  // Valeur par défaut

But finding a good default value is not always possible.

  1. Use a constructor (best solution): force the initialization of the field when creating the object.

The constructors

A constructor is a block of code called automatically when creating an object:

Alarm(String message) {
    this.message = message;  // this.message = field, message = paramètre
}

Features:

  • Looks like a method but without return type
  • Has the same name as the class
  • May have arguments
  • Automatically called by Java when creating the object

Initialization sequence when writing new Alarm("Temp"):

  1. Java allocates memory for object
  2. Java initializes all fields to their default value (or declared value)
  3. Java calls the constructor

With a constructor that takes a message, you can no longer create an alarm without a message:

Alarm alarm = new Alarm("Temperature too high");  // OK
Alarm alarm = new Alarm();  // Erreur de compilation !

The final fields

To prevent a field from being modified after initialization in the constructor:

class Alarm {
    final String message;  // Ne peut être assigné qu'une seule fois

    Alarm(String message) {
        this.message = message;
    }
}

A final field:

  • Must be assigned either to the declaration or in the constructor
  • Cannot be reassigned afterwards
  • If not initialized, compile error

Reminder: final on a reference ≠ immutable object. The final field cannot be reassigned, but if it is a reference to a mutable object, the internal state of that object can still change.

To make an object truly immutable, all its fields must be:

  1. final (not reassignable)
  2. Themselves immutable (ex: String, int, etc.)

More about constructors

Multiple constructors (constructor overload):

Alarm(String message) {
    this.message = message;
}

Alarm() {
    this("Default message");  // Appelle l'autre constructor
}
  • You can have several constructors with different arguments
  • Constructors can call each other with this(arguments) — but only as first statement in the constructor
  • Useful to avoid code duplication

The default constructor: If no constructor is defined, Java automatically generates one without arguments. As soon as you define a constructor, Java no longer generates this constructor by default. If we always want to have a constructor without arguments, we must write it explicitly.

Destructors in Java

Many OO languages ​​have destructors — code called when you want to get rid of an object. In Java this is not necessary.

Analogy: The heap is like a lake, the objects are boats. When a variable goes out of scope, there is no longer a reference to the boat. But Java can’t just delete the object immediately — there could be other references.

The Java solution: the Garbage Collector (garbage collector). The JVM (Java Virtual Machine) monitors references. When an object no longer has any references pointing to it, it becomes unreachable, and the garbage collector frees its memory automatically.

Features of the Garbage Collector:

  • Automatic process — no need to free memory manually
  • Time consuming and unpredictable
  • Runs in the background on its own timing

In practice, we do not worry about memory management in Java (except in very specific performance cases).

The first pillar: abstraction

Abstraction is the first pillar of OOP.

In programming, a type describes two things:

  1. What the variable looks like: its structure (its data)
  2. What we can do with it: its operations

In many languages, we are limited to predefined types (int, boolean, etc.). But in OO languages, we can define new types by creating classes.

An Alarm class is a new type that describes:

  • What an alarm looks like (its fields: message, active, etc.)
  • What we can do with it (its methods: turnOn(), getReport(), etc.)

This is abstraction: creating types that correspond exactly to the domain of our application. If we are building an alarm tracking system, we create an Alarm class. If we build a monetary system, we create Money and Budget classes. If we follow fruits, we create a Banana class. These abstract types allow us to write code in the vocabulary of the problem domain.

Module 4 Summary

  • Define classes: fields to store data, methods to do things
  • Constructors: initialize the object on creation
  • Overloading: several methods (or constructors) with the same name but different arguments
  • Default constructor: automatically generated by Java if no constructor is defined
  • Garbage Collector: frees memory automatically — no manual management required
  • Abstraction: the first pillar of OOP — defining new types adapted to the problem domain

5. Hiding Information — Encapsulation

Total module duration: 24m 51s

Access modifiers

access modifiers control which codes can see and use members of a class. This is one of the most important features of Java.

Access levels:

ModifierVisibility
publicAccessible from anywhere
protectedAccessible from the same package and subclasses
(none)Package-private — accessible only from the same package
privateAccessible only from inside the classroom

Syntax:

public class Alarm {
    private boolean active;       // Uniquement dans cette classe
    private String message;       // Uniquement dans cette classe
    
    public void turnOn() { ... }  // Accessible partout
    
    private void stopSnoozing() { ... }  // Uniquement dans cette classe
}

Applies to:

  • Entire classes (package or public visibility)
  • Fields
  • Methods
  • Constructors

Return to our code

The Alarm class at this stage of training:

  • Field active: the current state of the alarm
  • Field message: message set at creation
  • turnOn() and turnOff() methods to change state
  • getReport() and getReport(boolean) methods to get the report
  • sendReport() method to display the report

Snooze the alarm

New functionality: being able to snooze an alarm (like on a telephone). When an alarm is active, you can snooze it — it becomes silent for 5 minutes. After this delay, it rings again (getReport returns the message).

Behavior:

  • snooze(): puts the alarm in snooze mode for 5 minutes
  • During snooze: getReport() returns an empty string (as if the alarm was inactive)
  • After 5 minutes: getReport() returns the message again

Implement snoozing

package com.pluralsight.alarms;

import java.time.LocalDateTime;

public class Alarm {
    private boolean active;
    private final String message;
    private LocalDateTime snoozeUntil;

    public Alarm(String message) {
        this.message = message;
        stopSnoozing();
    }

    public LocalDateTime getSnoozeUntil() {
        return snoozeUntil;
    }
    
    public void snooze() {
        if (active)
            snoozeUntil = LocalDateTime.now().plusMinutes(5);
    }

    public boolean isSnoozing() {
        return snoozeUntil.isAfter(LocalDateTime.now());
    }

    public String getMessage() {
        return message;
    }

    public void turnOn() {
        active = true;
        stopSnoozing();
    }

    public void turnOff() {
        active = false;
        stopSnoozing();
    }

    public String getReport() {
        return getReport(false);
    }

    public String getReport(boolean uppercase) {
        if (active && !isSnoozing()) {
            if (uppercase)
                return message.toUpperCase();
            else
                return message;
        } else
            return "";
    }

    public void sendReport() {
        System.out.println(getReport(true));
    }

    private void stopSnoozing() {
        snoozeUntil = LocalDateTime.now().minusSeconds(1);
    }
}

Technical points:

  • Java LocalDateTime: class to represent a specific moment in time
  • LocalDateTime.now().plusMinutes(5): now + 5 minutes
  • snoozeUntil.isAfter(LocalDateTime.now()): checks if the end of snooze is in the future
  • stopSnoozing(): initializes snoozeUntil to 1 second in the past (so isSnoozing() returns false)

Clean code tip: instead of if (expr) return true; else return false;, we simply write return expr;

Test snoozing

Testing functionality by adding main directly to the class — a valid Java technique for quickly testing a class:

public static void main(String[] args) throws InterruptedException {
    Alarm alarm = new Alarm("Temperature too high");
    alarm.turnOn();
    alarm.snooze();
    Thread.sleep(60000 * 6);  // Attend 6 minutes
    alarm.sendReport();        // Devrait afficher le message (snooze terminé)
}

To test, reduce the snooze time to 5 seconds rather than 5 minutes.

Our code is not enough

Analyze cases where things can go wrong:

Scenario 1: Create alarm → snooze → activate. The alarm does not ring for a few minutes because it is still in snooze mode, even though it has just been activated. Inconsistent!

Scenario 2: Create alarm → activate → snooze → deactivate → reactivate. The alarm is still in snooze mode after resetting. Inconsistent!

Solution: Add checks:

  • At turnOn(): call stopSnoozing() to cancel snooze
  • At turnOff(): call stopSnoozing() to cancel snooze
  • In snooze(): check that the alarm is active before snoozing

But a problem persists: external code can still bypass these protections.

A reason to control access

The problem: Even with all the checks in the methods, it is easy to put the alarm into an inconsistent state:

alarm.active = false;  // Bypass des méthodes turnOn/turnOff !
// Maintenant l'alarme peut être inactive ET en mode snooze

This code bypasses carefully crafted methods and accesses the field directly. The solution: make fields private:

private boolean active;
private LocalDateTime snoozeUntil;

Now this code no longer compiles:

alarm.active = false;  // Erreur de compilation ! Field privé

The private keywords also serve as documentation: they clearly indicate that these fields are not supposed to be accessed from the outside.

Benefits:

  • Safer: cannot put object in inconsistent state
  • Easier to understand: the interface is clear
  • Easier to modify: implementation can change without affecting client code

Interface and implementation

By making fields private and methods public, we clearly separate:

  • The interface (public part): methods and fields accessible from the outside
  • Implementation (private part): internal details of the class
public class Alarm {
    // IMPLÉMENTATION (privée)
    private boolean active;
    private final String message;
    private LocalDateTime snoozeUntil;
    private void stopSnoozing() { ... }

    // INTERFACE (publique)
    public Alarm(String message) { ... }
    public void snooze() { ... }
    public boolean isSnoozing() { ... }
    public String getMessage() { ... }
    public void turnOn() { ... }
    public void turnOff() { ... }
    public String getReport() { ... }
    public String getReport(boolean uppercase) { ... }
    public void sendReport() { ... }
}

Advantage of this separation: The implementation can be modified (renamed, refactored, optimized) without the client code noticing it — provided that the interface remains stable.

Fields vs Properties

Common Java convention for fields: make them private and provide accessor methods if necessary:

  • Getter: method which returns the value of a field
    public LocalDateTime getSnoozeUntil() {
        return snoozeUntil;
    }
    
  • Setter: method which assigns a value to a field
    public void setSnoozeUntil(LocalDateTime time) {
        snoozeUntil = time;
    }
    

Naming convention:

  • Getter: get + field name in UpperCamelCase → getMessage(), getSnoozeUntil()
  • For Boolean: is + name → isSnoozing(), isActive()
  • Setter: set + field name → setMessage(), setSnoozeUntil()

Getter without setter: if we want the field to be readable but not modifiable from the outside, we provide a getter but not a setter.

Advantage of getters/setters: From the client’s point of view, there is no difference between getMessage() (which returns a field) and isSnoozing() (which calculates a value) — both are properties of the alarm. The internal implementation can change without the interface changing.

The second pillar: encapsulation

Encapsulation is the second pillar of OOP. This is the concept of hiding information inside objects.

Benefits of encapsulation:

  1. Classes that are easier to understand and use: a class where all the members are public is like a car where you see all the engine components at the same time. Difficult to understand! By hiding internal details, we only see what is relevant.

  2. Classes more difficult to misuse: when internal fields are private, it becomes impossible (or at least much more difficult) to put the object in an inconsistent state.

  3. Facilitates future modifications: All software changes and evolves. When we know that no external code depends on the internal implementation, we can modify this implementation with confidence.

These benefits amplify: the larger the software system, the more critical encapsulation becomes.

Module 5 Summary

  • Access modifiers: public, private, protected, package-private (none)
  • Interface/implementation separation thanks to private and public
  • Convention: private fields, access via getters/setters if necessary
  • Encapsulation: the second pillar of OOP — hiding information for classes that are safer, more understandable, and easier to modify

6. Design with abstraction and encapsulation

Total module duration: 8m 9s

What design is

Object-oriented design concerns the structure of classes and their interactions.

When running a large Java application, you essentially see a network of interconnected objects. How do we decide on these connections? These are decisions made at design time when writing classes. The class structure determines how objects interact at runtime.

Design is the translation from the problem domain (the specifications) to the solution domain (the code). It is a non-trivial process that improves with experience and study.

From problem to code

The design process:

When we receive specifications (from a client, from an analyst), we must transform them into Java code. This translation goes through several stages:

  1. Identify concepts in specifications: nouns often become classes, verbs often become methods
  2. Design classes and their relationships — UML class diagrams
  3. Find out the necessary classes which are not mentioned in the specifications but which the system needs

A typical Java system may have hundreds or thousands of classes. Deciding which ones and how they interact is a lot of work.

Some guidelines

Guideline 1: Make things as private as possible

If a class, field, method or constructor does not need to be public, keep it local. Make it private or at least package-private. Ask yourself: “As private as possible?” »

The interface of a class must be as small as possible. The smaller the interface, the easier the class is to use and harder to misuse.

Guideline 2: Encapsulate fields — make them private

Fields that change unexpectedly are a common source of bugs. Make fields private by default. If external code needs to read a field, write a getter. If external code needs to modify a field, write a setter — but only if necessary.

Warning: systems where each object can read and write the fields of each other object (via getters and setters everywhere) become very difficult to manage. Don’t create getters/setters reflexively, only when necessary.

Guideline 3: Make fields final when possible

If a field does not need to change, declare it final. The more things that cannot be changed, the fewer possible bugs there are. These improvements accumulate.

Even better: if the field is both final AND contains an immutable object (like a String), it can never change and can never surprise you.


7. Inheritance

Total module duration: 30m 28s

Another type of alarm

Context: New feature request — a special type of alarm with the same functionality as a normal alarm, but with a priority (integer) attached.

Options:

  1. Add priority to the Alarm class: bad idea — not all alarms have a priority. The class would become a “catch-all”.
  2. Copy and paste the code: always a bad idea (duplication, difficult maintenance).
  3. Inheritance: create a new class that inherits from Alarm and adds priority.

Inheritance allows a class to inherit all the fields and methods of another class, and to add its own functionality.

Subclass Alarm

package com.pluralsight.alarms;

public class PrioritizedAlarm extends Alarm {

    private final int priority;

    PrioritizedAlarm(String message, int priority) {
        super(message);  // Appel du constructor de la superclasse
        this.priority = priority;
    }

    public int getPriority() {
        return priority;
    }
}

Terminology:

  • extends: Java keyword for inheritance
  • PrioritizedAlarm: the subclass or child class
  • Alarm: the superclass or parent class
  • PrioritizedAlarm inherits all public/protected fields and methods from Alarm

Superclasses and constructors

Problem: when creating a subclass object, who calls the superclass constructor?

Rule: If the superclass does not have a default constructor (no arguments), the constructor of the subclass must explicitly call the constructor of the superclass.

PrioritizedAlarm(String message, int priority) {
    super(message);  // OBLIGATOIRE : appel du constructor de Alarm
    this.priority = priority;
}
  • super(message): calls the constructor of the superclass with the message
  • Must be the first statement of the constructor
  • If the superclass has a default constructor, Java calls it automatically

Initialization sequence:

  1. Memory allocated for the object (including superclass fields)
  2. Constructor of superclass (super(message)) executed → initializes message
  3. The rest of the subclass constructor executed → initializes priority

Method overriding

New request: a HighVisibilityAlarm — like a normal alarm, but the report ends with an exclamation point.

package com.pluralsight.alarms;

public class HighVisibilityAlarm extends Alarm {

    public HighVisibilityAlarm(String message) {
        super(message);
    }

    @Override
    public String getReport(boolean uppercase) {
        String report = super.getReport(uppercase);  // Appel de la version superclasse
        if (report.isEmpty())
            return report;
        else
            return report + "!";  // Ajout de l'exclamation
    }
}

Key points:

  • Override: redefine a method of the superclass in the subclass
  • @Override: annotation which asks the compiler to check that an existing method is being overridden
  • super.getReport(uppercase): call version of method in superclass — avoid duplication
  • If we create a HighVisibilityAlarm and call getReport, the overridden version is called

Encapsulation meets inheritance

Problem: if we try to access the private fields of the superclass from the subclass, the code does not compile:

// Dans HighVisibilityAlarm
public String getReport(boolean uppercase) {
    if (active)  // Erreur ! 'active' est private dans Alarm
        ...
}

Solutions:

  1. Public Getters: expose fields via public methods
  2. The protected modifier:
// Dans Alarm
protected boolean active;  // Visible dans Alarm ET ses sous-classes

Warning: protected is not just more permissive than private for subclasses. A protected member is visible:

  • In the same class
  • In the same package (like package-private)
  • In subclasses, even in other packages

It’s like a more permissive version of package-private, not just an extension for subclasses.

Test subclass

public static void main(String[] args) {
    HighVisibilityAlarm alarm = new HighVisibilityAlarm("Test message");
    alarm.turnOn();
    alarm.sendReport();  // Affiche "TEST MESSAGE!"
}

What is happening:

  1. We call sendReport() — method inherited from Alarm
  2. sendReport() calls getReport(true) — but getReport is overridden in HighVisibilityAlarm
  3. The overridden version is called → displays the message in uppercase with !

Methods inherited from the superclass automatically call overridden versions of the subclass. This is the basis of polymorphism.

Clean up subclass

Before cleaning (duplicated code — copy and paste from superclass):

// Duplication de logique depuis Alarm
if (active && !isSnoozing()) {
    if (uppercase)
        return message.toUpperCase() + "!";
    ...
}

After cleaning (use of super):

@Override
public String getReport(boolean uppercase) {
    String report = super.getReport(uppercase);
    if (report.isEmpty())
        return report;
    else
        return report + "!";
}

Benefit: If the logic of getReport in Alarm changes, the subclass automatically adapts.

The @Override annotation:

  • Java Compiler Directive
  • Checks that a method of the superclass is overridden
  • If the name is incorrect (typo), the compiler reports the error
  • Recommended: always use @Override when overriding a method

The single-root hierarchy

Implicit class superclass: If a class does not specify a superclass with extends, it implicitly inherits from java.lang.Object.

           Object
              |
           Alarm
          /       \
PrioritizedAlarm  HighVisibilityAlarm

Key points:

  • All Java classes ultimately inherit from Object
  • Java only allows inheritance from one superclass at a time (single inheritance)
  • Object is the only class without a superclass
  • This structure forms a large hierarchy with Object at the root

Object methods (available on all Java objects):

  • toString(): textual representation of the object
  • equals(Object): equality comparison
  • hashCode(): hash code for collections
  • getClass(): object class
  • clone(): copy
  • wait(), notify(), notifyAll(): for thread synchronization

Object has a constructor with no arguments, so Java can call it automatically. This is why we didn’t need to call super() in the Alarm constructor — Java adds it implicitly.

The final keyword revisited

The final keyword can be applied to three different things:

ApplicationMeaning
final on a variable/fieldCannot be reassigned
final on a methodCannot be overridden in subclasses
final on a classCannot be inherited
// Méthode finale — ne peut être overridée
public final String getReport(boolean uppercase) { ... }

// Classe finale — ne peut avoir de sous-classes
public final class Alarm { ... }

Why final?: To guarantee specific behavior, for security reasons, or for performance optimizations.

Use sparingly: too many finals can unnecessarily restrict flexibility.

Sealed classes

New in Java 17: sealed classes.

A sealed class controls exactly which classes can inherit from it:

public sealed class Alarm permits PrioritizedAlarm, HighVisibilityAlarm {
    ...
}
  • sealed: the class is sealed
  • permits: explicit list of authorized subclasses
  • Subclasses of a sealed class must be sealed, non-sealed, or final
public final class PrioritizedAlarm extends Alarm { ... }
public final class HighVisibilityAlarm extends Alarm { ... }

When to use: advanced use cases (pattern matching, ADTs). The instructor warns against overuse: a hierarchy that we think is “complete” may turn out to be incomplete later.

Example trap: a Verb class with subclasses RegularVerb and IrregularVerb, sealed because “all English verbs are regular or irregular”. In reality, grammars evolve and new cases may appear.

Module 7 Summary

  • extends: keyword for inheritance in Java
  • super(args): call of the constructor of the superclass (required if the superclass does not have a default constructor)
  • super.methode(): call to a superclass method (useful in overrides)
  • @Override: annotation to check an override
  • All Java classes inherit from Object
  • protected modifier: visible in class, package, and subclasses
  • final on method/class: controls override/inheritance possibilities
  • Sealed classes (Java 17): explicit control of authorized subclasses

8. Polymorphism

Total module duration: 27m 19s

The “Is-a” relationship

Polymorphism is the fourth pillar of OOP. It is a complex concept, approached gradually.

The Is-a (is-a) relationship: In an inheritance hierarchy, a subclass is a special case of the superclass.

Animal
  |
Cat

If Cat is a subclass of Animal, then freaky (an object of class Cat) is:

  • A Cat (instance of Cat)
  • An Animal (instance of Animal)
  • An Object (instance of Object)

An object has multiple types thanks to the inheritance hierarchy.

Impact: A subclass can add things to the superclass, possibly modify some behavior, but can never reduce the superclass’s interface.

Example: if a method is public in the superclass, it cannot be made less visible in the subclass (eg: protected or without modifier). Java would refuse to compile.

Test the Is-a relationship

If HighVisibilityAlarm is an Alarm, then it must have everything an Alarm has — all of its public methods. And this is the case thanks to inheritance.

Rule: A subclass can:

  • ✅ Add members (new methods, new fields)
  • ✅ Change the behavior of methods (via override)
  • ✅ Make a method more visible (eg: from protected to public)
  • ❌ Delete inherited members
  • ❌ Reduce the visibility of a method (eg: from public to protected)

Upcasting

Upcasting: assign a reference to a variable whose type is a superclass of the object.

// Upcasting : PrioritizedAlarm → Alarm
PrioritizedAlarm pa = new PrioritizedAlarm("Temp high", 1);
Alarm alarm = pa;  // Upcasting — fonctionne grâce à la relation Is-a

Why it works: because a PrioritizedAlarm is also an Alarm (Is-a relationship). Java therefore agrees to store the reference in an Alarm type variable.

Upcasting also works when switching to a method:

static void printAlarm(Alarm alarm) {
    System.out.println(alarm.getReport());
}

// On peut passer n'importe quelle sous-classe d'Alarm
printAlarm(new PrioritizedAlarm("Temp", 1));       // OK
printAlarm(new HighVisibilityAlarm("Pressure"));   // OK

The most general version: any Java object can be upgraded to Object:

static void printAny(Object obj) {
    System.out.println(obj.toString());
}
// Fonctionne avec n'importe quel objet Java !

“Upcast” means: type conversion to the superclass — like when casting a float to an int, except here it is to the superclass.

Polymorphism in action

// Après upcasting
Alarm alarm = new HighVisibilityAlarm("Test");
alarm.turnOn();
String report = alarm.getReport(true);  // Quelle version est appelée ?

Question: getReport is defined in Alarm, but also overridden in HighVisibilityAlarm. Which one is called?

Answer: The version of HighVisibilityAlarm — the effective behavior depends on the actual type of the object, not the type of the variable.

This is the polymorphism: the object is upgraded to Alarm, we “forget” that it is HighVisibilityAlarm, but it still behaves like a HighVisibilityAlarm.

Example with animals:

Let’s imagine an Animal class with a vocalize() method and subclasses Cat, Dog, etc. :

class AnimalLaboratory {
    private SoundRecorder recorder;
    private SoundArchive archive;

    void captureAnimalSound(Animal animal) {
        String sound = animal.vocalize();  // Polymorphique !
        recorder.record(sound);
        archive.save(sound);
    }
}

captureAnimalSound does not need to know what type of animal is being passed to it. She calls vocalize() and gets the appropriate sound — a cat meows, a dog barks. This is polymorphism in action.

Conclusion: polymorphism allows you to write code that talks to superclasses but that behaves differently depending on the subclasses passed.

Write extensible code

package com.pluralsight.alarms;

import java.util.ArrayList;
import java.util.List;

public class Dashboard {
    private final List<Alarm> allAlarms = new ArrayList<>();

    public void add(Alarm alarm) {
        alarm.turnOn();
        allAlarms.add(alarm);
    }

    public void printBigReport() {
        for (Alarm alarm: allAlarms)
            System.out.println(alarm.getReport(true));
    }

    public static void main(String[] args) {
        Dashboard dashboard = new Dashboard();

        dashboard.add(new PrioritizedAlarm("Temperature too high", 42));
        dashboard.add(new HighVisibilityAlarm("Pressure too low"));
        dashboard.add(new TimeSensitiveAlarm("Never mind the other alarms, you're late for dinner"));

        dashboard.printBigReport();
    }
}

What this code does:

  • Dashboard stores Alarm in a list
  • add() takes an Alarm — but thanks to upcasting, it works with any subclass
  • printBigReport() loops through all alarms and calls getReport(true) — thanks to polymorphism, each alarm type produces its own report

The magic: Dashboard is written to work with Alarm. It works automatically with all subclasses, even those that don’t yet exist when Dashboard was written. This is called extensible code.

Polymorphism is everywhere

Classic example: System.out.println

HighVisibilityAlarm alarm = new HighVisibilityAlarm("Hello!");
System.out.println(alarm);  // Appelle toString() sur l'objet

println(Object) calls toString() on the object. toString() is defined in Object (the root of any Java hierarchy), so all objects have this method.

Override of toString():

// Dans la classe Alarm
@Override
public String toString() {
    return getReport();  // Retourne le rapport comme représentation textuelle
}

Now System.out.println(alarm) displays the alarm report — each subclass can provide its own textual representation.

This is polymorphism: println uses toString() on any object, and each class can override toString() to return its own representation.

Downcasting

The problem: after upcasting, we “forget” the specific type. Sometimes you want this guy back.

Alarm alarm = new PrioritizedAlarm("Temp", 1);
// alarm.getPriority(); // Erreur ! 'alarm' est de type Alarm, pas PrioritizedAlarm

// Solution : downcasting
PrioritizedAlarm pa = (PrioritizedAlarm) alarm;  // Cast explicite
System.out.println(pa.getPriority());             // OK !

Risk: if the object is not really of this type, we obtain a ClassCastException:

Alarm alarm = new HighVisibilityAlarm("Hello");
PrioritizedAlarm pa = (PrioritizedAlarm) alarm;  // ClassCastException !
// L'objet est un HighVisibilityAlarm, pas un PrioritizedAlarm
package com.pluralsight.alarms;

public class Downcasting {
    public static void main(String[] args) {
        Alarm alarm = new HighVisibilityAlarm("Hello, world!");

        if (alarm instanceof PrioritizedAlarm) {
            PrioritizedAlarm prioritizedAlarm = (PrioritizedAlarm) alarm;
            System.out.println(prioritizedAlarm.getPriority());
        }
    }
}

The instanceof operator: checks if an object is an instance of a class before downcasting:

if (alarm instanceof PrioritizedAlarm) {
    PrioritizedAlarm pa = (PrioritizedAlarm) alarm;  // Sûr !
}

Modern Java syntax (pattern matching, Java 16+):

if (alarm instanceof PrioritizedAlarm pa) {
    System.out.println(pa.getPriority());  // pa est directement disponible
}

Recommendation: Downcasting is often a sign that the design can be improved. Before downcasting, ask yourself if polymorphism can solve the problem.

Module 8 Summary

  • The Is-a relationship: a subclass is a special case of the superclass. An object has several types.
  • Upcasting: assign an object to a superclass variable. Occurs automatically.
  • Polymorphism: when calling a method via a superclass reference, it is the version of the actual subclass that is called.
  • Extensible code: thanks to polymorphism, we can write code that works with subclasses that do not yet exist.
  • Downcasting: cast to a more specific type. Risk of ClassCastException.
  • instanceof: check type before downcasting.

Inheritance and polymorphism are the defining features of the Java language, but there is another mechanism for polymorphism without class inheritance: interfaces.


9. Interfaces

Total module duration: 25m 52s

A Deeper Look at Alarms

Context: New request — color code alarms. HighVisibilityAlarm should be orange, PrioritizedAlarm green.

Initial approach: add a getColor() method in the Alarm superclass and override it in subclasses.

// Dans Alarm :
public Color getColor() {
    return null;  // Que retourner pour une Alarm de base ?
}

// Dans HighVisibilityAlarm :
@Override
public Color getColor() {
    return Color.ORANGE;
}

// Dans PrioritizedAlarm :
@Override
public Color getColor() {
    return Color.GREEN;
}

Problem: what to return in the getColor method of Alarm? Clients never create a base Alarm — only HighVisibilityAlarm or PrioritizedAlarm. The method in Alarm is never actually called.

Abstract Classes

The solution: abstract classes and abstract methods:

// Méthode abstraite : déclaration sans corps
public abstract Color getColor();

Abstract class: if a class contains an abstract method, the class itself must be declared abstract:

public abstract class Alarm implements Widget, PersistentObject {
    ...
    public abstract Color getColor();  // Pas d'implémentation — les sous-classes s'en chargent
    ...
}

Consequences:

  • We cannot create an instance of an abstract class: new Alarm(...) → error
  • We can create concrete subclasses and upgrade them to Alarm
  • Subclasses must implement all abstract methods (otherwise they must also be abstract)
// Dans HighVisibilityAlarm :
@Override
public Color getColor() {
    return Color.ORANGE;
}

The complete Alarm class with interfaces (module 9):

package com.pluralsight.alarms;

import java.awt.*;
import java.time.LocalDateTime;

public abstract class Alarm implements Widget, PersistentObject {
    protected boolean active;
    private final String message;
    private LocalDateTime snoozeUntil;

    public Alarm(String message) {
        this.message = message;
        stopSnoozing();
    }

    @Override
    public String getHelpText() {
        return "I'm an alarm. You can turn me on or off and snooze me";
    }

    @Override
    public void save() {
        System.out.println("Saving...");
    }

    public abstract Color getColor();

    // ... autres méthodes
}

The hierarchy problem

New context: the application is evolving. In addition to alarms, there will be widgets (gauges, switches, etc.) on the control panel. And some widgets must be able to be saved in the cloud (persistent objects).

The Challenge: These new requirements do not fit well into a single hierarchy. A widget like an alarm must:

  • Being treated as an alarm (by the code that handles alarms)
  • Being treated as a widget (by the code that manages the dashboard)
  • Being treated as a persistent object (by saving code)

An object can play multiple roles in a real system, depending on the context in which it is used. This is difficult to model with a single inheritance hierarchy.

Play multiple roles

Java only supports inheritance from a single superclass (simple inheritance). But a class can implement as many interfaces as it wants.

An interface: a contract that specifies which methods an object should respond to, without specifying how.

package com.pluralsight.alarms;

public interface Widget {
    String getHelpText();  // public et abstract par défaut
}
package com.pluralsight.alarms;

public interface PersistentObject {
    void save();  // public et abstract par défaut
}

Implementation of multiple interfaces:

public abstract class Alarm implements Widget, PersistentObject {
    
    @Override
    public String getHelpText() {
        return "I'm an alarm. You can turn me on or off and snooze me";
    }

    @Override
    public void save() {
        System.out.println("Saving...");
    }
    
    // ... reste de la classe
}

The magic of interfaces: an object can now play several roles:

package com.pluralsight.alarms;

public class Program {
    public static void main(String[] args) {
        HighVisibilityAlarm alarm = new HighVisibilityAlarm("Hello there!");
        activate(alarm);       // Traité comme une Alarm
        printHelpText(alarm);  // Traité comme un Widget
        saveItTwice(alarm);    // Traité comme un PersistentObject
    }

    private static void activate(Alarm alarm) {
        alarm.turnOn();
    }

    private static void printHelpText(Widget widget) {
        System.out.println(widget.getHelpText());
    }

    private static void saveItTwice(PersistentObject persistentObject) {
        persistentObject.save();
        persistentObject.save();
    }
}

The same HighVisibilityAlarm object can be passed to methods that expect very different types (Alarm, Widget, PersistentObject). This is the power of interfaces.

Interface constraints

Java does not support multiple inheritance of classes for good reasons:

  • Diamond problem: if two superclasses have a method with the same name and different implementations, which version to use in the subclass?
  • Field conflict with the same name
  • These issues generate hard-to-find bugs

Interfaces provide some benefits of multiple inheritance without the problems:

  • They cannot have fields (except static final)
  • They cannot inherit classes
  • All their methods are public and abstract (except default and static)

What is legal:

class A extends B {}                  // Classe hérite d'une classe
class A implements I1, I2 {}          // Classe implémente plusieurs interfaces
interface I1 extends I2 {}            // Interface hérite d'une interface

What is illegal:

class A extends B, C {}               // Héritage multiple de classes — INTERDIT
interface I1 extends Class {}         // Interface hérite d'une classe — INTERDIT

Evolution of interfaces: over time, Java has relaxed certain constraints. Interfaces can now have:

  • default methods: methods with a default implementation
  • static methods
  • static final fields (constants)

But in their current use, interfaces remain abstract contracts.

Module 9 Summary

  • Abstract classes: classes that cannot be instantiated directly. They can have abstract (bodyless) methods that subclasses must implement.
  • Interfaces: like extreme abstract classes — no implementation, no fields (except constants). Define a contract.
  • implements: keyword to implement an interface
  • A class can implement multiple interfaces → an object can play multiple roles
  • Polymorphism applies to interfaces: an interface reference can point to any object that implements that interface
  • Interfaces allow polymorphism without class inheritance

10. Design with inheritance and polymorphism

Total module duration: 10m 34s

More design thinking

This module presents some guidelines on the use of inheritance and polymorphism in the design of OO systems. These rules of thumb help beginners avoid common mistakes.

Inheritance concerns upcasting

Fundamental principle: inheritance exists primarily for upcasting and polymorphism.

The real reason to create a hierarchy:

      Alarm
     /     \
PrioritizedAlarm  HighVisibilityAlarm

This is to be able to write code that talks to Alarm and works with all its subclasses — including future ones.

Another use of inheritance: sharing code between subclasses. If multiple subclasses share logic (e.g. snoozing in Alarm), it makes sense to put that code in the superclass. But if we use inheritance only to share code — without ever updating — there is a lighter alternative: delegation.

Delegation: instead of inheriting from a class to use its methods, we create an object of this class like field and we delegate the calls to this object. It’s generally more flexible.

Guideline: Use inheritance when you need upcasting and polymorphism. If it’s just for reusing code, consider delegation instead.

Do not check types with instanceof

Bad practice: instanceof strings to determine behavior:

// MAUVAISE pratique — à éviter
static boolean isUrgent(Alarm alarm) {
    if (alarm instanceof PrioritizedAlarm) {
        PrioritizedAlarm pa = (PrioritizedAlarm) alarm;
        return pa.getPriority() > 2;
    } else if (alarm instanceof HighVisibilityAlarm) {
        return true;  // Toujours urgent
    } else if (alarm instanceof TimeSensitiveAlarm) {
        TimeSensitiveAlarm tsa = (TimeSensitiveAlarm) alarm;
        // ... vérification spécifique
    } else {
        return false;
    }
}

Problems:

  1. Difficult to read: complex sequence of conditions
  2. Fragile during modifications: if you add a new subclass, you have to update all the instanceof in the code — you may forget some
  3. Violates the principle of polymorphism: polymorphism is precisely designed to avoid this kind of manual type checking

Best practice: use polymorphism. Add an isUrgent() method in Alarm and override it in each subclass:

// Dans Alarm :
public boolean isUrgent() { return false; }  // Ou abstract

// Dans PrioritizedAlarm :
@Override
public boolean isUrgent() { return priority > 2; }

// Dans HighVisibilityAlarm :
@Override
public boolean isUrgent() { return true; }

Rule of thumb: When tempted to use instanceof, ask yourself whether polymorphism might solve the problem more elegantly.

Module 10 Summary

  1. Use inheritance for upcasting: inheritance is mainly at the service of polymorphism. If it’s just to share code, consider delegation.
  2. Avoid instanceof strings: replace with polymorphism. Add specific behaviors directly to the relevant classes.

These guidelines are starting points. With experience, you will develop your own judgment about when to apply these rules and when to deviate from them.


11. The static keyword

Total module duration: 13m 15s

Static members

The static keyword is the least object-oriented feature in Java. It is important to know it precisely to know when not to use it.

Normal (non-static) methods: are called on an object. Static methods: are called directly on the class, without creating an object.

// Méthode normale
Alarm alarm = new Alarm("Test");
alarm.turnOn();           // Appelée sur un objet

// Méthode static
Alarm.getDocumentation(); // Appelée sur la classe directement

Why main is static?: The JVM must call main at startup, but cannot first create an object of the class. main is therefore static.

Example:

package com.pluralsight.alarms;

public class Alarm {
    public static final String DOCUMENTATION = "Use turnOn() to activate.";

    protected boolean active;

    public static String getDocumentation() {
        return DOCUMENTATION;
    }

    public void turnOn() {
        active = true;
    }
}

static methods are essentially traditional procedures — they are defined in a class, but the class mainly serves as a namespace.

Mix static and non-static

Interaction rules:

SinceCan accessExample
Non-static methodOther non-static methods/fieldsthis.active, turnOn()
Non-static methodStatic methods/fieldsAlarm.DOCUMENTATION (or just DOCUMENTATION if in the same class)
Static methodOther static methods/fieldsDOCUMENTATION
Static method❌ Non-static methods/fields directlyError !
public static void doSomethingStatic() {
    // turnOn();  // Erreur ! turnOn() est non-static
    
    Alarm alarm = new Alarm("Test");
    alarm.turnOn();  // OK ! On a créé un objet
    
    System.out.println(DOCUMENTATION);  // OK ! DOCUMENTATION est static
}

Important: you cannot call a non-static method from a static method without creating an object first.

static methods cannot be overridden — no polymorphism with static methods.

Global variables and global constants

Field static public → global variable:

public static String documentation = "...";  // Variable globale — à éviter !

Any code can read and write this field. It’s essentially a global variable — and global variables are notorious for causing hard-to-track bugs.

Best practice: if the field must be visible everywhere and must not change, make it final:

public static final String DOCUMENTATION = "...";  // Constante globale — OK

Convention: Constant names are all capitals with underscores: MAX_SIZE, DEFAULT_TIMEOUT, DOCUMENTATION.

A global constant (public static final) is much safer than a global variable — no one can modify it.

Other uses of static

package com.pluralsight.alarms;

public class Example {
    public Example() {
        System.out.println("Example constructor");
    }
}

Initializers (initialization blocks):

class Example {
    {
        // Bloc d'initialisation — exécuté AVANT le constructor
        System.out.println("Initializer block");
    }
    
    Example() {
        System.out.println("Constructor");
    }
}

Output:

Initializer block
Constructor

Static initializers:

class Example {
    static {
        // Bloc d'initialisation static — exécuté UNE SEULE FOIS, au premier chargement de la classe
        System.out.println("Static initializer");
    }
}

The static block is executed the first time the class is used (to create an object, to access a static member, etc.). Useful for complex initializations of static fields (eg: loading a value from a file).

Static inner classes: Classes defined inside other classes with the static modifier. Their behavior differs from non-static inner classes.

Beware of static members

Recommendation: use static members as little as possible.

Why: static is often a shortcut. When you don’t know how to structure your classes, it’s tempting to make everything static. But :

  1. Static members propagate: a static method easily calls other static methods, which create other static methods, etc.
  2. Static fields become global variables: difficult to trace, sources of bugs
  3. Non-testable code: static methods with side effects are difficult to test
  4. Procedural style: too much static brings back a procedural style (before OOP) with global procedures and global variables

The question to ask: Before declaring something static, can we use a more OOP approach instead?

OOP became popular precisely because people wanted to solve the problems of traditional procedural systems—complicated interweavings of procedures and global variables. static can bring us back to this mode of programming.


12. Conclusion of the training

Total module duration: 8m 55s

Topics not covered

This training is complete, but Java is big. Here are some important OOP features not covered here, which can be found in other Pluralsight training courses:

The enums:

enum AlarmVolume {
    SILENCED, LOW, MEDIUM, HIGH
    // Seulement ces 4 valeurs sont possibles — aucune autre
}

Enums are constrained classes that only admit a finite number of predefined values. They may have methods. Concept related to OOP but not strictly OOP.

Records (Java 14+):

record AlarmRecord(String message, boolean active) {
    // Classe immuable axée sur les données
    // Constructor, getters, equals, hashCode, toString générés automatiquement
}
// Pour "modifier" : créer une copie avec un field différent
AlarmRecord modified = new AlarmRecord(original.message(), true);

Records are immutable data-driven classes. All fields are public final. Useful for DTOs, value objects.

Generic types:

List<Alarm> alarms = new ArrayList<>();        // Liste typée pour Alarm
Map<String, Alarm> alarmMap = new HashMap<>(); // Map typée

Allows you to write code that works with different types in complete type safety. Widely used with collections.

Lambda expressions and streams (Java 8+):

alarms.stream()
      .filter(Alarm::isActive)
      .map(Alarm::getReport)
      .forEach(System.out::println);

Provide functional programming concepts in Java. Very useful for processing collections.

Training Summary

The training covered the four pillars of OOP:

1. Abstract:

  • Take an entity from the domain (like an alarm) and create a class for it
  • The class describes data (fields) and operations (methods)
  • Working with object references and their consequences
  • Define class members: fields, methods, constructors

2. Encapsulation:

  • Make class members private or public
  • Distinction between interface (public part) and implementation (private part)
  • Classes easier to understand, use, and modify
  • Access modifiers: public, private, protected, package-private

3. Inheritance:

  • Extend a class and add functionality to it
  • Override of methods with @Override
  • Keyword super to access the superclass
  • All Java classes inherit from Object (single root hierarchy)
  • final classes/methods, sealed classes (Java 17)

4. Polymorphism:

  • Write generic code that works with multiple types (subclasses)
  • Upcasting: assign an object to a superclass variable
  • The effective type of the object determines which method is called
  • Downcasting and instanceof
  • Abstract classes: partially defined classes
  • Interfaces: pure contracts, allow an object to play several roles

The static keyword: the least OOP feature in Java. Useful sparingly for utility methods, constants. To be avoided in excess so as not to fall back into a procedural style.


13. Appendix: Demo Code Files

Module 3 — Working with Objects

Alarm.java (module 3)

package com.pluralsight.alarms;

public class Alarm {
    final String message;
    boolean active;

    Alarm(String message) {
        this.message = message;
    }

    void turnOn() { active = true; }
    void turnOff() { active = false; }

    String getReport() { return getReport(false); }

    private String getReport(boolean uppercase) {
        if (active) {
            return uppercase ? message.toUpperCase() : message;
        } else return "";
    }

    void sendReport() { System.out.println(getReport(true)); }
}

UsingObjects.java

package com.pluralsight.alarms;

public class UsingObjects {
    public static void main(String[] args) {
        Alarm alarm = new Alarm("Temperature too high");
        System.out.println(alarm.active);   // false
        System.out.println(alarm.message);
        System.out.println(alarm.getReport());
        alarm.turnOn();
        System.out.println(alarm.active);   // true
        System.out.println(alarm.getReport());
    }
}

Aliasing.java

package com.pluralsight.alarms;

public class Aliasing {
    public static void main(String[] args) {
        Alarm alarm1 = new Alarm("");
        System.out.println(alarm1.active);  // false
        Alarm alarm2 = alarm1;              // Copie la référence
        alarm2.turnOn();
        System.out.println(alarm2.active);  // true
        System.out.println(alarm1.active);  // true aussi — même objet !
    }
}

NoAliasingForPrimitives.java

package com.pluralsight.alarms;

public class NoAliasingForPrimitives {
    public static void main(String[] args) {
        int integer1 = 41;
        int integer2 = integer1;  // Copie la valeur
        integer1++;
        System.out.println(integer1);  // 42
        System.out.println(integer2);  // 41 — pas affecté
    }
}

EqualityAndIdentity.java

package com.pluralsight.alarms;

public class EqualityAndIdentity {
    public static void main(String[] args) {
        String s1 = "test string";
        String s2 = "test string";
        System.out.println(s1.equals(s2));  // true  (égalité)
        System.out.println(s1 == s2);       // true aussi (string interning)
    }
}

FinalDoesNotMeanImmutable.java

package com.pluralsight.alarms;

public class FinalDoesNotMeanImmutable {
    public static void main(String[] args) {
        final Alarm alarm = new Alarm("");
        System.out.println(alarm.active);  // false
        alarm.turnOn();                    // La référence est finale, l'objet est modifiable
        System.out.println(alarm.active);  // true
    }
}

AutoboxingAndUnboxing.java

package com.pluralsight.alarms;

import java.util.ArrayList;
import java.util.List;

public class AutoboxingAndUnboxing {
    public static void main(String[] args) {
        int aPrimitive = 42;
        List<Object> myList = new ArrayList<>();
        myList.add(aPrimitive);          // Autoboxing : int → Integer
        Integer anObject = aPrimitive;   // Autoboxing
        int anotherPrimitive = anObject; // Unboxing : Integer → int
        System.out.println(anotherPrimitive); // 42
    }
}

Module 4 — Define your own classes

Alarm.java (module 4)

package com.pluralsight.alarms;

class Alarm {
    boolean active;
    final String message;

    Alarm(String message) {
        this.message = message;
    }

    void turnOn() { active = true; }
    void turnOff() { active = false; }
    String getReport() { return getReport(false); }

    String getReport(boolean uppercase) {
        if (active) {
            return uppercase ? message.toUpperCase() : message;
        } else return "";
    }

    void sendReport() {
        System.out.println(getReport(true));
    }
}

Program.java (module 4)

package com.pluralsight.alarms;

public class Program {
    public static void main(String[] args) {
        Alarm alarm = new Alarm("Temperature too high");
        alarm.turnOn();
        alarm.sendReport();
    }
}

Module 5 — Encapsulation

Alarm.java (module 5, encapsulation + snoozing)

package com.pluralsight.alarms;

import java.time.LocalDateTime;

public class Alarm {
    private boolean active;
    private final String message;
    private LocalDateTime snoozeUntil;

    public Alarm(String message) {
        this.message = message;
        stopSnoozing();
    }

    public LocalDateTime getSnoozeUntil() { return snoozeUntil; }

    public void snooze() {
        if (active)
            snoozeUntil = LocalDateTime.now().plusMinutes(5);
    }

    public boolean isSnoozing() {
        return snoozeUntil.isAfter(LocalDateTime.now());
    }

    public String getMessage() { return message; }

    public void turnOn() {
        active = true;
        stopSnoozing();
    }

    public void turnOff() {
        active = false;
        stopSnoozing();
    }

    public String getReport() { return getReport(false); }

    public String getReport(boolean uppercase) {
        if (active && !isSnoozing()) {
            return uppercase ? message.toUpperCase() : message;
        } else return "";
    }

    public void sendReport() {
        System.out.println(getReport(true));
    }

    private void stopSnoozing() {
        snoozeUntil = LocalDateTime.now().minusSeconds(1);
    }
}

Module 7 — Legacy

Alarm.java (module 7, with protected)

package com.pluralsight.alarms;

import java.time.LocalDateTime;

public class Alarm {
    protected boolean active;  // protected pour les sous-classes
    private final String message;
    private LocalDateTime snoozeUntil;

    public Alarm(String message) {
        this.message = message;
        stopSnoozing();
    }
    // ... mêmes méthodes que module 5
}

PrioritizedAlarm.java (module 7)

package com.pluralsight.alarms;

public class PrioritizedAlarm extends Alarm {
    private final int priority;

    PrioritizedAlarm(String message, int priority) {
        super(message);
        this.priority = priority;
    }

    public int getPriority() { return priority; }
}

HighVisibilityAlarm.java (module 7)

package com.pluralsight.alarms;

public class HighVisibilityAlarm extends Alarm {

    public HighVisibilityAlarm(String message) {
        super(message);
    }

    @Override
    public String getReport(boolean uppercase) {
        String report = super.getReport(uppercase);
        if (report.isEmpty())
            return report;
        else
            return report + "!";
    }
}

Module 8 — Polymorphism

Dashboard.java

package com.pluralsight.alarms;

import java.util.ArrayList;
import java.util.List;

public class Dashboard {
    private final List<Alarm> allAlarms = new ArrayList<>();

    public void add(Alarm alarm) {
        alarm.turnOn();
        allAlarms.add(alarm);
    }

    public void printBigReport() {
        for (Alarm alarm : allAlarms)
            System.out.println(alarm.getReport(true));
    }

    public static void main(String[] args) {
        Dashboard dashboard = new Dashboard();
        dashboard.add(new PrioritizedAlarm("Temperature too high", 42));
        dashboard.add(new HighVisibilityAlarm("Pressure too low"));
        dashboard.add(new TimeSensitiveAlarm("Never mind the other alarms, you're late for dinner"));
        dashboard.printBigReport();
    }
}

TimeSensitiveAlarm.java

package com.pluralsight.alarms;

import java.time.LocalTime;

public class TimeSensitiveAlarm extends Alarm {

    public TimeSensitiveAlarm(String message) {
        super(message);
    }

    @Override
    public String getReport(boolean uppercase) {
        String report = super.getReport(uppercase);
        if (report.isEmpty())
            return report;
        else
            return LocalTime.now() + ": " + report;
    }
}

Downcasting.java

package com.pluralsight.alarms;

public class Downcasting {
    public static void main(String[] args) {
        Alarm alarm = new HighVisibilityAlarm("Hello, world!");

        if (alarm instanceof PrioritizedAlarm) {
            PrioritizedAlarm prioritizedAlarm = (PrioritizedAlarm) alarm;
            System.out.println(prioritizedAlarm.getPriority());
        }
        // alarm n'est pas un PrioritizedAlarm → bloc if non exécuté
    }
}

Module 9 — Abstract Interfaces and Classes

Widget.java (interface)

package com.pluralsight.alarms;

public interface Widget {
    String getHelpText();  // public et abstract par défaut
}

PersistentObject.java (interface)

package com.pluralsight.alarms;

public interface PersistentObject {
    void save();  // public et abstract par défaut
}

Alarm.java (module 9 — abstract class, implements 2 interfaces)

package com.pluralsight.alarms;

import java.awt.*;
import java.time.LocalDateTime;

public abstract class Alarm implements Widget, PersistentObject {
    protected boolean active;
    private final String message;
    private LocalDateTime snoozeUntil;

    public Alarm(String message) {
        this.message = message;
        stopSnoozing();
    }

    @Override
    public String getHelpText() {
        return "I'm an alarm. You can turn me on or off and snooze me";
    }

    @Override
    public void save() {
        System.out.println("Saving...");
    }

    public abstract Color getColor();  // Méthode abstraite — implémentée par les sous-classes

    // ... autres méthodes
    private void stopSnoozing() {
        snoozeUntil = LocalDateTime.now().minusSeconds(1);
    }
}

HighVisibilityAlarm.java (module 9)

package com.pluralsight.alarms;

import java.awt.*;

public class HighVisibilityAlarm extends Alarm {

    public HighVisibilityAlarm(String message) {
        super(message);
    }

    @Override
    public Color getColor() {
        return Color.ORANGE;
    }

    @Override
    public String getReport(boolean uppercase) {
        String report = super.getReport(uppercase);
        return report.isEmpty() ? report : report + "!";
    }
}

PrioritizedAlarm.java (module 9)

package com.pluralsight.alarms;

import java.awt.*;

public class PrioritizedAlarm extends Alarm {
    private final int priority;

    public PrioritizedAlarm(String message, int priority) {
        super(message);
        this.priority = priority;
    }

    @Override
    public Color getColor() {
        return Color.GREEN;
    }

    public int getPriority() { return priority; }
}

Program.java (module 9)

package com.pluralsight.alarms;

public class Program {
    public static void main(String[] args) {
        HighVisibilityAlarm alarm = new HighVisibilityAlarm("Hello there!");
        activate(alarm);       // Upcasté vers Alarm
        printHelpText(alarm);  // Upcasté vers Widget
        saveItTwice(alarm);    // Upcasté vers PersistentObject
    }

    private static void activate(Alarm alarm) {
        alarm.turnOn();
    }

    private static void printHelpText(Widget widget) {
        System.out.println(widget.getHelpText());
    }

    private static void saveItTwice(PersistentObject persistentObject) {
        persistentObject.save();
        persistentObject.save();
    }
}

Module 11 — The static keyword

Alarm.java (module 11 — static members)

package com.pluralsight.alarms;

public class Alarm {
    public static final String DOCUMENTATION = "Use turnOn() to activate.";

    protected boolean active;

    public static String getDocumentation() {
        return DOCUMENTATION;
    }

    public void turnOn() {
        active = true;
    }
}

Example.java (demonstrates initializers)

package com.pluralsight.alarms;

public class Example {
    public Example() {
        System.out.println("Example constructor");
    }
}

Program.java (module 11)

package com.pluralsight.alarms;

public class Program {
    public static void main(String[] args) {
        new Example();
    }
}

14. References and key concepts

Glossary

English termFrench termDescription
ObjectObjectBundle of data (fields) and operations (methods)
ClassClassBlueprint (template) to create objects
FieldFieldData stored in an object
MethodMethodOperation that an object can perform
ReferenceReferencePointer to an object in memory
HeapHeapMemory area where objects are stored
ConstructorBuilderInitialization block called when creating an object
InheritanceLegacyMechanism by which one class inherits members from another
Superclass / Parent classSuperclassThe class we inherit
Subclass / Child classSubclassThe class that inherits
PolymorphismPolymorphismAbility of an object to behave differently depending on its real type
UpcastingUpcastingAssign an object to a superclass variable
DowncastingDowncastingCast to a more specific type (with risk of ClassCastException)
OverrideOverload (redefinition)Redefine an inherited method in a subclass
OverloadOverloadMultiple methods with the same name but different arguments
EncapsulationEncapsulationHide the implementation behind a public interface
AbstractAbstractCreate new types adapted to the domain
InterfaceInterfaceContract specifying which methods an object must respond to
Abstract classAbstract classClass that cannot be instantiated directly
Abstract methodsAbstract methodBodyless method — subclasses must implement it
Access editAccess Modifierpublic, private, protected, or none (package-private)
AutoboxingAutoboxingAutomatic conversion primitive → wrapper object
UnboxingUnboxingAutomatic conversion wrapper object → primitive
AliasingAliasingMultiple variables referencing the same object
Garbage CollectorCrumb catcherAutomatically frees memory of unreferenced objects
Is-a relationshipRelationship Is aInheritance relationship — a subclass “is a” type of its superclass
Sealed classSealed ClassClass that controls exactly which classes can inherit from it

The four pillars of OOP

┌─────────────────────────────────────────────────────────┐
│                   Les 4 Piliers de l'OOP                │
├────────────────┬────────────────┬────────────────────────┤
│  Abstraction   │ Encapsulation  │    Inheritance         │
│                │                │                        │
│ Créer des      │ Cacher         │ Étendre des classes    │
│ types adaptés  │ l'implémen-    │ hériter des fields     │
│ au domaine     │ tation         │ et méthodes            │
│                │                │                        │
│ class Alarm {} │ private fields │ class PA extends A {}  │
│                │ public methods │                        │
├────────────────┴────────────────┴────────────────────────┤
│                    Polymorphism                          │
│                                                          │
│  Écrire du code générique qui fonctionne avec           │
│  plusieurs types — y compris ceux pas encore écrits     │
│                                                          │
│  Alarm alarm = new HighVisibilityAlarm("msg");          │
│  alarm.getReport();  // Version HighVisibilityAlarm      │
└──────────────────────────────────────────────────────────┘

Class hierarchy in the example

          Object
             │
           Alarm (abstract, implements Widget, PersistentObject)
          ╱      ╲
PrioritizedAlarm  HighVisibilityAlarm  TimeSensitiveAlarm
(Color.GREEN)     (Color.ORANGE)       (+ heure courante)

Access modifiers

                ┌──────────────────────────────────────────┐
                │   Portée de visibilité                   │
                │                                          │
                │ private │ package │ protected │ public   │
                │         │ -private│           │          │
Même classe     │   ✅    │   ✅    │    ✅     │   ✅     │
Même package    │   ❌    │   ✅    │    ✅     │   ✅     │
Sous-classes    │   ❌    │   ❌    │    ✅     │   ✅     │
Autre package   │   ❌    │   ❌    │    ❌     │   ✅     │
                └──────────────────────────────────────────┘

Search Terms

object-oriented · programming · java · backend · architecture · full-stack · web · static · alarm.java · encapsulation · classes · design · polymorphism · class · define · inheritance · interface · objects · abstract · access · alarm · constructors · fields · hierarchy

Interested in this course?

Contact us to book it or get a custom training plan for your team.