Monday, September 20, 2010

Common Java Object Functionality with Project Lombok

Project Lombok is a small library that can be used to reduce the amount of boilerplate Java code that is commonly written for Java classes.  Project Lombok does this via annotations that can be added to the Java class for which common methods are desired.  Most of the annotations are self-descriptive in their names: @Getter, @Setter, @EqualsAndHashCode, @ToString, and @NoArgsConstructor are examples.  In this post, I demonstrate going applying simple Lombok annotations to add these commonly written methods to a Java class.

Here is a simple class with no predefined overridden version of toString().

toString-less Person.java
package dustin.examples;

/**
 * Simple Person class without boilerplate.
 *
 * @author Dustin
 */
public class Person
{
   private String lastName;

   private String firstName;
}

When the above class is generated and its implicitly inherited (from Object) toString() method is called, the output looks like that shown in the next image.


We could write an explicit toString() method or use Project Lombok. The next code snippet demonstrates the Project Lombok approach.

Person.java with Lombok's @ToString Annotation
package dustin.examples;

import lombok.ToString;

/**
 * Simple Person class without boilerplate.
 *
 * @author Dustin
 */
@ToString
public class Person
{
   private String lastName;

   private String firstName;
}

The output from printing this class's contents with a Lombok-provided toString() are shown next.


There is a better toString() representation of the Person object now, but its fields are still not initialized, so we only see null values. We can use Lombok again to create the constructor.

Person.java with Lombok's @AllArgsConstructor Annotation
package dustin.examples;

import lombok.AllArgsConstructor;
import lombok.ToString;

/**
 * Simple Person class without boilerplate.
 *
 * @author Dustin
 */
@ToString
@AllArgsConstructor
public class Person
{
   private String lastName;

   private String firstName;
}

I now can (in fact, must) pass in parameters upon instantiation of the Person object. The results are shown in the next screen image. In this case, my client code (Main.java) shows a compile-time error in NetBeans because NetBeans doesn't believe that there is a constructor in Person accepting two Strings. Despite the red squiggly marks, the code does build when I ask NetBeans to build it.



A class like Person.java is often a data class that will need to be used in comparisons and possibly hashCode-based collection keys. It is important to create equals(Object) and hashCode() implementations correctly and to make sure they are created together. Because there are default equals and hashCode methods provided by the parent Object class, Java code using Person instances will be able to perform equals and/or hashCode, but they aren't likely to be what one really wants. When the Main executable class is changed to the next code listing, we see the output after that which tells us that the equality comparison is being done completely based on identity rather than on content.

Main.java That Tests equals() Implementation
package dustin.examples;

import static java.lang.System.out;

/**
 * Simple Main for uses Project Lombok-powered classes.
 *
 * @author Dustin
 */
public class Main
{
   public static void main(final String[] arguments)
   {
      //final Person person = new Person();
      final Person person = new Person("Miles", "Linda");
      out.println(person);

      final String sameLastName = "Smith";
      final String sameFirstName = "Sam";
      final Person person1 = new Person(sameLastName, sameFirstName);
      final Person person2 = new Person(sameLastName, sameFirstName);
      if (person1.equals(person2))
      {
         out.println("Same person!");
      }
      else
      {
         out.println("Different people!");
      }
   }
}


This is almost never what is wanted here. Instead, an explicit equals implementation is required. I like the fact that the Lombok annotation for this, @EqualsAndHashCode, only generates both of these together because it does not make sense to explicitly override them individually. The Person.java class listing is shown next with the addition of the @EqualsAndHashCode annotation.

Person.java with @EqualsAndHashCode
package dustin.examples;

import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.ToString;

/**
 * Simple Person class without boilerplate.
 *
 * @author Dustin
 */
@ToString
@AllArgsConstructor
@EqualsAndHashCode
public class Person
{
   private String lastName;

   private String firstName;
}

The output is better now.


I still don't have a good way to access each public field separately if needed. For example, if I wanted to do something in my code based on last name, I don't have a good way to get at that without taking drastic steps. I can use Lombok here again.

