Back to Blog
Java

Java Generic Interface: Syntax and Implementation

java generic interface: Learn how to define and implement generic interfaces in Java, including type bounds, wildcards, and runtime erasure, with practical code examples.

generic typestype safetyJava genericsinterface designtype erasure
Diagram showing a Java generic interface with type parameter T connecting to multiple implementations.

A Java generic interface lets you define a contract that works with a type parameter, so the same interface can be implemented for different types without losing compile-time type safety. This article explains how to declare, implement, and use generic interfaces, and where runtime behavior can surprise you.

Declaring a Generic Interface

To declare a generic interface, place a type parameter in angle brackets after the interface name. The type parameter can be used in method signatures, constants, and nested types. Here is a minimal example:

public interface Repository<T> { T findById(long id); void save(T entity); }

This interface declares two methods that reference T. The concrete type is supplied by the implementing class, not by the interface itself. The interface does not know what T is at runtime; it only exists at compile time.

You can also declare multiple type parameters. For example, a Mapper interface might map between two types:

public interface Mapper<S, D> { D toDto(S source); S toEntity(D dto); }

Each type parameter is independent and must be provided when the interface is implemented.

Implementing a Generic Interface

A class that implements a generic interface can either remain generic or bind the type parameter to a concrete type. The first approach is useful when the class itself should work with many types:

public class InMemoryRepository<T> implements Repository<T> { private final Map<Long, T> store = new HashMap<>(); @Override public T findById(long id) { return store.get(id); } @Override public void save(T entity) { // assign an id and put in map } }

Here the class declares the same type parameter T and passes it to the interface. The concrete type is determined when the class is instantiated:

Repository<User> userRepo = new InMemoryRepository<>(); Repository<Order> orderRepo = new InMemoryRepository<>();

The compiler checks that userRepo.findById(1) returns a User, not an Object. That is the primary benefit of using a generic interface.

Alternatively, you can create a non-generic class that binds the type parameter:

public class UserRepository implements Repository<User> { @Override public User findById(long id) { ... } @Override public void save(User entity) { ... } }

This is common when the class is specific to one domain type and you want to avoid repeating the type parameter everywhere.

Using Type Bounds to Restrict Type Parameters

Sometimes you need to require that the type parameter has certain capabilities. For example, you might want to compare elements or convert them to a string. You can use a bounded type parameter with the extends keyword:

public interface Sortable<T extends Comparable<T>> { List<T> sort(List<T> input); }

Now the type parameter T must implement Comparable<T>. This lets the interface use compareTo without casting. The bound can also be a class and multiple interfaces, separated by &:

public interface SerializableComparable<T extends Comparable<T> & Serializable> { // methods that rely on both capabilities }

The bound is enforced at compile time. If you try to implement Sortable<String> it works because String implements Comparable<String>, but Sortable<Object> fails because Object does not implement Comparable<Object>.

Wildcards in Generic Interface Usage

When you use a generic interface as a variable type, you often need to handle unknown type arguments. Wildcards let you relax the type constraint. Consider a Repository<T> that you want to read from without knowing the exact type:

public void printAll(Repository<?> repo) { Object entity = repo.findById(1); // works, but type is Object }

The unbounded wildcard ? means the type is unknown. You can read from the repository, but the returned value is Object. To preserve type safety, use a bounded wildcard when you need a specific supertype or subtype:

public void saveAll(Repository<? super User> repo, List<User> users) { for (User u : users) { repo.save(u); // valid because repo accepts User or any supertype } }

Here ? super User allows Repository<User>, Repository<Object>, or Repository<Object>—anything that can accept a User. This is the standard PECS rule: producer extends, consumer super. In this case, the repository is a consumer because you are calling save on it.

Wildcards are not allowed on the left side of an implements clause. You cannot write:

public class MyRepo implements Repository<?> { ... } // invalid

The type parameter must be a concrete type or another type variable when implementing an interface.

Common Pitfalls with Type Erasure

Java generics are implemented via type erasure. The compiler removes all type parameters and inserts casts where necessary. This means that at runtime, a Repository<User> and a Repository<Order> are the same class. You cannot use the type parameter to make runtime decisions. For example, the following code does not compile:

public class BadRepository<T> implements Repository<T> { private final Class<T> type; public BadRepository() { this.type = T.class; // error: T cannot be used as a class literal } }

Because T is erased, there is no T.class. You must pass the Class<T> explicitly if you need runtime type information:

public class GoodRepository<T> implements Repository<T> { private final Class<T> type; public GoodRepository(Class<T> type) { this.type = type; } }

Type erasure also means you cannot overload methods only by the type parameter:

public interface Processor<T> { void process(T item); void process(String item); // error: same erasure }

Both methods erase to process(Object) after erasure, so the compiler rejects the overload. You need to use different method names or a different signature.

Another consequence is that you cannot create arrays of generic types directly. new T[10] is illegal. You must use List<T> instead, or use Array.newInstance with the Class<T> object.

When to Prefer a Generic Interface Over an Abstract Class

Generic interfaces are the right choice when you want to define a contract that multiple unrelated classes can implement. Java allows a class to implement multiple interfaces, but only extend one class. If you need to combine capabilities, interfaces give you more flexibility.

An abstract class is better when you want to share concrete state or non-public helper methods. For example, if every repository needs a Connection and a close() method that releases it, an abstract class can hold that state and provide a template method. A generic interface cannot hold instance state.

Consider the tradeoff:

CriterionGeneric InterfaceAbstract Class
Multiple inheritanceYesNo
Instance stateNoYes
Default methodsYes (since Java 8)Yes
Constructor controlNoYes

If you need to enforce a common constructor or hold a shared field, an abstract class is more appropriate. If you only need to define method signatures and allow different implementations, a generic interface keeps your design decoupled.

Maintaining Backward Compatibility with Raw Types

Before Java 5, there were no generics. To maintain compatibility, you can still use a generic interface without a type argument, which is called a raw type. For example:

Repository repo = new InMemoryRepository(); // raw type

This compiles with a warning. The compiler treats T as Object, and all methods return Object. Raw types exist only for legacy code. New code should always specify a type argument, because raw types bypass the compile-time checks that generics provide.

If you are updating an existing API that uses a raw type, you can add a generic parameter without breaking binary compatibility, but source compatibility may require updating callers. The compiler will generate unchecked warnings where the type is not known. To suppress them, you can use @SuppressWarnings("unchecked"), but only after you have verified that the cast is safe.

A more subtle issue is that you cannot use a generic interface with a primitive type. Repository<int> is invalid; you must use the wrapper class Repository<Integer>. Autoboxing hides the conversion, but it adds a small runtime cost. For high-performance numeric code, consider specialized libraries that avoid boxing, but for most applications the cost is negligible.

Finally, remember that generic interfaces are erased at runtime, so any check like if (obj instanceof Repository<?>) is valid, but if (obj instanceof Repository<User>) is not. The runtime only sees Repository. This limitation is fundamental to Java's generics design and affects all generic types, not just interfaces.

java generic interface: Practical Usage and Code Examples | RYUSLOG DEV