logo le blog invivoo blanc

Zoom sur l’héritage, l’encapsulation, la surcharge et le polymorphisme

21 août 2023 | Java | 2 comments

L’avantage de la programmation orientée objet repose sur la protection d’accès et de lecture des données manipulées (Encapsulation), la factorisation et la portabilité du code (L’héritage) et la capacité de pouvoir définir des comportements spécifiques à un type d’objet sans impacter sa hiérarchie (Polymorphisme). Lors des échanges techniques, vous pourriez aborder ces thèmes, nous allons donc répondre ici aux différentes questions qui pourraient vous être posées sur le sujet.

I. En quoi consiste l’encapsulation ?

L’encapsulation consiste à définir la visibilité et l’accessibilité des propriétés et méthodes d’une classe pour mieux en maîtriser leur utilisation. Pour cela, il suffit de déclarer « private » les données à encapsuler et de définir des méthodes permettant de les lire et de les modifier : on appelle ces méthodes « getter » (pour la lecture) et « setter » (pour la modification).

Getter : méthode « public » permettant de définir la manière de lecture d’un attribut privé. Son type de retour est celui de la donnée retournée et son nom est souvent composé de « get » et du nom de l’attribut qu’elle retourne.
Setter : méthode « public » permettant de définir la manière de modification d’une donnée. Souvent, elle ne retourne rien (« void ») et prend un paramètre du même de type que la donnée à modifier. Son nom se compose de la mention « set » et du nom de l’attribut concerné.

Pour compléter la définition de la classe « Car », on peut rajouter des accesseurs permettant de lire et de modifier la marque et le matricule des voitures instanciées :

package invivoo;

public class Car {
	
	// Declaring attributes
	private String brand ;
	private String registration ;
	private int x ;
	private int y ;

	/**
	 * Default constructor to initialize coords.
	 */
	public Car(){
		x = 0 ;
		y = 0 ;
	}
	/**
	 * Another constructor to set the brand and the id number.
	 * 
	 * @param brand : car brand
	 * @param registration : registration number
	 */
	public Car(String brand, String registration){
		this() ; // call the first constructor (not mandatory)
		this.setBrand(brand); // this : to differentiate the given parameter and the instance attribute.
		this.setRegistration(registration);
	}
	/**
	 * A simple way to run the car
	 */
	public void move(){
		x++ ;
		y++ ;
	}

	// return the brand
	public String getBrand() {
		return brand;
	}

	// set the brand
	public void setBrand(String brand) {
		this.brand = brand;
	}

	// return the registration number
	public String getRegistration() {
		return registration;
	}

	// define (set) the registration number
	public void setRegistration(String registration) {
		this.registration = registration;
	}
}
package invivoo;

public class Main {

	public static void main(String[] args) {
		Car niceCar = new Car("Mercedes", "AB123YZ") ;

		// getting the brand by the getter (getBrand)
		System.out.println("Brand: " + niceCar.getBrand()); 
		
		// getting the registration number by the getter (getRegistration)
		System.out.println("Registration: " + niceCar.getRegistration()); 
		
		// modifying the registration number
		niceCar.setRegistration("AA000ZZ");
		
		// Printing again the registration number to check modification
		System.out.println("New Registration: " + niceCar.getRegistration());
		
	}
}
OUTPUT : 

Brand: Mercedes
Registration: AB123YZ
New Registration: AA000ZZ

II. Que signifie l’héritage en POO et quelle est son utilité ?

En résumé, l’héritage consiste à définir une classe par extension des mécanismes et attributs d’une autre classe. En outre, pour un ensemble d’entités du monde réel donné, cela consiste à regrouper leurs caractéristiques communes dans une classe mère de laquelle seront dérivées les classes (filles) correspondant à ces entités.

