Advanced Java

Ludovic Deneuville

Reminder

Exception

What are Exceptions?

  • Events that disrupt the normal flow of program execution
  • Indicate errors or unexpected conditions that occur during runtime

Tip

Exceptions are your friends.

NullPointerException

public class NullPointerExceptionExample {

    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.
    }
}

How to Prevent

Always check if an object reference is null before using it.

ArithmeticException

public class ArithmeticExceptionExample {
    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);
    }
}

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

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;
    }
}

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;
}

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);
    }
}

Use Custom Exceptions

BankAccount.java
import fr.ensai.bank.exception.InsufficientBalanceException;
import fr.ensai.bank.exception.TransferLimitExceededException;

public class Bank {

    ...

    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;
    }
}

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());
        }
    }
}

Encountering Exceptions

When an exception occurs in your code, you have two primary options:

  • propagate the exception
  • resolve the exception

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

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

Key Concepts

  • Source (List, Array…)
  • Intermediate Operations (filter, map…)
  • Terminal Operation (reduce, collect, forEach…)
  • Lazy evaluation

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)

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())

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

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();

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;
}

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();

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
}

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;
        
    ...
}

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.";
        }
    }
}

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

Interface

Back to Inheritance

  • Fighter: Primarily focused on combat.
  • Doctor: Primarily focused on healing.

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

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);
}

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

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);

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);

Downcasting with instanceof

Person p = new Doctor(...);

if (p instanceof Doctor d) {
    d.heal(anotherPerson);
} else {
    System.out.println(p + " is not a Doctor.");
}

To go further

  • Optional
  • Genericity
  • Annotations