Her er nyhederne i Java 15: Koden gemmes i forseglede og skjulte klasser

Illustration: mkabakov/Bigstock
Code hiding får en ny mulighed i den kommende udgave af Java.

Sommeren står for døren, og dermed er det tid til en ny omgang Java. I denne halvårlige ombæring er ‘forseglede’ klasser og interfaces den store nyhed.

At skjule koden, såkaldt ‘code hiding,’ er en af objektorienteringens klassiske dyder.

På den måde afkobles et programs forskellige dele, så afhængigheder minimeres. Der kan skabes snitflader – api’er – ved at skelne mellem ‘privat’ og ‘offentlig’ kode, og mellem kode, der er beregnet på at skulle nedarves, og kode, der ikke er det.

I Java findes nøgleordet final, der i forbindelse med klasser, interfaces og metoder betyder, at der ikke kan nedarves eller ‘overrides’ i en subklasse.

Nu tilføjes en facilitet for ‘forseglede’ klasser og interfaces, der ganske simpelt gør det muligt for en klasse at udpege præcist de subklasser, der må nedarve – og ingen andre.

I motivationen bag den nye facilitet, skriver Javas kloge hoved Brian Goetz, at når det handler om at genanvende kode, kan man som restriktion benytte adgangsniveauet package-private (som i class), hvor kun subklasser i samme pakke kan få lov til at nedarve en superklasse.

»Men formålet med et klasseshierarki er ikke altid at genbruge kode. Nogle gange er dets formål at modellere de forskellige muligheder, der findes i et problemfelt, såsom den slags former, der understøttes af et grafikbibliotek eller de slags lånemuligheder, der understøttes af en finansiel applikation. Når klassehierarkiet bruges på denne måde, kan begrænsning af underklasser strømline modelleringen.«

Det kan se således ud:

package com.example.geometry;
 
public sealed class Shape
    permits Circle, Rectangle, Square {...}

De nedarvede klasser skal enten befinde sig i samme pakke eller samme modul som superklassen.

På et senere tidspunkt skal Javas pattern matching, der lige nu er på et tidligt stadie, kunne benytte en konstruktion som

Shape rotate(Shape shape, double angle) {
 
    return switch (shape) {
        case Circle c    -> c;  // no action needed
        case Rectangle r -> r.rotate(angle);
        case Square s    -> s.rotate(angle);
    }
}

– hvor compileren kan regne ud, at alle muligheder er dækket i forgreningen, på baggrund af de subklasser, som er angivet i Shape-klassen.

Forseglede klasser er et såkaldt ‘preview’, og det betyder at der skal compiles med flaget:

--enable-preview --release 15

Skjulte klasser

Lidt i samme genre er skjulte klasser, der dog ikke vil have den store betydning for den almindelige programmør, men derimod for dem, der skriver frameworks og værktøjer.

Skjulte klasser er klasser, der ikke kan bruges direkte af andre klasser. Skjulte klasser er beregnet til frameworks, der genererer klasser under kørselstid og bruger dem indirekte via refleksion, hvor felter og metoder altså ‘slås op’ programmatisk.

Mange sprog, der bygger på JVM, Javas virtuelle maskine, er afhængige af dynamisk klassegenerering for at opnå fleksibilitet og effektivitet. I Java oversættes et lambda-udtryk ikke til en dedikeret class-fil på kompileringstidspunktet, men genererer i stedet bytekode, der dynamisk skaber en klasse for et objekt, der svarer til lambda-udtrykket, men først når det er nødvendigt. Andre eksempler er dynamiske proxyer, som er klasser, der også genereres programmatisk ud fra en række interfaces.

I alle disse tilfælde ønsker programmørerne at klassen ikke kan findes med metoder som Class.forName, samt adgangskontrol og kontrol over klassens levetid.

Det nye består i en udvidelse til Javas Lookup-api, så en skjult klasse kan defineres og kun tilgås ved hjælp af refleksion. JVM’en kan ikke linke til klassen og den kan ikke benyttes via classloaders. En skjult klasse kan også unloades, når den er ude af programmets virkefelter.

Ud med Javascript og ind med lav latency GC

Blandt andre nyheder i den nye udgave er ‘text blocks’, som kom med Java 13 – streng-konstanter, der løber over flere linjer – og den er nu ikke længere i preview, men færdig og klar til brug.

Records og indledende pattern matching for instanceof fra Java 14 er begge i second preview, så hvis alt går vel, skulle de to faciliteteter være færdigmodnede i den kommende Java 16.

