Liigu peamise sisu juurde

Suletud klassid (sealed classes)

Sissejuhatus

Vaikimisi võib iga klass laiendada mistahes mitte-final klassi. Selline avatus on paindlik ja sageli kasulik, kuid mõnes olukorras soovitakse alamtüüpe teadlikult piirata. Kõik lubatud variandid on ette teada ning uusi ei tohiks hiljem lisanduda.

Java 17 lisas selleks otstarbeks suletud klassid (sealed classes). Suletud klass määratleb selgesõnaliselt, millised klassid tohivad seda laiendada, piirates seeläbi võimalike alamtüüpide hulka.

Suletud klassi deklareerimine

Suletud klass deklareeritakse märksõnaga sealed. Lubatud alamklassid loetletakse permits märksõna kasutades üles:

sealed class Shape permits Circle, Rectangle, Triangle {
// ...
}

Sellest tulenevalt võivad klassi Shape laiendada ainult klassid Circle, Rectangle ja Triangle.

Iga katse laiendada klassi Shape klassiga, mida pole permits-nimekirjas, lõppeb kompileerimisveaga:

class Pentagon extends Shape { }    // Error: Pentagon is not allowed to extend Shape

Mida alamklassid peavad deklareerima

Iga permits-loendis nimetatud alamklass peab omakorda selgesõnaliselt määrama, kas ja kuidas seda tohib edasi laiendada. Selleks tuleb kasutada ühte kolmest modifikaatorist:

ModifikaatorTähendus
finalKlassi ei saa edasi laiendada. Hierarhia lõpeb siin.
sealedKlassi saab laiendada, kuid ainult oma permits-nimekirjaga.
non-sealedAvatud kõigi poolt laiendamiseks. Hierarhia muutub sellest punktist taas avatuks.
sealed class Shape permits Circle, Rectangle, Polygon { }

final class Circle extends Shape {
private double radius;
public Circle(double radius) { this.radius = radius; }
public double area() { return Math.PI * radius * radius; }
}

final class Rectangle extends Shape {
private double width, height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
public double area() { return width * height; }
}

// Polygon is itself sealed — it controls its own subtypes
sealed class Polygon extends Shape permits Triangle, Quadrilateral { }

final class Triangle extends Polygon { ... }
final class Quadrilateral extends Polygon { ... }

// FreeShape opens the hierarchy again
non-sealed class FreeShape extends Polygon {
// Seda klassi võib vabalt edasi laiendada
}

// This is valid now
class IrregularShape extends FreeShape {
// final/sealed/non-sealed not required
}

Antud näites:

  • Polygon on endiselt suletud ja kontrollib oma otseseid alamklasse.
  • FreeShape on non-sealed, mis tähendab, et alates sellest punktist on hierarhia taas avatud.
  • IrregularShape võib FreeShape klassi vabalt laiendada ilma täiendavate piiranguteta.

Suletud liidesed

Sulgemine toimib ka liideste peal. Liidest saab deklareerida märksõnaga sealed ning piirata selle implementatsioonide hulka permits märksõnaga.

sealed interface Result permits Success, Failure { }

record Success(String value) implements Result { }
record Failure(String errorMessage) implements Result { }

Siin võivad liidest Result implementeerida ainult Success ja Failure.

Selline lahendus on levinud olukordades, kus soovitakse modelleerida piiratud kogus võimalikke variante, näiteks operatsiooni tulemust, mis saab olla kas edukas või ebaõnnestunud. Kuna kõik võimalikud alamtüübid on ette teada, muutub kood selgemaks ning kompilaator saab aidata juhtumite täielikkuse kontrollimisel (nt. switch-avaldistes).

NB! kui märkasite, siis antud näites kasutati record klasse. Nende kohta saate rohkem infot siit.

Pattern matching (Java 21+)

Suletud tüüpide üks olulisemaid eeliseid ilmneb koos switch-avaldiste mustrite sobitamisega (pattern matching for switch).

Kuna kompilaator teab kõiki lubatud alamtüüpe, saab suletud tüübi üle tehtud switch-avaldis olla ammendav. Kõik võimalikud juhud peavad olema käsitletud. Kui mõni juhtum puudub, annab kompilaator vea ning default-haru ei ole vajalik.

sealed class Shape permits Circle, Rectangle, Triangle { }

final class Circle extends Shape { double radius; Circle(double r) { radius = r; } }
final class Rectangle extends Shape { double w, h; Rectangle(double w, double h) { this.w=w; this.h=h; } }
final class Triangle extends Shape { double b, h; Triangle(double b, double h) { this.b=b; this.h=h; } }
double area(Shape shape) {
return switch (shape) {
case Circle c -> Math.PI * c.radius * c.radius;
case Rectangle r -> r.w * r.h;
case Triangle t -> 0.5 * t.b * t.h;
// No default needed — the compiler knows these are all the cases
};
}

Siin ei ole default-haru vaja, sest Shape tüübi kõik võimalikud alamtüübid on loetletud.

Kui lisada Shape-ile uus lubatud alamklass, kuid jätta vastav case lisamata, tekib kompileerimisviga. See aitab vältida olukordi, kus uus alamtüüp jääb käsitlemata.

info

Ilma suletud tüüpide kasutamiseta tuleb switch-is tavaliselt lisada default-haru. See võib varjata käsitlemata juhtumeid. Suletud tüüpide korral kontrollib kompilaator juhtumite täielikkust ja puuduv haru tuvastatakse kohe.

Suletud vs final klassid

final klass välistab igasuguse laiendamise. Suletud klassi saab laiendada, kuid ainult selgesõnaliselt lubatud alamklasside poolt.

final class ImmutablePoint { ... }      // no one can extend this

sealed class Shape permits Circle, Rectangle { } // only these two can extend Shape

Esimesel juhul on laiendamine täielikult keelatud. Teisel juhul on laiendamine võimalik, kuid rangelt kontrollitud.

  • Kasuta final-modifikaatorit siis, kui klass ei tohi mitte mingil juhul olla aluseks teistele klassidele.
  • Kasuta sealed-modifikaatorit siis, kui soovid säilitada kontrolli alamtüüpide üle, lubades laiendamist, kuid ainult ette määratud ja teadaolevas hulgas klassidele.

Millal kasutada suletud klasse

Suletud klassid ja liidesed sobivad hästi, kui:

  • Modelleeritakse fikseeritud valdkonda, kus kõik variandid on eelnevalt teada (kujundid, makseviisid, käsutüübid, API vastused).
  • Soovitakse, et kompilaator tagaks kõigi juhtumite ammendava käsitlemise switch-avaldises.
  • Soovitakse vältida olukorda, kus välised teegid või moodulid lisavad hierarhiasse uusi alamtüüpe.

Näiteks klienditüüpide modelleerimine:

sealed abstract class AbstractCustomer
permits RegularCustomer, GoldCustomer, PlatinumCustomer {

private final String name;

protected AbstractCustomer(String name) {
this.name = name;
}

// ...
}

final class RegularCustomer extends AbstractCustomer {
public RegularCustomer(String name) {
super(name);
}
}

final class GoldCustomer extends AbstractCustomer {
public GoldCustomer(String name) {
super(name);
}
}

final class PlatinumCustomer extends AbstractCustomer {
public PlatinumCustomer(String name) {
super(name);
}
}
double discount(AbstractCustomer customer) {
return switch (customer) {
case RegularCustomer r -> 0.0;
case GoldCustomer g -> 0.10;
case PlatinumCustomer p -> 0.20;
};
}