Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

At this point in the course, you've used generics with classes and lists, but you might not have thought much about applying them to interfaces. In my experience, this is where a lot of developers start to see the real power of Java's type system. When we design an interface, we're defining a contract. The problem is that if that contract is too specific, you end up writing the same code over and over. If it's too vague, you lose all the safety that Java provides.

The duplication trap

Imagine we're building a system to manage a database of different entities—let's say User and Product. The naive way to handle this is to create a specific repository interface for every single entity type. It looks something like this:

public interface UserRepository {
    void save(User user);
    User findById(int id);
}

public interface ProductRepository {
    void save(Product product);
    Product findById(int id);
}

Now, this works fine for two entities. But what happens when your project grows to fifty? You'll find yourself copy-pasting the same method signatures across fifty different interfaces. It's tedious, and it's a maintenance nightmare. If you decide that findById should actually return an Optional instead of a nullable object, you now have fifty files to update. I've spent far too many hours of my career doing exactly that kind of mindless cleanup.

The casting nightmare

To avoid that duplication, you might be tempted to use a "catch-all" interface using Object. This seems clever at first because it's one interface to rule them all:

public interface Repository {
    void save(Object entity);
    Object findById(int id);
}

The problem here is that you've just told the compiler to stop helping you. Every time you call findById, you get back an Object. To actually do anything with that user or product, you have to cast it: User user = (User) repo.findById(1);. This is dangerous. If someone accidentally passes a ProductRepository where a UserRepository was expected, the code will compile perfectly, but it will blow up with a ClassCastException at runtime. In a professional codebase, runtime crashes are the enemy; we want the compiler to scream at us while we're still writing the code, not after we've deployed it.

Letting the compiler do the heavy lifting

This is where generic interfaces come in. By adding a type parameter to the interface itself, we can keep the contract flexible while maintaining strict type safety. We define the "shape" of the repository once, but we let the implementation decide what specific type it handles.

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

Now, when you implement this interface, you specify the type. The UserRepository becomes a specialized version of the Repository contract:

public class UserRepositoryImpl implements Repository<User> {
    @Override
    public void save(User user) {
        // Database logic for users
    }

    @Override
    public User findById(int id) {
        // Database logic for users
        return new User(); 
    }
}

Notice what happened here: we didn't have to write a new interface for User, and we didn't have to use Object. The compiler now knows exactly what UserRepositoryImpl returns. If you try to pass a Product into the save method of a UserRepositoryImpl, it won't even compile. That's the sweet spot—maximum flexibility with zero loss in safety.




📋 Practical Task

Implementing a Generic Cache System

You need to create a simple caching mechanism that can handle different types of data. Instead of creating separate caches for every data type, you will implement a generic interface.

Requirements:

  • Create a generic interface named Cache<K, V>. It should have two methods: void put(K key, V value) and V get(K key).
  • Implement this interface in a class called MemoryCache<K, V> using a HashMap to store the data internally.
  • In a main method, instantiate a MemoryCache<Integer, String> to store user IDs and usernames. Put a few values in and retrieve one.
  • Instantiate a second MemoryCache<String, Double> to store product SKUs and their prices. Put a few values in and retrieve one.
  • Verify that you cannot put a Double as a value into the User cache without triggering a compiler error.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.