| ////////////////////////////////////////// |
| |
| 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. |