BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage News Groovy 4.0.0 Introduces Switch Expressions and Sealed Types

Groovy 4.0.0 Introduces Switch Expressions and Sealed Types

This item in japanese

Version 4.0.0 of Apache Groovy introduces switch expressions, sealed types, built-in type checkers, built-in macro methods and incubating features such as records, JavaShell, POJO annotation, Groovy contracts, Groovy-Integrated Query and TOML support. This release also contains several smaller improvements and breaking changes due to features removed from this latest version.

Developers are encouraged to change their dependencies when upgrading to Groovy 4.0.0 as the Maven groupId has changed from org.codehaus.groovy to org.apache.groovy.

The Java Platform Module System (JPMS) doesn’t allow the same class and package name in multiple modules. Groovy 3 still provided duplicate versions of classes, however, those have been removed in Groovy 4 and new classes, such as groovy.xml.XmlSlurper, groovy.xml.XmlParser, groovy.ant.AntBuilder, groovy.test.GroovyTestCase should be used.

Switch expressions are now supported to complement the already existing switch statements:

def result = switch(i) {
    case 0 -> 'January'
    case 1 -> 'February'
    //...
    default -> throw new IllegalStateException('Invalid number of month')
}

A code block may be used for multiple statements:

case 0 -> { def month = 'January'; def year = Year.now().getValue(); 
    month + " " + year }

The implementation of switch differs from Java as not all possible values need case branches. When a default branch is not supplied, Groovy implicitly adds a default branch, returning null.

Sealed classes may be created with the sealed keyword or the @Sealed annotation. Permitted subclasses are automatically detected when compiled at the same time. The permits clause can be used together with the sealed keyword and the permittedSubclasses attribute can be used together with the @Sealed annotation to explicitly define the permitted subclasses:

@Sealed(permittedSubclasses = [Dog, Cat]) interface Animal {}
@Singleton final class Dog implements Animal {
    String toString() { 'Dog' }
}
@Singleton final class Cat implements Animal {
    String toString() { 'Cat' }
}
​​​​​​​sealed interface Animal permits Dog, Cat {}
@Singleton final class Dog implements Animal {
    String toString() { 'Dog' }
}
@Singleton final class Cat implements Animal {
    String toString() { 'Cat' }
}

Records, one of the new incubating features, are quite comparable to the existing @Immutable Groovy feature:

record Student(String firstName, String lastName) { }

The resulting class is implicitly final, with automatically generated firstName and lastName as private final fields, firstName() and lastName() methods, a default constructor with both arguments, a serialVersionUID of 0L and toString(), equals() and hashcode() methods.

Groovy provides built-in type checkers to either weaken type checking or strengthen type checking. This release introduces the groovy-typecheckers module which contains several type checkers. The @TypeChecked annotation may be used, after adding the dependency to the project, in order to verify if a regular expression is valid during compilation. The following expression misses the closing bracket which normally results in a runtime error:

@TypeChecked(extensions = 'groovy.typecheckers.RegexChecker')
def year() {
    def year = '2022'
    def matcher = year =~ /(\d{4}/
}

However, the type checker displays the error during compilation:

Groovyc: [Static type checking] - Bad regex: Unclosed group near index 6
(\d{4}

Comparable to type checkers, some macro methods are now available through the groovy-macro-library module such as the SV macro that creates a String from the variable names and values:

def studentName = "James"
def age = 42
def courses = ["Introduction to Java" , "Java Concurrency", "Data structures"]

println SV(studentName, age, courses)
studentName=James, age=42, courses=[Introduction to Java, Java Concurrency, 
    Data structures]

The NV macro creates a NamedValue which allows for further processing of the name and value:

def namedValue = NV(age)
assert namedValue instanceof NamedValue
assert namedValue.name == 'age' && namedValue.val == 42

The incubating JavaShell feature enables developers to run Java code snippets:

import org.apache.groovy.util.JavaShell

def student = 'record Student(String firstName, String lastName) {}'
Class studentClass = new JavaShell().compile('Student', student)
assert studentClass.newInstance("James", "Gosling")
    .toString() == 'Student[firstName=James, lastName=Gosling]'

POJO annotation is another incubating feature that allows using Groovy as a pre-processor comparable to Lombok. The @POJO annotation indicates that the class is more of a POJO than an advanced Groovy object and needs the @CompileStatic annotation to be processed.

The groovy-contracts incubating module allows the specification of class-invariants, and pre- and post-conditions on classes and interfaces:

import groovy.contracts.*

@Invariant({ coursesCompleted >= 0 })
class Student {
    int coursesCompleted = 0
    boolean started = false

    @Requires({ started })
    @Ensures({ old.coursesCompleted < coursesCompleted })
    def processCourses(newCoursesCompleted) { 
        coursesCompleted += newCoursesCompleted 
    }
}

An error message is shown whenever a precondition isn’t met, such as the started precondition which should be true:

def student = new Student()
student.processCourses(2)
org.apache.groovy.contracts.PreconditionViolation: <groovy.contracts.Requires> 
    Student.java.lang.Object processCourses(java.lang.Object) 

started
|
false

Another error message is shown whenever the post-condition isn’t met such as when the number of processed courses declines:

def student = new Student()
student.setStarted(true)
student.processCourses(-2)
org.apache.groovy.contracts.PostconditionViolation: <groovy.contracts.Ensures> 
    Student.java.lang.Object processCourses(java.lang.Object) 

old.coursesCompleted < coursesCompleted
|   |                | |
|   0                | -2
|                    false

The incubating Groovy-Integrated Query (GINQ or GQuery) feature allows querying collections such as lists, maps, custom domain objects, or other structured data, much like SQL:

from student in students
orderby student.age
where student.age > 20
select student.firstName, student.lastName, student.age
from student in students
leftjoin university in universities on student.university == university.name
select student.firstName, student.lastName, university.city

Support for TOML-based files, still in incubation, is provided with the groovy-toml module to build and parse an object graph:

def tomlBuilder = new TomlBuilder()
tomlBuilder.records {
    student {
        firstName 'James'
        // ...
    }
}

def tomlSlurper = new TomlSlurper()
def toml = tomlSlurper.parseText(tomlBuilder.toString())

assert 'James' == toml.records.student.firstName

Various smaller improvements were introduced such as caching the GString toString values to increase performance. Ranges could be specified as inclusive 1..10 or exclusive on the right 1..<10; now it’s also possible to be exclusive on the left 1<..10 or both 1<..<10. A leading zero for fractional values is now optional, so both .5 and 0.5 are now supported.

Groovy 4 has some breaking changes, such as the removal of the Antlr2 parser, and the generation of call-site based bytecode is no longer possible. Some modules such as groovy-jaxb and groovy-bsf have been removed. The groovy-all pom now includes the groovy-yaml module, while the groovy-testng module was removed.

The complete list of breaking changes and new features is available in the release notes.

About the Author

Rate this Article

Adoption
Style

BT