Sunday, April 26, 2015

Generating getters setters toString hashCode and equals

Generating code is a good way to remove so much boilerplate in Java that you would otherwise write by hand. Getters and setters is a big one. Eclipse can generate those, but Eclipse generation clashes with Checkstule rules that I use. The toString(), equals() and hashCode() methods are important to get right too. Previously I set up Eclipse templates but I still had to fill in the details by hand. Most of the time it's better to start with a default implementation for each that uses every non-static field in the class.

This post demonstrates some code I have written to automate the generation of these things.

Checkstyle vs Getters and Setters

I use Checkstyle wherever I can. It encourages me to write code with a consistent style and, like FindBugs, helps me to avoid common errors. Here are some Checkstyle rules that I use:

  1. Ensure that I have a jadvadoc comment on fields and methods. This includes checking that I have a @return tag for non-void methods and a @param tag for each parameter. Javadoc comments let me impart important context that cannot always be achieved by self documentating code.
  2. Final parameters on methods. Avoid accidentally re-assigning a parameter during method execution by making sure that all parameters are delcared as final. If I need a primitive (or wrapper or other immutable object like String) whose value will change, it is clearer to define a new local variable and assign it an initial value from the corresponding final parameter if needed.
  3. Avoid hidden fields. Don't have a parameter with the same name as an instance variable, because it is too easy to accidentally modify the wrong one. Using this on the instance variable will avoid these errors, but it is easy to forget.

In my opinion, these are very good rules to live by when coding and as I write new methods, I find myself automatically implementing these rules: they have become muscle memory for me.

However these rules have a cost when dealing with getters and setters. Let's say I create a new class, add ten fields, and in Eclipse select Source > Generate getters and setters > select all > OK and bam, I have ten corresponding accessor and mutator methods for my ten fields. Now I would have to edit twenty methods to correct the comments and edit ten methods to make parameters final and give them a different name. Here is an example of the getters and setters generated by Eclipse.

private String name;


private int id;


/**
 * @return the name
 */
public final String getName() {
   return name;
}


/**
 * @param name the name to set
 */
public final void setName(String name) {
   this.name = name;
}


/**
 * @return the id
 */
public final int getId() {
   return id;
}


/**
 * @param id the id to set
 */
public final void setId(int id) {
   this.id = id;
}

Eclipse generation cannot, as yet, make parameters final or give them different names, and the comments are very basic and only use the parameter name to make them different. It is less likely that following the Checkstyle rules will create as much value on getters and setters as on other methods. Getters and setters are very simple methods, usually one-liners. Because they are so simple that they can be auto-generated, we don't generally modify them after creation - so there is less need to give them the protection of checkstyle rules like final parameters and avoiding hidden fields. Thus, the cost of editing so many methods is rarely worth the value it gives.

Having said all that, there a couple of reasons why I still want to follow these rules, even on getters and setters if I can cut the cost significantly.

  • Comments are still valuable for fields/getters/setters that cannot be truly un-ambiguously named, which happens often enough no matter how much thought you can put into it. For the simple ones, simple comments are fine.
  • If my Eclipse Problems or Markers view is filled up with dozens of Checkstyle warnings from getters and setters, it is harder for me to spot the warnings I really should take care of. So I would prefer to keep my Problem/Markers view clean and automatically implement the Checkstyle rules if possible.

Generate Boilerplate

So here is how I have attempted to fix this issue and make it as painless as possible to generate getters and setters and implement the Checkstyle rules I want. At the same time, I will generate decent default toString(), equals() and hashCode() methods to copy and paste too.

As a pre-requisite, I have checked out my utils project from GitHub into Eclipse: https://github.com/robertmarkbram/RobUtils so that I can use three classes in particular:

  • OutputFieldList.java which has some utility functions that use reflection to inspect a class and do things I sometimes find useful such as
    • output a list of fields or getters and setters that are present in a class.
    • output sample equals(), hashCode() and toString() impelementations using the commons lang builders.
    • outputs sample getters and setters from fields defined in a class.
  • FieldComment.java which is an annotation that stores a field's comment that OutputFieldList will read and output as javadoc comments for getter and setter fields. It's based on a solution Abhi wrote about in this StackOverflow answer to the question: How to get a JavaDoc of a method at run time?
  • FieldCommentSampleClass.java which is just a place I copy the field declarations into so that I can use the FieldComments annotation in the same project as the other things in RobUtils.

