Archives de catégorie : POO

POO : Test d’auto-évaluation

En licence pro MI AW

Par Olivier Bailleux, Maître de conférences HDR, Université de bourgogne

comp-20-1

Complétez le constructeur de la classe suivante, qui représente un points dans un repère à deux dimensions.

public class Point
{
    private double x;
    private double y;

    public Point(double x, double y)
    {
        // À compléter
    }
}
"Réponse"

public Point(double x, double y)
{
    this.x = x; this.y = y;
}

comp-20-2

Réécrivez le constructeur à deux paramètres de la classe Point sans utiliser this.

"Solution"

public Point(double abscisse, double ordonnee)
{
    x = abscisse; y = ordonnee;
}

comp-20-3

Soit la méthode main suivante :

public static void main(String[] args)
{
    Point[] tab = new Point[10];
    System.out.println(tab[5]);
}

Y-a-t-il une erreur à la compilation ? À l’exécution ? Que contient la cellule tab[5] ? Qu’est-ce qui s’affiche à l’exécution, si applicable ?

"Solution"

Aucune erreur à la compilation ni à l’exécution. La cellule tab[5] (comme toutes les autres) contient la valeur null qui indique qu’elle n’a encore reçu aucune valeur. Affichage à l’exécution : null.

comp-20-4

Comment faire pour ajouter un point de coordonnées (5.2, 3.7) dans la cellule tab[5] ?

"Solution"

tab[5] = new Point(5.2, 3.7);

comp-20-5

Soit la méthode main suivante :

public static void main(String[] args)
{
    ArrayList<Point> tab = new ArrayList<>();
    System.out.println(tab.get(5));
}

Y-a-t-il une erreur à la compilation ? À l’exécution ? Que contient la cellule d’indice 5 de tab ? Qu’est-ce qui s’affiche à l’exécution, si applicable ?

"Solution"

Pas d’erreur à la compilation, mais erreur à l’exécution car la liste désignées par tab est vide, donc la cellule d’indice 5 n’existe pas.

comp-20-6

Comment faire pour ajouter un point de coordonnées (5.2, 3.7) en position 5 dans la liste désignées par tab ?

"Solution"

On ne peut pas tant qu’il n’y a rien aux positions 0, 1, 2, 3 et 4. Si ces chacune de ces cellules contiennent des références de Point ou la valeur null, alors on peu écrire :

tab.add(5, new Point(5.2, 3.7));


comp-21

Soit la classe suivante représentant un polygone constitué d’une liste de points.

public class Polygone
{
    private ArrayList<Point> liste;
  
    // À compléter
}

Réalisez le constructeur par défaut de cette classe.

"Solution"

public Polygone()
{
    liste = new ArrayList<>();
}

On peut aussi écrire :

  • this.liste = new ArrayList<>();
  • this.liste = new ArrayList<Point>();

comp-22-1

Réalisez une méthode retournant le nombre de sommets (points) du polygone courant.

"Solution"

public int nbSommets()
{
    return liste.size();
}

comp-22-2

La méthode suivante devrait permettre d’ajouter un nouveau point au polygone…

public static void addSommet(Point p)
{
    liste.add(p);
}

… mais elle comporte une erreur. Expliquez l’erreur et donnez une version corrigée.

"Solution"

Une méthode de classe ne peut accéder au attributs de l’instance courante car cette méthode peut être appelée sans être associée à une instance et même s’il n’existe aucune instance de la classe concernée.

public void addSommet(Point p)
{
    liste.add(p);
}

comp-23-1

On tente d’utiliser la classe Polygone de la manière suivante…

public static void main(String[] args)
{
    Polygone p;
    p.addSommet(new Point(12.5,5.6));
    // ...
}

…mais il y a une erreur. Indiquez quelle est cette erreur et donner une version corrigée.

"Solution"

La variable p n’est pas initialisée. Pour pouvoir utiliser la méthode d’instance addSommet, il faut une instance de la classe Polygone avec laquelle appeler cette méthode.

public static void main(String[] args)
{
    Polygone p = new Polygone();
    p.addSommet(new Point(12.5,5.6));
    // ...
}

comp-23-2

La méthode main suivante compile-t-elle sans erreur ? Si oui, y a-t-il une erreur à l’exécution ? Si non, que se passe-t-il à l’exécution ?

public static void main(String[] args)
{
    new Polygone().addSommet(new Point(12.5,5.6));
}
"Solution"

Le programme compile et s’exécute sans erreur. Une instance de Polygone est créée en mémoire dans le tas et un point est ajouté au polygone représenté par cette instance. Mais la référence de cette instance de Polygone est perdue, on ne pourra rien en faire et elle sera recyclée. l’exécution du programme ne produit aucune affichage.

comp-24-1

On déclare et initialise la variable de méthode suivante :

ArrayList<int> liste = new ArrayList<>();

Mais le programme ne compile pas. Il y a une erreur. Laquelle ? Pourquoi ? Comment résoudre le problème ?

"Solution"

Le type int est un type primitif, ce qui signifie que les données de ce type ne sont pas des objets mais de simple valeurs en mémoire. La classe ArrayList ne permet pas de créer des liste de valeurs de types primitifs. Mais on peut utiliser à la place des classes enveloppes. La classe enveloppe qui encapsule une donnée de type int est la classe Integer.

ArrayList<Integer> r = new ArrayList<>();

comp-24-2

Réalisez une méthode de classe acceptant un paramètre w désignant une instance de ArrayList représentant une liste d’entier et retournant l’instance de ArrayList obtenue en ajoutant 0 à la fin de la liste désignée par w. Attention, la liste passée en paramètre ne doit pas être modifiée.

"Solution"

Voici une solution possible.

public static ArrayList<Integer> ajoute0(ArrayList<Integer> w)
{
    ArrayList<Integer> r = new ArrayList<>();
    for(int x : w)
    {
        r.add(x);
    }
    r.add(0);
    return r;
}

Il existe d’autres solution mais dans tous les cas il est nécessaire de créer une nouvelle instance de ArrayList<Integer> et de recopier dans la nouvelle liste désignée par w, puis d’y ajouter la valeur 0.

La variante suivant utilise le constructeur en copie de la classe ArrayList :

public static ArrayList<Integer> ajoute0(ArrayList<Integer> w)
{
    ArrayList<Integer> r = new ArrayList<>(w);
    r.add(0);
    return r;
}

comp-24-3

Réalisez une méthode main qui crée une liste vide de type ArrayList<Integer>, ajoute les valeur 4 et 15 dans cette liste, puis appelle la méthode ajoute0 de la question précédente et affiche la liste retournée par cette méthode.

public static void main(String[] args)
{
    ArrayList<Integer> t = new ArrayList<>();
    t.add(4); t.add(15);
    ArrayList<Integer> u = ajoute0(t);
    System.out.println(u);
}

comp-25-1

La classe Integer est non modifiable. Ceci signifie qu’un objet de type Integer ne peut être modifié après sa création. Donnez l’affichage réalisé par l’excécution de la méthode main suivante (qui, pour information, compile sans erreur).

public static void main(String[] args)
{
    Integer x = 34;
    x = x + 1;
    System.out.println(x);
}
"Solution"

La valeur affichée est 35. Si vous vous êtes étonnés, passez à la question suivante. Et si vous n’êtes pas étonnés, passez aussi à l’exercice suivant.

comp-25-2

Soit la méthode main suivante :

public static void main(String[] args)
{
    Integer x = 34;  // Ligne A
    x = x + 1;       // ligne B
    System.out.println(x);
}

Pourquoi, alors que la classe Integer n’est pas modifiable, peut-on ajouter 1 à x ?

"Solution"

En ligne A, on crée une instance de Integer qui contient la valeur 34. La variable x ne contient pas 34. Elle contient la référence de cette instance contenant 34.

En ligne B, on crée une nouvelle instance de Integer contenant 35. La référence de cette nouvelle instance remplace dans x la référence de l’ancienne instance contenant 34. L’ancienne instance n’est plus référencée et la mémoire qu’elle utilise sera recyclée par le ramasse-miette de Java.

comp-25-3

L’affirmation suivante est elle correcte :

Toute classe dotée d’un constructeur en copie est modifiable.

"Solution"

C’est faux. Des classes modifiables et des classes non modifiables peuvent avoir des constructeurs en copie.

comp-25-4

Que faut-il rechercher dans la documentation des méthodes d’une classe pour déterminer si cette classe est modifiable ?

"Solution"

Il faut rechercher des méthodes permettant de modifier l’instance courante, c’est à dire la valeur d’au moins un de ses attributs ou de tout objet désigné par au moins un de ses attributs.

comp-25-5

Quel est le principal avantage et quel est le principal inconvénient de l’utilisation de classes non modifiables ?

"Solution"

  • Avantage : on supprime les effets de bord, c’est à dire la possibilité que la modification d’un objet désigné par une variable impacte l’objet désigné par une autre variable.
  • Inconvénient : Obtenir une version modifiée d’un objet ne peut se faire qu’en la reconstruisant complètement, ce qui consomme plus de ressources de mémoire et de ressources CPU qu’une simple modification.


comp-26-1

Dessinez la configuration mémoire immédiatement après l’exécution de la ligne A de le méthode main suivante:

public static void main(String[] args)
{
    Polygone poly1 = new Polygone();   // Ligne A
    // ...
}
"Solution"

image-20211011140154702

comp-26-2

Dessinez la configuration mémoire immédiatement après l’exécution de la ligne B de le méthode main suivante:

public static void main(String[] args)
{
    Polygone poly1 = new Polygone();
    Point q = new Point(1.0, 2.0);     // Ligne B
    // ...
}
"Solution"

image-20211011140258107

comp-26-3

Dessinez la configuration mémoire immédiatement après l’exécution de la ligne C de le méthode main suivante:

public static void main(String[] args)
{
    Polygone poly1 = new Polygone();
    Point q = new Point(1.0, 2.0);     
    Point[] tab = new Point[2];        // Ligne C
    // ...
}
"Solution"

image-20211011140337119

comp-26-4

Dessinez la configuration mémoire immédiatement après l’exécution de la ligne D de le méthode main suivante:

public static void main(String[] args)
{
    Polygone poly1 = new Polygone();
    Point q = new Point(1.0, 2.0);     
    Point[] tab = new Point[2];
    tab[0] = q;
    tab[1] = q;                        // Ligne D
    // ...
}
"Solution"

image-20211011140412245

comp-26-5

Dessinez la configuration mémoire immédiatement après l’exécution de la ligne E de le méthode main suivante:

public static void main(String[] args)
{
    Polygone poly1 = new Polygone();
    Point q = new Point(1.0, 2.0);     
    Point[] tab = new Point[2];
    tab[0] = q;
    tab[1] = q;
    poly1.addSommet(tab[0]);
    poly1.addSommet(tab[1]);           // Ligne E
    // ...
}
"Solution"

image-20211011140447383

comp-26-6

Dessinez la configuration mémoire immédiatement après l’exécution de la ligne F de le méthode main suivante:

public static void main(String[] args)
{
    Polygone poly1 = new Polygone();
    Point q = new Point(1.0, 2.0);     
    Point[] tab = new Point[2];
    tab[0] = q;
    tab[1] = q;
    poly1.addSommet(tab[0]);
    poly1.addSommet(tab[1]);
    Polygone poly2 = poly1;            // Ligne F
}
"Solution"

image-20211011140511398


comp-27-1

Pour mémoire, voici l’attribut de la classe Polygone.

public class Polygone
{
    private ArrayList<Point> liste = new ArrayList<>();
  
	  //...
}

Voici la définition d’un constructeur en copie de la classe Polygone.

public Polygone(Polygone m)
{
    this();
    for(Point p : m.liste)
    {
        liste.add(p);
    }
}

Ce constructeur es copie est il satisfaisant dans le cas où la classe Point est modifiable ? Et si elle est non modifiable ?

"Solution"

Dans le cas ou Point est non modifiable, il n’y a pas de problème. Mais si elle est modifiable, on risque un effet de bord, i.e. une modification d’un point dans le polygone créé en copie affecterait le polygone original, et réciproquement.

comp-27-2

En supposant que la classe Point dispose d’une méthode de clonage appelée clone, réalisez un constructeur en copie de Polygone respectant les bonnes pratiques de programmation (i.e., effectuant une copie en profondeur).

"Solution"

public Polygone(Polygone m)
{
    this();
    for(Point p : m.liste)
    {
        liste.add(p.clone());
    }
}

Si on utilisait une constructeur en copie de Point, il faudrait remplacer p.clone() par new Point(p).


comp-28-1

Soit la classe Main suivante :

public class Main
{
    public static void printTabInt(int[] t)
    {
        for(int i=0; i< t.length; i++)
        {
            System.out.print(t[i] + " ");
        }
        System.out.println();
    }

    public static void main(String[] args)
    {
        int[] tab1 = {1, 2, 3, 4, 5};
        int[] tab2 = tab1;           // Ligne A
        printTabInt(tab2);
        printTabInt(tab1);
    }
}

La ligne A provoque-t-elle une erreur à la compilation ? Une erreur à l’exécution ? Si le programme s’exécute, donnez les affichage réalisés.

"Solution"

Il n’y a aucune erreur et l’exécution du programme affiche :

1 2 3 4 5 
1 100 3 4 5 

comp-28-2

On conserve la même classe que pour la question précédente, mais on modifie la méthode main qui devient :

{
    int[] tab1 = {1, 2, 3, 4, 5};
    int[] tab2 = tab1;   // Ligne A

    tab2[1] = 100;       // Ligne B

    printTabInt(tab2);
    printTabInt(tab1);
}

Donnez les affichages réalisés.

"Solution"

1 100 3 4 5 
1 100 3 4 5 

Les deux variables désignent le même tableau, qui est modifié par la ligne B, qui, en quelque sorte, impacte la donnée désignée par la variable tab1. Il s’agit d’un effet de bord.

comp-28-3

On considère à présent une version modifiée de la classe Main précédente, dans laquelle les tableaux de int ont été remplacé par des tableau de Integer :

public class Main
{
    public static void printTabInt(Integer[] t)
    {
        for(int i=0; i< t.length; i++)
        {
            System.out.print(t[i] + " ");
        }
        System.out.println();
    }


    public static void main(String[] args)
    {
        Integer[] tab1 = {1, 2, 3, 4, 5};
        Integer[] tab2 = tab1;

        tab2[1] = 100;

        printTabInt(tab2);
        printTabInt(tab1);

    }
}

Quels sont les affichages réalisés par l’exécution du programme ?

"Solution"

1 100 3 4 5 
1 100 3 4 5 

Exactement les mêmes que précédemment. On a toujours un effet de bord. La classe Integer n’est pas modifiable, mais le problème ici vient du fait qu’un tableau, quel que soit le type de ces cellules, est un objet modifiable.

comp-28-4

Dans la méthode main suivante…

public static void main(String[] args)
{
    int[] tab1 = {1, 2, 3, 4, 5};
    int[] tab2 = tab1;  // Ligne A

    tab2[1] = 100;      

    printTabInt(tab2); // Méthode d'affichage d'un tableau de int
    printTabInt(tab1);
}

La ligne A ne réalise pas une duplication du tableau désigné par tab1, ce qui entraîne ensuite un effet de bord (qui, dans une application réelle, pourrait être voulu ou au contraire inopiné et source d’un bug).

Comment faire pour réaliser une copie en profondeur qui rende le résultat de la copie complètement indépendant de l’original ?

"Solution"

Avec les connaissances transmises dans le cadre de cet enseignement, on peut réaliser une méthode de duplication d’un tableau.

public static int[] duplique(int[] original)
{
    int[] copie = new int[original.length];
    for(int i=0; i<original.length; i++)
    {
        copie[i] = original[i];
    }
    return copie;
}

La ligne A de la méthode main doit alors être remplacée par :

int[] tab2 = duplique(tab1);

Mais il est possible d’utiliser la méthode de classe standard Arrays.copyOf. Vous pouvez vous documenter à ce sujet, qui est hors programme.

comp-28-5

Soit la méthode main suivante :

public static void main(String[] args)
{
    String s1 = "aaaaa";
    String s2 = s1;       // Ligne A
    s2 = s2 + 'X';        // Ligne B

    System.out.println(s1);
    System.out.println(s2);
}

Donnez les affichages réalisés à l’exécution.

"Solution"

aaaaa
aaaaaX

Il n’y a pas d’effet de bord car la ligne B crée une nouvelle instance de String.

POO – CC2 avec corrigés

POO – CC2 avec corrigés

Partie A

A1 (2 points)

On rappelle que l’exécution des lignes de code suivantes…

String s = "aaa";
s = s + 'b';
System.out.println(s);

… a pour effet l’affichage :

aaab

Donner le code d’une méthode de classe rep qui accepte en paramètre un caractère c et un entier n, et qui retourne la chaîne constituée de n occurrences de c. Par exemple, l’appel…

System.out.println(rep('x', 5));

…devra avoir pour effet l’affichage :

xxxxx
"Solution"

public static String rep(char c, int n)
{
    String r = "";
    for(int i=0; i<n; i++)
    {
        r = r + c;
    }
    return r;
}

A2 (1 point)

Donnez les lignes de code permettant d’obtenir la configuration suivante en mémoire, en utilisant la méthode rep de la question précédente. Vous pouvez répondre même si vous n’avez pas traité la question précédente ou si votre réponse est incorrecte. Le correcteur considérera que la méthode rep fonctionne comme attendu.

image-20211007125701303

"Solution"

String t = rep('a',3);
String u = t;


Partie B

On considère la classe Terrain suivante :

public class Terrain
{
    private String nom;
    private double x1; private double y1;
    private double x2; private double y2;

    public Terrain(String nom, double x1, double y1, double x2, double y2)
    {
        this.nom = nom;
        this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2;
    }
  
    public Terrain clone()
    {
        return new Terrain(nom, x1, y1, x2, y2);
    }

    public double surface()
    {
        return (x2-x1) * (y2-y1);
    }

    public String toString()

    {
        return "Terrain : " + nom + "(" + x1 + "," + y1 + ") (" + x2 + "," + y2 +")";
    }
}

Qui représente un terrain (par exemple agricole) délimité par des coordonnées géographiques exprimées en mètres dans une repère local quelconque.

La classe suivante représente un cadastre regroupant une collection de terrains.

public class Cadastre 
{
    private ArrayList<Terrain> terrains;

    public Cadastre()
    {
        this.terrains = new ArrayList<>();
    }

    public void add(double x1, double y1, double x2, double y2, String nom)
    {
        terrains.add(new Terrain(nom, x1, y1, x2, y2));
    }

    public double surface()
    {
        double r = 0.0;
        for(Terrain t : terrains)
        {
            r = r + t.surface();
        }
        return r;
    }
}

B1 (1 point)

Ajoutez à la classe Terrain une méthode renomme permettant de changer le nom du terrain courant. Le nouveau nom devra être passé en paramètre.

"Solution"

public void renomme(String nouveauNom)
{
    this.nom =nouveauNom;
}

B2 (2 points)

Réalisez un constructeur en copie pour la classe Cadastre. Ce constructeur doit permettre de créer une instances de Cadastre identique à celle désignée en argument, mais ne pouvant donner lieu à aucun effet de bord ultérieur : toute modification du cadastre original ne devra avoir aucun effet sur la copie, et réciproquement.

"Solution"

public Cadastre(Cadastre model)
{
    this();
    for(Terrain t : model.terrains)
    {
        terrains.add(t.clone());
    }
}

B3 (1 point)

Avec les constructeurs et méthodes disponibles dans les classes Terrain et Cadastre, (en supposant que le constructeur en copie soit correctement programmé – vous pouvez donc répondre à cette question même si vous n’avez pas répondu à la précédente, ou si votre réponse est incorrecte) est-il possible d’obtenir la configuration suivante en mémoire ?

image-20211006121931111

Si oui, donnez les lignes de code permettant d’obtenir cette configuration. Si non, expliquez pourquoi.

"Solution"

Non, c’est impossible car le seul moyen d’ajouter un terrain à un cadastre est d’utiliser la méthode add qui crée une copie du terrain à ajouter. On ne peut donc avoir deux instances de Cadastre contenant une référence d’une même instance de Terrain.


Partie C

Soit la classe suivante, qui représente (partiellement, par souci de concision et simplicité) un véhicule à moteur thermique.

public abstract class VehiculeThermique
{
    private double qteCarburant; // Quantité de carburant dans le réservoir
    private int charge;          // Charge actuelle en Kg
    private String nom;          // Nom du véhicule

    public VehiculeThermique(String nom)
    {
        this.nom = nom;
        this.charge = 0;
        this.qteCarburant = 0.0;
    }

    public int getCharge() { return charge;}
    public double getQteCarburant() { return qteCarburant;}
    public void setCharge(int poids) {charge = poids;}
  
    public void remplir(double qte)
    {
        qteCarburant = qteCarburant + qte;
    }

    public abstract double consommation();

    public double autonomie()
    {
        // À compléter
    }

    public String toString()
    {
        return "Véhicule consommant " + consommation() + "l / 100Km";
    }
}

La méthode consommation retourne la consommation du véhicule courant exprimée en litres par 100Kms.

La méthode autonomie retourne le nombre de kilomètres restant à parcourir avec la quantité de carburant actuellement dans le réservoir (valeur de l’attribut qteCarburant). Pour mémoire, avec \(q\) litres de carburant et une consommation de \(c\) litres au 100 Kms, on peut parcourir \(100 \: q/c\) Kms.

Soit la classe suivante, qui représente un modèle particulier de véhicule à moteur thermique appelé Fourgon à essence.

public class FourgonEssence extends VehiculeThermique
{
    private double indiceOctane;

    public FourgonEssence(String nom, double ioct)
    {
        // À compléter
    }

    public String getTypeCarburant() { return "Essence";}

    public String toString()
    {
        // À compléter
    }
}

C1 (2 points)

