I'm reading the second edition of Effective Java in a group at work and writing some thoughts/notes about it here.
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.
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.