Java: Marker Interfaces
A marker interface, or tag interface, is an interface without any methods or fields. It's typically used to flag that instances of the implementing classes have some internal capability and behaves in a certain way when interacting with them.
It may also be the case that they have no semantical purpose other than to group related types together. It then serves as slightly more typesafe alternative to Object
and can be seen as a poor man's substitute for discriminated union types.
Examples from the Java API
RandomAccess
is a marker interface that should be implemented by List
classes that provide efficient non-sequential access. Specifically the Javadoc says:
As a rule of thumb, a List implementation should implement this interface if, for typical instances of the class, this loop:runs faster than this loop:for (int i = 0, n = list.size(); i < n; i++) list.get(i);
for (Iterator i = list.iterator(); i.hasNext();) i.next();
This allows clients to choose the most efficient code for traversing a given list. ArrayList
implements this interface, while LinkedList
does not.
Note: Since RandomAccess
is intended specifically for lists, an alternative approach would have been to simply add a boolean providesRandomAccess()
method to the List
interface. However, at the time RandomAccess
was introduced this was not an option, since List
was already around, and adding methods to an interface is not a backward compatible change. Had this feature been introduced today on the other hand, a default method could have been a good alternative.
Other examples from the standard API:
Interface | Purpose |
---|---|
Serializable
|
Says that the object state may be serialized |
Cloneable
|
Says that Object.clone may perform a field-by-field copy of the object |
Remote
|
Says that methods may be invoked from a non-local virtual machine |
EventListener
|
Interface that all AWT event listeners must implement |
CopyOption
|
Union type for LinkOption and StandardCopyOption
|
A custom marker interface
Suppose we have a LogUtil.logAsynchronously(Object o)
method that writes o.toString()
to a log file. To return as quickly as possible from logAsynchronously
we would like to call toString
in the background thread. This is however only safe if other threads don't change the state of o
. For this reason we add an ImmutableObject
marker interface to allow for clients to opt in for this feature.
interface ImmutableObject {}
The logAsynchronously
method could then use instanceof
as follows:
class LogUtil {
public void logAsynchronously(Object o) {
if (o instanceof ImmutableObject) {
runInBackground(() -> log(o.toString()));
} else {
String msg = o.toString();
runInBackground(() -> log(msg));
}
}
…
}
Use as discriminated union type
Other languages, such as F# and TypeScript support discriminated union types which means you can write things like:
function print(value: string | number) {
// value can be a string or a number
}
Java does not provide support for this. If you want to express "an instance of either A
or B
" you can create an empty interface AorB
and let both classes implement this interface.
CopyOption
is an example of this. It is an empty interface implemented by LinkOption
and StandardCopyOption
. This allows for methods such as Files.copy
to have a signature like this:
public static Path copy(Path source,
Path target,
CopyOption... options) {
…
}
This approach doesn't let you create union types of classes out of your control, like String
and Number
as in the TypeScript example above. On the upside, you can easily extend the union type with new classes by letting more classes implement the empty interface.
Restricting Use
By letting the marker interface, M
, extend another interface, I
, you make sure that M
can only be implemented by classes that also implement I
.
For example, RandomAccess
is intendend to be used by List
implementations. If the authors wanted to enforce this, they could have implemented RandomAccess
like this:
interface RandomAccess extends List {}
Criticism
The use of marker interfaces have long been criticized and nowadays most of the use cases are better solved with annotations.
- Violates the principle of least astonishment
- The primary purpose of an interface is to establish a contract of communication. Since an empty interface is a contract that says nothing, it's at first glance quite useless. Some argue that marker interfaces aren't actual interfaces at all.
- Forces a non-object oriented programming style
-
The use of
instanceof
almost always begs the question: Why isn't this solved with polymorphism / virtual methods? Where it would be natural to do
marker interfaces requires you to doobj.doSomething()
I.e. the logic is pushed outside the class itself. If this affects many places in the code, maintenance becomes a nightmare.if (obj instanceof Marker) { doSomething(obj); }
- Requires casting
- If you have say, a
Cloneable
variable, you'd expect to be able to clone it. But no; You're forced to cast it to something else first. This is because the contract of a marker interface such asCloneable
is useless. All you get are the implicitly declared methods fromObject
, which is rarely what you're looking for. This ties back to the first item: Marker interfaces aren't proper interfaces at all. - Requires out of band knowledge
- If objects implementing marker interfaces requires special treatment, the programmer must be aware of this. Even if the programmer reads up on the documentation of the marker interface, it may be unclear where to be cautious. While an
Animal
doesn't implement the marker interface, aCat
might. Or some future—not yet known—subclass might. Or an undocumented anonymous subclass might.
Marker Interfaces vs Annotations
Marker interfaces and annotations serve the same purpose: Convey metadata about the class to its consumers. The fundamental difference however, is that annotations are meant to be used for this purpose, while interfaces are not.
- Parameters
- A marker interface is like a boolean flag, while an annotation can carry an actual value. In other words, annotations let you express things like
@Author("John")
. - Inheritance
-
Whether or not an annotation is inherited can be controlled through the
@Inherited
annotation. Marker interfaces are always inherited and there's no way for subclasses to opt out. For example, if you extend aSerializable
class, your class will automatically also be serializable. - Retention
-
Sometimes the metadata is only relevant during compilation. Sometimes it should be available also in runtime.
@Retention
allows you to control this aspect for annotations. Marker interfaces are always available in runtime. - Applicability
- Marker interfaces can only be "applied" to classes and interfaces. Annotations can also be applied to methods, fields, local variables, parameters, packages and other annotations.
- Repetition
- A marker interface can be applied at most once. Annotations can be applied multiple times. You can for example have multiple
@Author
annotations on the same class.
When to use marker interfaces?
As shown above, annotations are superior in many ways. Marker interfaces however, give you something that annotations don't: an actual type. If (and only if) you need to express something like "This method accepts an immutable value as argument" then you should use an ImmutableValue
marker interface rather than an @ImmutableValue
annotation. (You can actually still use an annotation for this, but you would need to use a compiler plugin like the Checker Framework.)
The EventListener
interface is an example where an annotation would not have been sufficient. Most annotations (even non-repeating, inherited, without arguments) would not have been suitable as a marker interface: @NotNull
, @JsonIgnore
, @Interned
to mention a few.