classDiagram
class Person {
- name: String
- health: int
+ takeDamage(int amount)
+ receiveHealing(int amount)
}
class Fighter {
- attackPower: int
+ fight(Person target)
}
class Doctor {
- healingPower: int
+ heal(Person patient)
}
Person <|-- Fighter
Person <|-- Doctor
Advanced Java
Reminder
Advices for Rated TP:
- Javadoc
- Comment if necessary
- PascalCase (Class, Method, Variables)
- Use clear, meaningful names for variables and methods
- well-indented code, easy to read
Enum
What are Enums?
- Defines a set of named constants
- Represents a fixed and predefined set of possible values
- Examples: Day of week, colors, playing cards
Declare a basic Enum
Grade.java
public enum Grade {
EXCELLENT,
VERY_GOOD,
GOOD,
AVERAGE,
INSUFFICIENT
}- These are the constant values of the enum
Use an Enum
Grade grade = Grade.VERY_GOOD;
switch (grade) {
case EXCELLENT:
System.out.println("Excellent!");
break;
case VERY_GOOD:
System.out.println("Very good!");
break;
...
}- Enums are implicitly:
- static (no need to create an instance)
- final (no modification allowed)
Adding Behavior to Enum
- Enums are More Than Just Constants
- They are special classes
- They can have methods and attributes
Grade.java
public enum Grade {
EXCELLENT(90), VERY_GOOD(80), GOOD(70), AVERAGE(60), INSUFFICIENT(50);
@Getter
private final int minimumScore;
private Grade(int minimumScore) {
this.minimumScore = minimumScore;
}
public String getFeedback() {
switch (this) {
case EXCELLENT: return "Outstanding!";
case VERY_GOOD: return "Well done!";
case GOOD: return "Satisfactory.";
case AVERAGE: return "Needs more effort.";
case INSUFFICIENT: return "Requires significant improvement.";
default: return "Unknown grade.";
}
}
}- constructor is private
- we cannot access it from outside the class
- we can use enum constants to call the constructor
- compiler error if constructor is public
Enum methods
Grade studentGrade = Grade.VERY_GOOD;
System.out.println("Grade: " + studentGrade);
System.out.println("Minimum Score: " + studentGrade.getMinimumScore());
System.out.println("Feedback: " + studentGrade.getFeedback());
// Iterate
for (Grade g : Grade.values()) {
System.out.println(g);
}Enums: Pros and Cons
- ✅ Readability: Makes code easier to understand by using descriptive names for values
- ✅ Type Safety: Avoids errors related to typos or invalid values
- ✅ Maintainability: Easier to modify and add new values (for dev)
- ❌ Limited Extensibility: Difficult to add or modify values at runtime
- ❌ Reduced Flexibility: Restricts values to a predefined set
- ✅ Compile-Time Validation: Catches enum-related issues early during compilation.
Exception
What are Exceptions?
- Events that disrupt the normal flow of program execution
- Indicate errors or unexpected conditions that occur during runtime
Exceptions are your friends.
What to do if you leave the road.
- Reminder
- Exceptions are your friend
- Error (OutOfMemoryError, StackOverflowError)
NullPointerException
public class HowToRaiseNullPointerException {
public static void printLength(String text){
System.out.println(text.length());
}
public static void main(String[] args) {
String myText = null;
printLength(myText); // will throw a NullPointerException.
}
}Always check if an object reference is null before using it.
ArithmeticException
public class HowToRaiseArithmeticException {
public static void main(String[] args) {
int numerator = 10;
int denominator = 0;
int result = numerator / denominator; // Will throw ArithmeticException
System.out.println("The division result is: " + result);
}
}Encountering Exceptions
When the 🚋 leaves the tracks, you must decide how to react:
- ❌ Stop everything
- The journey ends immediately (program crashes)
- 🔧 Have a Plan B
- You handle the situation locally (try/catch)
- 📞 Escalate the situation
- You let someone else handle it to higher level
- propagate the exception
- resolve the exception
Why Should We Raise Exceptions?
- To controll error management
- To preventing unexpected program crashes
- To alter the normal execution flow when an error occurs
- To prevent the system from entering an unstable or inconsistent state
We are also allowed to raise exceptions.
To control the execution.
Useful for debugging, it give more details.
Example
BankAccount.java
package fr.ensai.bank.domain;
@AllArgsConstructor
class BankAccount {
private double balance;
private double transferLimit;
/**
* Transfers a specified amount from this account to another account.
* The amount cannot exceed the available balance or the transfer limit.
*
* @param recipient the account to which the amount will be transferred
* @param amount the amount to be transferred
*/
public void transfer(BankAccount recipient, double amount) {
this.balance -= amount;
recipient.balance += amount;
}
}This code can lead to an incoherent state
- recipient: OtherBankAccount
@AllArgsConstructor: Lombokrecipient.balance += amount;: valid code, no need of a setter inside- Other solution: return boolean instead of void
- False if the transfert cannot be done
Throw an Exception
BankAccount.java
/**
* Transfers a specified amount from this account to another account.
*
* @param recipient the account to which the amount will be transferred
* @param amount the amount to be transferred
* @throws IllegalArgumentException if the amount exceeds the available balance or the transfer limit
*/
public void transfer(BankAccount recipient, double amount)
throws IllegalArgumentException {
if (amount > this.balance)
throw new IllegalArgumentException(
"Insufficient balance for the transfer. Attempted: " + amount + ", Available: " + this.balance);
if (amount > this.transferLimit)
throw new IllegalArgumentException(
"Transfer amount exceeds the transfer limit. Attempted: " + amount + ", Limit: " + this.transferLimit);
this.balance -= amount;
recipient.balance += amount;
}- If amount < 0 ?
- throws: method can throw this exception
- throw: raise
Define Custom Exceptions
InsufficientBalanceException.java
package fr.ensai.bank.exception;
public class InsufficientBalanceException extends Exception {
public InsufficientBalanceException(String message) {
super(message);
}
}TransferLimitExceededException.java
package fr.ensai.bank.exception;
public class TransferLimitExceededException extends Exception {
public TransferLimitExceededException(String message) {
super(message);
}
}Usefull for business errors
- Inherit class Exception
public class Exception extends Throwable
- as a class
- you can specify message
Use Custom Exceptions
BankAccount.java
import fr.ensai.bank.exception.InsufficientBalanceException;
import fr.ensai.bank.exception.TransferLimitExceededException;
public class BankAccount {
...
public void transfer(BankAccount recipient, double amount)
throws InsufficientBalanceException, TransferLimitExceededException {
if (amount > this.balance)
throw new InsufficientBalanceException(
"Insufficient balance for the transfer. Attempted: " + amount + ", Available: " + this.balance);
if (amount > this.transferLimit)
throw new TransferLimitExceededException(
"Transfer amount exceeds the transfer limit. Attempted: " + amount + ", Limit: " + this.transferLimit);
this.balance -= amount;
recipient.balance += amount;
}
}package exception
Exception Handling
Bank.java
public class Bank {
public void makeTransfert(BankAccount sender, BankAccount receiver, amount) {
try {
sender.transfer(receiver, 250.0);
} catch (InsufficientBalanceException e) {
System.out.println("Transfer failed: " + e.getMessage());
} catch (TransferLimitExceededException e) {
System.out.println("Transfer failed: " + e.getMessage());
} finally {
System.out.println("Account 1 balance: " + sender.getBalance());
System.out.println("Account 2 balance: " + receiver.getBalance());
}
}
}- You can have multiple
catchblocks to handle different types of exceptions- The order of catch blocks matters (most specific to most general).
- simpliest way:
catch (Exception e)
- Finally: block always executes, regardless of whether an exception occurred or not.
- Used for cleanup code (e.g., closing resources).
- Without finally, code after catch blocks is only executed if no unhandled exception occurs.
Types of Exception
| Exception | Description | Checked at Compilation |
|---|---|---|
ClassNotFoundException |
Trying to access a class whose definition is not found. | Yes |
FileNotFoundException |
File is not accessible or does not open. | Yes |
IOException |
Input-output operation failed or was interrupted. | Yes |
NoSuchFieldException |
A class does not contain the specified field. | Yes |
NoSuchMethodException |
Accessing a method that is not found. | Yes |
NullPointerException |
Referring to the members of a null object. | No |
NumberFormatException |
A method could not convert a string into a numeric format. | No |
ArithmeticException |
An exceptional condition has occurred in an arithmetic operation. | No |
ArrayIndexOutOfBoundsException |
An array has been accessed with an illegal index. | No |
RuntimeException |
Represents an exception that occurs during runtime. | No |
IllegalArgumentException |
A method receives an argument that does not fit the given condition. | No |
see Types of Exception in Java with Examples
- RuntimException (not checked at compilation)
Stream
Java Streams
- Introduced in Java 8
- Declarative style
- Concise and readable code
- Efficient data processing
- Support for parallel processing
How it works
- Takes input from the Collections, Arrays, or I/O channels
- Provide a result as per the pipelined methods
- Intermediate operation is lazily executed and returns a stream
- Terminal operations mark the end of the stream and return the result
A first example
Long sumOdds = Stream
.iterate(0L, i -> i + 1L) // Generate an infinite stream of Longs starting from 0, incrementing by 1
.limit(10) // Limit the stream to the first 10 elements
.filter(i -> (i % 2) == 0) // Filter the stream to keep only even numbers
.map(i -> i + 1) // Transform each element by adding 1
.sorted() // Sort the stream
.reduce(0L, Long::sum); // Reduce the stream to a single Long by summing all elements- Lazy evaluation:
- != eager
- Computation on the elements of the stream is only performed when it’s necessary
- don’t generate all natural number
- limit 10
Intermediate operations are not executed until a terminal operation is invoked.
This approach can optimize performance, especially for large datasets, by reducing the number of iterations and computations.
Key Concepts
- Source (List, Array…)
- Intermediate Operations (filter, map…)
- Terminal Operation (reduce, collect, forEach…)
- Lazy evaluation
- Lazy evaluation: very important concept
- Like dplyr
Create a Stream
import java.util.stream.Collectors;
import java.util.stream.Stream;
ArrayList<String> arrayList = new ArrayList<>(List.of("a", "b", "c", "d"));
Map<String, Integer> map = Map.of("a", 1, "b", 2, "c", 3);
String text = "I am your father";
// Creates a stream from an arrayList
Stream<String> arrayListStream = arrayList.stream();
// Creates a stream from the map's entry set
Stream<Map.Entry<String, Integer>> mapEntryStream = map.entrySet().stream();
// Extract list of words from a text an store it in a List
List<String> wordsList = Stream.of(text.split(" ")).collect(Collectors.toList());Intermediate Operations
They return a new stream.
| Operation | Description | Example |
|---|---|---|
filter(predicate) |
Selects matching elements. | stream.filter(x -> x > 10) |
map(function) |
Transforms elements. | stream.map(String::toUpperCase) |
flatMap(function) |
Transforms to streams, then flattens. | stream.flatMap(List::stream) |
distinct() |
Removes duplicates. | stream.distinct() |
sorted() |
Sorts elements. | stream.sorted() |
sorted(comparator) |
Sorts elements (using comparator). | stream.sorted(Comparator.reverseOrder()) |
peek(consumer) |
Performs action on each element. | stream.peek(System.out::println) |
limit(maxSize) |
Truncates to maxSize elements. |
stream.limit(10) |
skip(n) |
Discards first n elements. |
stream.skip(5) |
- Lambda functions
- method reference (String::toUpperCase)
- Intermediate operations return a new modified stream
Terminal Operations
| Operation | Description | Example |
|---|---|---|
forEach(action) |
Performs an action on each element. | stream.forEach(System.out::println) |
collect(collector) |
Accumulates elements into a result container. | stream.collect(Collectors.toList()) |
reduce(identity, accumulator) |
Reduces elements to a single value. | stream.reduce(0, Integer::sum) |
count() |
Returns the number of elements. | stream.count() |
anyMatch(predicate) |
Checks if any element matches. | stream.anyMatch(x -> x > 10) |
allMatch(predicate) |
Checks if all elements match. | stream.allMatch(x -> x > 0) |
noneMatch(predicate) |
Checks if no elements match. | stream.noneMatch(x -> x < 0) |
findFirst() |
Returns the first element. | stream.findFirst() |
min(comparator) |
Returns the minimum element. | stream.min(Comparator.naturalOrder()) |
- an attempt to reuse the same reference after calling the terminal operation will trigger the IllegalStateException
- Java 8 streams can’t be reused
Parallel Streams
- processed in parallel for improved performance
- Use parallelStream()
- Suitable for CPU-intensive operations on large datasets
MapReduce
- A programming model for processing and generating large datasets
- Designed for distributed computing on clusters of machines.
- Breaks down complex tasks into simpler, parallel operations
- Invented by Google in the 2000s
- If memory space is not enought to load the full data set
- https://ludo2ne.github.io/ENSAI-2A-Big-Data/docs/lab/lab2/lab2.html#how-to-distribute-elementary-statistical-tasks
MapReduce workflow
- Split: Input data is divided into smaller chunks
- Map: Each chunk is processed by a Map function
- Reduce: Values are aggregated by a Reduce function
MapReduce principle
List<Integer> values = Arrays.asList(1, 2, 5, 10, 3, 4, 6, 8, 9);
List<Integer> valuesSquares = new ArrayList<>();
Function<Integer, Integer> square = n -> n * n;
for (int n : values) {
valuesSquares.add(square.apply(n));
}
int sumOfSquares = 0;
for (int v : valuesSquares) {
sumOfSquares += v * v;
}
double meanSumOfSquares = sumOfSquares / valuesSquares.size();- compute squares (map)
- sum squares (reduce)
- final step (O(1))
Manual implementation
public List<Double> manualMap(List<Double> list, DoubleUnaryOperator function) {
List<Double> result = new ArrayList<>();
for (double element : list) {
result.add(function.applyAsDouble(element));
}
return result;
}
public double manualReduce(List<Double> list, double identity, DoubleBinaryOperator accumulator) {
double result = identity;
for (double element : list) {
result = accumulator.applyAsDouble(result, element);
}
return result;
}- DoubleUnaryOperator (sqrt, power…)
- Interface with one method
- applyAsDouble(Double d) -> double
- DoubleBinaryOperator (add, mult…)
- applyAsDouble(Double d1, Double d2) -> double
MapReduce using streams
List<Integer> values = Arrays.asList(1, 2, 5, 10, 3, 4, 6, 8, 9);
double sumOfSquares = values.stream()
.map(n -> n * n)
.reduce(0, (a, b) -> a + b);
double meanSumOfSquares = sumOfSquares / values.size();
double meanSumOfSquaresAvg = values.stream()
.mapToInt(n -> n * n)
.average();- mapToInt() -> IntStream
- map() specialization
Interface
Back to Inheritance
- Fighter: Primarily focused on combat.
- Doctor: Primarily focused on healing.
- Python: Allows multiple inheritance.
- Example: A Square could be considered both a Rectangle and a Rhombus.
Introducing a New Character
- ArmyDoctor: A special character that can both heal and combat.
- Java does not support multiple inheritance directly
classDiagram
class Person {
- name: String
- health: int
+ takeDamage(int amount)
+ receiveHealing(int amount)
}
class Fighter {
- attackPower: int
+ fight(Person target)
}
class Doctor {
- healingPower: int
+ heal(Person patient)
}
Person <|-- Fighter
Fighter <|-- ArmyDoctor
Doctor <|-- ArmyDoctor
Person <|-- Doctor
Defining Abilities as Contracts
Instead of focusing solely on fixed “classes,” let’s think about abilities.
We can define contracts for different types of actions:
- The ability to combat
- The ability to heal
This approach provides flexibility in how we define our game characters.
What is a Java Interface?
- A Java Interface acts as a blueprint
- It specifies a particular set of abilities or behaviors
- It defines a contract that any class can choose to implement.
Create an Interface
Combatant.java
public interface Combatant {
/**
* Represents the ability to engage in combat.
*/
void fight(Person p);
}Healer.java
public interface Healer {
/**
* Represents the ability to provide healing to a Person.
*/
void heal(Person p);
}- All methods are
public abstract - default method
default void heal(Person p) { ...})-> no longer abstract
- static method:
- called using interface name without instanciation
- private method:
- to factorize code inside (no access from outside)
- attribute (constants) are implicitly public, static and final
Parent class
Person.java
public class Person {
private String name;
private int health;
public Person(String name, int health) {
this.name = name;
this.health = health;
}
public void takeDamage(int amount) {
this.health -= amount;
}
public void receiveHealing(int amount) {
this.health += amount;
}
}Implementing the Interfaces
Doctor.java
public class Doctor extends Person implements Healer {
private int healingPower;
public Doctor(String name, int health, int healingPower) {
super(name, health);
this.healingPower = healingPower;
}
@Override
public void heal(Person patient) {
patient.receiveHealing(this.healingPower);
}
}Multiple Interfaces
ArmyDoctor.java
public class ArmyDoctor extends Person implements Healer, Combatant {
private int healingPower;
private int combatSkill;
public ArmyDoctor(String name, int health, int healingPower, int combatSkill) {
super(name, health);
this.healingPower = healingPower;
this.combatSkill = combatSkill;
}
@Override
public void heal(Person patient) {
patient.receiveHealing(this.healingPower * 0.5);
}
@Override
public void fight(Person target) {
target.takeDamage(this.combatSkill * 0.2);
}
}UML
classDiagram
class Person {
- name: String
- health: int
+ takeDamage(int amount)
+ receiveHealing(int amount)
}
class Fighter {
- attackPower: int
}
class ArmyDoctor {
- healingPower: int
- combatSkill: int
}
class Doctor {
- healingPower: int
}
class Combatant {
<<interface>>
+ fight(Person target)
}
class Healer {
<<interface>>
+ heal(Person patient)
}
Person <|-- Fighter
Person <|-- Doctor
Person <|-- ArmyDoctor
Fighter ..|> Combatant
ArmyDoctor ..|> Combatant
ArmyDoctor ..|> Healer
Doctor ..|> Healer
Upcasting and Downcasting
Upcasting
Convert an object:
- of a more specific type
- to a more general type
specific type: a subclass or a class that implements an interface
Does this code compile and run?
Doctor d = new Doctor(...);
Person p = (Person) d;
d.takeDamage(5);Doctor d = new Doctor(...);
Person p = (Person) d;
p.heal(anotherPerson);- Compiles and runs
- Compile error
Downcasting
Converting an object of:
- a more general type
- to a more specific type
Does this code compile and run?
Person p = new Doctor(...);
Doctor d = (Doctor) p;
d.heal(anotherPerson);Person p = new Doctor(...);
Fighter f = (Fighter) p;
p.takeDamage(5);- Compiles and runs
- Compile error java.lang.ClassCastException
Downcasting with instanceof
Person p = new Doctor(...);
if (p instanceof Doctor d) {
d.heal(anotherPerson);
} else {
System.out.println(p + " is not a Doctor.");
}if (p instanceof Doctor d)
// equivalent since Java 16
if (p instanceof Doctor){
Doctor d = (Doctor) p;
}To go further
- Optional
- Genericity
- Annotations
- Optional: deal with null value
public static <T> void printArray(T[] array) {
for (T element : array) {
System.out.print(element + " ");
}
System.out.println();
}