Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
64: Generic Interfaces
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)andV get(K key). - Implement this interface in a class called
MemoryCache<K, V>using aHashMapto store the data internally. - In a
mainmethod, instantiate aMemoryCache<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
Doubleas a value into the User cache without triggering a compiler error.
There are no comments for now.