Java Sealed Classes in JDK 17

History

Java Sealed Classes were proposed by JEP 360 and delivered in JDK 15 as a preview feature. They were proposed again, with refinements, by JEP 397 and delivered in JDK 16 as a preview feature. This JEP proposes to finalize Sealed Classes in JDK 17, with no changes from JDK 16.

Web Stories

Overview

Enhance the Java programming language with sealed classes and interfaces. Java Sealed classes and interfaces restrict which other classes or interfaces may extend or implement them.

One of the primary purposes of inheritance is code reuse: When you want to create a new class and there is already a class that includes some of the code that you want, you can derive your new class from the existing class. In doing this, you can reuse the fields and methods of the existing class without having to write (and debug) them yourself.

However, what if you want to model the various possibilities that exist in a domain by defining its entities and determining how these entities should relate to each other? For example, you’re working on a graphics library. You want to determine how your library should handle common geometric primitives like circles and squares. You’ve created a Shape class that these geometric primitives can extend. However, you’re not interested in allowing any arbitrary class to extend Shape; you don’t want clients of your library declaring any further primitives. By sealing a class, you can specify which classes are permitted to extend it and prevent any other arbitrary class from doing so.

Java sealed classes

Goals

  • This JDK 17 feature java sealed classes will allow the author of a class or interface to control which code is responsible for implementing it.

  • Provides a more declarative way than access modifiers to restrict the use of a superclass.

  • Support future directions in pattern matching by providing a foundation for the exhaustive analysis of patterns.

Non-Goals

  • It is not a goal to provide new forms of access control such as “friends”.

  • It is not a goal to change final in any way, so no need to get confused.

Declaring Java Sealed Classes

To seal a class, add the sealed modifier to its declaration. Then, after any extends and implements clauses, add the permits clause. This clause specifies the classes that may extend the sealed class.

For example, the following declaration of Shape specifies three permitted subclasses, CircleSquare, and Rectangle:

 
Figure 3-1 Shape.java
				
					public sealed class Shape
    permits Circle, Square, Rectangle {
}
				
			

Define the following three permitted subclasses, CircleSquare, and Rectangle, in the same module or in the same package as the sealed class:

Figure 3-2 Circle.java
				
					public final class Circle extends Shape {
    public float radius;
}
				
			
Figure 3-3 Square.java

Square is a non-sealed class. This type of class is explained in Constraints on Permitted Subclasses.

				
					public non-sealed class Square extends Shape {
   public double side;
}   
				
			
Figure 3-4 Rectangle.java
				
					public sealed class Rectangle extends Shape permits FilledRectangle {
    public double length, width;
}
				
			

Rectangle has a further subclass, FilledRectangle:

Figure 3-5 FilledRectangle.java
				
					public final class FilledRectangle extends Rectangle {
    public int red, green, blue;
}
				
			

Alternatively, you can define permitted subclasses in the same file as the sealed class. If you do so, then you can omit the permits clause:

				
					package com.techshitanshu.geometry;

public sealed class Figure
    // The permits clause has been omitted
    // as its permitted classes have been
    // defined in the same file.
{ }

final class Circle extends Figure {
    float radius;
}
non-sealed class Square extends Figure {
    float side;
}
sealed class Rectangle extends Figure {
    float length, width;
}
final class FilledRectangle extends Rectangle {
    int red, green, blue;
}

				
			

Constraints on Permitted Subclasses

Permitted subclasses have the following constraints:

  • They must be accessible by the sealed class at compile time.

    For example, to compile Shape.java, the compiler must be able to access all of the permitted classes of ShapeCircle.javaSquare.java, and Rectangle.java. In addition, because Rectangle is a sealed class, the compiler also needs access to FilledRectangle.java.

  • They must directly extend the sealed class.

  • They must have exactly one of the following modifiers to describe how it continues the sealing initiated by its superclass:

    • final: Cannot be extended further

    • sealed: Can only be extended by its permitted subclasses

    • non-sealed: Can be extended by unknown subclasses; a sealed class cannot prevent its permitted subclasses from doing this

    For example, the permitted subclasses of Shape demonstrate each of these three modifiers: Circle is final while Rectangle is sealed and Square is non-sealed.

  • They must be in the same module as the sealed class (if the sealed class is in a named module) or in the same package (if the sealed class is in the unnamed module, as in the Shape.java example).

    For example, in the following declaration of com.example.graphics.Shape, its permitted subclasses are all in different packages. This example will compile only if Shape and all of its permitted subclasses are in the same named module.

