Pour programmer en POO, il faut savoir identifier un objet pour à la fois l’instancier, autrement dit, le créer et bien entendu, le manipuler. Vous allez découvrir l’importance de la notion de Classification propre à la POO.
identifier un objet
instancier
Classification
Voici une liste d’affirmations avant d’entrer dans les explications et les démonstrations :
attributs
encapsulation
privés
private
l'état de l'objet
comportements
Exemple d’objet : Un Adhérent d’une association peut être un objet
Adhérent
Nom de la classe : Adhérent
Attributs (quelques caractéristiques) :
prenom
nom
age
adresse
montant
Méthodes (comportements et accès) :
getNom()
setNom()
getPrenom()
setPrenom()
getAge()
setAge()
getAdresse()
setAdresse()
cotiser(float montant)
Voici une représentation graphique de notre objet de type Adhérent qui nous montre le principe d’encapsulation !
Notre objet possède des propriétés (attributs) privées encapsulées car on ne peut pas y accéder directement depuis le monde extérieur mais uniquement à l’aide de méthodes publiques !
encapsulées
publiques
Pour vous, parmi cette liste ci-dessous, que pouvons-nous considérer comme des objets ?
Dans cette illustration ci-dessus, nous avons bien 2 objets distincts mais ce sont toujours 2 Adhérents issus de la classe Adherent.
2 Adhérents
Adherent
public
Nous verrons un plus loin dans ce cours que nous pouvons aussi avoir des méthodes private (privée).
On dit que ces 2 objets sont des instances de la même classe, ici la classe Adherent
Attention : Instance et Objet sont synonymes dans l’univers de la POO.
La classe est un MOULE ou un MODELE pour créer des objets, y compris des châteaux de sable !
En Java chaque objet est l’instance d’une classe. Sans classe, il ne peut pas y avoir d’objet !
Toutes les instances d'une même classe partagent la même structure de données (caractéristiques) et les mêmes méthodes (comportements)
Toutes les instances d'une même classe
même structure de données
mêmes méthodes
Seule, la valeur des propriétés est spécifique à chaque instance.
La classe permet une représentation abstraite d’objets concrets (Adhérent, Moto, Client, Fournisseur,…) ou d’objets abstraits (Action, Compte Bancaire, Portefeuille d’actions, Commande, Devis, Achat,…).
représentation abstraite
La réprésentation d’une classe dans un Diagramme de classe (UML) est découpée en 3 blocs :
Nom de la classe
variables
opérations
void
Remarque : le symbole - signifie private et le + signifie public dans le diagramme de classe ci-dessous :
-
+
J’ai ajouté du code dans la méthode cotiser() et setNom() à titre de démonstration, ainsi que 2 constructeurs pour justement pouvoir réaliser des instanciations d’Adhérents.
cotiser()
package fr.formation.semaine2; public class Adherent { // attributs PRIVÉS (encapsulation) private String prenom; private String nom; private int age; private Adresse adresse; // aspect aborder ultérieurement (agrégation) private float montant; // constructeur pour initialiser l'objet aborder plus loin... public Adherent(String prenom, String nom, Adresse adresse, int age) { this.prenom = prenom; this.nom = nom; this.adresse = adresse; this.age = age; } // constructeur sans argument public Adherent() { } // méthode PUBLIQUE : mise à jour de la cotisation public void cotiser(float montant) { if (this.age < 18 && montant <= 0) { throw new IllegalArgumentException("Problème avec le montant ou l'âge ! (age = "+this.age+ " montant = "+this.montant+ ")"); } setMontant(montant); System.out.println("Cotisation mise à jour pour " + this.nom + ", le montant est de " + montant + " €"); } // getter PUBLIQUE : accès en lecture au nom public String getNom() { return nom; } // setter PUBLIQUE : modification contrôlée du nom public void setNom(String nom) { if (nom == null || nom.trim().isEmpty()) { throw new IllegalArgumentException("Le nom ne peut pas être vide !"); } this.nom = nom; } public void setPrenom(String prenom) { this.prenom = prenom; } public void setAge(int age) { this.age = age; } public void setMontant(float montant) { this.montant = montant; } // getters supplémentaires (optionnels) public Adresse getAdresse() { return this.adresse; } public float getMontant() { return montant; } }
Comme nous le verrons dans la pratique, Java propose un grand nombre de classes qui nous facilitent le développement. Et pour développer des applications, nous devons à la fois les utiliser, voire même en hériter et créer nos propres classes comme la classe Adresse que j’ai ajouté à Adhérent dont voici le code ci-dessous puis la représentation graphique.
Adresse
package fr.formation.semaine2; public class Adresse { private int numero; private String rue; private int codePostal; private String ville; public Adresse(int numero, String rue, int codePostal, String ville) { super(); this.numero = numero; this.rue = rue; this.codePostal = codePostal; this.ville = ville; } public Adresse() { super(); } public int getNumero() { return numero; } public void setNumero(int numero) { this.numero = numero; } public String getRue() { return rue; } public void setRue(String rue) { this.rue = rue; } public int getCodePostal() { return codePostal; } public void setCodePostal(int codePostal) { this.codePostal = codePostal; } public String getVille() { return ville; } public void setVille(String ville) { this.ville = ville; } @Override public String toString() { return "Adresse : " + numero + ", "+ rue + " "+codePostal + " "+ville; } }
La syntaxe de la déclaration est la même que ce soit pour le type primitif ou bien celui d’un objet. J’ai omis volontairement la visibilité (public, private et protected) que nous verrons plus loin.
int indice; char monCaractere; float prix; float montant; int age;
String chaineA; String prenom; String nom; ArrayList tableauB; String chaineC; Adherent jeanTrucmuche; Commande cde12345; Adresse adresse;
Attention chaineA, prenom, tableauB et chaineC,… ne sont que des références d’objets et non des objets ! (revoir le cours sur la pile et le tas si vous avez oublié).
Chaque objet contenant des données peut subir des opérations (traitements). Les opérations sont réalisées en utilisant les méthodes qui recoivent des paramètres et peuvent renvoyer un résultat unique. Les méthodes utilisables sont définies dans la classe.
Une méthode d’instance traduit le comportement des objets de la classe à laquelle appartient cette méthode.
Une méthode est constituée :
{
}
Comme déjà dit, nous allons souvent utiliser des classes Java comme celles, ci-dessous :
Si vous jetez un oeil dans la documentation Java concernant la classe String voici quelques méthodes avec leur fonctionnalité :
String
Cela signifie que lorsque l’on utilise la classe String pour instancier (créer un objet) à partir de cette classe, et bien on peut accéder à toutes les méthodes (comportements ou traitements) possibles qui existent déjà dans cette classe ! Pour accèder à une méthode, il suffit d’utiliser la référence de l’objet suivie d’un . (point) et du nom de la méthode.
.
Du coup, nous avons défini la structure d’une classe, abordé la notion d’objet, découvert le code Java correspondant, mais comment crée-t-on des objets ?
new
Démonstration avec la classe Java String d’une instanciation (new String()) et d’un appel de méthode (.chatAt()) dont nous voyons la syntaxe plus loin ! Il nous faut d’abord aborder l’utilisation des constructeurs.
new String()
.chatAt()
Reprenons une partie du code de nos classes Adresse et Adherent en affichant uniquement les constructeurs pour chacune des classes :
// constructeur pour initialiser un objet Adhérent public Adherent(String prenom, String nom, Adresse adresse, int age) { this.prenom = prenom; this.nom = nom; this.adresse = adresse; this.age = age; } // constructeur sans argument public Adherent() { }
// constructeur pour initialiser un objet Adresse public Adresse(int numero, String rue, int codePostal, String ville) { super(); this.numero = numero; this.rue = rue; this.codePostal = codePostal; this.ville = ville; } // constructeur sans argument public Adresse() { super(); }
Maintenant que nous avons des constructeurs, nous allons pouvoir les utiliser pour créer nos objets dans une autre classe que nous nommons Main qui contient justement une méthode main() pour exécuter nos instanciations. Pour simplifier, cette classe est dans le même packagequ’Adhérent et Adresse. Donc, pas besoin de faire des importations.
Main
main()
package
Main.java
package fr.formation.semaine2; /** * Pour instancier des objets Adherent et Adresse */ public class Main { public static void main(String[] args) { // création de 2 objets de type Adresse Adresse adresseDeJeanTrucmuche = new Adresse(12, "Rue du figuier",30140, "Anduze"); Adresse adresseDeMagaliVergne = new Adresse(2, "Rue des arbouses",75500, "Paris"); // création de nos 2 adhérents // ici on appelle le constructeur avec arguments pour initialiser les attributs au moment de la création Adherent adherentJeanTrucmuche = new Adherent("Jean", "Trucmuche", adresseDeJeanTrucmuche, 52); // ici, on appelle un constructeur vide sans argument pour créer un objet "adherentMagaliVergne" Adherent adherentMagaliVergne = new Adherent(); // du coup, on utilise les méthodes publiques pour initialiser les valeurs des attributs // j'ai volontairement omis l'initialisation de l'adresse adherentMagaliVergne.setNom("Vergne"); adherentMagaliVergne.setPrenom("Magali"); adherentMagaliVergne.setAge(42); // Ensuite, on peut affecter à chacun un montant de cotisation avec la méthode cotiser() // observez comment les méthodes ci-dessus sont appelées ! // Quelle est la syntaxe ? Comment allez-vous appeler la méthode cotiser() ? } }
Dans l’image ci-dessous, nous constatons l’utilisation du mot-clé new avec l’appel des constructeurs contenant des arguments. Remarquez que les constructeurs correspondent à des méthodes sans void malgré le fait qu’ils ne retournent rien. Ils portent le nom de la classe et sont ici, public.
On peut aussi appeler un constructeur sans argument !
Ce constructeur doit être présent si et seulement si d’autres constructeurs avec arguments existent, sinon il est implicite ! Pas besoin de l’écrire.
// ici, on appelle un constructeur vide sans argument pour créer un objet "adherentMagaliVergne" Adherent adherentMagaliVergne = new Adherent();
L’utilisation d’un constructeur sans argument permet d'instancier un objet en préparant l'espace mémoire pour y stocker les informations le concernant ! Il vous faudra par conséquent utiliser des méthodes dites setters ou autres pour initialiser les valeurs des attributs pour l’objet concerné comme ci-dessous.
constructeur sans argument permet d'instancier un objet en préparant l'espace mémoire pour y stocker les informations
setters
adherentMagaliVergne.setNom("Vergne"); adherentMagaliVergne.setPrenom("Magali"); adherentMagaliVergne.setAge(42);
A ce stade, vous avez compris que l’instanciation consiste à créer un objet, à partir d’une classe (avec le mot clé new ) en appelant un constructeur. Dans le schèma ci-dessus, on comprend le principe d’Encapsulation qui permet de rendre inaccessible certaines propriétés d’un objet, voire même d’une méthode et de disposer de méthodes public pour initialiser certaines valeurs et aussi de pouvoir les récupérer.
Encapsulation
Adherent adh456 = new Adherent("Philippe", "Bouget", "98 rue de Java",75013, "Paris");
Nous l’avons vu plus haut, il faut la référence de l’objet suivi d’un . (point) pour invoquer une méthode d’instance correspond à la demande d’un service à cet objet. Ce qui correspond à l’envoi d’un Message.
// instanciation d'un objet Adherent Adherent nouvelAdherent = new Adherent();
La syntaxe est toujours la suivante : maRéférenceObjet.maMéthode(paramètres ou rien)
Voici un appel de la méthode setNom() de la classe Adherent :
nouvelAdherent.setNom("Coca");
Autre démonstration d’appel de méthodes de la classe String que l’on utilise dans un programme quelconque.
Il faut d’abord créer un objet dont la référence est réponse :
String reponse = new String("O");
Ensuite on peut appeler (invoquer) ses méthodes (String) :
boolean ditOui = false; if ( reponse.length() == 1) { if ( reponse.equalsIgnoreCase("o")) { ditOui = true; } }
Si nous reprenons notre exemple avec Adherent et Adresse. Les 2 adresses ainsi que les adhérents sont instanciés à partir d’une méthode main() de la classe Main qui existe dans le même package. Il est évident que dans la réalité, ils ne seront pas instanciés dans cette méthode ! Ce processus est purement pédagogique mais néanmoins logique.
Reprenons notre code Java un peu transformé pour bien comprendre ce qui se passe :
Nous avons bien 2 adresses et 2 adhérents instanciés dans la méthode main() de notre classe Main
package fr.formation.semaine2; /** * Pour instancier des objets Adherent et Adresse */ public class Main { public static void main(String[] args) { // Création de deux objets Adresse (instances distinctes) Adresse adresse1 = new Adresse(12, "Rue du figuier", 30140, "Anduze"); Adresse adresse2 = new Adresse(2, "Rue des arbouses", 75500, "Paris"); // Création d'un Adherent avec le constructeur paramétré // L'objet adherent1 REFERENCE l'objet adresse1 (pas de copie !) Adherent adherent1 = new Adherent("Jean", "Trucmuche", adresse1, 52); // Création d'un Adherent avec le constructeur par défaut Adherent adherent2 = new Adherent(); // Initialisation des attributs via des setters adherent2.setNom("Vergne"); adherent2.setPrenom("Magali"); adherent2.setAge(42); // objet adherent2 REFERENCE l'objet adresse2 adherent2.setAdresse(adresse2); } }
Illustration simplifiée
Selon vous, que se passe-t-il si on affecte adresse1 à adhérent2 ?
adresse1
adhérent2
adherent2.setAdresse(adresse1);
L’utilisation des Strings est fondamentale en informatique. Le langage Java a créé quelques fonctionnalités spécifiques pour cette classe.
L’instanciation est automatique (pas besoin d’utiliser la syntaxe new String()) :
String maChaine = new String("MaChaine est instanciée avec un constructeur explicite !");
String maChaine = "MaChaine est instanciée sans constructeur (implicite)"; String s = "Salut"; String prenom = "Omar";
String prenom = "Jonnhy"; String nom = "Depp"; String nomComplet = prenom + " " + nom;
La classe String donne des méthodes pour comparer 2 chaînes :
Ces 3 méthodes permettent de comparer le contenu d’un String avec le contenu d’un autre _String._Mais attention !
Les objets de type String ne sont pas modifiables (immuables). Il faut dans ce cas plutôt utiliser une classe comme StringBuilder qui, elle, n’a pas cet inconvénient !
StringBuilder
Voici une illustration :
Que se passe t-il ?
Lors de l’étape 2, chaine1 pointe vers l’objet String “Au revoir”, tandis que l’objet String “Bonjour” auparavant référencé est candidat à la poubelle (garbage Collector). Donc, maintenant, chaine1 pointe vers “Au revoir”. Lors de l’étape 4, la fonction de comparaison retournera VRAI (true) car chaine1 et chaine2 pointent vers des chaînes identiques.
garbage Collector
Pourquoi cet apparté sur cette méthode equals() de la classe String ?
equals()
boolean chainesIdentiques = false; String chaine1 = "Texte"; String chaine2 = "Texte"; // si on fait cela, qu'est-ce qui est comparé ? if ( chaine1 == chaine2 ) // on compare la référence (en théorie) { chainesIdentiques = true; } System.out.println(chainesIdentiques);
Le code compare les références mémoire des objets String, pas leur contenu. == vérifie si chaine1 et chaine2 pointent vers le même objet en mémoire. .equals() (chaine1.equals(chaine2)) compare le contenu des chaînes.
==
.equals()
chainesIdentiques
true
Le Pool de Chaînes (String Pool) Java optimise la mémoire en réutilisant les instances de String pour les littéraux (chaînes écrites directement dans le code).
String chaine1 = "Texte"; String chaine2 = "Texte";
Le compilateur Java place une seule instance de “Texte” dans le String Pool (une zone mémoire spéciale pour les chaînes littérales). chaine1 et chaine2 pointent vers la même instance dans le pool.
Résultat : chaine1 == chaine2 retourne true car elles référencent le même objet.
+---------------------+ | Stack | +----------+----------+ | chaine1 | 0x1234 | ---+ +----------+----------+ | | chaine2 | 0x1234 | ----+ +----------+----------+ | v +---------------------+ +-------+ | String Pool | | | +---------------------+ | | | Adresse: 0x1234 | <---------+| | Valeur: "Texte" | | | +---------------------+ | | | | (Les deux variables | | pointent vers la même | | instance en mémoire.) | |
Autre exemple en utilisation les constructeurs de la classe String
boolean chainesIdentiques = false; String chaine1 = new String("Texte"); String chaine2 = new String("Texte"); // si on fait cela, qu'est-ce qui est comparé ? if ( chaine1 == chaine2 ) // on compare la référence { chainesIdentiques = true; } System.out.println(chainesIdentiques); // false car objets distincts System.out.println(chaine1.equals(chaine3)); // true (même contenu mais références différentes)
Toujours utiliser .equals() pour les comparaisons et éviter les concaténations dans les boucles (utiliser plutôt StringBuilder).
Problème : On veut stocker directement un type primitif (par exemple un int) dans une ArrayList. Comment faire ?
int
Tableau des correspondances :
Solution : Utiliser un wrapper qui permet de stocker la référence au type primitif. A chaque type primitif correspond une classe, qu’on nomme wrapper (attention à la majuscule !) : par exemple Boolean est le wrapper du type primitif boolean.
Boolean
boolean
Attention aux exceptions avec Integerpour int et Character pour char. Le wrapper stocke simplement une référence au type primitif, ce qui permet plus de possiblités de traitement.
Integer
Character
char
Integer(String s); Integer(int i);
int i = 4; Integer objetInt = new Integer(i); int j = objetInt.intValue();
Quelques méthodes de la classe Integer :
Toute classe appartient à un package. Si on ne le spécifie pas explicitement, la classe appartient au package par défaut. Trois raisons pour utiliser les packages :
Pour affecter une classe à un package, on utilise la clause package obligatoirement en première instruction :
package fr.info.paie;
Il existe de nombreux packages de base en Java que nous découvrirons au fur et à mesure selon nos besoins.
java.lang java.awt java.io java.math java.net java.text java.util javax.swing java.util
import java.util.ArrayList; // référence explicite à une classe import java.text.*; // référence à toutes les classes d'un package public class ExempleImport { public static void main(String[] args) { DateFormat df =DateFormat.getDateInstance(DateFormat.SHORT); ArrayList liste = new ArrayList(); liste.add(df.parse("22/03/00")); } }
Un objet contient des données dont certaines peuvent elles-même être contenues dans d’autres objets. Les données d’un objet sont souvent l’agrégation (la réunion) des données contenues dans d’autres objets. L’agrégation est souvent confondue avec l’héritage ! Observez ce diagramme, le sens de l’agrégation n’est pas le même que celui de l’héritage. Nous aborderons aussi la composition.
Souvenez-vous de notre classe Adherent et Adresse. Quelle est la relation entre Adresse et Adherent selon vous ?
Dans cette structure nous avons la présence d’une agrégation car Adhérent contient un attribut de type Adresse. C’est une agrégation car l’adhérent possède une adresse mais celle-ci est définie en dehors de lui. Nous verrons ultérieurement quelles peuvent être les conséquences de cela dans notre code par rapport à une relation de composition. Nous aborderons ceci avec l’héritage.
Voici un exemple plus explicite :
Le père Noël doit préparer la distribution des cadeaux aux enfants. Chaque enfant possède une liste de cadeaux et ne peut pas avoir 2 fois le même cadeau. Cette application doit permettre au Père Noël de gérer une liste d’enfants, une liste de cadeaux et des méthodes pour affecter à chaque enfant un cadeau. Cet exemple illustre parfaitement le principe de l’agrégation. Le Père Noël doit posséder 2 listes d’objets Enfants et Jouets.
Classe Enfant :
package fr.bouget.agregation; import java.util.ArrayList; import java.util.Iterator; public class Enfant { private String prenom; private String nom; private int age; private ArrayList<Jouet> listeCadeaux; /** * Constructeur sans argument */ public Enfant() { // appel du second constructeur this(null,null,0); } /** * Constructeur avec 3 arguments * @param nom * @param prenom * @param age */ public Enfant(String nom, String prenom, int age) { this.nom=nom; this.prenom=prenom; this.age=age; // on instancie une liste qui va contenir des objets de type Jouet this.listeCadeaux = new ArrayList<Jouet>(); } /** * @return retourne le prenom */ public String getPrenom() { return prenom; } /** * @param prenom : initialise le prenom */ public void setPrenom(String prenom) { this.prenom = prenom; } /** * @return retourne le nom */ public String getNom() { return nom; } /** * @param nom : initialise le nom */ public void setNom(String nom) { this.nom = nom; } /** * @return retourne l'age */ public int getAge() { return age; } /** * @param age : initialise l'age */ public void setAge(int age) { this.age = age; } /** * @return retourne la liste la listeCadeaux */ public ArrayList<Jouet> getListeCadeaux() { return listeCadeaux; } /** * @param listeCadeaux : initialise la liste de cadeaux */ public void setListeCadeaux(ArrayList<Jouet> listeCadeaux) { this.listeCadeaux = listeCadeaux; } /** * Ajouter un cadeau à sa liste de cadeaux * @param j */ public void ajouterUnCadeau(Jouet j) { this.listeCadeaux.add(j); } /** * Afficher la liste de cadeaux de l'enfant */ public void afficher() { System.out.println(this.getNom()+ " "+ this.getPrenom()+" "+ this.getAge()+" ans"); if (!listeCadeaux.isEmpty()) { System.out.println("Voici le(s) cadeaux reçu(s) : "); Iterator<Jouet> jouets = listeCadeaux.iterator(); while(jouets.hasNext()) { Jouet j = jouets.next(); // ici on appelle la méthode de la classe Jouet j.afficher(); } } } @Override public String toString() { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(this.getNom()+ " "+this.getPrenom()+" "+ this.getAge()+ " ans"); // on regarde s'il a des cadeaux : if (!listeCadeaux.isEmpty()) { stringBuilder.append("\nVoici le(s) cadeaux reçu(s) : \n"); for (Jouet jouet : listeCadeaux) { stringBuilder.append(jouet.getLibelle()+"\n"); } } return stringBuilder.toString(); } // ======================================== méthode hors TP ============ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + age; result = prime * result + ((nom == null) ? 0 : nom.hashCode()); result = prime * result + ((prenom == null) ? 0 : prenom.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (!(obj instanceof Enfant)) return false; Enfant other = (Enfant) obj; if (age != other.age) return false; if (nom == null) { if (other.nom != null) return false; } else if (!nom.equals(other.nom)) return false; if (prenom == null) { if (other.prenom != null) return false; } else if (!prenom.equals(other.prenom)) return false; return true; } }
Classe Jouet :
package fr.bouget.agregation; /** * La classe jouet possède 2 attributs : un libellé et un état, distribué ou pas. * @author Philippe * */ public class Jouet { private String libelle; private boolean distribuer=false; /** * Contructeur avec un argument * @param libelle */ public Jouet(String libelle) { this.libelle=libelle; } public Jouet() { this(null); } /** * @return the libelle */ public String getLibelle() { return libelle; } /** * @return retourne distribuer */ public boolean isDistribuer() { return distribuer; } /** * @param distribuer : initialise distribuer */ public void setDistribuer(boolean distribuer) { this.distribuer = distribuer; } /** * @param libelle the libelle to set */ public void setLibelle(String libelle) { this.libelle = libelle; } public void afficher() { System.out.println(this.libelle + " (distribué = " + (isDistribuer()==true ? "Oui" : "Non")+ ")\n"); } @Override public String toString() { return this.libelle + " (distribué = " + (isDistribuer()==true ? "Oui" : "Non")+ ")\n"; } }
Classe PereNoel :
package fr.bouget.agregation; import java.util.ArrayList; import java.util.Iterator; /** * ici, je n'ai pas fait d"héritage car j'ignore si le père noël est une personne * et même s'il existe ! * Son job, affecter des cadeaux (1 ou plusieurs aux enfants) * @author Philippe * */ public class PereNoel { private ArrayList<Enfant> listeEnfants; private ArrayList<Jouet> listeJouets; /** * Constructeur sans argument */ public PereNoel() { listeEnfants = new ArrayList<Enfant>(); listeJouets = new ArrayList<Jouet>(); } /** * Méthode qui permet d'ajouter un enfant à sa liste * @param e */ public void ajouterEnfant(Enfant e) { // ici il faut tester si l'enfant n'existe pas déjà dans // la liste : if (!listeEnfants.contains(e)) { listeEnfants.add(e); } } /** * Méthode qui permet de supprimer un enfant de sa liste * @param e */ public void supprimerEnfant(Enfant e) { if (listeEnfants.contains(e)) { listeEnfants.remove(e); } } /** * Méthode qui permet d'ajouter un objet de type Jouet à sa liste de jouets * @param j */ public void ajouterJouet(Jouet j) { // ici on ne peut pas avoir plusieurs fois le même jouet dans notre liste : if (!listeJouets.contains(j)) { listeJouets.add(j); } } /** * Méthode qui permet de supprimer un jouet de la liste * @param j */ public void supprimerJouet(Jouet j) { if (listeJouets.contains(j)) { listeJouets.remove(j); } } public void afficherListeDesEnfants() { System.out.println("Voici la liste des enfants :\n"); System.out.println("Démo en utilisant un Iterator<> et une boucle While() :"); Iterator<Enfant> enfant = listeEnfants.iterator(); while(enfant.hasNext()) { // on récupère un objet de type enfant dans la liste : Enfant e = (Enfant)enfant.next(); e.afficher(); // ici, quelle mérhode appelle t-on ? } System.out.println("\nDémo en utilisant une boucle foreach :"); for (Enfant petit : listeEnfants) { petit.afficher(); } } public void afficherListeDesJouets() { System.out.println("Voici la liste des jouets :"); for (Jouet jouet : listeJouets) { jouet.afficher(); } } /** * Méthode qui permet au PèreNono d'associer un enfant et un jouet * On pourrait améliorer cette méthode en y ajoutant une exception personnalisée * qui permettrait de savoir si le cadeau existe et si l'enfant exite aussi dans la liste. * @param e * @param j */ public void distribuer(Enfant e, Jouet j) { // on teste si l'enfant et le jouet sont dans les listes // du père Noël et aussi que le jouet n'est pas déjà distribué : if (listeEnfants.contains(e) && (listeJouets.contains(j) && j.isDistribuer()==false)) { // avant d'effectuer la distribution, le père noel // veut vérifier que l'enfant ne possède pas déjà ce jouet // parmi ses cadeaux : if (!e.getListeCadeaux().contains(j)) { // on affecte le jouet à la liste de cadeaux de l'enfant e.ajouterUnCadeau(j); // on précise que ce jouet n'est plus disponible, donc // distribuer = true (vrai) : j.setDistribuer(true); } } else { System.out.printf("Désolé, le jouet %s n'est peut-être pas disponible !\n", j.getLibelle()); System.out.printf("Ou bien l'enfant %s %s n'est peut-être pas dans la liste !", e.getNom() , e.getPrenom()); } } }
Classe TestExemple :
package fr.bouget.agregation; /** * Classe permettant de tester l'appli PèreNono * @author Philippe * Date : Décembre 2020 * * Objectifs : * - manipulation de la notion de classe, propriétés et méthodes * - utilisation des principes d'agrégation * - manipulation d'un ArrayList<> * */ public class TestExemple { public static void main(String[] args) { // on va d'abord crée un Père Noel ! (même s'il n'existe pas) PereNoel papaNono = new PereNoel(); // Ensuite on va crée 3 enfants : // Noémie Truc à 8 ans : Enfant nono = new Enfant("Bidule","Noémie",8); // Joachim Machin à 6 ans : Enfant jojo = new Enfant("Machin","Joachim",6); // Soufiane Touti à 9 ans : Enfant souf = new Enfant("Truc","Soufiane",9); Enfant doublon = new Enfant("Bidule","Noémie",8); // Ensuite on crée les jouets à distribuer : // on pourrait en mettre davantage Jouet joujou1 = new Jouet("Ferrari 308GTB"); Jouet joujou2 = new Jouet("BarbiZou la poupée qui gazouille "); Jouet joujou3 = new Jouet("Super puzzle avec 180 pièces "); Jouet joujou4 = new Jouet("Mikado"); Jouet joujou5 = new Jouet("Rubik's Cube"); Jouet joujou6 = new Jouet("iPad"); // le père Noel met dans sa hote des jouets : papaNono.ajouterJouet(joujou1); papaNono.ajouterJouet(joujou2); papaNono.ajouterJouet(joujou3); papaNono.ajouterJouet(joujou4); papaNono.ajouterJouet(joujou5); papaNono.ajouterJouet(joujou6); // le père Noel met dans sa liste des enfants : papaNono.ajouterEnfant(nono); papaNono.ajouterEnfant(jojo); papaNono.ajouterEnfant(souf); // il affiche la liste des enfants à visiter et // les cadeaux à distribuer (ça va, il a pas trop de boulot): papaNono.afficherListeDesEnfants(); papaNono.afficherListeDesJouets(); // il affecte des cadeaux aux 3 enfants : papaNono.distribuer(nono,joujou1); papaNono.distribuer(jojo, joujou2); papaNono.distribuer(souf,joujou3); papaNono.distribuer(souf,joujou4); papaNono.distribuer(nono,joujou5); papaNono.distribuer(jojo,joujou6); // il va afficher les enfants avec leurs cadeaux : nono.afficher(); jojo.afficher(); souf.afficher(); // peut-il ajouter un autre jouet(joujou2) à nono ? papaNono.distribuer(nono,joujou2); // pour le fun, on teste de la méthode equals rédéfinie dans la classe Enfant (hors TP) if (doublon.equals(nono)) System.out.println("\nIls ne portent pas la même référence mais ils ont la même valeur !"); if (doublon!=nono) System.out.println("\nOn compare leurs références... ce n'est pas la même !"); } }
Une variable d’instance est généralement déclarée privée (private) pour assurer l’encapsulation. Elle peut contenir une valeur de type primitif ou une référence à un objet. On peut l’initialiser, sinon elle prend la valeur par défaut liée à son type.
private int age; private String nom;
Chaque objet (instance) possède ses propres valeurs pour les variables d’instance.
Une méthode est l’équivalent d’une fonction effectuant un traitement spécifique pour un objet. Comme une fonction, une méthode peut recevoir plusieurs arguments et peut retourner un seul résultat. Une méthode d’instance peut accéder à toutes les variables d’instance de la classe.
public String getPrenomNom() { return prenom.trim()+ " "+ nom.trim(); }
Les paramètres des méthodes : La définition de la méthode indique le type et le nom des paramètres attendus. A l’appel de la méthode, les valeurs des paramètres sont reçues dans des variables locales.
public void setTaille(int t) { taille = t; } public void getTaille() { return this.taille; }
this
this représente la référence de l’objet courant. Dans l’exemple ci-dessous, le mot clé this permet de lever une ambiguïté de nommage entre le paramètre et la variable d’instance.
public void setTaille(int taille) { this.taille = taille; }
private int matricule; private String diplome; // premier constructeur Employe(int matricule) { // ici nous appelons le second constructeur en lui faisant passer des paramètres this(matricule,"titre CDA"); } Employe(int matricule, String diplome) // second constructeur { this.matricule = matricule; this.diplome = diplome; }
Cet exemple de constructeurs ci-dessus dans lequel on appelle le premier constructeur qui lui-même appelle le second constructeur grâce à la syntaxe spécifique this(). Ceci évite de réécrire les mêmes instructions dans les différents constructeurs !
this()
Les valeurs des paramètres sont stockées dans des variables locales. On peut définir ses propres variables locales qui peuvent être des types primitifs ou des références d’objets. Une variable locale doit être déclarée et initialisée avant d’être utilisée. Les variables locales sont détruites en fin d’exécution de la méthode.
public class Employe { // variable d'instance private String nom; private int matricule; // variable de classe ou statique private static int memoDernierMatricule; public void setNom(String nom); { // variable locale String prefixe = "_FI_"; this.nom = prefixe + nom ; } }
Quand on définit deux ou plusieurs méthodes de même nom, on dit que la méthode est surchargée. Ce n’est possible que si les méthodes différent entre elles par le nombre et/ou le type des paramètres.
Par exemple la classe String possède une méthode substring() qui est surchargée comme ci-dessous :
String substring (int indexDebut) String substring (int indexDebut, int indexFin)
L’instanciation par l’instruction new invoque un constructeur. Toute classe possède un constructeur par défaut, sans paramètres. Si le développeur.euse écrit un constructeur avec des paramètres, le constructeur par défaut n’existe plus. Les initialisations, implicites ou explicites, sont effectuées avant l’exécution des constructeurs.
Un constructeur est une méthode qui porte le même nom que la classe, il n’est pas typé et ne doit pas être déclaré void.
Exemple :
Client unClient = new Client("Martin"); // appel extérieur du constructeur ... public class Client { private String nom; ... // Définition d'un conctructeur recevant le nom en argument public Client(String n) // réception de l'argument dans une // variable locale n { // initialisation de la variable d'instance nom nom = n; } ... }
Déclaration possible de plusieurs constructeurs dans une même classe, de signatures différentes.
Client c = new Client(); // par défaut Client c = new Client("Martin"); // nom Client c = new Client("Martin", 47); // nom et age Client c = new Client(6002372); // numéro
En l’absence de déclaration explicite d’un constructeur, le compilateur en crée un par défaut (sans paramètres). A partir d’un constructeur il est possible d’en appeler un autre de la même classe avec l’instruction this(listeDesArguments) déjà vu plus haut dans le cours.
public class Client { String nom; int age; public Client(String nom) { this(nom, 0) ; // appel du constructeur à 2 arguments ... } public Client(String nom, int age) { this.nom = nom ; this.age = age ; } }
Il faut faire la différence entre membres de classe et membres d’instance (d’objet).
Les méthodes et variables définies au niveau d’une classe sont appelées méthodes et variables de classe (statiques). La variable de classe est un moyen d’ECHANGER des informations entre les différents OBJETS d’une même classe. Une variable de classe est déclarée STATIQUE avec le mot-clé STATIC. Cette variable STATIQUE est PARTAGEE par toutes les instances de la classe.
Les variables de classe sont initialisées au chargement de la classe. Une variable de classe n’existe qu’en un seul exemplaire.
public class Employe { private int matricule; private static int dernierMatricule; public Employe() { matricule = ++dernierMatricule; }
Les méthodes de classe sont utilisables sans instanciation préalable de la classe :
// appel de la méthode parseInt() de la classe Integer int rayon = Integer.parseInt("41113"); // appel de la méthode sin de la classe Math double sinusAngle = Math.sin(3.14159);
Les méthodes de classe ne peuvent pas accéder aux membres d’instance (variables ou méthodes) et sont l’équivalent des librairies utilitaires du langage C ou d’autres langages.
Les membres de classes permettent de définir des méthodes main() pour démarrer :
public static static void main(String[] args) { Compte compte1 = new Compte(); Compte compte2 = new Compte(1234); }
double racineD = Math.sqrt(d);
double surface = Math.PI Math.PI * Math.pow(rayon,2);
Les membres de classe peuvent aussi être utilisés pour des variables globales à toutes les instances de la classe.
public class Client { private static int prochainNumClient = 1; private int numClient; public Client() // constructeur { numClient = prochainNumClient; prochainNumClient++; // incrémenter à chaque instanciation. } }
Le modificateur final permet de créer des constantes. La constante est toujours écrite en MAJUSCULE et est non modifiable.
final
public final static int VALEUR_MAX = 100; public final static int VALEUR_MIN = 10;
Elle offre des fonctions et des constantes telles que :
public static final double E = 2.718281828459045; public static final double PI = 3.141592653589793; public static double pow(double a, double b); public static double random();
Exemples d’utilisation :
// Choix aléatoire de 6 chiffres de loto (de 1 à 49) // Version simplifiée sans vérification entre les 6 numéros... // Merci de compléter cet algorithme classique en exercice... int[] grille = new int[6]; int nbNum = 0; while (nbNum < 6) { int n = ((int)(Math.random() * 49) + 1); grille[nbNum] = n; nbNum++; System.out.println(n); }
Pour résumer, une méthode de classe est toujours déclarée static. Elle correspond à un SERVICE directement rendu par la CLASSE. Dans la méthode ci-dessous, l’accès aux membres de la classe est impossible.
static
Exemple de déclaration de la méthode sin() de la classe Math :
public class Math { public static double sin(double angle) { ... return ...; } }
Exemple d’invocation de la méthode Sin() :
double sinus = Math.sin(3.14);
L’appel de la méthode statique sin() se fait directement à partir du nom de la classe Math , soit Math.sin() suffit. Vous n’avez pas besoin d’instancier un objet de la classe Math pour invoquer la méthode sin(). Il en est ainsi pour toutes les méthodes de classe !
Java fournit des techniques intéressantes pour formater les dates et les nombres. Voici un exemple avec l’une d’elle, pas forcément la plus récente. Il faut voir la documentation java.
Il faut pour cela :
Instancier DateFormat ou NumberFormat puis appeler la méthode sur l’instance en lui passant la donnée à formater. La méthode renvoie la donnée formatée.
DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT); Date d = df.parse("22/03/98"); ... String dateAffichage = df.format(d);
Vous pouvez aussi utiliser la classe DecimalFormat et sa méthode format() :
DecimalFormat df = new DecimalFormat("###.##"); System.out.println("nombre formatée : "+df.format(145.56298) );
Ce code permettra d’afficher le nombre passé en paramètre avec 2 décimales seulement en arrondissant à 145,55.
Vous avez aussi la classe Random avec des méthodes pour générer des nombres aléatoires :
int unEntier = (new Random()).nextInt(49); System.out.println("Voici un entier tiré au hasard : "+ unEntier) ;
Cette méthode attend comme argument la limite supérieure, ici 49, les tirages iront de 0 à 49. Il y a d’autres méthodes de cette classe permettant de récupérer des double, des flottants,…
Faites un petit tours dans le package java.util , il y a plein de trésors.
Remarques sur les membres de classe :
La réutilisation avant l’objet
Un problème classique en programmation : sur des structures de données similaires on doit faire des traitements identiques pour certaines données communes et des traitements spécifiques pour un certain nombre de données spécifiques. On s’oriente alors trop souvent vers deux types de solutions qui vont bafouer les principes de base de la programmation.
Soit on intègre les traitements dans un même programme et on utilise une structure alternative pour les traitements spécifiques. On aboutit ainsi à un mégalithe.
Soit on duplique le code commun dans des programmes différents et on aboutit à plusieurs monolithes contenant la logique commune dupliquée.
Dans une application de santé publique, représentons l’objet Individu et Patient :
Classe : Individu Attributs : -nom : String -prenom : String
Méthodes : +getNom() :String +getPrenom() :String
Classe : Patient Attributs : -nom : String -prenom : String -numeroSecu :String
Méthodes : +getNom() :String +getPrenom() :String +getNumeroSecu():String
La relation d’héritage avec UML :
L’héritage en Java donne le code suivant :
class Individu { private String nom ; private String prenom ; public String getNom() { return nom ; } public String getPrenom() { return prenom ; } } class Patient extends Individu { private String numeroSecu; public String getNumeroSecu() { return numeroSecu ; } }
Toutes les classes sont rattachées directement ou indirectement à la classe Object (java.lang.Object) de Java. Comme nous l’avons vu plus haut, les classes sont organisées de manière hiérarchique. Toutes les classe ont un ancêtre et un seul.
java.lang.Object <– Employe <– Programmeur
Ici, la classe Programmeur hérite de la classe Employé qui elle-même hérite comme tous les objets Java, de la classe Object du paquetage Java.lang.Object. Les termes utilisés pour exprimer le principe de l’héritage sont multiples :
Dans notre exemple, la classe Employé est dite :
La classe Programmeur est dite :
Sous-classe
Toutes les classes (sauf la classe Object ) ont UN ANCETRE ET UN SEUL.
Exemple : Class Programmeur extends Employé
Nous n’avons pas besoin de préciser que la classe Employé est dérivée de la classe Object, ceci est implicite.
C’est l’ancêtre commun (Root) de toutes les classes Java. Il propose des méthodes de base pour tous les comportements généraux comme :
Ces méthodes de base peuvent être utilisées directement sur tous les objets Java.
Un objet issu d’une classe dérivée est composé de deux parties
Toutes les méthodes publiques de la classe ancêtre sont héritées, donc utilisables à partir de la classe dérivée.
Dans un langage OO, c’est l’héritage des méthodes qui va considérablement augmenter le taux de réutilisation du code. Les constructeurs ne font pas partie de l’héritage.
class Programmeur extends Employé { private String langage ; // constructeur 1 public Programmeur( String nom , String langue ) { super( nom ); this.langage = langue; } // constructeur 2 public Programmeur( String nom ) { this( nom , "Cobol" ) ; } }
Voici les règles dans l’utilisation du mot-clé this :
Quelques règles à savoir :
Les constructeurs ne font pas partie de l’héritage : Si la classe ancêtre ne possède pas de constructeur sans argument, le constructeur de la classe dérivée doit appeler le constructeur de son ancêtre.
A la création d’un objet d’une classe dérivée on peut écrire un constructeur pour initaliser les données spécifiques à la classe dérivée.
Comment faire pour construire la partie ancêtre de la classe dérivée ?
On peut dans le constructeur de la classe dérivée appeler par super() un constructeur de la super classe en respectant la signature du constructeur. Cet appel à super() doit être la première instruction du constructeur.
class Patient extends Individu { private String numeroSecu; public Patient (String n, String p, String num) { super ( n , p) ; numeroSecu = num ; } ... } class Individu { private String nom ; private String prenom ; public Individu (String n, String p) { nom = n ; prenom = p ; } ... }
Quelques règles sur les constructeurs d’une même classe :
Une classe peut avoir plusieurs constructeurs. Un constructeur peut faire appel à un autre de la même classe en utilisant this().
Réécriture d’une méthode : Une méthode contenue dans une classe ancêtre peut être réécrite (on dit aussi redéfinie) dans une classe dérivée. La méthode de la classe dérivée doit avoir la même signature que celle de la classe ancêtre, elle va surcharger (override) cette méthode ancêtre. C’est toujours la méthode la plus interne qui sera exécutée. Par exemple si on a défini une méthode print() pour la classe individu, on pourra écrire un print() spécifique pour la classe Patient. Mais dans le print() de Patient on pourra aussi utiliser, grâce à super(), le print() d’Individu, puis compléter …
Exemple de réécriture de méthode :
class Individu { private String nom ; private String prenom ; public void print() print() { ... // formate et imprime nom et prénom } ... }
Une classe Patient qui hérite de Individu :
class Patient extends Individu { private String numeroSecu; public void print() { super.print() // appel du print() de la super classe ... // impression spécique à la classe dérivée } ... }
Obligatoirement dans un CONSTRUCTEUR TOUJOURS en PREMIERE INSTRUCTION UNE SEULE FOIS par constructeur.
La référence super () permet d’appeler le constructeur de l’ancêtre. Le constructeur de la classe dérivée doit fournir les valeurs nécessaires au constructeur de l’ancêtre comme dans l’exemple ci-dessous :
public Programmeur( String unNom ) { super( unNom ) ; // Appel du constructeur de l'ancêtre }
Exemple avec l’utilisation de super :
public Employe( String n) { ... } public Programmeur( String n) { super(n) ; ... }
on tape la commande : new Programmeur(“Vincent”);
1 – appel du constructeur Programmeur 2 – appel par SUPER du constructeur Employé avec passage de n 3 – retour après le mot-clé super du constructeur Programmeur pour exécuter la suite.
C’est la capacité d’envoyer un même message à des objets de classes différentes, mais d’activer en fait une méthode spécifique à chaque classe. Le polymorphisme est intéressant dans un contexte d’héritage. Le même message peut être envoyé à divers objets de la hiérarchie mais il activera la méthode approprié en fonction de la nature de l’objet récepteur.
Exemple : si on a mis dans un Vector des objets Individu, Patient, Employe,… on activera la méthode print() de la même façon sur tous les éléments du Vector, sans se soucier de leur spécificité.
Il permet de comparer une instance d’objet à une classe Java. Il renvoie vrai si le nom de la classe fait partie des classes dont l’objet référencé est issu.
patient1 = new Patient(...) if (patient1 instanceof Patient) // Vrai if (patient1 instanceof Individu) // Vrai aussi ind1 = new Individu(...) if (ind1 instanceof Individu) // Vrai if (ind1 instanceof Patient) // Faux
Patient est un Individu Chaudière fait partie de Maison