On définit la classe A1
par :
package fr.univ_paris1.mass.poo.cc4.exo1;
public class A1 {
private double x;
private double y;
public A1(double x, double y) {
this.x = x;
this.y = y;
}
public A1 f(A1 that) {
return new A1(x + that.y, y + that.x);
}
public A1 g() {
return new A1(x + y, x - y);
}
public A1 h(A1 u, A1 v) {
return new A1(x + u.x, y - v.y);
}
@Override
public String toString() {
return (x + y) + " " + (x - y);
}
}
Question : quel est alors l'affichage produit par le code suivant ?
package fr.univ_paris1.mass.poo.cc4.exo1;
public class TestA1 {
public static void main(String[] args) {
A1 x = new A1(1,2);
System.out.println(x);
System.out.println(x.g());
System.out.println(x.g().g());
A1 y = new A1(3,-1);
System.out.println(y);
System.out.println(x.f(y));
A1 z = new A1(0,0);
System.out.println(z.h(new A1(1,2), new A1(3,4)));
System.out.println(z);
}
}
L'affichage obtenu est :
3.0 -1.0 2.0 4.0 6.0 -2.0 2.0 4.0 5.0 -5.0 -3.0 5.0 0.0 0.0
Il faut être ici bien attentif au sens d'une expression comme
x.g().g()
. On doit l'interpréter comme (x.g()).g()
:
l'ordinateur commence par réaliser l'appel x.g()
. D'après la
définition de la classe, cet appel renvoie un nouvel objet de type
A1
. C'est ce nouvel objet qui appelle de son côté de nouveau la
méthode g
. Ici, concrètement, tout se passe comme si on avait
remplacé la ligne System.out.println(x.g().g());
par les lignes
suivantes :
A1 u = x.g();
A1 v = u.g();
System.out.println(v);
En général, il est très bénéfique de raisonner comme on le fait au-dessus, en décomposant les appels de méthodes en des étapes intermédiaires plus simples.
Il faut aussi être très attentif à des lignes comme
System.out.println(z.h(new A1(1,2), new A1(3,4)));
. On pourrait
en effet raisonner à l'envers et penser que c'est l'objet z
qui
est affiché après l'exécution de l'appel de méthode. Or, ce n'est jamais le
cas : un appel de méthode est remplacé par son résultat. Ici, la méthode
h
renvoie un nouvel objet, donc comme dans l'exemple précédent,
on peut réécrire la ligne en plusieurs étapes :
A1 w = new A1(1,2);
A1 q = new A1(3,4);
A1 p = z.h(w,q);
System.out.println(p);
Cette décomposition en étapes simplifie grandement l'analyse du programme.
On définit la classe B1
par :
package fr.univ_paris1.mass.poo.cc4.exo2;
public class B1 {
private int boo;
private char X;
public B1(int boo, char X) {
this.boo = boo;
this.X = X;
}
@Override
public String toString() {
if (boo > 0) {
return X + " :-) " + boo;
} else {
return X + " :-( " + (-boo);
}
}
public B1 flip() {
return new B1(-boo, X);
}
public char poke(char Y) {
char Z = X;
X = Y;
return Z;
}
public int zap(int gla) {
boo = boo + gla;
return boo / 2; // quotient de la division entière
}
}
Question : quel est alors l'affichage produit par le code suivant ?
package fr.univ_paris1.mass.poo.cc4.exo2;
public class TestB1 {
public static void main(String[] args) {
B1 john = new B1(2, 'J');
System.out.println(john);
B1 bob = john.flip();
System.out.println(john + " <-> " + bob);
System.out.println(john.zap(1) + " <-> " + bob.zap(-2));
System.out.println(john + " <-> " + bob);
B1 robert = new B1(5, 'R');
B1 alfred = robert;
System.out.println(alfred.poke('A'));
System.out.println(robert + " <-> " + alfred);
B1 angie = new B1(-2,'A');
System.out.println(angie.flip().zap(2));
System.out.println(angie);
}
}
L'affichage obtenu est :
J :-) 2 J :-) 2 <-> J :-( 2 1 <-> -2 J :-) 3 <-> J :-( 4 R A :-) 5 <-> A :-) 5 2 A :-( 2
On retrouve ici le même type de problèmes que dans l'exercice précédent. Par
exemple, la ligne System.out.println(john.zap(1) + " <-> " +
bob.zap(-2));
ne conduit bien entendu pas au même affichage que la
ligne qui la précède (System.out.println(john + " <-> " +
bob);
). En effet, dans cette second ligne, on se contente d'afficher
les deux objets, en utilisant leur méthode toString
. En revanche,
la première ligne contient des appels à la méthode zap
. Comme
dans les exemples précédents, on obtient le résultat de l'affichage en
écrivant un code équivalent :
int u = john.zap(1);
int v = bob.zap(-2);
System.out.println(u + " <-> " + v);
Il est ainsi clair que l'affichage est celui des résultats entiers des deux appels, et non pas un nouvel affichage des objets.
Soit la classe C1
définie de façon incomplète par :
public class C1 {
private int x;
public C1(int x) {
this.x = x;
}
public void f() {
à compléter (A)
}
à compléter (B)
}
Question : Compléter la classe C1
afin que le
programme (méthode main ci-dessous) affiche le texte suivant
[1] [2] [3] [4]
public class TestC1 {
public static void main(String[] args) {
C1 foo = new C1(1);
System.out.println(foo);
for(int i=0;i<3;i++) {
foo.f();
System.out.println(foo);
}
}
}
Une solution possible est la suivante :
public class C1 {
private int x;
public C1(int x) {
this.x = x;
}
@Override
public String toString() {
return "[" + x + "]";
}
public void f() {
x = x + 1;
}
}
La solution doit nécessairement passer par la redéfinition de la méthode
toString
, sinon le programme proposé devrait obligatoirement
produire des affichages de la forme C1@...
. En outre, comme
l'affichage change à chaque tour de la boucle, il semble naturel de s'appuyer
sur la variable x
de l'objet qu'on modifie sans difficulté dans
f
.
Soit les classes D1
et E1
définies par :
public class D1 {
public int f() {
return 2;
}
public int g() {
return 3;
}
}
public class E1 extends D1 {
@Override
public int f() {
return 4;
}
}
Question : quel est l'affichage produit par le programme ci-dessous.
public class TestED1 {
public static void main(String[] args) {
E1 x = new E1();
System.out.println(x.f());
D1 y = new D1();
System.out.println(y.f());
y = x;
System.out.println(y.g());
Object o = x;
System.out.println(o);
}
}
Il s'agit d'un cas classique d'héritage dans lequel la classe
E1
hérite de la classe D1
. En particulier, la classe
E1
hérite de la méthode g
mais redéfinit la méthode
f
. De ce fait, le premier appel x.f()
renvoie 4.
L'objet désigné par y
étant au départ de type
D2
, l'appel y.f()
donne bien sûr comme
résultat 2.
L'appel suivant y.g()
donne nécessairement 3 puisque
la méthode g
utilisée est toujours celle de D1
(elle
n'est pas redéfinie dans E1
).
Enfin, l'affichage de o
conduit à l'appel de la méthode
toString
. Comme celle-ci n'est pas redéfinie, on obtient
l'affichage du type de o
(suivi des éléments habituels). Or, ici,
o
désigne un objet de type E1
. L'affichage sera donc
de la forme E1@...
. On obtient finalement :
4 2 3 E1@3f68336
Un objet de la classe Intervalle
représente un intervalle
fermé. L'objectif de l'exercice est de compléter la classe donnée ci-dessous
de la façon suivante :
toString
représente l'intervalle sous la forme
classique [a,b] ;contient
renvoie true
si et seulement si
x
est contenu dans l'intervalle appelant ;longueur
de l'intervalle [a,b] est b-a ;fixeMinimum
renvoie un nouvel intervalle avec la
même borne supérieure que l'intervalle appelant et la borne inférieure
passée en paramètre ;étend
construit le plus petit intervalle contenant à
la fois l'intervalle appelant et le paramètre x
.public class Intervalle {
private double inf;
private double sup;
public Intervalle(double inf, double sup) {
this.inf = inf;
this.sup = sup;
}
@Override
public String toString() {
// à compléter
}
public boolean contient(double x) {
// à compléter
}
public double longueur() {
// à compléter
}
public Intervalle fixeMinimum(double inf) {
// à compléter
}
public Intervalle etend(double x) {
// à compléter
}
}
Une solution possible est :
public class Intervalle {
private double min;
private double max;
public Intervalle(double min, double max) {
this.min = min;
this.max = max;
}
@Override
public String toString() {
return "[" + min + ", " + max + "]";
}
public boolean contient(double x) {
return x >= min && x <= max;
}
public double longueur() {
return max - min;
}
public Intervalle fixeMaximum(double max) {
return new Intervalle(min,max);
}
public Intervalle fixeMinimum(double min) {
return new Intervalle(min,max);
}
public Intervalle etend(double x) {
if(x<min) {
return new Intervalle(x,max);
} else if (x>max) {
return new Intervalle(min,x);
} else {
return this;
}
}
}