Java Generic Interfaces
Java Generic Interfaces
A generic interface declares type parameters the same way a generic class does — angle brackets after the interface name, with the parameters usable anywhere a type name appears in the interface body. The difference between a generic interface and a generic class is where the type decision is enforced: a class holds state, so the type argument matters from construction time through the object's lifetime; an interface declares a contract, so the type argument matters for every class that implements it and every call site that invokes it. Comparable<T>, Iterable<E>, Supplier<T>, Predicate<T>, and Function<T,R> in the JDK are all generic interfaces — each one is written once and serves every type that implements or uses it.
What Is a Generic Interface?
A generic interface is declared with one or more type parameters in angle brackets after the interface name. Those parameters appear in the abstract method signatures, default method bodies, and optionally in static method signatures inside the interface.
SYNTAX:
public interface InterfaceName<T> {
void doSomething(T input); // T used as parameter type
T produce(); // T used as return type
boolean test(T value); // T used in conditional method
}
public interface InterfaceName<T, R> {
R transform(T input); // T in, R out - independent types
}
// With bounded type parameter:
public interface Sortable<T extends Comparable<T>> {
int compareTo(T other);
}
IMPLEMENTING A GENERIC INTERFACE - three patterns:
PATTERN 1: Pass the type parameter through (implementing class stays generic)
class TypedProcessor<T> implements Processor<T> { ... }
// TypedProcessor<String> is a Processor<String>
// TypedProcessor<Order> is a Processor<Order>
PATTERN 2: Fix the type argument (implementing class is concrete)
class StringProcessor implements Processor<String> { ... }
// Always a Processor<String> - no type parameter on the class
PATTERN 3: Lambda expression (for single-abstract-method interfaces)
Processor<String> p = input -> input.toUpperCase();
// Only works when Processor has exactly one abstract method
Basic Overview - The Four Things to Know About Generic Interfaces
1. WHERE THE TYPE PARAMETER LIVES IN AN INTERFACE
Fresher view : the <T> after the interface name is the same
syntax as in a generic class. Method signatures
inside the interface use T wherever a type would
normally go. Each implementing class decides
what T actually is.
Deeper view : the interface itself is never instantiated -
the type parameter is resolved by the implementing
class or by the caller creating an anonymous class
or lambda. The compiler checks every implementation
against the declared T and rejects mismatches.
2. THE THREE IMPLEMENTATION PATTERNS
Fresher view : you can implement a generic interface by
staying generic yourself (pass T through),
by locking T to a specific type (concrete class),
or by using a lambda if the interface has one
abstract method.
Deeper view : when a class passes T through - "implements
Processor<T>" - the class and interface share the
same parameter name by convention but it is a
single binding: Box<String> implementing Container<T>
means T=String for that Box. When a class fixes T -
"implements Processor<String>" - the compiler
verifies that every method the class provides
matches the String-specialised contract.
3. DEFAULT METHODS IN GENERIC INTERFACES
Fresher view : a generic interface can have default methods
(methods with a body) that use the type parameter.
Implementing classes inherit these and can override
them or use them as-is.
Deeper view : default methods in generic interfaces are erased
the same way as abstract methods - T becomes Object
in bytecode. The default method implementation is
stored in the interface's class file and called via
invokeinterface at runtime. Overriding a default
method in a generic implementing class follows the
same override rules as any method override.
4. FUNCTIONAL INTERFACES AND LAMBDAS
Fresher view : if a generic interface has exactly ONE abstract
method, any lambda with the matching signature is
automatically an implementation of it.
Predicate<String> p = s -> s.isEmpty(); works
because Predicate<T> has one abstract method test(T).
Deeper view : the compiler matches the lambda's parameter types
and return type against the single abstract method
after substituting the type argument. The lambda
implements the interface's SAM (Single Abstract
Method). Adding @FunctionalInterface protects the
interface from accidentally gaining a second
abstract method, which would break every lambda
that implements it.
Declaring and Using a Generic Interface
Single Type Parameter
// File: Transformer.java
// A generic interface that transforms a value of type T into another T.
// The transformation contract is the same for every T - only the type changes.
public interface Transformer<T> {
// Abstract method - every implementation must provide this
T transform(T input);
// Default method - uses T in its signature and delegates to transform()
// Implementing classes inherit this for free or can override it
default T transformAndLog(T input) {
T result = transform(input);
System.out.println(" Transformed: " + input + " -> " + result);
return result;
}
// Static factory - creates a no-op transformer that returns input unchanged
// Static methods in interfaces CAN use T only via their OWN type parameter
static <T> Transformer<T> identity() {
return input -> input; // lambda - works because Transformer has one abstract method
}
}// File: TransformerDemo.java
public class TransformerDemo {
// PATTERN 1: Generic implementing class - passes T through
// StringTransformer<T> can wrap any Transformer<T>
static class UpperCaseTransformer implements Transformer<String> {
@Override
public String transform(String input) {
return input == null ? "" : input.toUpperCase();
}
}
// PATTERN 2: Concrete implementing class - T fixed to Integer
static class DoubleValueTransformer implements Transformer<Integer> {
@Override
public Integer transform(Integer input) {
return input == null ? 0 : input * 2;
}
}
public static void main(String[] args) {
System.out.println("=== UpperCaseTransformer (T = String) ===");
Transformer<String> upper = new UpperCaseTransformer();
upper.transformAndLog("hello, meesho!");
upper.transformAndLog("product catalog");
System.out.println();
System.out.println("=== DoubleValueTransformer (T = Integer) ===");
Transformer<Integer> doubler = new DoubleValueTransformer();
doubler.transformAndLog(500);
doubler.transformAndLog(1299);
System.out.println();
System.out.println("=== Lambda implementation (T = String) ===");
// Lambda works because Transformer has exactly one abstract method
Transformer<String> trimmer = input -> input == null ? "" : input.trim();
trimmer.transformAndLog(" Laptop Stand ");
trimmer.transformAndLog(" USB Hub ");
System.out.println();
System.out.println("=== Identity transformer ===");
Transformer<String> noOp = Transformer.identity();
noOp.transformAndLog("unchanged value");
}
}Output:
=== UpperCaseTransformer (T = String) ===
Transformed: hello, meesho! -> HELLO, MEESHO!
Transformed: product catalog -> PRODUCT CATALOG
=== DoubleValueTransformer (T = Integer) ===
Transformed: 500 -> 1000
Transformed: 1299 -> 2598
=== Lambda implementation (T = String) ===
Transformed: Laptop Stand -> Laptop Stand
Transformed: USB Hub -> USB Hub
=== Identity transformer ===
Transformed: unchanged value -> unchanged value
Two Type Parameters
When two independent type roles exist in a contract — an input type and an output type — a two-parameter generic interface expresses both cleanly. This is the shape of Function<T,R> in java.util.function, and it is the pattern behind converters, mappers, and serializers.
// File: Converter.java
// Converter<S, T> converts a value of type S (Source) to type T (Target).
// S and T are completely independent - Converter<String, Integer> and
// Converter<Order, OrderDto> are unrelated parameterized types.
public interface Converter<S, T> {
T convert(S source);
// Default method - converts and validates the result
default T convertOrDefault(S source, T defaultValue) {
if (source == null) return defaultValue;
T result = convert(source);
return result != null ? result : defaultValue;
}
}// File: ConverterDemo.java
public class ConverterDemo {
record ProductDto(String id, String name, String formattedPrice) {}
record Product(String productId, String name, double price) {
String getProductId() { return productId; }
String getName() { return name; }
double getPrice() { return price; }
}
public static void main(String[] args) {
System.out.println("=== Converter<String, Integer>: parse price strings ===");
// Lambda implementing Converter<String, Integer>
Converter<String, Integer> priceParser = source -> {
try {
return Integer.parseInt(source.replaceAll("[^0-9]", ""));
} catch (NumberFormatException e) {
return null;
}
};
System.out.println("Rs.1499 -> " + priceParser.convert("Rs.1499"));
System.out.println("Rs.2,999 -> " + priceParser.convert("Rs.2,999"));
System.out.println("free -> " + priceParser.convertOrDefault("free", 0));
System.out.println();
System.out.println("=== Converter<Product, ProductDto>: domain to DTO ===");
// Lambda implementing Converter<Product, ProductDto>
Converter<Product, ProductDto> toDto = product ->
new ProductDto(
product.getProductId(),
product.getName(),
"Rs." + String.format("%.0f", product.getPrice())
);
Product p1 = new Product("ZPT-101", "Basmati Rice 5kg", 499.0);
Product p2 = new Product("ZPT-102", "Cold Press Oil 1L", 349.0);
ProductDto dto1 = toDto.convert(p1); // ProductDto - no cast
ProductDto dto2 = toDto.convert(p2);
System.out.println(dto1);
System.out.println(dto2);
System.out.println();
System.out.println("=== convertOrDefault with null source ===");
ProductDto fallback = new ProductDto("UNKNOWN", "Product unavailable", "N/A");
ProductDto result = toDto.convertOrDefault(null, fallback);
System.out.println(result);
}
}Output:
=== Converter<String, Integer>: parse price strings ===
Rs.1499 -> 1499
Rs.2,999 -> 2999
free -> 0
=== Converter<Product, ProductDto>: domain to DTO ===
ProductDto[id=ZPT-101, name=Basmati Rice 5kg, formattedPrice=Rs.499]
ProductDto[id=ZPT-102, name=Cold Press Oil 1L, formattedPrice=Rs.349]
=== convertOrDefault with null source ===
ProductDto[id=UNKNOWN, name=Product unavailable, formattedPrice=N/A]
JDK Generic Interfaces You Use Every Day
Understanding that Comparable<T>, Iterable<E>, Runnable, Comparable<T>, and java.util.function.* are all generic interfaces - and understanding what their type parameters mean - makes every interaction with them more intentional.
// File: JdkGenericInterfacesDemo.java
import java.util.*;
import java.util.function.*;
public class JdkGenericInterfacesDemo {
// Implementing Comparable<T> - the class fixes T to itself (the canonical pattern)
// This makes instances of OrderSummary sortable by amount
record OrderSummary(String orderId, double amount) implements Comparable<OrderSummary> {
// T is fixed to OrderSummary - compareTo accepts exactly OrderSummary
@Override
public int compareTo(OrderSummary other) {
return Double.compare(this.amount, other.amount);
}
}
// Implementing Iterable<E> - the class fixes E to CartItem
// This makes CartCollection usable in a for-each loop
record CartItem(String name, double price) {}
static class CartCollection implements Iterable<CartItem> {
private final List<CartItem> items = new ArrayList<>();
void add(CartItem item) { items.add(item); }
// E is fixed to CartItem - iterator() returns Iterator<CartItem>
@Override
public Iterator<CartItem> iterator() {
return items.iterator();
}
}
public static void main(String[] args) {
System.out.println("=== Comparable<OrderSummary> - sorting by amount ===");
List<OrderSummary> orders = new ArrayList<>(List.of(
new OrderSummary("ORD-3", 7500.0),
new OrderSummary("ORD-1", 1200.0),
new OrderSummary("ORD-2", 4300.0)
));
Collections.sort(orders); // uses compareTo(OrderSummary)
orders.forEach(o -> System.out.println(" " + o.orderId() + ": Rs." + o.amount()));
System.out.println();
System.out.println("=== Iterable<CartItem> - for-each loop ===");
CartCollection cart = new CartCollection();
cart.add(new CartItem("Wireless Mouse", 799.0));
cart.add(new CartItem("Laptop Stand", 1299.0));
cart.add(new CartItem("USB-C Cable", 299.0));
for (CartItem item : cart) { // compiler calls cart.iterator() behind the scenes
System.out.println(" " + item.name() + " - Rs." + item.price());
}
System.out.println();
System.out.println("=== java.util.function generic interfaces ===");
// Predicate<T> - one abstract method test(T t): boolean
Predicate<OrderSummary> highValue = order -> order.amount() > 5000.0;
System.out.println("ORD-3 is high value? " + highValue.test(new OrderSummary("ORD-3", 7500.0)));
// Function<T, R> - one abstract method R apply(T t)
Function<OrderSummary, String> formatter =
order -> order.orderId() + " = Rs." + order.amount();
System.out.println(formatter.apply(new OrderSummary("ORD-2", 4300.0)));
// Supplier<T> - one abstract method T get()
Supplier<OrderSummary> defaultOrder = () -> new OrderSummary("ORD-DEFAULT", 0.0);
System.out.println("Default: " + defaultOrder.get());
// Consumer<T> - one abstract method void accept(T t)
Consumer<OrderSummary> logger = order ->
System.out.println(" [LOG] Processing: " + order.orderId());
orders.forEach(logger);
}
}Output:
=== Comparable<OrderSummary> - sorting by amount ===
ORD-1: Rs.1200.0
ORD-2: Rs.4300.0
ORD-3: Rs.7500.0
=== Iterable<CartItem> - for-each loop ===
Wireless Mouse - Rs.799.0
Laptop Stand - Rs.1299.0
USB-C Cable - Rs.299.0
=== java.util.function generic interfaces ===
ORD-3 is high value? true
ORD-2 = Rs.4300.0
Default: OrderSummary[orderId=ORD-DEFAULT, amount=0.0]
[LOG] Processing: ORD-1
[LOG] Processing: ORD-2
[LOG] Processing: ORD-3
Generic Interface vs Generic Class - The Key Difference
GENERIC INTERFACE:
- Declares a CONTRACT that implementations must satisfy
- Cannot hold state (no instance fields)
- Multiple unrelated classes can implement the same generic interface
with different type arguments simultaneously
- The type argument is resolved by the implementing class
or by the lambda/anonymous class at the usage site
- Default methods can provide shared behavior using T
- Static methods can provide factory behavior using their OWN type params
Comparable<T> is a contract: "objects of type T can be compared
against each other." String, Integer, LocalDate, and OrderSummary
all implement it independently, with T fixed to themselves.
GENERIC CLASS:
- Declares an IMPLEMENTATION with state
- Holds T-typed fields, sets them in the constructor
- One class, one parameterization per instance
- The type argument is resolved when calling new ClassName<>()
ArrayList<E> is an implementation: it HOLDS elements of type E
in an internal array, manages their storage and retrieval.
THE PRACTICAL RELATIONSHIP:
Most real-world designs pair a generic interface with one or more
generic (or concrete) implementing classes:
Converter<S,T> <- generic interface (contract)
PriceStringConverter <- concrete class implementing Converter<String, Integer>
ProductToDtoConverter <- concrete class implementing Converter<Product, ProductDto>
s -> s.toUpperCase() <- lambda implementing Converter<String, String>
The interface defines the shape. The implementations define the behavior.
Any code that works with Converter<String, Integer> can use any of
these implementations interchangeably.
Basic Example
public class Main {
interface Box<T> {
T get();
}
public static void main(String[] args) {
Box<String> nameBox = () -> "Alice";
Box<Integer> ageBox = () -> 30;
System.out.println("Name: " + nameBox.get());
System.out.println("Age: " + ageBox.get());
}
}Output
Name: Alice Age: 30
- ›
Box<T>is a generic interface with one abstract method,get(), that returns whatever typeTis bound to. - ›
Box<String>andBox<Integer>are two different parameterizations of the exact same interface — one contract, two type arguments. - ›Since
Box<T>has exactly one abstract method, a lambda can implement it directly —() -> "Alice"satisfiesBox<String>, and() -> 30satisfiesBox<Integer>. - ›Calling
get()on each variable returns a value already typed asStringorInteger, with no cast needed anywhere.
Extending Generic Interfaces
A generic interface can extend one or more other interfaces, including other generic interfaces. The type parameters can be passed through, fixed, or combined.
// File: InterfaceExtensionDemo.java
import java.util.List;
public class InterfaceExtensionDemo {
// Base generic interface
interface Repository<T, ID> {
void save(T entity);
T findById(ID id);
List<T> findAll();
}
// Extended interface - adds pagination, keeps the same parameters
interface PageableRepository<T, ID> extends Repository<T, ID> {
List<T> findPage(int page, int pageSize);
int countAll();
}
// Narrower extended interface - fixes ID to Long, T stays free
interface LongIdRepository<T> extends Repository<T, Long> {
// Methods inherited from Repository<T, Long>
// findById() now returns T for a Long ID specifically
}
record Product(Long id, String name) {}
// Implementing PageableRepository<Product, Long>
// T=Product, ID=Long - both parameters supplied
static class InMemoryProductRepo implements PageableRepository<Product, Long> {
private final java.util.Map<Long, Product> store = new java.util.LinkedHashMap<>();
@Override public void save(Product p) { store.put(p.id(), p); }
@Override public Product findById(Long id) { return store.get(id); }
@Override public List<Product> findAll() { return List.copyOf(store.values()); }
@Override
public List<Product> findPage(int page, int pageSize) {
return store.values().stream()
.skip((long) (page - 1) * pageSize)
.limit(pageSize)
.toList();
}
@Override public int countAll() { return store.size(); }
}
public static void main(String[] args) {
InMemoryProductRepo repo = new InMemoryProductRepo();
repo.save(new Product(1L, "Wireless Mouse"));
repo.save(new Product(2L, "Laptop Stand"));
repo.save(new Product(3L, "USB Hub"));
repo.save(new Product(4L, "Webcam HD"));
repo.save(new Product(5L, "Keyboard"));
System.out.println("Total products: " + repo.countAll());
System.out.println("Page 1 (size 2):");
repo.findPage(1, 2).forEach(p -> System.out.println(" " + p));
System.out.println("Page 2 (size 2):");
repo.findPage(2, 2).forEach(p -> System.out.println(" " + p));
System.out.println("By ID 3: " + repo.findById(3L));
}
}Output:
Total products: 5
Page 1 (size 2):
Product[id=1, name=Wireless Mouse]
Product[id=2, name=Laptop Stand]
Page 2 (size 2):
Product[id=3, name=USB Hub]
Product[id=4, name=Webcam HD]
By ID 3: Product[id=3, name=USB Hub]
Best Practices
Use a generic interface when multiple unrelated types need to satisfy the same contract. Comparable<T> works for String, Integer, LocalDate, and any custom class. One interface, countless implementations — no duplication of the sort contract. If the behavior is so specific to one type that no other type could meaningfully implement it, a concrete method on that class is a better fit than a generic interface.
Prefer two-parameter generic interfaces over two single-parameter ones when the types are related. Converter<S,T> expresses "I convert from S to T" in one interface with the relationship visible in the signature. Two separate interfaces Source<S> and Target<T> would lose the converter's directional relationship. Pair the type parameters when they describe two sides of one operation.
Use @FunctionalInterface on any single-abstract-method generic interface designed for lambda use. It prevents accidental addition of a second abstract method (which would silently break every lambda targeting the interface) and communicates to callers that lambda syntax is the intended usage. Every interface in java.util.function is annotated this way.
In implementing classes, be explicit about which implementation pattern you are using. class UpperCase implements Transformer<String> clearly fixes T to String. class Box<T> implements Container<T> clearly passes T through. Both patterns are valid; the choice should be deliberate and visible in the class declaration, not something a reader has to infer from the method signatures.
Do not design a generic interface whose type parameter serves no purpose in any method signature. A type parameter that appears only in the class declaration and never in any method signature is purely decorative — it adds complexity without adding type safety. The type parameter earns its place only when it appears in at least one abstract or default method.
Common Mistakes
Mistake 1 - Implementing the Same Generic Interface Twice With Different Type Arguments
import java.util.List;
// WRONG - a class cannot implement the same generic interface twice
// with different type arguments. COMPILE ERROR.
// After erasure, both become Converter, and two erasures of the same
// interface conflict in the class file.
class DoubleConverter
implements Converter<String, Integer>,
Converter<String, Double> { // COMPILE ERROR
// "a type cannot implement both Converter<String,Integer>
// and Converter<String,Double>"
}
// CORRECT - use two separate methods or two separate classes
class StringToIntConverter implements Converter<String, Integer> {
@Override public Integer convert(String s) { return Integer.parseInt(s); }
}
class StringToDoubleConverter implements Converter<String, Double> {
@Override public Double convert(String s) { return Double.parseDouble(s); }
}Mistake 2 - Treating the Type Argument as Known Inside the Interface
// WRONG - an interface body cannot assume T is a specific type.
// Calling String-specific methods on T is a COMPILE ERROR because T
// might not be String at runtime.
interface Processor<T> {
default void printLength(T input) {
System.out.println(input.length()); // COMPILE ERROR
// length() is not a method of Object - T is erased to Object
// Only Object methods (toString, equals, hashCode, getClass) are callable
}
}
// CORRECT - use bounded type parameter if specific methods are needed,
// or work only with Object-level methods
interface StringProcessor {
default void printLength(String input) { // concrete type - String methods available
System.out.println(input.length());
}
}
// Or bounded - allows any CharSequence method
interface CharSequenceProcessor<T extends CharSequence> {
default void printLength(T input) {
System.out.println(input.length()); // CharSequence has length()
}
}Mistake 3 - Adding a Second Abstract Method to a Lambda-Target Interface
import java.lang.annotation.*;
// A generic interface intended for lambda use
@FunctionalInterface
interface Filter<T> {
boolean test(T item);
// Adding a SECOND abstract method breaks every lambda that targets Filter<T>
// COMPILE ERROR when @FunctionalInterface is present:
// "Multiple non-overriding abstract methods found in interface Filter"
// boolean testWithContext(T item, String context); <- would be COMPILE ERROR
// Default methods are fine - they do not count as abstract
default Filter<T> negate() {
return item -> !test(item);
}
}
// CORRECT - the @FunctionalInterface annotation protects the interface.
// The second abstract method is caught at the interface declaration,
// not silently at every lambda call site that now fails to compile.Mistake 4 - Forgetting That Implementing a Generic Interface Fixes the Type Argument
// WRONG ASSUMPTION - a developer might think they can call
// validate() with any type because the interface declared it as T.
// Once the class fixes T to UpiRequest, the compiler enforces it.
class UpiGatewayFixed implements PaymentGateway<UpiRequest> {
@Override
public boolean validate(UpiRequest request) { return true; }
@Override
public PaymentResult process(UpiRequest request) {
return PaymentResult.success("TXN-1", request.amount());
}
@Override
public String gatewayName() { return "UPI"; }
}
// WRONG - the method below tries to pass CardRequest where UpiRequest is expected
class MistakeCaller {
static void demo() {
UpiGatewayFixed gateway = new UpiGatewayFixed();
CardRequest card = new CardRequest("1234567812345678", "123", "12", "2027", 1000.0, "ORD-1");
// gateway.validate(card); // COMPILE ERROR
// "incompatible types: CardRequest cannot be converted to UpiRequest"
// The type parameter T was fixed to UpiRequest when the class declared
// "implements PaymentGateway<UpiRequest>"
}
}Interview Questions
Q1. What is a generic interface in Java, and how does it differ from a regular interface?
A generic interface declares one or more type parameters in angle brackets after the interface name — interface Converter<S, T> or interface Comparable<T>. Those parameters appear in the abstract method signatures as placeholders: T convert(S source) says "convert from S to T" without committing to specific types. A regular interface uses concrete types directly in its method signatures. The difference is generality: a generic interface can be implemented by multiple unrelated classes, each supplying different type arguments — String, Integer, and OrderSummary all implement Comparable<T> with T fixed to themselves. A regular interface has a fixed contract regardless of the implementing class.
Q2. What are the three ways to implement a generic interface, and when do you choose each?
The first pattern passes the type parameter through: class Box<T> implements Container<T>. The implementing class remains generic and the caller decides T when creating an instance — new Box<String>() makes T = String throughout. Choose this when the implementation is general-purpose and should work for any type. The second pattern fixes the type argument: class UpperCaseTransformer implements Transformer<String>. The class is concrete — it is always a Transformer<String>. Choose this when the implementation is specific to one type and has no use with other types. The third pattern is a lambda expression when the interface has exactly one abstract method: Transformer<String> t = s -> s.toUpperCase(). Choose lambdas for short, inline, one-off implementations.
Q3. Why can't a class implement the same generic interface twice with different type arguments?
Because of type erasure: at the bytecode level, both Converter<String, Integer> and Converter<String, Double> erase to the same raw type Converter. A class file cannot contain two implementations of the same erased interface — the method signatures after erasure (Object convert(Object)) would collide. The compiler catches this at the source level and rejects the declaration with an error about implementing the same interface more than once. The workaround is two separate classes or two differently named interfaces.
Q4. What is the relationship between generic interfaces and functional interfaces, and how do lambdas work with them?
A functional interface is a generic or non-generic interface with exactly one abstract method. When a generic interface has one abstract method — like Predicate<T> with test(T t), or Function<T,R> with R apply(T t) — a lambda expression is automatically an implementation of that interface for whatever type argument is declared at the usage site. The compiler substitutes the type argument into the single abstract method signature and verifies the lambda's parameter type and return type match. Adding @FunctionalInterface to the interface causes the compiler to reject any accidental addition of a second abstract method, protecting every existing lambda from silently breaking.
Q5. How does implementing Comparable
implements Comparable<String> tells the compiler that compareTo() accepts exactly String. The compiler verifies both sides of every comparison: the object compared and the argument to compareTo() must both be String. implements Comparable (raw type) means compareTo() accepts Object, requires an explicit cast inside the method, and produces unchecked warnings. The raw type also prevents the class from being used with APIs that require T extends Comparable<T> as a bound, because the raw implementation does not satisfy the bounded constraint. Using raw Comparable is always the wrong choice in new code.
Q6. Can a generic interface have default methods that use the type parameter, and how do they behave?
Yes. Default methods in a generic interface can use the interface's type parameters in their signatures and bodies. default T transformAndLog(T input) in Transformer<T> uses T in both the parameter and the return type. When a class implements Transformer<String>, the default method transformAndLog(String input) is inherited with T = String already substituted. The default method body interacts with the type through the same Object-based mechanism as abstract methods — after erasure, T becomes Object in the bytecode, and the compiler inserts any necessary casts. Implementing classes can override a default method exactly as they would override an abstract method.
FAQs
Can a generic interface have static methods, and can those methods use the interface's type parameter?
A generic interface can have static methods, but those methods cannot use the interface's own type parameter — for the same reason static members of a generic class cannot. Static methods belong to the interface itself, not to any parameterization of it. A static method can declare its own independent type parameter: static <T> Transformer<T> identity() works, where T here is the static method's own parameter, not the interface's T. This is the pattern used by Comparator.comparing(keyExtractor) and Function.identity() in the JDK.
What happens when a generic interface extends a non-generic interface?
The generic interface inherits the non-generic interface's contract alongside its own. The implementing class must satisfy both. For example, if Sortable<T> extends Serializable (non-generic), any class implementing Sortable<Order> must also be Serializable. The two are completely independent — the non-generic parent contributes no type parameters, and the generic child's type parameter has no effect on the non-generic parent's methods.
Can a generic interface declare a type parameter with a bound?
Yes. interface Sortable<T extends Comparable<T>> restricts T to types that are comparable with themselves. The bound appears in the type parameter declaration, not in the method signatures (though it may appear there too). The bound is enforced at every usage site: Sortable<String> compiles because String implements Comparable<String>, but Sortable<Object> does not compile because Object does not implement Comparable<Object>.
Is there a performance difference between implementing a generic interface versus a concrete interface?
No meaningful difference. After type erasure, both compile to the same bytecode structure. Method dispatch through an interface (invokeinterface bytecode instruction) works identically for generic and concrete interfaces. The type argument exists only in the compiler's checking phase. The JIT compiler applies the same optimizations — including inlining — to interface method calls regardless of whether the interface was generic at the source level.
Can a record implement a generic interface?
Yes. A record can implement any number of interfaces, including generic ones. record OrderSummary(String id, double amount) implements Comparable<OrderSummary> is valid and commonly written. The record's canonical constructor, accessor methods, and generated equals(), hashCode(), and toString() are independent of the interface implementation. The methods required by the interface must be explicitly implemented in the record body, just as in any class.
Can an annotation type extend a generic interface?
No. Annotation types (@interface) implicitly extend java.lang.annotation.Annotation and cannot extend any other interface, generic or otherwise. The @interface declaration syntax does not support an extends clause. Annotation types can have elements of generic types as their element types (using Class<?> or arrays), but the annotation type itself does not participate in the generic interface hierarchy.
Summary
A generic interface declares a contract parameterized by type — the implementing class or the lambda expression decides what that type actually is. The interface defines the shape; the type argument defines the scope. Comparable<T> says "I can compare to another T" without knowing what T is; String implements Comparable<String> fills in that T and makes the contract specific. Converter<S,T> says "I convert from S to T"; a lambda s -> Integer.parseInt(s) fills in S=String and T=Integer at the usage site.
Three rules govern every decision in this topic. First, a class cannot implement the same generic interface twice with different type arguments — erasure makes both implementations identical bytecode, and the conflict is rejected at compile time. Second, default methods in generic interfaces use the type parameter the same way abstract methods do — callers receive them with T already resolved to their declared type argument. Third, a single-abstract-method generic interface is automatically a lambda target — the compiler substitutes the type argument into the method signature and verifies the lambda against it.
The interfaces you use most often in Java — Comparable<T>, Iterable<E>, Predicate<T>, Function<T,R>, Supplier<T>, Consumer<T> — are all generic interfaces. Understanding that they are generic interfaces, not magic, means understanding exactly why Predicate<String> accepts a lambda with a String parameter, why Iterable<CartItem> makes for (CartItem item : collection) work, and why Comparable<T> makes sorting self-contained rather than requiring an external comparator.
What to Read Next
Learn how to write a method that works with any data type.