-
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
66: Practice Exercise: Building a Generic Pair and Container Class
I've seen a lot of developers start their journey with generics by trying to "fake it" using the Object class. It feels like the right move at first because Object can hold anything. But in a production environment, this is a recipe for a midnight emergency call. Let's look at a piece of code a learner sent me recently who was trying to build a simple Pair class to hold two related values.
public class Pair {
private Object first;
private Object second;
public Pair(Object first, Object second) {
this.first = first;
this.second = second;
}
public Object getFirst() { return first; }
public Object getSecond() { return second; }
}
// Usage
Pair myPair = new Pair("Age", 30);
String label = (String) myPair.getFirst();
Integer value = (Integer) myPair.getSecond();
The Casting Nightmare
At first glance, this works. But look at the usage code. I'm forced to manually cast the result of getFirst() to a String and getSecond() to an Integer. This is where things fall apart. What happens if some other part of the program puts an Integer in the first slot and a String in the second? The compiler won't say a word, but your app will crash with a ClassCastException at runtime.
Relying on manual casting is essentially telling the Java compiler, "Trust me, I know what I'm doing," and in a large codebase, we are almost always wrong. We want the compiler to catch these mistakes before the code ever runs.
Letting the Compiler Do the Heavy Lifting
To fix this, we need to introduce type parameters. Instead of telling Java that the pair holds Object, we tell it that the pair holds "some type T" and "some type U". This shifts the responsibility of type checking from you (at runtime) to the compiler (at compile-time).
public class Pair<L, R> {
private final L left;
private final R right;
public Pair(L left, R right) {
this.left = left;
this.right = right;
}
public L getLeft() { return left; }
public R getRight() { return right; }
}
// Usage
Pair<String, Integer> myPair = new Pair<>("Age", 30);
String label = myPair.getLeft(); // No casting needed!
Integer value = myPair.getRight(); // No casting needed!
Now, if I try to put an Integer where a String is expected, the IDE will highlight it in red immediately. That's the power of generics: we've traded "runtime surprises" for "compile-time errors."
Nesting Generics in Container Classes
Once you have a generic Pair, you'll often find yourself needing a way to group these pairs together. This is where a generic Container class comes in. You might be tempted to just use an ArrayList, but creating your own container allows you to add specific domain logic—like ensuring the container is never empty or adding a method to find a pair based on the "left" key.
When you build a container for a generic type, you can either make the container itself generic, or make it generic based on the Pair's types. For example, a PairContainer<L, R> would be a class that specifically manages a collection of Pair<L, R> objects. I prefer this approach because it keeps the type signatures clean and prevents you from accidentally mixing different types of pairs in the same list.
- Use
<T>for a single generic type (T for Type). - Use
<L, R>or<K, V>when you have multiple distinct types. - Always mark your generic fields as
finalif they shouldn't change after the pair is created; it makes your generic classes thread-safe and easier to reason about.
📋 Practical Task
Exercise: Implementing a Type-Safe StudentGrade Registry
Your task is to build a small system to manage student grades using generics. You need to create two classes: a GradePair and a GradeRegistry.
- GradePair<S, G>: This class should hold two values: a student identifier (S) and a grade (G). It should have a constructor and getter methods for both.
- GradeRegistry<S, G>: This class should act as a container for
GradePair<S, G>objects. It should contain:- A
ListofGradePair<S, G>. - A method
addGrade(S student, G grade)that creates a newGradePairand adds it to the list. - A method
getGrades()that returns the full list of pairs.
- A
Testing your code: In your main method, instantiate a GradeRegistry<String, Double>. Add a few students (using their names as Strings and their GPAs as Doubles). Iterate through the registry and print each student's name and grade. Verify that you do not have to use any explicit casting (e.g., (String) or (Double)) to access the data.
There are no comments for now.