Donnez le code de la méthode autonomie de la classe VehiculeThermique et le code à ajouter à la classe FourgonEssence pour que la méthode autonomie fonctionne correctement, sachant qu’un fourgon à essence consomme, pour parcourir 100 Kms, 5 litres d’essence plus 0.02 litre par Kg de charge.

"Solution"

public double autonomie()
{
    return 100 * (getQteCarburant() / consommation());
}
public double consommation()
{
    return 5.0 + (0.02*getCharge());
}

C2 (1 point)

Donnez le code du constructeur de la classe FourgonEssence. Le deuxième paramètre est l’indice d’octane du carburant utilisable.

"Solution"

public FourgonEssence(String nom, double ioct)
{
    super(nom);
    this.indiceOctane = ioct;
}

C3 (1 point)

La classe FourgonEssence contient une méthode…

    public String getTypeCarburant() 
    { 
        return "Essence indice octane " + indiceOctane;
    }

…qui retourne le type de carburant utilisé.

Comment faire pour imposer à toute classe concrète dérivée de VehiculeThermique d’avoir une méthode ayant pour signature…

public String getTypeCarburant()

… en utilisant une interface ?

Précisez ce qu’il faut mettre dans l’interface et quel modification il faut apporter à la définition de la classe VehiculeThermique.

"Solution"

public interface Thermique
{
    String getTypeCarburant();
}

C4 (1 point)

Réalisez une méthode toString pour la classe FourgonEssence. Cette méthode doit appeler la méthode toString de la classe VehiculeThermique et utiliser sa valeur de retour. L’exécution des lignes de code suivantes…

VehiculeThermique v1 = new FourgonEssence("Fourgon d'Olivier", 0.98);
v1.setCharge(150);
System.out.println(v1);

…doit avoir pour effet l’affichage :

Fourgon à essence : Véhicule consommant 8.0l / 100Km
"Solution"

public String toString()
{
    return "Fourgon à essence : " + super.toString();
}


Partie D

La classe suivante représente un tableau évolué d’entiers dans lequel certaines cases peuvent être vides (ce qui est interdit dans un tableau d’entiers classique dans lequel toute case à une valeur). Attention, cette classe est un un prototype non finalisé qui n’a pas encore les comportements souhaités.

public class SuperTabInt
{
    private int[] data;
    private boolean[] flag;

    public SuperTabInt(int size)
    {
        this.data = new int[size];
        this.flag = new boolean[size];
    }

    public void write(int i, int val)
    {
        data[i] = val; flag[i] = true;
    }

    public int read(int i)
    {
        return data[i];
    }

    public String toString()
    {
        String r = "[ ";
        for(int i=0; i<data.length; i++)
        {
            if(flag[i]) r += data[i] + " ";
            else r += "# ";
        }
        return r + "]";
    }
}

Le principe de représentation est le suivant :

Soit une instance de SuperTabInt représentant un tableau évolué. Soit i un indice valide dans ce tableau évolué. Si la cellule d’indice i du tableau évolué est vide, alors flag[i] vaut false, sinon flag[i] vaut true. Dans le cas où la cellule d’indice i n’est pas vide, sa valeur est dans data[i].

Mais on ne peut évidemment pas utiliser les crochets pour accéder à une cellule d’un tableau évolué représenté par une instance de SuperTab. cette écriture avec crochets est réservée aux tableaux ordinaires. Pour modifier ou lire une cellule de tableau évolué, on doit utiliser les méthodes read et write .

Voici un exemple qui commence par la création d’un tableau évolué de 10 cellules…

SuperTabInt tab = new SuperTabInt(10);
tab.write(2, 55);
tab.write(3, 56);
System.out.println(tab.read(3));
System.out.println(tab); // Appel de toString

…qui produit les affichages suivants :

