Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

Pourquoi une erreur de cast n'est pas détectée dès la compilation ?

9 réponses
Avatar
Francois
Bonjour à tous,

Je précise que je suis débutant en Java.
Pouvez vous m'expliquer pourquoi ce code se compile correctement. En
fait, je ne comprends pas pourquoi l'erreur manifeste de ce code
(provenant de l'opération de cast illégale il me semble) n'est pas
détectée dès la compilation, mais seulement au moment de l'exécution ?

Merci d'avance.

//-------------------------------------
class Animal
{
public void faireDuBruit()
{
System.out.println("???????") ;
}
}


class Chien extends Animal
{
public void faireDuBruit()
{
System.out.println("Woua Woua !") ;
}
}

class Test
{
public static void main( String [] args)
{
Animal a = new Animal() ;
Chien c = (Chien) a ; // On "cast" la référence a
c.faireDuBruit() ; // Boum !!
}
}
//-------------------------------------


--
François

9 réponses

Avatar
Mayeul
Francois a écrit :
Bonjour à tous,

Je précise que je suis débutant en Java.
Pouvez vous m'expliquer pourquoi ce code se compile correctement. En
fait, je ne comprends pas pourquoi l'erreur manifeste de ce code
(provenant de l'opération de cast illégale il me semble) n'est pas
détectée dès la compilation, mais seulement au moment de l'exécution ?

Merci d'avance.




Bonjour,

Disons que le compilateur Java n'est pas assez "malin" pour détecter les
erreurs de cast évidentes. Ça ne me semble pas bien grave, car dans la
plupart des cas les erreurs de cast ne sont pas évidentes, ni même
détectables avant l'exécution.

--
Mayeul
Avatar
Francois
Oops, désolé pour la fausse manip, je voulais faire un message dans le
forum, non un message personnel. Je recommence.

Mayeul a écrit :

Disons que le compilateur Java n'est pas assez "malin" pour détecter les
erreurs de cast évidentes. Ça ne me semble pas bien grave, car dans la
plupart des cas les erreurs de cast ne sont pas évidentes, ni même
détectables avant l'exécution.



Merci beaucoup pour la réponse.

Il semble bien que, pour les cast, Java ne regarde absolument pas les
types des objets mais seulement ceux des variables.


--
François
Avatar
Sylvain SF
Francois a écrit :

Disons que le compilateur Java n'est pas assez "malin" pour détecter
les erreurs de cast évidentes. Ça ne me semble pas bien grave, car
dans la plupart des cas les erreurs de cast ne sont pas évidentes,
ni même détectables avant l'exécution.





il ne s'agit pas d'ingéniosité mais de déterminisme.
le compilo ne peut pas savoir si le cast est valide ou non.

Il semble bien que, pour les cast, Java ne regarde absolument pas
les types des objets mais seulement ceux des variables.



on va supposer que "variables" signifie "variables de type primitif"
comme un byte, un int, etc... et "objets" signifie "instances de
classe".

si tu parles de cela, le compilo verifie les 2 "familles" et applique
des casts implicites si et seulement si il sont valides (ie conversion
non destructrice de type primitif ou upcast d'une référence).

dans ton code:

class A {}
class B extends A {}

le compilo vérifie et valide:

B b = new B();
A a = b;

comme il vérifie et refuse:

A a = new A();
B b = a;

le cast que tu fasses un cast explicite invalide accepté par le
compilo et générant une CastClassException au runtime (ce n'est
pas "c.faireDuBruit()" qui bombe dans ton code mais bien le cast)
n'est pas un défaut du compilo, ce n'est que un bug (volontaire)
de codage.

le compilo doit en effet évidemment accepter les downcast (cast
d'une classe de base vers une classe dérivée) pour accepter les
cas où une instance dérivée n'est pas fournie par un conteneur
de cette classe mais par celui de la classe de base, ex:

Animal getPet(){
return new Chien();
}

Chien chien = (Chien) getPet();

ceci est valide (même si tordu car inventé pour l'exemple).
plus génériquement on ferait plutôt:

Animal getPet(){
return new Chien();
}

Animal animal = getPet();
if (animal instanceof Chien){
Chien c = (Chien) animal;
c.faitQlqChose();
}

Sylvain.
Avatar
Francois
Sylvain SF a écrit :

il ne s'agit pas d'ingéniosité mais de déterminisme.
le compilo ne peut pas savoir si le cast est valide ou non.



C'est justement ce que je ne comprends pas trop. J'ai l'impression le
compilateur pourrait, me semble-t-il, savoir si le cast est valide ou
non. Mais je vais lire la suite...

Il semble bien que, pour les cast, Java ne regarde absolument pas
les types des objets mais seulement ceux des variables.



on va supposer que "variables" signifie "variables de type primitif"
comme un byte, un int, etc... et "objets" signifie "instances de
classe".



Pour moi, "variables" signifie "variables de type primitif" c'est-à-dire
celles contenant un byte, un int etc... et celles contenant une
référence à une classe donnée d'objet.
Et effectivement, pour moi, "objets" signifie "instances de classe".


si tu parles de cela, le compilo verifie les 2 "familles" et applique
des casts implicites si et seulement si il sont valides (ie conversion
non destructrice de type primitif ou upcast d'une référence).

dans ton code:

class A {}
class B extends A {}

le compilo vérifie et valide:

B b = new B();
A a = b;

comme il vérifie et refuse:

A a = new A();
B b = a;



Oui et pour moi, c'est sur les variables a et b que le compilateur fait
les vérifications et pas sur les objets créés sur le tas. Le compilateur
dit : "hors de question de mettre le contenu de la variable a de type A
dans la variable b de type B". Peu importe le type de l'objet référencé
par a, ça pourrait être un objet de type B que le compilateur n'en
voudrait pas non plus.

le cast que tu fasses un cast explicite invalide accepté par le
compilo et générant une CastClassException au runtime (ce n'est
pas "c.faireDuBruit()" qui bombe dans ton code mais bien le cast)



Oui c'est exact, je m'étais trompé.

n'est pas un défaut du compilo, ce n'est que un bug (volontaire)
de codage.

le compilo doit en effet évidemment accepter les downcast (cast
d'une classe de base vers une classe dérivée) pour accepter les
cas où une instance dérivée n'est pas fournie par un conteneur
de cette classe mais par celui de la classe de base, ex:



Ok, mais le compilateur ne peut pas avant vérifier si le cast est
compatible avec le type de l'objet référencé ?

Animal getPet(){
return new Chien();
}

Chien chien = (Chien) getPet();

ceci est valide (même si tordu car inventé pour l'exemple).
plus génériquement on ferait plutôt:

Animal getPet(){
return new Chien();
}

Animal animal = getPet();
if (animal instanceof Chien){
Chien c = (Chien) animal;
c.faitQlqChose();
}



C'est là où je butte un peu (désolé). Je pensais que le test "if (animal
instanceof Chien){...}" pouvait être fait par le compilateur justement ?

Merci beaucoup pour la réponse. :-)


--
François
Avatar
Francois
Francois a écrit :

Pour moi, "variables" signifie "variables de type primitif" c'est-à-dire
celles contenant un byte, un int etc... et celles contenant une
référence à une classe donnée d'objet.



Je rectifie "celles contenant une référence à un objet d'une classe
donnée". Exemple : avec "Chien var ;", var est une variable de type
référence à un objet de type-classe Chien.


--
François
Avatar
Sylvain SF
Francois a écrit :

dans:

class A {}
class B extends A {}

le compilo vérifie et valide:

B b = new B();
A a = b;

comme il vérifie et refuse:

A a = new A();
B b = a;



Oui et pour moi, c'est sur les variables a et b que le compilateur fait
les vérifications et pas sur les objets créés sur le tas. Le compilateur
dit : "hors de question de mettre le contenu de la variable a de type A
dans la variable b de type B". Peu importe le type de l'objet référencé
par a, ça pourrait être un objet de type B que le compilateur n'en
voudrait pas non plus.



les "objets du tas" ??? c'est quoi ça, oublie la notion de pile et tas,
non applicable à Java.
les contrôles de types sont les mêmes quel que soit l'endroit et le
moment où sont instanciées les objets et ils sont tous dynamiques.

le compilo refuse de mettre un a (qu'il sait être un A à ce moment)
dans une var. b de type B car un tel cast est invalide; tes classes
ne sont peut être pas assez "remplies" pour que cela soit évident
mais imagine:

class Animal {
public void faitDuBruit() {}
}

class Chien extends Animal {
public void faitDuBruit() {}
public void aboie() {}
public void mord() {}
}

dans ce cas le compilo "voit" bien que mettre un a dans un B,
ce qui te permettrait d'invoquer A.aboie() ou A.mord() est
invalide puisque ces méthodes n'existent pas.

Ok, mais le compilateur ne peut pas avant vérifier si le cast est
compatible avec le type de l'objet référencé ?



non, il ne le peut pas avec les moyens d'un compilo, il le pourrait
seulement s'il faisait une analyse de code et/ou une évaluation
dynamique mais ce serait alors un analyseur de code ou un interpréteur
de code, plus un compilo.

Animal animal = getPet();
if (animal instanceof Chien){
Chien c = (Chien) animal;
c.faitQlqChose();
}



C'est là où je butte un peu (désolé). Je pensais que le test "if (animal
instanceof Chien){...}" pouvait être fait par le compilateur justement ?



pour cela il aurait besoin de compiler entièrement "animal = getPet()"
et de pouvoir l'exécuter pour évaluer la référence retournée avant de
compiler la ligne suivante; c'est dans 99% des cas impossible.

imagine:

Animal getPet(int type){
switch (type){
case kDOG:
return new Chien();
....
}
}

sauf (à nouveau) à analyser le code et/ou l'éxecuter, le compilo
ne peux rien savoir de plus que le fait qu'un Animal sera retourné,
tout downcast est sous la seule responsabilité du codeur.

Sylvain.
Avatar
Francois
Sylvain SF a écrit :

les "objets du tas" ??? c'est quoi ça, oublie la notion de pile et tas,
non applicable à Java.



Ah... Une fois de plus j'ai dû lire de mauvaises références. J'ai lu
quelque part que les variables du type "référence à un objet" ne
contenaient pas l'objet lui-même, mais justement une référence sur
l'objet en question (une sorte de "pointeur" [avec des méga guillemets]
vers l'objet) et que l'objet lui (son code en binaire) se trouvait dans
une zone de la mémoire appelée le tas. Je n'en sais pas plus et c'est
peut-être des bêtises alors.

le compilo refuse de mettre un a (qu'il sait être un A à ce moment)
dans une var. b de type B car un tel cast est invalide; tes classes
ne sont peut être pas assez "remplies" pour que cela soit évident
mais imagine:

class Animal {
public void faitDuBruit() {}
}

class Chien extends Animal {
public void faitDuBruit() {}
public void aboie() {}
public void mord() {}
}

dans ce cas le compilo "voit" bien que mettre un a dans un B,
ce qui te permettrait d'invoquer A.aboie() ou A.mord() est
invalide puisque ces méthodes n'existent pas.



Oui c'est très clair.


Ok, mais le compilateur ne peut pas avant vérifier si le cast est
compatible avec le type de l'objet référencé ?



non, il ne le peut pas avec les moyens d'un compilo, il le pourrait
seulement s'il faisait une analyse de code et/ou une évaluation
dynamique mais ce serait alors un analyseur de code ou un interpréteur
de code, plus un compilo.



Ah Ok, je crois que je commence à comprendre (enfin il est tard, alors
faut faire gaffe :-) )

pour cela il aurait besoin de compiler entièrement "animal = getPet()"
et de pouvoir l'exécuter pour évaluer la référence retournée avant de
compiler la ligne suivante; c'est dans 99% des cas impossible.



Ok. Le compilo ne peut pas faire cela car le compilo compile seulement
par définition et c'est tout. Il y a des cas où il faudrait exécuter.
D'ailleurs ci-dessous :

imagine:

Animal getPet(int type){
switch (type){
case kDOG:
return new Chien();
....
}
}

sauf (à nouveau) à analyser le code et/ou l'éxecuter, le compilo
ne peux rien savoir de plus que le fait qu'un Animal sera retourné,



Génial, tu viens de porter l'estocade ! Évidemment, mon exemple initial
était très (trop) basique, si bien que je me disais "mais qu'il est bête
ce compilo". Avec ton exemple, on voit bien qu'il ne peut rien savoir à
l'avance le pauvre. Après, je pourrais toujours dire "un compilo devrait
voir certains cas basiques (comme mon exemple initial) et laisser de
côtés les cas pas basiques (comme ton exemple)", mais de toute façon, on
a ceci ...

tout downcast est sous la seule responsabilité du codeur.



... exactement ! Ça doit être une des fonctions du cast : dire au
compilateur "là cher compilateur, tu me laisses gérer moi le codeur, je
prends la main !". Mais ce n'est pas sans danger...

Merci beaucoup pour ton aide. :-)


--
François
Avatar
Sylvain SF
Francois a écrit :

Ah... Une fois de plus j'ai dû lire de mauvaises références. J'ai lu
quelque part que les variables du type "référence à un objet" ne
contenaient pas l'objet lui-même, mais justement une référence sur
l'objet en question (une sorte de "pointeur" [avec des méga guillemets]
vers l'objet)



c'est correct, y compris pour les guillemets (une réf. Java peut être
vue comme réf '&' du C++)

que l'objet lui (son code en binaire) se trouvait dans une zone de la
mémoire appelée le tas. Je n'en sais pas plus et c'est peut-être des
bêtises alors.



le code - le byte-code - des classes est où le class loader a voulu
le mettre (cela peut varier selon les VM); schématiquement toutes
les réf. (et donc toutes les variables d'instance de ces réf. sont
dans un tas), la pile ne sert qu'à empiler les réf. et les valeurs
primitives lors des appels de méthodes.

mais encore une fois, contrairement à C ou C++, il ne faut pas voir
la pile comme le lieu où serait stockées les variables immédiates
d'une méthode et le tas comme le lieu où serait stocké les instances
allouées dynamiquement, car a) toutes les instances de classes sont
dynamiques, b) ce lieu n'a strictement aucune incidence; la lecture
des specs de la VM telle que définie par Sun peu être intéressante
mais n'apprend rien sur le sujet initial.

tout downcast est sous la seule responsabilité du codeur.



... exactement ! Ça doit être une des fonctions du cast : dire au
compilateur "là cher compilateur, tu me laisses gérer moi le codeur,
je prends la main !". Mais ce n'est pas sans danger...



pile poil.

Merci beaucoup pour ton aide. :-)



pas de quoi.

Sylvain.
Avatar
TestMan
Francois a écrit :
Bonjour à tous,

Je précise que je suis débutant en Java.
Pouvez vous m'expliquer pourquoi ce code se compile correctement. En
fait, je ne comprends pas pourquoi l'erreur manifeste de ce code
(provenant de l'opération de cast illégale il me semble) n'est pas
détectée dès la compilation, mais seulement au moment de l'exécution ?

Merci d'avance.

//-------------------------------------
class Animal
{
public void faireDuBruit()
{
System.out.println("???????") ;
}
}


class Chien extends Animal
{
public void faireDuBruit()
{
System.out.println("Woua Woua !") ;
}
}

class Test
{
public static void main( String [] args)
{
Animal a = new Animal() ;
Chien c = (Chien) a ; // On "cast" la référence a
c.faireDuBruit() ; // Boum !!
}
}
//-------------------------------------




Bonjour,

Tu lui dis : mon animal est un chien ... le compilo te fait simplement
confiance.

Le transtypage en Java n'est vérifié qu'à l'exécution entre temps, il te
fait "confiance". On peut assimiler ceci à un contrat que tu passes avec
lui du style "si si tu vas voir ça vas marcher". Si le contrat est faux,
à l'exécution il te jettes.

Rien ne t'empècherais entre le moment de la compil du Test et son
exécution de rendre cette assertion valide (en déplaçant extends sur
Animal par exemple et recompilant)

A+
TM