Introduction à la Programmation Orientée Objet (POO)
La Programmation Orientée Objet est un paradigme qui permet de modéliser des systèmes complexes en les organisant autour d'objets plutôt que de simples séquences d'instructions. D'un point de vue conceptuel, les objets (entités concrètes) précèdent les classes. Cependant, lors de l'exécution du code, la classe agit comme un modèle ou un plan à partir duquel les objets sont instanciés.
L'essence de la POO réside dans l'organisation du code via des classes et la structuration des données via des objets. Ce paradigme repose sur trois piliers fondamentaux :
- Encapsulation : Protéger les données internes et contrôler leur accès.
- Héritage : Réutiliser et étendre le code existant.
- Polymorphisme : Permettre à une même interface de représenter différents types sous-jacents.
Définition et Invocation des Méthodes
Une méthode en Java est définie par des modificateurs d'accès, un type de retour, un nom explicite, une liste de paramètres et éventuellement des exceptions levées. Le mot-clé return permet de renvoyer une valeur et de terminer l'exécution de la méthode, contrairement à break qui sert uniquement à sortir d'une boucle ou d'un switch.
public class MethodExample {
public static void main(String[] args) {
MethodExample instance = new MethodExample();
System.out.println(instance.generateGreeting());
System.out.println(instance.findMaximum(15, 28));
}
public String generateGreeting() {
return "Bonjour le monde";
}
public int findMaximum(int first, int second) {
return Math.max(first, second);
}
}
Méthodes statiques et non statiques
Les méthodes statiques appartiennent à la classe elle-même et sont chargées en mémoire dès le chargement de la classe. Les méthodes non statiques (d'instance) nécessitent la création d'un objet pour être invoquées.
public class Scholar {
public static void speakStatic() {
System.out.println("Message statique du chercheur.");
}
public void speakInstance() {
System.out.println("Message d'instance du chercheur.");
}
}
public class MainApp {
public static void main(String[] args) {
Scholar.speakStatic();
Scholar scholarObj = new Scholar();
scholarObj.speakInstance();
}
}
Passage par valeur et référence d'objet
En Java, les paramètres sont toujours passés par valeur. Pour les types primitifs, c'est la valeur elle-même qui est copiée. Pour les objets, c'est la valeur de la référence (l'adresse mémoire) qui est copiée, ce qui permet de modifier l'état de l'objet original.
public class ParameterPassing {
public static void main(String[] args) {
int numericValue = 5;
modifyPrimitive(numericValue);
System.out.println(numericValue); // Affiche 5
Entity record = new Entity();
record.label = "Initial";
modifyReference(record);
System.out.println(record.label); // Affiche "Modifié"
}
public static void modifyPrimitive(int value) {
value = 100;
}
public static void modifyReference(Entity obj) {
obj.label = "Modifié";
}
}
class Entity {
String label;
}
Création d'Objets et Constructeurs
L'instanciation d'une classe se fait via le mot-clé new, qui appelle implicitement un constructeur. Le constructeur doit porter exactement le même nom que la classe et ne possède aucun type de retour (pas même void). Si aucun constructeur n'est défini, le compilateur en génère un par défaut sans arguments. Dès qu'un constructeur avec arguments est créé, le constructeur par défaut doit être explicitement redéfini si on souhaite l'utiliser.
public class Citizen {
String identifier;
public Citizen() {
this.identifier = "Anonyme";
}
public Citizen(String identifier) {
this.identifier = identifier;
}
}
public class Registry {
public static void main(String[] args) {
Citizen defaultCitizen = new Citizen();
Citizen namedCitizen = new Citizen("Jean Dupont");
}
}
Les Trois Piliers de la POO
1. Encapsulation
L'encapsulation consiste à rendre les attributs d'une classe privés (private) et à fournir des méthodes publiques (getters et setters) pour interagir avec ces données, assurant ainsi un contrôle sur les modifications.
public class BankAccount {
private double balance;
public double getBalance() {
return balance;
}
public void setBalance(double balance) {
if (balance >= 0) {
this.balance = balance;
}
}
}
2. Héritage et Redéfinition
L'héritage, via le mot-clé extends, permet à une classe fille d'acquérir les propriétés et méthodes d'une classe mère. Toutes les classes héritent implicitement de la classe Object. Les membres private de la classe mère ne sont pas hérités directement.
Le mot-clé super permet de faire référence à la classe parente, tandis que this fait référence à l'instance courante. Lors de la redéfinition de méthode (override), la signature doit être identique, et la visibilité ne peut pas être réduite.
public class Vehicle {
protected String brand = "Générique";
public Vehicle() {
System.out.println("Constructeur du véhicule");
}
public void displayInfo() {
System.out.println("Véhicule de marque : " + brand);
}
}
public class Car extends Vehicle {
private String brand = "Sport";
public Car() {
super(); // Appel explicite au constructeur parent
System.out.println("Constructeur de la voiture");
}
@Override
public void displayInfo() {
System.out.println("Voiture locale : " + this.brand);
System.out.println("Marque héritée : " + super.brand);
}
}
3. Polymorphisme
Le polymorphisme permet à une référence de type parent de pointer vers un objet de type enfant. Le comportement exécuté dépend de l'objet réel en mémoire (liaison dynamique), et non du type de la référence. Cela s'applique uniquement aux méthodes d'instance redéfinies, et non aux méthodes statiques, finales ou privées.
public class PolymorphismDemo {
public static void main(String[] args) {
BaseComponent compA = new ComponentA();
compA.execute();
BaseComponent compB = new ComponentB();
compB.execute();
// Vérification de type et conversion (casting)
if (compA instanceof ComponentA) {
((ComponentA) compA).specificAction();
}
}
}
class BaseComponent {
public void execute() {
System.out.println("Exécution de base");
}
}
class ComponentA extends BaseComponent {
@Override
public void execute() {
System.out.println("Exécution spécifique à A");
}
public void specificAction() {
System.out.println("Action exclusive à A");
}
}
class ComponentB extends BaseComponent {
@Override
public void execute() {
System.out.println("Exécution spécifique à B");
}
}
Membres statiques et blocs d'initialisation
Les variables et méthodes statiques sont partagées par toutes les instances d'une classe. Les blocs d'initialisaiton statiques s'exécutent une seule fois lors du chargement de la classe, tandis que les blocs d'instance s'exécutent avant le constructeur à chaque instanciation.
public class Configuration {
static {
System.out.println("1. Bloc statique (chargement de la classe)");
}
{
System.out.println("2. Bloc d'instance (création d'objet)");
}
public Configuration() {
System.out.println("3. Constructeur");
}
}
Classes Abstraites et Interfaces
Classes Abstraites
Une classe abstraite ne peut pas être instanciée. Elle sert de modèle conceptuel et peut contenir à la fois des méthodes concrètes et des méthodes abstraites (sans implémentation). Les sous-classes doivent implémenter toutes les méthodes abstraites, sauf si elles sont également déclarées comme abstraites.
public abstract class GeometricShape {
public abstract double calculateArea();
public void printType() {
System.out.println("Ceci est une forme géométrique.");
}
}
public class Circle extends GeometricShape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
@Override
public double calculateArea() {
return Math.PI * radius * radius;
}
}
Interfaces
Une interface définit un contrat strict. Elle ne contient que des constantes (implicitement public static final) et des signatures de méthodes (implicitement public abstract). Une classe peut implémenter plusieurs interfaces via implements, contournant ainsi la limitation de l'héritage multiple de classes.
public interface DataRepository {
int MAX_RETRIES = 3;
void save(String data);
String retrieve(String id);
}
public class DatabaseRepository implements DataRepository {
@Override
public void save(String data) {
System.out.println("Sauvegarde en base : " + data);
}
@Override
public String retrieve(String id) {
return "Données pour " + id;
}
}
Classes Internes
Java permet de définir une classe à l'intérieur d'une autre. Cela permet de regrouper logiquement des classes qui ne sont utilisées qu'à un seul endroit et d'augmenter l'encapsulation.
- Classe interne membre : Définie au niveau de la classe externe, elle a accès à tous ses membres, même privés.
- Classe interne statique : N'a pas accès aux membres d'instance de la classe externe.
- Classe interne locale : Définie à l'intérieur d'une méthode.
- Classe interne anonyme : Déclarée et instanciée simultanément, souvent utilisée pour implémenter des interfaces à la volée.
public class NetworkManager {
private String serverAddress = "192.168.1.1";
// Classe interne membre
public class ConnectionHandler {
public void handle() {
System.out.println("Connexion à " + serverAddress);
}
}
public static void main(String[] args) {
NetworkManager manager = new NetworkManager();
NetworkManager.ConnectionHandler handler = manager.new ConnectionHandler();
handler.handle();
// Classe interne anonyme
Runnable backgroundTask = new Runnable() {
@Override
public void run() {
System.out.println("Tâche asynchrone exécutée");
}
};
backgroundTask.run();
}
}