56
[ # # 55 56 # # # # # # ]

Mais en l’état, cette classe ne donne pas satisfaction. On souhaite qu’une exception de type BadAccess soit levée lors de certaines situations :

  • Tentative de lecture ou écriture à un indice négatif.
  • Tentative de lecture ou écriture à un indice au delà de la dernière cellule accessible.
  • Tentative de lecture d’une cellule vide. (Une cellule est vide tant qu’aucune valeur n’y a été écrite par la méthode write).

La classe d’exception BadAccess est définie de la manière suivante.

public class BadAccess extends Exception
{
    private int code;  // Code d'erreur
    private int index; // indice auquel l'erreur s'est produite

    public BadAccess(int code, int index)
    {
        this.code = code;
        this.index = index;
    }

    public String toString()
    {
        String msg;
        if(code==1) msg = "Negative index";
        else if(code==2) msg = "Index too large";
        else msg = "Attempt to read an empty cell";
      
        return "SuperTab error : " + msg + " at index " + index;
    }
}

Lors de la création d’une instance d’exception de cette classe, on indique un code (1, 2 ou 3) qui renseigne le type d’erreur et l’indice auquel l’erreur s’est produite.


D1 (2 points)

Réalisez des nouvelles versions des méthodes write et read de la classe SuperTabInt qui lèvent une exception de type BadAccess dans les situation décrites ci-dessus (avec les codes et indices d’erreur appropriés).

"Solution"

public void write(int i, int val) throws BadAccess
{
    if(i<0) throw new BadAccess(1, i);
    else if(i>=data.length) throw new BadAccess(2, i);
    data[i] = val; flag[i] = true;
}
public int read(int i) throws BadAccess
{
    if(i<0) throw new BadAccess(1, i);
    else if(i>=data.length) throw new BadAccess(2, i);
    else if(!flag[i]) throw new BadAccess(3, i);
    return data[i];
}

D2 (2 points)

Donnez le code complet d’une méthode main qui réalise les actions suivantes :

  1. Création d’une instance de SuperTabInt représentant un tableau évolué de 10 entiers.
  2. Écriture des valeurs 55, 56 et 33 aux indices 2, 3 et 5, respectivement.
  3. Affichage de la valeur située à l’indice 4 (comme cette case est vide, une exception sera levée par la méthode read).

Dans le cas où une exception est levée, deux actions doivent être réalisées :

  1. Affichage du tableau évolué à l’aide de la méthode toString de la classe SuperTabInt.
  2. Affichage de la chaîne produite par la méthode toString de la classe BadAccess.

En l’occurrence, avec les actions demandées, l’affichage produit serait :

[ # # 55 56 # 33 # # # # ]
SuperTab error : Attempt to read an empty cell at index 4

Mais ce message pourrait être différent pour des variantes du programme (non demandées) qui tenteraient une lecture ou écriture à un indice incorrect.

"Solution"

public static void main(String[] args)
{
    SuperTabInt tab = new SuperTabInt(10);

    try
    {
        tab.write(2, 55);
        tab.write(3, 56);
        tab.write(5, 33);
        System.out.println(tab.read(4));
    }
    catch(BadAccess e)
    {
        System.out.println("Tableau : " + tab);
        System.out.println(e);
    }
}


Partie E

Soit la méthode de classe suivante…

public static boolean sorted(List<Double> w)
{
    for(int i=0; i<w.size()-1; i++)
    {
        if(w.get(i) > w.get(i+1)) return false;
    }
    return true;
}

… qui permet de vérifier si une liste de valeurs de type Double est triée par ordre croissant (i.e. chaque valeur est au moins égale à la précédente, si applicable).

E1(1 point)

Donnez un exemple de liste de 10 valeurs qui maximise le temps d’exécution de la méthode, et d’une liste de 10 valeurs qui minimise ce temps d’exécution.

"Solution"

Donnée facile :    2.0, 1.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0
Donnée difficile : 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0

E2 (2 points)

Soit la méthode main suivante :

public static void main(String[] args)
{
    List data = new ArrayList<Double>();
		
    // Code produisant dans data une liste de 100000 valeurs
    // qui maximise le temps d'exécution de la méthode sorted
  
    System.out.println("Liste remplie");
    System.out.println(sorted(data));
}

On exécute le programme et on constate l’affichage immédiat suivant :

Liste remplie
true

La deuxième ligne semble s’afficher sans délai après la première. En pratique, il y a un délai qui inclut le temps d’exécution de la méthode sorted, mais qui est trop court pour être perceptible par un observateur humain.

On refait l’expérience en remplaçant la première ligne de code de main par la ligne suivante :

   List data = new LinkedList<Double>();

On observe le même comportement, mais il s’écoule une dizaine de secondes entre l’affichage de Liste remplie et l’affichage de true. Expliquez pourquoi. Précisez les complexités en temps dans le pire des cas de la méthode sorted avec chacune des deux structures de données ArrayList et LinkedList.

"Solution"

Avec ArrayList, l’accès à un élément situé à une position \(i\) arbitraire se fait en temps constant. La complexité de la méthode sorted est donc linéaire. Avec LinkedList, l’accès à un une position \(i\) arbitraire se fait en temps linéaire. La complexité de la méthode sorted est donc quadratique.

E3 (1 point)

Réalisez une méthode sortedOpt qui fait exactement la même chose que la méthode sorted mais en ayant le même ordre de complexité en temps dans le pire des cas pour une liste de type ArrayList et pour une liste de type LinkedList.

"Solution"

En utilisant un itérateur, on peut parcourir la liste en temps linéaire tout en comparant les éléments successifs.

public static boolean sortedOpt(List<Double> w)
{
    double current = w.get(0);
    for(double x : w)
    {
        if(x < current) return false;
        current = x;
    }
    return true;
}


POO – CC1 avec corrigés

Partie A

A1 (2 point)

Réalisez une méthode de classe…

public static void amplitude(int[] tab)
{
  // À compléter
}

…qui retourne la différence entre la plus grande et la plus petite valeur du tableau tab. Par exemple si tab contient les valeurs 5, 1, 4, 23, 4 ,4, 8, 5, alors la valeur de retour devra être 22 c’est à dire 23-1.

"Solution"

public static int amplitude(int[] tab)
{
    int mini = tab[0]; int maxi = tab[0];
    for(int i=1; i<tab.length; i++)
    {
        if(tab[i] > maxi) maxi = tab[i];
        else if(tab[i] < mini) mini = tab[i];
    }
    return  maxi - mini;
}

A2 (1 point)

Réalisez une méthode main qui crée un tableau d’entier contenant les valeurs de l’exemple précédent, qui appelle la méthode amplitude avec ce tableau et affiche le résultat.

"Solution"

public static void main(String[] args)
{
    int[] tab = {5, 1, 4, 23, 4, 4, 8, 5};
    System.out.println(amplitude(tab));
}


Partie B

Soit une classe Terrain représentant un terrain agricole de forme rectangulaire localisé par les coordonnées géographiques (qui sont des valeurs de types double, en mètres) de deux de ses coins opposés. A cette fin, la classe Terrain a 4 attributs privés de type double nommés x1, y1, x2, y2. Elle possède en outre un attribut privé nom de type String qui permet d’attribuer un nom à chaque terrain.

La classe Terrain dispose d’un constructeur Terrain(double x1, double y1, double x2, double y2, String nom) permettant la création d’une instance à l’aide des informations fournies en paramètres. Elle dispose également d’une méthode toString retournant une description textuelle du terrain courant. On supposera que x1 < x2 et y1 < y2.

public class Terrain
{
    private String nom;
    private double x1; private double y1;
    private double x2; private double y2;

    public Terrain(String nom, double x1, double y1, double x2, double y2)
    {
        this.nom = nom;
        this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2;
    }

    public String toString()

    {
        return "Terrain : " + nom + "(" + x1 + "," + y1 + ") (" + x2 + "," + y2 +")";
    }
}

Soit une classe Cadastre représentant une liste de plusieurs terrains. Nous appellerons cadastre toute instance de cette classe. Cette classe a un attribut privé terrains de type ArrayList<Terrain> qui permet le stockage de toutes les instances de Terrain du cadastre courant.


B1 (1 point)

Donnez la définition du constructeur de la classe Cadastre.

"Solution"

public Cadastre()
{
    this.terrains = new ArrayList<>();
}

B2 (1 point)

Donnez la définition d’une méthode d’instance public void add(double x1, double y1, double x2, double y2, String nom) de la classe Cadastre qui ajoute un nouveau terrain à la liste.

"Solution"

public void add(double x1, double y1, double x2, double y2, String nom)
{
    terrains.add(new Terrain(nom, x1, y1, x2, y2));
}

B3 (1 point)

On suppose que la classe Terrain a une méthode d’instance public double surface() qui retourne la surface du terrain courant.

Donnez la définition d’une méthode d’instance public double surface() de la classe Cadastre qui retourne la somme des surfaces des terrains du cadastre courant.

"Solution"

public double surface()
{
    double r = 0.0;
    for(Terrain t : terrains)
    {
        r = r + t.surface();
    }
    return r;
}

B4 (1point)

Donnez les lignes de codes, supposées être dans une méthode de classe void test() située dans une classe Test, permettant de créer un cadastre, d’y ajouter deux terrains, et d’afficher la surface cumulée des terrains du cadastre.

"Solution"

Cadastre cad = new Cadastre();
cad.add(100,200,120,230,"parcelle 1");
cad.add(560,320,570,330,"parcelle 2");
System.out.println(cad.surface());

Partie C

On considère les classes et interface suivantes.

image-20210926101630425


La classe Point représente juste un point avec deux coordonnées de type double.

public class Point
{
    private double x,y;

    public Point(double x, double y) { ... }

    public String toString() { ... }
}

La classe Courbe représente une liste de points pouvant être interprétée comme une courbe si on imagine que chaque point est relié au suivant (si applicable).

public class Courbe
{
    private ArrayList<Point> points;

    public Courbe(){ ... }

    public void add(double x, double y) { ... }

    public String toString() { return }
}

Voici une représentation graphique de l’instance de courbe représentée par la liste de points [(1.0,1.0), (2.0,3.0), (3.0,2.0)].

image-20210926104152664

Ceci est juste donné à titre d’exemple. La partie affichage graphique n’est pas du tout traitée dans cet exercice.


L’interface Graphable oblige les classes qui l’implémentent à être dotées d’une méthode permettant de produire une instance de courbe représentée par une liste de n points dont les abscisses (coordonnées \(x\)) vont de min à max avec des écarts constants entre ces abscisses.

Par exemple si n vaut 5, min vaut 10.0 et max vaut 12.0, les abscisses des 5 points devront être 10.0, 10.5, 11.0, 11.5 et 12.0.

public interface Graphable
{
    public Courbe getCourbe(double min, double max, int n);
}

La classe abstraite Fonction représente une fonction calculable. Elle dispose d’une méthode abstraite getImage qui accepte en paramètre une valeur x et retourne l’image de x par cette fonction. Par exemple, dans une classe dérivée de Fonction qui représente la fonction \(x \mapsto x^2\), l’appel getImage(3.0) retournera 9.0.

public abstract class Fonction implements Graphable
{
    public abstract double getImage(double x);

    public Courbe getCourbe(double min, double max, int n)
    {
        // À compléter
    }
}

C1 (2 points)

Implémentez la méthode getCourbe de la classe Foncton.

"Solution"

public Courbe getCourbe(double min, double max, int n)
{
    double delta = (max - min) / (n - 1);
    double x = min;
    Courbe r = new Courbe();
    for(int i = 0; i< n; i++)
    {
        r.add(x, getImage(x));
        x = x + delta;
    }
    return r;
}

C2 (1 point)

Réalisez une classe Xcarre sans attribut, dérivée de Fonction représentant la fonction qui à tout réel \(x\) associe \(x^2\).

"Solution"

public class Xcarre extends Fonction
{
    public double getImage(double x)
    {
        return x * x;
    }
}

C3 (2 points)

On souhaite que la méthode main suivante…

public static void main(String[] args)
{
    Fonction f = new Xcarre();
    System.out.println(f.getNom());
}

…produise l’affichage

Fonction carré

Donnez le code complet de la ou des méthodes concrètes et / ou abstraites à ajouter aux classes Fonction et Xcarre pour obtenir ce résultat sans ajouter d’attribut à ces classses.

"Solution"

Dans Fonction :

public abstract String getNom();

Dans Xcarre :

public String getNom()
{
    return "Fonction carré";
}

Partie D

La classe Fraction représente une fraction (un nombre rationnel) ayant un dénominateur et un numérateur.

public class Fraction
{
    private int num;
    private int den;

    public Fraction(int numerateur, int denominateur) throws  DenZero
    {
        if(denominateur == 0) throw new DenZero();
        this.num = numerateur;
        this.den = denominateur;
        normalise();
    }

    public int getDenum() {return den;}

    public int getNum() {return num;}

    public void normalise()
    {
        // Code classifié très secret cosmique
    }

    public Fraction add(Fraction f)
    {
        // À comlpléter
    }

    public String toString()
    {
        return num + " / " + den;
    }
}

Regardez attentivement le code du constructeur. Il lève une exception de type DenZero si par malheur on tente de l’utiliser pour créer une fraction ayant un dénominateur nul. Dans le cas contraire, il initialise les attributs et appelle une méthode de normalisation. Le code de cette méthode est ultra secret et ne sera pas révélé ici. Il permet de normaliser la fraction de manière à ce que le dénominateur soit toujours positif et de la simplifier de manière à minimiser les valeurs du numérateur et du dénominateur. Par exemple, si les valeurs données sont 4 et 12, après simplification au aura 1 / 3.

La classe DenZero est définie ainsi :

public class DenZero extends Exception
{
    public String toString()
    {
        return "Dénominateur nul";
    }
}

D1 (2 points)

Complétez le fonction testFrac suivante de manière à ce qu’elle crée une instance de Fraction représentant la fraction \(a/b\) et affiche sa représentation à l’aide de la méthode toString. Dans le cas où le constructeur de la classe Fraction lève une exception de type DenZero, le message "ERREUR : Dénominateur nul" doit s’afficher.

public static void testFrac(int a, int b)
{
    // À compléter
}
"Solution"

public static void testFrac(int a, int b)
{
    try
    {
        Fraction r = new Fraction(a,b);
        System.out.println(r.toString());
    }
    catch(DenZero e)
    {
        System.out.println("ERREUR  : " + e);
    }
}

D2 (2 points)

On rappelle que la somme de deux fractions valides (avec dénominateurs non nuls) \(a/b\) et \(c/d\) a pour résultat la simplification (avec la méthode normalize) de \((ad + bc) / bd\).

Complétez une méthode add de la classe Fraction en faisant en sorte qu’elle retourne la somme de la fraction courante et de cette désignée par le paramètre f. La fraction courante ne doit pas être modifiée. Si l’exception DenZero est levée lors de la construction du résultat (Ça ne devrait pas se produire, mais il faut le prévoir quand même), la méthode add doit lever une exception de type RuntimeException avec message "Erreur inattendue" passé en argument à son constructeur.

public Fraction add(Fraction f)
{
    // À compléter
}
"Solution"

public Fraction add(Fraction f)
{
    int n = (getNum() * f.getDenum()) + (f.getNum() * getDenum());
    int d = getDenum() * f.getDenum();
    try
    {
        return new Fraction(n, d);
    }
    catch (DenZero e)
    {
        throw new RuntimeException("Erreur inattendue");
    }
}

Partie E

Un multi-ensemble est une structure de donnée pouvant contenir des éléments, comme un ensemble, mais à la différence d’un ensemble, chaque élément peut avoir plusieurs occurrences. Par exemple, {2, 5, 5, 56} est un multi-ensemble dont les éléments sont 2, 5 et 56. L’élément 5 a deux occurrences, alors que les autres en ont une seule.

La classe MultiInt représente un multi-ensemble d’entiers naturels (c’est à dire supérieurs ou égaux à 0).

public class MultiInt
{
    // Attributs à completer

    public MultiInt(int max)
    {
        // À compléter
    }

    public void add(int e)
    {
				// À compléter
    }

    public void remove(int e)
    {
				// À compléter
    }
    // ...
}

La classe MultiInt comporte d’autres méthodes, mais seules celles qui sont mentionnées plus haut nous intéressent dans cet exercices.

Le constructeur permet de créer un multi-ensemble pouvant contenir les éléments compris entre 0 et max, chacun pouvant n’avoir aucune occurrence ou avoir un nombre quelconque d’occurrences.


E1 (4 points)

Vous devez donner le ou les attributs de la classe et le code du constructeur et des deux méthodes. Si e est un entier compris entre 0 et max et m est une instance de MultiInt construite de la manière suivante…

MultiInt m = new MultiInt(max);

…alors :

  • m.add(e) ajoute une occurrence de e dans le multi-ensemble désigné par m,
  • m.remove(e) retire une occurrence de e s’il en reste au moins une et sinon n’a aucun effet.

Vous devez essayer de trouver une solution permettant de minimiser la complexité en temps (en fonction du nombre d’éléments du multi-ensemble courant) des méthodes add et remove. Vous devez également donner l’ordre de grandeur asymptotique des ces complexité (constante ou logarithmique ou linéaire ou quadratique ou autre …) en justifiant brièvement votre réponse.

"Solution"

public class MultiInt
{
    private int[] tab;

    public MultiInt(int max)
    {
        tab = new int[max+1];
    }

    public void add(int e)
    {
        tab[e]++;
    }

    public void remove(int e)
    {
        if(tab[e]>0) tab[e]--;
    }

    public int count(int e)
    {
        return tab[e];
    }
}

Complexité :

  • add : \(\Theta(1)\) (temps constant)
  • remove : \(\Theta(1)\) (temps constant)


POO : Test Partie C

POO : Test Partie C

En licence pro MI AW

Par Olivier Bailleux, Maître de conférences HDR, Université de bourgogne


On considère l’interface suivante :

public interface RandGen
{
    public void setMin(double min);
    public void setMax(double max);
    public double getMin();
    public double getMax();
    public String getLaw();
    public double draws();
}

Les classes qui implémentent cette interface ont vocation à produire des nombres aléatoires de type double compris entre deux valeurs pouvant être déterminées par les méthodes setMin et setMax et pouvant être consultées à l’aide des méthodes getMin et getMax. Ces classes doivent aussi disposer d’une méthode getLaws qui retourne une chaîne de caractères décrivant la loi de probabilité utilisée et d’une méthode draws dont le rôle est de produire une valeur aléatoire dans l’intervalle getMin() .. getMax().


La classe abstraite GenericRandGen implémente l’interface RandGen.

public abstract class GenericRandGen implements RandGen
{
    private double min;
    private double max;

    public GenericRandGen()
    {
        this.min = 0.0; this.max = 1.0;
    }

    public void setMin(double min) {this.min = min;}

    public void setMax(double max) {this.max = max;}

    public double getMin() {return this.min;}

    public double getMax() {return this.max;}

    public abstract String getLaw();

    public abstract double draws();

    public String toString()
    {
        return "Générator " + getLaw() + " in [" + min + "," + max + "]";
    }

    public double[] sample(int n)
    {
        // À compléter
    }
}

La méthode sample produit un tableau de longueur n rempli de valeurs alétoires produites par la méthode draws.

Q1 (1.5 point)

Complétez la méthode sample de la classeGenericRandGen.

public double[] sample(int n)
{
    // À compléter
}

Merci d’envoyer votre réponse à Olivier Bailleux via teams.

Q2 (1.5 points)

Complétez la classe UniformGen qui représente un générateur aléatoire permettant de produire des valeurs selon une loi de probabilité uniforme. Ces valeurs peuvent être obtenues à partir de la méthode Math.random() qui produit une valeur comprise entre 0.0 et 1.0 selon une loi uniforme.

public class UniformGen extends GenericRandGen
{
    // À compléter
}

Merci d’envoyer votre réponse à Olivier Bailleux via teams.

Q3 (2 points)

Réalisez une méthode main, situé dans une classe Main, qui :

  • Crée un générateur de type UniformGen.
  • Initialise à 1.0 et 3.0 les bornes minimum et maximum de l’intervalle des valeurs pouvant être produites par ce générateur.
  • Utilise la méthode sample pour produire une tableau de 10 nombres aléatoires.
  • Affiche à l’écran les valeurs récupérées dans ce tableau.
public class Main
{
    public static void main(String[] args)
    {
        // À compléter
    }
}

Merci d’envoyer votre réponse à Olivier Bailleux via teams.

Java : test partie B

Test partie B

Classes et objets

Introduction

On souhaite réaliser une classe permettant de gérer des tableaux extensibles de valeurs Booléennes. Par tableau extensible, on entend qu’il est possible de lire ou d’écrire une valeur au-delà de la taille d’un tableau, ce que ne permettent pas les tableaux classiques.

Prenons un exemple. Je crée (dans une méthode) un tableau classique de boolean de longueur 5 comme ceci.

boolean[] tb = new boolean[5];

J’obtiens un tableau de 5 cellules, numérotées de 0 à 4, qui contiennent toutes la valeur false (c’est la valeur par défaut des cellules lorsqu’on crée un tableau de valeurs de type boolean).

Je peux facilement changer la valeur d’une cellule d’indice entre 0 et 4, mais si je tente d’écrire une valeur dans la cellule d’indice 7…

tb[7] = true;

… le programme s’arrête avec pertes et fracas en affichant un message d’erreur. Même problème si je tente de lire la valeur tb[10], par exemple.

Avec un tableau extensible, ces opérations sont autorisées. Imaginons que nous disposions d’une classe BoolExt représentant un tableau extensible de boolean. Pour créer un tel tableau, j’écris…

BoolExt te = new BoolExt();

… ce qui crée un tableau extensible de taille 1. Ce tableau n’a qu’une seule cellule et elle a la valeur false. Je ne peux pas l’utiliser avec les crochets car il ne s’agit pas d’un vrai tableau mais d’une instance de la classe BoolExt. Pour écrire ou lire des valeurs dans ce tableau extensible, je dois utiliser des méthodes spécifiques.

Je peux écrire une valeur à la position de mon choix, même au-delà de la taille actuelle du tableau, avec la méthode write. Par exemple…

te.write(4, true);

…a pour effet d’agrandir le tableau et de placer la valeur true en position 4. L’objet désigné par la variable te représente alors le tableau suivant.

Au lieu de produire une erreur, l’écriture dans la cellule d’indice 4 a provoqué l’agrandissement du tableau et les nouvelles cellules crées entre les indices 0 et 4 ont été remplie avec la valeur false.

Pour lire la valeur contenue dans une cellule, je dois utiliser la méthode read. Par exemple…

System.out.println(te.read(2));
System.out.println(te.read(4));

… produit l’affichage false true.

Mais il y a mieux encore. J’ai le droit de lire le contenu d’une cellule située au-delà de la fin du tableau. Au lieu de produire une erreur comme avec les tableaux ordinaires, cette opération retourne false, comme si le tableau avait une longueur illimitée et que toutes les valeurs situées après le dernier true étaient false. Ainsi, par exemple, la ligne…

System.out.println(te.read(7));

… produit l’affichage false.

Le fait d’écrire false à un emplacement situé après le dernier true, comme par exemple…

te.write(11, false);

…est équivalent à ne rien faire du tout puisque la position 11 est située au-delà de la position du dernier true (qui est à l’indice 4) et que toute lecture à cette position retournera false.


Récapitulons.

Tout se passe comme si un tableau extensible comportait une partie concrète, effectivement représentée en mémoire par un tableau de boolean, prolongé par une partie imaginaire infinie, non représentée en mémoire, dont toutes les cellules sont supposées contenir la valeur false.

img

On appelle taille du tableau le nombre de cellules de la partie concrète. Cette parie concrète sera effectivement représentée en mémoire par un tableau de boolean désigné par un attribut de la classe BoolExt. La partie imaginaire est le prolongement imaginaire infini de la partie concrète et toutes les cellules de cette partie imaginaire sont supposées contenir la valeur false.

Attention ! Il n’y a pas nécessairement la valeur true dans la dernière cellule de la partie concrète car pour ne pas compliquer le code, l’écriture d’une valeur false à la place du dernier true ne provoque pas le rétrécissement de la partie concrète.

Par exemple, à l’issue de l’exécution du code suivant…

BoolExt te = new BoolExt();
te.write(4, true);
te.write(3, true);
te.write(4, false);

… la partie concrète du tableau est la suivante.

Elle a une longueur égale à 5 parce qu’à un moment donné, la valeur true la plus éloignée du début était en position 4, bien qu’elle ait ensuite été remplacée par false.

Q1

Vous devez compléter la classe BoolExt en donnant les codes du constructeur et des méthodes adjust, et read. Pour comprendre le rôle de la méthode ajust, analysez le code de la méthode write.

public class BoolExt
{
   private boolean[] data;
 
   public BoolExt() 
   {
     // À compléter  
   }
 
   private void adjust(int newSize) 
   {
     // À compléter  
   }
 
   public void write(int index, boolean value) 
   {
      if((index >= data.length) && (value == true))
      {
        adjust(index+1);
      }
      data[index] = value;
   }
 
   public boolean read(int index) 
   {
     // À compléter 
   }
 
   public int size() 
   {
     return data.length;
   }
 
   public String toString()
   {
     String r = "";
     for(int i=0; i < data.length; i++)
     {
       if(data[i]) r = r + "1";
       else r = r + "0";
     }
     return r;
   }
}

La méthode ajust augmente la taille de la partie concrète, c’est-à-dire la taille du tableau désigné par l’attribut data, de manière à ce que la nouvelle taille soit égale à la valeur du paramètre newSize. En pratique, java ne permet pas de redimensionner un tableau, donc cette opération suppose de créer un nouveau tableau plus grand dans lequel le contenu de l’ancien tableau est recopié, puis de remplacer l’ancien tableau par le nouveau.

Cette méthode adjust est appelée par la méthode write lors d’une écriture d’une valeur true dans une cellule située au-delà de la limite de la partie concrète. (Dans le cas de l’écriture d’une valeur false, un agrandissement n’est pas nécessaire puisque toutes les valeurs situées dans la partie imaginaire sont considérées comme false).

Notez que la méthode toString utilise les caractères 0 et 1 à la place de false et true pour des raisons de lisibilité et concision.


Commencez par le constructeur, qui doit créer un tableau extensible de une cellule contenant false.

Merci de transmettre votre réponse au responsable de l’unité d’enseignement via Teams.


Ensuite, donnez le code de la méthode adjust qui augmente la taille du tableau désigné par l’attribut data. En pratique, on ne peut par rallonger un tableau donc la seule solution est de le remplacer par un tableau plus grand. Mais il faut recopier le contenu de l’ancien tableau dans le nouveau.

Merci de transmettre votre réponse au responsable de l’unité d’enseignement via Teams.


Maintenant, donnez le code de la méthode read, en prenant en compte le fait que l’indice de la cellule à lire peut se situer au delà de la partie concrète du tableau, et que dans ce cas la valeur à retourner est false.

Merci de transmettre votre réponse au responsable de l’unité d’enseignement via Teams.

Q2

Réalisez un constructeur en copie pour la classe BoolExt. Ce constructeur doit accepter en paramètre une instance de BoolExt et créer une nouvelle instance identique mais complètement indépendante en mémoire.

Merci de transmettre votre réponse au responsable de l’unité d’enseignement via Teams.

Q3

Dessinez la représentation en mémoire, dans la pile et le tas, des données créées par les lignes de code suivantes.

BoolExt t1 = new BoolExt() ;
t1.write(5, true);
t1.write(1, true);
BoolExt t2 = t1;
BoolExt t3 = new BoolExt(t1);

Merci de transmettre votre réponse au responsable de l’unité d’enseignement via Teams.

Java : test partie A

Partie A

Programmation procédurale en Java

Q1

public class Main
{
    public static void test()
    {
       System.out.println("Bonjour");
    }
    public static void main(String[] args)
    {
       System.out.println(test());
    }
}

Ce programme compile-t-il ? Si oui qu’est ce qui est affiché à son exécution ? Si non pourquoi ?

"Réponse"

Il ne compile pas car la méthode test ne retourne rien (void) et sa valeur de retour ne peut donc être affichée, dans main, par la méthode d’affichage printf.

Puisque l’affichage est réalisé par la méthode test, la méthode main doit juste appeler cette méthode test.

public static void main(String[] args)
{
   test();
}

Q2

public class Main
{
    public static String test()
    {
       return System.out.println("Bonjour");
    }
    public static void main(String[] args)
    {
       System.out.println(test());
    }
}

Ce programme compile-t-il ? Si oui qu’est ce qui est affiché à son exécution ? Si non pourquoi ?

"Réponse"

Il ne compile pas car la méthode println ne retourne rien (void) alors que la méthode test est censée retourner une chaine. Une version correcte de cette méthode est :

public static String test()
{
   return "Bonjour";
}

Q3

Soit la méthode suivante :

    public static int m1(int k)
    {
        int sum = 0;
        for(int i=1; i<=k; i=i+2)
        {
            sum = sum + i;
        }
        return sum;
    }

Quelle est la valeur retournée par m1(3) et par m1(5) ?

"Réponse"

4 et 9

Q4

Soit la classe suivante.

public class Main
{
    public static int[] tab = new int[5];

    public static void printTabInt(int[] t)
    {
        for(int i=0; i<t.length; i++)
        {
            System.out.print(t[i] + " ");
        }
        System.out.println();
    }

    public static void test1()
    {
        int[] t = new int[5];
        t = tab; t[2] = 12;
        printTabInt(tab);
    }

    public static void test2()
    {
        int[] t = tab;
        t[2] = 12;
        printTabInt(tab);
    }

    public static void main(String[] args)
    {
        test1(); test2();
    }
}

Ce programme compile-t-il ou comporte-t-il une erreur ?

"Réponse"

Il compile mais il comporte néanmoins une maladresse, quelque chose d’inutile. Essayez de trouver quoi avant de répondre à la questions suivante.

Les deux appels à test1 et test2 produisent-ils exactement le même affichage à l’écran ?

"Réponse"

Oui. Mais dans test1, l’initialisation de la variable de méthode t est inutile car elle crée un tableau de 5 entiers qui ne sera jamais utilisé.

    public static void test1()
    {
        int[] t = new int[5];   // ligne A
        t = tab;                // ligne B
        t[2] = 12;
        printTabInt(tab);
    }

Voici les données en mémoire juste après l’exécution de la ligne A.

image-20210920115817844

Et juste après l’exécution de la ligne B.

image-20210920115931325

Q5

Soit la classe suivante.

public class Main
{
    public static int[] tab = new int[5];

    public static void printTabInt(int[] t)
    {
        for(int i=0; i< t.length; i++)
        {
            System.out.print(t[i] + " ");
        }
        System.out.println();
    }

    public static void test1()
    {
        int[] t = new int[5];
        t[3] = 5;
        t = tab; 
        t[2] = 12;
        printTabInt(t);
        printTabInt(tab);
    }

    public static void main(String[] args)
    {
        test1();
    }
}

Donnez les affichages réalisés par l’exécution du programme.

"Réponse"

0 0 12 0 0 
0 0 12 0 0 

Q6

Soit les lignes de code suivantes :

String[] t = new String[2];
String s = "Tim";
t[0] = s; t[1] = s;

Dessinez les données en mémoire à l’issue de l’exécution de ces lignes.

"Réponse"

image-20210920123626822

Q7

Soit les lignes de code suivantes :

String[] t = new String[2];
String s = "Tim";
t[0] = s; 
t[1] = t[0] + "Tom";

Dessinez les données en mémoire à l’issue de l’exécution de ces lignes.

"Réponse"

image-20210920124007347


Vous avez commis des erreurs ? N’ayez aucune inquiétude. Le but de ce test était de vous faire commettre quelques erreurs pour attirer votre attention sur certaines subtilités. Vous devez vous expliquer à vous-même (vous auto-expliquer) ces erreurs et comment il fallait raisonner pour les éviter.

Au besoin, reprenez les ressources pédagogiques de la partie A pour revoir les points problématiques. N’hésitez pas à poser des questions à votre enseignant. Après avoir bien compris vos erreurs, retaites le test, mais pas tout de suite ! Attendez au moins une semaine, voire deux.

Java en LP : partie D

Partie D : Les exceptions

Objectif

Être capable d’implémenter au sein d’une application telle que celles décrites dans les objectifs des parties B et C une gestion des exceptions : implémentation de classes d’exception, levée et interception d’exceptions. Être capable de prévoir le comportement d’un programme dans lequel des exceptions peuvent être produites et interceptées.

Prérequis

Partie C


D1 : Lancer et gérer une exception

Java permet à une méthode ou un constructeur de lancer une exception lorsqu’une situation particulière se produit lors de son exécution. Une exception est un objet, plus précisément une instance d’une classe dérivant de la classe prédifinie Exception.

Nous allons découvrir ce ce concept avec un exemple. La classe ci-dessous représente une liste de notes obtenues par un élève à une unité d’enseignement.

public class SuiviUE
{
    private String idUE;  //Intitulé de l'UE
    private double coeff; //Coefficient
    private ArrayList<Double> notes;

    public SuiviUE(String nom, double coeff)
    {
        this.coeff = coeff;
        this.idUE = nom;
        this.notes = new ArrayList<>();
    }

    public void add(double note)
    {
        notes.add(note);
    }

    public String toString()
    {
        return "UE : " + idUE + " coefficient : " + coeff
                + " notes " + notes;
    }
}

On y trouve un constructeur créant une liste de note vide, une méthode pour ajouter une note, et une méthode pour afficher une description de l’instance courante. Une schéma très classique qui correspond aux compétences introduites dans la partie B du cours.

A titre d’exercice de rappel, ajoutez à la classe SuiviEU une méthode moyenne qui calcule et retourne la moyenne des notes situées dans la liste.

"Solution"

public double moyenne()
{
    double sum = 0.0;
    for(double x : notes) sum += x;
    return sum / notes.size();
}

Faisons maintenant l’expérience suivante, qui consiste à calculer la moyenne d’une instance de suiviUE à laquelle aucune note n’a été ajoutée.

public class Main
{
    public static void main(String[] args)
    {
        SuiviUE r = new SuiviUE("Java", 2.0);
        System.out.println(r.moyenne());
    }
}

On obtient l’affichage…

NaN

…qui signifie "Non Arithmetic Number". C’est ce qui se produit, dans beaucoup de langages de programmation, lors d’une tentative de division par 0 d’un nombre flottant (de type double).

Nous allons changer ce comportement en faisant en sorte que lorsqu’il n’y a pas de note, une exception soit levée, puis nous allons montrer comment exploiter cette exception. Mais pour commencer, il nous faut une classe dérivant de Exception.

public class Defaillance extends Exception
{
    private String id;

    public Defaillance(String ue)
    {
        //super();           // ligne A
        this.id = ue;
    }

    public String getID()
    {
        return id;
    }
}

Cette classe ne comporte pas d’attribut (mais on pourrait lui en ajouter si nécessaire) ni de méthode (mais on pourrait aussi en ajouter en cas de besoin). Son constructeur, qui permet de créer un objet de type exception, fait appel à un constructeur de la superclasse Exception qui accepte en argument une chaîne de caractères. Cette chaîne pourra être récupérée avec la méthode toString héritée de la classe Exception.

Si on enlève la mise en commentaire de la ligne A, le constructeur par défaut de la superclasse Exception est appelé explicitement, alors que sinon il est appelé implicitement, ce qui en pratique ne change rien.

Voici une nouvelle version de la méthode moyenne qui lève une exception de type Defaillance lorsque la liste de notes est vide.

public double moyenne() throws Defaillance // ligne A
{
    if(notes.size() == 0)
    {
        throw new Defaillance(idUE);       // ligne B
    }
    double sum = 0.0;
    for(double x : notes) sum += x;
    return sum / notes.size();
}

Deux nouveaux mot-clés importants apparaissent ici :

  • throws permet de dire au compilateur java que la méthode moyenne est susceptible de produire une exception du type indiqué. Il est possible d’indiquer ici plusieurs types d’exceptions.
  • throw permet de lever (lancer, produire) une exception et doit être suivi par une instance d’exception, généralement produite avec new et un constructeur d’une classe d’exception.

À chaque fois qu’une méthode susceptible de lancer une exception est appelée par une méthode m, la méthode m doit :

  • soit intercepter l’exception et la gérer,
  • soit la remonter à la méthode ayant appelé m.

Nous allons illustrer le premier cas.

public class Main
{
    public static void main(String[] args)
    {
        SuiviUE r = new SuiviUE("Java", 2.0);
        // r.add(12.0); r.add(17.0);  // ligne A
        try                           // ligne B
        {
            System.out.println(r.moyenne());
        }
        catch(Defaillance d)          // ligne C
        {
            System.out.println("Défaillance à l'UE : " + d.getID());
            return;
        }
        System.out.println(r);
    }
}

L’instruction trycatch permet d’intercepter les exceptions d’un ou plusieurs types qui se produisent dans le bloc de code try et et définir les actions à réaliser si de telles exceptions se produisent. Il peut y avoir plusieurs blocs catch permettant de spécifier des traitement spécifiques à différents types d’exceptions susceptibles de se produire lors de l’exécution du code du bloc try.

À l’issue de l’exécution du try catch, l’exécution du code de la méthode reprend son cours, qu’il y ait eu exception ou pas, sauf si le traitement d’une exception a mis fin à cette exécution par une instruction return, ce qui est le cas dans l’exemple.

Exercices de découverte

D-dec-10

Tentez de déterminer ce que va afficher le programme d’exemple ci-dessus, en prenant en compte que la ligne A est un commentaire, qui ne s’exécutera pas.

"Solution"

Défaillance à l'UE : Java

D-dec-11

Tentez de déterminer ce que va afficher le programme d’exemple ci-dessus si on retire la mise en commentaire de la ligne A.

"Solution"

14.5
UE : Java coefficient : 2.0 notes [12.0, 17.0]

D-dec-12

Tentez de déterminer ce que va afficher le programme d’exemple ci-dessus si on remet en commentaire la ligne A, comme dans l’exercice D-dec-10, et qu’on supprime le return.

"Solution"

Défaillance à l'UE : Java
UE : Java coefficient : 2.0 notes []

D-dec-13

On remplace la ligne C par :

        catch(Exception d)          // ligne C

On refait les expériences décrites dans les trois exercices précédentes et on obtient exactement les mêmes résultats. Pourquoi ?

"Solution"

Comme la classe Defaillance hérite de la classe Exception, toute instance de Defaillance est aussi une instance de Exception et sera donc capturée.

D2 : Plus loin avec les exceptions

Dans la section précédente, nous avons introduit une classe SuiviUE qui représente une liste de notes attribuées à un même élève (non désigné) pour une même unité d’enseignement identifiée par une chaîne de caractères et à laquelle est associé un coefficient.

Dans cette section, nous reprenons la même classe mais avec une petite modification : les attributs n’ont plus les droits d’accès private mais par défaut, c’est à dire qu’ils sont accessibles depuis toutes les classes du package dans lequel se trouve la classes SuiviUE.

public class SuiviUE
{
    String idUE;  //Intitulé de l'UE
    double coeff; //Coefficient
    ArrayList<Double> notes;

    public SuiviUE(String nom, double coeff)
    {
        this.coeff = coeff;
        this.idUE = nom;
        this.notes = new ArrayList<>();
    }

    public void add(double note)
    {
        notes.add(note);
    }

    public String toString()
    {
        return "UE : " + idUE + " coefficient : " + coeff
                + " notes " + notes;
    }

    public double moyenne() throws Defaillance
    {
        if(notes.size() == 0)
        {
            throw new Defaillance(idUE);
        }
        double sum = 0.0;
        for(double x : notes) sum += x;
        return sum / notes.size();
    }
}

Nous allons ajouter à ce package une nouvelle classe SuiviEtu qui représente les notes d’un même élève, identifié par un numéro, dans plusieurs unités d’enseignement. Les notes de cet élève, ou cette élève, pour chaque unité d’enseignement auquel il est inscrit (ou elle est inscrite), sont stockées dans une instance de SuiviUE.

Nous avons là une structure de donnée un peu complexe qui peut être difficile à bien visualiser mentalement en lisant la description ce-dessus. Pour mieux visualiser les données représentées et leur organisation, voici une représentation graphique qui correspond à un exemple dans lequel :

  • Il y a une instance de SuiviEtu représentant un élève identifié par le numéro 1233, qui est inscrit à deux unités d’enseignement : Java et BD.
  • Pour chacune de ces deux unités d’enseignement (UE en abrégé), il y a une instance de SuiviUE qui contient l’intitulé de l’UE concernée, son coefficient, et les notes de l’élève 1233 pour cette UE.

image-20210912230610160

Avant d’écrire le code de la classe SuiviUE, nous allons voir comment elle devra être utilisée pour créer les données représentées ci-dessus, mais, dans un premier temps, sans nous soucier de la gestion des exceptions.

public class Main
{
    public static void main(String[] args)
    {// Attention, les exceptions ne sont pas gérées
        SuiviEtu fiche = new SuiviEleve(1233);
        fiche.addUE("Java", 2.0);
        fiche.addUE("BD", 3.0);

        fiche.addNote("Java", 12.0);
        fiche.addNote("Java", 17.0);
        fiche.addNote("BD", 14.0);
        System.out.println(fiche.moyenne());
    }
}

Appelons élève courant la personne apprenante désignée par le numéro passé en paramètre au constructeur de la classe SuiviEtu On voit que la classe SuiviEtu devra disposer d’un constructeur, d’une méthode addUE pour ajouter une unité d’enseignement au suivi de l’élève courant, d’une méthode addNote pour ajouter une note de l’élève dans une des UE auxquelles l’élève courant est inscrit, et une méthode moyenne permettant de calculer la moyenne général de l’élève courant.

Examinons à présent le détail du code de la classe SuiviEtu en considérant différentes situation pouvant produire des exceptions (il nous faudra donc les gérer dans la version définitive de notre fonction main donnée en exemple ci dessus). Procédons par étape. Voici une première partie de la classe SuiviEtu dans laquelle il n’y a ni levée, ni gestion d’exception.

public class SuiviEtu
{
    private int numEtu;
    private ArrayList<SuiviUE> suivi;

    public SuiviEtu(int num)
    {
        this.numEtu = num;
        this.suivi = new ArrayList<>();
    }

    public void addUE(String idUE, double coeff)
    {
        suivi.add(new SuiviUE(idUE, coeff));
    }

    // À compléter
}

Vérifiez que vous comprenez bien ce code. Le constructeur est assez classique. La méthode addUE permet d’ajouter une UE à laquelle l’élève courant est inscrit. Pour se faire, elle crée une instance de SuiviUE et elle l’ajoute à la liste des UE suivies par l’élève courant, qui est désignée par l’attribut suivi. N’allez pas trop vite. Si vous ne comprenez pas de manière limpide, revenez en arrière et regardez l’illustration basée sur l’exemple présenté plus haut.

À présent regardons le code de la méthode moyenne permettant de calculer la moyenne générale de l’élève courant.

public double moyenne() throws Defaillance     // ligne A
{
    double sum = 0.0; double sumCoeffs = 0.0;
    for(SuiviUE sue : suivi)
    {
        sum += sue.moyenne() * sue.coeff;
        sumCoeffs += sue.coeff;
    }
    return sum / sumCoeffs;
}

Il y a une nouveauté. Cette méthode appelle la méthode moyenne de la classe suiviUE, qui est susceptible de produire une exception de type Defaillance. Mais au lieu d’intercepter cette exception avec un trycatch, elle la fait remonter en indiquant throws Defaillance (ligne A). Ceci signifie que c’est la méthode qui appelle la méthode moyenne de la classe SuiviEtu qui va devoir soit gérer cette exception, soit la faire remonter.

Mais tout ceci ne nous sert à rien tant qu’on ne peut pas attribuer de note à l’élève courant, ce qui est le rôle de la méthode addNote de la classe SuiviEtu Cette méthode devra rechercher dans la liste des UE auxquelles est inscrit l’élève courant (désignée par l’attribut suivi) l’instance de SuiviUE dans laquelle il faut placer la note à ajouter. Cette recherche est basée sur l’intitulé de l’UE concernée. Mais que faire si l’UE en question n’est pas dans la liste ? C’est à dire par exemple si on veut ajouter une note à l’UE "Allemand" mais que cette UE n’a pas été ajoutée à celle suivies par l’élève courant avec la méthode addUE ?

On va lever une exception ! Mais pour éviter de tout mélanger, on va introduire pour cet usage un nouveau type d’exception appelé BadUE.

public class BadUE extends Exception
{
    private String idUE;

    public BadUE(String id)
    {
        this.idUE = id;
    }

    public String getId()
    {
        return idUE;
    }
}

Voilà, maintenant voici le code de la méthode addNote de la classe SuiviEtu.

public void addNote(String idUE, double note) throws BadUE
{
    for(SuiviUE sue : suivi)
    {
        if(sue.idUE.equals(idUE))
        {
            sue.add(note); return;
        }
    }
    throw new BadUE(idUE);
}

Le principe est assez simple : on parcours la liste des instances de SuiviUE enregistrées dans la liste désignée par l’attribut suivi jusqu’à trouver celle ayant l’intitulé recherché. Si on la trouve, alors on utilise la méthode add de la classe SuiviUE pour ajouter la note, puis on arrête l’exécution de la méthode avec return. Si on ne la trouve pas, on lève une exception de type BadUE.

Si vous êtes débutant ou débutante, vous devez faire attention à ne pas confondre l’identifiant (ou nom ou intitulé) d’une unité d’enseignement et l’instance de SuiviUE représentant les notes obtenue par un élève dans cette UE.

Bien que le principe soit simple, il y a une subtilité qui, si elle n’est pas maîtrisée, pourra vous amener à faire des erreurs lorsque vous aurez besoin de comparer deux chaînes de caractères (représentant des identifiants d’UE).

Pour chaque instance sue de la la classe SuiviUE stockée dans la liste désignée par l’attribut suivi de l’instance courante de la classe SuiviEtu

for(SuiviUE sue : suivi)

…on veut comparer l’intitulé de l’UE représentée par sue, c’est à dire sue.idUE, avec l’intitulé de l’UE à rechercher, c’est à dire idUE, passé en paramètre. On veut donc comparer les deux chaînes de caractères désignées respectivement par idUE et sue.idUE. Pour comparer ces chaînes, on pourrait être tenter d’écrire…

if(sue.idUE == idUE)    // ERREUR !

…, ce qui serait une mauvaise idée. En effet, les chaînes sont des objets, des instances de String, désignées par leur référence, c’est à dire leurs adresses en mémoire. L’opérateur == compare les références et non les contenus des chaînes. Pour comparer les contenus, on peut utiliser la méthode equals qui a été définie dans le classe pédéfinie String pour cet usage. D’où la ligne…

if(sue.idUE.equals(idUE))

Il nous reste à revoir notre méthode main car telle qu’elle est codée plus haut, elle ne gère pas les deux types d’exceptions qu’elle peut recevoir : Defaillance et BadUE. Voici une manière de procéder.

public class Main
{
    public static void main(String[] args)
    {
        SuiviEtu fiche = new SuiviEtu(1233);
        fiche.addUE("Java", 2.0);
        fiche.addUE("BD", 3.0);
        try
        {
            fiche.addNote("Java", 12.0);
            fiche.addNote("Java", 17.0);
            fiche.addNote("BD", 14.0);
            System.out.println(fiche.moyenne());
        }
        catch(Defaillance ue)
        {// Traitement des exceptions produites par fiche.moyenne()
            System.out.println("Défaillance à l'UE : " + ue.getID());
        }
        catch(BadUE ue)
        {// Traitement des exceptions produites par fiche.addNote(...)
            System.out.println("UE non référencée : " + ue.getId());
        }
    }
}

Examinez ce code avec attention. La création de la fiche de suivi de l’élève 1233 et l’ajout des deux UE auxquelles cet élève est inscrit reste identique puisque ni le constructeur, ni la méthode addUE de la classe SuiviEtu ne sont susceptibles de lever une exception.

Par contre, les lignes qui suivent ont été placées dans un trycatch chargé d’intercepter, si applicable, les deux types d’exception pouvant être produites.

Ce programme est un peu compliqué lorsqu’on l’examine morceau par morceau sans avoir une vue d’ensemble. Certaines personnes peuvent avoir cette vue d’ensemble juste en lisant les codes des classes, mais si vous êtes débutante ou débutant, cela peut être très difficile. Ce schéma devrait vous aider. Il présente chacune des deux classes, leurs attributs, constructeurs et principales méthodes, et fait apparaitre le graphe d’appel qui matérialise par une flèche les appels de méthodes.

image-20210918221710711

Par exemple, la méthode main appelle la méthode moyenne de la classe SuiviEtu qui appelle la méthode moyenne de la classe SuiviUE. On voit que la méthode moyenne de SuiviUE peut lever une exception de type Defaillance, qui est récupérée pat la méthode moyenne de la classe SuiviEtu qui la remonte à la méthode main.

Exercices de découverte

D-dec-20

Lors de l’exécution de la méthode main ci dessus, aucune exception n’est levée. Faite une modification pour qu’une exception de type Defaillance soit levée et dites quels seront alors les affichages réalisés lors de l’exécution.

"Solution"

Dans le bloc try on met en commentaire la ligne qui ajoute une note pour l’UE "BD".

try
{
    fiche.addNote("Java", 12.0);
    fiche.addNote("Java", 17.0);
    //fiche.addNote("BD", 14.0);
    System.out.println(fiche.moyenne());
}

À l’exécution, le programme affiche uniquement :

Défaillance à l'UE : BD

D-dec-21

En repartant de la méthode main donnée plus haut, celle qui ne produit aucune exception, faire une modification pour qu’une exception de type BadUE soit levée et dites quels seront alors les affichages réalisés lors de l’exécution.

"Solution"

Par exemple on peut remplacer "BD" par "GEO" dans la troisième ligne du bloc try.

try
{
    fiche.addNote("Java", 12.0);
    fiche.addNote("Java", 17.0);
    fiche.addNote("GEO", 14.0);
    System.out.println(fiche.moyenne());
}

À l’exécution, le programme affiche uniquement :

UE non référencée : GEO

D-dec-22

Dans la version de la classe SuiviEtu donnée plus haut, la méthode moyenne, si elle reçoit une exception de type Defaillance, la fait remonter. Modifiez la méthode de manière à ce que, si elle reçoit une exception de type Defaillance, elle affiche le message "L’élève n’a pas de note dans l’UE {ID de l’ue}" et retourne 0.0.

"Solution"

public double moyenne_bis()
{
    double sum = 0.0; double sumCoeffs = 0.0;
    for(SuiviUE sue : suivi)
    {
        try
        {
            sum += sue.moyenne() * sue.coeff;
        }
        catch(Defaillance d)
        {
            System.out.println("L'élève n'a pas de note dans l'UE " + d.getID());
            return 0.0;
        }
        sumCoeffs += sue.coeff;
    }
    return sum / sumCoeffs;
}

D3 : Compléments

Le bloc Finaly

Un trycatch peut être suivi d’un bloc finally. Le code situé dans ce bloc est systématiquement exécuté à l’issue du trycatch qu’i y ait eu ou non interception d’exception, et même si un return est réalisé dans un bloc catch.

Un usage typique est de refermer un fichier ayant été ouvert dans le bloc try, mais cela sort du périmètre de cet enseignement.

Les exceptions "run time"

Les exceptions dérivant de la classe RuntimeException ont la particularité de remonter automatiquement, si elle ne sont pas interceptées, sans que les méthodes où elles se produisent aient à le spécifier avec throws. L’usage typique est la gestion des erreurs de programmations qui doivent entrainer un arrêt de l’exécution du programme. Java produit lui même de telles exceptions, par exemple lors d’une tentative d’appel d’une méthode avec une variables contenant une valeur null au lieu de la référence d’un objet (NullPointerException), ou encore lors d’une tentative d’accès à une cellule de tableau à un indice incorrect (OutOfBoundException).

Vous pouvez créer et intercepter des exceptions de type RuntimeException, et vous pouvez également intercepter celles produites par Java. Les exceptions de ce type non interceptées remontent jusqu’à la machine virtuelle Java, provoquant l’affichage d’un message d’erreur indiquant dans quelle méthode le problème est survenu.

Faisons l’expérience suivante.

public class TestRuntimeExceptions
{
    public static void main(String[] args)
    {
        int[] tab = new int[5];
        tab[5] = 12;
        System.out.println("Exécution terminée");
    }
}

À l’exécution, le programme s’arrête et affiche :

Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 
Index 5 out of bounds for length 5	at TestRuntimeExceptions.main(TestRuntimeExceptions.java:8)

On voit que le message "Exécution terminée" ne s’est pas affiché car le programme a été interrompu avant par une exception de type ArrayIndexOutOfBoundsException déclenchée par une tentative d’accès à tab[5] alors que le tableau désigné par tab possède 5 cellules allant de tab[0] à tab[4]. (Une erreur classique de débutant.)

Voici un exemple d’interception de toute exception de type RuntimeException.

public static void main(String[] args)
{
    try
    {
        int[] tab = new int[5];
        tab[5] = 12;
        System.out.println("Exécution terminée");
    }
    catch(RuntimeException e)
    {
        System.out.println("Une erreur s'est produite.");
    }
}

Lors de l’exécution, on obtient l’affichage :

Une erreur s'est produite.

Le genre de message un peu énervant parce que pas très informatif…

Si on écrit ceci…

public static void main(String[] args)
{
    try
    {
        int[] tab = new int[5];
        tab[5] = 12;
        System.out.println("Exécution terminée");
    }
    catch(RuntimeException e)
    {
        System.out.println(e);
    }
}

…alors on obtient l’affichage :

ArrayIndexOutOfBoundsException: Index 5 out of bounds for length 5

L’affichage de l’instance d’exception e a invoqué la méthode toString de la classe ArrayOutOfBoundsException qui a fourni des informations utiles.

Exercices de découverte

D-dec-30

Soit la méthode main suivante :

public static void main(String[] args)
{
    try
    {
        int[] tab = new int[5];
        tab[5] = 12;
        System.out.println("Exécution terminée");
    }
    catch(RuntimeException e)
    {
        System.out.println("----- ERREUR D'EXECUTION -----");
        throw e;
    }
}

Tentez de prévoir ce que le l’exécution de cette méthode va afficher à l’écran.

"Solution"

Comme une exception de type Runtime Exception est produite dans le bloc try, le bloc catch sera exécuté et affichera "----- ERREUR D'EXECUTION -----". Le code situé dans le bloc try après la ligne où l’erreur se produit ne sera pas exécuté.

Mais dans le bloc catch, l’exception est relancée par throw e ! (Celle là, on ne vous l’avait encore jamais faite.) Donc cette exécution est remontée à la machine virtuelle et provoque un affichage détaillé de l’erreur à l’origine de l’exception. On obtient l’affichage :

----- ERREUR D'EXECUTION -----
Exception in thread "main" ArrayIndexOutOfBoundsException: 
Index 5 out of bounds for length 5 at TestRuntimeExceptions.main(TestRuntimeExceptions.java:10)

D-dec-31

Soit la méthode main suivante :

public class TestRuntimeExceptions
{
    public static void main(String[] args)
    {
        try
        {
            int[] tab = new int[5];
            tab[5] = 12;
            System.out.println("Exécution terminée");
        }
        catch(RuntimeException e)
        {
            System.out.println("----- ERREUR D'EXECUTION -----");
        }
    }
}

On voudrait que le message "Exécution terminée" s’affiche toujours à l’issue de l’exécution du programme, qu’il y ait ou non une exception produite dans le bloc catch. Avec la nouvelle version, si on écrit tab[5] = 12 dans le bloc try, on devra avoir l’affichage…

----- ERREUR D'EXECUTION -----
Exécution terminée

…alors que si on écrit tab[4] = 12 on aura l’affichage :

Exécution terminée

Essayez de trouver deux manières d’obtenir ce résultat, une utilisant un bloc finally et une autre solution sans utiliser un tel bloc.

"Solution"

Avec un bloc finally, on peut écrire :

public static void main(String[] args)
{
    try
    {
        int[] tab = new int[5];
        tab[5] = 12;
    }
    catch(RuntimeException e)
    {
        System.out.println("----- ERREUR D'EXECUTION -----");
    }
    finally
    {
        System.out.println("Exécution terminée");
    }
}

Sans utiliser un tel bloc, on peut écrire :

    public static void main(String[] args)
    {
        try
        {
            int[] tab = new int[5];
            tab[4] = 12;
        }
        catch(RuntimeException e)
        {
            System.out.println("----- ERREUR D'EXECUTION -----");
        }
        System.out.println("Exécution terminée");
    }

En effet, s’il n’y a pas de return ou de levée d’une exception ou autre instruction qui arrête le programme dans le bloc catch, l’exécution reprend son cours après les blocs trycatch et le bloc finally n’est pas vraiment utile. En revanche, si un bloc catch interrompt le programme, le bloc finally sera quand même exécuté alors que le code qui est au delà ne le sera jamais.

D-dec-32

Soit la méthode main suivante :

public static void main(String[] args)
{
    try
    {
        int[] tab = new int[5];
        tab[5] = 12;      // ligne A
    }
    catch(RuntimeException e)
    {
        System.out.println("----- ERREUR D'EXECUTION -----");
        return;
    }
    System.out.println("Exécution terminée");
}

Qu’affiche très exactement l’exécution du programme ? Même question si on supprime la ligne A ou si on la met complètement en commentaire ?

"Solution"

Avec la ligne A :

----- ERREUR D'EXECUTION -----

Sans la ligne A :

Exécution terminée

D-dec-33

Soit la méthode main suivante :

    public static void main(String[] args)
    {
        try
        {
            int[] tab = new int[5];
            tab[5] = 12;      // ligne A
        }
        catch(RuntimeException e)
        {
            System.out.println("----- ERREUR D'EXECUTION -----");
            return;
        }
        finally
        {
            System.out.println("Exécution terminée");
        }
    }

Qu’affiche très exactement l’exécution du programme ? Même question si on supprime la ligne A ou si on la met complètement en commentaire ?

"Solution"

Avec la ligne A :

----- ERREUR D'EXECUTION -----
Exécution terminée

Sans la ligne A :

Exécution terminée

Exercices d’assimilation

D-ass-00

La méthode suivante permet de saisir un entier au clavier.

    public static int lireInt()
    {
        return new Scanner(System.in).nextInt();
    }

Mais si la valeur saisie n’est pas un entier, elle produit une exception de type InputMismatchException. Proposez une nouvelle version de la méthode lireInt qui en cas de saisie correcte, retourne une instance de la classe enveloppe Integer contenant l’entier saisi, et qui dans le cas contraire retourne null.

"Indice

Voici un squelette de la méthode à compléter. L’exception attendue dans la partie catch est de type InputMismatchException, mais le type Exception plus général convient aussi dans ce cas.

public static Integer lireInt()
{
    try
    {
        // À compléter
    }
    catch(Exception e)
    {
        // À compléter
    }
}

"Solution"

Voici une solution complète. Des variantes sont possibles. Au besoin faites des essais sur machine.

public static Integer lireInt()
{
    try
    {
        return new Scanner(System.in).nextInt();
    }
    catch(Exception e)
    {
        return null;
    }
}

D-ass-01

Proposez une troisième version de la méthode lireInt de l’exercice précédent qui en cas de saisie correcte, retourne l’entier saisi (la valeur de retour est donc de type int, comme pour la première version), et qui, en cas de saisie incorrecte, redemande à l’utilisateur de faire une nouvelle saisie jusqu’à ce qu’une saisie correcte soit réalisée.

"Indice

Voici un squelette d’une solution simple. La présence du while(true) pout choquer certains puristes, mais elle ne me semble pas nuire à la lisibilité du code. La sortie de la boucle se fait par l’instruction return lorsque le « job » de la méthode est terminé, c’est-à-dire quand l’utilisateur a réalisé une saisie correcte.

public static Integer lireInt()
{
    while(true)
    {
        try
        {
            // À compléter
        }
        catch(Exception e)
        {
            // À compléter
        }
    }
}

"Solution"

public static Integer lireInt()
{
    while(true)
    {
        try
        {
            return new Scanner(System.in).nextInt();
        }
        catch(Exception e)
        {
            System.out.println("Saisie incorrecte, veuillez recommencer");
        }
    }
}

D-ass-02

Modifiez le constructeur de la classe Date du badge B pour qu’il lève une exception de type BadDate, à définir, si la valeur du jour ou du mois est incorrecte ou si celle de l’année est inférieure à MINY ou supérieure MAXY, deux constantes de classe définies dans BadDate. La classe BadDate dispose d’un constructeur acceptant un paramètre de type String qui appelle le constructeur Exception(String message). C’est ce constructeur qui devra être utilisé pour lever (si applicable) l’exception en lui passant en argument un message indiquant la nature du problème (Année incorrecte, ou Jour incorrect ou Mois incorrect). Un jour est considéré comme incorrect s’il est inférieur à 1 ou supérieur à 31. On ne prend pas en compte la durée réelle des mois dans cette version simplifiée. Réalisez une méthode de test qui tente de créer une date incorrecte, intercepte l’exception et affiche un message d’erreur pertinent.

"Indice

Commençons par définir la classe d’exception.

public class BadDate extends Exception
{
    public BadDate(String message)
    {
        super(message);
    }
}

"Indice

Regardons les modifications à faire dans la classe Date. Voici la déclaration des constantes (initialisées avec des valeurs arbitraires à titre d’exemple) et le squelette du constructeur à modifier.

public class Date
{
    final public static int MAXY = 2100;
    final public static int MINY = 1700;

    private int jour;
    private int mois;
    private int annee;

    public Date(int jour, int mois, int annee) throws BadDate
    {
        // À compléter

        this.jour=jour; this.mois=mois; this.annee=annee;
    }
}

"Indice

Voici le détail du constructeur de la classe Date, qui lève une même exception, mais avec des messages d’erreur différents selon le paramètre erroné.

public Date(int jour, int mois, int annee) throws BadDate
{
    if((annee<MINY)||(annee>MAXY))
    {
        throw new BadDate("Annee incorrecte");
    }
    else if ((jour<1)||(jour>31))
    {
        throw new BadDate("Jour incorrect");
    }
    else if ((mois<1)||(mois>12))
    {
        throw new BadDate("Mois incorrect");
    }
    this.jour=jour; this.mois=mois; this.annee=annee;
}

Il vous reste à écrire la méthode de test.

"Solution"

Voici le détail de la méthode de test.

public static void test()
{
    try
    {
        Date d = new Date(32,7,2012);
    }
    catch(BadDate e)
    {
        System.out.println(e);
    }
}

Java en LP : partie C

Partie C : Classes et objets

Objectif

Être capable d’implémenter une application comportant plusieurs classes, y compris des classes abstraites, reliées par héritage, ainsi que des interfaces, et des itérateurs, en maîtrisant l’implémentation et le comportement des constructeurs, de méthodes redéfinies avec liaisons dynamiques. Être capable de prévoir le comportement d’un tel programme, y compris les objets et données créés dans la mémoire de l’ordinateur (pile, tas, zone des données statiques).

Prérequis

Parties A et B


C1 : Exemple introductif

On appelle classe dérivée d’une classe S toute classe D qui hérite de S dans le sens suivant :

  • Toutes les variables de classe et attributs de S appartiennent implicitement à D.

  • Toutes les méthodes de S appartiennent à D, mais peuvent être redéfinies dans D.

  • D peut posséder des variables d’instance, de classe, des méthodes d’instance, de classe, spécifiques qui n’appartiennent pas à S.

Les redéfinitions d’attributs, variables de classe, méthodes de classes ne seront pas abordées.

Nous allons découvrir ces notions avec un exemple qui commence par la définition d’une classe Personne, qui représente un individu, et d’une classe Document, qui représente un document ayant un nom et un ou plusieurs auteurs (qui sont des instances de Personne).

Pour l’instant il n’y a pas d’héritage, mais seulement un lien de composition entre Document et Personne.

public class Personne
{
    private String nom;
    private Date dateNaiss;

    public Personne(String nom)
    {
        // DateNaiss est initialisé implicitement à null
        this.nom=nom;
    }

    public Personne(String nom, Date naiss)
    {
        this(nom); // Appel du constructeur à 1 paramètre
        this.dateNaiss =  naiss;
    }

    public String toString()
    {
        String s = nom;
        if(dateNaiss!=null)
        {
            s = s + "né le " + dateNaiss;
        }
        return s;
    }
}
public class Document
{
    private String nom;
    private Personne[] auteurs;

    public Document(String n, Personne[] a)
    {
        this.nom=n; auteurs = a;
    }

    public int nbAuteurs()
    {
        return auteurs.length;
    }

    public String toString()
    {
        String da;
        if(nbAuteurs()==0) da="inconnu";
        else
        {
            da="";
            for (int i = 0; i < nbAuteurs(); i++)
                da = da + auteurs[i] + " ";
        }
        return nom + "\nAuteur(s) : "+da;
    }
}

Les deux classes peuvent être représentées par ce schéma appelé diagramme de classes.

1

La notion de composition correspond à l’auxiliaire avoir : un document a un ou plusieurs auteurs. A présent nous allons introduire une classe Livre qui représente un cas particulier de document : un livre est un document. Cet auxiliaire être traduit la notion d’héritage.

public class Livre extends Document
{
    private long isbn;
    private int annee;

    public Livre(String nom, Personne[] aut, long isbn, int apub)
    {
        super(nom, aut); // Appel du constructeur de Document
        this.isbn=isbn;
        annee=apub;
    }

    public long getIsbn()
    {
        return isbn;
    }

    public String toString()
    {
        String s = "Livre : "+super.toString(); // Appel toString de document
        return s+"\nISBN : "+isbn+" ("+annee+")";
    }
}

Le mot-clé extends traduit l’héritage. La classe Livre est dérivée de Document. Document est la superclasse de Livre.

Le code de tout constructeur de la classe dérivée doit commencer par l’appel d’un constructeur de sa superclasse à l’aide du mot clé super.

La méthode toString existe déjà dans la superclasse et elle est ici redéfinie. La méthode toString de la superclasse est appelée depuis la classe dérivée grâce à la syntaxe super.toString().

Voici le diagramme de classe complet. Toute instance de Livre est une instance de Document. Toute instance de Document a des auteurs qui sont des instances de Personne. Donc, évidemment, toute instance de Livre a des auteurs…

image-20210909153314999

Il y a quelques règles à connaitre en matière d’héritage et de constructeurs :

  1. Si une classe n’a pas de constructeur explicite alors Java lui ajoute un constructeur implicite qui appelle le constructeur par défaut de sa superclasse, et initialise ses variables d’instance avec des valeurs par défaut.
  2. Si une classe a au moins un constructeur explicite alors Java ne produit pas de constructeur par défaut implicite pour cette classe, mais on peut bien sûr lui donner un constructeur par défaut explicite.
  3. Si le code d’un constructeur explicite d’une classe dérivée ne commence pas par super(…) alors ce constructeur appelle implicitement le constructeur par défaut de sa superclasse. Et si la superclasse n’a pas de constructeur par défaut, le programme ne compile pas !
  4. Si le code d’un constructeur explicite d’une classe dérivée appelle explicitement un constructeur de sa superclasse, son code doit commencer par super(…).

Pour mémoire, rappelons le sens de quelques termes importants.

  • implicite : produit automatiquement pas le compilateur, non visible dans le code.
  • explicite : écrit par le programmeur.
  • constructeur par défaut : sans paramètres.
  • valeur par défaut : placée automatiquement dans une variable qui n’est pas initialisée explicitement.

Exercices de découverte

C-dec-10

Complétez ces lignes de code permettant de créer une instance de Document représentant un document ayant un seul auteur nommé N. Wirth et ayant pour titre "Algorithm + Data Structure = Program", puis d’afficher la description de ce document produite par la méthode toString.

Personne[] aut = {/* création d'une instance de Personne */};
Document doc = new /* À compléter */;
System.out.println(/* À compléter */);

L’instance de Personne doit être créée avec le constructeur à un paramètre, qui ne nécessite pas la date de naissance.

Vous pouvez utiliser un ordinateur pour rechercher la bonne syntaxe par essai et erreurs.

"Solution"

Personne[] aut = {new Personne("N. Wirth")};
Document doc = new Document("Algorithm + Data Structure = Program", aut);
System.out.println(doc);

C-dec-11

Complétez cette ligne de code permettant de créer une instance de Livre représentant un document ayant un seul auteur nommé N. Wirth et ayant pour titre "Algorithm + Data Structure = Program", un numéro isbn égal à 130224189, ayant été publié en 1976.

Personne[] aut = {new Personne("N. Wirth")};
Livre w = new Livre (// À compléter //);
"Solution"

Livre w = new Livre
        ("Algorithm + Data Structure = Program",
          aut,  130224189, 1976
        );

C2 : Héritage et types

En Java, chaque variable et chaque paramètre a un type. Nous avons vu qu’il existe des types primitifs : int, boolean, double, long. Chaque nom de classe représente aussi un type. Mais du fait de l’héritage, une instance de classe peut avoir plusieurs types !

Voici un premier exemple d’utilisation de notre classe Livre

Livre w = new Livre(
        "Patience dans l'azur",
        new Personne[]{new Personne("Hubert Reeves")},
        2020099179, 1981);

System.out.println(w.toString());
System.out.println(w.getIsbn());

…qui produit l’affichage :

Livre : Patience dans l'azur
Auteur(s) : Hubert Reeves 
ISBN : 2020099179 (1981)
2020099179

Maintenant, il est aussi possible d’écrire…

Document w = new Livre(
        "Patience dans l'azur",
        new Personne[]{new Personne("Hubert Reeves")},
        2020099179, 1981);

System.out.println(w.toString());

…car toute instance de Livre est aussi par héritage une instance de Document. Mais… Il n’est plus possible d’appeler la méthode getIsbn.

System.out.println(w.getIsbn()); // Ne compile pas

Parce que c’est une méthode de Livre qui n’existe pas dans Document, et que la variable w est de type Document. Pas de problème avec la méthode toString qui est redéfinie dans la classe Livre mais existe dans Document et, cerise sur le gâteau, c’est la méthode toString de Livre qui sera exécutée et non celle de Document.

Ce petit miracle s’appelle la liaison dynamique et nous y reviendrons plus tard. Observons ce qui se produit en mémoire.

image-20210910122934576

Exercices de découverte

C-dec-20

Donnez les lignes de code permettant, après la déclaration précédente,

  • d’afficher le nombre d’auteurs du livre,
  • d’afficher le numéro isbn du livre,
  • d’afficher la description de ce document produite par la méthode toString de la classe Livre.
"Solution"

System.out.println(w.nbAuteurs());
System.out.println(w.getIsbn());
System.out.println(w.toString());

C-dec-21

On écrit les lignes suivantes.

Personne[] aut = {new Personne("N. Wirth")};
Livre w = new Livre
        ("Algorithm + Data Structure = Program",
          aut,  130224189, 1976
        );
Document d = w;                      // ligne A
System.out.println(d);               // ligne B
System.out.println(d.nbAuteurs());   // ligne C
System.out.println(d.getIsbn());     // ligne D

Toutes les lignes passent à la compilation sauf la dernière, qui comporte une erreur. Expliquez pourquoi.

"Solution"

Toute instance de Livre est aussi, par héritage, une instance de Document, et c’est pourquoi sa référence peut être mise dans une variable de type Document. Les méthodes nbAuteurs et toString étant définies dans la classe Document, elles sont accessibles via la variable d.

Par contre, la méthode getIsbn n’est pas définie dans la classe Document et donc pas accessible via une variable de type Document, quand bien même cette variable contient une instance de Livre.

C-dec-21

Quand on exécute le code de l’exercice précédent (en supprimant la dernière ligne puisqu’elle est incorrecte), on obtient l’affichage suivant :

Livre : Algorithm + Data Structure = Program
Auteur(s) : N. Wirth 
ISBN : 130224189 (1976)
1

Bien que la variable d soit de type Document, c’est la méthode toString de la classe Livre qui a été exécutée. Tentez de trouver une explication.

"Solution"

La méthode toString est définie dans Document et redéfinie dans Livre. Comme elle est définie dans Document, elle est donc accessible via la variable d de type Document. Mais comme cette variable pointe une instance de Livre (une classe dérivée de Document), c’est la méthode toString de Livre qui est exécutée. Si la méthode n’avait pas été redéfinie dans Livre, c’est celle de Document qui aurait été exécutée.

Ce comportement n’est pas évident à prévoir. Il résulte d’un choix faite par les inventeurs de la programmation orientée objet. Si vous avez eu du mal à deviner la réponse, refaites les exercices de découverte dans quelques jours.

C3 : Compléments sur l’héritage et la structuration des programmes

Notion de package

Un package regroupe un ensemble de classes. Le fichier dans lequel est définie une classe d’un package p doit comporter, au début, la déclaration :

package p;

Si une classe C utilise une classe U située dans un package q différent de celui où elle se trouve, une des déclarations suivantes doit apparaître avant la définition de C :

import q.U; // Importe la classe U du package q
import q.*; // Importe toutes les classes du package q

Droits d’accès

Jusqu’à maintenant, nous avons utilisé deux droits d’accès : public et private. Il en existe deux autres : protected et « par défaut ». Ce dernier s’applique lorsque le droit d’accès à un élément n’est pas précisé.

  • public : L’élément est accessible depuis toutes les classes du programme.
  • private : L’élément est accessible seulement depuis la classe où il est défini ou déclaré.
  • protected : L’élément est accessible depuis les classes dérivées de la classe où il est défini et déclaré et depuis toutes les classes du même package.
  • par défaut : L’élément est accessible seulement depuis les classes du package de la classe où il est défini ou déclaré.

Les classes ont elles-mêmes des droits d’accès. Une classe directement définie dans un fichier source .java, comme nous l’avons fait jusqu’à présent, peut avoir le droit public ou « par défaut ». Une classe public est utilisable dans tout le programme où elle est définie, une classe avec droit d’accès par défaut n’est utilisable que depuis le package où elle est définie.

Hiérarchie de classe

Une classe déclarée final ne peut avoir de classe dérivée. Mais en dehors de cette restriction, toute classe dérivée peut elle-même avoir des classes dérivées. D’ailleurs toutes les classes descendent d’une classe définie par Java nommée Object. Par contre Java ne permet pas l’héritage multiple : chaque classe (excepté Object) a une seule superclasse.

image-20210910130821540

Or il se trouve que la classe Object possède nativement certaines méthodes, dont notamment la méthode toString. On peut donc utiliser cette méthode héritée depuis n’importe quelle classe, même si elle n’y est pas redéfinie.

Exercices de découverte

C-dec-30

On ajoute la méthode suivante à la classe Document.

private void test()
{
    System.out.println("Exécution de test dans la classe Document");
}

On ajoute la méthode suivante à la classe Livre.

public void test()
{
    System.out.println("Exécution de test dans la classe Livre");
}

Le programme compile. Que peut-on en déduire en matière de possibilité de redéfinition d’une méthode private dans une classe dérivée ?

"Solution"

Non seulement on peut redéfinir une méthode private , mais on peut rendre public sa redéfinition. En fait cela se généralise : on peut toujours redéfinir une méthode avec des droits d’accès moins restrictifs.

C-dec-31

On ajoute la méthode suivante à la classe Document.

public void test()
{
    System.out.println("Exécution de test dans la classe Document");
}

On ajoute la méthode suivante à la classe Livre.

private void test()
{
    System.out.println("Exécution de test dans la classe Livre");
}

Le compilateur indique une erreur. Que peut-on en déduire ?

"Solution"

Une méthode public , ne peut être redéfinie avec un droit d’accès private. En fait cela se généralise : on ne peut redéfinir une méthode avec des droits d’accès plus restrictif que ceux dont elle dispose dans la superclasse.

C-dec-32

On ajoute la méthode suivante à la classe Document.

private void test()
{
    System.out.println("Exécution de test dans la classe Document");
}

On ajoute la méthode suivante à la classe Livre.

private void test()
{
    super.test();
    System.out.println("Exécution de test dans la classe Livre");
}

Le programme va-t-il compiler sans erreur ? Si oui, que se produira-t-il à l’exécution de test avec une instance de Livre ? Si non, expliquez l’erreur.

"Solution"

Il y a une erreur car la méthode test de Document est privée, donc elle ne peut être appelée par aucune méthode de Livre.

C4 : Interface

Une interface contient des signatures de méthodes et des commentaires expliquant ce que sont censées faire ces méthodes.

Par exemple, l’interface List<T> est implémentée par les classes ArrayList<T> et LinkedList<T>.

Autre exemple : il existe une interface prédéfinie Comparable<T> dans laquelle est déclarée une méthode int compareTo(T e) permettant de comparer deux objets de type T (l’objet courant et l’objet e) en respectant une relation d’ordre sur les objets de type T. Cette méthode doit retourner un entier négatif si this < e, un entier positif si this > e, et en cas d’égalité elle doit retourner 0.

Une interface définit un type. Par exemple, on peut déclarer une variable de type List.

Une interface ne contient pas de code et ne possède pas de constructeur.

Une classe peut implémenter une ou plusieurs interfaces. Elle doit alors implémenter toutes les méthodes déclarées dans cette interface (en respectant les spécifications données dans les commentaires de l’interface).

Voici un exemple d’utilisation de l’interface Comparable. Cette interface est déjà prédéfinie dans le langage Java, sous la forme suivante :

public interface Comparable<T>
{
	public int compareTo(T e);
}

Pour rendre une classe comparable, on fait en sorte qu’elle implémente cette interface :

public class Date implements Comparable<Date>
{
    // ...
	
    public int compareTo(Date d)
    {
        if(d.annee!=annee) 
            return this.annee-d.annee;

        else if(d.mois!=mois) 
            return this.mois-d.mois;

        else 
            return this.jour-d.jour;
    }
}

A présent, nous pouvons créer une liste de dates et utiliser la méthode statique sort de la classe prédéfinie Collections pour trier cette liste sans avoir à coder un algorithme de tri.

    public static void test()
    {
        List<Date> w = new ArrayList<>();
        w.add(new Date(25,1,1962));
        w.add(new Date(26,1,1962));
        w.add(new Date(25,2,1961));

        Collections.sort(w);

        System.out.println(w);
    }

L’affichage produit par l’exécution nous permet de vérifier que le tri a bien été exécuté.

[25/2/1961, 25/1/1962, 26/1/1962]

L’appel Collections.sort(w) fonctionne si et seulement si w désigne une instance d’une classe qui implémente l’interface List représentant une liste dont les éléments sont des instances d’une classe qui implémente l’interface Comparable. C’est un peu compliqué, mais très puissant puisque cela évite d’avoir à programmer une méthode de tri lorsque les conditions sont réunies.

Exercices de découverte

C-dec-40

Relisez le code de la classe Personne. Si on décide que cette classe doit implémenter l’interface Comparable, que doit-on ajouter à cette classe ? À quoi cela pourrait servir ? Proposez un exemple d’utilisation.

"Solution"

On doit ajouter dans la classe Personne une méthode compareTo permettant de comparer l’instance courante avec une autre instance. Cette comparaison pourrait porter sur les noms, en étant basée sur l’ordre alphabétique, mais pourrait aussi être basée sur les dates de naissances, à condition de disposer d’un moyen de comparer des instances de Date. (Il serait alors judicieux que la classe Date implémente aussi l’interface Comarable.)

Cela permettrait, par exemple de trier des listes d’instances de Personne en utilisant la méthode Collection.sort.

C5 : Classes et méthodes abstraites

Jusqu’à maintenant, nous avons parlé de classes, dans lesquelles toute méthode non héritée doit être implémentée (codée), et d’interfaces, dans lesquelles des méthodes sont déclarées, mais pas implémentées.

Une classe abstraite est une sorte d’intermédiaire entre ces deux concepts : certaines méthodes peuvent être implémentées alors que d’autres, appelées méthodes abstraites, sont juste déclarées. Une classe abstraite peut comporter des variables d’instances et des variables de classe.

Une classe abstraite ne peut pas être instanciée.

Une classe abstraite n’a d’intérêt que si elle a au moins une classe dérivée « concrète », qui implémente ses méthodes abstraites.

Fait remarquable : Une méthode concrète peut très bien appeler une méthode abstraite ! Le code exécuté est celui de l’implémentation de cette méthode dans une classe dérivée qui est déterminée lors de l’exécution du programme. On parle alors de liaison dynamique.

Pour bien comprendre le principe de la liaison dynamique, nous allons développer un exemple dans lequel une classe abstraite Animal a deux classes dérivées concrètes : Chat et Elephant.

image-20210910151842924

La classe abstraite Animal a une variable d’instance nom et un constructeur qui initialise cette variable. Elle a une méthode abstraite reactionDanger et une méthode concrète dangerDetecte qui appelle la méthode abstraite reactionDanger.

La méthode abstraite reactionDanger devra être implantée dans les classes Chat et Elephant.

public abstract class Animal
{
    private String nom;

    public Animal(String nom)
    {
        this.nom = nom;
    }

    public abstract void reactionDanger(); // Méthode abstraite

    public void dangerDetecte() // Méthode concrète
    {
        System.out.println(nom + " a détecté un danger.");
        reactionDanger(); // Appel d'une méthode abstraite
    }
}

On voit dans cet exemple que la méthode concrète dangerDetecte de la classe Animal appelle une méthode abstraite reactionDanger de cette même classe. Lors de l’exécution, c’est une implémentation concrète de reactionDanger définie dans une classe dérivée qui sera appelée. C’est typiquement un cas de liaison dynamique.

Voici deux exemples de classes qui dérivent de la classe abstraite Animal.

public class Chat extends Animal
{
    public Chat(String n)
    {
        super("Le chat "+n);
    }

    public void reactionDanger()
    {
        System.out.println("Il miaule et prend la fuite");
    }
}
public class Elephant extends Animal
{
    public Elephant(String n)
    {
        super("L’éléphant "+n);
    }

    public void reactionDanger()
    {
        System.out.println("Il barrit et il charge !");
    }
}

Voici un exemple d’utilisation des classes que nous venons de définir.

Animal felix = new Chat("Felix");        // ligne A
Animal jumbo = new Elephant("Jumbo");    // ligne B
felix.dangerDetecte();                   // ligne C
jumbo.dangerDetecte();                   // ligne D

L’exécution de la ligne C produit l’affichage "Le chat Felix a détecté un danger. Il miaule et prend la fuite. "

L’exécution de la ligne D produit l’affichage "L’éléphant Jumbo a détecté un danger. Il barrit et il charge ! "

Dans les deux cas, la méthode appelée, dangerDetecte, est héritée de la superclasse Animal et appelle la méthode reactionDanger, abstraite dans la classe Animal, mais implémentée dans les classes Chat et Elephant.


Les classes abstraites permettent notamment d’éviter la duplication de code : on y implémente des algorithmes qui sont utilisés ou utilisables par plusieurs classes dérivées concrètes.

Par exemple, la classe abstraite AbstractList<T> de Java a parmi ses classes dérivées les classes ArrayList<T> et (indirectement) LinkedList<T>. Elle implante des algorithmes qui sont les mêmes pour ces deux classes, comme par exemple la méthode…

public boolean addAll(int i, Collection<? extends T> c) 

…qui insère dans la liste courante, à partir de la position i, tous les éléments de c. Cette méthode addAll utilise la méthode…

public void add(int i, T e) 

…qui devra être implémentée dans toute classe concrète dérivant de AbstractList.

Voici une vue partielle de la hiérarchie des classe java dédiées à la représentation des listes.

image-20210910154953409

C-dec-50

Que se passe-t-il si on écrit, par exemple dans une méthode main, la ligne suivante ?

Animal w = new Animal("Flipper le dauphin");
"Solution"

Il y a une erreur car on ne peut pas créer d’instance d’une classe abstraite.

C-dec-51

On ajoute une méthode enrouleTrompe à la classe Elephant.

public void enrouleTrompe() {...}

Que se passe-t-il si on écrit, par exemple dans une méthode main, les lignes suivantes ?

Animal jumbo = new Elephant("Jumbo");
jumbo.enrouleTrompe();
"Solution"

Il y a une erreur car même si enrouleTrompe est une méthode d’instance de la classe Elephant, elle n’est pas définie dans Animal donc pas accessible via la variable jumbo.

C6 : Les itérateurs

Un itérateur est un objet (!) permettant de parcourir facilement tous les objets d’une collection, et en particulier d’une liste. En Java, toute classe implémentant l’interface prédéfinie Iterable<T> permet d’utiliser des itérateurs. C’est le cas notamment des classes ArrayList et LinkedList, mais aussi des tableaux.

L’interface Iterable<T> est indissociable d’une autre interface prédéfinie nommée Iterator<T>.

public interface Iterator<T>
{
    boolean hasNext(); // vrai s'il y a un élément suivant
    T next();          // élément suivant
    void remove();     // suppression de l’élément suivant
}
public interface Iterable<T>
{
    Iterator<T> iterator()
    // ...
}

Pour comprendre l’intérêt et la manière d’utiliser ces concepts, considérons l’exemple d’une instance de ArrayList<Integer>.

ArrayList<Integer> w = new ArrayList<>();

w.add(15); w.add(12); w.add(33);

Iterator<Integer> scan = w.iterator(); // ligne A

while(scan.hasNext())
{
    System.out.println(scan.next());
}

En ligne A, La méthode iterator retourne un itérateur, c’est à dire une instance d’une classe qui implémente l’interface Iterator.

Dans la boucle while, Les méthodes hasNext et next de l’itérateur permettent de parcourir la liste et de récupérer ses éléments.

Cette boucle peut être réécrite avec une syntaxe plus lisible qui masque l’utilisation d’un itérateur et qui est la manière la plus courante de procéder.

  for(int x : w)
  {
      System.out.println(x);
  }

C-dec-60

Soit une classe C représentant une collection d’objets (liste, ensemble…). Soit w une instance de C. À quelle condition peut-on utiliser la syntaxe suivante :

for(x : w) {...}
"Solution"

Il faut que la classe C implémente l’interface Iterable<...> (directement ou parce qu’elle dérive d’une classe qui implémente cette interface).

C7 : Classes internes

Les compétences relatives aux notions introduites ici ne seront pas évaluée et sont présentées à titre informatif.

Les classes internes anonymes

Il arrive parfois qu’on ait besoin de créer une classe qui dérive d’une classe existante ou qui implémente une interface, et que l’on ait besoin que d’une seule instance de cette classe. Puisqu’une seule instance est requise, il n’est pas nécessaire de donner un nom à la classe et il est possible de créer la classe et son unique instance en une seule ligne de programme. Voici un exemple dont l’unique intérêt est la simplicité.

abstract class Oiseau
{
    abstract void voler();
}
public class Main
{
    public static void main(String[] args)
    {
        Oiseau bird = new Oiseau()
        {
            void voler()
            {
                System.out.println("Flap flap");
            }
        };

        bird.voler();
    } 
}

La classe Oiseau est abstraite donc on ne peut pas créer directement avec new une instance de cette classe.

La variable bird de ma méthode main reçoit une référence d’une instance d’une classe dérivée de Oiseau, qui n’a pas de nom et dans laquelle la méthode voler est implémentée. Cette classe dérivée n’aura qu’une seule instance.

Les classes internes statiques

On peut définir une ou plusieurs classes dans une classe comme dans l’exemple suivant :

public class Test
{
    public static abstract class Animal
    {
        private String nom;

        public Animal(String nom) {this.nom = nom;}
        public abstract void reactionDanger();

        public void dangerDetecte()
        {
            System.out.println(nom + " a detecte un danger.");
            reactionDanger();
        }
    }

    public static class Chat extends Animal
    {
        public Chat(String n) {super("Le chat "+n);}

        public void reactionDanger()
        {
            System.out.println("Il miaule et prend la fuite");
        }
    }
}

Pour instancier la classe Chat en dehors de la classe Test, on doit utiliser la syntaxe illustrée par cet exemple.

   Test.Animal felix = new Test.Chat("Felix");
   felix.reactionDanger();

Les classes membres

Le principe est le même que pour les classes internes statiques, mais chaque instance de la classe interne est liée à une instance de la classe enveloppante, et à accès aux variables d’instance de la classe enveloppante. Dans cet exemple, la classe Liste représente une liste chaînée dont les éléments de type double, sont contenus dans des maillons, qui sont des instances d’une classe interne Maillon.

public class Liste
{
    Maillon deb;

    private class Maillon
    {
        Maillon suiv;
        double val;

        Maillon(double val)
        {
           this.val=val;
           suiv=deb;
           deb=this;
        }
    }
  
    public void ajoute(double x)
    {
        Maillon m = new Maillon(x);
    }

    public void print()
    {
        Maillon cur=deb;
        while(cur!=null)
        {
            System.out.println(cur.val);
            cur = cur.suiv;
        }
    }
}

Le constructeur de Maillon a accès aux attributs de la classe Liste.

La méthode ajoute ajoute un élément en début de liste. La méthode print affiche les éléments de la liste.

La classe Maillon étant private, elle ne peut être exploitée que dans la classe Liste.

Exercices d’assimilation

C-ass-00

Réalisez une classe Video qui représente un cas particulier de Document. Les instances de Video représentent des vidéos qui, en plus de tous les attributs d’un document, ont une durée. Cette durée doit être l’un des paramètres du constructeur de la classe Video. La classe Video doit redéfinir la méthode toString de sorte que cette méthode retourne la description retournée par la méthode toString de Document, complétée avec la durée. La classe Video doit être dotée en outre d’une méthode getDuree qui retourne la durée de la vidéo représentée par l’instance courante.

"Indice1"

Voici un squelette de la classe Video

public class Video extends Document
 {
   private double duree;
 
   public Video(String n, Personne[] a, double duree)
   {
 			// À compléter
   }
 
   public double getDuree()
   {
  		// À compléter
   }
 
   public String toString()
   {
      // À compléter
   }
 }

"Indice2"

Voici le constructeur de la classe Video, qui doit appeler (en première ligne) celui de la superclasse (c’est-à-dire Document).

public Video(String n, Personne[] a, double duree)
 {
   super(n, a);
   this.duree = duree;
 }

"Solution"

Voici le reste de la classe Video, avec pour particularité l’appel de de la méthode toString de la superclasse par celle de la classe Video.

public double getDuree()
 {
   return this.duree;
 }
 
 public String toString()
 {
   return super.toString() + " durée : " + this.duree;
 }

C-ass-01

Réalisez une classe Roman qui représente un cas particulier de Livre. Les instances de Roman représentent des romans qui, en plus de tous les attributs d’un livre, ont un genre, représenté par une chaîne de caractère. Ce genre doit être l’un des paramètres du constructeur de la classe Roman. La classe Roman doit redéfinir la méthode toString de sorte que cette méthode retourne la description retournée par la méthode toString de Livre, complétée avec le genre.

"Indice1"

Voici un squelette de la classe Roman

public class Roman extends Livre
 {
   private String genre;
 
   public Roman(String nom, Personne[] aut, long isbn, int apub, String genre)
   {
       // À compléter
   }
 
   public String toString()
   {
       // À compléter
   }
 }

"Indice2"

Voici le constructeur de la classe Roman, qui doit appeler (en première ligne) celui de la superclasse (c’est-à-dire Livre).

public Roman(String nom, Personne[] aut, long isbn, int apub, String genre)
 {
   super(nom, aut, isbn, apub);
   this.genre = genre;
 }

"Solution"

Voici la méthode toString de la classe Roman, qui appelle la méthode toString de sa superclasse Livre.

public String toString()
 {
   return super.toString() + " genre : " + this.genre;
 }

C-ass-02

Exercice créé par Marie-Laure Mugnier, Université de Montpellier

Les Pokémons sont des gentils animaux qui sont passionnés par la programmation objet en général et par le polymorphisme en particulier. Il existe quatre grandes catégories de Pokémons :

Les Pokémons sportifs : Ces Pokémons sont caractérisés par un nom, un poids (en kg), un nombre de pattes, une taille (en mètres) et une fréquence cardiaque mesurée en nombre de pulsations à la minute. Ces Pokémons se déplacent sur la terre à une certaine vitesse que l’on peut calculer grâce à la formule suivante : vitesse = nombre de pattes * taille * 3.

Les Pokémons casaniers : Ces Pokémons sont caractérisés par un nom, un poids (en kg), un nombre de pattes, une taille (en mètres) et le nombre d’heures par jour où ils regardent la télévision. Ces Pokémons se déplacent également sur la terre à une certaine vitesse que l’on peut calculer grâce à la formule suivante : vitesse = nombre de pattes * taille * 3.

Les Pokémons des mers : Ces Pokémons sont caractérisés par un nom, un poids (en kg) et un nombre de nageoires. Ces Pokémons ne se déplacent que dans la mer à une vitesse que l’on peut calculer grâce à la formule suivante : vitesse = poids / 25 * nombre de nageoires.

Les Pokémons de croisière : Ces Pokémons sont caractérisés par un nom, un poids (en kg) et un nombre de nageoires. Ces Pokémons ne se déplacent que dans la mer à une vitesse que l’on peut calculer grâce à la formule suivante : vitesse =(poids / 25 * nombre de nageoires) / 2.

Pour chacune de ces quatre catégories de Pokémons, on désire disposer d’une méthode toString qui retourne (dans une chaine de caractères) les caractéristiques du Pokémon.

Programmez ces classes en Java. Les attributs sont privés. Pour éviter de devoir créer autant de fichiers que de classes, vous pouvez ne déclarer qu’une classe "public", celle qui contient la méthode main de l’application.

Le but est de minimiser les duplications de code en utilisant le polymorphisme (méthodes abstraites, redéfinition, héritage, liaison dynamique).

Voici un exemple de méthode main permettant de tester votre solution.

public static void main(String[] args)
 {
   Pokemon p1 = new PokSportif("Tim", 1.2, 4, 0.3, 70);
   System.out.println(p1.toString());
 
   Pokemon p2 = new PokMaison("Tom", 1.8, 4, 0.5, 5);
   System.out.println(p2.toString());
 
   Pokemon p3 = new PokMer("Tum", 2.4, 2);
   System.out.println(p3.toString());
 
   Pokemon p4 = new PokMer("Tem", 3.3, 2);
   System.out.println(p4.toString());
 }

L’affichage attendu lors de l’exécution de la méthode de test est le suivant.

Tim : Pokemon sportif de fréquence cardiaque 70
4 pattes, taille = 0.3 m
vitesse : 3.59, poids : 1.2

Tom : Pokemon domestique regardant la télé 5 heures par jour
4 pattes, taille = 0.5 m
vitesse : 6.0, poids : 1.8

Tum : Pokemon de mer
2 nageoires
vitesse : 0.19, poids : 2.4

Tem : Pokemon de mer
2 nageoires
vitesse : 0.26, poids : 3.3
"Indice1"

On peut utiliser l’architecture suivante avec 3 classes abstraites et 4 concrètes.

image-20210910210105543

"Indice2"

Commençons par la classe abstraite Pokemon au sommet de notre hiérarchie.

abstract class Pokemon
{
   String nom;
   double poids;
 
   public Pokemon(String nom, double poids)
   {
     this.nom = nom;
     this.poids = poids;
   }
 
   public abstract double vitesse();
 
   public abstract String description();
 
   public String toString()
   {
     return nom + " : " + description() + "\nvitesse : " 
            + vitesse() + ", poids : " + poids;
   }
}

On met dans cette classe tout ce qui est commun à tous les Pokémons. Les attributs ne sont pas privés mais ont un accès par défaut (package) pour éviter d’alourdir le code avec des accesseurs. Le constructeur sert uniquement à être appelé par les constructeurs des classes dérivées (on ne peut pas créer une instance de classe abstraite). La méthode concrète toString construit une description de n’importe quel Pokémon en faisant appel au méthodes abstraites vitesse et description qui seront définies dans les classes dérivées.

Il vous reste à implémenter les 2 autres classes abstraites et les 4 classes concrètes dans le même esprit.

"Indice3"

Intéressons-nous à la classe abstraite PokTer

abstract static class PokTer extends Pokemon
{
   private int pattes;
   private double taille;
 
   public PokTer(String nom, double poids, int nb_pattes, double taille)
   {
     super(nom, poids);
     this.pattes = nb_pattes;
     this.taille = taille;
   }
 
   public double vitesse()
   {
     return pattes * taille * 3;
   }
 
   public String description()
   {
     return pattes + " pattes, taille = " + taille + " m";
   }
}

Cette classe regroupe tout ce qui est commun aux Pokémons sportifs et aux Pokémons casaniers un nombre de pattes, une taille, le calcul de la vitesse qui est réalisé par une méthode concrète dont les classes dérivées hériteront, et une partie de la description qui sera réutilisée dans les classes dérivées. Je vous conseille de développer maintenant la classe concrète PokSportif et de faire des essais sur machine sans attendre d’avoir complètement terminé l’exercice.

"Indice4"

Voici le détail de la classe PokSportif.

static class PokSportif extends PokTer
{
   private int frequence;

   public PokSportif(String nom, double poids, int nb_pattes, double taille, int frequ)
   {
     super(nom, poids, nb_pattes, taille);
     this.frequence = frequ;
   }

   public String description()
   {
     return "Pokemon sportif de fréquence cardiaque " + frequence + "\n"
         \+ super.description();
   }
}

On voit apparaitre l’attribut fréquence qui est spécifique aux Pokémons sportifs. Le point important est que la méthode description redéfinie ici est celle qui sera effectivement exécutée, mais qu’elle appelle la méthode description de la superclasse PokTer qui crée la partie de la description commune aux Pokémons sportifs et aux Pokémons de maisons (casaniers).

Vous avez maintenant tous les éléments techniques pour terminer l’exercice, d’abord en implémentant l’autre type de Pokémon terrestre, puis la branche des Pokémons aquatiques.

"Indice5"

Voici le détail de la classe PokMaison.

static class PokMaison extends PokTer
 {
   private int htv;
 
   public PokMaison(String nom, double poids, int nb_pattes, double taille, int htv)
   {
     super(nom, poids, nb_pattes, taille);
     this.htv = htv;
   }
 
   public String description()
   {
     return "Pokemon domestique regardant la télé " + htv + 
            " heures par jour\n" + super.description();
   }
 }

Et la classe abstraite qui mutualise les éléments communs des Pokémons aquatiques.

static class PokAqua extends Pokemon
 {
   private int nageoires;

   public PokAqua(String nom, double poids, int nageoires)
   {
      super(nom, poids);
      this.nageoires = nageoires;
   }

   public double vitesse()
   {
     return *round*((poids / 25) * nageoires);
   }

   public String description()
   {
     return nageoires + " nageoires";
   }
 }

On retrouve ici les principales techniques mises en œuvre précédemment. On peut d’ailleurs imaginer d’autres variantes des classes déjà écrites qui seraient tout à fait acceptables. Le point intéressant est que la vitesse des Pokémons des mers et celle des Pokémons de croisière ses calcules de manière assez similaire. Une grosse partie du calcul est réalisé par la méthode vitesse de la classe abstraite PokAqua qui sera directement héritée par la classe PokMer et sera redéfinie dans PokCrois. Les deux classes manquantes sont données au prochain épisode.

"Solution"

Voici le détail de la classe PokMer.

static class PokMer extends PokAqua
{
   public PokMer(String nom, double poids, int nageoires)
   {
     super(nom, poids, nageoires);
   }

   public String description()
   {
     return "Pokemon de mer\n » + super.description();
   }
}
static class PokCrois extends PokAqua
{
   public PokCrois(String nom, double poids, int nageoires)
   {
     super(nom, poids, nageoires);
   }

   public double vitesse()
   {
     return super.vitesse() / 2.0;
   }

   public String description()
   {
     return "Pokemon de mer\n" + super.description();
   }
}

Ceci termine l’exercice. Il est important que vous compreniez bien ce qui se passe lorsque la méthode toString est invoquée avec un objet de type PokCrois. Cette méthode est définie dans la classe Pokemon au sommet de la hiérarchie. Elle fait appel :

  • à la méthode vitesse de la classe PokCrois qui fait appel à la méthode vitesse de la classe PokAqua,

  • à la méthode description de la classe PokCrois, qui elle-même appelle la méthode description de PokAqua.

Cet enchaînement d’appels n’est pas si évident à suivre ! Il faut prendre le temps d’y réfléchir. Le principe général est qu’en cas de redéfinitions, c’est la variante de la méthode la plus « proche » de l’objet concerné qui est appelée.

Exercices de consolidation

C-cons-00

Vous devez développer une petite hiérarchie de classes permettant de représenter des ensembles d’entiers de différentes manières.

image-20210910181135430

L’interface IntSet déclare trois méthodes permettant respectivement d’ajouter un élément dans l’ensemble courant, de tester si un entier appartient l’ensemble courant, et de tester si l’ensemble courant est inclus dans un autre ensemble désigné en paramètre. Cette interface dérive de Iterator<Integer> pour que toutes les classes implémentant cette interface dispose d’itérateurs permettant d’énumérer leurs éléments.

La classe abstraite AintSet permet l’implémentation de la méthode subsetOf qui utilise les méthodes abstraites isIn et iterator.

Quant aux deux classes BoolIntSet et ListIntSet, elles implémentent deux représentations différentes d’un ensemble d’entiers : une représentation par un ArrayList d’entiers et une représentation par tableau de Booléens.

image-20210910180941252

"Indice1"

Commençons par déclarer l’interface IntSet.

public interface IntSet extends Iterable<Integer>
{
   public void add(int e);
   public boolean isIn(int e);
   public boolean subsetOf(IntSet s);
}

"Indice2"

Intéressons-nous maintenant à la classe abstraite AintSet.

public abstract class AintSet implements IntSet
{
   public boolean subsetOf(IntSet s)
   {
       Iterator<Integer> iter = iterator();
       while(iter.hasNext())
       {
         int e = iter.next();
         if(!s.isIn(e))
         {
             return false;
         }
       }
       return true;
   }
}

Il faut bien comprendre que les méthodes abstraites déclarées dans l’interface IntSet et celles héritées par cette interface sont héritées par AintSet et sont donc implicitement présentes dans cette classe. C’est ce qui permet de mettre ici l’implémentation concrète de la méthode subsetOf dont le code est commun aux deux représentations possibles des ensembles. Cette méthode utilise un itérateur, dont la disponibilité est garantie par le fait que AintSet implémente indirectement (via IntSet), l’interface Iterable<Integer>. L’algorithme implémenté dans la méthode subsetOf consiste à parcourir, grâce à un itérateur, tous les éléments de l’ensemble courant et de vérifier grâce à un appel à isIn si chacun d’eux appartient à l’ensemble désigné par le paramètre s.

La méthode isIn est abstraite, mais ce sera une de ses implémentations concrètes dans une classe dérivée qui sera exécutée en pratique. C’est toute la force du concept de liaison dynamique. Il vous reste maintenant à implémenter les deux classes dérivées BoolIntSet et ListIntSet. Commencez par ListIntSet, qui sera traitée par le prochain indice. Ne le consultez pas tout de suite !

"Indice3"

public class ListIntSet extends AintSet
{
   private ArrayList<Integer> data;
  
   class IterIntSet implements Iterator<Integer>
   {
     private int cursor;
 
     public IterIntSet(){this.cursor = 0;}
 
     public boolean hasNext()
     {
       if(cursor<data.size()) return true;
       else return false;
     }
 
     public Integer next()
     {
       this.cursor++;
       return data.get(cursor-1);
     }
   }

  // À compléter
}

La classe ListIntSet implémente concrètement un ensemble sous la forme d’une liste d’entiers. Elle doit également implémenter une classe interne de type Iterator<Integer>. Voici une partie du code de cette classe.

Les éléments de l’ensemble représenté seront stockés dans la liste désignée par l’attribut data. Cet attribut est accessible depuis la classe interne IterIntSet qui implémente l’itérateur.

Cette classe IterIntSet a son propre attribut cursor, de type int, qui représente une sorte de curseur permettant de parcourir la liste des éléments de l’ensemble.

Vous pouvez comprendre les détails du fonctionnement en examinant attentivement le code des différentes méthodes.

Il y a toutefois une autre approche possible, plus « professionnelle », qui consiste à utiliser un itérateur de la classe ArrayList<Integer>, qui elle-même implémente Iterable<Integer> . Vous pouvez essayer et tester votre solution sur machine.

Il vous reste à compléter cette classe avec les définitions du constructeur, des méthodes add, isIn et iterator.

"Indice4"

Voici le constructeur et les méthodes add et iterator de la classe ListIntSet.

public ListIntSet() {this.data = new ArrayList<>();}
 
public void add(int e) {this.data.add(e);}

public Iterator<Integer> iterator(){return new IterIntSet();}

Et la méthode isIn. Elle pourrait utiliser l’itérateur. Le choix qui a été fait ici est de parcourir la liste des éléments avec une boucle for.

public boolean isIn(int e)
{
   for(int x : this.data)
   {
     if( x== e) return true;
   }
   return false;
}

Il vous reste à implémenter la classe BoolIntSet.

"Solution"

Voici le détail de la classe BoolIntSet qui termine l’exercice. Prenez le temps de bien analyser son contenu.

public class BoolIntSet extends AintSet
{
   private boolean[] data;
  
   // Implémenter l'itérateur comme classe membre lui permet
   // l'accès à l'attribut privé data
   class IterIntSet implements Iterator<Integer>
   {
       private int cursor;

       public IterIntSet() {this.cursor = -1;}

       public boolean hasNext()
       {
           cursor++;
           while(cursor<data.length)
           {
               if(data[cursor]) return true;
               else cursor++;
           }
           return false;
       }

       public Integer next() {return cursor;}
   }
 
   public BoolIntSet() {this.data = new boolean[256];}
 
   public void add(int e) {this.data[e] = true;}
 
   public boolean isIn(int e){return this.data[e];}
 
   public Iterator<Integer> iterator(){return new IterIntSet();}
}

public BoolIntSet() { this.data = new boolean[256]; }

public void add(int e) { this.data[e] = true; }

public boolean isIn(int e) { return this.data[e]; }

public Iterator<Integer> iterator() { return new IterIntSet(); } }

Cet exercice est difficile à maîtriser car il comporte des subtilités algorithmiques et des notions assez avancées de programmation objet.

Java en LP – Partie B

Partie B

Classes et objets

Objectif

Être capable d’implémenter une classe comportant des attributs (variable d’instance), des méthodes d’instance, des constructeurs, exploitant des données représentées par des types primitifs, des tableaux, des chaînes, des listes de type ArrayList, et des classes enveloppes (telles que Integer, Double…). Être capable d’utiliser une telle classe en maîtrisant les effets de bord induits par les attributs modifiables. Être capable de réaliser une application utilisant plusieurs classes par composition (sans héritage) et de prévoir le comportement d’un tel programme, y compris les objets et données créés dans la mémoire de l’ordinateur (pile, tas, zone des données statiques).

Prérequis

Partie A


B1 : Exemple introductif

Présentation

Nous allons maintenant aborder la « vraie » programmation orientée objet, c’est à dire la création d’instances de classes, donc d’objets, et l’utilisation de méthodes d’instances pour agir sur ces objets.

Voici un exemple introductif avec une classe Date qui modélise une date constituée d’un jour, d’un mois et d’une année. Java dispose déjà d’une classe Date, mais on peut utiliser un même nom pour des classes différentes, à condition qu’elles soient dans des packages différents.

public class Date
 {
   private int jour;
   private int mois;
   private int annee;

   public Date()
   {
     this.jour=1; this.mois=1; this.annee=1900;
   }

   public Date(int jour, int mois, int annee)
   {
     this.jour=jour; this.mois=mois; this.annee=annee;
   }

   public String toString()
   {
     return this.jour + "/" + this.mois + "/" + this.annee;
   }
 }

Détaillons les différents éléments de cette classe. Voici la déclaration de trois attributs aussi appelés variables d’instance.

private int jour;
private int mois;
private int annee;

Ces trois attributs sont privés. Ils ne sont accessibles que depuis les méthodes de la classe Date. Chaque instance de la classe, c’est-à-dire chaque objet créé d’après cette classe, dispose de ses propres exemplaires de ces attributs.

Ensuite nous avons la définition d’un premier constructeur.

public Date()
 {
   this.jour=1; this.mois=1; this.annee=1900;
 }

Un tel constructeur sans paramètre est appelé constructeur pas défaut. Comme tout constructeur, son rôle est d’initialiser une instance de la classe lors de sa construction lancée par l’opérateur new.

Voici maintenant un deuxième constructeur qui accepte 3 paramètres.

public Date(int jour, int mois, int annee)
 {
   this.jour=jour; this.mois=mois; this.annee=annee;
 }

Comme dans le constructeur par défaut, les attributs initialisés sont préfixés par this, qui est la référence de l’objet en cours de construction. Dans le premier constructeur, ce préfixe est facultatif. Mais ici il permet de distinguer les attributs des paramètres de mêmes noms. Si on écrivait…

public Date(int jour, int mois, int annee)
 {
   jour=jour; mois=mois; annee=annee;
 }

… ce constructeur n’aurait strictement aucun effet sur les attributs de la classe, ni sur autre chose d’ailleurs puis qu’il recopierait le contenu du paramètre jour dans lui-même, et pareillement pour mois et annee.

Nous avons ensuite une méthode d’instance.

public String toString()
 {
   return this.jour + "/" + this.mois + "/" + this.annee;
 }

Cette méthode n’a aucun paramètre et retourne une chaîne de caractère qui décrit l’objet courant, c’est-à-dire l’objet via lequel elle est appelée. Ici, le préfixe this est facultatif, mais il a le mérite de bien mettre en évidence les attributs.

Maintenant que nous avons une classe, nous pouvons l’utiliser pour créer des instances et les utiliser. Voici un exemple simple.

public class Main
 {
   public static void main(String[] args)
   {
     Date d1 = new Date(11,9,2017);
     Date d2 = new Date();
 
     System.out.println(d1.toString());
     System.out.println(d2.toString());
   }
 }

Les deux premières lignes de la méthode main font respectivement appel aux deux constructeurs de la classe Date.

    Date d1 = new Date(11,9,2017);
    Date d2 = new Date();

Les références des objets créés sont placées dans deux variables de méthode. Voici ce qui se passe en mémoire.

image1

Les deux lignes suivantes appellent la méthode toString avec chacune des deux instances et affiche les chaînes retournées.

    System.out.println(d1.toString());
    System.out.println(d2.toString());

Le résultat est l’affichage suivant…

1/9/2017
1/1/1900

Quelques précisions

Constructeur par défaut

Lorsqu’on ne définit aucun constructeur dans une classe, Java crée un constructeur par défaut qui initialise chaque variable d’instance avec une valeur par défaut : 0 pour un entier, 0.0 pour un flottant, null pour une référence… Dès lors qu’on définit un constructeur avec paramètre(s), aucun constructeur par défaut n’est produit de manière automatique. Si on en veut un, il faut le définir. Dans notre exemple, le constructeur par défaut ne sert pas à grand-chose d’autre qu’illustrer le concept.

Destruction d’une instance

Toute instance est détruite automatiquement par la machine virtuelle dès lors qu’aucune variable ne contient sa référence. Le programmeur n’a donc pas à se soucier du recyclage de la mémoire.

Éditer et compiler ses programmes

Dans une application qui contient plusieurs classes, les bytecodes de ces classes sont regroupés dans un fichier ayant l’extension «.jar». On peut les produire en ligne de commande, via une console ou avec un script, mais Il existe des logiciels appelés IDE qui facilitent l’édition, la compilation et la mise au point des programmes, comme par exemple : NetBeans, Eclipse, IntelliJ IDEA, BlueJ (à vocation pédagogique !)… Je vous conseille d’en utiliser un.

Exercices de découverte

B-dec-10

Quels éléments syntaxiques (notation) permettent de savoir si une variable est une variable de méthode, une variable d’instance (aussi appelée attribut) ou une variable de classe dans un code Java ?

"Solution"

Les variables de méthodes sont déclarées uniquement à l’intérieur d’une méthode. La déclaration d’une variable de classe comporte le mot clé static. Les variables déclarées à l’extérieur d’une méthode sans le mot clé staticsont des variables d’instance, aussi appelée attributs.

B-dec-11

Quels éléments syntaxiques (notation) permettent de savoir si une méthode est une méthode de classe ou une méthode d’instance ?

"Solution"

La définition d’une méthode de classe comporte le mot clé static, contrairement à celle d’une méthode d’instance.

B-dec-12

On considère la classe Date définie précédemment et la classe Main suivante.

public class Main
{
    public static void main(String[] args)
    {
        Date d = new Date();
        d.annee = 1905;
        System.out.println(d.toString());
    }
}

Il y a une erreur. Trouvez là sans utiliser d’IDE ni de compilateur Java.

"Solution"

L’attribut annee de la classe Date est private. la méthode main est dans une autre classe et ne peut donc pas accéder à cet attribut.

B-dec-13

Ajoutez dans la classe Date un constructeur acceptant un seul paramètre de type int et qui permet de créer une instance de Date représentant le premier janvier de l’année passée en paramètre.

"Solution"

public Date(int annee)
{
    this.jour = 1; this.mois = 1; this.annee = annee;
}


B2 : Accesseurs et bonnes pratiques

A l’instar de ce qui est fait dans notre classe Date, le fait de rendre private les variables d’instance est considéré comme une bonne pratique en programmation objet. Plutôt que de permettre aux « utilisateurs » de la classe d’accéder directement à ces variables, on préfère mettre à sa disposition des méthodes publiques appelées accesseurs.

Voici les accesseurs de la classe Date qui ont été produits automatiquement par mon IDE.

 int getJour()
 {
   return jour;
 }
 
 public int getAnnee()
 {
   return annee;
 }
 
 public int getMois()
 {
   return mois;
 }
 
 public void setAnnee(int annee)
 {
   this.annee = annee;
 }
 
 public void setMois(int mois)
 {
   this.mois = mois;
 }
 
 public void setJour(int jour)
 {
   this.jour = jour;
 }

Avec cette approche, on pourrait changer la représentation en mémoire d’une date, par exemple en utilisant un seul entier qui combinerait le jour, le mois et l’année avec une formule du genre (d = année x 10000 + mois x 100 + jours) sans avoir à changer quoi que ce soit dans les méthodes utilisant la classe Date. Il suffirait de modifier le code des accesseurs pour que tout fonctionne correctement.

Par exemple getMois() retournerait (d/100)%10000 et setMois(int m) effectuerait l’opération d = getAnnee()*10000 + m*100 + getJour().

Exercices de découverte

B-dec-20

Réalisez une méthode d’accès setDate permettant de modifier simultanément le jour, le mois et l’année de l’instance courante de la classe Date. Cette méthode doit avoir trois paramètre nommés jour, mois et annee.

"Solution"

public void setDate(int jour, int mois, int annee)
 {
   this.jour = jour; this.mois = mois; this.annee = annee;
 }

B-dec-21

Donnez les lignes de code permettant de créer une instance de Date avec le constructeur par défaut de la classe Date, puis de modifier cette instance à l’aide de la méthode setDate de façon à ce que la date représentée soit le 12/10/2005.

"Solution"

Date d = new Date();
d.setDate(12, 10, 2005);


B3 : Composition de classes

On parle de composition de classes lorsqu’une classe a des attributs ayant pour types d’autres classes. Par exemple on peut définir une classe Periode dont les attributs désignent deux instances de Date et une instance de String.

public class Periode
 {
   private Date debut;
   private Date fin;
   private String nom;
 

  // À compléter
 }

Chacun des deux attributs debut et fin peut contenir une référence d’une instance de Date. Ces instances ont vocation à représenter le début et la fin d’une période constituée d’une ou plusieurs journées. Le troisième attribut est de type String. Il a vocation à contenir la référence d’une instance de String, c’est-à-dire une chaîne de caractères qui donne un nom à la période représentée.

On peut ajouter dans notre classe le constructeur suivant…

public Periode(Date deb, Date fin, String nom)
 {
   this.debut = deb;
   this.fin = fin;
   this.nom = nom;
 }

…puis la méthode toString suivante…

public String toString()
 {
   return "[ " + nom + " " + debut + " - " + fin + " ]";
 }

…et une méthode permettant de tester la classe…

public static void test()
 {
   Date d1 = new Date(4, 9, 2018);
   Date d2 = new Date(19, 9, 2018);
   Periode p = new Periode(d1, d2, "Vacances");
   System.out.println(p.toString());
 }

Notez bien que test est une méthode de classe qui ne nécessite pas d’objet de type Periode pour être appelée, et qui créée elle-même un objet de ce type. Pour appeler cette méthode test, par exemple depuis une méthode main, on écrit simplement…

Periode.test();

L’affichage obtenu est…

[ Vacances 4/9/2018 - 19/9/2018 ]

Voici ce qui se passe dans la mémoire…

image3

Exercices de découverte

B-dec-30

On considère deux variantes d’un constructeur de la classe Periode. En pratique, il faudra en choisir une car on ne peut implémenter dans une même classe deux constructeurs ayant exactement les mêmes paramètres.

/// Version 1
public Periode(Periode m)
{
    this.debut = m.debut;
    this.fin = m.fin;
    this.nom = m.nom;
}
/// Version 2
public Periode(Periode m)
{
    this.debut = new Date(m.debut.getJour(), m.debut.getMois(), m.debut.getAnnee());
    this.fin = new Date(m.fin.getJour(), m.fin.getMois(), m.fin.getAnnee());
    this.nom = m.nom;
}

On considère les lignes de code suivantes.

Date d1 = new Date(4, 9, 2018);
Date d2 = new Date(19, 9, 2018);
Periode p = new Periode(d1, d2, "Vacances");
Periode q = new Periode(p);
d1.setJour(5);
System.out.println(p.toString());
System.out.println(q.toString());

Donnez les affichages obtenus dans le cas où la version 1 du constructeur à 1 paramètre est utilisée et dans le cas où la version 2 est utilisée.

"Solution"

Version 1 :

[Vacances 5/9/2018 - 19/9/2018]
[Vacances 5/9/2018 - 19/9/2018]

Version 2 :

[Vacances 5/9/2018 - 19/9/2018]
[Vacances 4/9/2018 - 19/9/2018]

Pour bien comprendre ce résultat, il faut dessiner la configuration en mémoire dans les deux scénarios. Avec la version 1 du constructeur, il n’y a au total que deux objets de type Date dans le tas. Avec la version 2, on a deux new Date(...) en plus et au total 4 objets de type Datedans le tas. Dans le premier cas, les attributs debut des instances de Periode désignées par p et q pointent sur une même instance de Date. Si on la modifie, cela se répercute sur les deux périodes. Dans le deuxième cas, les attributs debut des instances de Periode désignées par p et q pointent sur deux instances de Date distinctes. Modifier l’une d’elle n’a pas d’effet sur l’autre.


B4 : Composition par tableaux

Dans cet exemple, nous créons une classe Memo qui contient plusieurs dates stockées dans un tableau. Cette classe a surtout une vocation pédagogique : montrer comment gérer un tableau référencé par une variable d’instance et comment les instances de la classe concernée sont représentées en mémoire.

public class Memo
 {
   private Date[] tab;
   private int nDates;


   // À compléter
 }

Une instance de cette classe représente une liste de dates, chacune représentée par une instance de Date. Un tableau est utilisé à cette fin, dans un but plus pédagogique que pratique. Examinons d’abord le constructeur de la classe…

public Memo(int n)
 {
   tab = new Date[n]; nDates=0;
 }

Ce constructeur accepte en paramètre la capacité du mémo, c’est-à-dire le nombre maximum de dates qu’il va pouvoir contenir. Cela correspond à la taille du tableau désigné par l’attribut tab. L’attribut nDate indique le nombre de dates effectivement stockées dans le mémo. Cet attribut est donc initialisé à 0, puisqu’à sa création le mémo est vide. Incidemment, chaque cellule du tableau créé contient la référence null.

Pour ajouter une date à une instance de Memo, on adjoint à la classe une méthode add définie de la manière suivante…

public void add(Date d)
{
   if(nDates >= tab.length)
   {
     System.out.println("Tableau plein !");
   }
   else
   {
     tab[nDates] = d; nDates++;
   }
}

On passe en argument à add la référence d’une instance de Date à ajouter au mémo. Mais comme le tableau de stockage a une taille limitée, déterminée à sa création, on vérifie qu’il reste encore de la place en comparant le nombre de dates enregistrées avec la longueur (tab.length) du tableau de stockage.

Dans le cas où il reste de la place, la référence de l’instance de Date à ajouter est placée dans la première cellule libre du tableau, qui a pour indice la valeur de l’attribut nDate. Cette valeur est ensuite augmentée de 1 pour refléter le nouveau nombre de dates stockées dans le mémo.

Ajoutons à notre classe Memo une méthode d’affichage…

public void print()
{
   for(int i=0; i<nDates; i++)
   {
     System.out.println(tab[i].toString());
   }
}

Puis testons ce que nous avons programmé avec la méthode suivante, qui peut se trouver dans la classe Memo ou ailleurs.

public static void test()
{
   Memo m = new Memo(5);
   m.add(new Date(12,10,2017));
   m.add(new Date(14,12,2017));
   m.print();
}

Voici la situation en mémoire à la fin de l’exécution de cette méthode.

image4

Exercices de découverte

B-dec-40

Réalisez une méthode d’instance read de la classe Memo permettant de récupérer la référence de la date située à une position donnée en argument dans l’instance courante de Memo. Si m désigne une instance de Memoet i est un entier positif ou nul alors l’appel m.read(i) doit retourner…

  • la référence de la date située en position i dans le mémo s’il contient au moins i+1 dates,
  • la valeur null dans le cas contraire.

Les positions sont numérotées à partir de 0 comme il est d’usage en programmation.

"Solution"

public Date read(int i)
{
    if(i >= nDates) return null;
    else return this.tab[i];
}

B-dec-41

Réalisez un constructeur de la classe Memo permettant de créer un mémo contenant exactement deux dates.

public Memo(Date d1, Date d2)
{
    // À compléter
}
"Solution"

public Memo(Date d1, Date d2)
{
    tab = new Date[2]; nDates=2;
    tab[0] = d1;
    tab[1] = d2;
}


B5 : La classe standard ArrayList

Comme nous l’avons vu dans l’exemple de la classe memo, l’utilisation d’un tableau comporte une limitation assez embêtante : sa taille doit être fixée une fois pour toute lors de sa création. Pour éviter ce problème, on peut utiliser une classe nommée ArrayList permettant de faire des listes extensibles.

Cette classe est générique, dans le sens où elle peut être paramétrée par un type. ArrayList<T> représente une liste de références d’instances de T. Une telle liste est plus souple d’utilisation qu’un tableau car sa capacité n’est pas limitée (du moins elle n’est limitée que par les ressources mémoire allouées à la machine virtuelle java).

Exemple introductif

Examinons un exemple. Je veux créer une liste de dates (techniquement, d’instances de la classe Date). J’écris simplement…

ArrayList<Date> t = new ArrayList<>();

Une liste vide est créée dans le tas et sa référence est placée dans w. Cette liste à une capacité initiale de 10 emplacements (valeur par défaut allouée par le compilateur), mais cette capacité sera automatiquement augmentée si on y place plus de 10 éléments.

J’ajoute deux dates à ma liste…

t.add(new Date(1,1,1901));
t.add(new Date(2,2,1902));

Examinons ce qui s’est passé dans la mémoire.

image5

La représentation d’une instance d’ArrayList a été simplifiée (tout comme celle des instances de String que nous avons eu l’occasion de voir précédemment). En réalité, une instance d’ArrayList comporte plusieurs attributs dont un désigne un tableau qui pourra être remplacé par un autre de taille différente à chaque fois que c’est nécessaire. Mais puisque cette classe a été créée pour nous simplifier la vie, autant en simplifier la représentation.

Précisions sur la classe ArrayList<T>

L’essentiel à savoir concernant la classe ArrayList<T>, c’est que :

  • toute instance w contient w.size() éléments,
  • tout élément est soit une référence de type T, soit null.
  • la position de chaque élément est désignée par un indice,
  • le premier élément a pour indice 0.

Voici quelques méthodes de la classe ArrayList. Il y en a beaucoup d’autres, que vous pourrez trouver dans la documentation officielle à l’URL https://docs.oracle.com/javase/8/docs/api/

  • int size() : retourne le nombre d’éléments stockés.
  • boolean add(T e) : ajoute l’élément e en fin de liste. La valeur de retour est toujours true. Elle peut être ignorée.
  • void add(int i, T e) : ajoute l’élément e en position i et décale (si applicable) les éléments suivants d’une position. La plus grande valeur autorisée pour i est size().
  • T get(int i) : retourne l’élément situé en position i. La plus grande valeur autorisée pour i est size()-1.
  • T remove(int i) : retire l’élément situé en position i et le retourne. Les éléments suivants (si applicable) sont décalés d’une position pour boucher le « trou ».
  • int indexOf(Object e) : retourne l’indice de la position de la première occurrence de l’objet e dans la liste, ou -1 si cet objet n’est pas dans la liste. (Toute référence a le type Object par héritage.)
  • T set(int i, T e) : remplace l’élément situé en position i par e.
  • void clear(int i) : retire tous les éléments de la liste.

Un exemple d’utilisation

Soit une classe Personne qui représente une personne et dispose d’une méthode toString permettant d’afficher le nom et le prénom de la personne représentée.

public class Personne
{
  private String nom;
  private String prenom;
  
  public Personne(String nom, String prenom)
  {
    this.nom = nom; this.prenom = prenom;
  }
  
  publis String toString()
  {
    return nom + " " + prenom;
  }
}

Voici une classe Document dont chaque instance représente un document ayant un nom et des auteurs. Voici les attributs de la classe Document.

public class Document
 {
   private String nom;
   private ArrayList<Personne> auteurs;

   // À compléter
}

Pour initialiser ces attributs, il nous faut un constructeur. En voici un…

public Document(String n, Personne[] a)
{
   this.nom=n;
   auteurs = new ArrayList<Personne>();
   for(int i=0; i<a.length; i++)
   {
     auteurs.add(a[i]);
   }
}

Il permet de créer un document en une seule fois, à partir des données reçues en argument, à savoir la référence n d’une chaîne de caractères et un tableau a de références d’instances de la classe Personne. Cela suppose qu’au moment d’utiliser ce constructeur, on dispose de ces données qui ont été préalablement créées.

La première ligne…

  this.nom=n;

…place juste dans l’attribut nom la référence d’instance de String contenue dans n.

La ligne suivante…

  auteurs = new ArrayList<Personne>();

… appelle le constructeur par défaut de ArrayList<Personne> pour créer une liste vide à laquelle on pourra ajouter autant des références à des instances de Personne que désiré. L’opérateur new retourne la référence de la liste créée, qui est placée dans l’attribut auteurs.

Ensuite, une boucle for

  for(int i=0; i<a.length; i++)
   {
     auteurs.add(a[i]);
   }

… parcourt le tableau passé en argument et ajoute chacune des références qu’il contient à la liste des auteurs du document en cours de construction.

Nous allons ajouter une méthode permettant de récupérer le nombre d’auteurs d’un document…

public int nbAuteurs()
 {
   return auteurs.size();
 }

… qui fait bêtement appel à une méthode de la classe ArrayList retournant le nombre d’éléments de l’instance d’ArrayList concernée.

La méthode suivante, toString, permet de créer une chaîne de caractères donnant une description de l’instance courante de Document.

public String toString()
{
   String da = "";
   for (int i = 0; i < nbAuteurs(); i++)
   {
     da = da + auteurs.get(i) + " ";
   }
   return nom + "\nAuteur(s) : " + da;
}

Notez au passage le moyen utilisé pour récupérer les informations sur les auteurs mérite quelques explications. La ligne…

da = da + auteurs.get(i) + " ";

…récupère la référence désignant l’élément situé en position i dans la liste. Jusque-là rien d’étonnant. Mais ensuite cet élément, qui est de type Personne (entendez par là qu’il s’agit d’une référence d’une instance de Personne), est ajouté à la chaîne en cours de construction grâce à l’opérateur +. Mais attendez, une instance de Personne n’est pas une chaîne de caractère ! Comment Java fait-il pour ajouter une instance de Personne à une chaine de caractères ?

En réalité, dans un tel contexte java remplace implicitement auteurs.get(i) par auteurs.get(i).toString(). C’est une sorte de notation raccourcie qui permet d’ajouter un objet à une chaîne, et qui fonctionne bien si l’objet en question dispose d’une méthode toString correctement programmée. Dans le cas contraire, c’est une chaîne représentant la référence de l’objet concerné qui est ajoutée à la chaîne.

Bon, notre classe Document n’offre pas encore beaucoup de possibilités, mais elle permet de créer et d’afficher des objets de type Document, à condition toutefois de programmer une classe Personne disposant à minima d’un constructeur et d’une méthode toString. Une personne pourrait par exemple être décrite par deux attributs de type String représentant son nom et son prénom.

La création d’une instance de Document peut alors prendre la forme suivante…

Personne auteur1 = new Personne("Brian","Kernighan");
Personne auteur2 = new Personne("Dennis","Ritchie");
Document doc = new Document("Le langage C",new Personne[]{auteur1, auteur2});

La syntaxe…

Personne[]{auteur1, auteur2}

…permet de créer un tableau d’instances de Personne et de l’initialiser en même temps.

Exercices de découverte

B-dec-50

Ajoutez à la classe Document un constructeur qui crée une instance représentant un document sans auteur (la liste d’auteurs est vide), mais ayant un nom.

public Document(String nom)
{
    // À compléter
}
"Solution"

public Document(String nom)
{
    this.nom = nom;
    this.auteurs = new ArrayList<Personne>();
}   

B-dec-51

Ajoutez à la classe Document une méthode addAuteur permettant d’ajouter un auteur au document courant.

public void addAuteur(Personne p)
{
     // À compléter
}
"Solution"

public void addAuteur(Personne p)
{
     this.auteurs.add(p);
}

B-dec-52

Ajoutez à la classe Document une autre méthode addAuteur permettant d’ajouter un auteur au document courant.

public void addAuteur(String nom, String Prenom)
{
     // À compléter
}
"Solution"

public void addAuteur(String nom, String Prenom)
{
     this.auteurs.add(new Personne(nom, prenom));
}


Exercices d’assimilation

B-ass-00

Réalisez une classe Compteur qui comporte un attribut (variable d’instance) cpt de type int, un constructeur par défaut qui initialise cette variable à 0, une méthode d’instance plusUn qui incrémente cette variable, et une méthode d’instance lecture qui retourne la valeur de la variable cpt.

Réalisez une méthode main située dans une classe Main qui crée une instance de Compteur, réalise trois incrémentations de cette instance et affiche sa valeur (i.e., la valeur de sa variable cpt).

"Indice"

public class Compteur
{
    private int cpt;

    public Compteur()
    {
        // À compléter
    }

    public void pluUn()
    {
        // À compléter
    }

    public int lecture()
    {
        // À compléter
    }

    public static void main(String[] args)
    {
        Compteur c = new Compteur();
        // À compléter
    }
}

"Solution"

public class Compteur
{
    private int cpt;

    public Compteur()
    {
        cpt = 0;
    }

    public void pluUn()
    {
        cpt = cpt + 1;
    }

    public int lecture()
    {
        return cpt;
    }

    public static void main(String[] args)
    {
        Compteur c = new Compteur();

        c.pluUn(); c.pluUn();; c.pluUn();
        System.out.println(c.lecture());
    }
}

B-ass-01

Réécrivez à la classe Memo (en vous limitant aux attributs, au constructeur à un paramètre et aux méthodes add et print) en remplaçant le tableau par un ArrayList.

"Indice"

public class MemoExt
{
    private ArrayList<Date> tab;

    public MemoExt()
    {
        tab = new // À compléter
    }

    public void add(Date d)
    {
        // À compléter
    }

    public void print()
    {
        for(int i=0; i<tab.size(); i++)
        {
            // À compléter
        }
    }
}

L’attribut nDate n’est plus nécessaire car le nombre de dates stockées peut être obtenu avec la méthode size de la classe ArrayList.

"Solution"

public class MemoExt
{
    private ArrayList<Date> tab;

    public MemoExt()
    {
        tab = new ArrayList<>();
    }

    public void add(Date d)
    {
        tab.add(d);
    }

    public void print()
    {
        for(int i=0; i<tab.size(); i++)
        {
            System.out.println(tab.get(i).toString());
        }
    }
}

B-ass-02

Réalisez une classe Point qui représente les coordonnées (x,y) d’un point dans un repère cartésien à 2 dimensions. Les coordonnées x et y sont représentées par des nombres en virgule flottantes de type double. La classe Point dispose d’un constructeur permettant de créer un point à partir de ses coordonnées, ainsi que d’une méthode toString, et deux accesseurs getX et getY permettant de récupérer les coordonnées. Elle possède aussi une méthode d’instance public double dist(Point p) qui calcule et retourne la distance entre le point courant et le point désigné par le paramètre p.

La méthode de classe prédéfinie double Math.sqrt(double x), qui retourne la racine carrée de x, vous sera utile.

"Indice"

public class Point
{
    private double x;
    private double y;

    public Point(double x, double y)
    {
        // À compléter
    }

    public double getX() {return x;}

    public double getY() {return y;}

    public String toString()
    {
        // À compléter
    }

    public double dist(Point p)
    {
        double dx = // À compléter
        double dy = // À compléter
        return Math.sqrt(dx*dx + dy*dy);
    }
}

"Solution"

public class Point
{
    private double x; private double y;

    public Point(double x, double y)
    {
        this.x = x; this.y = y;
    }

    public double getX() {return x;}

    public double getY() {return y;}

    public String toString()
    {
        return "(" + x + "," + y + ")";
    }

    public double dist(Point p)
    {
        double dx = this.x - p.x;
        double dy = this.y - p.y;
        return Math.sqrt(dx*dx + dy*dy);
    }
}

B-ass-03

Réalisez une classe Polygone qui représente un polygone sous la forme d’une liste de points. Cette classe dispose d’un constructeur à trois paramètres de type Point qui produit un triangle et un autre à 4 paramètres de type Point qui produit un quadrilatère. Elle dispose également d’une méthode d’instance perimetre qui retourne la somme des longueurs des côtés du polygone courant et d’une méthode toString qui produit une chaîne indiquant juste le nombre de cotés et le périmètre du polygone courant.

"Indice

public class Polygone
{
    private ArrayList<Point>  liste;

    public Polygone()
    {
        // À compléter
    }

    public Polygone(Point a, Point b, Point c)
    {
        this(); // appelle le constructeur par défaut
        // À compléter
    }

    public Polygone(Point a, Point b, Point c, Point d)
    {
        this(a, b, c); // appelle le contsructeur de triangle
        // À compléter
    }

    public double perimetre()
    {
        // À compléter
    }

    public String toString()
    {
        // À compléter
    }
}

Bien que ce ne soit pas obligatoire, dans cette solution partielle, certains constructeurs utilisent this(...) pour appeler un autre constructeur de manière à éviter de refaire ce que cet autre constructeur sait déjà faire. Ceci évite de répliquer du code identique dans plusieurs constructeurs. Lorsqu’un constructeur en appelle un autre, il faut que cet appel soit réalisé par sa première ligne de code.

"Indice

Voici le code complet des constructeurs et quelques élément de la méthode perimetre. Il vous reste à terminer cette méthode et écrire ma méthode toString.

public Polygone()
{
    liste = new ArrayList<>();
}

public Polygone(Point a, Point b, Point c)
{
    this();
    liste.add(a); liste.add(b);  liste.add(c);
}

public Polygone(Point a, Point b, Point c, Point d)
{
    this(a, b, c); liste.add(d);
}

public double perimetre()
{
    double sum = 0.0;
    for(int i=1; i<liste.size(); i++)
    {
        sum = sum + // À compléter
    }
    sum = sum + // À compléter;
    return sum;
}

"Solution"

public class Polygone
{
    private ArrayList<Point>  liste;

    // Constructeurs (voir indice 1)

    public double perimetre()
    {
        double sum = 0.0;
        for(int i=1; i<liste.size(); i++)
        {
            sum = sum + liste.get(i-1).dist(liste.get(i));
        }
        sum = sum + liste.get(0).dist(liste.get(liste.size()-1));
        return sum;
    }

    public String toString()
    {
        String s = "Polygone de " + liste.size() + " sommets";
        return s + " de périmètre " + perimetre();
    }
}

B-ass-04

Cet exercice fait suite à l’exercice précédent. Vous devez ajouter à la classe Polygone une méthode de classe (static) triangle qui crée et retourne un polygone à trois sommets dont les coordonnées lui sont passées en argument. cette méthode aura donc 6 paramètres de type double.

Vous devez également réaliser une méthode main qui crée une instance de Polygone représentant un triangle en appelant la méthode triangle et qui affiche à l’écran la description de ce triangle obtenue par un appel de la méthode toString de la classe Polygone.

"Indice"

public static Polygone triangle(double x1, double y1, double x2, double y2, double x3, double y3)
{
    Point a = // À compléter
    Point b = // À compléter
    Point c = // À compléter
    return new // À compléter
}
public static void main(String[] args)
{
    Polygone p = // À compléter
    System.out.println(p);
}

La syntaxe la plus stricte pour la deuxième ligne de main devrait être System.out.println(p.toString());. Mais lorsque la référence d’un objet est utilisée à la place d’une chaîne, le compilateur java appelle automatiquement la méthode toString de la classe concernée.

"Solution"

public static Polygone triangle(double x1, double y1, double x2, double y2, double x3, double y3)
{
    Point a = new Point(x1, y1);
    Point b = new Point(x2, y2);
    Point c = new Point(x3, y3);
    return new Polygone(a, b, c);
}
public static void main(String[] args)
{
    Polygone p = Polygone.triangle(0.0, 0.0, 1.0, 0.0, 0.0, 1.0);
    System.out.println(p);
}

B6 : Objets modifiables et non modifiables

Nous revenons ici sur une notion que nous avons déjà abordée sans la nommer dans la partie A. On l’appelle effet de bord. Prenons un exemple avec la classe Date.

public static void effetDeBord()
 {
   Date d1 = new Date(11,11,1918);
   Date d2 = d1;
   d2.setAnnee(1919);
   System.out.println(d1);
 }

On définit deux variables d1 et d2. On place dans d2 le contenu de d1, puis on change l’année de d2 et on affiche d1. Résultat…

11/11/1919

La modification de la date désignée par d2 a changé aussi celle désignée par d1. Ce n’est pas étonnant quand on regarde la configuration des données en mémoire.

image6

Les deux variables de méthode d1 et d2 contiennent la même référence, qui désigne la même instance de Date. Pour éviter un tel effet de bord, on peut par exemple doter la classe Date d’un constructeur en copie ou d’une méthode de clonage. Concentrons-nous sur la première solution.

public Date(Date d)
 {
   this.jour = d.jour;
   this.mois = d.mois;
   this.annee = d.annee;
 }

Voici comment utiliser ce constructeur pour dupliquer une instance de Date.

public static void pasDeffetDeBord()
 {
   Date d1 = new Date(11,11,1918);
   Date d2 = new Date(d1);
   d2.setAnnee(1919);
   System.out.println(d1);
 }

Cette fois-ci, la modification de la date désignée par d2 n’a pas d’effet sur celle désignées par d1. Voici la nouvelle configuration en mémoire.

image7

Une autre manière de permettre la duplication d’instance de Date, applicable à toute classe, comme la technique du constructeur en copie, consiste à ajouter à la classe Date une méthode de clonage.

public Date clone()
 {
   return new Date(this.jour, this.mois, this.annee);
 }

Voici un exemple d’utilisation.

public static void pasDeffetDeBord2()
 {
   Date d1 = new Date(11,11,1918);
   Date d2 = d1.clone();
   d2.setAnnee(1919);
   System.out.println(d1);
}

Exercices de découverte

B-dec-60

Réalisez une méthode d’instance de la classe Date retournant la date située une année plus tard que la date courante, sans modifier la date courante.

    public Date plusUnAn()
    {
        Date cpy = new // À compléter
        // À compléter
        return cpy;
    }
"Solution"

    public Date plusUnAn()
    {
        Date cpy = new Date(this);
        cpy.setAnnee(getAnnee()+1);
        return cpy;
    }

Il y a d’autres solutions valables, telles que celle-ci.

    public Date plusUnAn()
    {
        Date cpy = new Date(this.getJour(), this.getMois(), this.getAnnee() + 1);
        return cpy;
    }

B-dec-61

Réalisez une variante de la première variante de la méthode d’instance plusUnAn qui utilise la méthode de clonage de la classe Date au lieu de son constructeur en copie.

"Solution"

    public Date plusUnAn()
    {
        Date cpy = clone(); // ou this.clone()
        cpy.setAnnee(getAnnee()+1);
        return cpy;
    }

B7 : Les constantes

Le mot final rend une variable non modifiable et la transforme donc en constante. Sauf que… Examinons un exemple. image8

On ne peut pas modifier les références représentées par jours et Armistice, mais cela n’empêche pas de modifier les objets désignés par ces références. Par exemple on peut écrire…

jours[6]="sun";
Armistice.setAnnee(1919);

Ce qui modifiera les objets concernés.

Exercices de découverte

B-dec-70

Pour afficher le nombre $\pi$, on peut écrire :

System.out.println(Math.PI);

Donnez la déclaration de la variable d’instance PI dans le classe Math.

"Solution"

public static final double PI = 3.141592653589793;

B8 : Les classes enveloppes

En java, les types primitifs ne sont pas des objets. Ils constituent une sorte d’entorse au paradigme de programmation objet. Mais il existe des classes enveloppes qui permettent d’encapsuler des données de types primitifs dans des objets : Integer, Double, Boolean… Ces types enveloppe peuvent être utiles pour utiliser des données primitives dans de situation où seuls des objets sont utilisables, comme dans des ArrayList. Voici un exemple.

image9

Exercices de découverte

B-dec-80

Complétez cette illustration de la configuration en mémoire à l’issue de l’exécution des 4 premières lignes de code de l’exemple précédent.

image-20210905232113983

"Solution"

image-20210905232223671

B9 : Objets non modifiables

Les objets n’ayant que des attributs privés et ne disposant pas de méthodes permettant de modifier leurs attributs (ni les données accessibles par ces attributs) sont dits non modifiables, tout comme les classes qui définissent de tels objets.

Par exemple, les classes enveloppes sont non modifiables, ainsi que la classe String. L’intérêt de telles classes c’est qu’elles empêchent les effets de bord, mais en contrepartie d’une perte d’efficacité. On peut le voir en considérant deux classes permettant de représenter des chaînes : String, qui est non modifiable, et StringBuffer, qui est modifiable.

La méthode de classe suivante…

public static String makeString1(String s, int n)
 {
   String r = "";
   for(int i=0; i<n; i++)
   {
     r = r + s;
   }
   return r;
 }

…permet de construire une chaîne par n concaténations successives d’une chaîne s. Par exemple…

System.out.println(makeString1("abcdefgh",10));

… affiche…

abcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefghabcdefgh

Mais cette méthode n’est pas très efficace. Pour fabriquer la chaîne retournée, cette méthode crée successivement n nouvelles chaînes intermédiaires qui sont ensuite recyclées par le ramasse-miettes de la machine virtuelle. En effet, chaque concaténation réalisée par l’opérateur + ne modifie pas une chaîne existante (puisque les chaînes sont des objets non modifiables) mais en crée une nouvelle.

image10

On peut réaliser la même chose en utilisant la classe modifiable StringBuffer qui dispose d’une méthode append permettant d’ajouter une chaîne à la fin de la chaîne courante sans en recréer une nouvelle.

public static String makeString2(String s, int n)
 {
   StringBuffer r = new StringBuffer();
   for(int i=0; i<n; i++)
   {
     r.append(s);
   }
   return r.toString();
 }

Lors de la création d’une instance de StringBuffer, une certaine ressource de mémoire est réservée pour le stockage de la chaîne courante. Cette quantité de mémoire est augmentée en fonction des besoins.

image11

Exercices de découverte

B-dec-90

À quoi peut-on reconnaître une classe non modifiable en regardant son code ?

"Solution"

Une classe est non modifiable si et seulement si aucune de ses méthodes ne permet de changer la valeur d’un des attributs d’un objet après sa création.

Exercices d’assimilation

B-ass-50

Réalisez un constructeur en copie pour la classe Memo (en version ArrayList comme demandé par l’exercice 2). Ce constructeur doit accepter en paramètre une instance de Memo déjà créée et faire en sorte que l’instance construite soit identique, mais que toute modification de la copie créée (ajout ou modification d’une date) n’affecte pas l’original et réciproquement.

"Indice

public Memo(Memo m)
{
    this(); // Pour créer la liste vide
    for(int i=0; i<tab.size(); i++)
    {
        tab.add(// À compléter);
    }
}

"Indice

public Memo(Memo m)
{
    this();
    for(int i=0; i<tab.size(); i++)
    {
        tab.add( new Date(// À compléter) );
    }
}

Il est important de créer de nouvelles instances de Date identiques à celles qui sont dans le mémo servant de modèle. Si on plaçait dans la liste du nouveau mémo les références des dates du mémo servant de modèle, une modification d’une de ces dates affecterait les deux mémos, parce que Date est une classe modifiable. Le problème ne se poserait pas si c’était une classe non modifiable.

"Solution"

public Memo(Memo m)
{
    this();
    for(int i=0; i<tab.size(); i++)
    {
        tab.add( new Date(m.tab.get(i)) );
    }
}

Voici une variante qui fonctionne aussi avec un code un peu plus propre.

public Memo(Memo m)
{
    this();
    for(Date e : tab)
    {
        tab.add( new Date(e) );
    }
}

Il serait encore plus conforme aux bonnes pratiques de mettre dans la classe Memo une méthode permettant de récupérer la date située à une position passée en argument, et d’utiliser cette méthode au lieu d’accéder à l’attribut tab. Toutefois, l’accès à cet attribut privé est parfaitement légal puisque le modificateur d’accès private privatise un attribut dans le scope d’une classe et non de chacune des instances de cette classe : une méthode d’instance peut accéder aux attributs privés d’une autre instance, si elle a accès la référence de cette autre instance.

B-ass-51

Ajoutez à la classe Polygone une méthode…

public Polygone ext(Point p)

…qui produit un nouveau polygone constitué des points du polygone courant plus le point désigné par le paramètre p. L’ancien et le nouveau polygone doivent exister indépendamment l’un de l’autre. Pour rendre le code plus lisible, vous devrez définir un constructeur en copie de la classe Polygone, qui doit appeler le constructeur par défaut de cette classe. Notez que la classe Point n’est pas modifiable.

"Indice"

Voici un constructeur en copie pour la classe Polygone.

public Polygone(Polygone m)
{
    this();
    for(Point p : m.liste)
    {
        liste.add(p);
    }
}

Il reste à écrire la méthode ext.

"Solution"

// Constructeur en copie donné dans l'indice 1

public Polygone ext(Point p)
{
    Polygone poly = new Polygone(this);
    poly.liste.add(p);
    return poly;
}

Exercices d’approfondissement

B-cons-00

Vous devez développer un programme permettant de simuler un système d’offres par soumissions cachetées pour l’achat d’objets identiques. Il y a N objets en vente. Il peut y avoir plus (ou moins, ou autant) d’acheteurs intéressés que d’objets en vente. Chaque acheteur peut faire une ou plusieurs offres qui ne sont pas connues par les autres acheteurs. Les N meilleures offres seront retenues.

Chaque offre est représentée par une instance de la classe Offre suivante.

public class Offre
{
    public int id;
    public double montant;

    public Offre(int id, double montant)
    {
        this.id = id; this.montant = montant;
    }

    public int getId() {return id;}

    public double getMontant() {return montant;}

    public String toString()
    {
        return "[ " + id + " : " + montant + "]";
    }
}

Les offres sont placées dans une instance de la classe PrioFile, que vous devez implémenter. Une telle instance est appelée file de priorité. Elle contient une liste d’offres. Pour créer une file de priorité, on écrit…

PrioFile file = new PrioFile(n);

…où nest le nombre d’objets en vente. La file de priorité créée comportera au plus n offres, chacune représentée par une instance de la classe Offre. Tant que la file contient moins de n offres, chaque nouvelle offre y est automatiquement ajoutée. Au delà, quand une nouvelle offre est proposée, elle n’est ajoutée que si son montant dépasse celui de l’offre la plus récente parmi celles ayant le plus petit montant.

Pour soumettre une offre, on écrit…

file.soumission(id, montant);

…où id est l’identifiant d’une acheteur potentiel et montant est le montant proposé.

La classe PrioFile dispose d’une méthode toString qui affiche la liste complète des offres retenues.