Programmation Orientée Objet
Java repose solidement sur la POO qui offre une approche de programmation centrée sur les concepts de classes et d'objets. Les classes définissent des modèles de données et de comportements, tandis que les objets sont des instances spécifiques de ces classes.
Voyons comment nous allons réaliser les modèles nécessaires qui vont structurer et contenir les données utilisées dans le projet.
Création des classes
Avant de plonger dans la création de nos classes, prenons un moment pour comprendre ce qu'est une classe et ses éléments essentiels.
Classe Java
Une classe contient :
- Attributs : Ce sont les variables qui définissent les caractéristiques de l'objet.
- Méthodes : Ce sont les fonctions qui définissent le comportement de la classe.
- Constructeur : C'est une méthode spéciale portant le même nom que la classe, utilisée pour initialiser un objet dès sa création. Si la classe n'a pas d'attribut, par défaut, la classe aura un constructeur sans paramètre.
Les classes en Java doivent respecter le principe de l'encapsulation. Cela se réalise en définissant les attributs comme étant private et en fournissant des méthodes public pour y accéder qui sont :
- Setters et Getters : Ce sont des méthodes permettant de contrôler l'accès aux attributs. Les setters modifient les valeurs, tandis que les getters les récupèrent.
Exemple
public class MyClass {
private int myNumber;
public MyClass(int myNumber) { // Le constructeur
this.myNumber = myNumber;
}
public int getMyNumber() { // Getter
return this.myNumber;
}
public void setMyNumber(int myNumber) { // Setter
this.myNumber = myNumber; // Ici pour pouvoir faire la distinction entre
// myNumber en paramètre et celui déclaré au
// niveau de la classe, il suffit de rajouter
// this.nomDeLAttribut pour indiquer celui de
// la classe. Celui sans 'this.' réfère donc
// la variable passée en paramètre.
}
}
Dans l'exemple ci-dessus, nous avons utilisé private
comme modificateur d'accès.
Il existe 4 modificateurs d'accès, en voici la liste avec leur scope :
Modificateur d'accès | Classe | Package | Sous-classe | Tout le monde |
---|---|---|---|---|
public |
Oui | Oui | Oui | Oui |
protected |
Oui | Oui | Oui | Non |
Sans modificateur | Oui | Oui | Non | Non |
private |
Oui | Non | Non | Non |
Classe Travel
On peut maintenant commencer la création de la classe Travel
qui représentera un voyage proposé par l'agence.
À vous de jouer !
- Créez un package
model
, dans le packageagencymanagement
, qui comportera l'ensemble de nos modèles. - Créez la classe
Travel
en suivant la même logique que l'exemple d'une classe Java. - Pour cette classe, nous aurons besoin d'un id, d'un nom, d'un aéroport de départ, d'un aéroport d'arrivée, d'une destination, d'un nombre maximal de participants ainsi que d'un prix.
Type de données primitifs
Type de données primitifs | Description |
---|---|
byte |
Nombre entier sur 8 bits |
short |
Nombre entier sur 16 bits |
int |
Nombre entier sur 32 bits |
long |
Nombre entier sur 64 bits |
float |
Nombre décimal sur 32 bits |
double |
Nombre décimal sur 64 bits |
boolean |
Valeur true ou false |
char |
Un caractère simple |
Il existe un objet très utilisé en Java mais qui n'est pas un type primitif : l'objet String
. Il est
utilisé pour contenir les chaînes de caractères.
Attention
- Prenez soin de choisir les types appropriés et d'ajouter le constructeur, les getters et les setters. Votre IDE peut vous les générer.
Classe User
À présent, pour représenter les profils de nos utilisateurs, nous allons créer une classe User
.
Nous aimerions pouvoir construire notre utilisateur comme suit :
À vous de jouer !
Essayez de construire ce modèle User
en identifiant les informations qu'il doit contenir.
Les méthodes toString, equals et hashCode
En plus des éléments courants comme les constructeurs, les getters et les setters, il est fréquent de redéfinir les méthodes toString, equals et hashCode dans une classe.
Ces méthodes sont héritées de la classe Object en Java.
Concernant les méthodes toString, equals et hashCode
Par défaut, toutes les classes créées en Java héritent de la classe Object
. Parmi les méthodes héritées
de Object
, on retrouve toString
, equals
et hashCode
. Si on redéfinit pas ces méthodes, elles se basent par défaut sur la référence de l'objet :
toString
renvoie une représentation textuelle de l'objet qui est la référence.equals
compare deux objets pour vérifier leur égalité en comparant les références.hashCode
fournit un code de hachage unique pour l'objet en calculant lehash
de la référence.
Exemple
public class MyClass {
...
@Override
public boolean equals(Object o) {
// S'ils ont la même référence, c'est qu'il s'agit du même objet.
if (this == o) return true; // En Java, si le if ne contient qu'une ligne, il n'est
// pas nécessaire d'encadrer le code avec { ... }
// Il faut que ça soit le même type d'objet.
// getClass() est aussi une méthode héritée de Object.
if (o == null || getClass() != o.getClass()) return false;
// Maintenant qu'on sait que c'est le même type d'objet, on peut 'cast' Object en MyClass.
// Le `cast` est possible parce que MyClass est une sous-classe de Object.
MyClass myClass = (MyClass) o;
return myClass.getId() == id;
}
@Override
public int hashCode() {
return Objects.hash(id);
}
@Override
public String toString() {
return "MyClass{" +
"id=" + id +
'}';
}
}
Il faut toujours surcharger hashCode
lorsque vous surchargez equals
D'après la documentation du hashCode()
:
The general contract of hashCode is:
Whenever it is invoked on the same object more than once during an execution of a Java application, the hashCode method must consistently return the same integer, provided no information used in equals comparisons on the object is modified. This integer need not remain consistent from one execution of an application to another execution of the same application.
If two objects are equal according to the equals method, then calling the hashCode method on each of the two objects must produce the same integer result.
It is not required that if two objects are unequal according to the equals method, then calling the hashCode method on each of the two objects must produce distinct integer results. However, the programmer should be aware that producing distinct integer results for unequal objects may improve the performance of hash tables.
Cette documentation fournit des détails sur le contrat entre hashCode()
et equals()
et pourquoi il est
indispensable de surcharger la méthode hashCode()
lorsque vous surchargez equals()
pour maintenir le comportement
correct des objets dans les structures de données basées sur les hashs comme HashSet
, HashMap
, etc.
À vous de jouer !
- Redéfinissez
equals
ethashCode
. Deux utilisateurs sont différents si et seulement si leursid
sont différents.
Votre IDE peut générer ces méthodes.
Essayez votre code dans le Main
À vous de jouer !
Dans le Main
:
- Créez un voyage
firstTravel
dont le départ est prévu depuis Paris à destination de Tokyo, pour un maximum de 5 participants au prix de 2000 euros. - Créez un utilisateur nommé
John Doe
avec le contact suivant :john.doe@gmail.com
et loggez ses informations ( pensez à redéfinir toString).
Exemple :
Résultat attendu :
Utilisateur John Doe, contact : john.doe@mail.com
Proposition d'offres et intégration des réductions
Héritage, polymorphisme et classe abstraite
Héritage et polymorphisme
Comme pour plusieurs langages de programmation objet, Java dispose aussi d'un système d'héritage. Par contre,
un objet Java ne peut hériter que d'une seule classe. Le multi héritage en Java n'est pas possible. Quand on crée
un nouvel objet, si on ne le fait pas explicitement hériter d'une classe, il sera automatiquement hérité par Object
au moment de la compilation. La classe Object
n'étend aucune autre classe.
Le mot clé qui permet d'hériter d'une classe est extends
:
Pour voir comment fonctionne l'héritage, nous allons coder un cas pratique.
Nous voulons proposer à nos utilisateurs différents types d'offres durant leur voyage, pouvant inclure des services de restauration ou d'hôtellerie.
Nos offres partagent des caractéristiques entre elles, mais ont aussi leurs propres caractéristiques. Nous allons retranscrire cela dans le code.
À vous de jouer
Nous vous laissons choisir le type pour chaque attribut. Dans model
:
- Créez les classes
Offer
,RestaurentOffer
etHotelOffer
. - Ajoutez les attributs
id
,score
,description
,nbOfReviews
dansOffer
. Nous souhaitons respecter le principe d'encapsulation, mais nous voulons aussi que les classes enfants puissent. accéder aux attributs sans utiliser de getters et de setters. (cf. le tableau des modificateurs d'accès). - Ajoutez les attributs
nbOfPersons
etnbOfStars
dansRestaurentOffer
. - Ajoutez les attributs
isBreakfastIncluded
etnbOfAvailableDays
dansHotelOffer
. - Faites hériter
RestaurentOffer
etHotelOffer
deOffer
. - Dans
Main.java
, créez une annonce de typeHotelOffer
.
Indice
- Créez une méthode
String getDetails()
dans les sous-classesRestaurantOffer
etHotelOffer
. Loggez le détail de la classeRestaurantOffer
Les différentes manières de créer une String
Il existe plusieurs moyens de créer une String
en Java. Voici les façons les plus courantes de le faire :
return "Premier détail " + attribut1 + " second détail " + attribut2;
return "Premier détail %s second détail %s".formatted(attribut1, attribut2));
StringBuilder sb = new StringBuilder();
sb.append("Premier détail ");
sb.append(attribut1);
sb.append(" second détail ");
sb.append(attribut2);
return sb.toString();
À présent, nous souhaiterions savoir si nos offres sont valides ou pas.
Pour cela, nous allons ajouter cette méthode dans la classe Offer
:
public boolean isValid() {
return score >= 0 &&
score <= 5 &&
description != null &&
!description.isBlank() &&
nbOfReviews >= 0;
}
Mais comme vous vous en doutez, on voudrait faire de la validation pour chaque sous-type d'offre.
Pour ce faire, nous allons devoir employer une des notions du polymorphisme en Java, qui est la surcharge de méthode.
La méthode enfant doit avoir la même signature que celle présente dans la classe parent. Elle devrait aussi être annotée par @Override
même
si ce n'est pas requis pour compiler le code. Cela contribue à rendre le code source plus lisible.
Il faudra aussi que la validation des méthodes enfants prennent en compte celle du parent.
À vous de jouer !
- Redéfinissez la méthode
isValid()
dans chaque classe enfant.
Classe abstraite
Vous remarquerez qu'on a une méthode getDetails()
dans toutes les sous-classes et on voudrait l'avoir pour toutes les futures sous classes.
Aussi, il y a peu d'intérêt à instancier uniquement la classe Offre
. C'est là que rentrent en jeu les classes abstraites.
Pour que l'on ne puisse pas instancier une classe Offre
dans le code, il faut rendre cette classe abstraite de la
manière suivante :
De plus, si on veut garantir que l'ajout de getDetails()
soit effectué à chaque fois qu'une nouvelle sous-classe sera créée, il faudra ajouter une méthode abstraite dans la classe abstraite pour rendre obligatoire la création de la méthode getDetails()
:
public abstract class Offer {
...
// Ajouter cette ligne en dessous des attributs.
protected abstract String getDetails();
...
}
Regardez ce qui se passe lorsque vous supprimez une des méthodes getDetails()
et que vous essayez de compiler.
Pensez à rajouter @Override
sur toutes les méthodes getDetails()
.
Modificateur d'accès et surcharge
Vous avez probablement remarqué que les modificateurs d'accès utilisés ne sont pas les mêmes entre la classe parent et les classes enfants. En Java, le modificateur d'accès d'une méthode de surcharge peut autoriser plus, mais pas moins,
d'accès que la méthode surchargée. Par exemple, une méthode d'instance protected
dans la classe parent peut être
rendue public
, mais pas private
, dans la sous-classe.
Interfaces
On a presque défini toutes nos classes. On veut désormais offrir des réductions à notre clientèle, comment pourrait-on les appliquer ?
L'idée est de définir une interface commune appelée Discount
qui énonce la méthode nécessaire pour calculer une réduction. Cette interface servira de contrat, en spécifiant le comportement attendu.
Ensuite, nous créerons deux classes distinctes : PercentDiscount
et ValueDiscount
. Ces deux classes implémenteront l'interface Discount
, ce qui signifie qu'elles doivent fournir une implémentation de la méthode définie dans cette interface.
Exemple
Dans cet exemple, une interface Animal est définie avec une méthode shout() (crier).
Interface :
public interface Animal {
void shout(); // Toutes les déclarations de méthodes dans une interface sont implicitement `public`
// de sorte que vous pouvez omettre le modificateur `public`.
}
Les classes Chien et Chat implémentent cette interface, ce qui signifie qu'elles devront fournir une implémentation concrète de la méthode shout().
Implémentation :
// Classe Dog qui implémente l'interface Animal
public class Dog implements Animal {
// ...
@Override
public void shout() {
bark(); // Pour cet exemple, aboyer est utilisé pour implémenter crier
}
// Autres méthodes spécifiques aux chiens
}
// Classe Cat qui implémente l'interface Animal
public class Cat implements Animal {
// ...
@Override
public void shout() {
meow(); // Pour cet exemple, miauler est utilisé pour implémenter crier
}
// Autres méthodes spécifiques aux chats
}
À vous de jouer
- Créez l'interface
Discount
et les classesPercentDiscount
etValueDiscount
. - Ajoutez un tableau de
Discount
à la classeTravel
.
Indice
Lorsque vous déclarez un tableau discounts
dans la classe Travel
comme étant de type Discount
, cela
signifie que discounts peut contenir n'importe quelle instance d'une classe qui implémente Discount
. Ainsi,
vous pouvez utiliser des instances de PercentDiscount
, ValueDiscount
, ou même d'autres classes qui
implémentent Discount sans modifier le code de la classe Travel
. Cela s'appelle du polymorphisme
paramétrique.
Le polymorphisme paramétrique est aussi connu sous le nom generic types ou templates.
Essayez votre code dans le Main
À vous de jouer
Dans le Main
:
- Modifiez
firstTravel
en ajoutant une réduction de 50%.
Indice
- Calculez le nouveau prix après l'application de cette réduction, affichez-le et assurez-vous qu'il est bien de 1000 euros.
- Ajoutez une nouvelle réduction de type
ValueDiscount
au tableaudiscounts
et calculez le nouveau prix après l'application de toutes les réductions. Affichez et assurez vous que le prix prend bien en compte toutes les réductions. Pensez à augmenter la taille du tableauDiscount[]
.
Récapitulatif
Bravo ! Vous avez terminé votre première initiation à Java. Pour aller plus loin, nous allons maintenant explorer les API Java pour découvrir les fonctionnalités avancées que le langage a à offrir.
Check
N'oubliez pas de Commit
votre travail !