00:00

Marker Interface in Java – Tagging Classes with Empty Interfaces

Last updated:

A marker interface (or tagging interface) is an interface with no methods and no constants. Implementing it does not add behaviour; it simply marks a class as having a certain property. Other code, such as the JVM, a library or your own framework, checks for the marker with instanceof and treats marked objects differently.

This tutorial covers the JDK's marker interfaces (Serializable, Cloneable, RandomAccess), how to create your own, how marker interfaces compare with marker annotations, and how sealed interfaces and pattern matching (Java 17 to 25) change the way modern Java code tags and handles types.

1. What Is a Marker Interface?

Marker interface
public interface Serializable { }      // the entire declaration in java.io

class Customer implements Serializable { ... }   // "Customer objects may be serialized"

The marker carries meaning through the type system: an object is either an instance of the marker type or not. Code that cares checks obj instanceof Marker or declares parameters of the marker type.

2. Marker Interfaces in the JDK

MarkerPackageMeaningChecked by
Serializablejava.ioObjects may be converted to a byte streamObjectOutputStream throws NotSerializableException otherwise
Cloneablejava.langObject.clone() may make a field-by-field copyObject.clone() throws CloneNotSupportedException otherwise
RandomAccessjava.utilThe list supports fast indexed accessAlgorithms such as Collections.binarySearch choose a strategy
Remotejava.rmiMethods may be invoked remotely via RMIThe RMI runtime
EventListenerjava.utilTags all event listener interfacesTools and frameworks
JdkMarkers.java
import java.io.*;
import java.util.*;

public class JdkMarkers {
    static class Plain { int x = 1; }

    static class Saveable implements Serializable {
        @Serial
        private static final long serialVersionUID = 1L;   // version tag for the serialized form
        int x = 1;
    }

    static class Sheep implements Cloneable {
        String name = "Dolly";
        @Override public Sheep clone() throws CloneNotSupportedException { return (Sheep) super.clone(); }
    }
    static class Goat {
        String name = "Billy";
        public Goat copy() throws CloneNotSupportedException { return (Goat) super.clone(); }
    }

    static void serialize(Object o) {
        try (ObjectOutputStream out = new ObjectOutputStream(new ByteArrayOutputStream())) {
            out.writeObject(o);
            System.out.println("Serializable: " + o.getClass().getSimpleName() + " OK");
        } catch (IOException e) {
            System.out.println("Serializable: " + e);
        }
    }

    public static void main(String[] args) {
        serialize(new Saveable());
        serialize(new Plain());

        try { System.out.println("Cloneable: cloned " + new Sheep().clone().name); }
        catch (CloneNotSupportedException e) { System.out.println("Cloneable: " + e); }
        try { new Goat().copy(); }
        catch (CloneNotSupportedException e) { System.out.println("Cloneable: " + e); }

        // RandomAccess tells algorithms that get(i) is fast
        System.out.println("ArrayList is RandomAccess:  " + (new ArrayList<>() instanceof RandomAccess));
        System.out.println("LinkedList is RandomAccess: " + (new LinkedList<>() instanceof RandomAccess));
    }
}
Output
Serializable: Saveable OK
Serializable: java.io.NotSerializableException: JdkMarkers$Plain
Cloneable: cloned Dolly
Cloneable: java.lang.CloneNotSupportedException: JdkMarkers$Goat
ArrayList is RandomAccess:  true
LinkedList is RandomAccess: false

The classes are identical except for the marker, yet serialization and cloning succeed only for marked classes. See Serialization and Object Cloning.

3. Creating a Custom Marker Interface

You can define your own marker to tag classes that need special handling, such as events that must be audited:

CustomMarker.java
import java.util.List;

public class CustomMarker {
    // Custom marker interface: no methods, just a type tag
    interface Auditable { }

    record Payment(String id, double amount) implements Auditable { }
    record Login(String user) implements Auditable { }
    record PageView(String url) { }

    static void process(Object event) {
        if (event instanceof Auditable auditable) {       // check the marker (pattern matching, Java 16+)
            System.out.println("AUDIT  -> " + auditable);
        }
        System.out.println("handled  " + event.getClass().getSimpleName());
    }

    // Unlike an annotation, the marker is a TYPE: the compiler can enforce it
    static void archive(Auditable record) {
        System.out.println("ARCHIVE -> " + record);
    }

    public static void main(String[] args) {
        List<Object> events = List.of(new Payment("P-1", 999.0), new PageView("/java"), new Login("asha"));
        events.forEach(CustomMarker::process);
        archive(new Login("ravi"));
        // archive(new PageView("/x"));   // compile error: PageView is not Auditable
    }
}
Output
AUDIT  -> Payment[id=P-1, amount=999.0]
handled  Payment
handled  PageView
AUDIT  -> Login[user=asha]
handled  Login
ARCHIVE -> Login[user=ravi]

The key strength of a marker interface is visible in archive(Auditable record): the compiler rejects unmarked objects, so mistakes are caught before the program runs. The check in process uses pattern matching for instanceof (final since Java 16), which tests the marker and gives you a typed variable in one step.

