Avant de commencer à lancer des benchmarks comme des confettis, rafraîchissons notre mémoire sur ce qui distingue ces deux concepts :
- Classes Abstraites : Le poids lourd de la POO. Peut avoir un état, des constructeurs, et des méthodes abstraites et concrètes.
- Interfaces : Le concurrent léger. Traditionnellement sans état, mais depuis Java 8, elles se sont renforcées avec des méthodes par défaut et statiques.
Voici une comparaison rapide pour nous mettre en route :
// Classe abstraite
abstract class AbstractVehicle {
protected int wheels;
public abstract void drive();
public void honk() {
System.out.println("Beep beep!");
}
}
// Interface
interface Vehicle {
void drive();
default void honk() {
System.out.println("Beep beep!");
}
}
Le Casse-tête de la Performance
Maintenant, vous vous demandez peut-être : "D'accord, ils sont différents, mais est-ce que cela compte vraiment en termes de performance ?" Eh bien, cher codeur curieux, c'est exactement ce que nous allons découvrir. Allumons JMH et voyons ce qu'il en est.
Entrée de JMH : Le Chuchoteur de Benchmark
JMH (Java Microbenchmark Harness) est notre fidèle acolyte pour cette enquête de performance. C'est comme un microscope pour le temps d'exécution de votre code, nous aidant à éviter les pièges des benchmarks naïfs.
Pour commencer avec JMH, ajoutez ceci à votre pom.xml
:
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>1.35</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>1.35</version>
</dependency>
Configuration du Benchmark
Créons un benchmark simple pour comparer l'appel de méthode sur des classes abstraites et des interfaces :
import org.openjdk.jmh.annotations.*;
import java.util.concurrent.TimeUnit;
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@State(Scope.Benchmark)
@Fork(value = 1, warmups = 2)
@Warmup(iterations = 5, time = 1)
@Measurement(iterations = 5, time = 1)
public class AbstractVsInterfaceBenchmark {
private AbstractVehicle abstractCar;
private Vehicle interfaceCar;
@Setup
public void setup() {
abstractCar = new AbstractVehicle() {
@Override
public void drive() {
// Vrooom
}
};
interfaceCar = () -> {
// Vrooom
};
}
@Benchmark
public void abstractClassMethod() {
abstractCar.drive();
}
@Benchmark
public void interfaceMethod() {
interfaceCar.drive();
}
}
Exécution du Benchmark
Maintenant, exécutons ce benchmark et voyons ce que nous obtenons. Rappelez-vous, nous mesurons le temps d'exécution moyen en nanosecondes.
# Exécuter le benchmark
mvn clean install
java -jar target/benchmarks.jar
Les Résultats Sont Là !
Après avoir exécuté le benchmark (les résultats peuvent varier en fonction de votre matériel spécifique et de votre JVM), vous pourriez voir quelque chose comme ceci :
Benchmark Mode Cnt Score Error Units
AbstractVsInterfaceBenchmark.abstractClassMethod avgt 5 2.315 ± 0.052 ns/op
AbstractVsInterfaceBenchmark.interfaceMethod avgt 5 2.302 ± 0.048 ns/op
Eh bien, eh bien, eh bien... Qu'avons-nous ici ? La différence est... roulement de tambour... pratiquement négligeable ! Les deux méthodes s'exécutent en environ 2,3 nanosecondes. C'est plus rapide que vous ne pouvez dire "optimisation prématurée" !
Qu'est-ce que Cela Signifie ?
Avant de tirer des conclusions, analysons cela :
- Les JVM modernes sont intelligentes : Grâce à la compilation JIT et à d'autres optimisations, la différence de performance entre les classes abstraites et les interfaces est devenue minime pour les appels de méthode simples.
- Ce n'est pas toujours une question de vitesse : Le choix entre classes abstraites et interfaces doit principalement être basé sur des considérations de conception, pas sur des micro-optimisations.
- Le contexte compte : Notre benchmark est très simple. Dans des scénarios réels avec des hiérarchies plus complexes ou des appels fréquents, vous pourriez voir des résultats légèrement différents.
Quand Utiliser Quoi
Donc, si la performance n'est pas le facteur décisif, comment choisir ? Voici un guide rapide :
Choisissez les Classes Abstraites Quand :
- Vous devez maintenir un état entre les méthodes
- Vous voulez fournir une implémentation de base commune pour les sous-classes
- Vous concevez des classes étroitement liées
Choisissez les Interfaces Quand :
- Vous voulez définir un contrat pour des classes non liées
- Vous avez besoin d'héritage multiple (rappelez-vous, Java n'autorise pas l'héritage multiple de classes)
- Vous concevez pour la flexibilité et l'extension future
L'Intrigue S'épaissit : Les Méthodes Par Défaut
Mais attendez, il y a plus ! Depuis Java 8, les interfaces peuvent avoir des méthodes par défaut. Voyons comment elles se comparent :
@Benchmark
public void defaultInterfaceMethod() {
interfaceCar.honk();
}
Exécuter cela avec nos benchmarks précédents pourrait montrer que les méthodes par défaut sont légèrement plus lentes que les méthodes de classe abstraite, mais encore une fois, nous parlons de nanosecondes ici. La différence est peu susceptible d'avoir un impact significatif sur les applications réelles.
Conseils d'Optimisation
Bien que la micro-optimisation entre classes abstraites et interfaces ne vaille peut-être pas votre temps, voici quelques conseils généraux pour garder votre code rapide :
- Gardez-le simple : Des hiérarchies de classes trop complexes peuvent ralentir les choses. Visez un équilibre entre élégance de conception et simplicité.
- Méfiez-vous des problèmes de diamant : Avec les méthodes par défaut dans les interfaces, vous pouvez rencontrer des problèmes d'ambiguïté. Soyez explicite si nécessaire.
- Profilez, ne devinez pas : Mesurez toujours la performance dans votre cas d'utilisation spécifique. JMH est excellent, mais envisagez également des outils comme VisualVM pour une vue d'ensemble plus large.
À Retenir
En fin de compte, la différence de performance entre les classes abstraites et les interfaces n'est pas le goulot d'étranglement de votre code. Concentrez-vous sur de bons principes de conception, la lisibilité et la maintenabilité. Choisissez en fonction de vos besoins architecturaux, pas sur des nano-optimisations.
Rappelez-vous, l'optimisation prématurée est la racine de tout mal (ou du moins une bonne partie). Utilisez le bon outil pour le travail, et laissez la JVM se charger de gratter ces dernières nanosecondes.
Réflexion
Avant de partir, réfléchissez à ceci : Si nous chipotons sur des nanosecondes, résolvons-nous les bons problèmes ? Peut-être que les vrais gains de performance se trouvent dans nos algorithmes, nos requêtes de base de données ou nos appels réseau. Gardez à l'esprit la vue d'ensemble, et que votre code soit toujours performant !
Bon codage, et que vos abstractions soient toujours logiques et vos interfaces claires !