Voordat we benchmarks als confetti gaan rondstrooien, laten we ons geheugen opfrissen over wat deze twee van elkaar onderscheidt:

  • Abstracte Klassen: De zwaargewicht in OOP. Kan toestand, constructors en zowel abstracte als concrete methoden hebben.
  • Interfaces: De lichtgewicht uitdager. Traditioneel zonder toestand, maar sinds Java 8 hebben ze een upgrade gekregen met standaard- en statische methoden.

Hier is een snelle vergelijking om ons op gang te helpen:


// Abstracte klasse
abstract class AbstractVehicle {
    protected int wielen;
    public abstract void rijden();
    public void toeteren() {
        System.out.println("Toet toet!");
    }
}

// Interface
interface Vehicle {
    void rijden();
    default void toeteren() {
        System.out.println("Toet toet!");
    }
}

De Prestatiepuzzel

Nu denk je misschien, "Zeker, ze zijn verschillend, maar maakt dat echt uit qua prestaties?" Nou, nieuwsgierige programmeur, dat is precies wat we hier gaan uitzoeken. Laten we JMH opstarten en kijken wat er gebeurt.

Maak kennis met JMH: De Benchmark Fluisteraar

JMH (Java Microbenchmark Harness) is onze trouwe metgezel voor dit prestatieonderzoek. Het is als een microscoop voor de uitvoeringstijd van je code, waardoor we de valkuilen van naïeve benchmarking kunnen vermijden.

Om te beginnen met JMH, voeg dit toe aan je 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>

De Benchmark Opzetten

Laten we een eenvoudige benchmark maken om methoden op abstracte klassen en interfaces te vergelijken:


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 abstractAuto;
    private Vehicle interfaceAuto;

    @Setup
    public void setup() {
        abstractAuto = new AbstractVehicle() {
            @Override
            public void rijden() {
                // Vrooom
            }
        };
        interfaceAuto = () -> {
            // Vrooom
        };
    }

    @Benchmark
    public void abstractClassMethod() {
        abstractAuto.rijden();
    }

    @Benchmark
    public void interfaceMethod() {
        interfaceAuto.rijden();
    }
}

De Benchmark Uitvoeren

Laten we nu deze benchmark uitvoeren en zien wat we krijgen. Vergeet niet, we meten de gemiddelde uitvoeringstijd in nanoseconden.


# Voer de benchmark uit
mvn clean install
java -jar target/benchmarks.jar

De Resultaten Zijn Binnen!

Na het uitvoeren van de benchmark (resultaten kunnen variëren afhankelijk van je specifieke hardware en JVM), zie je misschien iets als dit:


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

Nou, nou, nou... Wat hebben we hier? Het verschil is... tromgeroffel... praktisch verwaarloosbaar! Beide methoden worden uitgevoerd in ongeveer 2,3 nanoseconden. Dat is sneller dan je "voortijdige optimalisatie" kunt zeggen!

Wat Betekent Dit?

Voordat we conclusies trekken, laten we het eens nader bekijken:

  1. Moderne JVM's zijn slim: Dankzij JIT-compilatie en andere optimalisaties is het prestatieverschil tussen abstracte klassen en interfaces minimaal geworden voor eenvoudige methode-aanroepen.
  2. Het gaat niet altijd om snelheid: De keuze tussen abstracte klassen en interfaces moet voornamelijk gebaseerd zijn op ontwerpoverwegingen, niet op micro-optimalisaties.
  3. Context is belangrijk: Onze benchmark is super eenvoudig. In echte scenario's met complexere hiërarchieën of frequente aanroepen, kun je iets andere resultaten zien.

Wanneer Wat Te Gebruiken

Dus, als prestaties niet de doorslaggevende factor zijn, hoe kies je dan? Hier is een snelle gids:

Kies Abstracte Klassen Wanneer:

  • Je toestand over methoden heen moet behouden
  • Je een gemeenschappelijke basisimplementatie voor subklassen wilt bieden
  • Je nauw verwante klassen ontwerpt

Kies Interfaces Wanneer:

  • Je een contract voor niet-verwante klassen wilt definiëren
  • Je meervoudige overerving nodig hebt (onthoud, Java staat geen meervoudige klasse-overerving toe)
  • Je ontwerpt voor flexibiliteit en toekomstige uitbreiding

Het Plot Dikt In: Standaardmethoden

Maar wacht, er is meer! Sinds Java 8 kunnen interfaces standaardmethoden hebben. Laten we eens kijken hoe ze zich verhouden:


@Benchmark
public void defaultInterfaceMethod() {
    interfaceAuto.toeteren();
}

Het uitvoeren van dit naast onze eerdere benchmarks kan laten zien dat standaardmethoden iets trager zijn dan methoden van abstracte klassen, maar nogmaals, we hebben het over nanoseconden. Het verschil is onwaarschijnlijk om echte toepassingen significant te beïnvloeden.

Optimalisatietips

Hoewel micro-optimalisatie tussen abstracte klassen en interfaces misschien niet de moeite waard is, zijn hier enkele algemene tips om je code snel te houden:

  • Houd het simpel: Overmatig complexe klassehiërarchieën kunnen dingen vertragen. Streef naar een balans tussen elegantie en eenvoud in ontwerp.
  • Pas op voor diamantproblemen: Met standaardmethoden in interfaces kun je ambiguïteitsproblemen tegenkomen. Wees expliciet wanneer nodig.
  • Profiel, niet raden: Meet altijd prestaties in je specifieke gebruikssituatie. JMH is geweldig, maar overweeg ook tools zoals VisualVM voor een breder beeld.

De Conclusie

Uiteindelijk is het prestatieverschil tussen abstracte klassen en interfaces niet de bottleneck van je code. Focus op goede ontwerpprincipes, leesbaarheid en onderhoudbaarheid. Kies op basis van je architecturale behoeften, niet op nano-optimalisaties.

Onthoud, voortijdige optimalisatie is de wortel van alle kwaad (of in ieder geval een groot deel ervan). Gebruik het juiste gereedschap voor de klus en laat de JVM zich zorgen maken over het uitpersen van die laatste paar nanoseconden.

Stof Tot Nadenken

Voordat je gaat, overweeg dit: Als we ons druk maken over nanoseconden, lossen we dan de juiste problemen op? Misschien wachten de echte prestatieverbeteringen in onze algoritmen, databasequery's of netwerkoproepen. Houd het grotere geheel in gedachten, en moge je code altijd performant zijn!

Veel programmeerplezier, en moge je abstracties altijd logisch zijn en je interfaces helder!