Trouble with Properties in Groovy Constructors

·

5 min read

I ran into some issues with groovy constructors today. The real code is rather complicated, so here's a contrived sample that illustrates the issue.

Take this very simple groovy class:

class Person {
  String firstName
  String lastName
  String fullName
}

Since the class has no constructor, groovy creates an empty no-arg constructor for us and allows us to pass in named parameters. If you've never seen this before, it's a pretty nice feature:

def person = new Person(firstName: 'Jane', lastName: 'Doe')
assert person.firstName == 'Jane'
assert person.lastName == 'Doe'

Now let's say I decide that I want to do something every time a Person is instantiated - maybe write a message to the console. So I declare a no-arg constructor and output the message:

Person() {
  println 'A new person was created!'
}

Wait - does using named parameters still work even though we declared a constructor and it's no longer being created by the compiler? Yes it does, as it says in the groovy documentation:

If no (or a no-arg) constructor is declared, it is possible to create objects by passing parameters in the form of a map (property/value pairs).

class Person {
  String firstName
  String lastName
  String fullName

  Person() {
    println 'A new person was created!'
  }
}

def person = new Person(firstName: 'Jane', lastName: 'Doe')
assert person.firstName == 'Jane'
assert person.lastName == 'Doe'

So far so good. But wait - we notice that most users of this class pass in the first and last names but not the full name. That shouldn't be a problem - we can set the full name in the constructor.

class Person {
  String firstName
  String lastName
  String fullName

  Person() {
    // Set the full name if it wasn't passed in
    // Real code would check to see if firstName or lastName is null
    fullName ?= "$firstName $lastName"
  }
}

Everything's great, right? Not so fast. This doesn't work as expected:

def person = new Person(firstName: 'Jane', lastName: 'Doe')
assert person.firstName == 'Jane'
assert person.lastName == 'Doe'
assert person.fullName == 'Jane Doe'

image001.png

What's going on?

Back to the groovy docs:

When no (or a no-arg) constructor is declared, Groovy replaces the named constructor call by a call to the no-arg constructor followed by calls to the setter for each supplied named property.

The no-arg constructor is called first, and only afterwards are the setters called for each named property. So when we try to set fullName in the constructor, firstName and lastName have not been set yet.

Hmmm.... what's the solution?

I will spare you all my misguided attempts and just tell you that after a lot of trial and error and googling, I discovered that the MapConstructor annotation takes a post element which allows you to declare a closure containing statements which will be appended to the end of the generated constructor. In other words, you can annotate the class with @MapConstructor, which will cause a public no-args constructor to be created, and you can give it some code to run after the constructor is run and after the property setters are called. Nice, no?

import groovy.transform.MapConstructor

@MapConstructor(post={ init() })
class Person {
  String firstName
  String lastName
  String fullName

  void init() {
    // Set the full name if it wasn't passed in
    // Real code would check to see if firstName or lastName is null
    fullName ?= "$firstName $lastName"
  }
}

def person = new Person(firstName: 'Jane', lastName: 'Doe')
assert person.firstName == 'Jane'
assert person.lastName == 'Doe'
assert person.fullName == 'Jane Doe'