permits is only one way to say what subclasses are permitted. Quoting JEP 409,
When the permitted subclasses are small in size and number, it may be convenient to declare them in the same source file as the sealed class. When they are declared in this way, the sealed class may omit the permits clause and the Java compiler will infer the permitted subclasses from the declarations in the source file. (The subclasses may be auxiliary or nested classes.) For example, if the following code is found in Root.java then the sealed class Root is inferred to have three permitted subclasses:
abstract sealed class Root { ... final class A extends Root { ... } final class B extends Root { ... } final class C extends Root { ... } }
Root is automatically inferred to permit A, B, and C.
Having a keyword for sealed allows more flexibility in the language design for permitted subclasses, and makes it easier to tell at a glance if a class is sealed.
Whether sealed or permits are used in any other contexts isn't really relevant. (I don't think they are, although they might be in future language versions.) Other uses of sealed or permits wouldn't affect the design of this part of the language.
You can follow this link for examples.
In short, sealed classes gives you the control of which models, classes etc. that can implement or extend that class/interface.
Example from the link:
public sealed interface Service permits Car, Truck {
int getMaxServiceIntervalInMonths();
default int getMaxDistanceBetweenServicesInKilometers() {
return 100000;
}
}
This interface only permits Car and Truck to implement it.
The JEP 409 explains it as
A sealed class or interface can be extended or implemented only by those classes and interfaces permitted to do so.
A more practical explanation is the following:
The situation in the past was:
- You could not restrict an interface being extended by another interface
- You could not constraint which classes where able to implement a specific interface.
- You had to declare a class as final in order to not be extended by another class. This way no class could extend the declared final class. This was black or white approach.
The current situation with sealed keyword is:
You can now restrict an interface being extended by other interfaces and make a rule for only some specific interfaces which will be allowed to extend it.
Example:
public sealed interface MotherInterface permits ChildInterfacePermitted {} //Has to be declared either as sealed or non-sealed public non-sealed interface ChildInterfacePermitted extends MotherInterface {} public interface AnotherChildInterface extends MotherInterface {} //compiler error! It is not included in the permits of mother intefaceYou can now create an interface and select only specific classes that are allowed to implement that interface. All other classes are not allowed to implement it.
Example:
public sealed interface MotherInterface permits ImplementationClass1 {} //Has to be declared either as final or as sealed or as non-sealed public final class ImplementationClass1 implements MotherInterface {} public class ImplementationClass2 implements MotherInterface {} //compiler error! It is not included in the permits of mother intefaceYou can now restrict a class being extended (same as before with final) but you can now allow some specific classes to extend it. So now you have more control as before the keyword final was absolute restricting every class from extending the declared final class
Example:
public sealed class MotherClass permits ChildClass1 {} //Has to be declared either as final or as sealed or as non-sealed public non-sealed class ChildClass1 extends MotherClass {} public class ChildClass2 extends MotherClass {} //compiler error! It is not included in the permits of MotherClass
Important notes:
The sealed class and its permitted subclasses must belong to the same module, and, if declared in an unnamed module, to the same package.
Example:
Let's say that we have the same unnamed module and the following packages
-packageA -Implementationclass1.java -packageB -MotherClass.javaor
-root -MotherClass.java -packageA -Implementationclass1.javaYou will get the error Class is not allowed to extend sealed class from another package. So if you have an unnamed module all participating classes and interfaces for the sealed function must be placed exactly on the same package.
Every permitted subclass must directly extend the sealed class.
I1obviously permitsCandDbut notEandF. Is this correct?
More accurately, you can say that C and D are in the set of permitted direct subclasses of I1, which is a term defined in section 9.1.4. The JLS doesn't really define what "I1 permits C and D" means though.
As for your switch expression, the reason why it works is two-fold. First, you are able to write a type pattern in a switch label if the type of the switch selector expression is downcast-convertible to that type.
14.11.1
A pattern case element p is switch compatible with T if p is applicable at type T (14.30.3).
14.30.3:
A pattern p is said to be applicable at a type T if one of the following rules apply:
- A type pattern that declares a pattern variable of a reference type U is applicable at another reference type T if T is downcast convertible to U (5.5).
Obviously, E is downcast-convertible to I1 through a widening reference conversion, because E implements I1. Note that this fact has nothing to do with permits. It is simply a result of E implements I2 and I2 extends I1. Surely you would agree that implements and extends are transitive!
Second, switch expressions need to be exhaustive. Your switch expression is always exhaustive because it has a default case. However, it is still exhaustive even without the default case.
From now on, we will consider your switch expression but without the default case, because that is where permits plays a role. The rules to determine whether the set of case labels you wrote are exhaustive are specified in 14.11.1.1. The important bit of your case is (this is kind of an inductive definition):
- A set of case elements is exhaustive for a type T if it contains a pattern that is unconditional at type T (14.30.3).
- A set of case elements is exhaustive for a type T that includes an abstract and sealed class or interface named C, if it is exhaustive for every applicable permitted direct subtype of T.
"applicable permitted direct subtype of T" in your case is really just the same as "permitted direct subtype of T". You can also treat "a type T that includes an abstract and sealed class or interface named C" as the same as T - the "includes" relationship isn't relevant to your case. With T=I1 in mind, we can start "running" this algorithm.
We use the second rule first - the permitted direct subtypes of I1 are I2, C and D. Since we have a C c and D d in the case elements, we know that our set of case elements is exhaustive for C and D (first rule). Is it also exhaustive for I2? To determine that, we use the second rule again. The permitted direct subtypes of I2 are E and F. Using the first rule, we know that the case elements E e and F f are exhaustive for E and F respectively. We have now proven that that the set of case elements are exhaustive for I2, C and D, so it is exhaustive for I1, according to the second rule.
So if you are talking about how switch patterns work, I think "inductive" is a better word to describe how the exhaustiveness of switch case labels are verified.
If I read the JLS
§8.1.6and§9.1.4, correctly the classes that a sealed class/interface permits, are just the direct sub classes/interfaces.
Each sealed class or interface needs to specify at least one direct permitted class (or interface). There's no need to specify non-directs subclasses, they are granted with the permission by default (since they are allowed to extend their direct parent).
Specification explicitly tells that only direct subclasses can be provided in the permits clause, §8.1.6. Permitted Direct Subclasses:
Every permitted direct subclass specified by the permits clause must be a direct subclass of
C(§8.1.4), or a compile-time error occurs.
Permitted classes should be necessarily marked with either of these modifiers: sealed, non-sealed and final (the latter modifier for obvious reasons can't be used with interfaces).
If a subclass is final it can't be extended.
By declaring a permitted subclass (subinterface) as non-sealed you're loosening the constraints. Such class is allowed to be extended as a regular class (no permits clause required).
If the subclass is being marked as sealed then the cycle repeats: it in turn has to have a permite clause specifying its direct subclasses.
Here's a quote from the JEP 409: Sealed Classes:
A sealed class imposes three constraints on its permitted subclasses:
1. The sealed class and its permitted subclasses must belong to the same module, and, if declared in an unnamed module, to the same package.
2. Every permitted subclass must directly extend the sealed class.
3. Every permitted subclass must use a modifier to describe how it propagates the sealing initiated by its superclass:
A permitted subclass may be declared
finalto prevent its part of the class hierarchy from being extended further. (Record classes are implicitly declaredfinal.)A permitted subclass may be declared
sealedto allow its part of the hierarchy to be extended further than envisaged by its sealed superclass, but in a restricted fashion.A permitted subclass may be declared
non-sealedso that its part of the hierarchy reverts to being open for extension by unknown subclasses. Asealedclass cannot prevent its permitted subclasses from doing this.
That said, if you change the declaration of the sealed super interface I1 by specifying non-direct subclasses E and F after permits clause it would not compile:
public sealed interface I1
permits I2, C, D, E, F {
/*...*/
}
And we restore the initial declaration of I1 (by removing E and F) the following code would work fine
public static String foo(I1 i1) {
return switch (i1) {
case C c -> "1";
case D d -> "2";
case E e -> "3";
case F f -> "4";
default -> "5";
};
}
main()
public static void main(String[] args) {
System.out.println(foo(new E()));
System.out.println(foo(new F()));
}
Output:
3
4