Summary
The Java language has always had the notion of marker interfaces. A problem with these is that if a parent class has the marker, all its children inevitably do too. On the other hand, marker annotations don't necessarily have this limitation. Here's how to define a marker that can be cancelled in a subclass.
Advertisement
A marker interface is an interface with no methods,
which a class can implement to show that it has certain properties.
Well-known examples are Cloneable
and Serializable. A class can implement
Serializable to indicate that it has been
designed to be serialized, for example.
One problem with marker interfaces is that when a class implements an interface, all of its subclasses inherit that implementation. Since Number implements Serializable,
any subclass such as Integer
or AtomicInteger
does too.
What can you do if you want to define a subclass of
Number that is not Serializable? You are forced to
resort to a hack: define a writeObject method that throws NotSerializableException. Instances of your
class will still appear to be serializable (instanceof
Serializable is still true) but will not actually be.
Yuck.
Annotations, introduced with Tiger (J2SE 5), provide an
alternative to marker interfaces. If serialization were being
defined now, then instead of specifying serializability like
this...
public class Number implements Serializable {...}
...you might specify it like this...
@Serializable
public class Number {...}
Annotations can be defined such that they either are or are not
inherited by subclasses. We could decide that the
hypothetical @Serializable annotation is
inheritable when we define it:
@Inherited
public @interface Serializable {}
In this case, subclasses of our @Serializable
Number class like Integer are
automatically @Serializable themselves. This is
usually what you want, but just as with the marker interface
there is no way to cancel serializability in a subclass. Once
an inheritable marker annotation is in a superclass, a
subclass can't turn it off.
Alternatively, we could omit the @Inherited
annotation from the definition of @Serializable.
Then Integer is not automatically
@Serializable even if Number is. We
can explicitly choose whether or not to make it so. But we no
longer get the behaviour that we usually want, which is that a
class is automatically @Serializable if its parent
is.
We could define a @NotSerializable marker
annotation, just as we could define a NotSerializable
marker interface, so a subclass could override the inherited
@Serializable by saying it was
@NotSerializable after all. But how ugly.
Fortunately, there is a nicer solution using what I'll call
negatable marker annotations. The idea is simple. We
change the definition of @Serializable to this:
@Inherited
public @interface Serializable {
boolean value() default true;
}
With this definition, we can still define Number
as before:
@Serializable
public class Number {...}
Subclasses of Number are now automatically
@Serializable too. But if we want to define a
subclass that is not, we simply write:
@Serializable(false)
public class UnserializableInteger extends Number {...}
It could hardly be simpler!
When we write @Serializable with this definition,
it's equivalent to writing @Serializable(true). We
can omit true since it's the default value, and
then there's nothing left but @Serializable(). The
annotation syntax allows us to omit the empty parentheses too,
and that leaves us with plain @Serializable.
The code that uses reflection to know whether a class is
@Serializable or not is different with this
definition. With a plain marker annotation, we would use:
Class c = whatever;
Serializable ann = c.getAnnotation(Serializable.class);
boolean isSerializable = (ann != null);
With a negatable marker annotation, we must use:
Class c = whatever;
Serializable ann = c.getAnnotation(Serializable.class);
boolean isSerializable = (ann != null && ann.value());
The class is serializable if the @Serializable
annotation is present (ann != null)
and the value of the annotation is true
(ann.value(), which we could also write as
ann.value() == true).
Negatable marker annotations can have other applications than
cancelling inherited annotations. For example, suppose you have
a convention that an interface called
SomethingMBean is an "MBean interface",
whatever that might be. You can add a negatable
@MBean annotation to supplement this convention.
You can say that an interface is an MBean interface even if
isn't called SomethingMBean, by using
@MBean. And, you can say that an interface is
not an MBean interface even if it is called
SomethingMBean, by using
@MBean(false).
Negatable marker annotations also allow you to be more
explicit. With plain marker annotations, if a class is marked
@Serializable then you know that its designers
intended it to be serializable. But if it is not marked
@Serializable, you only know that either
its designers intended it not to be serializable or
they didn't think about the question. A negatable marker
annotation allows you to write @Serializable(false)
to be explicit about it.
I haven't seen this idea elsewhere, but I would amazed if it
has not been independently invented several times. Let me
know!
> One problem with marker interfaces is that when a class implements an interface, all of its subclasses inherit that implementation.
That is not a problem, it's a feature: when subclassing is viewed from the perspective of subtyping and substitutability, rather than jumbled ad hoc code reuse, it is inevitable that a supertype -- whether class or interface -- defines and constrains the interface and capabilities of its descendants. That viewpoint is pretty much embodied in most object type systems. There may be some well-defined cases where you want to work against it, but on the whole the assumption should be that such a workaround is exactly that: an intentional and designed departure from the norm and from well-reasoned best practice.
> What can you do if you want to define a subclass of Number that is not Serializable? You are forced to resort to a hack: define a writeObject method that throws NotSerializableException.
Yes, it's a hack, but at least it looks like a hack. Trying to encourage support for non-substitutability through annotations to make the workaround more transparent seems to be answering the wrong design question, I'm afraid.
> I haven't seen this idea elsewhere, but I would amazed if it has not been independently invented several times.
Generally this idea of inheritance with cancellation comes up quite often in one form or another. I have noticed people either stating a need for it or implementing it in different languages since at least the late 1980s, so it probably stretches back further. Most of the time the stated need masks a different design issue.
Kevlin, I'm not sure we're talking about the same things here. The "idea" I was referring to was that of having a marker that is usually positive but can be explicitly negative. I don't think that inherited interfaces should be cancellable in general, since as you say that would violate substitutability (Eiffel commits this sin because of both visibility reduction and covariant parameters, for example). However, it does make sense for some markers to be cancellable in subclasses.
@Serializable is perhaps not the best example because of the substitutability point you mention: if I accept a Number parameter, then I expect any value passed to that parameter to obey Number's contract and be serializable. A better example might be @Cloneable (if Object.clone() weren't so screwed up otherwise). This is not something you are saying to the users of your class but to Object.clone(): "I've done the necessary work to ensure I can be cloned safely by copying my fields." This is not necessarily true of subclasses so they probably shouldn't inherit this marker, or at least they should be able to cancel it.
In any case negatable markers have uses other than on classes, as I mentioned.
I agree with the usefullness of the feature (and thank you for writing it out so neatly with examples and everything). But I object to the specific example. It is true that once-upon-a-time (before annotations existed), marker interfaces were used as a way of simply "tagging" classes, and some of the things represented in this fashion were not things one would necessarily expect subclasses to implement. But some ARE, and you wouldn't want to cancel those in subclasses because it would break substitutability.
Isn't the point here that using a marker interface to denote something 'this is serializable' is only one way to express the concept ? That you might have a class model wherein 'serializability' was not naturally part of the type hierarchy ?
The use of annotations would be more natural here than using a marker interface.
publicclass Serializer {
publicvoid serialize(Serializable obj, OutputStream outStream) {
if (! obj.isSerializable())
return;
else
// Go for it!!!
}
}
Then you could have your standard serializable class, a derived but not serializable subclass and a derived serializable class that forces all its subclasses to be serializable too:
Interestingly, doing it this way puts the decision as to whether an actual object is serializable or not with that object. eg: a SensitiveDocument instance can decide whether or not its serializable depending on its content.
That would be the traditional way to do it. It's MUCH easier on the client than the annotation check and is relatively easy for the implementor. I do admit it's a little harder for a user of a class to work out whether instances of a class are, or are not, actually serializable - they have to trust the documentation or read the source code I guess. That might be a moot point however, because someone relying on this capability STILL has to check at runtime if the object 'implements' the protocol (meaning it answers 'yes') and handle the situation where it doesn't.
(Am I missing something really obvious here? Be gentle with me if I am please. :-)
I like David Waddell's comment about "you might have a class model wherein 'serializability' was not naturally part of the type hierarchy". This is starting to sound a little bit like aspects (though I haven't actually used aspects so I could be speaking out of my proverbial). Can anyone think of any other examples where this sort of capability would prove useful?
Eamonn, why did the JMX specification decide to go with a naming convention to determine whether an interface represents an MBean and not just have an MBean marker interface that other interfaces inherit from?
I have to agree with Brendan, this conditional annotation thing looks like an ungodly hack. Of course, annotations in general look like an unnecessary addition to the language for this kind of use.
Tagging interfaces that contain methods that determine if the interface is actually in force seems adequate and simple to use.
Brendan, I don't want to get too involved in the particular question of serialization. However, notice that what you are talking about is a way for an instance to say whether it is serializable, whereas marker interfaces and annotations are a way for a class to say whether it is serializable. (In this particular case there's a certain amount of redundancy with exceptions: what if you say isSerializable() but then throw NotSerializableException from writeObject?)
Now, maybe a lot of the time where marker interfaces are used, you really do want to say something about the instance rather than the class. But not always. One example where your technique clearly isn't applicable is when the marker interface or annotation is used by tools that run on source code or class files, rather than on runtime instances. The apt tool allows you to do all sorts of interesting processing on your source files based on annotations, for example.
And of course annotations can be applied to other things besides classes, as I mentioned. In these other cases it is useful to be able to say @Interesting(false) explicitly either because leaving it out is ambiguous (did I decide it was uninteresting, or did I not think about it?), or because the default interestingness is sometimes true and needs to be contradicted.
Concerning your question about why the JMX API uses a naming convention rather than a marker interface, I wasn't around when this stuff was invented and can't say. It was quite a long time ago, and might even precede e.g. java.rmi.Remote. If we were doing it over we probably would have an MBean interface like the Remote interface.