I begin by writing out the fields in the actual class they belong to. I write out comments for each.

public class Foo {
   /** User friendly label for display. */
   private String name;

   /** Unique ID within datastore. */
   private int id;

}

I then copy these comments into FieldCommentSampleClass.java, which is just a scratch pad that has the advantage of being in the same project as the @FieldComment annotation. Within FieldCommentSampleClass.java, I do a couple of find and replace operations to change the javadoc comments to @FieldComment annotations.

  1. Replace /**  with @FieldComment(comment=".
  2. Replace */ with ").

So that now have fields with the @FieldComment annotation:

public class FieldCommentSampleClass {
   @FieldComment(comment="User friendly label for display. ")
   private String name;

   @FieldComment(comment="Unique ID within datastore. ")
   private int id;
}

I then run the main method in OutputFieldList.java with these two calls. This will generate getters and setters, plus decent toString(), hashCode() and equals() methods for me.

public static void main(final String[] args) throws Exception {
   generateGetAndSetMethods(FieldCommentSampleClass.class, //
      OutputFields.NO_FIELDS, //
      OutputGetters.OUTPUT_GETTERS, //
      OutputSetters.OUTPUT_SETTERS);
   outputToStringHashAndEquals(Foo.class);
}

I now have the following output that I copy and paste into my original class.

/** @return user friendly label for display.  */
public String getName() {
   return name;
}

/** @return unique ID within datastore.  */
public int getId() {
   return id;
}


/** @param theName user friendly label for display.  */
public void setName(final String theName) {
   this.name = theName;
}


/** @param theId unique ID within datastore.  */
public void setId(final int theId) {
   this.id = theId;
}


@Override
public boolean equals(final Object obj) {
   if (obj == this) {
      return true; // test for reference equality
   }
   if (obj == null) {
      return false; // test for null
   }
   if (obj instanceof FieldCommentSampleClass) {
      final FieldCommentSampleClass other = (FieldCommentSampleClass) obj;
      // @formatter:off
      return new org.apache.commons.lang.builder.EqualsBuilder()
            .append(name, other.name)
            .append(id, other.id)
            .isEquals();
      // @formatter:on
   } else {
      return false;
   }
}


@Override
public int hashCode() {
   // @formatter:off
   return new org.apache.commons.lang.builder.HashCodeBuilder()
         .append(name)
         .append(id)
         .toHashCode();
   // @formatter:on
}


@Override
public String toString() {
   org.apache.commons.lang.builder.ToStringBuilder.setDefaultStyle(org.apache.commons.lang.builder.ToStringStyle.SHORT_PREFIX_STYLE);
   // @formatter:off
   return new org.apache.commons.lang.builder.ToStringBuilder(this)
      .append("name", name)
      .append("id", id)
      .toString();
   // @formatter:on
}

Final notes

A few notes to keep in mind.

  • OutputFieldList needs JDK 8 because it uses the Stream API with lambdas.
  • The toString(), equals() and hashCode() methods generated use the fully qualified class names for the apache commons lang builders. I them use Source > Add Import (or control+shift+m) to replace the fully qualified class names with just the class names.
  • If the code I am generating boiler plate for relies on custom code (e.g. field types are not standard java types), I may have to modify my RobUtils project so that it can reference those types, which is easy: Project > Properties > Java Build Path > Projects tab > Add and select the projects I need to reference.
  • I am pragmatic about following these Checkstyle rules.
    • When editing code written by someone else that has Checkstyle warnings, should I fix them? Perhaps. If there are just a few and I have time, sure. If there are many then only fix the ones in the actual lines of code I need to edit.
    • I don't force my own practices on other developers in a team. If the team has a policy already in place around the use of tools like Checkstyle or Findbugs, that takes precedence. Otherwise I practice what makes sense to me. If other developers are interested, explain how I use the tooling to help me write good code, but don't proselytise.
    • As I mentioned above, there is a cost to taking on processes like Checkstyle rules. If the cost is too high (i.e. there is little benefit to be gained), or the rules doesn't make sense to you, don't do it. Checkstyle warnings can be suppressed for a class, method or code block and rules can be removed entirely from a project specific configuration if the rule is too burdensome.