blob: 1d507fca067e09f19a97948c517ab74e537ba4d2 [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.
//////////////////////////////////////////
= Record classes (incubating)
Record classes, or _records_ for short, are a special kind of class
useful for modelling plain data aggregates.
They provide a compact syntax with less ceremony than normal classes.
Groovy already has AST transforms such as `@Immutable` and `@Canonical`
which already dramatically reduce ceremony but records have been
introduced in Java and record classes in Groovy are designed to align
with Java record classes.
For example, suppose we want to create a `Message` record
representing an email message. For the purposes of this example,
let's simplify such a message to contain just a _from_ email address,
a _to_ email address, and a message _body_. We can define such
a record as follows:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_message_defn,indent=0]
----
We'd use the record class in the same way as a normal class, as shown below:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_message_usage,indent=0]
----
The reduced ceremony saves us from defining explicit fields, getters and
`toString`, `equals` and `hashCode` methods. In fact, it's a shorthand
for the following rough equivalent:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_message_equivalent,indent=0]
----
Note the special naming convention for record getters. They are the same name as the field
(rather than the often common JavaBean convention of capitalized with a "get" prefix).
Rather than referring to a record's fields or properties, the term _component_
is typically used for records. So our `Message` record has `from`, `to`, and `body` components.
Like in Java, you can override the normally implicitly supplied methods
by writing your own:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_point3d,indent=0]
----
You can also use generics with records in the normal way. For example, consider the following `Coord` record definition:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_generics_defn,indent=0]
----
It can be used as follows:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_generics_usage,indent=0]
----
== Special record features
=== Compact constructor
Records have an implicit constructor. This can be overridden in the normal way
by providing your own constructor - you need to make sure you set all the fields
if you do this.
However, for succinctness, a compact constructor syntax can be used where
the parameter declaration part of a normal constructor is elided.
For this special case, the normal implicit constructor is still provided
but is augmented by the supplied statements in the compact constructor definition:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_compact_constructor,indent=0]
----
=== Serializability
Groovy _native_ records follow the
https://docs.oracle.com/en/java/javase/16/docs/specs/records-serialization.html[special conventions]
for serializability which apply to Java records.
Groovy _record-like_ classes (discussed below) follow normal Java class serializability conventions.
== Groovy enhancements
=== Argument defaults
Groovy supports default values for constructor arguments.
This capability is also available for records as shown in the following record definition
which has default values for `y` and `color`:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_point_defn,indent=0]
----
Arguments when left off (dropping one or more arguments from the right) are replaced
with their defaults values as shown in the following example:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_point_defaults,indent=0]
----
This processing follows normal Groovy conventions for default arguments for constructors, essentially automatically providing the constructors with the following signatures:
[source,groovy]
----
ColoredPoint(int, int, String)
ColoredPoint(int, int)
ColoredPoint(int)
----
Named arguments may also be used (default values also apply here):
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_point_named_args,indent=0]
----
You can disable default argument processing as shown here:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_point_named_args_off,indent=0]
----
This will produce a single constructor as per the default with Java.
It will be an error if you drop off arguments in this scenario.
You can force all properties to have a default value as shown here:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_point_named_args_on,indent=0]
----
Any property/field without an explicit initial value will be given the default value for the argument's type (null, or zero/false for primitives).
.Diving deeper
****
We previously described a `Message` record and displayed it's rough equivalent.
Groovy in fact steps through an intermediate stage where the `record` keyword
is replaced by the `class` keyword and an accompanying `@RecordType` annotation:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_message_annotation_defn,indent=0]
----
Then `@RecordType` itself is processed as a _meta-annotation_ (annotation collector)
and expanded into its constituent sub-annotations such as `@TupleConstructor`, `@POJO`,
`@RecordBase`, and others. This is in some sense an implementation detail which can often be ignored.
However, if you wish to customise or configure the record implementation,
you may wish to drop back to the `@RecordType` style or augment your record class
with one of the constituent sub-annotations.
****
=== Declarative `toString` customization
As per Java, you can customize a record's `toString` method by writing your own.
If you prefer a more declarative style, you can alternatively use Groovy's `@ToString` transform
to override the default record `toString`.
As an example, you can a three-dimensional point record as follows:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_point3d_tostring_annotation,indent=0]
----
We customise the `toString` by including the package name (excluded by default for records)
and by caching the `toString` value since it won't change for this immutable record.
We are also ignoring null values (the default value for `z` in our definition).
We can have a similar definition for a two-dimensional point:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_point2d_tostring_annotation,indent=0]
----
We can see here that without the package name it would have the same toString as our previous example.
=== Obtaining a list of the record component values
We can obtain the component values from a record as a list like so:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_to_list,indent=0]
----
You can use `@RecordOptions(toList=false)` to disable this feature.
=== Obtaining a map of the record component values
We can obtain the component values from a record as a map like so:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_to_map,indent=0]
----
You can use `@RecordOptions(toMap=false)` to disable this feature.
=== Obtaining the number of components in a record
We can obtain the number of components in a record like so:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_size,indent=0]
----
You can use `@RecordOptions(size=false)` to disable this feature.
=== Obtaining the n^th^ component from a record
We can use Groovy's normal positional indexing to obtain a particular component in a record like so:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_get_at,indent=0]
----
You can use `@RecordOptions(getAt=false)` to disable this feature.
== Optional Groovy features
=== Copying
It can be useful to make a copy of a record with some components changed.
This can be done using an optional `copyWith` method which takes named arguments.
Record components are set from the supplied arguments.
For components not mentioned, a (shallow) copy of the original record component is used.
Here is how you might use `copyWith` for the `Fruit` record:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_copywith,indent=0]
----
The `copyWith` functionality can be disabled by setting the
`RecordOptions#copyWith` annotation attribute to `false`.
=== Deep immutability
As with Java, records by default offer shallow immutability.
Groovy's `@Immutable` transform performs defensive copying for a range of mutable
data types. Records can make use of this defensive copying to gain deep immutability as follows:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_immutable,indent=0]
----
These examples illustrate the principal behind
Groovy's record feature offering three levels of convenience:
* Using the `record` keyword for maximum succinctness
* Supporting low-ceremony customization using declarative annotations
* Allowing normal method implementations when full control is required
=== Obtaining the components of a record as a typed tuple
You can obtain the components of a record as a typed tuple:
[source,groovy]
----
include::../test/RecordSpecificationTest.groovy[tags=record_components,indent=0]
----
Groovy has a limited number of `TupleN` classes.
If you have a large number of components in your record, you might not be able to use this feature.
== Other differences to Java
Groovy supports creating _record-like_ classes as well as native records.
Record-like classes don't extend Java's `Record` class and such classes
won't be seen by Java as records but will otherwise have similar properties.
The `@RecordOptions` annotation (part of `@RecordType`) 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 JDK16.
EMULATE::
Produces a record-like class for all JDK versions.
AUTO::
Produces a native record for JDK16+ and emulates the record otherwise.
Whether you use the `record` keyword or the `@RecordType` annotation
is independent of the mode.