4. Marker Interfaces vs Marker Annotations

Annotations, added in Java 5, offer another way to tag code. An annotation with no elements is called a marker annotation; examples in the JDK are @Override, @FunctionalInterface (Java 8) and @SafeVarargs. (@Deprecated is no longer one: since Java 9 it has the since and forRemoval elements.)

MarkerAnnotation.java
import java.lang.annotation.*;
import java.util.List;

public class MarkerAnnotation {
    // Annotation-based alternative: a marker annotation
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @interface Cacheable { }

    @Cacheable static class ProductService { }
    static class OrderService { }

    public static void main(String[] args) {
        for (Class<?> c : List.of(ProductService.class, OrderService.class)) {
            boolean cache = c.isAnnotationPresent(Cacheable.class);
            System.out.println(c.getSimpleName() + " cacheable? " + cache);
        }
    }
}
Output
ProductService cacheable? true
OrderService cacheable? false
AspectMarker interfaceMarker annotation
Defines a typeYes: usable for parameters and variablesNo
Compile-time checkingYes (method can require the marker type)Only with annotation processors
Inherited by subclassesAlwaysOnly if @Inherited (and only for classes)
Can markClasses and interfaces onlyClasses, methods, fields, parameters, packages, modules…
Can carry dataNoYes, by adding elements later
Checked withinstanceofReflection: isAnnotationPresent

A useful rule (from Effective Java): use a marker interface when you want to define a type that methods can accept; use a marker annotation to mark program elements other than classes, or in frameworks that already rely heavily on annotations (such as Spring and JPA).

5. Modern Java: Sealed Interfaces and Pattern Matching

A marker interface is open: any class can implement it, so code that checks the marker can never be sure it has seen every kind of object. When you control the full set of types and want the compiler to check that each one is handled, modern Java offers a better tool: a sealed interface (final in Java 17) combined with pattern matching for switch and record patterns (both final in Java 21).

SealedEvents.java
import java.util.List;

public class SealedEvents {
    // Sealed interface: only the listed types may implement it (Java 17+)
    sealed interface Event permits Payment, Login, PageView { }

    record Payment(String id, double amount) implements Event { }
    record Login(String user) implements Event { }
    record PageView(String url) implements Event { }

    // Exhaustive switch with record patterns (Java 21+): no default branch needed
    static String describe(Event event) {
        return switch (event) {
            case Payment(String id, double amount) when amount > 500 -> "AUDIT large payment " + id;
            case Payment(String id, double amount) -> "payment " + id + " for " + amount;
            case Login(String user) -> "AUDIT login by " + user;
            case PageView(String url) -> "page view " + url;
        };
    }

    public static void main(String[] args) {
        List<Event> events = List.of(new Payment("P-1", 999.0), new Payment("P-2", 45.0),
                                     new Login("asha"), new PageView("/java"));
        events.forEach(e -> System.out.println(describe(e)));
    }
}
Output
AUDIT large payment P-1
payment P-2 for 45.0
AUDIT login by asha
page view /java

The switch has no default branch because the compiler knows every permitted Event type. If you add a new record to the permits list and forget to handle it, compilation fails with "the switch expression does not cover all possible input values". A marker check with instanceof gives no such guarantee.

UseWhen
Marker interfaceAn open set of classes shares a property, and methods should accept only marked types (for example Serializable)
Marker annotationYou need to tag methods, fields or other non-type elements, or a framework reads annotations
Sealed interface + pattern matchingYou own a fixed set of types and want exhaustive, compiler-checked handling of each one

Marker interfaces themselves have not changed in Java 25: Serializable, Cloneable, RandomAccess and the others work exactly as in earlier releases. The examples on this page use only features that are final in Java 21 and run unchanged on Java 25 (LTS). See Java 17 features and Java 21 features for more on sealed types and pattern matching.

6. Criticism and Pitfalls

  • Inheritance cannot be undone: if a parent class is Serializable, every subclass is too, even if it should not be.
  • Cloneable is a poor design: it does not declare clone(), so the marker alone does not make an object cloneable through the interface. Copy constructors or factory methods are usually better.
  • Meaning is implicit: nothing in the interface explains what it promises; document it clearly.
  • Serializable carries security risk: deserializing untrusted data can run unexpected code. If you must deserialize, set an ObjectInputFilter (Java 9+) to allow only expected classes; Java 17 added JVM-wide filter factories. For new code, prefer formats such as JSON with explicit mapping.
  • Declare serialVersionUID: mark it with @Serial (Java 14+) so the compiler checks its declaration, as shown in JdkMarkers.java.

7. Summary

  • A marker interface has no methods and tags a class with a property.
  • JDK markers include Serializable, Cloneable and RandomAccess.
  • Markers are checked with instanceof or enforced by parameter types at compile time.
  • Marker annotations are more flexible but do not define a type.
  • For a fixed set of types, a sealed interface with pattern matching (Java 17–21, unchanged in Java 25) gives compiler-checked, exhaustive handling.

Related Tutorials