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