For this example, we'll suppose we made a faulty assumption that only the person's last name might change. Because of this assumption, we'll only provide a Lombok @Setter annotation for last name, but with provide an @Getter annotation for both fields. The changed Person code is shown next.

Person.java with @Getter and @Setter
package dustin.examples;

import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

/**
 * Simple Person class without boilerplate.
 *
 * @author Dustin
 */
@ToString
@AllArgsConstructor
@EqualsAndHashCode
public class Person
{
   @Getter
   @Setter
   private String lastName;

   @Getter
   private String firstName;
}

Here is the updated Main class to run this example:

Main.java that Makes Use of New Setter/Getter
package dustin.examples;

import static java.lang.System.out;

/**
 * Simple Main for uses Project Lombok-powered classes.
 *
 * @author Dustin
 */
public class Main
{
   public static void main(final String[] arguments)
   {
      //final Person person = new Person();
      final Person person = new Person("Miles", "Linda");
      out.println(person);

      final String sameLastName = "Smith";
      final String sameFirstName = "Sam";
      final Person person1 = new Person(sameLastName, sameFirstName);
      final Person person2 = new Person(sameLastName, sameFirstName);
      if (person1.equals(person2))
      {
         out.println("Same person!");
      }
      else
      {
         out.println("Different people!");
      }

      final Person accessiblePerson = new Person("Garzminski", "Gary");
      out.println("The last name is " + accessiblePerson.getLastName());
      out.println("The first name is " + accessiblePerson.getFirstName());
      //accessiblePerson.setFirstName("Grady");
      accessiblePerson.setLastName("Garfunkel");
      out.println("The new last name is " + accessiblePerson.getLastName());
   }
}

I had to comment out the call to set the Person's first name so that the code would build. It now runs as shown in the next screen snapshot.


It is probable that this collection of Lombok annotations would be commonly desired, especially for data-oriented classes. For this reason, Project Lombok provides aggregated annotations such as @Data that provide a collection of these annotations. In this case, I could have gotten very similar behavior to the several individual annotations I provided by using @Data. The @Data annotation leads to Lombok apply @Getter to all fields and @Setter to all non-final fields. The other major difference from what I used is that it uses @RequiredArgsConstructor rather than @AllArgsConstructor.

One of the best ways to see what Project Lombok has done to the compiled .class file is to use javap. This is shown in the next screen snapshot.


We see in this output that a bunch of the methods commonly seen an boilerplate code are available in the compiled Person.class. There is a two-argument parameterized constructor, hashCode(), equals(Object), toString(), and the expected get and set methods.

Project Lombok is not without concerns and limitations.  Many of these are articulated in responses to Hamlet D'Arcy's post Java Without the Boilerplate - Project Lombok. One limitation is the reduced support in IDEs other than Eclipse (though there is decent NetBeans support and javac is supported).  A concern is the need for others using and maintaining the code to have a new dependency on Lombok.  This concern can be somewhat mitigated through the use of the delombok, which could be used in the build process if necessary.

Other articles and blog posts covering Project Lombok include the Project Lombok - Never Write Java Boilerplate Code Again, Java Without the Boilerplate - Project Lombok, Project Lombok: Bye Bye BoilerplateJava Posse's Project Lombok InterviewProject Lombok: Put an End to Java Verbosity, Project Lombok - A Must-Have in Your Java Toolkit, Project Lombok: Interesting Bean Shortcuts with Annotation Processor, Interview: Reinier and Roel on Lombok, Reducing Boilerplate Code with Project Lombok, Rapid Development with LombokLombok Reduces Your Boilerplate Code, and Better Alternative for Getters and Setters.

2 comments:

Dustin said...

A just-published blog post on Project Lombok is Tired of Getters and Setters? Lombok them!

Morten Christensen said...

I also looked at Project Lombok but deemed it too risky to use (f.x. there are issues with the latest JDK). I also found the customisation features lacking, so I just released my own open source tool for generating java value objects that used 100% standard java features and is extremely customisable. You can check it out at "http://valjogen.41concepts.com". Let me know what you think?