Интерфейс — это инструмент для объявления контракта. Маркер-интерфейс — это интерфейс, который не объявляет контракт, а используется для других целей.
Примеры
Serializable
Разрешает записывать объекты этого класса в ObjectOutputStream.
Проблема: нет никакой гарантии, что это произойдёт успешно, т. к. объект может содержать не-Serializable поля.
Нормальное решение: убрать маркер-интерфейс, позволить записывать таким способом любые объекты. Всё равно хуже уже не будет.
Cloneable
Меняет поведение метода Object#clone(). Для не-Cloneable он бросает исключение, для Cloneable — возвращает shallow copy объекта.
Проблема: не объявляет контракт для клонирования, а изменяет поведение метода суперкласса. Имеет смысл только для изменяемых объектов.
Решение: для изменяемых объектов — copy constructor, для неизменяемых — подобие метода copy из data-классов в Kotlin.
Type
В интерфейсе нет осмысленных методов (в Java 1.8 добавлен первый метод — getTypeName), это маркер, которым помечены типы, описывающие разновидности типов в Java: Class из Java 1.0 описывает классы и интерфейсы, примитивные типы и массивы; GenericArrayType, ParameterizedType, TypeVariable, WildcardType описывают generic-типы из Java 1.5.
Этот интерфейс — неудачная попытка изобразить тип-сумму, который в Java не поддерживается явным образом.
На самом деле, такие типы допустимы в multi-catch из Java 1.7 в очень ограниченном виде: catch (SomeException|SomeOtherException e)
Тип-сумма также называется следующими терминами: алгебраический тип, меченое объединение, дизъюнктивное объединение, tagged union, variant record, choice type, discriminated union, disjoint union, sum type. Схожие по смыслу понятия: sealed class, union-тип.
Проблема: нет надёжного способа узнать, какой конкретно перед нами тип. instanceof не подходит, так как нет механизма языка, который мог бы гарантировать, что все возможные варианты рассмотрены.
Решение: использовать полиморфизм для ветвления. Например, объявить интерфейс Type так:
public interface Type {
<R> R visit(Visitor<R> visitor);
interface Visitor<R> {
R isClass(Class<?> type);
R isGenericArray(GenericArrayType type);
R isParameterizedType(ParameterizedType type);
R isTypeVariable(TypeVariable<?> type);
R isWildcardType(WildcardType type);
}
}
Каждая реализация должна вызвать соответствующий её типу метод.
public class Class<T> implements ..., Type, ... {
// тело класса пропущено...
@Override public <R> R visit(Visitor<R> visitor) {
return visitor.isClass(this);
}
}
public class RealGenericArrayType implements GenericArrayType {
// тело класса пропущено...
@Override public <R> R visit(Visitor<R> visitor) {
return visitor.isGenericArray(this);
}
}
// и так далее
Тогда можно будет инспектировать тип просто и однозначно (но, конечно, многословно):
String typeDescription = someType.visit(new Type.Visitor<String>() {
@Override public String isClass(Class<?> type) {
return "normal class";
}
@Override public String isGenericArray(GenericArrayType type) {
return "generic array, LOL";
}
@Override public String isParameterizedType(ParameterizedType type) {
return "parameterized type";
}
@Override public String isTypeVariable(TypeVariable<?> type) {
return "type variable ¯\\_(ツ)_/¯";
}
@Override public String isWildcardType(WildcardType type) {
return "wildcard";
}
});
System.out.println("Given type is a " + typeDescription);
Эта техника также называется double dispatch, т. к. во время выполнения происходит два виртуальных вызова, один за другим.
Другое
Если по необъяснимым причинам возникла необходимость пометить класс каким-то маркером, то аннотации подходят для этого лучше.
Массивы — жертвы интерфейсов-маркеров: они реализуют Cloneable и Serializable, но виртуальных методов у них нет: vtable и itable технически отсутствуют.