Reviewing Project Lombok or the Right Way to Write a Library

You could consider this a parody of my own Spring Batch or How Not to Design an API. Credit where credit is due however and this brings me to Project Lombok.

What is Project Lombok?

Every Java developer knows that Java involves writing a lot of boilerplate code. Create a value object (or “data transfer object”) and you might have a behaviour-less class with a dozen properties. Those twelve statements are the only important thing in the class. After that you create a hundred lines of getters, setters (if not immutable), an equals/hashCode and a toString method, possibly all IDE generated.

This is an error-prone process, even when generated by an IDE. An IDE won’t typically tell you if, say, you add another data member and don’t regenerate your equals and hashCode methods.

Project Lombok seeks to greatly reduce the need for such boilerplate by using annotations to automatically generate it so you don’t have to. This fits in well with my own philosophy:

Lines of Code Are The Enemy

This is not a new or revolutionary idea. Decades ago it was noted that the number of lines of code was an important metric for both programmer productivity and expected bugs, meaning that whether you were dealing with assembly language or Erlang the metrics were roughly the same. This in part explains the steady move towards higher level languages as the more you can get done in one line of code the better.

Bugs per lines of code addresses this point, referencing Code Complete and other sources.

This is why I think things like first-class properties and closures are advantages in the .Net world (over Java): because they can do the same thing with less code, even if you can do the same thing with IDE-generated getters and setters and anonymous inner classes (respectively).

What Can Project Lombok Do?

Project Lombok has seven annotations for minimizing boilerplate code.

@Getter / @Setter

Never write public int getFoo() {return foo;} again.
@ToString
No need to start a debugger to see your fields: Just let lombok generate a
toString for you!
@EqualsAndHashCode
Equality made easy: Generates hashCode and equals implementations
from the fields of your object.
@Data
All together now: A shortcut for @ToString, @EqualsAndHashCode,
@Getter on all fields, and @Setter on all non-final fields. You even get
a free constructor to initialize your final fields!
@Cleanup
Automatic resource management: Call your close() methods safely with
no hassle.
@Synchronized
synchronized done right: Don't expose your locks.
@SneakyThrows
To boldly throw checked exceptions where no one has thrown them before!

The effect can be dramatic. From @Data you can replace this:

import java.util.Arrays;

public class DataExample {
    private final String name;
    private int age;
    private double score;
    private String[] tags;

    public DataExample(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return age;
    }

    public void setScore(double score) {
        this.score = score;
    }

    public double getScore() {
        return score;
    }

    public String[] getTags() {
        return tags;
    }

    public void setTags(String[] tags) {
        this.tags = tags;
    }

    @Override
    public String toString() {
        return "DataExample(" + name + ", " + age + ", " + score + ", " + Arrays.deepToString(tags) + ")";
    }

    @Override
    public boolean equals(Object o) {
        if (o == this) return true;
        if (o == null) return false;
        if (o.getClass() != this.getClass()) return false;
        DataExample other = (DataExample) o;
        if (name == null ? other.name != null : !name.equals(other.name)) return false;
        if (age != other.age) return false;
        if (Double.compare(score, other.score) != 0) return false;
        if (!Arrays.deepEquals(tags, other.tags)) return false;
        return true;
    }

    @Override
    public int hashCode() {
        final int PRIME = 31;
        int result = 1;
        final long temp1 = Double.doubleToLongBits(score);
        result = (result * PRIME) + (name == null ? 0 : name.hashCode());
        result = (result * PRIME) + age;
        result = (result * PRIME) + (int) (temp1 ^ (temp1 >>> 32));
        result = (result * PRIME) + Arrays.deepHashCode(tags);
        return result;
    }

    public static class Exercise {
        private final String name;
        private final T value;

        private Exercise(String name, T value) {
            this.name = name;
            this.value = value;
        }

        public static  Exercise of(String name, T value) {
            return new Exercise(name, value);
        }

        public String getName() {
            return name;
        }

        public T getValue() {
            return value;
        }

        @Override
        public String toString() {
            return "Exercise(name=" + name + ", value=" + value + ")";
        }