Et api til adgang af hukommelse uden for den virtuelle maskine, så som det populære bibliotek Memcached, er nu i anden incubator-udgave. I modsætning til preview er incubator-projekter et vedhæng til klassebibliotekerne og kan fjernes igen på et senere tidspunkt.

To nye garbage collectors er nu produktionsmodne. Det drejer sig om ZGC og Shenandoah, der begge har lav forsinkelse under skraldeindsamlingen som mål. ZGC går efter pauser på under 10 millisekunder.

Javascript-motoren Nashorn fjernes. Den er for tung at vedligeholde og Oracles GraalVM kan nu byde på samme mulighed, men i C-implementering. Og så omskrives en række netværksbiblioteker.

Java 15 er sat til at udkomme i midten af september, og tidlige udgaver kan downloades fra projektets hjemmeside.

Tips og korrekturforslag til denne historie sendes til tip@version2.dk
Følg forløbet
Kommentarer (19)
sortSortér kommentarer
  • Ældste først
  • Nyeste først
  • Bedste først
#2 Lasse Mølgaard

Grimt sprog udvikler sig over tid til at ligne Standard ML mere!

Tjah jeg synes nu selv at switch eksemplet var slemt.

Det er mange år siden jeg sidst programmerede i Java, men:

Ville det ikke være nok at lave metoden rotate abstrakt?

På denne måde skal alle nedarvninger af klassen implementere hvordan koden rotere de enkelte geometriske former.

Efterfølgende kan man så gøre ting som:

Shape s = new Square(); // Fordi Square er nedarvet fra Shape  
s.rotate(); // Her kaldes Square.rotate()

Hvorfor komplicere ting mere end højst nødvendigt?

  • 1
  • 0
#3 Torben Mogensen Blogger

Jeg tænkte det samme. Eksemplet ligner sumtyper og pattern matching, men med horribel syntaks og stadig kun med pattern matching på øverste knude.

Og hvorfor skal det hele være klasser? Klasser er efter min mening en horribel sammenblanding af mange begreber, som egentlig burde være adskilt. Det virker som om man i stedet for at indføre en ny slags type bare tilføjer en modifikator til klasser (sealed, final, static, private, osv.). Dermed mudres begreberne.

I ML er f.eks. produkter (dette og hint) og sumtyper (dette eller hint) adskilte begreber, og det gør det nemt at se hvad, der sker.

  • 5
  • 0
#4 Jonas Høgh

På denne måde skal alle nedarvninger af klassen implementere hvordan koden rotere de enkelte geometriske former.

Det kan du godt, men det giver ikke samme resultat som et lukket hierarki og pattern matching.

Med et åbent typehierarki og virtuelle metoder, er det nemt at tilføje en ny klasse (her en en ny type figur), men det er svært at tilføje nye operationer (fx translation i stedet for rotation)

Med en sumtype og pattern matching er det derimod svært at tilføje nye typer, men nemt at tilføje nye operationer. Java har traditionelt ingen gode konstruktioner til denne type problemer.

  • 1
  • 0
#6 Lasse Mølgaard

Med et åbent typehierarki og virtuelle metoder, er det nemt at tilføje en ny klasse (her en en ny type figur), men det er svært at tilføje nye operationer (fx translation i stedet for rotation)

Okay skulle så lige bruge Google for at få en genopfriskning af translation. :-)

Hurtigt resume sagde, at geometrisk translation er blot at forskyde en figur i en-eller-anden retning uden at ændre på rotation eller form.

I mit eksempel ville det blot være endnu en abstrakt metode, som alle klasser skal nedarve.

Men bruges translation til andet i geometri?

  • 0
  • 0
#8 Lasse Mølgaard

Ja naturligvis. Problemet er, at definitionen af "alle klasser" ikke er lukket. Hvis du har implementeringer af Shape defineret i trediepartsbiblioteker, virker de nu ikke længere.

Som jeg ser det vil der stadigvæk være problemer med inkompatabilitet i tredjepartsbiblioteker uanset om du bruger den nye syntaks eller ej, hvis du tilføjer nye egenskaber eller blot ændre egenskaber i Shape.

Som jeg ser switch eksemplet, så er det blot syntaktisk sukker.

Lidt på samme måde som class extension i C#, hvor du kan tilføje din egen funktion til eksempelvis String.

Dette kunne også løses ved at lave en statisk funktion i ens tredjepart bibliotek i en klasse som f.eks en klasse med navn "SpecialString".

Den eneste forskel i koden ville være at i stedet for at kalde string.myfunction(), så kaldes SpecialString.myfunktion().

Det er knapt så kønt, men effekten er den samme.

Men endnu en grund til, at jeg synes switch eksemplet er slemt er fordi programmet skal først via type cast finde ud af hvilken type objekt vi har med at gøre, før den kan køre den relevante algoritme på objektet.

