2. La classe Object

Voyons tout d'abord les méthodes de cette classe.

Exemple 3. Les méthodes de la classe Object

public  class Object {  
     public Object() {...}  // contructeur  
      
     public String toString() {...}   
      
     protected  native Object clone()  throws CloneNotSupportedException {...}   
      
     public equals(java.lang.Object) {...}  
     public  native  int hashCode() {...}   
      
     protected  void finalize()  throws Throwable {...}   
      
     public  final  native Class getClass() {...}   
      
     // méthodes utilisées dans la gestion des threads  
     public  final  native  void notify() {...}   
     public  final  native  void notifyAll() {...}  
     
     public  final  void wait(long)  throws InterruptedException {...}   
     public  final  void wait(long,  int)  throws InterruptedException {...}   
}

Nous reviendrons en détails plus tard sur la signification des clauses throws … Exception. De même, les méthodes notify(), notifyAll(), wait(long) et wait(long, int) seront également revues en détails dans le chapitre sur les unités d’exécution (threads). Une des possibilités offertes par l'héritage, est que tout objet instance d'une classe qui hérite d'une autre classe, peut utiliser les méthodes de cette autre classe. Donc, tout objet Java peut utiliser, sous certaines conditions, l'ensemble des méthodes de la classe Object. Certaines de ces méthodes sont très spécialisées, mais d'autres sont utilisées très fréquemment. D'autres enfin, sont utilisées de manière spéciale par la machine Java, et ont donc un statut particulier.

2.1. La méthode toString()

La première de ces méthodes est la méthode toString(). Cette méthode est utilisée par la machine Java toutes les fois où elle a besoin de représenter un objet sous forme d'une chaîne de caractères. Par exemple, il est légal d'écrire la ligne suivante :
Marin marin =  new Marin ("Surcouf",  "Robert",  25000) ;  
System.out.println("Marin : " + marin) ;
La méthode println de l'objet System.out prend en paramètre un objet de type String. La machine Java a donc besoin de convertir ("Marin : " + marin) en objet de type String. Cela passe par la conversion de l'objet marin en objet String. Cette conversion s'effectue par appel à la méthode toString() de la classe Object. Il est donc presque équivalent d'écrire le code précédent et celui-ci :
System.out.println("Marin : " + marin.toString()) ;
En apparence, les deux codes sont équivalents. En fait, en toute rigueur, le second échouera avec une ignoble NPE si l'objet marin est nul, alors que le premier affichera null. Dans la pratique, on peut se demander ce que ce code va nous afficher. La réponse est immédiate si l'on tente de l'exécuter : une chaîne de caractères un peu cabalistique, qui va ressembler à Marin@b82e3f203. Les chaînes de caractères retournées par la méthode toString() de la classe Object ont toutes cette forme : le nom de la classe, suvie du caractère @, et une adresse en hexadécimal, qui est l'adresse mémoire où l'objet considéré est enregistré. Cela garantit l'unicité de ce code en fonction de l'objet, ce qui a son importance. En tout cas, il apparaît clairement que ce résultat c'est pas très sympathique, et en tout cas peu explicite pour un utilisateur humain normalement constitué. Heureusement pour nous, il est possible de changer ce comportement, et d'afficher une chaîne de caractères plus amicale. On utilise pour cela un mécanisme qui s'appelle la "surcharge". Sans entrer dans les détails de ce mécanisme, disons pour le moment qu'il est possible de réécrire cette méthode toString() dans la classe Marin. Il suffit pour cela d'ajouter les lignes de code suivantes à la classe que nous avons déjà écrite.

Exemple 4. Surcharge de toString() dans la classe Marin

public String toString() {  
    String resultat =  super.toString() ;  
    resultat +=  "\nNom : " + nom ;   
    resultat +=  "\nPrénom : " + prenom ;   
    resultat +=  "\nSalaire : " + salaire ;   
     return resultat ;  
}

