Exercices sur l’héritage et le polymorphisme. #
Questions/Réponses #
Veuillez répondre mentalement, sur papier ou bien en créant le code nécessaire pour répondre à ces questions avant de regarder la réponse.
Prenez note qu'il est permis d'utiliser le robot conversationnel du cours lors des exercises. Cependant vous devriez vous entraîner à produire vos propres réponses.
Réponses uniques? #
Les exercices comportent une solution vous permettant de comparer votre approche avec la nôtre. Il n'y a pas de solution unique aux problèmes en général. Vous pouvez arriver avec une solution qui est préférable ou moins bonne que celle que nous offrons. Pour faire ces questions, vous devez avoir fait toutes les lectures préalables. Vous disposez alors toujours des fondements nécessaires pour faire les exercices. Nous vous encourageons tout de même à faire vos propres recherches en complément de vos lectures. Dans certains cas, au sein de la solution que nous offrons, nous pouvons utiliser des notions techniques qui n'ont pas été vues directement dans le cours, mais qui devraient vous être facilement accessibles.
Question 1 #
Pourquoi le code suivant entraîne t-il une erreur à la compilation?
public class Test extends JFrame, Thread {
String test;
public Test(String test) {
this.test = test;
}
public void run() {
System.out.println(test);
}
}
Réponse
En Java, il est impossible de faire de l'héritage multiple, il faut donc soit hériter de JFrame ou de Thread, mais pas les deux à la fois. Une façon de contourner le problème serait d'implémenter l'interface Runnable, puis de passer en paramètre cette classe à un thread.
Question 2 #
Voici ci dessous, une classe permettant de lire une image de type PNG et d'en extraire les occurrences de gradients de couleur :
public class PNGGradientExtractor {
int[][] gradientMatrix;
public PNGGradientExtractor(File file) {
// Lit le fichier et charge
loadImage(file);
}
public void loadImage(File file) {
//Charge l'image et la met en format "raw" dans la matrice gradientMatrix
return;
}
public HashMap getGradientMap() {
// Retourne une hashmap avec l'occurence de gradient dans l'image
return null;
}
}
À l'aide de l'héritage et des classes abstraites, veuillez implémenter les classes qui permettront de : a. Créer une classe abstraite GradientExtractor; b. Créer une classe GIFGradientExtractor; c. modifier PNGGradientExtractor pour tenir compte des changements précédent. Pour simplifier l'exercice, ce qui diffère le GIF du PNG est le chargement de l'image dans la matrice gradientMatrix. VOUS DEVEZ FAIRE SEULEMENT LA STRUCTURE (CLASSES ET MÉTHODES) SANS IMPLÉMENTATION!
Réponse
public abstract class GradientExtractor {
protected int [][] gradientMatrix;
public HashMap getGradientMap() {
// Retourne une hashmap avec l'occurence de gradient dans l'image
return null;
}
public abstract void loadImage(File file);
}
public class GIFGradientExtractor extends GradientExtractor {
public GIFGradientExtractor(File file) {
// Lit le fichier et charge
loadImage(file);
}
public void loadImage(File file) {
//Charge l'image et la met en format "raw" dans la matrice gradientMatrix
return;
}
}
public class PNGGradientExtractor extends GradientExtractor {
public PNGGradientExtractor(File file) {
// Lit le fichier et charge
loadImage(file);
}
public void loadImage(File file) {
//Charge l'image et la met en format "raw" dans la matrice gradientMatrix
return;
}
}
Question 3 #
Voici une classe permettant de calculer la regression linéaire d'une série temporelle d'entier :
public class SerieTemporelle {
int[] serie;
public SerieTemporelle(int[] serie) {
this.serie = serie;
}
public void calculerRegressionLineaire() {
int MAXN = 1000;
double[] x = new double[MAXN];
double[] y = new double[MAXN];
// first pass: read in data, compute xbar and ybar
double sumx = 0.0, sumy = 0.0, sumx2 = 0.0;
for (int i = 0; i < serie.length; i++) {
x[i] = i;
y[i] = serie[i];
sumx += x[i];
sumx2 += x[i] * x[i];
sumy += y[i];
}
double xbar = sumx / serie.length;
double ybar = sumy / serie.length;
// second pass: compute summary statistics
double xxbar = 0.0, yybar = 0.0, xybar = 0.0;
for (int i = 0; i < serie.length; i++) {
xxbar += (x[i] - xbar) * (x[i] - xbar);
yybar += (y[i] - ybar) * (y[i] - ybar);
xybar += (x[i] - xbar) * (y[i] - ybar);
}
double beta1 = xybar / xxbar;
double beta0 = ybar - beta1 * xbar;
// print results
System.out.println("y = " + beta1 + " * x + " + beta0);
// analyze results
int df = serie.length - 2;
double rss = 0.0; // residual sum of squares
double ssr = 0.0; // regression sum of squares
for (int i = 0; i < serie.length; i++) {
double fit = beta1*x[i]+beta0;
rss += (fit - y[i]) * (fit - y[i]);
ssr += (fit - ybar) * (fit - ybar);
}
double R2 = ssr / yybar;
double svar = rss / df;
double svar1 = svar / xxbar;
double svar0 = svar/serie.length + xbar*xbar*svar1;
System.out.println("R^2 = " + R2);
System.out.println("std error of beta_1 = " + Math.sqrt(svar1));
System.out.println("std error of beta_0 = " + Math.sqrt(svar0));
svar0 = svar * sumx2 / (serie.length * xxbar);
System.out.println("std error of beta_0 = " + Math.sqrt(svar0));
System.out.println("SSTO = " + yybar);
System.out.println("SSE = " + rss);
System.out.println("SSR = " + ssr);
}
public static void main(String[] args) {
int[] serie = {100, 22, 55, 10, 5, 66, 71, 8, 91};
SerieTemporelle serieTemporelle = new SerieTemporelle(serie);
serieTemporelle.calculerRegressionLineaire();
}
}
À l'aide du polymorphisme paramétrique (les templates), veuillez modifier le code afin de permettre des séries temporelles de plusieurs classes (ex. Double, Integer, etc.).
Réponse
Question 4 #
À partir du code suivant, veuillez en extraire une classe supérieure qui sera héritée et deux interfaces :
public class VoitureEssence {
public boolean isRunning() {
return false;
}
public void addGaz(int litres) {
}
public double getSpeed() {
return 0;
}
}
public class VoitureElectrique {
public boolean isRunning() {
return false;
}
public void chargeBattery(int mah) {
}
public double getSpeed() {
return 0;
}
}
Réponse
public class Voiture {
public boolean isRunning() {
return false;
}
public double getSpeed() {
return 0;
}
}
public class VoitureElectrique extends Voiture implements MoteurElectrique {
@Override
public void chargeBattery(int mah) {
}
}
public class VoitureEssence extends Voiture implements MoteurEssence {
@Override
public void addGaz(int litres) {
}
}
public interface MoteurElectrique {
void chargeBattery(int mah);
}
public interface MoteurEssence {
void addGaz(int litres);
}
Question 5 #
Dans le code ci-dessous, quel est le type de polymorphisme utilisé?
public class Classe1 {
public void uneMethode(String arg) {
}
public void uneMethode(StringBuffer arg) {
}
}
Réponse
Il s'agit du polymorphisme ad hoc.Question 6 #
Dans le code ci-dessous, quel est le type de polymorphisme utilisé?
public class Classe1 {
public void uneMethode() {
}
}
public class Classe2 extends Classe1 {
public void uneMethode() {
}
public static void main(String[] args) {
Classe2 uneClase = new Classe2();
((Classe1) uneClase).uneMethode();
}
}
Réponse
Il s'agit du polymorphisme par héritage (ou d'héritage).Question 7 #
Considérons la classe Point suivante :
public class Point {
public Point (int abs, int ord) {
x = abs; y = ord;
}
public void deplace (int dx, byte dy) {
x += dx; y += dy;
}
public void deplace (byte dx, int dy) {
x += dx; y += dy;
}
int x, y;
}
- On voit que la classe Point a deux méthodes qui portent le même nom : Quelle technique est mise en œuvre pour y parvenir ici ?
- Quel est le résultat de la compilation de chacune des deux classes suivantes ? Expliquez chacun de ces résultats.
public class Test1 {
public static void main (String args[]) {
int n=1; byte b=1;
Point a = new Point(n,n);
a.deplace(b, b);
}
}
public class Test2 {
public static void main (String args[]) {
int n=1; byte b=1;
Point a = new Point(n,n);
a.deplace (2*b, b);
}
}
Réponse
- Il s’agit de la surdéfinition ou surcharge, car les deux méthodes ont des signatures différentes.
- Test1 génère une erreur de compilation à cause de l’ambiguïté dans le choix de la méthode deplace à appeler. Cette ambiguïté est liée au fait que chacune des deux méthodes peut être appelée, compte tenu des conversions implicites de byte en int. Test2 compile normalement, l’ambiguïté soulignée ci-dessus est levée par le fait que 2*b force d’office la conversion du premier paramètre réel en int, et donc c’est la méthode déplace (int dx, byte dy) qui est appelée.
Question 8 #
On suppose qu’il existe une classe A dotée d’un constructeur par défaut.
Soient les trois instructions suivantes :
A a = new A();
Object o = new Object();
o=a;
A l’issue de ces trois instructions, on a :
- deux variables de même type et contenant les mêmes références ;
- deux variables de type différent contenant les mêmes références ;
- deux variables de même type contenant des références différentes ;
- rien de tout cela car une erreur est générée.
Réponse
Bonne réponse : 2.
Question 9 #
On dispose d’une interface I mettant en œuvre plusieurs méthodes. Soit
interface I {
void methode1();
void methode2();
void methode3();
void methode4();
}
On voudrait faire partager cette interface par deux classes ClasseA et ClasseB pouvant être regroupées dans une classe de base ClasseDeBase et partageant au moins une méthode (methodeDifferee) présente dans cette classe de base mais non encore définie. De plus, ClasseA ne doit implémenter que methode1 et methode2 de I, alors que ClasseB doit implémenter methode3 et methode4 de I.
Un programmeur songe à la solution suivante :
abstract class ClasseDeBase {
abstract public void methodeDifferee();
}
public class ClasseA extends ClasseDeBase implements I {
public void methodeDifferee() {
System.out.print("instructions de la méthode différée ici");
}
void methode1() {
System.out.print("instructions de méthode1 ici");
}
void methode2() {
System.out.print("instructions de méthode2 ici");
}
}
public class ClasseB extends ClasseDeBase implements I {
public void methodeDifferee() {
System.out.print("instructions de la méthode différée ici");
}
void methode3() {
System.out.print("instructions de méthode3 ici");
}
void methode4() {
System.out.print("instructions de méthode4 ici");
}
}
Réponse
Le problème avec la solution est que chacune des classes ClasseA et ClasseB doit impérativement implémenter toutes les méthodes de l’interface I, et pas seulement certaines.
Une solution c’est de faire implémenter l’interface plutôt par la classe abstraite ClasseDeBase. C’est-à-dire :
abstract class ClasseDeBase implements I {
abstract public void methodeDifferee();
void methode1() {
System.out.print("instructions même à redéfinir plus tard de méthode1 ici");
}
void methode2() {
System.out.print("instructions même à redéfinir plus tard de méthode2 ici");
}
void methode3() {
System.out.print("instructions même à redéfinir plus tard de méthode3 ici");
}
void methode4() {
System.out.print("instructions même à redéfinir plus tard de méthode4 ici");
}
}
public class ClasseA extends ClasseDeBase {
public void methodeDifferee() {
System.out.print("instructions de la méthode différée ici");
}
void methode1() {
System.out.print("instructions de méthode1 ici");
}
void methode2() {
System.out.print("instructions de méthode2 ici");
}
}
public class ClasseB extends ClasseDeBase {
public void methodeDifferee() {
System.out.print("instructions de la méthode différée ici");
}
void methode3() {
System.out.print("instructions de méthode3 ici");
}
void methode4() {
System.out.print("instructions de méthode4 ici");
}
}
Question 10 #
On dispose de différentes classes d’animaux (Poissons, Reptiles, Oiseaux, Mammifères) qui partagent en commun la méthode seDeplace. On voudrait effectuer un traitement qui consiste juste pour chaque animal d’une classe à afficher comment il se déplace. Ainsi, pour un Poisson p, p.seDeplace doit afficher « je suis un poisson, je nage » ; un Reptile « je suis un reptile, je rampe » ; un Oiseau « je suis un oiseau, je vole » ; un Mammifère « je suis un mammifère, je marche, je vole et je nage ». Proposer une solution en utilisant un seul tableau d’objets.
Réponse
public class Main {
public static void main(String[] args) {
Animaux[] tableau = new Animaux[4];
tableau[0] = new Oiseaux();
tableau[1] = new Poissons();
tableau[2] = new Reptiles();
tableau[3] = new Mammiferes();
for(int i=0; i<tableau.length;i++) {
tableau[i].seDeplace();
}
}}
abstract class Animaux {
abstract void seDeplace();
}
class Oiseaux extends Animaux {
void seDeplace() {
System.out.println("je suis un oiseau, je vole");
}
}
class Poissons extends Animaux {
void seDeplace() {
System.out.println("je suis un poisson, je nage");
}
}
class Reptiles extends Animaux {
void seDeplace() {
System.out.println("je suis un reptile, je rampe");
}
}
class Mammiferes extends Animaux {
void seDeplace() {
System.out.println("je suis un mammifère, je nage, je vole ou je marche");
}
}
Question 11 #
Expliquez la différence entre l’héritage simple et l’implémentation d’interfaces en Java.
Réponse
L’héritage simple permet à une classe de dériver d’une seule classe parente, héritant de ses méthodes et attributs. L’implémentation d’interfaces permet à une classe d’adopter plusieurs comportements en implémentant plusieurs interfaces, ce qui contourne l’absence d’héritage multiple en Java.
Question 12 #
Écrivez une interface Java nommée Volant avec une méthode void voler(), puis une classe Oiseau qui implémente cette interface.
Réponse
interface Volant {
void voler();
}
class Oiseau implements Volant {
public void voler() {
System.out.println("L’oiseau vole.");
}
}
Question 13 #
Expliquez la différence entre la redéfinition (override) et la surcharge (overload) de méthodes en Java.
Réponse
La redéfinition (override) consiste à fournir une nouvelle version d’une méthode héritée dans une sous-classe, avec la même signature. La surcharge (overload) consiste à définir plusieurs méthodes du même nom dans une même classe, mais avec des paramètres différents.
Question 14 #
Écrivez une classe Personne avec un attribut nom et une méthode afficherNom(). Créez une sous-classe Etudiant qui ajoute un attribut matricule et redéfinit la méthode afficherNom() pour afficher le nom et le matricule.
Réponse
class Personne {
String nom;
Personne(String nom) { this.nom = nom; }
void afficherNom() { System.out.println(nom); }
}
class Etudiant extends Personne {
String matricule;
Etudiant(String nom, String matricule) {
super(nom);
this.matricule = matricule;
}
void afficherNom() {
System.out.println(nom + " (" + matricule + ")");
}
}
Question 15 #
Écrivez une classe Java Animal avec une méthode parler() qui affiche "Je suis un animal". Créez deux sous-classes Chien et Chat qui redéfinissent la méthode parler() pour afficher respectivement "Wouf" et "Miaou".
Réponse
class Animal {
void parler() {
System.out.println("Je suis un animal");
}
}
class Chien extends Animal {
void parler() {
System.out.println("Wouf");
}
}
class Chat extends Animal {
void parler() {
System.out.println("Miaou");
}
}
Question 16 #
Expliquez à quoi sert l’interface Serializable en Java et donnez un exemple de situation où il est nécessaire de l’utiliser.
Réponse
L’interface Serializable en Java permet de marquer une classe dont les objets peuvent être sérialisés, c’est-à-dire convertis en un flux d’octets pour être facilement stockés ou transmis. Par exemple, il est nécessaire de l’utiliser lors de la sauvegarde de l’état d’un objet dans un fichier ou lors de l’envoi d’un objet à travers un réseau.
Question 17 #
Écrivez un exemple de classe Java qui implémente l’interface Comparable pour permettre le tri naturel d’objets selon un attribut.
Réponse
import java.util.*;
class Etudiant implements Comparable<Etudiant> {
String nom;
int note;
Etudiant(String nom, int note) {
this.nom = nom;
this.note = note;
}
public int compareTo(Etudiant autre) {
return Integer.compare(this.note, autre.note);
}
public String toString() {
return nom + ": " + note;
}
public static void main(String[] args) {
List<Etudiant> liste = new ArrayList<>();
liste.add(new Etudiant("Alice", 85));
liste.add(new Etudiant("Bob", 70));
liste.add(new Etudiant("Charlie", 95));
Collections.sort(liste);
for (Etudiant e : liste) {
System.out.println(e);
}
}
}
Question 18 #
Quelle est la différence entre Comparable et Comparator en Java ? Donnez un exemple d’utilisation de Comparator pour trier une liste d’objets selon un critère différent du tri naturel.
Réponse
L’interface Comparable impose un ordre naturel à une classe d’objets en définissant la méthode compareTo(), tandis que l’interface Comparator permet de définir des ordres de tri alternatifs à l’aide de la méthode compare(). Voici un exemple d’utilisation de Comparator :
import java.util.*;
class Etudiant {
String nom;
int note;
Etudiant(String nom, int note) {
this.nom = nom;
this.note = note;
}
public String toString() {
return nom + ": " + note;
}
public static void main(String[] args) {
List<Etudiant> liste = new ArrayList<>();
liste.add(new Etudiant("Alice", 85));
liste.add(new Etudiant("Bob", 70));
liste.add(new Etudiant("Charlie", 95));
// Tri par ordre alphabétique
Collections.sort(liste, new Comparator<Etudiant>() {
public int compare(Etudiant e1, Etudiant e2) {
return e1.nom.compareTo(e2.nom);
}
});
for (Etudiant e : liste) {
System.out.println(e);
}
}
}
Question 19 #
Donnez un exemple d’instanciation anonyme d’une interface Comparator en Java.
Réponse
// Exemple d'instanciation anonyme d'un Comparator pour trier une liste d'entiers par ordre décroissant
List<Integer> liste = Arrays.asList(5, 2, 9, 1);
Collections.sort(liste, new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return b - a; // ordre décroissant
}
});
System.out.println(liste); // Affiche [9, 5, 2, 1]
Ici, on crée une instance anonyme de l’interface Comparator directement dans l’appel à Collections.sort, sans créer de classe séparée.
Question 20 #
Pourquoi est-il utile d’utiliser des classes anonymes ou des expressions lambda pour implémenter des interfaces fonctionnelles en Java ? Donnez un exemple de cas où cela simplifie le code.
Réponse
Les classes anonymes et les expressions lambda permettent d’implémenter des interfaces fonctionnelles de manière concise et sans avoir à créer une classe nommée séparée. Cela simplifie le code, surtout pour des implémentations ponctuelles. Par exemple :
import java.util.*;
public class Exemple {
public static void main(String[] args) {
List<String> liste = Arrays.asList("un", "deux", "trois", "quatre");
// Tri avec une expression lambda
Collections.sort(liste, (a, b) -> b.compareTo(a));
liste.forEach(element -> System.out.println(element));
}
}
Question 21 #
Expliquez le principe de substitution de Liskov (Liskov Substitution Principle, LSP) en programmation orientée objet. Donnez un exemple simple en Java illustrant une violation de ce principe.
Réponse
Le principe de substitution de Liskov stipule que toute classe dérivée doit pouvoir être utilisée à la place de sa classe parente sans altérer le bon fonctionnement du programme. Autrement dit, les objets d’une sous-classe doivent pouvoir remplacer les objets de la superclasse sans que le code client ait à se soucier des différences.
Exemple de violation :
class Rectangle {
protected int largeur, hauteur;
public void setLargeur(int l) { largeur = l; }
public void setHauteur(int h) { hauteur = h; }
public int getAire() { return largeur * hauteur; }
}
class Carre extends Rectangle {
@Override
public void setLargeur(int l) { largeur = hauteur = l; }
@Override
public void setHauteur(int h) { largeur = hauteur = h; }
}
// Utilisation
Rectangle r = new Carre();
r.setLargeur(5);
r.setHauteur(10);
System.out.println(r.getAire()); // Affiche 100, mais on s’attendrait à 50 pour un rectangle
Ici, la classe Carre
(carré) viole le principe de substitution de Liskov car elle modifie le comportement attendu de la classe Rectangle
: un carré impose que la largeur et la hauteur soient toujours égales, ce qui n’est pas le cas d’un rectangle. Le code client utilisant un Rectangle
ne peut plus raisonner correctement si on lui passe un Carre
.
Question 22 #
Expliquez comment l’utilisation d’une interface peut simplifier le code en Java. Donnez un exemple concret où l’interface permet d’écrire du code plus flexible et réutilisable.
Réponse
L’utilisation d’une interface permet de définir un contrat commun à plusieurs classes, sans imposer d’héritage d’implémentation. Cela favorise la flexibilité, la réutilisation et le découplage du code. Grâce aux interfaces, on peut écrire des méthodes ou des classes qui manipulent des objets de types différents, à condition qu’ils respectent le même contrat (interface).
Exemple :
interface Affichable {
void afficher();
}
class Personne implements Affichable {
public void afficher() {
System.out.println("Je suis une personne.");
}
}
class Voiture implements Affichable {
public void afficher() {
System.out.println("Je suis une voiture.");
}
}
// Méthode générique
void afficherTout(Affichable[] objets) {
for (Affichable obj : objets) {
obj.afficher();
}
}
// Utilisation
Affichable[] tab = { new Personne(), new Voiture() };
afficherTout(tab);
Ici, la méthode afficherTout
peut traiter n’importe quel objet qui implémente l’interface Affichable
, ce qui rend le code plus générique, flexible et facile à étendre.
Question 23 #
Expliquez la différence entre l’opérateur ==
et la méthode equals()
en Java. Donnez un exemple où l’utilisation de l’un ou de l’autre change le résultat.
Réponse
L’opérateur `==` compare les références (adresses mémoire) pour les objets, tandis que la méthode `equals()` compare le contenu (la valeur logique) si elle est redéfinie. Pour les types primitifs, `==` compare directement les valeurs.
Exemple :
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
System.out.println(p1 == p2); // false (références différentes)
System.out.println(p1.equals(p2)); // true (contenu identique)
Question 24 #
Qu’est-ce que la méthode hashCode()
? Pourquoi est-il important de la redéfinir lorsqu’on redéfinit equals()
?
Réponse
La méthode `hashCode()` retourne un entier représentant le code de hachage d’un objet. Elle est utilisée dans les structures de données comme `HashMap` ou `HashSet` pour organiser et retrouver rapidement les objets. Si deux objets sont égaux selon `equals()`, ils doivent obligatoirement avoir le même `hashCode()`. Sinon, les collections basées sur le hachage ne fonctionneront pas correctement.
Question 25 #
Peut-on utiliser les méthodes equals()
et hashCode()
avec des types primitifs comme int
ou double
? Expliquez pourquoi.
Réponse
Non, les types primitifs (`int`, `double`, etc.) ne sont pas des objets et ne possèdent donc pas de méthodes comme `equals()` ou `hashCode()`. Ces méthodes existent uniquement pour les objets. Pour comparer des valeurs primitives, on utilise directement les opérateurs (`==`, `!=`, etc.). Si on veut utiliser des méthodes comme `equals()` ou `hashCode()` avec des valeurs numériques, il faut utiliser les classes enveloppes (`Integer`, `Double`, etc.).
Question 26 #
Qu’est-ce qu’un record en Java ? Expliquez ses avantages et donnez un exemple d’utilisation.
Réponse
Un record est un type spécial de classe introduit en Java 16 pour représenter des données immuables de façon concise. Un record déclare automatiquement les champs, le constructeur, les méthodes `equals()`, `hashCode()` et `toString()`, ce qui simplifie la création de classes « porteuses de données » (data classes). Les records sont particulièrement utiles pour les objets dont l’identité est définie uniquement par leurs valeurs, comme les points, coordonnées, ou résultats de calculs.
Exemple :
record Point(int x, int y) {}
Point p1 = new Point(1, 2);
Point p2 = new Point(1, 2);
System.out.println(p1.equals(p2)); // true
System.out.println(p1); // Point[x=1, y=2]
Ici, il n’est pas nécessaire d’écrire le constructeur ou de redéfinir `equals()` et `hashCode()` : tout est généré automatiquement par le compilateur.
Question 27 #
Comment peut-on transformer un tableau d’entiers en une liste d’entiers en utilisant les streams ?
Réponse
On peut utiliser Arrays.stream()
et boxed()
pour convertir un tableau primitif en stream d’objets, puis collecter dans une liste :
int[] tab = {1, 2, 3};
List<Integer> l = Arrays.stream(tab).boxed().collect(Collectors.toList());
Question 28 #
Comment peut-on vérifier en Java qu’un objet est une instance d’une classe donnée ? Ou qu’un objet satisfait à une interface donnée ?
Réponse
En Java, on peut utiliser l’opérateur instanceof
pour vérifier si un objet est une instance d’une classe ou d’une interface (directement ou par héritage). On peut aussi utiliser la méthode isAssignableFrom()
de la classe Class
pour vérifier les relations d’héritage ou d’implémentation entre deux classes ou interfaces.
Exemple avec instanceof
:
if (objet instanceof MonInterface) {
// objet implémente MonInterface
}
if (objet instanceof MaClasseParente) {
// objet hérite de MaClasseParente
}
Question 29 #
Si j’ai une instance d’une classe, comment puis-je savoir si elle n’est pas en fait d’une classe dérivée (c’est-à-dire d’une sous-classe) ?
Réponse
Pour savoir si une instance est exactement d’une classe donnée (et non d’une sous-classe), on peut comparer sa classe réelle avec la classe attendue à l’aide de getClass()
:
if (objet.getClass() == MaClasse.class) {
// objet est exactement de type MaClasse
} else {
// objet est d’une sous-classe de MaClasse (ou d’un autre type)
}
Question 30 #
Expliquez à quoi sert l’annotation @Override en Java et dans quels cas son utilisation est recommandée.
Réponse
L’annotation @Override indique qu’une méthode dans une sous-classe redéfinit une méthode de sa superclasse ou implémente une méthode d’une interface. Elle aide le compilateur à vérifier que la méthode respecte la signature de la méthode originale, évitant ainsi des erreurs comme une surcharge involontaire. Son utilisation est recommandée pour améliorer la lisibilité du code et prévenir des erreurs lors de la maintenance, surtout dans les projets complexes.
Question 31 #
Quelles sont les principales caractéristiques de l’interface Iterable en Java ? Donnez un exemple simple d’utilisation.
Réponse
L’interface Iterable permet à une classe d’être parcourue à l’aide d’une boucle for-each. Elle définit une méthode iterator() qui retourne un Iterator. Cette interface est essentielle pour les collections comme List ou Set. Exemple :
import java.util.*;
class Exemple {
public static void main(String[] args) {
List<String> liste = Arrays.asList("un", "deux", "trois");
for (String s : liste) {
System.out.println(s);
}
}
}
Question 32 #
Expliquez le rôle de l’interface Cloneable en Java et les précautions à prendre lors de son utilisation.
Réponse
L’interface Cloneable marque une classe comme pouvant être clonée via la méthode clone() de la classe Object. Sans implémenter cette interface, appeler clone() lève une CloneNotSupportedException. Une précaution importante est de redéfinir clone() pour effectuer une copie profonde (deep copy) si la classe contient des références à des objets modifiables, afin d’éviter des modifications non désirées de l’original. Exemple :
class Exemple implements Cloneable {
int valeur;
Exemple(int valeur) { this.valeur = valeur; }
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
Question 33 #
Comment les méthodes clone(), equals() et l’opérateur == sont-ils liés en termes de comparaison et de copie d’objets en Java ?
Réponse
En Java, clone(), equals() et l’opérateur == servent des objectifs distincts mais sont liés par leur rapport à la gestion des objets. L’opérateur == compare les références des objets, vérifiant s’ils pointent vers la même instance en mémoire. La méthode equals(), définie dans Object, compare par défaut les références (comme ==), mais peut être redéfinie pour comparer le contenu des objets (par exemple, les valeurs de leurs champs). La méthode clone() crée une copie d’un objet, mais le type de copie (superficielle ou profonde) influence la relation entre l’original et la copie. Par exemple, une copie superficielle partage les références des objets imbriqués, ce qui peut affecter les comparaisons avec equals() si ces objets sont modifiés. Une bonne pratique est de s’assurer que equals() reflète les mêmes critères d’égalité pour l’original et sa copie, surtout pour une copie profonde. Exemple :class Personne implements Cloneable {
String nom;
Personne(String nom) { this.nom = nom; }
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Personne)) return false;
Personne autre = (Personne) obj;
return nom.equals(autre.nom);
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public static void main(String[] args) throws CloneNotSupportedException {
Personne p1 = new Personne("Alice");
Personne p2 = (Personne) p1.clone();
System.out.println(p1 == p2); // false (différentes références)
System.out.println(p1.equals(p2)); // true (même contenu)
}
}
Question 34 #
Quelles précautions faut-il prendre lors de la redéfinition de equals() pour une classe implémentant Cloneable ?
Réponse
Lorsqu’une classe implémente Cloneable et redéfinit equals(), il est crucial de s’assurer que la méthode equals() est cohérente avec le comportement de clone(). Cela signifie que deux objets, l’original et sa copie, doivent généralement retourner true pour equals() si la copie est une duplication fidèle (surtout pour une copie profonde). Il faut vérifier que tous les champs pertinents pour equals() sont correctement copiés dans clone(), en particulier pour les objets imbriqués. Si clone() effectue une copie superficielle, les références partagées peuvent entraîner des incohérences si les objets imbriqués sont modifiés. De plus, equals() doit respecter les propriétés du contrat d’égalité (réflexivité, symétrie, transitivité, cohérence et non-nullité). Exemple :class Livre implements Cloneable {
String titre;
Auteur auteur;
Livre(String titre, Auteur auteur) {
this.titre = titre;
this.auteur = auteur;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Livre)) return false;
Livre autre = (Livre) obj;
return titre.equals(autre.titre) && auteur.equals(autre.auteur);
}
@Override
public Object clone() throws CloneNotSupportedException {
Livre copie = (Livre) super.clone();
copie.auteur = (Auteur) auteur.clone(); // Copie profonde
return copie;
}
}
class Auteur implements Cloneable {
String nom;
Auteur(String nom) { this.nom = nom; }
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof Auteur)) return false;
return nom.equals(((Auteur) obj).nom);
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}