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
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.
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.
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.
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 ?
Si oui, donnez les lignes de code permettant d’obtenir cette configuration. Si non, expliquez pourquoi.
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.
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.
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
.
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
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).
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 :
- Création d’une instance de
SuperTabInt
représentant un tableau évolué de 10 entiers. - Écriture des valeurs 55, 56 et 33 aux indices 2, 3 et 5, respectivement.
- 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 :
- Affichage du tableau évolué à l’aide de la méthode
toString
de la classeSuperTabInt
. - Affichage de la chaîne produite par la méthode
toString
de la classeBadAccess
.
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.
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.
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
.
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
.
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;
}