Things I’ve learned from Effective Java – Part 17 min read

In this series, I want to introduce one of the most-read books in Java programming: Effective Java. For the most part, this book focuses on the most fundamental classes and libraries, how to use them judiciously, and what we can do to avoid traps and pitfalls that are usually found. Each chapter consists of several items presented in a short-form essay that provides a specific advice and also also concrete code examples to demonstrate the idea.

If you’re a Java programmer and have yet to read this book, I highly recommend you read it. There are over 80 items in this book, and we cannot go through them all at once in this blog post, but I want to extract and summarize some of the items that I’ve found intuitively easy to apply in practice.

Consider static factory methods instead of constructors

This is the first item of the book, where the author encourages users to use static factory methods in place of constructors, which raises several advantages:

  • Unlike constructors, static factory methods have names; a static factory method with a well-chosen name will make the code much easier to read. For example, the BigInteger(int, int, Random), which returns a BigInteger that’s probably prime, would be better expressed by BigInteger.probablePrime.
  • Static factory methods don’t require creating a new object each time they’re invoked. This allows immutable classes to use existing instances, for example, the Boolean.valueOf(boolean) never constructs a new object.
  • The third advantage is that the static factory method can return an object of any subtype of their return type, which gives us the flexibility of choosing a class for the returned object. By doing so, we don’t have to make all classes public to return them. For example, there are over 40 implementations of the Collection framework, instead of exposing over 40 public classes, these class instances are uncovered by static factory method of the java.util.Collections class, such as Collections.singletonList.
  • The class returned return from the static factory method can vary depending on the input parameters. Any subtype of the returned type is permissible.
  • some other advantages…

Always override toString

In the chapter “Methods Common to All Objects,” the author has noted an item that we should always override toString. Why? Because the default behavior of the Object class, which is the parent class of all other classes, implements the toString method by the name of that class + @ + some random number (e.g., PhoneNumber@123b54) which doesn’t look quite instructive. If provide a good toString for the class, the class will much more pleasant to use and easier to debug. For example, 123-456-789 indeed gives us more information than PhoneNumber@123b54, also if we want to debug the application, providing a good toString method for PhoneNumber can give us usable diagnostic message:

System.out.println("Fail to connect to the phone number: " + phoneNumber);

The benefit of the suitable toString method is not limited to the class itself but is also applicable to classes that use it, especially collection classes. For example, while printing entries in the phone number map, we’d instead prefer namvdo=123-456-789 to namvdo=PhoneNumber@123b125.

Also, a good toString method should include all useful information of the class containing it, if the class has a lot of fields, it’s necessary to introduce fields that are conducive to the string representation and discard the unnecessary ones.

Finally, if your class provides public APIs and is widely used by others, it’s essential to document your intentions clearly on whether or not you have specified the format returned from your toString method.

Use public accessors, not public fields for public classes

During your career, sometimes you may stumble upon code which looks something like this:

public class City {
  public double lon;
  public double lat;
  public String name;
}

Why is this one bad? The first apparent reason is the City class breaks encapsulation. Now you cannot change the Point representation without changing the API, and also, you cannot enforce any invariants. For example, if we want the name of any City instance not to be null or blank, we cannot oblige that, and if there is any change in your City representation, the clients may break. So, we can rewrite the City class by providing the accessors and mutators, which is easily generated by modern IDEs.

Let’s look at another example in which a class holds a task to execute and the initial delay before executing this task:

public class HealthCheckTask {
    public Task task;
    public long initialDelay;
}

If any client can access to the reference of HealthCheckTask instance, they can easily violate the constraints that the initialDelay must not be negative:

HealthCheckTask healthCheckTask = ...;
healthCheckTask.initialDelay = -1L;
...

To fix that, we make its states private and expose the public accessors and mutators:

public class HealthCheckTask {
   private Task task;
   private long initialDelay;
   
   public void setInitialDelay(long initDelay) {
       if (initialDelay < 0) {
         throw new IllegalArgumentException("Initial delay must not be negative.");
       }
       this.initialDelay = initDelay;
   }
   
   // other getters and setters 
}

Don’t use raw types

Generic in Java is one feature that empowers us to write a type-safe program. Generic classes or interfaces are those accepting types as parameters. For example, List<String> can be read as a “List of Strings”. We can expect only string instances that can be added to this list while adding other types not string would raise the compile time error, which is what we desire. However, if using raw types, we will lose all benefits of generic, e.g., raw type of List<E> is List. For example:

List colors = new ArrayList();
colors.add(Colors.RED);
colors.add(Colors.BLUE);
colors.add(Colors.GREEN);
...
colors.add(new Pencil()); // compile just fine

The code fragment above will compile just fine and only give us some vague warnings, but clearly don’t want to add a pencil to a list of colors; if later we want to access the list elements, there can be some errors thrown at runtime:

for(int i = 0; i < colors.size(); i++) {
    Color color = (Collor) collors.get(i); // ClassCastException
    System.out.println(color.getColor());
}

But instead, if you provide the type parameter for your collection class, we can eliminate the casting and compile-time error if we mistakenly add a pencil to colors:

List<Color> colors = new ArrayList<>();
colors.add(Colors.RED);
...
colors.add(new Pencil()); // incompatable type, Pencil cannot be converted to Color

for(Color color : colors) {
  ...
}

The author also mentioned the difference between the List (raw type) and the List<Object>; the former is opted out of the generic system, while the latter preserves it. They are not the same thing:

public void safeAdd(List<Object> things, Object elem) {
  things.add(elem); 
}

...
safeAdd(new ArrayList<String>(), "123"); // compile time error 

As we can see, if we pass a list of strings when it requires a list of objects, we eventually get the type-safe benefit from this fact. However, if we use a raw type, we gain no advantages and likely having an exception at runtime:

public void notSafeAdd(List things, Object elem) {
  things.add(elem);
}

notSafeAdd(new ArrayList<String>(), 123); // compile OK

Refer to objects by their interfaces

In this item, the author recommends, if appropriate, using interfaces as types for all variables, fields, parameters, and return values. One of the most obvious reasons is that by referring to the interface, we later switch to different implementations on the existing class without affecting clients. For example:

Map<String, Integer> studentIdsToScores = new HashMap<>(); // recommended!
HashMap<String, Integer> studentIdsToScoresMap = new HashMap<>(); // not recommended

In the first approval, if later we want to change the implementation, for example, switch to a TreeMap to get the alphabet order to the student id, we can replace the new HashMap<>() with the new TreeMap<>() and also provides the custom order if needed. But in second case, we also need to change the LHS, and if the client using any methods that doesn’t exist in the new implementation, the code will no longer compile.

If there is no appropriate interface, such as methods cannot be found in the interface but the concrete class, then return the class that is least specific in the class hierarchy; for example, it’s legitimate to put LinkedList as the return type when the program relies on the extra methods, such as getFirst() or getLast() methods which aren’t provided by the standard the List interface. Also, it’s entirely applicable to use concrete classes as the return type of value classes, such as String or BigDecimal, since these type of classes hardly have multiple implementations.

Previous Article
Next Article
Every support is much appreciated ❤️

Buy Me a Coffee