        @Override
        public boolean equals(Object o) {
            if (o == this) return true;
            if (o == null) return false;
            if (o.getClass() != this.getClass()) return false;
            Exercise<?> other = (Exercise<?>) o;
            if (name == null ? other.name != null : !name.equals(other.name)) return false;
            if (value == null ? other.value != null : !value.equals(other.value)) return false;
            return true;
        }

        @Override
        public int hashCode() {
            final int PRIME = 31;
            int result = 1;
            result = (result * PRIME) + (name == null ? 0 : name.hashCode());
            result = (result * PRIME) + (value == null ? 0 : value.hashCode());
            return result;
        }
    }
}

with

import lombok.AccessLevel;
import lombok.Setter;
import lombok.Data;
import lombok.ToString;

@Data
public class DataExample {
    private final String name;
    @Setter(AccessLevel.PACKAGE)
    private int age;
    private double score;
    private String[] tags;

    @ToString(includeFieldNames = true)
    @Data(staticConstructor = "of")
    public static class Exercise {
        private final String name;
        private final T value;
    }
}

 

With Annotations? How?

Most developers experience with annotations involves:

  • Putting @Override in overriden methods;
  • Using @SuppressWarnings, often to disable an IDE warning about casting generic collections; and
  • Using API specific annotations in JPA< J2EE, Spring, etc (eg @Resource, @Transactional).

Far fewer developers have ever written an annotation. If you haven’t it’s worth having a read of Sun's Annotation Tutorial. Annotations can be used to basically generate code at compile-time. That’s how Project Lombok works.

Is This Kosher?

There is some debate about this. A certain school of thought believes that code should still compile and possibly even work without the source annotations present. Alternatively, the belief is that annotations shouldn’t be used as a substitute for language features.

Java 7 had debate over many new features, some changing the language. One is first-class properties, to avoid the boilerplate of getters and setters or provide them in a far terser manner, much like C#/.Net does. First-class properties didn’t make it into Java 7. Project Lombok gives a viable alternative.

Possibly more controversial is that you can use @SneakyThrows to throw checked exceptions without declaring them. This stokes the debate that is old as Java itself: are checekd exceptions a mistake? I view them as a failed experiment in software engineering.

So Project Lombok is somewhat controversial (some have even gone so far as to call it a “hack”) so perhaps the best thing to come out of it is the debate about Java and its future. Up until a couple of years ago Java was a hotbed for debate about software design and an incubator for new technologies, methodologies and architectures. Java (though the Spring framework) popularized dependency injection and inversion of control as well as the use of MVC in Web frameworks.

But after Java 5 Sun seems to have lost its way. It lost such luminaries as Joshua Bloch (to Google). Java 5 itself was a huge change and debate still rages about the complexity of Java generics and the wisdom of type erasure. So much so that closures for Java 7 were declined (but out of nowhere, it was announced closures are now coming to Java 7).

Microsoft has demonstrated leadership for pushing the .Net platform forwards to the point that Java is now playing catch-up (which wasn’t the case prior to .Net 3.0/3.5) whereas Java 7 was mired in debate and lacked leadership and vision from Sun about where to go next, complicated by grand misadventures like the stillborn JavaFX.

But anyway, perhaps all this debate will help reinvigorate a Java development community that seems to have given up.

What has Project Lombok Done Right?

Firstly, using Lombok with Maven is easy. I can’t emphasize enough how useful that is. There’s nothing more frustrating than digging around to find the right artifact(s) and/or repositories to get a particular library to work in Maven. Usually it’s not hard but sometimes it is. I have better things to do than try and figure out what someone should just mention in their project’s documentation.

Also the documentation isn’t super-extensive (as, say, Spring’s typically is) but it sure beats some others (eg typical Apache projects). At least there are examples of all the annotations.

Another thing I really like about it is that it generates toString(), hashCode() and equals() methods automatically but unlike some Apache Commons libraries, it doesn’t do it via reflection at run-time.

Lastly, Project Lombok is released under the highly permissive MIT license.

What Could Project Lombok Do Better?

