Java version: Java SE 17 (LTS) — content applicable to Java 8+
Table of Contents
- Welcome to OOP
- OOP in 6 minutes
- The object-oriented debate
- Mechanics and design
- Content of this training
- Use items
- Things under the hood
- More about references
- The dreaded NullPointerException
- Object aliasing
- Passing objects to methods
- Identity and equality
- Identity and equality summary
- The meaning of constants
- Autoboxing
- Module 3 Summary
- Define a class
- Declare a field
- Define method
- A story of development
- Add features to class
- Null hits again
- Initialize fields
- The constructors
- The final fields
- More about constructors
- Destructors in Java
- The first pillar: abstraction
- Module 4 Summary
- Access modifiers
- Return to our code
- Snooze the alarm
- Implement snoozing
- Test snoozing
- Our code is not enough
- A reason to control access
- Interface and implementation
- Fields vs Properties
- The second pillar: encapsulation
- Module 5 Summary
- Another type of alarm
- Subclass Alarm
- Superclasses and constructors
- Overriding of methods
- Encapsulation meets inheritance
- Test subclass
- Clean subclass
- The single-root hierarchy
- The final keyword revisited
- Les sealed classes
- Module 7 Summary
- The “Is-a” relationship
- Test Is-a relationship
- Upcasting
- Polymorphism in action
- Write extensible code
- Polymorphism is everywhere
- The downcasting
- Module 8 Summary
- A Deeper Look at Alarms
- Abstract classes
- The hierarchy problem
- Play multiple roles
- Interface constraints
- Module 9 Summary
- More design thinking
- Inheritance concerns upcasting
- Don’t check types with instanceof
- Module 10 Summary
- Static members
- Mix static and non-static
- Global variables and global constants
- Other uses of static
- Attention to static members
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:
- Abstraction
- Encapsulation
- Inheritance
- 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:
- Java does OOP correctly, as it is supposed to be done.
- Java is not OOP enough — it remains too procedural, it does not go far enough in that direction.
- Java misses the point of OOP — it focuses on parts of the OOP that are not so important and neglects more important parts.
- 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:
- The mechanics: Java features that support the fundamental ideas of OOP — this is primarily what this training covers.
- 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:
- Modules 1-2: Introduction
- Modules 3-6: Abstraction and encapsulation (working with objects, defining classes, hiding information, design)
- Modules 7-10: Inheritance and polymorphism (inheritance, polymorphism, abstract classes, interfaces, design)
- Module 11: The
statickeyword — the least OOP of Java features - 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
varinstead 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:
- Access its fields:
alarm.active - 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:
| Primitives | Objects | |
|---|---|---|
| Stored value | The value directly (ex: 42) | A reference to the object |
| Location | Stack | Heap (heap) |
| Initialization | Literal (ex: 42, true) | Via new |
| Default | 0, 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:
- References can be
null - There may be aliasing (several variables pointing to the same object)
- Distinction between identity and equality of objects
finalreferences 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
nullas 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.
| Concept | Description | Operator |
|---|---|---|
| Identity | Both variables point to the same object in memory | == |
| Equality | Both 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 compareStringor 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
Stringwith 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
finalreference: 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:
| Primitive | Wrapper class |
|---|---|
int | Integer |
boolean | Boolean |
double | Double |
float | Float |
long | Long |
char | Character |
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:
- References can be
null→ source ofNullPointerException - Aliasing: several variables can reference the same object. This happens every time an object is passed to a method.
- Identity vs equality:
==compares references (identity).equals()compares the contents (depending on the implementation). - Constants and immutability: a
finalreference cannot be reassigned, but the pointed object can still be modified (unless it is immutable). - 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
classfollowed 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 +
.javaextension: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.) orvoidif 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:
- An alarm must contain a message describing the alarm (eg: “Temperature too high”)
- A
getReportmethod returning the message if the alarm is active, an empty string otherwise — with option to display in uppercase - 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:
- Default value at declaration:
String message = "No message set"; // Valeur par défaut
But finding a good default value is not always possible.
- 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"):
- Java allocates memory for object
- Java initializes all fields to their default value (or declared value)
- 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:
final(not reassignable)- 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:
- What the variable looks like: its structure (its data)
- 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:
| Modifier | Visibility |
|---|---|
public | Accessible from anywhere |
protected | Accessible from the same package and subclasses |
| (none) | Package-private — accessible only from the same package |
private | Accessible 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()andturnOff()methods to change stategetReport()andgetReport(boolean)methods to get the reportsendReport()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 minutessnoozeUntil.isAfter(LocalDateTime.now()): checks if the end of snooze is in the futurestopSnoozing(): initializessnoozeUntilto 1 second in the past (soisSnoozing()returnsfalse)
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(): callstopSnoozing()to cancel snooze - At
turnOff(): callstopSnoozing()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:
-
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.
-
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.
-
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
privateandpublic - 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:
- Identify concepts in specifications: nouns often become classes, verbs often become methods
- Design classes and their relationships — UML class diagrams
- 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:
- Add priority to the
Alarmclass: bad idea — not all alarms have a priority. The class would become a “catch-all”. - Copy and paste the code: always a bad idea (duplication, difficult maintenance).
- Inheritance: create a new class that inherits from
Alarmand 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 inheritancePrioritizedAlarm: the subclass or child classAlarm: the superclass or parent classPrioritizedAlarminherits all public/protected fields and methods fromAlarm
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:
- Memory allocated for the object (including superclass fields)
- Constructor of superclass (
super(message)) executed → initializesmessage - 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 overriddensuper.getReport(uppercase): call version of method in superclass — avoid duplication- If we create a
HighVisibilityAlarmand callgetReport, 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:
- Public Getters: expose fields via
publicmethods - The
protectedmodifier:
// 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:
- We call
sendReport()— method inherited fromAlarm sendReport()callsgetReport(true)— butgetReportis overridden inHighVisibilityAlarm- 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
@Overridewhen 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)
Objectis the only class without a superclass- This structure forms a large hierarchy with
Objectat the root
Object methods (available on all Java objects):
toString(): textual representation of the objectequals(Object): equality comparisonhashCode(): hash code for collectionsgetClass(): object classclone(): copywait(),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:
| Application | Meaning |
|---|---|
final on a variable/field | Cannot be reassigned |
final on a method | Cannot be overridden in subclasses |
final on a class | Cannot 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 sealedpermits: explicit list of authorized subclasses- Subclasses of a
sealed classmust besealed,non-sealed, orfinal
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 Javasuper(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 protectedmodifier: visible in class, package, and subclassesfinalon 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 ofCat) - An
Animal(instance ofAnimal) - An
Object(instance ofObject)
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:
DashboardstoresAlarmin a listadd()takes anAlarm— but thanks to upcasting, it works with any subclassprintBigReport()loops through all alarms and callsgetReport(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
defaultandstatic)
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:
defaultmethods: methods with a default implementationstaticmethodsstatic finalfields (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:
- Difficult to read: complex sequence of conditions
- Fragile during modifications: if you add a new subclass, you have to update all the
instanceofin the code — you may forget some - 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
- Use inheritance for upcasting: inheritance is mainly at the service of polymorphism. If it’s just to share code, consider delegation.
- Avoid
instanceofstrings: 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:
| Since | Can access | Example |
|---|---|---|
| Non-static method | Other non-static methods/fields | this.active, turnOn() |
| Non-static method | Static methods/fields | Alarm.DOCUMENTATION (or just DOCUMENTATION if in the same class) |
| Static method | Other static methods/fields | DOCUMENTATION |
| Static method | ❌ Non-static methods/fields directly | Error ! |
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 :
- Static members propagate: a static method easily calls other static methods, which create other static methods, etc.
- Static fields become global variables: difficult to trace, sources of bugs
- Non-testable code: static methods with side effects are difficult to test
- 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
privateorpublic - 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
superto access the superclass - All Java classes inherit from
Object(single root hierarchy) finalclasses/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 term | French term | Description |
|---|---|---|
| Object | Object | Bundle of data (fields) and operations (methods) |
| Class | Class | Blueprint (template) to create objects |
| Field | Field | Data stored in an object |
| Method | Method | Operation that an object can perform |
| Reference | Reference | Pointer to an object in memory |
| Heap | Heap | Memory area where objects are stored |
| Constructor | Builder | Initialization block called when creating an object |
| Inheritance | Legacy | Mechanism by which one class inherits members from another |
| Superclass / Parent class | Superclass | The class we inherit |
| Subclass / Child class | Subclass | The class that inherits |
| Polymorphism | Polymorphism | Ability of an object to behave differently depending on its real type |
| Upcasting | Upcasting | Assign an object to a superclass variable |
| Downcasting | Downcasting | Cast to a more specific type (with risk of ClassCastException) |
| Override | Overload (redefinition) | Redefine an inherited method in a subclass |
| Overload | Overload | Multiple methods with the same name but different arguments |
| Encapsulation | Encapsulation | Hide the implementation behind a public interface |
| Abstract | Abstract | Create new types adapted to the domain |
| Interface | Interface | Contract specifying which methods an object must respond to |
| Abstract class | Abstract class | Class that cannot be instantiated directly |
| Abstract methods | Abstract method | Bodyless method — subclasses must implement it |
| Access edit | Access Modifier | public, private, protected, or none (package-private) |
| Autoboxing | Autoboxing | Automatic conversion primitive → wrapper object |
| Unboxing | Unboxing | Automatic conversion wrapper object → primitive |
| Aliasing | Aliasing | Multiple variables referencing the same object |
| Garbage Collector | Crumb catcher | Automatically frees memory of unreferenced objects |
| Is-a relationship | Relationship Is a | Inheritance relationship — a subclass “is a” type of its superclass |
| Sealed class | Sealed Class | Class 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