A retenir :

  • En Java, il est impossible d’hériter de deux ou plusieurs classes. Si un tel besoin se présente, on pourrait se servir des « interfaces ».
  • L’héritage se fait grâce au mot-clé « extends »,
  • L’appel d’un constructeur de la classe mère depuis une classe fille se fait avec le mot-clé « super ».
  • L’héritage permet d’éviter de répéter un code plusieurs fois et de ne pas modifier un code déjà existant dans une classe de base :
    • Exemple : Class Point à Namedpoint : un point nommé en plus de ses coordonnées et mécanismes intégrés.
  • Il existe en Java des classes non héritables, par exemple, la classe « String ».

III. Qu’est-ce que la « surcharge » en POO ?

En programmation orientée objet, la surcharge, aussi appelée « overloading », consiste à déclarer, dans une même classe, deux méthodes de même nom mais avec des sémantiques différentes :

  • Même nom de méthode,
  • Paramètres différents (soit sur le nombre ou le/les type(s)),
  • Le type de retour n’est pas pris en compte.
public class Car {

	private String brand;
	private String registration;
private int x;
	private int y;
	
	// A way to create a car  
	public car () {
		brand = BrandType._DEFAULT_BRAND;
	}
	// Another one to create a car by providing parameters  
	public Car (String brand, String registration) {
		this.brand = brand;
		this.registration = registration;
	}
}

Figure 3 – Illustration de la Surcharge du constructeur

L’aspect de la « surcharge » est retrouvé lors de la déclaration de plusieurs constructeurs dans une même classe.

Supposons maintenant que la classe « Car » dispose d’une méthode move avec un comportement standard de déplacement.

On pourrait alors définir une autre méthode de déplacement un peu plus sophistiquée en précisant l’accélération et un angle de rotation (si l’angle est nul on avance tout droit…). Notamment, en surchargeant la méthode move déjà existante.

// Following the diagonal
	public void move () {
		x++;
		y++;
	}
	
	public void move (double speed, double rotation) {
		// calculate new position with speed and rotation... 
	}

Figure 5 – Illustration de la surcharge de méthode

IV. Qu’est-ce que la « redéfinition » en POO ?

La redéfinition, aussi appelée « overriding », consiste à définir le comportement d’une méthode selon le type de l’objet qui l’invoque, i.e., elle consiste à donner une nouvelle implémentation à une méthode héritée sans changer sa signature :

  • Le même nom de méthode
  • Même type de retour
  • Même paramètre (nombre et type)
public class Vehicule {
	
	public Vehicule () {
		
	}
	
	public void describe () {
		System.out.println("Un véhicule");
	}	
}
public class Scoot extends Vehicule {

	public Scoot(){
		
	}
	
	@Override
	public void describe() {		
		System.out.println("Excatement un Scoot.");
	}
}

Figure 7 – Illustration de la redéfinition par l’héritage

Remarques :

Les méthodes (abstraites et par défaut) définies dans une interface sont aussi considérées comme des méthodes d’instance.

public interface Mobile {
	public void move() ;
}
public class Train implements Mobile {

	@Override
	public void move() {		
		System.out.println("Train : Sur les rails.");
	}
}
public class Avion implements Mobile {

	@Override
	public void move() {		
		System.out.println("Avion : Par l'air");
	}
}
import java.util.ArrayList;
import java.util.List;

public class Main {

	public static void main(String[] args) {
		
		List<Mobile> mobiles = new ArrayList<>() ;
		mobiles.add(new Avion()) ;
		mobiles.add(new Train()) ;
		
		for(Mobile m : mobiles)
			m.move(); 

	}
}
Output :
Avion : Par l'air
Train : Sur les rails.

Figure 8 – Illustration de la redéfinition en suivant un contrat

NB : une méthode redéfinie peut bel et bien être surchargée

Maintenant, vous savez donc comment répondre aux questions sur ces quatre grands principes de la POO et expliciter leur mise en œuvre en Java. Le prochain article sera un focus sur les modificateurs d’accès du langage Java qui vont nous permettre de mettre en pratique le principe de l’encapsulation.

Vous pouvez également retrouver mon premier article où j’explique le concept de POO en Java.