Java pour les nuls (moi, donc)

Non non, je ne fais pas une crise d’infériorité, c’est juste que là… je comprend pas. J’aime bien donner des cours, ça permet de se poser des questions que jamais on ne se poserait en prod, et de tomber sur des trucs tout bêtes, limite de débutants, mais qui bloquent bien ! J’ai deux cas a soumettre à votre sagacité, des trucs tous cons, et j’avoue que.. je n’arrive pas à les expliquer… Si vous avez des idées, surtout n’hésitez pas !

1) Le mystère des comparaisons de String

La classe la plus banale de Java : String. Dans la Javadoc, ils précisent bien que

s= »coucou »;

et

s=new String(« coucou »);

sont équivalents.

Eh ben…regardez :

String s1= »bob »;
String s2= »bob »;
String s3=new String(« bob »);
if(s1==s2)
System.out.println(« ca marche sans le new »);
if(s1==s3)
System.out.println(« ca marche avec le new »);

Le premier test fonctionne, et pas le second ! Je sais qu’il ne faut pas faire l’erreur d’utiliser ==, mais bien .equals, mais là il s’agit de comparer les pointeurs.. Je m’étonne en fait que le premier test fonctionne.

2) L’héritage perdu

Soit une classe mère :

public class Mere {
public Mere()
{
System.out.println(« mere sans param »);
}

public Mere(String s)
{
System.out.println(« mere « +s);
}
}

et une classe Fille…vide :

public class Fille extends Mere
{
}

Testez :

Mere m=new Mere();
Mere m2=new Mere(« bla »);
Fille f=new Fille();
//Fille f2=new Fille(« bli »);

La dernière ligne est en commentaire, et pour cause : elle provoque une erreur du compilateur ! « Constructeur inexistant ». En revanche, la ligne « Fille f=new Fille() » appelle bien le constructeur de la classe mère, et pas un constructeur implicite. Pourquoi hériter d’un constructeur et pas de l’autre ?

Alors, vos avis ?

Update : j’ai compris pour le cas 2 ! Merci à Jib pour m’avoir aiguillé sur la voie de la rédemption !

Alors, je vous explique : ce qui me paraissait super bizarre, c’est que Fille sache bien exploiter le constructeur sans paramètre de la classe mère, mais pas l’autre ! C’était quoi, de la ségrégation ?

Je ne voyais pas en quoi le principe du « constructeur implicite » jouait , puisqu’on héritait bien de quelque chose, il n’y avait rien d’implicite. Jib a toutefois bien fait d’insister. Suivez le raisonnement exact de Java :

  1. On n’hérite PAS des constructeurs lors d’un mécanisme d’héritage. Je l’avoue, je l’avais complètement oublié. Mais ce qui m’a aidé dans cet oubli, c’est le fait que la classe Fille « hérite » quand même d’un des constructeurs de la classe mère, du moins en apparence. Mais…
  2. …vu que la classe fille n’hérite d’aucun constructeur, et qu’elle n’en possède pas, la règle rappelée par Jib s’applique : un constructeur implicite est mis en place.
  3. Deuxième effet kiss-cool : le constructeur implicite, contrairement à ce que je croyais, ne reste pas sans rien faire. En fait, il a un rôle bien particulier : il appelle, implicitement, le constructeur sans paramètre de la classe mère.
  4. Vu que la classe mère a quant à elle un constructeur sans paramètre bien réel, ce dernier est appelé. CQFD.

Voilà pourquoi ma classe Fille se retrouve « héritant » du constructeur sans paramètre de ma classe Mere, et pas de l’autre. Tout simplement parce qu’il n’y a pas d’héritage direct des constructeurs, mais une construction implicite qui appelle implicitement un constructeur parent. Pffiou !

Update 2 : Vu que, quand yen a marre, ya Malabar, j’ai employé les grands moyens pour comprendre cette histoire d’affectation de String, et ça a été l’occasion pour moi de faire un truc que je voulais faire depuis longtemps : décompiler du Bytecode !

On utilise pour cela l’instruction javap, qui prend en entrée un fichier .class, et qui vous le « désassemble » pour obtenir un byte code plus ou moins lisible.

Tentons de désassembler le code suivant :

String s1= »bob »;
String s2= »bob »;
String s3=new String(« bob »);

Cela donne le code suivant (j’ai fait trois paragraphes correspondant aux 3 instructions) :

0: ldc #16; //String bob
2: astore_1

3: ldc #16; //String bob
5: astore_2

6: new #18; //class java/lang/String
9: dup
10: ldc #16; //String bob
12: invokespecial #20; //Method java/lang/String. »<init> »
15: astore_3

On a donc la réponse : les deux chaînes s1 et s2 pointent bel et bien sur le même espace mémoire (#16) tout simplement parce que le compilateur Java a su détecter que la chaîne utilisée était bien la même dans les deux cas. Pour preuve, en ligne 10, c’est à nouveau cette adresse qui est chargée dans le registre pour pouvoir appeler le constructeur de String (j’ai essayé de faire 3 tonnes d’affectations entre la déclaration de s1 et s2, ça marche pareil, il retombe sur ses pattes).

Autre enseignement de ces quelques lignes : dans les affectations de type s= »toto », il n’y a pas de construction explicite de l’objet String qui est effectuée (pas d’appel à « new »), mais simplement le chargement d’une référence vers un espace mémoire représentant ce String, et qui va être partagé par tous ceux qui contiennent la même chaine.

Pour les développeurs débutants, c’est super piégeux, car le fait que s1==s2 donne le résultat attendu peut faire croire qu’on est dans le bon, alors que ça ne marchera plus dès lors qu’on manipulera des String générés par des éléments externes (une lecture dans une base, par exemple).

Pour les développeurs un peu plus aguerris, ça nous amène à la réflexion suivante : en fait, les objets String ne sont pas immuables par une volonté particulière, mais simplement par nécessité : il serait super risqué de donner la possibilité de modifier ces « espaces mémoires mis en commun ».

Reste une question : est ce que ces espaces communs sont des instances de String à part entière, ou bien des simulâcres de classes pour donner une cohérence à l’organisation « 100% classes » de String ? Je ne sais pas encore…

Update 3 : une doc intéressante fournie par un de mes stagiaires, merci Michel !

On y apprend deux choses :

  • String est vraiment une classe à part, elle est considérée comme étant un « literal », au même titre qu’un type scalaire (int…), son comportement est donc assez différent d’une classe classique que l’on instancie
  • L’article m’a aiguillé sur la méthode intern() de la classe String qui confirme ce que je pensais : Java gère en interne un « pool » de chaînes de caractères afin de repérer les chaînes similaires et éviter les doublons d’instance.

A noter qu’apparemment, C# fonctionne de la même manière.