Declaring Sealed Interfaces

Like sealed classes, to seal an interface, add the sealed modifier to its declaration. Then, after any extends clause, add the permits clause, which specifies the classes that can implement the sealed interface and the interfaces that can extend the sealed interface.

The following example declares a sealed interface named Expr. Only the classes ConstantExprPlusExprTimesExpr, and NegExpr may implement it:

				
					package com.example.expressions;

public class TestExpressions {
  public static void main(String[] args) {
    // (6 + 7) * -8
    System.out.println(
      new TimesExpr(
        new PlusExpr(new ConstantExpr(6), new ConstantExpr(7)),
        new NegExpr(new ConstantExpr(8))
      ).eval());
   }
}

sealed interface Expr
    permits ConstantExpr, PlusExpr, TimesExpr, NegExpr {
    public int eval();
}

final class ConstantExpr implements Expr {
    int i;
    ConstantExpr(int i) { this.i = i; }
    public int eval() { return i; }
}

final class PlusExpr implements Expr {
    Expr a, b;
    PlusExpr(Expr a, Expr b) { this.a = a; this.b = b; }
    public int eval() { return a.eval() + b.eval(); }
}

final class TimesExpr implements Expr {
    Expr a, b;
    TimesExpr(Expr a, Expr b) { this.a = a; this.b = b; }
    public int eval() { return a.eval() * b.eval(); }
}

final class NegExpr implements Expr {
    Expr e;
    NegExpr(Expr e) { this.e = e; }
    public int eval() { return -e.eval(); }
}
				
			

Narrowing Reference Conversion and Disjoint Types

Narrowing reference conversion is one of the conversions used in type checking cast expressions. It enables an expression of a reference type S to be treated as an expression of a different reference type T, where S is not a subtype of T. A narrowing reference conversion may require a test at run time to validate that a value of type S is a legitimate value of type T. However, there are restrictions that prohibit conversion between certain pairs of types when it can be statically proven that no value can be of both types.

APIs Related to Sealed Classes and Interfaces

The class java.lang.Class has two new methods related to sealed classes and interfaces:

  • java.lang.constant.ClassDesc[] permittedSubclasses(): Returns an array containing java.lang.constant.ClassDesc objects representing all the permitted subclasses of the class if it is sealed; returns an empty array if the class is not sealed
  • boolean isSealed(): Returns true if the given class or interface is sealed; returns false otherwise

Sealed classes and pattern matching

A significant benefit of sealed classes will be realized in JEP 406, which proposes to extend switch with pattern matching. Instead of inspecting an instance of a sealed class with ifelse chains, user code will be able to use a switch enhanced with patterns. The use of sealed classes will allow the Java compiler to check that the patterns are exhaustive.

For example, consider this code using the sealed hierarchy declared earlier:

Shape rotate(Shape shape, double angle) {
        if (shape instanceof Circle) return shape;
        else if (shape instanceof Rectangle) return shape;
        else if (shape instanceof Square) return shape;
        else throw new IncompatibleClassChangeError();
}

The Java compiler cannot ensure that the instanceof tests cover all the permitted subclasses of Shape. The final else clause is actually unreachable, but this cannot be verified by the compiler. More importantly, no compile-time error message would be issued if the instanceof Rectangle test was omitted.

In contrast, with pattern matching for switch (JEP 406)the compiler can confirm that every permitted subclass of Shape is covered, so no default clause or other total pattern is needed. The compiler will, moreover, issue an error message if any of the three cases is missing:

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

JVM support for sealed classes

The Java Virtual Machine recognizes sealed classes and interfaces at runtime, and prevents extension by unauthorized subclasses and subinterfaces.

Although sealed is a class modifier, there is no ACC_SEALED flag in the ClassFile structure. Instead, the class file of a sealed class has a PermittedSubclasses attribute which implicitly indicates the sealed modifier and explicitly specifies the permitted subclasses:

PermittedSubclasses_attribute {
    u2 attribute_name_index;
    u4 attribute_length;
    u2 number_of_classes;
    u2 classes[number_of_classes];
}

The list of permitted subclasses is mandatory. Even when the permitted subclasses are inferred by the compiler, those inferred subclasses are explicitly included in the PermittedSubclasses attribute.

The class file of a permitted subclass carries no new attributes.

When the JVM attempts to define a class whose superclass or superinterface has a PermittedSubclasses attribute, the class being defined must be named by the attribute. Otherwise, an IncompatibleClassChangeError is thrown.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top
Java Sealed Classes