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
63: Wildcards in Generics
A few years ago, I was reviewing a PR for a junior dev who was building a financial reporting module. He had written a perfectly logical method to sum up a list of values: public double sum(List<Number> numbers). Everything looked great until he tried to pass a List<Integer> into it. He was genuinely frustrated—he knew Integer was a subclass of Number, so why on earth was the Java compiler telling him the types were incompatible? He felt like he was fighting the language rather than using it.
This is the "Generics Trap." In Java, generics are invariant. This means that while an Integer is a Number, a List<Integer> is not a List<Number>. If Java allowed this, you could accidentally add a Double into a list that was supposed to only hold Integers, which would crash your program with a ClassCastException at runtime. To solve this without losing type safety, we use wildcards.
Expanding Read Access with Upper Bounds
When you want a method to accept a list of Numbers, or any subclass of Number (like Integer, Double, or Long), you use the ? extends T syntax. This is known as an upper bound.
public double sum(List<? extends Number> numbers) {
double total = 0;
for (Number n : numbers) {
total += n.doubleValue();
}
return total;
}
By using ? extends Number, you're telling the compiler: "I don't care exactly what specific subtype of Number this list holds, as long as it's some kind of Number." Now, our friend from the anecdote can pass in a List<Integer> or a List<Double> without a hitch.
There is a catch, though. When you use extends, the list becomes effectively read-only for that specific type. You can pull Number objects out of the list, but you cannot put anything into it (except for null). Why? Because the compiler doesn't know if the list is actually a List<Integer> or a List<Double>, so it forbids adding elements to prevent you from putting a Double into an Integer list.
Allowing Writes with Lower Bounds
On the flip side, you'll run into situations where you want to add elements to a collection. If you're writing a utility that moves items from one list to another, you need a lower bound: ? super T.
Imagine you have a method that adds a list of integers to a destination collection. You don't just want it to work for a List<Integer>; it should also work for a List<Number> or even a List<Object>, because all of those can safely hold an integer.
public void addIntegers(List<? super Integer> destination, List<Integer> source) {
for (Integer i : source) {
destination.add(i);
}
}
The ? super Integer wildcard tells Java: "This list can hold Integers, or any supertype of Integer." This is the opposite of the upper bound. Here, you can safely write to the list, but you can't safely read from it as anything more specific than an Object, because the list might be a List<Object>.
I usually tell my mentees to remember the PECS mnemonic: Producer Extends, Consumer Super. If your collection is producing values for you to use, use extends. If your collection is consuming values that you are putting into it, use super.
📋 Practical Task
Exercise: Implementing a Flexible Asset Transfer System
You are building a system for a portfolio manager. You have a class hierarchy where Asset is the base class, and Stock and Bond are subclasses of Asset.
Your task is to implement a PortfolioManager class with two methods:
public double calculateTotalValue(List<? extends Asset> assets): This method should iterate through a list of assets and sum their values (assume theAssetclass has agetValue()method). It must be able to accept aList<Stock>, aList<Bond>, or aList<Asset>.public void transferAssets(List<? extends Asset> source, List<? super Asset> destination): This method should move all assets from the source list into the destination list.
Requirements:
- Ensure the
calculateTotalValuemethod uses an upper-bound wildcard to allow any asset subtype. - Ensure the
transferAssetsmethod uses both an upper-bound for the source (the producer) and a lower-bound for the destination (the consumer). - Verify your logic by creating a
List<Stock>and transferring its contents into aList<Asset>.
There are no comments for now.