blob: a64766483dcd32ef66b33d27a2adc458b532363d [file] [log] [blame]
//////////////////////////////////////////
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
//////////////////////////////////////////
= Sealed hierarchies (incubating)
Sealed classes, interfaces and traits restrict which subclasses can extend/implement them.
Prior to sealed classes, class hierarchy designers had two main options:
* Make a class final to allow no extension.
* Make the class public and non-final to allow extension by anyone.
Sealed classes provide a middle-ground compared to these all or nothing choices.
Sealed classes are also more flexible than other tricks previously used
to try to achieve a middle-ground. For example, for class hierarchies,
access modifiers like protected and package-private give some ability to restrict inheritance
hierarchies but often at the expense of flexible use of those hierarchies.
Sealed hierarchies provide full inheritance within a known hierarchy of classes, interfaces
and traits but disable or only provide controlled inheritance outside the hierarchy.
As an example, suppose we want to create a shape hierarchy containing
only circles and squares. We also want a shape interface to
be able to refer to instances in our hierarchy.
We can create the hierarchy as follows:
[source,groovy]
----
include::../test/SealedSpecificationTest.groovy[tags=simple_interface_keyword,indent=0]
----
Groovy also supports an alternative annotation syntax.
We think the keyword style is nicer but you might choose the annotation style if your editor doesn't yet have Groovy 4 support.
[source,groovy]
----
include::../test/SealedSpecificationTest.groovy[tags=simple_interface_annotations,indent=0]
----
We can have a reference of type `ShapeI` which, thanks to the `permits` clause,
can point to either a `Circle` or `Square` and, since our classes are `final`,
we know no additional classes will be added to our hierarchy in the future.
At least not without changing the `permits` clause and recompiling.
In general, we might want to have some parts of our class hierarchy
immediately locked down like we have here, where we marked the
subclasses as `final` but other times we might want to allow further
controlled inheritance.
[source,groovy]
----
include::../test/SealedSpecificationTest.groovy[tags=general_sealed_class,indent=0]
----
.<Click to see the alternate annotations syntax>
[%collapsible]
====
[source,groovy]
----
include::../test/SealedSpecificationTest.groovy[tags=general_sealed_class_annotations,indent=0]
----
====
&nbsp; +
In this example, our permitted subclasses for `Shape` are `Circle`, `Polygon`, and `Rectangle`.
`Circle` is `final` and hence that part of the hierarchy cannot be extended.
`Polygon` is implicitly non-sealed and `RegularPolygon` is explicitly marked as `non-sealed`.
That means our hierarchy is open to any further extension by subclassing,
as seen with `Polygon -> RegularPolygon` and `RegularPolygon -> Hexagon`.
`Rectangle` is itself sealed which means that part of the hierarchy can be extended
but only in a controlled way (only `Square` is permitted).
Sealed classes are useful for creating enum-like related classes
which need to contain instance specific data. For instance, we might have the following enum:
[source,groovy]
----
include::../test/SealedSpecificationTest.groovy[tags=weather_enum,indent=0]
----
but we now wish to also add weather specific instance data to weather forecasts.
We can alter our abstraction as follows:
[source,groovy]
----
include::../test/SealedSpecificationTest.groovy[tags=weather_sealed,indent=0]
----
Sealed hierarchies are also useful when specifying Algebraic or Abstract Data Types (ADTs) as shown in the following example:
[source,groovy]
----
include::../test/SealedSpecificationTest.groovy[tags=sealed_ADT,indent=0]
----
Sealed hierarchies work well with records as shown in the following example:
[source,groovy]
----
include::../test/SealedSpecificationTest.groovy[tags=sealedRecord_ADT,indent=0]
----
== Differences to Java
* Java provides no default modifier for subclasses of sealed classes
and requires that one of `final`, `sealed` or `non-sealed` be specified.
Groovy defaults to _non-sealed_ but you can still use `non-sealed/@NonSealed` if you wish.
We anticipate the style checking tool CodeNarc will eventually have a rule that
looks for the presence of `non-sealed` so developers wanting that stricter
style will be able to use CodeNarc and that rule if they want.
* Currently, Groovy doesn't check that all classes mentioned in `permittedSubclasses`
are available at compile-time and compiled along with the base sealed class.
This may change in a future version of Groovy.
Groovy supports annotating classes as sealed as well as "native" sealed classes.
The `@SealedOptions` annotation supports a `mode` annotation attribute
which can take one of three values (with `AUTO` being the default):
NATIVE::
Produces a class similar to what Java would do.
Produces an error when compiling on JDKs earlier than JDK17.
EMULATE::
Indicates the class is sealed using the `@Sealed` annotation.
This mechanism works with the Groovy compiler for JDK8+ but is not recognised by the Java compiler.
AUTO::
Produces a native record for JDK17+ and emulates the record otherwise.
Whether you use the `sealed` keyword or the `@Sealed` annotation
is independent of the mode.