Java: Pattern matching voor Switch in Java 21


calendar icon

12 december 2023

|
calendar icon 8 minuten

Pattern matches voor Switch is een van de features die in Java 21 wordt uitgerold. Het idee van pattern matches borrelt al sinds Java 12, maar wordt nu in al zijn kracht uitgebracht. In onze blog laten we aan de hand van voorbeelden zien hoe krachtig pattern matches zijn en hoe jij pattern matches kan gebruiken om je code simpeler te maken. Klinkt goed toch?

Waarom pattern matching je leven makkelijker maakt?

Pattern matching helpt developers om conditional logic op een expressieve en leesbare manier te programmeren. Door het verminderen van boilerplate-code wordt de code compacter en makkelijker te begrijpen. Het zorgt er ook voor dat developers null pointers naadloos kunnen afhandelen. En verschillende patronen in 1 case worden afgehandeld. Een krachtige aanvulling die het leven van de ontwikkelaar eenvoudiger kan maken.

Hoe werkt type matching?

Om type matching beter te begrijpen, kunnen we een vergelijking maken tussen de oude/huidige manier van werken en hoe je dit kunt verbeteren met met pattern matching. Stel, je hebt een stuk code dat verschillende objecttypen moet controleren en vervolgens specifieke acties moet ondernemen:

if (unknownExample instanceof String) {
    String s = (String) unknownExample;
    // Perform actions on String
} else if (unknownExample instanceof Integer) {
    Integer i = (Integer) unknownExample;
    // Perform actions on Integer
} else {
    // Handle other actions
}



In dit voorbeeld zie je dat we eerst het objecttype moeten controleren met behulp van instanceof en vervolgens moeten casten naar het juiste type om de gewenste acties uit te voeren. Dit kan leiden tot code duplication en slechte leesbaarheid. Met pattern matching kunnen we dezelfde situatie op een meer gestructureerde en leesbare manier aanpakken:

Object example = switch (unknownExample) {
    case String s -> s + " het object is van het type String";
    case Integer i -> i + 50;
    default -> "Onverwachte waarde";
};



In dit geval behandelt de Switch-expressie de verschillende objecttypen direct. Dit zorgt niet alleen voor minder code, maar de code is ook intuïtiever en gemakkelijker te begrijpen. Pattern matching zorgt ervoor dat je verschillende cases kunt afhandelen op basis van het type van het object, ongeacht de daadwerkelijke waarde ervan. Als geen van de cases overeenkomt, wordt de default-case uitgevoerd.

Null pointers

Met pattern matching kan je als developer ook eenvoudiger null pointers afhandelen. Voorheen moesten null pointers buiten de switch worden afgehandeld, waardoor je code er dan als volgt uit ziet:

if (object != null) {
    // Perform operations on object
} else {
    // Handle null case
}



Pattern matching stelt ons in staat om null pointers te behandelen binnen de switch, waardoor we de hoeveelheid code verminderen en de logica vereenvoudigen. Het is echter cruciaal om de null pointer expliciet te behandelen, aangezien deze niet automatisch in de default case wordt afgehandeld. Met het volgende voorbeeld handelen we een null pointer af mat een case:

Object example = switch (unknownExample) {
    case String s -> s + "The object if of type String";
    case Integer i -> i + 50;
    case null -> "The object is null"
    default -> "Unexpected value";
};



Specifieke matches met pattern matching

Een ander voordeel van pattern matching is dat je op eenvoudige wijze kunt omgaan met specifieke voorwaarden en constante waarden. Voorheen moesten voorwaarden en constanten buiten de switch worden afgehandeld, waardoor je code er als volgt uitziet:

if (value > 0) {
    // Perform operations when value is positive
} else if (value < 0) {
    // Perform operations when value is negative
} else {
    // Perform operations when value is zero
}



Met pattern matching en specifieke gevalspatronen (guarded case labels) kunnen we deze voorwaarden en constanten direct binnen de switch verwerken! Ook dit draagt weer bij aan een code die beter is gestructureerd en biedt een vereenvoudige logica. Hieronder een voorbeeld van hoe je dit kunt doen:

int value = -5;
String result = switch (value) {
    case int n when n > 0 -> "Value is positive";
    case int n when n < 0 -> "Value is negative";
    case int n when n == 0 -> "Value is zero";
    default -> "Unexpected value";
};



In dit voorbeeld gebruiken we de "guarded" case labels (met het when-gedeelte) om specifieke voorwaarden te controleren en de juiste acties uit te voeren. Dit zorgt voor een leesbare en onderhoudbare code.

Type Casting en Downcasting

Met pattern matching kun je ook eenvoudig type casten en downcasten. Voorheen moesten dergelijke conversies buiten de switch worden afgehandeld, wat je code onoverzichtelijk kon maken. Hier is een voorbeeld van hoe dat eruit zou zien:

