Skip to content

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.

Diagramme d'architecture

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 package agencymanagement, 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 :

User tom = new User(0, "Tom", "Dupont", "tom.dupont@gmail.com");

À 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 le hash 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 et hashCode. Deux utilisateurs sont différents si et seulement si leurs id 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 :

User tom = new User(0, "Tom", "Dupont", "tom.dupont@gmail.com");

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 :

public class MyClass extends ParentClass {
    // ...
}

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 et HotelOffer.
  • Ajoutez les attributs id, score, description, nbOfReviews dans Offer. 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 et nbOfStars dans RestaurentOffer.
  • Ajoutez les attributs isBreakfastIncluded et nbOfAvailableDays dans HotelOffer.
  • Faites hériter RestaurentOffer et HotelOffer de Offer.
  • Dans Main.java, créez une annonce de type HotelOffer.
Indice
public HotelOffer(...) {
    super(...); // Pour instancier les attributs de la classe parent.
                // super est utilisé pour appeler des fonctions ou accéder
                // à des attributs définis dans la classe parent.
    ...;
}
  • Créez une méthode String getDetails() dans les sous-classes RestaurantOffer et HotelOffer. Loggez le détail de la classe RestaurantOffer
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.
Indice
@Override
public boolean isValid() {
    return super.isValid() &&
    ...
}

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 :

public abstract class Offer {
    ...
}

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 classes PercentDiscount et ValueDiscount.
  • Ajoutez un tableau de Discount à la classe Travel.
Indice
public interface Discount {
    double apply(double price);
}
...
price * (1 - ((double) percent / 100)); // price * (1 - (percent / 100)) utilise une division entière 
                                        // si percent est un entier, conduisant à une troncature. 
                                        // Ajouter (double) avant percent assure une division en nombres décimaux, 
                                        // préservant les décimales dans le résultat.
...
...
price - value;
...
...
private Discount[] discounts;
...

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
// TODO: Création d'un tableau de discount de taille 1
// TODO: Ajout d'une PercentDiscout dans le tableau
// TODO: Ajout du tableau de discount dans le travel
  • Calculez le nouveau prix après l'application de cette réduction, affichez-le et assurez-vous qu'il est bien de 1000 euros.
Indice
firstTravel.getDiscounts()[0]...
  • Ajoutez une nouvelle réduction de type ValueDiscount au tableau discounts 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 tableau Discount[].
Indice

Pensez à gérer le cas ou firstTravel.getDiscounts().length est 0.

double discountedPrice = ...;
for (Discount discount : firstTravel.getDiscounts()) {
    discountedPrice = ...
}

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

.
└── model
│   ├── Travel.java
│   ├── User.java
│   ├── Discount.java
│   ├── PercentDiscount.java
│   ├── ValueDiscount.java
│   ├── Offer.java
│   ├── RestaurentOffer.java
│   └── HotelOffer.java
└── Main.java`

N'oubliez pas de Commit votre travail !