Shivani S
Shivani S

Reputation: 17

Exception signature in Java interface

I have the below interface which could be implemented in many ways.

public interface IStuffManager {

   public void createStuff() throws Exception
public class StuffFileManager implements IStuffManager {

public void createStuff() throws IOException {
   // file IO
}

Calling code cannot propagate exception due to interface implementation and can only throw unchecked exception:

StuffManager stuffManager = new StuffManager();
try {
   stuffManager.createStuff();
} catch (IOException e) {
   throw new IllegalStateException(e);
}

Throwing Exception is apparently a code smell. How to fix? It is possible that a caller may want to handle the exception thrown differently.

Suggestions I am getting:

  1. In method stuff, catch IOException and throw IllegalStateException(e)
  2. Throw a custom Exception. That will mean 3 exception stacks when caught which will be very hard to read.

Upvotes: 0

Views: 390

Answers (1)

rzwitserloot
rzwitserloot

Reputation: 103253

It's about signatures.

When you implement a method in an interface, that method is the implementation, and your object has to be a drop-in replacement for said interface. In other words, if you write:

class ArrayList implements java.util.List {
  int size() { ... }
}

Then that size() method isn't just 'your' size method. It's your implemention of List's size() method and therefore you must assume that a caller has no idea you are an arraylist. Maybe someone writes this code:

List myList = whoKnows();
myList.size();

Let's say whoKnows() happens to return new ArrayList();. The above code has no idea, and it would be rather bad form if it needed to know.

This gets us to exception signatures:

They are as much a part of a methods nature as their name.

If you are implementing size(), and this list, say, is a 'virtual' list that represents a table in a database, then you may want to say: Well, it would make sense to throw SQLException. However, you can't: Someone might well use your list with no idea that you are a class DbTableBackedList, and therefore, you cannot ask them to deal with SQLException: They simply didn't, and you can't make them, because perhaps that code was written long before you even created your class.

In other words, part of the 'deal' of writing an implementation of the List interface's size() method is that you do not throw SQLException.

So what do you do when you can throw that after all?

This gets us back to the nature of checked exceptions: They are as much part of a method signature as the name.

That means, if the exception is fundamental to the nature of the method, you should declare it. In other words, this method:

public String readFileAsString(File file) {}

is broken as designed. By its nature, it does I/O. Hence, the only correct way to write this method, is to declare it as throws IOException. On the other hand, this method:

public void saveGame() throws IOException {}

is just as wrong: saving a game is not inherently I/O bound. Nobody said that games necessarily save 'to a file'. It may save to a db (in which case, SQLException is more appropriate), or to the cloud, or to memory, or who knows, really. Unless you document that the saveGame() method inherently is I/O bound, it does not make sense to sneak that into its nature by adding throws IOException to its signature. Even if the current implementation of saveGame does save to files.

You can have 2 identical methods: They both are named saveGame and they both have line for line identical code. And still there can be a difference: Once version has built into its bones that it is fundamentally file-bound; it is today, it will be tomorrow, it will be forever, or at least until a major breaking change of this library shows up. The other is file-bound today, but it is not baked into its bones: Maybe tomorrow it won't be, and it's cloud-based instead, or goes to a DB.

That's the subtle nature at play. The point is, with interfaces, the interface already defined this nature for you, and that nature does not involve explicitly throwing IOException here.

Thus, you do the same thing here as what you would do in this hypothetical saveGame method: wrap it, making your own exception if need be. Yes, this means you get 2 stacks (you said '3', no idea where that is coming from): You throw your custom exception and its cause is the underlying exception. I don't see how that causes 3 stacks; it causes 2, just like wrapping in IllegalStateException (given that this has nothing to do with state, that'd be the wrong thing to do):

class MyException extends RuntimeException {
  public MyException(Throwable cause) {
    super(cause);
  }
}

...

catch (IOException e) {
  throw new MyException(e);
}

is all you need to do.

You have one unique extra option available to you, but I recommend against it: throw new UncheckedIOException(e);. That's pretty much the only 'unchecked' variant of a known exception baked into the core, so it happens to work out here. But this isn't really appropriate, a custom exception seems better.

Upvotes: 2

Related Questions