if (shape instanceof Circle) {
    Circle circle = (Circle) shape;
    double radius = circle.getRadius();
    // Perform action on Circle-object
} else if (shape instanceof Rectangle) {
    Rectangle rectangle = (Rectangle) shape;
    double width = rectangle.getWidth();
    double height = rectangle.getHeight();
    // Perform action on Rectangle-object
} else {
    // Handle other actions
}



Met pattern matching en type casting kunnen we deze conversies direct binnen de switch uitvoeren. Hieronder een voorbeeld van hoe je dit kan doen:

Object shape = new Circle();
String result = switch (shape) {
    case Circle c -> {
        double radius = c.getRadius();
        yield "Circle with radius " + radius;
    }
    case Rectangle r -> {
        double width = r.getWidth();
        double height = r.getHeight();
        yield "Rectangle with width " + width + " and height " + height;
    }
    default -> "Unknown shape";
};



In dit voorbeeld zien we dat het type casting binnen elk geval wordt gedaan op basis van het exacte subtype van shape. Het aantal cast-operaties buiten de switch wordt verminderd.

Decompositie van Datastructuren

Met pattern matching kun ook complexe datastructuren, zoals tuples of records, op een gestructureerde manier ontleden en verwerken. Wanneer je hier geen pattern matching voor gebruikt, moet je dit buiten de switch afhandelen, wat de leesbaarheid van de code kan belemmeren. Hier is een voorbeeld van hoe dat eruit zou zien:

if (data instanceof Pair) {
    Pair<Integer, String> pair = (Pair<Integer, String>) data;
    int value = pair.getFirst();
    String text = pair.getSecond();
    // Perform action on the disected value
} else if (data instanceof Triple) {
    Triple<Integer, Double, String> triple = (Triple<Integer, Double, String>) data;
    int value = triple.getFirst();
    double number = triple.getSecond();
    String text = triple.getThird();
    // Perform action on the disected value
} else {
    // Handle other actions
}



Gelukkig hebben we pattern matching! Dit biedt de mogelijkheid om complexe datastructuren te ontleden, en kunnen we deze ontledingen direct binnen de switch uitvoeren. Het resultaat? een overzichtelijke en compact code! Hieronder weer een voorbeeld van hoe je dit kan doen:

Object data = new Pair<>(42, "Hello");
String result = switch (data) {
    case Pair p(int value, String text) -> {
        // Perform action on the disected value
        yield "Value: " + value + ", Text: " + text;
    }
    case Triple t(int value, double number, String text) -> {
        // Perform action on the disected value
        yield "Value: " + value + ", Number: " + number + ", Text: " + text;
    }
    default -> "Unknown data structure";
};



In dit voorbeeld worden ingewikkelde datastructuren zoals Pair en Triple direct in elk switch-geval uitgepakt. Dit resulteert in een code die minder herhaling heeft en gemakkelijker te begrijpen is, terwijl de ontleding van de waarden op een georganiseerde manier wordt uitgevoerd.

Onze conlusie: pattern matching is een krachtige taalconstructie

Pattern matching is een krachtige taalconstructie die ervoor zorgt dat developers complexe logica op een expressieve en leesbare manier kunnen programmeren. Het biedt verschillende voordelen die bijdragen aan het vereenvoudigen en verbeteren van code, en daarmee het leven van de developer een stuk makkelijker kan maakt 😉.

Hieronder nog even de voordelen op een rijtje:

  • Expressieve Logica: Pattern matching stelt je in staat om logica uit te drukken op een manier die lijkt op natuurlijke (lees:begrijpelijke) taal. Het leidt tot code die dichter bij de intentie van de ontwikkelaar staat en gemakkelijker te begrijpen is.
  • Boilerplate Verminderen: Minder boilerplate zorgt voor een kortere en meer gestructureerde code. Het verminderd herhaling en overbodige conditionele checks.
  • Elegante Null Pointer Handling: Pattern matching biedt een geïntegreerde manier om null pointers af te handelen, waardoor de kans op null pointer exceptions wordt verminderd. Je kunt null cases rechtstreeks binnen de switch behandelen.
  • Meerdere Patronen in 1 Case: Ontwikkelaars kunnen verschillende patronen in één case combineren. Dit zorgt voor een compactere en efficiëntere code.
  • Leesbare Specifieke Matches: Het gebruik van guarded case labels maakt het mogelijk om specifieke voorwaarden en constanten direct binnen de switch te verwerken. Je raadt het al: ook dit draagt bij aan een eendoudigere en gestructureerde logica!
  • Efficiënte Type Conversies: Pattern matching vereenvoudigt type casting en downcasting binnen de switch, waardoor je gemakkelijk en veilig met verschillende typen kan werken.
  • Ontleding van Datastructuren: Complexere datastructuren, zoals tuples, kunnen direct binnen de switch worden ontleden. Helpt jou weer met een overzichtelijke en gestructureerde code te schrijven.

Pattern matching is een krachtig instrument om logica te implementeren en complexe scenario's af te handelen. Code is niet alleen efficiënter, maar ook makkelijker te onderhouden en uit te breiden, en daarmee wordt de ontwikkelervaring aanzienlijk verbetert.

Deel:

Recente blogs