Advanced Generics
1. Non-Generic Approach (Problem Illustration)
-
Usage:
-
Using
Object
type to store and retrieve values when the specific type is not known at compile time or needs to be flexible. -
Points to Note:
-
The
Print
class (non-generic version) uses anObject
fieldvalue
. -
Object
is the superclass of all other classes in Java. -
setPrintValue(Object value)
can accept any object type (e.g.,Integer
,String
) because they are all subclasses ofObject
. -
getPrintValue()
returns anObject
. To use it as its actual type, explicit typecasting is required (e.g.,(int)printValue
,(String)printValue
). - Without typecasting, direct use might lead to compile-time errors or require operations only available on
Object
. - Typecasting can lead to
ClassCastException
at runtime if the cast is incorrect. - Examples:
// Non-generic Print class
public class Print {
Object value;
public Object getPrintValue() {
return value;
}
public void setPrintValue(Object value) {
this.value = value;
}
}
// Usage
public class Main {
public static void main(String args[]) {
Print printObj1 = new Print();
printObj1.setPrintValue(1); // Here i am passing Integer 1
Object printValue = printObj1.getPrintValue();
// we can not use printValue directly, we have to typecast it, else it will be compile time error
// Because it is a object, it can be anything, we need to typecast
if (((int) printValue == 1)) {
// do something
}
// Continuing with comments above, if we code setPrintValue("Hello");
// then we need to typecast (String)printValue
}
}
2. Generic Class
-
Usage:
-
To create classes that can work with different data types without sacrificing type safety.
- The type parameter (e.g.,
<T>
) acts as a placeholder for the actual type that will be specified when an object of the generic class is created. -
Points to Note:
-
Syntax:
public class ClassName<T> { ... }
orpublic class ClassName<K, V> { ... }
for multiple type parameters. The syntax is Dam Diamond syntax<>
. -
T
(or any other valid identifier) is the type parameter. - The type parameter
T
can be used to declare instance variables, method parameters, and return types. - When instantiating a generic class, you specify the actual type argument:
Print<Integer> p = new Print<Integer>();
. - The "Diamond syntax"
<>
can be used for type inference if the compiler can determine the type:Print<Integer> p = new Print<>();
. - Generic Type (in above example
<T>
) can be any non-primitive object type. (e.g.,Integer
,String
, notint
). - Using generics eliminates the need for explicit typecasting when retrieving values, providing compile-time type safety. Then we no longer need typecasting.
- Examples:
// Generic Print class
public class Print<T> {
T value;
public T getPrintValue() {
return value;
}
public void setPrintValue(T value) {
this.value = value;
}
}
// Usage
public class Main {
public static void main(String args[]) {
Print<Integer> printObj1 = new Print<Integer>(); // or new Print<>();
printObj1.setPrintValue(1);
Integer printValue = printObj1.getPrintValue(); // No cast needed
if (printValue == 1) {
//do something
}
}
}
// 3. More than one Generic Types Example
// Class with multiple generic types
public class Pair<K, V> {
private K key;
private V value;
public void put(K key, V value) {
this.key = key;
this.value = value;
}
// ... getters for key and value
}
// Usage of Pair
public class Main {
public static void main(String args[]) {
Pair<String, Integer> pairObj = new Pair<>();
pairObj.put("hello", 1243);
}
}
3. Subclassing Generic Classes
3.1. Non Generic Subclass
-
Usage:
-
A non-generic class extends a generic class by providing a specific type argument for the superclass's type parameter.
-
Points to Note:
-
The subclass itself is not generic. It "fixes" the type parameter of the generic superclass.
- Examples:
public class Print<T> {
T value;
// ... methods
}
public class ColorPrint extends Print<String>{
// ColorPrint will always work with String for the Print superclass part
}
3.2. Generic Subclass
-
Usage:
-
A generic class can extend another generic class.
- The subclass can pass its own type parameter(s) to the superclass or specify a concrete type.
-
Points to Note:
-
Syntax:
public class SubClass<T> extends SuperClass<T> { ... }
- The subclass uses the same type parameter
T
(or a different one) and passes it to the generic superclass. - The comment "//Then we don't need to specific the type" implies that the subclass remains generic.
- Examples:
public class Print<T> {
T value;
public void setPrintValue(T value) { this.value = value; }
// ... other methods
}
public class ColorPrint<T> extends Print<T>{ //Then we don't need to specific the type
// This subclass is also generic and uses the same type T as Print
}
// Usage
public class Main {
public static void main(String args[]) {
ColorPrint<String> colorPrintObj = new ColorPrint<>();
//Note that ColorPrint<String> colorPrintObj = new ColorPrint<String>();
//is also correct
colorPrintObj.setPrintValue("2");
}
}
4. Generic Method
-
Usage:
-
Methods that have their own type parameters, independent of any class-level type parameters.
- Useful when a method's logic is generic, but the class itself doesn't need to be generic, or the method needs different type parameters than the class.
- What if we only wants to make Method Generic, not the complete Class, we can write Generic Methods too.
-
Points to Note:
-
Type Parameter should be before the return type of the method declaration.
- Type Parameter scope is limited to method only.
- The syntax is like `public
void setValue(T object ){} - Can be static or non-static.
- Examples:
// public class Pair<K, V> { private K key; public K getKey(){return key;} ... } // Assumed Pair class
public class GenericMethod {
public <K, V> void printValue(Pair<K, V> pair1, Pair<K, V> pair2) {
// Example usage: assuming Pair has getKey()
if (pair1.getKey().equals(pair2.getKey())) { // Original OCR has pair2.getKey() twice. Corrected to pair1.
//do something
}
}
}
// Another example from the IDE screenshot
// package Generics;
public class Print{ // Could be non-generic or generic itself
public <T> void setValue(T busObject){ // T is a type parameter for this method
//do something
}
}
// Usage in Main (from IDE screenshot)
// package Generics;
// class Bus {} // Assumed
// class Car {} // Assumed
public class Main {
public static void main(String args[]) {
Print printObj = new Print();
printObj.setValue(new Bus()); // Type T inferred as Bus
// Here is 9th-line, we can change the parameter to new Car() as well, since setValue is a generic method
printObj.setValue(new Car()); // Type T inferred as Car
}
}
5. Raw Type
-
Usage:
-
Using a generic class or interface without specifying any type arguments.
- It's a name of the generic class or interface without any type argument.
-
Points to Note:
-
Essentially bypasses generic type checking, behaving like pre-generics code (uses
Object
). - Leads to loss of type safety and requires manual casting, potentially causing
ClassCastException
at runtime. - Generally discouraged; generics were introduced to avoid this.
- The comment
//internally it passes Object as parametrized type.
forPrint<String> parametrizedTypePrintObject = new Print<>();
(when using diamond operator for inference) is distinct from a raw type instantiation. - For a raw type
Print rawTypePrintObject = new Print(); //Raw type, i didn't pass the type
, allT
s are treated asObject
. - Examples:
public class Print<T> {
T value;
public void setPrintValue(T value) { this.value = value; }
public T getPrintValue() { return value; }
}
public class Main {
public static void main(String args[]) {
// Parameterized type
Print<String> parametrizedTypePrintObject = new Print<>();
//internally it passes Object as parametrized type. (This comment is a bit confusing, it infers String)
parametrizedTypePrintObject.setPrintValue("test");
// Raw type
Print rawTypePrintObject = new Print(); //Raw type, i didn't pass the type
rawTypePrintObject.setPrintValue(1);
rawTypePrintObject.setPrintValue("hello");
// Object val = rawTypePrintObject.getPrintValue(); // Requires casting
}
}
6. Bounded Generics
It can be used at generic class and method since sometimes we want to bound the type of T
.
6.1. Upper Bound (<T extends Number>
)
-
Usage:
-
Restricts the type parameter
T
to be a specific type (BoundType
) or a subtype ofBoundType
. -
BoundType
can be a class or an interface. -
<T extends Number>
meansT
can be of typeNumber
or its Subclass only. - Here superclass (in this example
Number
) we can have interface too. -
Points to Note:
-
Syntax:
public class ClassName<T extends Number> { ... }
- Here
Number
is a superclass in java likeString
which has the type:Integer
,Double
,BigInteger
,BigDecimal
,... (Note:String
is not a superclass ofNumber
types;Number
is the superclass ofInteger
,Double
etc.) - So now
T
only can be the type ofNumber
can be. - If
BoundType
is an interface,extends
is still used (notimplements
). -
Notice that there are three similar things:
- If we create a superclass and a subclass, then we use
subclassName extends superclassName
- If we create a interface and implement it in a class, then we use
className implements interfaceName
- If we create a generic class and bound it, then we use
genericClassName<T extends BoundedClassName>
- Examples:
- If we create a superclass and a subclass, then we use
// Upper Bound
// Class with upper bounded type parameter
public class Print<T extends Number> {
T value;
public void setPrintValue(T value) { this.value = value; }
public T getPrintValue() { return value; }
}
public class Main {
public static void main(String args[]) {
// Now here is a correct example
// Correct
Print<Integer> parametrizedTypePrintObject = new Print<Integer>();
// Incorrect example (if T is bounded by Number)
// Print<String> parametrizedTypePrintObject = new Print<>(); // Compile error if String is not a Number
}
}
6.2. Multi Bound (<T extends ClassA & InterfaceB & InterfaceC>
)
-
Usage:
-
Restricts the type parameter
T
to be a subtype of a class AND implement one or more interfaces. -
Points to Note:
-
Recall that Java don't allowed a subclass extends more than one superclass, but it do allow a class to implements more than one interface by override.
- Multi Bound:
<T extends Superclass & interface1 & interface N>
- The first restrictive type should be concrete class.
- 2,3 and so on... can be interfaces.
- The class (if present) must be listed first in the bound list.
- You can have at most one class in the bound.
- You can have multiple interfaces.
- Examples:
class ParentClass {}
interface Interface1 {}
interface Interface2 {}
// Class 'A' conforming to bounds
public class A extends ParentClass implements Interface1, Interface2{
}
// Generic class with multi-bound
public class Print<T extends ParentClass & Interface1 & Interface2> {
T value;
public T getPrintValue() { return value; }
public void setPrintValue(T value) { this.value = value; }
}
public class Main {
public static void main(String args[]) {
//This is allowed
A obj = new A();
Print<A> printObj = new Print<>();
printObj.setPrintValue(obj);
}
}
//Now if class A be this
//public class A extends ParentClass implements Interface1 {
//}
//Then in the Main function it will have error since print class need to implement two interface
7. Wildcards
7.1. Wildcards Introduction & Invariance
-
Usage:
-
Used to create more flexible generic methods or variables when the exact type argument is unknown or needs to vary.
- Represented by
?
. -
Points to Note:
-
Generics are invariant. This means
List<A>
is not related toList
by subtyping, even ifA
is a subtype ofB
. - Therefore,
List<Vehicle> vehicleList = busList; //NO
is invalid ifBus
extendsVehicle
. - Similarly,
busList = vehicleList; //no
is invalid. - However, direct object assignment
Vehicle vehicleob = busObj; // ok
is valid due to polymorphism. - Line 26 (
vehicleob = busObj;
) is valid because parent classVehicle
can keep the object of a child classBus
. - Line 17 (
vehicleList = busList;
) and 19 (busList = vehicleList;
) are not valid since list of vehicle is not a parent of list of bus also because vehiclelist can have object Bus and Car. - Examples:
// package Generics;
import java.util.ArrayList;
import java.util.List;
class Vehicle {}
class Bus extends Vehicle {}
class Car extends Vehicle {}
public class Main {
public static void main(String args[]) {
List<Vehicle> vehicleList = new ArrayList<>();
vehicleList.add(new Bus());
vehicleList.add(new Car());
List<Bus> busList = new ArrayList<>();
// vehicleList = busList; //NO (invariance)
// busList = vehicleList; //no (invariance)
Vehicle vehicleob = new Vehicle();
Bus busobj = new Bus();
vehicleob = busobj; // ok (polymorphism)
}
}
// Futhermore
// public class Print{
// public void setPrintValues(List<Vehicle> vehicleList){}
// }
// Here we fix the type of list be Vehicle, then we cannot setPrintValues(busList) since busList is List<Bus>
7.2. Upper Bounded Wildcard (<? extends Type>
)
-
Usage:
-
Represents an unknown type that is a subtype of
Type
(orType
itself). i.e. class Name and all its child class below. - Useful when you want to read items from a generic collection but don't need to add to it. (Producer Extends - PECS).
-
Points to Note:
-
List<? extends Vehicle>
means a list of some specific type that extendsVehicle
. - You can safely get elements out of such a list and treat them as
Vehicle
. - You generally cannot add elements to such a list (except
null
), because the compiler doesn't know the exact subtype. - Examples:
import java.util.List;
class Vehicle {}
// class Bus extends Vehicle {} // Assumed
public class Print {
// This method can accept List<Vehicle>, List<Bus>, List<Car>, etc.
public void setPrintValues(List<? extends Vehicle> vehicleList) {
for (Vehicle v : vehicleList) { // Can safely read as Vehicle
System.out.println(v);
}
// vehicleList.add(new Bus()); // Compile Error: not safe
}
}
7.3. Lower Bounded Wildcard (<? super Type>
)
-
Usage:
-
Represents an unknown type that is a supertype of
Type
(orType
itself). i.e. class Name and all its super class above. - Useful when you want to add items to a generic collection. (Consumer Super - PECS).
-
Points to Note:
-
List<? super Vehicle>
means a list of some specific type that is a supertype ofVehicle
(e.g.,List<Vehicle>
,List<Object>
). - You can safely add instances of
Vehicle
or its subtypes (likeBus
) to this list. - When you retrieve elements, you can only be sure they are
Object
. - The example
//printObj.setPrintValues(busList); //But this doesn't work
(page 8 line 15) forList<? super Vehicle>
is becauseList<Bus>
is not a "list of a supertype of Vehicle".List<Object>
orList<Vehicle>
would work. - Examples:
import java.util.List;
import java.util.ArrayList; // Added for main example
class Vehicle {}
class Bus extends Vehicle {}
public class Print {
public void setPrintValues(List<? super Vehicle> vehicleList) {
vehicleList.add(new Vehicle());
vehicleList.add(new Bus());
}
}
public class Main { // From page 8, lines 10-15
public static void main(String[] args) {
Print printObj = new Print();
List<Object> objList = new ArrayList<>();
List<Vehicle> vehicleList = new ArrayList<>(); // Assuming vehicleList is initialized
List<Bus> busList = new ArrayList<>(); // Assuming busList is initialized
//Then in the main function the following will works
printObj.setPrintValues(objList);
printObj.setPrintValues(vehicleList);
//But this doesn't work
//printObj.setPrintValues(busList);
}
}
7.4. Unbounded Wildcard (<?>
)
-
Usage:
-
Represents a list (or other generic type) of an unknown type.
- Used when the method's logic does not depend on the specific type of elements.
- Unbounded wildcard
<?>
only you can read. -
Points to Note:
-
Use this when we know that our method can mostly work on your objects. All the methods available in the
Object
class are applicable, as we know that every class in Java is a subclass of theObject
class. - You can only read elements as
Object
. - You cannot add elements to a collection of type
List<?>
(exceptnull
). - Examples:
import java.util.List;
public class PrintUtils {
//wild card method
public void computeList(List<?> source) {
Object ob = source.get(0); // Can get as Object
// source.add("new element"); // Compile Error
}
}
7.5. Wildcard Method vs. Generic Type Method
-
Wildcard Method:
-
Allows different concrete types for different parameters if they all conform to their respective wildcard bounds.
-
Example:
public void computeList(List<? extends Number> source, List<? extends Number> destination)
-
source
could beList<Integer>
anddestination
could beList<Float>
. - Can use
super
keywords in wild card method:public void computeList(List<? super Number> source, List<? extends Number> destination)
- Generic Type Method:
-
-
Requires a relationship between the types of the parameters if they share the same type parameter.
-
Example:
public <T extends Number> void computeList1(List<T> source, List<T> destination)
- Both
source
anddestination
must beList
of the same typeT
. - Cannot use
super
keyword for type parameter bounds (e.g.,<T super Number>
is illegal). But we cannot do this in generic type method. - Generic type can have more type:
public <K,V,T,Z> void computeList1(List<T> source, List<T> destination)
- From here we now the difference between generic type method and wild card method is that we can have different type.
- Examples:
- Both
import java.util.List;
import java.util.ArrayList;
class Number {} // Simplified for example
class Integer extends Number {}
class Float extends Number {}
class Print {
//wild card method
public void computeList(List<? extends Number> source, List<? extends Number> destination){
// ...
}
//generic type method
public <T extends Number> void computeList1(List<T> source, List<T> destination){
// ...
}
}
public class Main {
public static void main(String args[]) {
Print printObj = new Print();
List<Integer> wildCardIntegerSourceList = new ArrayList<>();
List<Float> wildCardIntegerDesitanationList = new ArrayList<>(); // Corrected spelling from OCR
printObj.computeList(wildCardIntegerSourceList, wildCardIntegerDesitanationList);
// This would cause an error for computeList1 if T is bound to be the same for both
// printObj.computeList1(wildCardIntegerSourceList, wildCardIntegerDesitanationList);
// Correct usage for computeList1 would be:
// printObj.computeList1(wildCardIntegerSourceList, wildCardIntegerSourceList);
}
}
8. Type Erasure
Generic Class Erasure
-
Usage:
-
The process by which the Java compiler translates generic code into non-generic Java bytecode.
-
Points to Note:
-
//Byte code
-
//T will be replaced by object
- For an unbounded generic class
Print<T>
,T
is replaced byObject
. - Examples:
// Original Generic Class (Unbounded)
public class Print <T> {
T value;
public void setValue(T val) {
this.value = val;
}
}
// After Type Erasure (Bytecode equivalent)
/*
public class Print {
Object value;
public void setValue(Object val) {
this.value = val;
}
}
*/
Generic Class Bound Type Erasure
-
Usage:
-
For bounded generic classes, the type parameter is replaced by its first bound.
-
Points to Note:
-
For
Print<T extends Number>
,T
is replaced byNumber
. - Examples:
// Original Generic Class (Bounded)
public class Print<T extends Number> {
T value;
public void setValue(T val) {
this.value = val;
}
}
// After Type Erasure (Bytecode equivalent)
/*
public class Print {
Number value;
public void setValue(Number val) {
this.value = val;
}
}
*/
Generic Method Erasure
-
Usage:
-
For generic methods, type parameters are replaced by
Object
if unbounded. -
Points to Note:
-
public <T> void setValue(T val)
becomespublic void setValue(Object val)
. - Examples:
// Original Class with Generic Method (Unbounded)
public class Print {
public <T> void setValue(T val) {
System.out.println("do something");
}
}
// After Type Erasure (Bytecode equivalent)
/*
public class Print {
public void setValue(Object val) {
System.out.println("do something");
}
}
*/
Generic Bound Type Method Erasure
-
Usage:
-
For bounded generic methods, type parameters are replaced by their first bound.
-
Points to Note:
-
public <T extends Bus> void setValue(T val)
becomespublic void setValue(Bus val)
. - You create generic class, but the type code internally remove those.
- Examples:
class Bus {} // Assumed
// Original Class with Generic Method (Bounded)
public class Print {
public <T extends Bus> void setValue(T val) {
System.out.println("do something");
}
}
// After Type Erasure (Bytecode equivalent)
/*
public class Print {
public void setValue(Bus val) {
System.out.println("do something");
}
}
*/