Objets mutables et immuables

1. Introduction

Les objets mutables sont des objets qui peuvent être modifiés après leur création, contrairement aux objets immuables, qui eux, ne peuvent pas être modifiés. En Java, la plupart des objets sont mutables, à quelques exceptions près, comme les classes URL et File et les classes-enveloppe : Integer, Byte, Float, Char, etc.

Les Classes-enveloppe ou Wrappers, sont des classes qui encapsulent des données de type primitif, pour les utiliser comme des objets ordinaires.

Par exemple, la classe String est immuable, contrairement à StringBuilder, qui elle est mutable.

Considérons l’extrait Java suivant :

String message = "In Java, String objects are ";
message = message + "immutable";

StringBuilder builder = new StringBuilder();
builder.append("StringBuilder objects are ")
    .append("mutable");

La variable message sera d’abord assignée à un objet (ligne 1), puis à un autre objet (ligne 2). Après la deuxième affectation, le premier objet String sera perdu, mais restera en mémoire jusqu’à ce que le ramasse-miettes fasse son travail et libère la mémoire.

De son côté, la variable builder ne sera assignée qu’une seule fois (ligne 4), mais son contenu sera modifié après chaque appel de la méthode append().

Les objets mutables ont des méthodes de modification, telles que : set(), append(), add(), etc.

1.1. Différences entre les objets mutables et immuables

Mutables Immuables

peuvent prendre n’importe quelle valeur ou n’importe quel état sans créer de nouvel objet.

ne peuvent pas être modifiés.

fournissent des méthodes de modification pour changer leur contenu.

ne fournissent aucune méthode de modification.

peuvent ou non être sûrs pour les threads.

sont sûrs pour les threads.

ne doivent pas être utilisés en tant que clés dans un Map : peuvent créer un comportement inattendu.

ont tendance à utiliser moins de mémoire que les objets immuables.

1.2. Écrire des classes immuables

Règles d’écriture des classes immuables :
  • La classe doit être déclarée final : elle ne doit pas pouvoir être héritée.

  • Les champs non transitoires (intrinsèques) doivent être private et final : ils ne peuvent pas être modifiés après l’initialisation.

1.3. Écrire des classes mutables

Il existe une règle simple pour écrire des classes mutables : elles doivent avoir des méthodes permettant de modifier les valeurs des champs, de façon directe ou indirecte.

2. Exercice : Écrire une classe mutable

Écrivez une classe mutable appelée MutableIntWrapper, qui implémente l’interface MutableInt, présentée ci-dessous. Cette classe doit implémenter des opérations arithmétiques (addition, soustraction, etc.), ainsi que des opérations d’incrémentation et de décrémentation.

Voici un exemple d’utilisation de la classe MutableIntWrapper :

package fr.unantes.sce.examples;

import fr.unantes.sce.mutable.MutableInt;
import fr.unantes.sce.mutable.MutableIntWrapper;

import java.util.List;
import java.util.function.Consumer;

public class MutableIntExample {
    public static void main(String[] args) {
        MutableInt eight = new MutableIntWrapper(8);
        MutableInt fifteen = new MutableIntWrapper(15);
        eight.add(fifteen);
        assert eight.get() == 23;


        var list = List.of(1,2,3);
        list.forEach(System.out::println);
    }
}

L’interface à implémenter est la suivante :

Listing 1. L’interface MutableInt
package fr.unantes.sce.mutable;

/**
 * An interface representing a mutable integer value.
 */
public interface MutableInt {

    /**
     * Gets the current value of the mutable integer.
     * @return The current value of the mutable integer.
     */
    int get();

    /**
     * Sets a new value for the mutable integer.
     * @param newValue The new value to set for the mutable integer.
     */
    void set(int newValue);

    /**
     * Adds the value of the other mutable integer to the current value of this mutable integer.
     * @param other The other mutable integer to add.
     * @return The current mutable integer after adding the other mutable integer.
     */
    MutableInt add(MutableInt other);

    /**
     * Subtracts the value of the other mutable integer from the current value of this mutable integer.
     * @param other The other mutable integer to subtract.
     * @return The current mutable integer after subtracting the other mutable integer.
     */
    MutableInt subtract(MutableInt other);

    /**
     * Multiplies the value of the other mutable integer with the current value of this mutable integer.
     * @param other The other mutable integer to multiply.
     * @return The current mutable integer after multiplying the other mutable integer.
     */
    MutableInt multiply(MutableInt other);

    /**
     * Divides the value of the current mutable integer by the value of the other mutable integer.
     * @param other The other mutable integer to divide by.
     * @return The current mutable integer after dividing by the other mutable integer.
     */
    MutableInt divide(MutableInt other);

    /**
     * Increments the value of the mutable integer by 1.
     */
    void increment();

    /**
     * Increments the value of the mutable integer by 1 and returns the updated value.
     * @return The updated value after incrementing.
     */
    int incrementAndGet();

    /**
     * Returns the current value of the mutable integer and then increments it by 1.
     * @return The current value of the mutable integer before incrementing.
     */
    int getAndIncrement();

    /**
     * Decrements the value of the mutable integer by 1.
     */
    void decrement();

    /**
     * Decrements the value of the mutable integer by 1 and returns the updated value.
     * @return The updated value after decrementing.
     */
    int decrementAndGet();

    /**
     * Returns the current value of the mutable integer and then decrements it by 1.
     * @return The current value of the mutable integer before decrementing.
     */
    int getAndDecrement();
}

3. Exercice : Écrire une classe immuable

  1. Implémentez une classe nommée ImmutableInt, qui implémente des méthodes arithmétiques de base.

  2. Utilisez l’interface Immutable, présentée ci-dessous, pour guider votre implémentation .

  3. Une fois la classe implémentée, suggérez des solutions pour éviter :

    1. que les méthodes soient appelées avec un argument nul

    2. un dépassement de capacité du champ entier de la classe

Listing 2. L’interface Immutable
package fr.unantes.sce.immutable;

public interface Immutable<T> {
    Immutable<T> add(Immutable<T> other);

    Immutable<T> subtract(Immutable<T> other);

    Immutable<T> multiply(Immutable<T> other);

    Immutable<T> divide(Immutable<T> other);

    T value();
}

Voici un exemple d’utilisation de la classe ImmutableInt :

Listing 3. Exemple d’utilisation de la classe ImmutableInt
package fr.unantes.sce.examples;

import fr.unantes.sce.immutable.Immutable;
import fr.unantes.sce.immutable.ImmutableInt;

public class ImmutableIntExample {
    public static void main(String[] args) {
        Immutable<Integer> one = new ImmutableInt(1);
        Immutable<Integer> twenty = new ImmutableInt(20);
        Immutable<Integer> result = one.add(twenty);

        assert result.value() == 21;
        assert one.value() == 1;
    }
}

4. Exercice : Optimisation de l’instantiation de ImmutableInt

Modifier l’implémentation de la classe ImutableInt pour la faire mettre en cache les valeurs les plus utilisées (de 0 à 255) et empêcher l’instanciation de plus d’une instance par valeur.

Pour cela, vous allez appliquer le patron de conception Poids mouche.

Étapes
  1. Créez un champ statique de type tableau d’entiers, de taille 256.

  2. Initialisez ce champ avec des entiers immuables de 0 à 255.

  3. Créez une méthode statique nommée valueOf(int), qui vérifie si la valeur de l’argument est dans le cache. Si oui, elle retourne la valeur du cache. Si non, elle crée une nouvelle instance.

    1. La méthode valueOf(int) joue le rôle d’une méthode fabrique

  4. Rendez inaccessible le seul constructeur de la classe