javalogo

How to force subclasses to implement toString in Java

The Problem
In our logs we were seeing the typical mysterious output that the default Object.toString method exhibits for classes in Java that don’t supply their own toString.

I’m sure you’ve seen it before but It looked something like:

com.example.www.my.package.Name@141a95fb

That’s a real shame because it tells you nothing. Of course every dev who creates a new class should care about implementing the equals and toString methods, but we all know that this is overlooked a lot. And unfortunately you can’t force anyone to override toString in their classes. Not at the compiler level anyway.

Or can you?

A False Start

At first another dev on my team tried to create an interface with a toString() method.

public interface Loggable {
    String toString();
}

Instances of this interface were the only accepted parameters to the log methods.

public void logMessage(String msg, Loggable l) {
   writeLog(String.format("%s: %s", msg, l));
}

Obviously the other dev had the right idea. We needed to somehow enforce that implementations of Loggable implement a toString() method so that when they are logged, the logs have enough information to indicate what’s happening.

The problem with this idea, though, is that it doesn’t enforce this. We can still create a class that implements Loggable without supplying the toString() method. Since our class inherits a toString() method from Object the compiler is totally fine with it.

So we have to trick the compiler.

The Approach

First, we create an abstract class that requires the method. Second, it implements toString() and makes a call to it:

public abstract class Loggable {

  String toLogString();

  String toString() {
    return toLogString();
  }

}

Now without anyone realizing it, we have overridden the toString() method that comes down from Object and replaced it with our own that forwards the call to the toLogString() method. And since the toLogString method is not implemented yet, subclasses will be forced by the compiler to implement it, achieving our goal of requiring custom toString behavior.

You can take this one step further and make the toString() method final so that no subclasses can override it, but I’m not sure that’s necessary.

Once the Loggable abstract class is set up, we can now accept an instance of it and always know that when we call toString(), it is going to forward to whatever implementation of toLogString() that has been provided in the subclass. Read on for a concrete example.

Recall that we had this method previously:

public void logMessage(String msg, Loggable l) {
  writeLog(String.format("%s: %s", msg, l));
}

It can remain the same. Now let’s say for the sake of example our application has a “Name” class to keep track of people’s names.

public class Name {
  private final String first;
  private final String last;

  Name(String first, String last) {
    this.first = first;
    this.last = last;
  }

  public String getName() {
    return first + " " + last;
  }
}

In order to be able to pass an instance of Name to the “logMessage” method, it has to be an instance of Loggable, so the developer extends Loggable in the class signature.

public class Name extends Loggable {

If the developer only does that and forgets to add an implementation of the toLogString() method, the compiler will complain:

Name.java:1: error: Name is not abstract and does not override abstract method toLogString() in Loggable
public class Name extends Loggable {
       ^
1 error

Yes! That’s what we want! This will trigger any dev to go ahead and implement the toLogString() method:

public class Name extends Loggable {
  private String first;
  private String last;

  Name(String first, String last) {
    this.first = first;
    this.last = last;
  }

  public String getName() {
    return first + " " + last;
  }

  @Override
  public String toLogString() {
    return getName();
  }
}

And now the compiler is fine. And when we call the logMessage method, the magic we expect to happen will happen automatically:

public class Logger {

  public void logMessage(String msg, Loggable l) {
    writeLog(String.format("%s: %s", msg, l));
  }

  public static void main(String[] args) {
    Logger l = new Logger();
    Name n = new Name("Scott", "Shipp");
    l.logMessage("Name", n);
  }
}

This would output:

Name: Scott Shipp

Notice that in the call to “String.format” our instance of Loggable is automatically treated as a String. In fact we can now pass instances of Loggable to many methods that we normally think of as taking a String (like String.format, System.out.println, StringBuilder.append, etc). That’s because our Loggable has a valid “toString()” — the one supplied by the abstract class — and these methods will implicitly call .toString()!

Hope this helps someone out there!

Leave a Reply

Your email address will not be published. Required fields are marked *