Observation

There are many Exception subtypes that don't have any added state or behavior. For example, ClosedByInterruptException code.

Question

Why is subtyping without adding state or behavior "acceptable" in the case of Exception? From what I know, an interface should be used when defining new behavior, and a class should be used when implementing behavior or when state is added (which may cause behavior to be overriden). Other than Exception types, would applying be acceptable?

Please don't use markers as an example.

Elaboration

Below is a design that is frowned upon:

class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }
}

class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }
}

abstract class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

   //...assume methods that use String name are not overridable
}

For those who don't understand why this is frowned upon, see bottom of post.

For those who do understand, this would fall under the same category as ClosedByInterruptException: the subtypes do not define new state or behavior. So why would exceptions like this exist if the design is frowned upon?

Although the exception is more descriptive, an extended description can be included via a String message. Seeing how a SocketException is thrown in the case of a socket being closed early, rather than SocketClosedException or SocketNotConnectedException, why does a newer API use the design in question?


For those wondering why the design is frowned upon

Looking at the Cat and Dog example, both would perform the exact same way, since no new behaviors were added, nor were behaviors overwritten. The only difference would come from type checking via instanceof. If you use instanceof, you are attempting to perform some kind of behavior based on the type, for example:

Animal animal = ...;

if(animal instanceof Dog) {
    //perform some behavior
} else if(animal instanceof Cat) {
    //perform some behavior
}

In a case like this, the behavior should be contained within the type by overriding a method, and the behavior should be triggered via polymorphism.

有帮助吗?

解决方案

Java's exception handing mechanism uses the class hierarchy to model a taxonomy of exception cases.

  • Catch phrases effectively do instanceof tests. Thus they can distinguish ClosedByInterruptException from more general cases AsynchronousCloseException, ClosedChannelException, etc.
  • Throwable#toString() includes the class name. Thus the stack traceback shows the specific type of exception that occurred.
  • A throws clause also distinguishes the given exception type from its more general cases (represented as superclasses).

The design guideline you referred to applies in the common case of treating an instance in the ordinary object-oriented way -- as something with state, behavior, and identity. Just send it messages.

But sometimes people use the class hierarchy as a modeling tool. Another example would be instances read from and written to a database. The database I/O framework can pair the classes with database tables.

其他提示

If exceptions would be implemented the way you suggest, it would mean any exception would have to be caught and eveluated at every level exception handlers exist. This would defeat a large part of their current purpose and appeal and would ultimately not be that much better than returning error codes and evaluating them on every level of the call stack, like we used to do in the pre-OO era. So maybe, from an OO perspective, it is a kludge but the benefits are substantial.

As for the animal-cat example, these kind of violations are often done for consistency and practical reasons. You may not have a full implementation for all concrete animals yet but your program structure requires the one generic class and the extensions, like when you use the visitor pattern for example.

Your observation is probably the explanation for the fact that a lot of people cannot be bothered to create a custom exception class for any problem domain level type of issue and that this bad practice is so common.

Why is subtyping without adding state or behavior "acceptable" in the case of Exception? From what I know, an interface should be used when defining new behavior, and a class should be used when implementing behavior or when state is added.

Default methods were only added to Java in release 8, the Exception class has been in the source since release 1, so until release 8, there was no way pass functionality down the tree unless a class was used.

Using an interface for an exception would contribute to copy-pasta code. You'd have to re-implement the logic for each exception (which aren't that different). A class allows you to use a constructor or two, and you're done.

By extending from throwable, you're adding state - the stack trace shows the specific exception that occurred, and a few methods also return the name of the exception, such as toString().


In the Wikipedia article Marker interface pattern:

A major problem with marker interfaces is that an interface defines a contract for implementing classes, and that contract is inherited by all subclasses. This means that you cannot "unimplement" a marker. In the example given, if you create a subclass that you do not want to serialize (perhaps because it depends on transient state), you must resort to explicitly throwing NotSerializableException (per ObjectOutputStream docs)

This is the class definition for java.lang.Throwable:

public class Throwable implements Serializable

Looking at the Cat and Dog example, both would perform the exact same way, since no new behaviors were added, nor were behaviors overwritten. The only difference would come from type checking via instanceof.

The try/catch mechanism of java relies heavily on instanceof. It allows you to catch broad exceptions IOException, or specific exceptions ArrayIndexOutOfBounds. It allows specific exceptions to exist, and new functionality to be added to an exception, even if rarely done. This isn't as easy by 'overriding a method and having the behavior triggered by polymorphism'.

Design patterns come and go. Just because a pattern is used somewhere, or something is implemented a specific way, even in the JDK official source, doesn't mean it is the best choice. It might've been at the time, or maybe no one thought of implementing said way.

I don't think it to be a bad practice. That's called design:

  • You can model a type hierarchy or a set of interfaces so as to give junior programmers a starting point.
  • Software is not writen in one day, so perhaps you can have subtypes with no added behavior or state to be completed tomorrow, the same way you can write a bunch of interfaces to make a squeleton of a design and flesh them out along subsequent days.
  • In the case of exceptions in Java I think it is indeed very useful since you can catch specific exceptions, otherwise you would be using case structures to determine what exception was thrown.

Just for the record: I prefer composition and interfaces over inheritance and abstract classes but they do have their place and uses and the Java Exception handling is an example of inheritance put to good use, even if no further behavior or state is added by subclasses.

许可以下: CC-BY-SA归因
scroll top