Aller au contenu

POO en Java

Concepts objets

Objectifs

Découvertes des Objets

Qu’est-ce qu’un Objet ?

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.

Voici une liste d’affirmations avant d’entrer dans les explications et les démonstrations :

Exemple d’objet : Un Adhérent d’une association peut être un objet

Nom de la classe : Adhérent

Attributs (quelques caractéristiques) :

Méthodes (comportements et accès) :

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 !

objet


Peut-on parler d’objet ?

Pour vous, parmi cette liste ci-dessous, que pouvons-nous considérer comme des objets ?

Distinction entre Objet et Classe

illustration objets et classe

Dans cette illustration ci-dessus, nous avons bien 2 objets distincts mais ce sont toujours 2 Adhérents issus de la classe Adherent.

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.

Classe : Notion d’abstraction

La classe est un MOULE ou un MODELE pour créer des objets, y compris des châteaux de sable !

moule

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)

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,…).

La réprésentation d’une classe dans un Diagramme de classe (UML) est découpée en 3 blocs :

Remarque : le symbole - signifie private et le + signifie public dans le diagramme de classe ci-dessous :

classe

Classe en Java

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.

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;
	}
}

Représentation graphique de la classe Adherent (UML et Java)

classe uml vers Java

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.

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;
	}
}

Classe Adresse (UML et Java)

classe adresse et code java

Déclaration des variables (primitifs et classes)

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é).

Méthodes d’instance : comportement des objets

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 :

syntaxe méthodes java

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é :

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.

méthodes classe String java

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 ?

Instanciation : le mot magique 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.

démonstration String

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.

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() ?
	
	}
}

Création d’un objet

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.

instanciation

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.

adherentMagaliVergne.setNom("Vergne");
adherentMagaliVergne.setPrenom("Magali");
adherentMagaliVergne.setAge(42);

Comprendre la notion d’encapsulation

instanciation constructeur vide et setters

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.

Adherent adh456  = new Adherent("Philippe", "Bouget", "98 rue de Java",75013, "Paris");

Comment appeler une méthode ?

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;
	}
}

Illustration de la communication entre objets

illustration communication entre objets

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);
    }
}

creation depuis le main

Illustration simplifiée

creation depuis le main

Selon vous, que se passe-t-il si on affecte adresse1 à adhérent2 ?

 adherent2.setAdresse(adresse1);

Syntaxe spécifique pour les Strings

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";

Concaténation avec +

String prenom = "Jonnhy";
String nom = "Depp";
String nomComplet = prenom + " " + nom;

Comparaison de chaînes avec Strings

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 !

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.

Pourquoi cet apparté sur cette méthode equals() de la classe String ?

Le piège de la comparaison avec ==

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.

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.

Illustration mémoire

+---------------------+
|        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)

Bonnes pratiques

Toujours utiliser .equals() pour les comparaisons et éviter les concaténations dans les boucles (utiliser plutôt StringBuilder).

Utilisation des Wrappers (Classes enveloppes)

Problème : On veut stocker directement un type primitif (par exemple un int) dans une ArrayList. Comment faire ?

Tableau des correspondances :

Type origine (primitif) Classe Enveloppe
boolean Boolean
integer Integer
long Long
float Float
double Double
char Character

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.

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(String s);
Integer(int i);
int i = 4;

Integer objetInt = new Integer(i);

int j = objetInt.intValue();

Quelques méthodes de la classe Integer :

Pourquoi utiliser des Packages ?

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"));
	}
}

Agrégation : Les cadeaux du Père Noël

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.

agregation et heritage

Souvenez-vous de notre classe Adherent et Adresse. Quelle est la relation entre Adresse et Adherent selon vous ?

classe

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 !");
	
	}

}


Quelques notions complémentaires (déjà vues)

variables d’instance

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.

Méthodes 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();
 }
public void setTaille(int t)
 {
	taille = t;
 }

 public void getTaille()
 {
	return this.taille;
 }

La référence 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;
 }

Utilisation du mot clé THIS dans un constructeur

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 !

Variables locales

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 ;
	}
}

Surcharge des méthodes

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)

Utilisation des Constructeurs

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;
 }
 ...
 }

Surcharge des constructeurs

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 ;
		}
}

Petit Résumé

Méthodes et variables de classe

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 membres de classe, s’utilisent sans avoir besoin d’instancier 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(&quot;41113&quot;);
 // 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.

Exemple :

public class Client
 {
	private static int prochainNumClient = 1;
	private int numClient;

	public Client() // constructeur
	{
		numClient = prochainNumClient;
		prochainNumClient++; // incrémenter à chaque instanciation.
	}
 }

Constantes

Le modificateur final permet de créer des constantes. La constante est toujours écrite en MAJUSCULE et est non modifiable.

public final static int VALEUR_MAX = 100;
public final static int VALEUR_MIN = 10;

Découverte de la classe Math

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.

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 !

Formatage de dates, de nombres et générateur aléatoire

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(&quot;22/03/98&quot;);
...
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 :

Notion sur l’Héritage et le polymorphisme (voir cours spécifique)

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.

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 ;
 }
 }

L’ancêtre commun à toutes les classes

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 :

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.

Composition d’un objet

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.

Opérateurs this et super

Exemple :

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 , &quot;Cobol&quot; ) ;
 	}
}

Voici les règles dans l’utilisation du mot-clé this :

Héritage et constructeurs

Quelques règles à savoir :

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().

Constructeur avec super

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.

Polymorphisme

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é.

Opérateur instanceof

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

Héritage et Agrégation

Exemple :

Patient est un Individu Chaudière fait partie de Maison

Résumé