Java er et konservativt språk som verdsetter bakoverkompatibilitet over fancy syntaks. Vi programmerere elsker å velte oss i skinnende nye features, men om du har drevet med utvikling en stund, så vet du også å sette pris på stabilitet. Java 21 ble akkurat sluppet, og da er det verdt å ta en kikk på hvordan språket har utviklet seg i det siste.
¶Records i Java
Hvis du har programmert Java før, så har du helt sikkert fått med deg at records
er blitt en del av språket allerede.
Andre språk har hatt lignende funksjonalitet lenge, så det var på høy tid da det landet i Java 16 i 2021.
Med records
så slipper du å skrive boilerplate som constructor
, gettere, equals
, hashcode
og toString
selv.
I tillegg så er records
immutable, det vil si du kan ikke endre på verdiene når de først er satt.
Immutable?
Hva betyr det i praksis? La oss opprette en record
, og prøve å endre på et felt.
record Hund(String navn, int alder, List<String> venner) {}
var fido = new Hund("Fido", 5, new ArrayList<>(List.of("Hexi", "Loke")));
fido.navn = "Knut"; // Kompilerer ikke fordi navn er final
Som forventet så virker ikke det. Det følgende derimot, er lov;
fido.venner.add("Storm"); // Muterer innholdet i listen
System.out.println(fido); // Hund[navn=Fido, alder=5 venner=[Hexi, Loke, Storm]]
Records er altså ikke immutable hvis et felt er mutable.
Validering av records
For å unngå ulovlig tilstand i en applikasjon, så vil man gjerne validere records
ved opprettelse. En ting jeg savner
er muligheten til å sette default-verdier, så enn så lenge må du håndtere det selv.
Det kan man fint gjøre med den kompakte constructor
-varianten:
record Hund(String navn, int alder, List<String> venner) {
public Hund {
if (navn == null || navn.isBlank()) throw new IllegalArgumentException("Navn kan ikke være tomt");
if (alder < 0) throw new IllegalArgumentException("Alder må være positiv");
if (venner == null) venner = List.of();
}
}
For mer avanserte datastrukturer så kan man fint bruke Builder
-patternet for å støtte validering og default-verdier,
med den ekstra koden det medfører selvsagt.
Oppsummert så er records
et nyttig verktøy å ha i verktøykassen, og er definitivt et steg i riktig retning for Java.
¶Hva er nytt i Java 21 for records?
En record
er lett å konstruere, og nå kan man endelig hente ut data via record patterns.
Dette er en populær feature i andre språk, som f.eks JavaScript
og Clojure
.
Hvordan funker det?
Før record
-patterns så måtte man plukke ut verdier selv:
static void printHund(Object obj) {
if (obj instanceof Hund h) {
System.out.println(h.navn() + " er " + h.alder() " år gammel");
}
}
I Java 21 kan du nå hente ut verdiene fra recorden
på følgende måte:
static void printHund(Hund obj) {
if (obj instanceof Hund(String navn, int alder, var venner)) {
System.out.println(navn + " er " + alder + " år gammel");
}
}
Du kan spesifisere typen til feltene i recorden
, eller du kan bruke var
.
Du kan gjøre destructuring i flere nivåer også.
record Katt(String navn, Hund fiende) {}
var katt = new Katt("Mjau", fido);
if (roach instanceof Katt(var navn, Hund(var hundeNavn, var _x, var _y))) {
System.out.println("Katten heter " + navn + " og eies av " + hundeNavn );
}
Merk at du må spesifisere alle feltene i en record når du destructurer, selv om du ikke trenger de.
Dette blir riktignok litt bedre når JEP-443 ferdigstilles, men den featuren er kun i preview i Java 21. Da kan du markere parameter som unødvendige ved å bruke underscore.
Jeg lurer litt på hvorfor Java har valgt å basere record
patterns på posisjon og ikke navn, slik som f.eks JavaScript
Problemet er illustrert i eksemplet under. Her er det fort gjort å blande navnene, ettersom navnet på record
-feltene er posisjonsbaserte:
if (obj instanceof Hund(String venner, int navn, var alder)) {
System.out.println(venner + " er " + navn + " år gammel");
}
¶Pattern matching med switch
Switch i Java har frem til nå vært sørgelige greier. Med Java 21, så kan man endelig gjøre pattern matching.
Da kan man f.eks gjøre følgende, hvor man matcher mot typen til objektet:
static String formattere(Object obj) {
return switch (obj) {
case Integer i -> String.format("int %d", i);
case Long l -> String.format("long %d", l);
case Double d -> String.format("double %f", d);
case String s -> String.format("String %s", s);
default -> obj.toString();
};
}
Nå støtter switch
matching mot null
:
static String behandle(String input) {
return switch (input) {
case null -> "Er null";
case "Fido", "Hexi" -> "En hund";
default -> "Noe annet";
};
}
Hvis du har en null
-sjekk i switchen
så slipper du å sjekke det på forhånd. Merk at default
ikke matcher mot null,
så du kan fortsatt få NullPointer
i en switch om du ikke har med null
-casen.
Du kan nå også begrense en case med en påfølgende when
:
static String aldersVurdering(Hund hund) {
return switch (hund) {
case Hund(var _n, var alder, var _v) when (alder < 3) -> "Ung";
case Hund(var _n, var alder, var _v) when (alder < 10 ) -> "Gammel";
default -> "Kjempegammel";
};
}
Det blir mer lesbart ettersom du da får et skille mellom matchingen på venstresiden av pilen og hvordan tilfellet skal håndteres på høyresiden.
¶Virtual threads
Dette er den mest spennende featuren i Java 21, men den fortjener en egen bloggpost. Sammen med structured concurrency (som er i preview), så åpner man opp for en forenklet programmeringsmodell for concurrency.
¶Er Java et spennende alternativ i 2023?
Java har “the last mover advantage”. Det vil si at man legger til ny funksjonalitet etter at de mer fremoverlente språkene har avslørt hva som fungerer bra i praksis. Ved å levere endringer i små steg, så kan Java oppdateres mye oftere enn tidligere. Det snakket en av arkitektene bak Java, Brian Goetz, om på årets Devoxx.
Java blir altså gradvis mer moderne. Det blir spennende å følge med i årene fremover om denne nye strategien vil holde Java relevant i konkurransen med andre språk.