I mit eksempel kan koden i princippet være ligeglad med hvordan funktionen er implementeret.

Den skal blot vide at funktionen eksisterer, hvilket den ved, fordi den er deklareret i Shapes signatur.

  • 0
  • 0
#9 Jonas Høgh

Som jeg ser det vil der stadigvæk være problemer med inkompatabilitet i tredjepartsbiblioteker uanset om du bruger den nye syntaks eller ej, hvis du tilføjer nye egenskaber eller blot ændre egenskaber i Shape.

Som jeg ser switch eksemplet, så er det blot syntaktisk sukker.

Ja, som skrevet ovenfor gør sumtyper det sværere at ændre i strukturen for typerne. Men operationer på typen i form af funktioner, der bruger pattern matching, er helt afkoblet fra typen.

Ja, det er bare syntaktisk sukker, men det gør mange ting utroligt meget lettere at løse elegant.

Men endnu en grund til, at jeg synes switch eksemplet er slemt er fordi programmet skal først via type cast finde ud af hvilken type objekt vi har med at gøre, før den kan køre den relevante algoritme på objektet.

Ja, der står i alle lærebøger om objektorienteret programmering, at type casting er det onde, og virtuelle metoder er vejen frem. Nogle gange har du bare ikke brug for den abstraktion, men for en simpel datatype der kan beskrive at en værdi enten har en struktur eller en anden struktur.

  • 1
  • 0
#10 Peter Stricker
  • 0
  • 0
#11 Lasse Mølgaard

Jeg kan ikke lige gennemskue problemet med at tilføje nye egenskaber til Shape. Kan du give et eksempel?

Jeg vil ikke vove mig at vise eksemplet i Java, fordi det er jeg ganske enkelt for rusten til, så jeg skriver i C# i stedet for, men ideen er den den samme.

Lad os sige du har en 2D figur som er beskrevet som værende en liste af tupler med x og y koordinater på kanterner i din figur.

I så fald ville nedestående kode kunne beskrive en trekant, hvor hjørnerne befinder sig i punkterne:

  • 1,1
  • 2,3
  • 4,2
   List<(int, int)> Shape = new List<(int, int)>{ (1, 1), (2, 3), (4, 2) };

Et stykke tid senere finder du ud af at implementationen af Shape performer ikke helt godt, så du vælger at ændre på hvordan en figur er udtrykt ved at gemme de samme informationer i et en-dimensionelt array!

Samme kode kan nu skrives som:

   int[] Shape = { 1, 1, 2, 3, 4, 2 };

Strukturen af arrayet er for ethvert index i arrayet vil følgende regler gælde:

  • index % 2 == 0 medfører at array[index] indeholder X koordinaten.
  • index % 2 == 1 medfører at array[index] indeholder Y koordinaten.

Et skift i hvordan en figur er beskrevet vil unægtelig gøre at tredjepartbiblioteker skal skrives om.

Der er da ikke nogen type cast i switch eksemplet.

Jo der er. Bagom det hele.

Switch casen er syntaktisk sukker for følgende algoritme:

Shape rotate(Shape shape, double angle) {  
   
    Shape tmp = shape;  
   
    if(shape instanceof Circle)   
    {  
        Circle c = (Circle) shape;  
        tmp = c;  
    }  
    else if(shape instanceof Rectangle)  
    {  
        Rectangle r = (Rectangle) shape;  
        tmp = r.rotate(angle)  
    }  
    else if(shape instanceof Square)  
    {  
        Square s = (Square) shape;  
        tmp = s.rotate(angle)  
    }  
   
    return tmp;  
}
  • 0
  • 0
#14 Lasse Mølgaard

Nej. Hvis du kalder din rotate med en Rectangle som første parameter, så får du en Shape tilbage. I switch eksemplet får du en Rectangle tilbage.

Desværre:

Der er et implicit typecast i switch-casen.

Følgende vil give en compiler fejl:

Rectangle r = shape;

Årsagen er at type checkeren ved ikke at objektet shape er i virkeligheden en rektangel.

Derfor skal der type castes.

Sekundært:

Signaturen for rotate i den abstrakte klasse er forskellig fra implementeringen i den nedarvet klasse:

Shape.rotate(Shape,int) vs Rectangle.rotate(int).

Det vil også give en fejl under kompilering.

  • 0
  • 0
#15 Peter Stricker

Signaturen for rotate i den abstrakte klasse er forskellig fra implementeringen i den nedarvet klasse