Cette méthode commence par un appel à la méthode super.toString(). Cette syntaxe signifie qu'elle appelle une méthode toString() qui doit être définie parmi les super-classes de Marin. Sur notre exemple simple, cette classe n'en étend aucune autre par déclaration, elle étend donc Object. Il se trouve par chance que la classe Object possède une méthode toString(), c'est donc celle-là qui va être appelée tout d'abord. Si cela n'avait pas été le cas, on aurait eu une erreur de compilation. Les lignes suivantes ajoutent des éléments supplémentaires. Le résultat de l'appel à cette méthode toString() devra ressembler à ceci :
 Marin@b82e3f203
 Nom : Surcouf
 Prénom : Robert
 Salaire :  25000

2.2. La méthode clone()

La méthode clone() est une méthode déclarée native. Une méthode native est une méthode qui n'est pas écrite en Java, mais dans un autre langage, qui peut être le C, le C++, ou tout autre langage. Java utilise un mécanisme spécial pour éventuellement passer des paramètres aux méthodes natives, les invoquer, et récupérer ce qu'elles retournent. Une méthode native n'est en général pas portable d'une machine à l'autre, on perd donc un des intérêts majeurs de Java en écrivant des méthodes natives. Ici, cette méthode fait partie de l'API standard, qui de toute façon existe pour toutes les machines / OS existantes. Le rôle de la méthode clone() est de dupliquer un objet rapidement, en dupliquant la zone mémoire dans laquelle il se trouve à l'aide d'un processus rapide. Pour cloner un objet, il suffit donc d'appeler cette méthode, qui nous renverra une copie conforme de cet objet. Attention toutefois le clonage des objets est interdit par défaut. Afin de l'utiliser, il faut surcharger la méthode clone() de la classe Object, qui est protected, par une méthode public de la classe de l'objet que l'on veut cloner. En plus, il faut que la classe dont on veut cloner les instances, implémente l'interface Cloneable. Cette interface ne comporte pas de méthode, elle est juste là pour autoriser le clonage. Tenter de cloner un objet qui n'implémente pas cette interface génèrera une exception de type CloneNotSupportedException (moins ignoble que l'ignoble NPE, mais tout de même...). La surcharge de cette méthode n'a pour objet que d'exposer publiquement la méthode clone() de la classe Object. Rendre une classe Marin clonable, peut donc se faire de la façon suivante.

Exemple 5. Surcharge de clone() dans la classe Marin

public  class Marin 
 implements Cloneable {  // déclaration indispensable

    // ici on propage l'exception, on aurait pu aussi 
    // l'attraper localement
    public Object clone()  throws CloneNotSupportedException {
       return  super.clone() ;
   }
} 

Notons que l'on peut surcharger la méthode clone() par une méthode qui ne jette aucune exception, dans la mesure où la clause throws ne fait pas partie de la signature d'une méthode. Dans ce cas, l'exception que peut jeter l'appel à super.clone() doit être attrapée localement.

2.3. La méthode equals()

Cette méthode permet, comme son nom peut le laisser supposer, de comparer deux objets, et notamment de savoir s'ils sont égaux. Un peu plus haut nous avons créé deux objets de la classe Marin, qui possédaient même nom, même prénom et même salaire, et nous les avons comparés avec ==. Nous avions alors vu que == comparait les adresses mémoire des objets, et dans ce cas renvoyait false. Ce comportement est logique, mais il serait utile d'avoir à disposition un moyen de comparer des objets qui puisse nous dire que si leurs champs sont égaux, alors ces objets sont égaux. En d'autres termes, remplacer une égalité technique en égalité sémantique. C'est l'objet de la méthode equals(). Écrivons une méthode equals() pour notre classe Marin, qui retourne true si les champs des deux objets comparés sont égaux.

Exemple 6. Surcharge de equals() dans la classe Marin

public  boolean equals(Object o) {  
     if (!(o  instanceof Marin))  
         return false ;  
      
    Marin marin = (Marin)o ;  
      
     return nom.equals(marin.nom) &&
           prenom.equals(marin.prenom) &&
           salaire == marin.salaire ;  
}

Analysons ce code. Remarquons tout d'abord que l'opérateur instanceof retourne systématiquement false si l'objet testé est nul. Cela garantit que l'objet marin est non nul. Tout d'abord, remarquons que la méthode equals() prend en paramètre un objet de type Object. Une erreur commune consisterait à déclarer l'objet passé en paramètre comme devant être de type Marin. Cette erreur est un peu subtile, et nous la détaillerons dans la suite. Disons ici qu'il est absolument nécessaire que cette méthode equals() prenne un Object en paramètre. La première chose à faire est de tester si l'objet passé en paramètre est non nul, et s'il est bien de la classe Marin. La comparaison d'un objet de type Marin avec un objet nul ou d'un autre type est légale, et elle retournera false systématiquement, ce qui est normal. L'opérateur instanceof, utilisé ligne 5, permet de tester la classe d'un objet. En l'occurrence, il retourne true pour tous les objets de la classe Marin, et de toute classe qui hériterait de Marin. Une fois que nous sommes sûr d'avoir un objet Marin en paramètre, alors il nous faut comparer ses champs un par un. Pour pouvoir accéder à ses champs, il faut le convertir en objet de la bonne classe, c'est ce que fait la ligne 8. Cette opération s'appelle un cast , elle consiste à déclarer un objet (ici marin), et à lui affecter la valeur de l'objet à convertir, en mettant devant et entre parenthèses le type dans lequel on veut faire cette conversion. Il faut toujours vérifier le type de l'objet que l'on caste à l'aide d'un instanceof, avant de faire le cast (il existe également une autre méthode, que nous verrons dans la suite). Un cast réalisé sur un objet qui ne serait pas de la bonne classe jetterait une exception. La comparaison des champs de marin est faite entre les lignes 10 et 12. On remarquera que la comparaison des chaînes de caractères se fait en utilisant aussi la méthode equals(), de la classe String. La classe String possède sa propre méthode equals(), qui retourne true si les deux chaînes de caractères contiennent les mêmes caractères. Nous reverrons en détails la méthode equals() de la classe String, à titre d'exemple supplémentaire. En toute rigueur, avant de tester l'égalité de nom et prenom, il nous faudrait tester si ces deux chaînes de caractères sont nulles ou pas. Si nom est nul (par exemple), alors notre méthode equals() échouera, en jetant l'ignoble NPE. Comme on peut le voir, l'écriture d'une méthode equals() correcte et complète est un processus qui mérite de l'attention.

2.4. La méthode hashCode()

Le rôle de la méthode hashCode() est de calculer un code numérique pour l'objet dans lequel on se trouve. Ce code numérique est censé être représentatif de l'objet, nous allons expliciter ce point immédiatement. Techniquement, la méthode hashCode() est une méthode native qui permet de calculer un nombre ( int) unique associé à une instance de n’importe quelle classe. Par défaut, la méthode de la classe Object retourne l'adresse à laquelle est rangé cet objet, nombre effectivement unique, puisqu'on ne peut pas ranger deux objets au même endroit en mémoire. En toute rigueur, ce point dépend de la JVM que l'on utilise, mais c'est le cas dans la JVM de Sun. Le point délicat est le contrat qui lie les méthodes equals() et hashCode() dans les spécifications de Java.
  • Deux objets égaux au sens de equals() doivent retourner le même hashCode() ;
  • Il n'est pas nécessaire que deux objets différents au sens de equals() retournent deux hashCode() différents...
Donc, surcharger la méthode equals() d'une classe entraîne systématiquement la surcharge de la méthode hashCode(). Ne pas respecter cette règle revient à s'exposer à des bugs obscurs et très difficiles à corriger, nous verrons des exemples précis dans la suite. Surcharger une méthode hashCode() se fait en respectant un algorithme précis. Il existe plusieurs variantes de cet algorithme, nous en donnons une ici. Supposons que nous ayons surchargé une méthode equals(), en écrivant l'égalité entre plusieurs champs de notre classe. La première chose à faire est de choisir deux nombres entiers, pas trop petits, 17 et 31 peuvent faire l'affaire. On initialise l'algorithme en prenant hashCode = 17. Pour chacun des autres champs c pris en compte par la méthode equals(), on construit l'entier hash suivant :
  • si c est un booléen, hash vaut 1 si c est true, 0 s'il est false ;
  • si c est de type byte, short, int ou char, alors hash vaut (int)c ;
  • si c est de type long, alors hash vaut (int)(c^(c >>> 32)) ;
  • si c est de type float, alors hash vaut Float.floatToIntBits(f) ;
  • si c est de type double, alors hash vaut Double.doubleToLongBits(f), et l'on prend le code de hachage du long que l’on récupère ;
  • si c est nul alors hash vaut 0 ;
  • si c est un objet non nul, alors hash vaut c.hashCode() ;
  • si c est un tableau, alors chacun des éléments du tableau est traité comme un champ à part entière.
Et pour chacun de ces champs, on met à jour hashCode :
hashCode =  31 * hashCode + hash
Pour notre classe Marin, la méthode hashCode() est la suivante.

Exemple 7. Surcharge de hashCode() dans la classe Marin

public  int hashCode() {  
     int hashCode =  17 ;  
    hashCode =  31 * hashCode + ((nom == null) ?  0 : nom.hashCode()) ;  
    hashCode =  31 * hashCode + ((prenom == null) ?  0 : prenom.hashCode()) ;  
    hashCode =  31 * hashCode + salaire ;
     return hashCode ;
}

Notons que cette implémentation de la méthode hashCode() est une implémentation parmi d'autres. En particulier, les nombres entiers choisis peuvent varier.

2.5. La méthode finalize()

La présentation de la méthode finalize() est une bonne occasion de parler de la destruction des objets. Effectivement, nous avons vu comment construire des objets, mais rien n'a été dit sur leur destruction. Et pour cause : en Java, il n'y a rien à faire pour détruire un objet, la notion de destructeur n'existe tout simplement pas. La machine Java fonctionne avec un ramasse-miettes ( garbage collector en anglais), qui est censé détecter les objets qui ne sont plus référencés par aucune variable, et les effacer lui-même. Idéalement, lorsque la dernière référence vers un objet est coupée, le ramasse-miettes enregistre cet objet, et de temps en temps, il se met en marche et libère la mémoire. Dans la réalité, les choses sont en fait un peu plus complexes. La notion de garbage collection est en fait très complexe. Une machine Java a à sa disposition plusieurs garbage collectors , qui fonctionnent sur des algorithmes différents. La méthode finalize() a été conçue aux tous débuts de Java, les dernières versions de machines virtuelles ont des garbage collectors conçus très récemment. La sémantique de la méthode finalize() est donc la suivante. Lorsque la machine Java sait qu'elle va supprimer un objet de la mémoire, elle invoque la méthode finalize(). Cette méthode est donc un callback, appelé par la machine Java avant l'effacement d'un objet. Notons tout de suite que l'appel de cette méthode ne se fait pas au moment où un objet n'est plus référencé, mais au moment où la machine Java décide de l'effacer. On n'a donc aucune maîtrise sur le temps au bout duquel l'objet est effacé, ni sur le temps au bout duquel la méthode finalize() est appelée. Quel thread appelle-t-il la méthode finalize() ? Cette question est épineuse, car tout dépend du type de garbage collector utilisé. Ce qui est sûr, c'est que cette méthode n'est pas appelée dans le thread applicatif, et que toute modification de l'objet dans lequel on se trouve risque fort de poser un problème de race condition . D'un façon générale, si l'on choisit d'écrire du code dans cette méthode finalize(), en aucun cas ce code doit avoir une quelconque importance pour le bon déroulement de l'application. Entre autres, il est inutile, et même nuisible, de fermer des fichiers, connexions à des bases de données ou toute autre ressource de ce type. Il est également inutile de tenter de vider le contenu des collections ou des tables de hachage. Poser du code dans une méthode finalize() a toute les chances d'apporter plus de bugs dans notre application, que de solutions à un quelconque problème. Notons qu'à partir de Java 9 la méthode finalize()est dépréciée. Même si l'on travaille sur une application de version antérieure, surcharger cette méthode dans une classe est à présent une mauvaise pratique. Les fuites de mémoire peuvent exister en Java, même si le mécanisme d'allocation et de libération de mémoire mis au point dans les dernières versions de JVM se révèlent très performant, en fait plus performant même que la classique allocation / libération manuelle du C ou du C++. Tenter de les résoudre en posant du code dans la méthode finalize() est inutile et risque d'introduire plus de problèmes que de solutions.

2.6. La méthode getClass()

Cette méthode retourne un objet, instance d'une classe particulière appelée Class. Tout est objet en Java, y compris les classes elles-mêmes ! Il existe donc une classe Class, qui modélise les classes Java. Nous verrons par la suite que cette classe est à la base des mécanismes d'introspection, et ouvre la porte à des méthodes de programmation très puissantes. Notons que la méthode toString() de la classe Class est surchargée, mais ne retourne pas le nom de la classe, comme on pourrait s'y attendre.
Marin m =  new Marin("Surcouf",  "Robert") ;
System.out.println("Classe de marin : " + m.getClass()) ;

> Classe de marin :  class Marin
Si l'on veut juste le nom de la classe, il faut invoquer sa méthode getName().
Marin m =  new Marin("Surcouf",  "Robert") ;
System.out.println("Classe de marin : " + m.getName()) ;

> Classe de marin : Marin
Java langage & API
Retour au blog Java le soir
Cours & Tutoriaux
Table des matières
Introduction : un peu d'histoire
1. Java : genèse d'un nouveau langage
Programmer en Java
1. Un premier exemple
1.1. Aperçu général, cycle de vie
1.2. Un premier programme
1.3. Programmes, applets, servlets, etc...
2. Une première classe
2.1. Écriture d'une classe
2.2. Instanciation de cette classe
3. Types de base, classes et objets
3.1. Types de base
3.2. Classes et objets
Classes Object et String
1. Introduction
2. La classe Object
2.1. La méthode toString()
2.2. La méthode clone()
2.3. La méthode equals()
2.4. La méthode hashCode()
2.5. La méthode finalize()
2.6. La méthode getClass()
3. La classe String
3.1. Introduction
3.2. Construction d'un objet de type String
3.3. Concaténation, StringBuffer et StringBuilder
3.4. Concaténations de chaînes de caractères depuis Java 8
3.5. Concaténations de chaînes de caractères depuis Java 11
3.6. Extraction d'une sous-chaîne de caractères
3.7. Comparaison de deux chaînes de caractères
3.8. Méthodes de comparaisons lexicographiques
3.9. Méthode de recherche de caractères
3.10. Méthode de modification de chaîne
3.11. Méthode de duplication
3.12. Support de l'unicode, internationalisation
Structure d'une classe
1. Introduction
2. Classes
2.1. Classes publiques
2.2. Classes internes
2.3. Classe membre
2.4. Classes locales
2.5. Classes anonymes
2.6. Le mot-clé this
3. Éléments statiques
3.1. Champ statique
3.2. Cas des constantes
3.3. Bloc statique
3.4. Classe membre statique
4. Membres d'une classe, visibilité
4.1. Bloc non statique
4.2. Accès à un membre, visibilité
4.3. Les champs
4.4. Signature d'une méthode
4.5. Les méthodes
4.6. Getters et Setters
5. Constructeur, instanciation
5.1. Chargement d'une classe
5.2. Constructeurs d'une classe
5.3. Instanciation d'un objet
5.4. Destruction d'objets
5.5. Le mot-clé final
6. Énumérations
6.1. Déclaration d'une énumération
6.2. Classe énumération
6.3. Méthode toString()
6.4. Méthode valueOf()
6.5. Méthode values()
6.6. Méthode ordinal()
6.7. Méthode compareTo()
6.8. Constructeurs privés
6.9. Classe utilitaire : EnumSet
Noms, opérateurs, tableaux
1. Introduction
2. Identificateurs, noms et expressions
2.1. Identificateurs et noms
2.2. Expressions
3. Opérateurs, ordre d'exécution
3.1. Ordre d'exécution
3.2. Les opérateurs
3.3. Les opérateurs ++ et --
3.4. Les opérateurs % et /
3.5. Les opérateurs <<, >> et >>>
3.6. L'opérateur instanceof
3.7. Les opérateurs &, | et ^
3.8. Les opérateurs && et ||
3.9. L'opérateur ? ... :
3.10. Les opérateurs d'affectation
4. Tableaux
4.1. Création d'un tableau
4.2. Initialisation d'un tableau
4.3. Utilisation d'un tableau comme un Object
4.4. Tableaux de tableaux
4.5. Copie de tableaux
5. Blocs, boucles et contrôles
5.1. Blocs
5.2. Mots-clés réservés
5.3. Tests : if et switch
5.4. Boucles : for, while, do ... while
5.5. Commandes continue et break
5.6. Commandes return et goto
Nombres, précision, calculs
1. Introduction
2. Calculs
2.1. Précision
2.2. Codage des nombres flottants
2.3. Le mot-clé strictfp
2.4. Conversion de types
3. Dépassements de capacité
3.1. Cas des entiers
3.2. Cas des flottants
3.3. Bibliothèques BigInteger et BigDecimal
4. Fonctions mathématiques
4.1. Fonctions usuelles
4.2. Générateurs aléatoires
5. Classes enveloppe
5.1. Associer les types de base à des objets
5.2. Auto-boxing
Héritage, abstraction, interfaces
1. Introduction
2. Abstraction et encapsulation
2.1. Abstraction
2.2. Encapsulation
3. Héritage
3.1. Définition de l'héritage
3.2. Conséquences pour les membres
3.3. Polymorphisme
3.4. Empêcher l'héritage
4. Classes abstraites
5. Interfaces
5.1. Introduction
5.2. Définition
5.3. Java 8 et les interfaces
5.4. Utilisation des interfaces
5.5. Définition de constantes dans les interfaces
5.6. Utilité des interfaces
Packages
1. Introduction
2. Notion de paquet
2.1. Déclaration d’appartenance à un paquet
2.2. Chargement d’une classe
2.3. Choix de nom
3. Archives, chemin de recherche, classpath
3.1. Archives
3.2. Variable CLASSPATH
3.3. Notion de classloader
3.4. Bilan sur les classes chargées
3.5. Visibilité
3.6. Conseils d'écriture, bibliothèque standard
Exceptions
1. Introduction
2. Erreurs et Exceptions
2.1. Classe Throwable, notion de stack trace
2.2. Classe Error
2.3. Classe RuntimException
2.4. Classe Exception
3. Déclenchement d'une exception
3.1. Exceptions déclenchées par la JVM
3.2. Exceptions déclenchées par l'application
4. Capter une exception
4.1. Traiter une exception localement
4.2. Code de captage
5. Créer ses propres exceptions
Entrées / sorties
1. Introduction
2. Notion de fichier
2.1. Introduction
2.2. La classe File
2.3. Construction d'une instance de File
2.4. Méthodes exposées
3. Flux de sortie
3.1. Introduction, notion de flux
3.2. Écriture de caractères, classe Writer
3.3. Bufferisation, construction d'un flux sur un autre
3.4. Utilisation de PrintWriter
3.5. Écriture d'octets, OuputStream
3.6. Écriture de types primitifs : DataOutputStream
3.7. Écriture d'objets : ObjectOutputStream
4. Flux d'entrée
4.1. Introduction
4.2. Lecture de caractères, classe Reader
4.3. Bufferisation, lecture ligne par ligne
4.4. Lecture d'octets : InputStream
4.5. Lecture de types primitifs : DataInputStream
4.6. Lecture d'objets : ObjectInputStream
5. Lecture et écriture de flux croisés
5.1. Introduction
5.2. Lire des caractères sur un flux binaire : classe InputStreamReader
5.3. Écrire des caractères sur un flux binaire : classe OutputStreamWriter
5.4. Remarque sur les jeux de caractères
6. Serialization d'objets
6.1. Enjeu de la sérialization d'objets
6.2. Serialization d'un objet
6.3. Sérialization d'une grappe d'objets
6.4. Première surcharge : méthode writeObject() readObject()
6.5. Deuxième surcharge : utilisation d'un externalizer
6.6. Troisième surcharge : utilisation d'un objet proxy
7. Flux compressés
7.1. Introduction
7.2. Flux de type gzip
7.3. Flux de type zip