cours
OOP en Java : Classes, objets, encapsulation, héritage et abstraction
Java est régulièrement l'un des trois langages les plus populaires au monde. Son adoption dans des domaines tels que le développement de logiciels d'entreprise, les applications mobiles Android et les applications web à grande échelle est inégalée. Son système de type solide, son écosystème étendu et sa capacité à "écrire une fois, exécuter n'importe où" le rendent particulièrement attrayant pour la construction de systèmes robustes et évolutifs. Dans cet article, nous verrons comment les fonctions de programmation orientée objet de Java permettent aux développeurs d'exploiter efficacement ces capacités, en leur permettant de créer des applications maintenables et évolutives grâce à une organisation et une réutilisation appropriées du code.
Note sur l'organisation et l'exécution du code Java
Avant de commencer à écrire du code, procédons à quelques réglages.
Comme pour sa syntaxe, Java a des règles strictes en matière d'organisation du code.
Tout d'abord, chaque classe publique doit se trouver dans son propre fichier, nommé exactement comme la classe mais avec une extension.java
. Ainsi, si je veux écrire une classe d'ordinateur portable , le nom de fichier doit être Laptop.java
-en respectant la casse. Vous pouvez avoir des classes non publiques dans le même fichier, mais il est préférable de les séparer. Je sais que nous prenons de l'avance - nous parlons de l'organisation des cours avant même de les écrire - mais il est bon d'avoir une idée approximative de l'endroit où placer les choses à l'avance.
Tous les projets Java doivent comporter un fichier Main.java
avec la classeMain. C'est ici que vous testez vos classes en créant des objets à partir d'elles.
Pour exécuter du code Java, nous utiliserons IntelliJ IDEAun IDE Java très répandu. Après avoir installé IntelliJ :
- Créez un nouveau projet Java (Fichier > Nouveau > Projet).
- Cliquez avec le bouton droit de la souris sur le dossier src pour créer le fichier
Main.java
et collez le contenu suivant :
public class Main {
public static void main(String[] args) {
// Create and tests objects here
}
}
Lorsque nous parlons de classes, nous écrivons du code dans d'autres fichiers que le fichierMain.java
. Mais s'il s'agit de créer et de tester desobjets , nous passons à Main.java
.
Pour exécuter le programme, vous pouvez cliquer sur le bouton vert de lecture situé à côté de la méthode principale :
La sortie sera affichée dans la fenêtre de l'outil Exécuter en bas.
Si vous êtes totalement novice en matière de Java, veuillez consulter notre cours d'introduction à Java. Cours d'introduction à Javaqui couvre les principes fondamentaux des types de données et du flux de contrôle Java avant de poursuivre.
Sinon, plongeons dans le vif du sujet.
Classes et objets Java
Qu'est-ce qu'une classe, exactement ?
Les classes sont des constructions de programmation en Java qui permettent de représenter des concepts du monde réel. Par exemple, considérez cette classe MenuItem (créez un fichier pour écrire cette classe dans votre IDE) :
public class MenuItem {
public String name;
public double price;
}
La classe nous donne un plan ou un modèle pour représenter les différents éléments du menu d'un restaurant. En modifiant les deux attributs de la classe, name
, et price
, nous pouvons créer d'innombrables objets de menu comme un hamburger ou une salade.
Ainsi, pour créer une classe en Java, vous commencez par une ligne décrivant le niveau d'accès de la classe (private
,public
, ou protected
) suivie du nom de la classe. Immédiatement après la mise entre parenthèses, vous décrivez les attributs de votre classe.
Mais comment créer des objets appartenant à cette classe ? Java permet cela grâce à des méthodes de construction :
public class MenuItem {
public String name;
public double price;
// Constructor
public MenuItem(String name, double price) {
this.name = name;
this.price = price;
}
}
Un constructeur est une méthode spéciale qui est appelée lorsque nous créons un nouvel objet à partir d'une classe. Il initialise les attributs de l'objet avec les valeurs que nous avons fournies. Dans l'exemple ci-dessus, le constructeur prend un nom et un prix en paramètre et les affecte aux champs de l'objet en utilisant le mot-clé "this" pour faire référence à une future instance de l'objet.
La syntaxe du constructeur est différente de celle des autres méthodes de classe car elle ne nécessite pas de spécifier un type de retour. En outre, le constructeur doit porter le même nom que la classe et doit avoir le même nombre d'attributs que celui que vous avez déclaré après la définition de la classe. Ci-dessus, le constructeur crée deux attributs parce que nous en avons déclaré deux après la définition de la classe : name
et price
.
Après avoir écrit votre classe et son constructeur, vous pouvez créer des instances (objets) dans votre méthode principale :
public class Main {
public static void main(String[] args) {
// Create objects here
MenuItem burger = new MenuItem("Burger", 3.5);
MenuItem salad = new MenuItem("Salad", 2.5);
System.out.println(burger.name + ", " + burger.price);
}
}
Sortie :
Burger, 3.5
Ci-dessus, nous créons deux objets MenuItem
dans les variables burger
et salad
. Comme l'exige Java, le type de la variable doit être déclaré, à savoir MenuItem
. Ensuite, pour créer une instance de notre classe, nous écrivons le mot-clé new suivi de l'invocation de la méthode du constructeur.
Outre le constructeur, vous pouvez créer des méthodes ordinaires qui confèrent un comportement à votre classe. Par exemple, ci-dessous, nous ajoutons une méthode pour calculer le prix total après taxes :
public class MenuItem {
public String name;
public double price;
// Constructor
public MenuItem(String name, double price) {
this.name = name;
this.price = price;
}
// Method to calculate price after tax
public double getPriceAfterTax() {
double taxRate = 0.08; // 8% tax rate
return price + (price * taxRate);
}
}
Nous pouvons maintenant calculer le prix total, taxes comprises :
public class Main {
public static void main(String[] args) {
MenuItem burger = new MenuItem("Burger", 3.5);
System.out.println("Price after tax: $" + burger.getPriceAfterTax());
}
}
Sortie :
Price after tax: $3.78
Encapsulation
L'objectif des classes est de fournir un schéma directeur pour la création d'objets. Ces objets seront ensuite utilisés par d'autres scripts ou programmes. Par exemple, nos objets MenuItem
peuvent être utilisés par une interface utilisateur qui affiche leur nom, leur prix et leur image à l'écran.
C'est pourquoi nous devons concevoir nos classes de manière à ce que leurs instances ne puissent être utilisées que de la manière que nous avons prévue. Pour l'instant, notre classe MenuItem
est très basique et sujette aux erreurs. Une personne peut créer des objets aux attributs ridicules, comme une tarte aux pommes à prix réduit ou un sandwich à un million de dollars :
// Inside Main.java
MenuItem applePie = new MenuItem("Apple Pie", -5.99); // Negative price!
MenuItem sandwich = new MenuItem("Sandwich", 1000000); // Unreasonably expensive
System.out.println("Apple pie price: $" + applePie.price);
System.out.println("Sandwich price: $" + sandwich.price);
Ainsi, la première chose à faire après avoir écrit une classe est de protéger ses attributs en limitant la façon dont ils sont créés et accédés. Pour commencer, nous voulons n'autoriser que des valeurs positives pour leprix et fixer une valeur maximale pour éviter d'afficher accidentellement des articles ridiculement chers.
Java nous permet d'accomplir cette tâche en utilisant des méthodes de type "setter" :
public class MenuItem {
private String name;
private double price;
private static final double MAX_PRICE = 100.0;
public MenuItem(String name, double price) {
this.name = name;
setPrice(price);
}
public void setPrice(double price) {
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
if (price > MAX_PRICE) {
throw new IllegalArgumentException("Price cannot exceed $" + MAX_PRICE);
}
this.price = price;
}
}
Examinons les nouveautés du bloc de code ci-dessus :
1. Nous avons rendu les attributs privés en ajoutant le mot-clé private
. Cela signifie qu'ils ne sont accessibles qu'à l'intérieur de la classe MenuItem. L'encapsulation commence par cette étape cruciale.
2. Nous avons ajouté une nouvelle constante: MAX_PRICE
:
- privé (accessible uniquement au sein de la classe)
- statique (partagé par toutes les instances)
- final (ne peut être modifié après l'initialisation)
- fixé à 100,0 $ comme prix maximum raisonnable
3. Nous avons ajouté une méthode setPrice()
qui :
- Prend un paramètre de prix
- Valide que le prix n'est pas négatif
- Valide que le prix ne dépasse pas MAX_PRICE
- Lance une exception IllegalArgumentException avec des messages descriptifs en cas d'échec de la validation.
- Le prix n'est fixé que si toutes les validations sont réussies
4. Nous avons modifié le constructeur pour utiliser setPrice()
au lieu d'assigner directement le prix. Cela permet de s'assurer que la validation des prix a lieu lors de la création de l'objet.
Nous venons de mettre en œuvre l'un des principaux piliers d'une bonne conception orientée objet - l'encapsulation. Ce paradigme permet de masquer les données et de contrôler l'accès aux attributs des objets, en garantissant que les détails de l'implémentation interne sont protégés des interférences externes et ne peuvent être modifiés qu'au moyen d'interfaces bien définies.
Faisons valoir notre point de vue en appliquant l'encapsulation à l'attributname. Imaginez un café qui ne sert que des lattes, des cappuccinos, des espressos, des americanos et des mokas.
Ainsi, les noms de nos éléments de menu ne peuvent être que l'un des éléments de cette liste. Voici comment nous pouvons appliquer cette règle dans le code :
// Rest of the class here
...
private static final String[] VALID_NAMES = {"latte", "cappuccino", "espresso", "americano", "mocha"};
private String name;
public void setName(String name) {
String lowercaseName = name.toLowerCase();
for (String validName : VALID_NAMES) {
if (validName.equals(lowercaseName)) {
this.name = name;
return;
}
}
throw new IllegalArgumentException("Invalid drink name. Must be one of: " + String.join(", ", VALID_NAMES));
}
Le code ci-dessus met en œuvre la validation des noms pour les éléments du menu d'un café. Voyons cela en détail :
1. Tout d'abord, il définit un tableau final statique privé VALID_NAMES
qui contient les seuls noms de boissons autorisés : latte, cappuccino, espresso, americano et mocha. Ce tableau étant :
- privé : accessible uniquement au sein de la classe
- statique : partagé par toutes les instances
- final : ne peut être modifié après l'initialisation
2. Il déclare un champ privé String name pour stocker le nom de la boisson.
3. La méthode setName()
met en œuvre la logique de validation :
- Prend en paramètre un nom de type chaîne
- Le convertit en minuscules pour rendre la comparaison insensible à la casse.
- Boucle sur le tableau
VALID_NAMES
- Si une correspondance est trouvée, le nom est défini et le message est renvoyé.
- Si aucune correspondance n'est trouvée, une exception de type IllegalArgumentException est levée avec un message descriptif énumérant toutes les options valides.
Voici la classe complète jusqu'à présent :
public class MenuItem {
private String name;
private double price;
private static final String[] VALID_NAMES = {"latte", "cappuccino", "espresso", "americano", "mocha"};
private static final double MAX_PRICE = 100.0;
public MenuItem(String name, double price) {
setName(name);
setPrice(price);
}
public void setName(String name) {
String lowercaseName = name.toLowerCase();
for (String validName : VALID_NAMES) {
if (validName.equals(lowercaseName)) {
this.name = name;
return;
}
}
throw new IllegalArgumentException("Invalid drink name. Must be one of: " + String.join(", ", VALID_NAMES));
}
public void setPrice(double price) {
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
if (price > MAX_PRICE) {
throw new IllegalArgumentException("Price cannot exceed $" + MAX_PRICE);
}
this.price = price;
}
}
Après avoir protégé la façon dont les attributs sont créés, nous voulons également protéger la façon dont on y accède. Pour ce faire, vous pouvez utiliser des méthodes de type "getter" :
public class MenuItem {
// Rest of the code here
...
public String getName() {
return name;
}
public double getPrice() {
return price;
}
}
Les méthodes Getter fournissent un accès contrôlé aux attributs privés d'une classe. Ils résolvent le problème de l'accès direct aux attributs, qui peut entraîner des modifications non souhaitées et rompre l'encapsulation.
Par exemple, sans getters, nous pourrions accéder directement aux attributs :
MenuItem item = new MenuItem("Latte", 4.99);
String name = item.name; // Direct access to attribute
item.name = "INVALID"; // Can modify directly, bypassing validation
Avec les getters, nous assurons un accès approprié :
MenuItem item = new MenuItem("Latte", 4.99);
String name = item.getName(); // Controlled access through getter
// item.name = "INVALID"; // Not allowed - must use setName() which validates
Cette encapsulation :
- Protège l'intégrité des données en empêchant les modifications non valides
- Permet de modifier l'implémentation interne sans affecter le code qui utilise la classe
- Fournit un point d'accès unique qui peut inclure une logique supplémentaire si nécessaire
- rend le code plus facile à maintenir et moins sujet aux bogues
Héritage
Notre classe commence à avoir fière allure, mais elle présente de nombreux problèmes. Par exemple, pour un grand restaurant servant de nombreux types de plats et de boissons, la classe n'est pas assez flexible.
Si nous voulons ajouter différents types de produits alimentaires, nous nous heurterons à plusieurs difficultés. Certains plats peuvent être préparés pour être emportés, tandis que d'autres doivent être consommés immédiatement. Les prix et les remises peuvent varier d'un produit à l'autre. Les plats peuvent nécessiter un suivi de la température ou un stockage spécial. Les boissons peuvent être chaudes ou froides avec des ingrédients personnalisables. Les articles peuvent nécessiter des informations sur les allergènes et des options de portions. Le système actuel ne permet pas de répondre à ces différentes exigences.
L'héritage offre une solution élégante à tous ces problèmes. Il nous permet de créer des versions spécialisées d'éléments de menu en définissant une classe de base MenuItem
avec des attributs communs, puis en créant des classes enfantines qui héritent de ces éléments de base tout en ajoutant des caractéristiques uniques.
Par exemple, nous pourrions avoir une classe Drink
pour les boissons avec des options de température, une classe Food
pour les articles nécessitant une consommation immédiate et une classe Dessert
pour les articles ayant des besoins de stockage particuliers - toutes ces classes héritant de la fonctionnalité principale des articles de menu.
Extension des classes
Mettons en œuvre ces idées en commençant par Drink
:
public class Drink extends MenuItem {
private boolean isCold;
public Drink(String name, double price, boolean isCold) {
this.name = name;
this.price = price;
this.isCold = isCold;
}
public boolean getIsCold() {
return isCold;
}
public void setIsCold(boolean isCold) {
this.isCold = isCold;
}
}
Pour définir une classe enfant qui hérite d'une classe parent, nous utilisons le mot-cléextends
après le nom de la classe enfant, suivi de celui de la classe parent. Après la définition de la classe, nous définissons les nouveaux attributs de cet enfant et implémentons son constructeur.
Mais remarquez que nous devons répéter l'initialisation de name
et price
avec isCold
. Ce n'est pas idéal car la classe mère peut avoir des centaines d'attributs. En outre, le code ci-dessus provoquera une erreur lorsque vous le compilerez, car ce n'est pas la bonne façon d'initialiser les attributs de la classe parente. La bonne méthode consiste à utiliser le mot-clé super
:
public class Drink extends MenuItem {
private boolean isCold;
public Drink(String name, double price, boolean isCold) {
super(name, price);
this.isCold = isCold;
}
public boolean getIsCold() {
return isCold;
}
public void setIsCold(boolean isCold) {
this.isCold = isCold;
}
}
Le mot-clésuper
est utilisé pour appeler le constructeur de la classe mère. Dans ce cas, super(name, price)
appelle le constructeur deMenuItem
pour initialiser ces attributs, évitant ainsi la duplication du code. Il suffit d'initialiser le nouvel attribut isCold
spécifique à la classeDrink
.
Ce mot-clé est très flexible car vous pouvez l'utiliser pour faire référence à la classe mère dans n'importe quelle partie de la classe enfant. Par exemple, pour appeler une méthode parent, vous utilisez super.methodName()
, tandis que super.attributeName
est destiné aux attributs.
Remise en cause de la méthode
Supposons maintenant que nous voulions ajouter une nouvelle méthode à nos classes pour calculer le prix total après taxes. Étant donné que les taux d'imposition peuvent varier d'un article de menu à l'autre (par exemple, aliments préparés ou boissons emballées), nous pouvons utiliser la méthode de superposition pour mettre en œuvre des calculs d'impôts spécifiques dans chaque classe enfant tout en conservant un nom de méthode commun dans la classe mère.
Voici à quoi cela ressemble :
public class MenuItem {
// Rest of the MenuItem class
public double calculateTotalPrice() {
// Default tax rate of 10%
return price * 1.10;
}
}
public class Food extends MenuItem {
private boolean isVegetarian;
public Food(String name, double price, boolean isVegetarian) {
super(name, price);
this.isVegetarian = isVegetarian;
}
@Override
public double calculateTotalPrice() {
// Food has 15% tax
return super.getPrice() * 1.15;
}
}
public class Drink extends MenuItem {
private boolean isCold;
public Drink(String name, double price, boolean isCold) {
super(name, price);
this.isCold = isCold;
}
@Override
public double calculateTotalPrice() {
// Drinks have 8% tax
return super.getPrice() * 1.08;
}
}
Dans cet exemple, la superposition de méthodes permet à chaque sous-classe de fournir sa propre implémentation de la méthode calculateTotalPrice()
:
La classe de base MenuItem
définit un calcul de taxe par défaut de 10 %.
Quand Food
et Drink
étendent MenuItem
ils surchargent cette méthode pour mettre en œuvre leurs propres taux d'imposition :
- Les produits alimentaires sont soumis à un taux d'imposition plus élevé de 15 %.
- Les boissons bénéficient d'un taux d'imposition inférieur de 8 %.
L'annotation@Override
est utilisée pour indiquer explicitement que ces méthodes remplacent la méthode de la classe mère. Cela permet de détecter les erreurs si la signature de la méthode ne correspond pas à celle de la classe mère.
Chaque sous-classe peut toujours accéder au prix de la classe parente en utilisant super.getPrice()
démontrant ainsi que les méthodes surchargées peuvent utiliser les fonctionnalités de la classe parente tout en ajoutant leur propre comportement.
En bref, la superposition de méthodes fait partie intégrante de l'héritage et permet aux sous-classes de fournir leur propre implémentation des méthodes définies dans la classe mère, ce qui permet d'obtenir un comportement plus spécifique tout en conservant la même signature de méthode.
Classes abstraites
Notre hiérarchie de classes MenuItem
fonctionne, mais il y a un problème : n'importe qui devrait pouvoir créer un simple objetMenuItem
? Après tout, dans notre restaurant, chaque élément du menu est soit un plat, soit une boisson - il n'existe pas d'élément de menu générique.
Nous pouvons éviter cela en faisant de MenuItem
une classe abstraite. Une classe abstraite ne fournit qu'un modèle de base - elle ne peut être utilisée que comme classe parente pour l'héritage, et ne peut pas être instanciée directement.
Pour rendre MenuItem
abstrait, nous ajoutons le mot-clé abstract
après son modificateur d'accès :
public abstract class MenuItem {
private String name;
private double price;
public MenuItem(String name, double price) {
setName(name);
setPrice(price);
}
// Existing getters/setters remain the same
// Make this method abstract - every subclass MUST implement it
public abstract double calculateTotalPrice();
}
Les classes abstraites peuvent également avoir des méthodes abstraites comme calculateTotalPrice()
ci-dessus. Ces méthodes abstraites servent de contrats qui obligent les sous-classes à fournir leurs implémentations. En d'autres termes, toute méthode abstraite d'une classe abstraite doit être implémentée par les classes enfantines.
Réécrivons donc Food
et Drink
en tenant compte de ces changements :
public class Food extends MenuItem {
private boolean isVegetarian;
public Food(String name, double price, boolean isVegetarian) {
super(name, price);
this.isVegetarian = isVegetarian;
}
@Override
public double calculateTotalPrice() {
return getPrice() * 1.15; // 15% tax
}
}
public class Drink extends MenuItem {
private boolean hasCaffeine;
public Drink(String name, double price, boolean hasCaffeine) {
super(name, price);
this.hasCaffeine = hasCaffeine;
}
@Override
public double calculateTotalPrice() {
return getPrice() * 1.10; // 10% tax
}
}
Grâce à la mise en œuvre de ce système de menus, nous avons vu comment l'abstraction et l'héritage fonctionnent ensemble pour créer un code flexible et facile à maintenir qui peut facilement s'adapter aux différentes exigences de l'entreprise.
Conclusion
Aujourd'hui, nous avons eu un aperçu de ce dont Java est capable en tant que langage de programmation orienté objet. Nous avons couvert les bases des classes, des objets et quelques piliers de la POO : encapsulation, héritage et abstraction à travers un système de menu de restaurant.
Pour que ce système soit prêt pour la production, il vous reste encore beaucoup de choses à apprendre, comme les interfaces (qui font partie de l'abstraction), le polymorphisme et les modèles de conception OOP. Pour en savoir plus sur ces concepts, reportez-vous à notre Introduction à la POO en Java.
Si vous souhaitez tester vos connaissances en Java, essayez de répondre à certaines des questions de notre article sur les questions d'entretien en Java. notre article Questions d'entretien sur Java.
FAQ sur la POO en Java
Quels sont les pré-requis pour suivre ce tutoriel Java OOP ?
Vous devez avoir des connaissances de base en programmation Java, notamment en ce qui concerne les types de données, les variables, le flux de contrôle (if/else, boucles) et la syntaxe de base. Notre cours Introduction à Java couvre ces fondamentaux si vous avez besoin d'une remise à niveau.
Pourquoi utiliser un système de menu de restaurant pour expliquer les concepts de la POO ?
Le système de menu d'un restaurant est un exemple intuitif qui démontre les applications réelles des principes de la POO. Il montre naturellement l'héritage (différents types d'articles de menu), l'encapsulation (protection des valeurs de prix et de nom) et les interfaces (programmes de fidélisation, contrôle de la température). La plupart des gens comprennent le fonctionnement des restaurants, ce qui facilite la compréhension des concepts.
Quelle est la différence entre les classes abstraites et les interfaces en Java ?
- Une classe ne peut étendre qu'une seule classe abstraite mais mettre en œuvre plusieurs interfaces.
- Les classes abstraites peuvent avoir des champs et des constructeurs, ce qui n'est pas le cas des interfaces.
- Les classes abstraites peuvent avoir des méthodes abstraites et concrètes ; les interfaces n'ont traditionnellement que des méthodes abstraites (avant Java 8).
- Les classes abstraites fournissent une implémentation de base, tandis que les interfaces définissent un contrat.

Je suis un créateur de contenu en science des données avec plus de 2 ans d'expérience et l'un des plus grands followings sur Medium. J'aime écrire des articles détaillés sur l'IA et la ML dans un style un peu sarcastıc, car il faut bien faire quelque chose pour les rendre un peu moins ennuyeux. J'ai produit plus de 130 articles et un cours DataCamp, et un autre est en cours d'élaboration. Mon contenu a été vu par plus de 5 millions de personnes, dont 20 000 sont devenues des adeptes sur Medium et LinkedIn.
Principaux cours sur Java
cours
Introduction to Object-Oriented Programming in Java
cours
Introduction to Data Visualization with Plotly in Python
blog
2022-2023 Rapport annuel DataCamp Classrooms
blog
Q2 2023 DataCamp Donates Digest
blog
Célébration de Saghar Hazinyar : Une boursière de DataCamp Donates et une diplômée de Code to Inspire

Fereshteh Forough
4 min
blog
Nous avons fait don de bourses DataCamp Premium à un million de personnes, et ce n'est pas fini.
blog
Les 20 meilleures questions d'entretien pour les flocons de neige, à tous les niveaux

Nisha Arya Ahmed
20 min