Generic Interfaces in Java
An app stores students, books and orders. Each needs the same operations: save one, find one by its id, list them all. Writing a separate StudentStore, BookStore and OrderStore interface would repeat the same contract three times. A generic interface writes the contract once, with type parameters for "what kind of thing" and "what kind of id", and each implementation fills them in.
A Generic Contract
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class Repositories {
interface Repository<T, ID> {
void save(ID id, T item);
Optional<T> findById(ID id);
List<T> findAll();
}
record Student(String name, int marks) { }
static class StudentRepository implements Repository<Student, String> {
private final Map<String, Student> data = new HashMap<>();
public void save(String id, Student s) { data.put(id, s); }
public Optional<Student> findById(String id) {
return Optional.ofNullable(data.get(id));
}
public List<Student> findAll() {
return new ArrayList<>(data.values());
}
}
public static void main(String[] args) {
Repository<Student, String> repo = new StudentRepository();
repo.save("S1", new Student("Navika", 91));
repo.save("S2", new Student("Advitha", 95));
String found = repo.findById("S2").map(Student::name).orElse("none");
System.out.println(found);
System.out.println(repo.findById("S9").isPresent());
System.out.println(repo.findAll().size() + " students");
}
}Advitha
false
2 studentsHow it works
interface Repository<T, ID>declares two type parameters: the stored type and the id type.implements Repository<Student, String>fills them in. Every method must now useStudentandStringin place ofTandID.findByIdreturns anOptional<T>, itself a generic type, which either holds a value or is empty.- Code that uses the repository depends only on the interface, so a
BookRepository implements Repository<Book, Long>would plug in the same way.
Two Ways to Implement
| Approach | Example | Result |
|---|---|---|
| Fill in the types | class StudentRepository implements Repository<Student, String> | A class for one specific type |
| Stay generic | class MemoryRepository<T, ID> implements Repository<T, ID> | A reusable class that works for any type |
The second approach lets one in-memory class serve every entity: new MemoryRepository<Student, String>(), new MemoryRepository<Book, Long>().
Generic Interfaces You Already Use
| Interface | Type parameter means | Single method |
|---|---|---|
Comparable<T> | What it can be compared with | int compareTo(T other) |
Comparator<T> | What it compares | int compare(T a, T b) |
Iterable<T> | What a for-each loop gives you | Iterator<T> iterator() |
List<E>, Set<E> | The element type | Many |
Function<T, R> | Input and result types | R apply(T t) |
Common Mistakes
Implementing the raw interface: implements Comparable. You lose type checking and need casts; write Comparable<Marks>.
Mixing up the order of type arguments. Repository<String, Student> means String items with Student ids; follow the declaration's order.
Implementing the same generic interface twice with different types. implements Comparable<A>, Comparable<B> isn't allowed.
Practice
- Easy: Write
interface Converter<F, T> { T convert(F from); }and an implementation that converts aStringto itsIntegerlength. - Harder: Write
class MemoryRepository<T, ID> implements Repository<T, ID>and use it for bothStudent(withStringids) and aBookrecord (withIntegerids).
Key Takeaways
Was this tutorial helpful?