Hvilket så også afslører et problem ved at diskutere små bidder af kode, der skal eksemplificere en ny konstruktion i Java eller andre sprog. Jeg gik ud fra at Shape.rotate(Shape, int) var en overloaded metode, og der også fandtes en - eventuelt abstrakt - Shape.rotate(int).

Vi kan heller ikke ud fra den foreliggende kodestump afgøre om Rectangle.rotate(int) muterer this eller returnerer en ny Rectangle.

Jeg er I øvrigt helt enig med den kritik du kommer med i din første kommentar; switch eksemplet har tilsyneladende ingen fordel fremfor en abstrakt metode.

Følgende vil give en compiler fejl: Rectangle r = shape;

Men det synes jeg til gengæld lige præcis viser fordelen ved den nye switch konstruktion. Du slipper for at lave et eksplicit cast fordi du får det implicit.

  • 0
  • 0
#16 Baldur Norddahl
Shape rotate(Shape shape, double angle) {  
   
    return switch (shape) {  
        case Circle c    -> c;  // no action needed  
        case Rectangle r -> r.rotate(angle);  
        case Square s    -> s.rotate(angle);  
    }  
}

Problemet med ovenstående er at din Circle/Rectangle/Square bliver konverteret til en Shape efter at du har roteret. Du har brug for noget i den her stil: (Scala)

trait Shape  
case class Circle() extends Shape  
case class Rectangle() extends Shape  
def rotate[A <: Shape](shape: A, angle: Double): A = shape match {  
  case c @ Circle() => c.asInstanceOf[A]  
  case r @ Rectangle() => r.asInstanceOf[A]  
}

Det er ikke særligt smukt med det nytteløse kald til asInstanceOf men ellers kan type tjekkeren ikke gennemskue at vi stadig har den oprindelige type.

Ovenstående giver følgende resultat:

@ rotate(Circle(),5)  
res6: Circle = Circle()  
   
@ rotate(Rectangle(),5)  
res7: Rectangle = Rectangle()
  • 0
  • 0
#17 Morten Andersen

Nu er det jo lidt svært at kommentere på, når det først er en fremtidig feature, den pattern matching. Men hvis rotate kunne erklæres som:

public <T extends A> T rotate(T a, double angle)  
{  
        return switch (shape) {  
        case Circle c    -> c;  // no action needed  
        case Rectangle r -> r.rotate(angle);  
        case Square s    -> s.rotate(angle);  
    }  
}

så ville man bevare den præcise returtype - dette forudsat at Rectangle & Square's rotate()-funktioner returnerer hhv. Rectangle og Square, ellers skulle det så give en compile-fejl.

  • 0
  • 0
#18 Morten Andersen

Ups i steder for A i min signatur skulle have stået Shape... legede lidt i IDEen :-) Så:

public <T extends Shape> T rotate(T shape, double angle)    
{    
        return switch (shape) {    
        case Circle c    -> c;  // no action needed    
        case Rectangle r -> r.rotate(angle);    
        case Square s    -> s.rotate(angle);    
    }    
}
  • 0
  • 0
#19 Torben Mogensen Blogger

Med et åbent typehierarki og virtuelle metoder, er det nemt at tilføje en ny klasse (her en en ny type figur), men det er svært at tilføje nye operationer (fx translation i stedet for rotation)

Med en sumtype og pattern matching er det derimod svært at tilføje nye typer, men nemt at tilføje nye operationer. Java har traditionelt ingen gode konstruktioner til denne type problemer.

Ja, det kaldes "The Expression Problem". https://en.wikipedia.org/wiki/Expression_problem

I funktionelle sprog skal alle regler for en funktion skrives samlet, f.eks.

f(Circle r) = ...

f(Rectangle pp) = ...

uden at der er andre funktioner mellem de to linjer. Men hvis man tillader, at der tilføjes nye regler til en funktion et helt andet sted, så er det relativt nemt at tilføje en ny gren af sumtypen: Man tilføjer blot en regel for hver funktion, der arbejder med sumtypen (lidt ligesom en ny underklasse indeholder alle metoder fra superklassen). Man skal dog håndtere overlappende patterns på en anden måde end traditionelt: I stedet for at prioritere efter rækkefølgen prioriterer man de mest specifikke regler før de mindre specifikke regler, og giver fejl hvis to regler overlapper uden at den ene er mere specifik end den anden.

Men i praksis synes jeg ikke problemet er stort i f.eks. SML: Du får en advarsel, hvis en funktion ikke dækker alle tilfælde i en sumtype, så du finder hurtigt de steder, du skal tilføje regler. Typeklasser i Haskell er et andet bud på at løse The Expression Problem.

  • 2
  • 0
Log ind eller Opret konto for at kommentere