Currently, Project Lombok only integrates (well) with Eclipse. Netbeans and IntelliJ users are out in the cold and support for those two IDEs seems anywhere from far off to never going to happen. I’m a huge fan of IntelliJ and frankly I wouldn’t use any other IDE given the choice. People tend to feel pretty strongly about their IDEs so currently Project Lombok’s lack of IntelliJ support is (unfortunately) a deal breaker.

I’ve had a bit of a look at the IntelliJ plug-in architecture. You can create plug-ins for new languages so all that’s really required is modification to the existing code for Java, assuming it is done with the same mechanism (which is a big and possibly incorrect assumption).

It seems like a serious limitation of IntelliJ’s internal compiler that it can’t handle compile-time annotations like this in a general sense. Surely one compile-time annotation is lie any other, right?

What Features Could Be Added?

Others have taken to this idea, despite the controversy. One such extension is Morbok, which uses the same idea to get rid of the boilerplate of creating loggers in classes.

It’s worth noting that @Cleanup should become obsolete with Java 7 (a year or more from now) with the advent of ARM (“Automatic Resource Management”). Again, this mimics another .Net feature, where such tedious try-catch-finally blocks are abbreviated using using() { … } blocks for anything that implements the IDisposable interface.

One limitation I noticed was that you can’t use @Data on enums. It would be useful to have an enum-specific version of this. One problem that Lombok could solve is the boilerplate around the use of values(). Enum.values() returns an array of the values. Because arrays aren’t immutable this needs to be copied each time you call it. This makes codes like this inefficient:

public enum Gender {
  MALE("M", "male"), FEMALE("F", "female");

  private final static Map LOOKUP;

  static {
    Map map = new HashMap();
    for (Gender gender : values()) {
      map.put(gender.code, gender);
    }
    LOOKUP = Collections.unmodifiableMap(map);
  }

  public static Gender find(String code) {
    return LOOKUP.get(code);
  }

  private final String code;
  private final String description;

  private Gender(String code, String description) {
    this.code = code;
    this.description = description;
  }

  public String getCode() { return code; }
  public String getDescription() { return description; }
}

That could easily be reduced to a couple of Lombok-style annotations. For example:

@Enum
@Finder
public enum Gender {
  MALE("M", "male"), FEMALE("F", "female");

  @Code
  private final String code;
  private final String description;

  private Gender(String code, String description) {
    this.code = code;
    this.description = description;
  }
}

Conclusion

I’m a big fan of Project Lombok. It’s my kind of library: lightweight, practical and doesn’t get in your face, in exactly the way that many Java Web frameworks aren’t. For example, Seam uses a flawed idea (component-oriented JSF) to solve a problem I don’t have. And for those of you that don’t think JSF is a failed experiment, it’s been about to take off for 7+ years now. At some point you just have to accept that it’s not going to work.

I personally come down on the side of the fence that accepts the utility of using annotations as a substitute for language features, especially considering how Java has stalled in that department in recent years (and Java 7 is delayed now til the end of 2010).

If you happen to be an Eclipse user, you’re in luck. Give it a try. If you’re not, well it’s a choice between those red lines under your seemingly non-existent methods and not using Lombok. But hope springs eternal for future IDE support.

4 comments:

R. Spilker said...

William, thanks for the great review. About enums: they all have a generated method valueOf to find the value with by name. So Suits.valueOf("DIAMOND") would actually return Suits.DIAMOND That said, if you want to search on another field, yes you need quite some boilerplate.

William Shields said...

@R.Spiker: yes I'd completely forgotten about valueOf() when I wrote this. Thanks for pointing that out. I've removed that example to correct that as I realized the one example was sufficient to get my point across anyway.

Peter said...

Nice summary!
Especially the conclusion is great: "Seam uses a flawed idea (component-oriented JSF) to solve a problem I don’t have."
!!
lol :-)

andrew said...

As I understand it when a field in injected using the static insertField method, the handled bit is set, and this is supposed to stop HandleGetter creating a getter as it is a generated field.I use insertField, but it is still generating a getter. I guess that means I need to do something more.


r4i

Post a Comment