Home > Articles > Thoughts on Effective Java

Thoughts on Effective Java

I'm reading the second edition of Effective Java in a group at work and writing some thoughts/notes about it here.

Chapter 2

Item 1 is about static factory methods.

The leader of my group offered a point that static factory methods can be hard to mock. I don't have a ton of experience at this time with mocking objects, so I haven't seen that first hand, but I'll trust him and keep it in mind for the future.

Item 1, advantage 4 says that static factory methods are good because they reduce verbosity in creating objects. The example they give is:

Map<String, List<String>> m = new HashMap<String, List<String>>();

Java 7 introduces the diamond operator, so this can become:

Map<String, List<String>> m = new HashMap<>();

thereby negating the verbosity-decreasing benefit of static factory methods.

I'm not trying to say the book is wrong. It says "Revised and Updated for Java SE 6" on the cover, and for Java 6, that seems like a valid argument. I just think it's interesting how new language features can change what constitues a best practice. The book even says, "Someday the language may perform this sort of type inference on constructor invocations as well as method invocations."

Item 2 is about builders.

In the process of talking about builders, the author talks about the JavaBeans pattern. This pattern seems like a terrible idea to me; forgetting to set a required parameter is relatively easy and could have disastrous results. The Builder pattern seems like a better choice because it's a way to give the compiler more information. I would rather have my IDE yell at me at compile time that my object isn't instantiated correctly than wrestle with bugs at run time.

Builders do introduce more code to write/maintain/test, but (as my group leader pointed out) the IDE can generate the class for you.

The book has required parameters going in the builder's constructor and optional parameters being set by additional methods. My question is: what if the number of required parameters gets large? Then you haven't solved your problem at all. One option would be to move the required parameters into methods, but then you're not providing the compiler with the information to know that some of the parameters are required. Yes, the build() method can check for them and perhaps throw an exception, but that only happens at run time.

I think that if you have so many required parameters, there might be something wrong with your design. Perhaps some arguments are logically related and should be combined into an instance of a new class which binds them together. I'm not sure if this is always the case, but it seems like it often would be.

Item 3 talks about singletons.

Our group leader told us to beware of the hidden state that often comes along with singletons. I have definitely run into that issue. When functions use state that is not from a parameter, things can get tricky. Knowing what state is used and how that will affect the execution of the function can be difficult for the developer.

While the book says that, "a single-element enum type is the best way to implement a singleton," our leader disagreed. He argues that using an enum is less readable. I had a similar gut feeling when I first read this part of the Item; I would not have thought to use an enum to implement a singleton. In my mind, an enum represents an enumeration of a few different kinds of things; a singleton is something that there will only ever be one of. These two ideas seem at odds.

Item 4 talks about noninstantiable classes. These classes are often used for utility methods.

Again, apparently static things are difficult to mock. And Java's garbage collector has apparently gotten good enough that doing something like (new FileUtility()).getPermissions(file) will result in the created FileUtility being garbage collected very quickly. This all happens fast enough that there is very little performance impact.

Item 7 says to avoid finalizers. I learned C++ in school, so the lack of guarantees with finalizers throws me off. In any case, I've never heard someone seriously advocate for finalizer usage.

Chapter 3

Item 8 is about the general contract for equals. The terms used in the contract are familiar from discrete math.

For value objects, you want to override equals(). equals() gets tricky when inheritance comes into play, though. One solution to that problem is to not use inheritance with value object -- make your value objects final. Inheritance can be useful for business logic, so feel free to use inheritance in that case. For classes that implement business logic, though, it doesn't make much sense to implement equals().

An interesting thing I hadn't thought about is that instanceof checks for null:

public class Main
{
    public static void main(String[] args)
    {
        String nullString = null;
        System.out.println(nullString instanceof String);
    }
}

Output:

false

One suggestion was to use an EqualsBuilder, like the one supplied in Apache Commons. Apparently, equals builders will help you avoid NPE's. To me, this seems like a cop-out. I don't think it's too terribly difficult to avoid writing an equals() method which won't throw an NPE; perhaps I haven't written enough Java.

Item 9 is about hashCode(). Equal objects must have equal hash codes.

The book contains an overview of how to write a hashCode() method that's good enough. Ground-breaking, cutting-edge, crazy high-performance hashing functions are going to be class-specific. Sometimes, this is fairly obvious; if your class has a unique ID, you can just return that as your hash code.

Item 10 is about toString(). This method should only be used for debugging or logging purposes.

The StringBuilder class gets this wrong--its toString() method is used for programmatic access to the string that is being built. It should probably have another method called build() to provide programmatic access, and leave toString() for logging purposes.

Item 11 is about clone(). To be frank, I find the Cloneable interface confusing, and I haven't run into a good use for it.

Item 12 is about compareTo(). The hard thing with compareTo() is that it's not particularly explicit about what the "natural" ordering means. By contrast, a Comparator<> can have a name which gives developers more information about how the comparison is done. This explicit information is probably good.

As of Java 7, if you break the general contract for comparisons, an exception will be thrown.