Principe

En Java, une interface est un plan, un contrat qui définit un ensemble de méthodes que toute classe l’implémentant doit obligatoirement mettre en œuvre. Ce sont les méthodes abstraites, les éléments fondamentaux du contrat. Une classe peut implémenter plusieurs interfaces, étendant ainsi ses capacités sans être liée par un héritage unique. Depuis Java 8, les interfaces ont évolué, permettant désormais de définir des méthodes avec une implémentation par défaut et des méthodes statiques.

Les interfaces sont donc des outils puissants pour la programmation orientée objet en Java, favorisant la réutilisabilité du code, la modularité et le polymorphisme. Elles offrent un moyen flexible de définir des comportements communs à différentes classes, sans contraindre leur hiérarchie d’héritage.

Exemple

Considérons l’interface Triable tel que :

interface Triable {
	public int comparer(Triable t);
}

On peut alors faire :

public void trier(List<Triable> List){
	boolean swap = true;
	int lim = list.size();
	Triable t1, t2;
	while (swap) {
		swap = false;
		for (int i = 0; i < lim - 1; i++) {
			t1 = list.get(i); t2 = list.get(2);
			if (t1.comparer(t2) > 0){
				list.set(i,   t2);
				list.set(i+1, t1);
			}
		}
		lim -= 1;
	}
}

C’est l’algorithme BubbleSort

Si on veut trier des bananes, on peut alors déclarer Banane tel que :

class Banane implements Triable {
	protected int longueur;
 
	public Banane(int longueur){ this.longueur = longueur; }
 
	public int comparer(Triable t){
		if (t instanceof Banane){
			Banane b = (Banane) t;
			return (longueur - b.longueur);
		}
		return 0;
	}
}

De nombreuses interfaces existent déjà pour toutes les collections et l’interface Comparable pour trier.

Les méthodes par défaut

Disponible avec Java8+

On peut définir des méthodes par défaut, telles que :

interface Vehicle {
	// Méthode abstraite
	void start();
 
	// Méthode par défaut
	default void honk(){
		System.out.println("Beep! Beep!");
	}
}

On peut alors déclarer une class tel que :

class Car implements Vehicle {
	@Override
	public void start(){
		System.out.println("Car started");
	}
}

On peut alors ne pas définir honk car ce n’est pas une méthode abstraite, son implémentation sera alors celle déclarée dans l’interface (le default).

Si on définit Bike tel que :

class Bike implements Vehicle {
	@Override
	public void start(){
		System.out.println("Bike started");
	}
}

On peut alors écrire un code de ce style :

Vehicle car  = new Car();
Vehicle bike = new Bike();
 
car.start(); // "Car started"
car.honk(); // "Beep! Beep!" car hérité de default
 
bike.start(); // "Bike started"
bike.honk(); // "Beep! Beep!" car hérité de default

Méthodes statiques et privées

Les méthodes statiques dans les interfaces sont supportées depuis Java 8

Les méthodes privées dans les interfaces sont supportées depuis Java 9

interface Vehicle {
	void start();
	static void info(){
		System.out.println("Interface vehicle");
	}
 
	private void helper(){
		System.out.println("Private helper method");
	}
}

Les interfaces fonctionnelles

Les interfaces fonctionnelles possèdent plusieurs caractéristiques :

  • interface ne comportant qu’une seule mĂ©thode abstraite
  • Peut comporter d’autres mĂ©thodes : par dĂ©faut, privĂ©es et statiques.
  • Il est recommandĂ© d’annoter toute IF (interface fonctionnelle) avec @FunctionalInterface. Le compilateur pourra ainsi effectuer des vĂ©rifications utiles.
  • Les IF s’utilisent principalement avec les Expressions lambda et les RĂ©fĂ©rences de mĂ©thodes

Exemple

@FunctionalInterface
interface Calculator {
	int calculate(int a, int b);
 
	default void printResult(int result){
		System.out.println("Result: " + result);
	}
 
	static void info(){
		System.out.println("This is a functional interface for calculation.");
	}
}

Interfaces fonctionnelles natives

On retrouve plusieurs interfaces natives :

  • Les Function: traitements acceptant un ou plusieurs paramètres et renvoyant un rĂ©sultat. Elles dĂ©clarent des mĂ©thodes T apply(..., ...)
  • Les Consumer: acceptent un ou plusieurs paramètres, mais ne renvoyant rien. Elles dĂ©clarent des mĂ©thodes du type void accept(..., ...)
  • Les Predicate: acceptent un ou plusieurs paramètres et renvoient un boolĂ©en. Elles dĂ©clarent des mĂ©thodes du type boolean test(..., ...)
  • Les Supplier: n’acceptent pas de paramètre, mais renvoient un rĂ©sultat. DĂ©clarent des mĂ©thodes du type T get()

Par exemple :

  1. BiFunction<T,U,R> déclare R apply(T t, R r)
  2. IntToDoubleFunction déclare double applyAsDouble(int i)
  3. DoubleConsumer déclare void accepte(Double d)
  4. BiPredicate<T,U> déclare boolean test(T t, U u)
  5. Supplier<T> déclare T get()

Il existe beaucoup d'interfaces fonctionnelles intégrées dans Java.

En général, l’interface va être réécrite plutôt que de passer des heures à chercher quelle interface convient.

Avec les expressions Lambda

Voir Expressions lambda

Si on définit une interface Doctor

@FunctionalInterface
public interface Doctor {
	void soigner(Humain h);
 
	default String examiner(Humain h){
		if (h.age > h.esperanceVie) return "mort";
		return "vivant";
	}
 
	default void soigner(Humain h){
		// ...
	}
}

Et une class Population

class Population {
	List<Humain> pop;
 
	// ...
 
	public void traiter(Doctor doc){
		for (Humain h: pop){
			if ("vivant".equals(doc.examiner(h))) doc.soigner(h);
		}
	}
}

On peut alors écrire ce code :

class Life {
	public static void main(String[] args){
		Population pop = new Population(...);
 
		Doctor badDoctor = h -> h.espereanceVie -= 10;
		pop.traiter(badDoctor);
 
		pop.traiter(h -> h.esperanceVie += 5);
	}
}

Cela nous permet de ne pas devoir définir une autre fonction tel que:

void traiter(int t){ esperanceVie += t; }

Les interfaces fonctionnelles et méthodes lambda nous donnent de la souplesse