Wednesday, June 30, 2021

Java Tutorial: Generics

Chapters

Java Generics

Generics were introduced in JDK5 and allow object types to be a parameter type to methods, class and interface. Generic types are different from method parameter-list.

Generics ensure type safety. During runtime, object types can be problematic due to their dynamic nature. For example, objects can be casted into their relative objects. Sometimes, object casting can be messy and error prone.

By using generics, we can reduce type errors during runtime and detect some of those errors at compile-time. Also, using generics may eliminate the use of explicit typecasting.

To declare generic types in a method,class or interface; We will use the diamond operator"<>" and put generic parameter names in there. Java has naming convention for generic parameter names:
  • E - Element (mostly used in Java Collections Framework)
  • K - Key
  • N - Number
  • T - Type
  • V - Value
  • S,U,V etc. - 2nd, 3rd, 4th types
Example: class ClassA<T>{ //codes }

If methods have parameter and argument types, then generics have type parameters and arguments.

Type arguments are actual types that can be passed to parameter types and they're used to create parameterized type. Parameterized type is a declaration of generic types with type arguments.
e.g. ClassA<String> classA;

Type parameters are used to declare parameter names or type variables in methods,class or interfaces.
e.g. class ClassA<T>{ //codes }

Generic Methods

We can make a method generic by putting the diamond operator before the method return type. We still need to follow method overloading and overriding rules if we want to overload or override generic methods. The scope of the declared parameter types is limited to the method block.
public class SampleClass{
  
  //Diamond operator with one parameter type
  <T> void displayMessage(T message){
    System.out.println(message);
  }
  
  //Diamond operator with multiple parameter types
  //parameter types must be separated by comma
  <T,U> void displayMessage(T message, U greetings){
    Greetings greet = null;
    
    //We're going to use explicit cast here for now.
    //We can use bounded type parameter here to
    //replace this explicit
    //We learn bounded type parameter later
    //later
    if(greetings instanceof Greetings)
      greet = (Greetings)greetings;
    else throw new NullPointerException("greet is null");
    
    System.out.println(greet.getGreetings());
    System.out.println(message);
  }
  
  //method with generic return type
  <T> T getMessage(T message){
    return message;
  }
  
  public static void main(String[]args){
    
    SampleClass sc = new SampleClass();
    
    //calling generic method with type witness.
    //type witness is a type argument specified
    //in a generic method/constructor call
    sc.<String>displayMessage("Hello Everyone!");
    
    //calling generic method using type inference which
    //we will discuss later.
    sc.displayMessage("Have a good day!",
                      new Greetings("Hi!"));
    String msg = new Greetings("Hey!").getGreetings();
    
    if(msg instanceof String)
      msg = sc.getMessage(msg);
      
    System.out.println("The message is: " + msg);
  }
}

class Greetings{
  private String greet;
  
  public Greetings(String greet){
    this.greet = greet;
  }
  
  public String getGreetings(){
    return greet;
  }
}
First off, we declare generic type parameters by using the diamond operator then, we can use those type parameters as type in method's parameter-list or as method's return type.

The generic types declaration in the example above is called unbounded type. During type erasure, the type of these unbounded type parameters is going to be the The Object class which is the parent class of all classes. We will discuss type erasure later.

We also used type inference in the example above. To put it simply, type inference is a way of omitting type arguments. We will discuss type inference later.

Generic Class and Interface

Class or interface can be generic too by putting the diamond operator next to the class or interface name. The scope of the declared parameter types is limited to the class or interface block.
public class SampleClass{
  
  public static void main(String[]args){
    
    //type arguments or parameterized type
    //when declaring a generic class
    Message<String> msg = new Message<String>();
    
    //Omitting the type arguments at the right side using type
    //inference.
    MyFileOp<String,Integer> mfo = new MyFileOp<>(1);
    
    //Error: "a" is not an Integer. This is one of the type safety
    //features of generics.
    //MyFileOp<String,Integer> mfo = new MyFileOp<>("a");
    
    msg.displayMessage(mfo.checkFileName("Filename"));
    msg.displayMessage(mfo.checkFilePath("C:\\test\\Filename.txt"));
    msg.displayMessage(String.valueOf(mfo.getFileID()));
  }
}

//generic interface
interface FileOp<T>{

  T checkFileName(T fileName);
  T checkFilePath(T filePath);
  
}

//generic class that inherited a generic interface
class MyFileOp<T,U> implements FileOp<T>{
  //Generic variable
  private U fileId;
  
  MyFileOp(U fileId){
    this.fileId = fileId;
  }
  
  @Override
  public T checkFileName(T fileName){
    System.out.println("Checking filename...");
    System.out.println("File name verified!");
    return fileName;
  }
  
  @Override
  public T checkFilePath(T filePath){
    System.out.println("Checking filepath...");
    System.out.println("File path verified!");
    return filePath;
  }
  
  U getFileID(){
    return fileId;
  }
}

//generic class without inheritance
class Message<T>{

  void displayMessage(T message){
    System.out.println("Message: " + message);
  }
}
In the example above, once the parameter types are declared by using the diamond operator in the class or interface, we can use those parameter types as types of instance or local variables, return types of class' methods, etc.

If a generic class inherits another generic class or interface then, we should redeclare the superclass parameter types in the subclass definition like we did in FileOp interface and MyFileOp class in the example above.

In the example above, MyFileOp class inherits FileOp interface then, FileOp type parameter is redeclared in MyFileOp as type T of MyFileOp. So, type T of MyFileOp<T,U> is also the type parameter of FileOp<T>. This process is called subtyping.

Generic Constructor

We can declare a constructor as generic by using the diamond operator before the constructor name. The scope of the declared type parameters is limited to constructor block.
public class SampleClass{
  private Object obj1,obj2;
  
  <T> SampleClass(T obj1,T obj2){
    this.obj1 = obj1;
    this.obj2 = obj2;
  }
  
  boolean compareObjects(){
    
    if(obj1 == obj2)
      return true;
      
    return false;
  }
  
  public static void main(String[]args){
    
    String str1 = new String("My String");
    String str2 = str1;
    
    //generic constructor call with type witness.
    //type witness is a type argument specified
    //in a generic method/constructor call
    //SampleClass sc = new <String>SampleClass(str1,str2);
    
    //type inference in generic constructor call
    SampleClass sc = new SampleClass(str1,str2);
    
    if(sc.compareObjects())
      System.out.println("objects are the same");
    else
      System.out.println("objects are not the same");
  }
}
Type Arguments in Generics Inheritance

We can use type arguments in class, method and constructor definition to specify the type of their type parameters if a subclass(generic or not) inherits them. Constructors can't be inherited but can be called by subclass using the super() keyword.
public class SampleClass{

  public static void main(String[]args){
  
    ClassB<String> b = new ClassB<>(
                             "ClassA","ClassB",1);
    System.out.println("ClassB Variable");
    
    //This is valid since the parameters of
    //compreString() in ClassB behave like Object
    //So, Object or any type that has Object
    //as parent class is valid as arguments 
    //in this method
    System.out.println(b.compareString(1,2));
    System.out.println();
    
    //Invalid upcast 'cause ClassB<T>
    //superclass is ClassA<String>
    //ClassA<Integer> a = b;
    
    //valid upcast
    ClassA<String> a = b;
    System.out.println("ClassA Variable");
    
    //This is valid 'cause the parameter type
    //of this method in ClassA is unbounded
    //type parameters. The base type of
    //unbounded type parameters is Object type
    //So, Object or any type that has Object
    //as parent class is valid as arguments 
    //in this method
    //Note: We will discuss base type in type
    //erasure topic
    System.out.println(a.compareString(2,2));
  }
}

abstract class ClassA<T>{
  private T nameA;
  
  <S>ClassA(T nameA, S num){
    this.nameA = nameA;
    System.out.println("num: " + num.toString());
  }
  
  <U>boolean compareString(U o1, U o2){
    System.out.println("o1: " + o1.toString());
    System.out.println("o2: " + o2.toString());
    
    return o1.toString().equals(o2.toString());
  }
  
  void displayName(){
    System.out.println(nameA);
  }
}

//Type argument being used as a specific type to "T"
//of ClassA.
class ClassB<U> extends ClassA<String>{
  private U nameB;
  
  ClassB(String nameA, U nameB, Integer num){
    //"T" in ClassA is specified as String in ClassB
    //"T" in ClassA is used as type of the first 
    //parameter of ClassA's constructor. So, the
    //first argument in the argument list should be
    //a String. Try changing the type of nameA and
    //the compiler will throw a compile-time error
    //
    //Then, "S" in ClassA's constructor is specified as
    //Integer in ClassB
    //"S" in ClassA's constructor is used as type of the
    //constructor's second parameter
    //So, the second argument in the argument list should
    //be an Integer. Try changing the type(except for int
    //primitive 'cause it will be automatically wrapped
    //into Integer) of num and the compiler will throw a
    //compile-time error
    <Integer>super(nameA,num);
    this.nameB = nameB;
  }
  
  //"U" in compareString() in ClassA is specified as String in
  //ClassB
  //"U" in ClassA is used as type of the paremeter-list of
  //compareString() in ClassA. So, the parameter-list of the
  //overriding method should be String. Try changing the type
  //of the parameter-list of the overriding class and the 
  //compiler will throw a compile-time error.
  //
  //Though, The parameters here still behave as Object type
  //which is the base type of unbounded type parameter.
  @Override
  <String> boolean compareString(String o1, String o2){
    //Can't use concat() even o1 is a String type 'cause
    //the parameters in this method still behave as Object
    //type
    //String str = o1.concat(o2);
  
    System.out.println("o1: " + o1.toString());
    System.out.println("o2: " + o2.toString());
    
    return o1.toString().equals(o2.toString());
  }
  
  @Override
  void displayName(){
    System.out.println(nameB);
  }
}
Note: Better use type arguments in method,class and constructor definition during inheritance. Using type arguments in a parent class without children, parent class constructor and overriden method doesn't make sense.
//This syntax is valid but the use of 
//type arguments here doesn't make sense
class ClassA<String>{
  Object obj;
}

Type Inference

Type inference is the ability of the compiler to determine the corresponding type argument declaration in a statement. We already encountered type inference in previous topics. For example, We can omit the type arguments in the instantiation statement and in the method call.
public class SampleClass{
  
  static <T> void displayMessage(T message){
    System.out.println("This is the message: " + message);
  }
  
  <T>SampleClass(T message){
    System.out.println(message);
  }
  
  public static void main(String[]args){
    //valid statement but can be shorten
    //ClassA<String> a = new ClassA<String>();
    
    //type inference in instantiation statement
    //This type inference may not work on java versions
    //prior to java SE7
    ClassA<String> a = new ClassA<>();
    
    //valid generic method call but can be shorten
    //SampleClass.<String>displayMessage("Message!");
    
    //type inference in generic method call
    SampleClass.displayMessage("Message!");
    
    //valid generic constructor call
    //new <String>SampleClass("Hey!");
    
    //type inference in generic constructor call
    new SampleClass("Hey!");
    
  }
}

class ClassA<T>{ /*codes*/ }
Unlike in the type inference of generic method and constructor call, we shouldn't remove the diamond operator in the type inference of the instantiation statement.
public class SampleClass{

  public static void main(String[]args){
  
    //this will throw a warning
    ClassA<String> a = new ClassA();
  }
}

class ClassA<T>{ /*codes*/ }
Type Inference of Generic Class and Constructor

We know that we can declare type parameters to methods,constructors and classes. What if a generic class has generic class in its block? Let's take a look at this example.
public class SampleClass{

  public static void main(String[]args){
    
    //valid type inference
    ClassA<Integer> a = new ClassA<>("Hey!",1);
    
    //This call will throw a warning related to
    //raw types
    //new ClassA("Hey!",1);
    
    //valid type inference.The parameters in ClassA's
    //constructor use unbounded generic types. The
    //base type of unbounded type parameters is Object.
    //So, any type that has Object as superclass
    //is a valid argument
    //new ClassA<>("Hey!",1);
    
    //valid type inference. Generic class doesn't
    //use type inference here but the generic
    //constructor does. 
    //new ClassA<Integer>("Hey!",1);
    
    //invalid type inference. Can't use type inference in
    //generic class because generic constuctor doesn't
    //use type inference
    //new <String>ClassA<>("Hey",1);
    
    //valid type inference.
    //new <String>ClassA<Integer>("Hey",1);
  }
}

//generic class
class ClassA<T>{
  
  //generic constructor
  <U>ClassA(U message,T num){
    System.out.println(message + " " + num);
  }
  
  /*
  //generic constructor/method
  //in a generic class can have
  //the same type parameter name
  //but it will make our generic 
  //code confusing and restrictive
  <T>ClassA(T message,Integer num){
    System.out.println(message + " " + num);
  }
  */
  
}
Target Typing in Type Inference

Target type is a type in an expression that is expected by the compiler. In typecasting(Objects) we use target typing to explicitly cast one object to another. e.g. String str = (String)obj1;
The type "(String)" is the target type.

In generics, we can use target typing to assign a generic return type of a method to a generic variable.
public class SampleClass{
  
  public static void main(String[]args){
    
    ClassA<String> a1 = new ClassA<>("ClassA");
    ClassA<String> a2 = new ClassA<>("ClasssA");
    
    //Try changing a1 and a2 to this
    //ClassA<Integer> a1 = new ClassA<>(1);
    //ClassA<Integer> a2 = new ClassA<>(2);
    
    System.out.println("Is a2 name longer than a1?");
    
    //The "<String>" in "ClassA<String>" is
    //the target type. The target type infers that 
    //compareNameLength() "<S>" type should be
    //<String> type. Try changing the type
    //arguments of a1 and a2 to Integer and this code
    //will throw a compile time error similar to this:
    //Inference variable S has incompatible equality
    //constraints
    ClassA<String> a3 = a1.compareNameLength(a2);
    
    if(a3 != null)
      System.out.println("Yes!");
    else System.out.println("No!");
  }
}

//Singleton Generic Class
class ClassA<T>{
  private T name;
  private ClassA<T> instance;
  
  ClassA(T name){
    this.name = name;
  }
  
  //<S> before ClassA<S> return type
  //is the declaration of type parameter
  //ClassA<S> is the return type
  //ClassA<S> o1 is the parameter type
  <S>ClassA<S> compareNameLength(ClassA<S> o1){
    if(o1.getName().toString().length() > 
       name.toString().length()) return o1;
    else return null;
  }
  
  T getName(){
    return name;
  }
  
}

Raw Types

Raw type is a class or interface type that doesn't declare type arguments. For new applications, raw types should be avoided. One of The reasons why raw types can still be used today is due to the backward compatibility with legacy codes prior to JDK 5.0. Lots of API classes prior to JDK 5.0 are non-generic.
Note: Class or interface type that is not generic is not a raw type. In other words, the type of class or interface that doesn't have type parameter is not a raw type.
public class SampleClass{

  public static void main(String[]args){
    //Declaring generic ClassA with
    //type arguments
    //ClassA<String> a1 = new ClassA<>("ClassA");
    
    //This declaration will throw a warning
    //regarding raw types 'cause we
    //we're trying to assign a raw type to
    //a generic variable
    //
    //The warning is similar to this:
    //Note: SampleClass.java uses unchecked or
    //unsafe operations
    //Note: Recompile with -Xlint:unchecked
    //for details
    ClassA<String> a2 = new ClassA("ClassA");
    
    //This however, won't throw a warning due to
    //backward compatibility with legacy code.
    //We can legally mix pre-generics(raw types)
    //and generics(parameterized type) code this way.
    //ClassA a2 = new ClassA<String>("ClassA");
    
    //This will also throw a warning regarding
    //raw types
    //InterfaceA<String> ia = new ClassB();
    
    //This declaration is also valid but not
    //recommended 'cause we're instantiating
    //a raw type of ClassA<T>
    //The type "T" in ClassA will be the
    //base type of unbounded type parameter
    //which is the Object type.
    //ClassA a3 = new ClassA();
    
  }
}

class ClassA<T>{
  private T name;
  
  ClassA(T name){
    this.name = name;
  }
  
  T getName(){
    return name;
  }
}

interface InterfaceA<T>{
}

class ClassB<T> implements InterfaceA<T>{
}
Note: "Unchecked or unsafe operations" in the compiler warning states that raw types may cause problematic type problems because compiler doesn't have enough information to perform type safety. One of the problematic problems that we're going to get when using raw types is the ClassCastException.
public class SampleClass{

  public static void main(String[]args){
  
    ClassA<String> a1 = new ClassA(10);
    //ClassCastException
    String str = a1.name;
  }
}

class ClassA{
  T name;

  CkassA(T name){
    this.name = name;
  }
}
The example above will compile just fine. However, when we run the compiled code, java will throw a run-time exception which is the ClassCastException.

If you're using a command prompt when compiling your java code then, you can type this command to reveal the warning that I mentioned earlier. This command assumes that the SampleClass.java is not in a package and the command prompt is pointing at the folder where SampleClass.java is located
javac SampleClass.java -Xlint:unchecked

Bounded Type Parameters

We can narrow down types that can be passed on type paramaters by creating a bounds by writing the type parameter, followed by extends keyword, followed by the upper bound.
Note: The use of extends keyword in this context is used to mean either "extends"(classes) or "implements"(interfaces)
public class SampleClass{

  public static void main(String[]args){
    
    //Integer is a subclass of Number so,
    //the type argument here is valid
    ArithmeticOperations<Integer> ao =
    new ArithmeticOperations<>();
    
    System.out.println(ao.addInt(2,3));
    System.out.println(ao.addDouble(2,3));
    
    //This won't work 'cause String is not
    //Number and not a subclass of Number
    //ArithmeticOperations<String> ao =
    //new ArithmeticOperations<>();
  }
}

//The type parameter "T" of this class has an
//upper bound which is the Number class. We
//use the extends keyword to bound "T" to
//Number class.
//If "T" is bounded to Number class then,
//"T" type parameter can only accept Number
//and its subclasses as type arguments
class ArithmeticOperations<T extends Number>{
  
  //intValue() returns int which is automatically
  //wrapped into Integer if int is assigned to
  //an Integer
  Integer addInt(T num1, T num2){
    return num1.intValue() + num2.intValue();
  }
  
  //doubleValue() returns double which is automatically
  //wrapped into Double if double is assigned to
  //a Double
  Double addDouble(T num1, T num2){
    return num1.doubleValue() + num2.doubleValue();
  }
}
In the example above, we see that we can use new methods like intValue() and doubleValue(). That's because the base type of "T" is now the Number type not the Object type anymore. In previous topics, we can only use methods provided by the The Object class like the toString() method.

In this example,we can use methods that the Number class provided like intValue(), doubleValue(), etc.

Multiple Bounds

A type variable in this bounds is a subclass of all bounds that are listed in the diamond operator separated by "&".
public class SampleClass{

  public static void main(String[]args){
    
    //Valid type argument 'cause ClassD
    //is a subclass of ClassC,InterfaceA
    //and InterfaceB
    ClassE<ClassD> e1 = new ClassE<>(new ClassD());
    
    //Invalid type argument 'cause ClassA is a
    //subclass of InterfaceA but not a subclass
    //of InterfaceB and ClassC
    //ClassE<ClassA> e2 = new ClassE<>(new ClassA());
    
    //Invalid type argument 'cause specified bounds are
    //exclusive
    //ClassE<ClassC> e2 = new ClassE<>(new ClassC());
  }
}

interface InterfaceA{
}

class ClassA implements InterfaceA{
}

interface InterfaceB{
}

class ClassB implements InterfaceB{
}

class ClassC{
}

class ClassD extends ClassC implements InterfaceA,
                                       InterfaceB{
}

//When implementing this kind of bounds, class must
//come first on the list before interfaces. Try putting
//ClassC in the last entry of the list and the compiler
//will throw a compiler-time error
//class ClassE<T extends InterfaceA & InterfaceB & ClassC>{/**/}
class ClassE<T extends ClassC & InterfaceA & InterfaceB>{
  T instance;
  
  ClassE(T instance){
    this.instance = instance;
  }
}
Using comma(,) to create multiple bounds is legal. However, the usage of this kind of multiple bounds doesn't make any sense.
public class SampleClass{

  public static void main(String[]args){
    
    //First off, we are required to put
    //three type arguments here to match
    //the type parameters in ClassE
    //
    //second, we can change the second and
    //third type arguments even those
    //replacements are not related to the
    //type parameters
    //
    //The only important part here is the
    //first argument. Change the first
    //type argument and the compiler will
    //throw a compile-time error
    //
    //The compiler won't throw a compile-time
    //error if we change the second and third
    //type arguments
    //
    //If we change the object type that is passed
    //to constructor to the type of second and
    //third type arguments then, the compiler
    //will throw a compile-time exception.
    //
    //The compiler won't throw a compile-time
    //error if we pass an object with a type
    //of the first argument or subclasses of
    //the first argument
    ClassE<ClassC,String,Integer> e =
    new ClassE<>(new ClassC());
  }
}

interface InterfaceA{
}

class ClassA implements InterfaceA{
}

interface InterfaceB{
}

class ClassB implements InterfaceB{
}

class ClassC{
}

class ClassD extends ClassC implements InterfaceA,
                                       InterfaceB{
}

//These type of multiple bounds is legal but 
//it doesn't make sense
class ClassE<T extends ClassC,InterfaceA,InterfaceB>{
  T instance;
  
  ClassE(T instance){
    this.instance = instance;
  }
}

//In my opinion, This multiple bounds in this example is simply
//equal to a bounded type parameter
/*
class ClassE<T extends ClassC>{
  T instance;
  
  ClassE(T instance){
    this.instance = instance;
  }
}
*/
Generic Wildcard

In generics, the "?" symbol is called the wildcard. This wildcard represents an unknown type and it can be used as a generic parameter, field, local variable and a return type(Though, it's a good practice to have specific return type). We never use the wildcard as a type argument for generic method call, generic class instantiation or type parameter of generic class.

Unbounded Wildcard

We can use unbounded wildcard if we want a type that doesn't rely on declared type parameter. Let's take a look at this example.
public class SampleClass{
  
  //No need to declare a paremeter type 'cause
  //we're using unbounded wildcard
  static void displayString(ClassA<?> obj){
    System.out.println(obj.toString());
  }
  
  /*
  The method above is similar to this
  method. Remember, the base type of 
  unbounded wildcard is Object
  static void displayString(ClassA<Object> obj){
    System.out.println(obj.toString());
  }
  */
  
  public static void main(String[]args){
    
    SampleClass.displayString(new ClassA<String>("String"));
    SampleClass.displayString(new ClassA<Integer>(200));
  }
}

class ClassA<T>{
  private T name;
  
  ClassA(T name){
    this.name = name;
  }
  
  @Override
  public String toString(){
    return name.toString();
  }
}
So, what's the difference betweenClassA<?> and ClassA<Object>?
ClassA<?> accepts any type that is a subclass of Object whereas ClassA<Object> only accepts Object instance. Take a look at this example.
public class SampleClass{
  
  static void displayString(ClassA<Object> obj){
    System.out.println(obj.toString());
  }
  
  public static void main(String[]args){
    
    //valid argument
    SampleClass.displayString(new ClassA<Object>( new Object() ));
    //invalid argument
    //SampleClass.displayString(new ClassA<Integer>(200));
  }
}

class ClassA<T>{
  private T name;
  
  ClassA(T name){
    this.name = name;
  }
  
  @Override
  public String toString(){
    return name.toString();
  }
}
Upper Bounded Wildcard

Just like type parameter, we can set bounds to the wildcard. To set an upper bound, use the "?" symbol, followed by the extends keyword, followed by the upper bound.
Upper bound restricts the unknown type to be the upper bound or the subclasses of the upper bound.
Note: The use of extends keyword in this context is used to mean either "extends"(classes) or "implements"(interfaces)
import java.util.List;
import java.util.Arrays;
public class SampleClass{

  static String concat(List<? extends CharSequence> lt){
    
    String str = "";
    for(CharSequence sq : lt)
      str = str.concat(sq.toString() + " ");
      
    return str;
  }
  
  public static void main(String[]args){
    List<String> lt = Arrays.asList("Java","Tutorial:","Generics");
    System.out.println("Combined Strings: " + SampleClass.concat(lt));
  }
}
In the example above, we use CharSequence interface as the upper bound. Thus, the parameter "lt" will only accept CharSequence and subclasses of CharSequence.

Note that we can apply upper bound to a type-parameter.
Example:
public static <T extends String> String message(T msg1, T msg2);

Lower Bounded Wildcard

To set a lower bound for the wildcard, we use the wildcard symbol(?), followed by the super keyword, followed by the lower bound. The lower bound could be a class or interface. Lower bound restricts the unknown type to be the lower bound or the superclasses of the lower bound.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  static void displayString(List<? super String> lt){
    
    for(Object o : lt)
      System.out.println(o.toString());
  }
  
  public static void main(String[]args){
    List<CharSequence> lt = Arrays.asList("Lower","Bounded",
                                                "Wildcard");
    SampleClass.displayString(lt);
  }
}
In the example above, we use String class as the lower bound. Thus, the parameter "lt" will only accept String and superclasses of String.

Wildcard Capture and Helper Methods

Sometimes, during evaluation, a particular type of a wildcard is inferred by the compiler. This phenomenon is called the wildcard capture. Let's take a look at this example.
import java.util.List;
public class SampleClass{
  
  static void swapNumber(List<? extends Number> lt,
                         int i1,int i2){
    //try changing o1 to String and you will receive a
    //compile-time error similar to this: CAP#1 can't be
    //converted to String
    //CAP#1 is the type of the wildcard that is inferred
    //String o1 = lt.get(i1);
    
    Number o1 = lt.get(i1);
    Number o2 = lt.get(i2);
    
    //Error: Number cannot be converted to CAP#1
    //....
    //Where CAP#1 is a fresh type-variable
    //CAP#1 extends Number from a capture of ? extends Number
    lt.set(i1,o2);
    lt.set(i2,o1);
    
  }
  
  public static void main(String[]args){
  }
}
In the example above, we can't use the set() method of lt 'cause of type error. Seems like the compiler is unsure about the type that we're putting in the set() method. It's possible to fix the error in the example by using type inference by creating a private helper method which captures the wildcard.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  static void swapNumber(List<? extends Number> lt,
                         int i1,int i2){
    
    //We use type inference here to capture 
    //CAP#1 and pass it on "T"
    swapNumberHelper(lt,i1,i2);
  }
  
  //We can also copy the bounds in swapNumber().
  //Though, a plain "T" is enough
  //private static <T extends Number>void
  //swapNumberHelper(List<T> lt,int i1,int i2){ /**/ }
  
  //Helper method
  private static <T>void swapNumberHelper(
                               List<T> lt,
                               int i1,int i2){
    T o1 = lt.get(i1);
    T o2 = lt.get(i2);
    lt.set(i1,o2);
    lt.set(i2,o1); 
  } 
  
  public static void main(String[]args){
    List<Integer> lt = Arrays.asList(2,4);
    
    System.out.println("First Element: " + lt.get(0));
    System.out.println("Last Element: " + lt.get(1));
    System.out.println("Swapping...");
    SampleClass.swapNumber(lt,0,1);
    System.out.println("First Element: " + lt.get(0));
    System.out.println("Last Element: " + lt.get(1));
  }
}
Not all wildcard capture errors can be fixed. Take a look at this example.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  static void swapNumber(List<? extends Number> lt1,
                         List<? extends Number> lt2,
                         int i1, int i2){
   swapNumberHelper(lt1,lt2,i1,i2);             

  }
  
  private static <T>void swapNumberHelper(
                               List<T> lt1,
                               List<T> lt2,
                               int i1, int i2){
    T o1 = lt1.get(i1);
    T o2 = lt2.get(i2);
    
    lt1.set(i1,o2);
    lt2.set(i2,o1);
  }
  
  public static void main(String[]args){
    List<Integer> lt1 = Arrays.asList(2,4);
	List<Float> lt2 = Array.asList(2.5f,3.4f);
	SampleClass.swapNumber(lt1,lt2,0,1);
  }
}
The wildcard capture error above can't be fixed because there are two wildcard capture types which are CAP#1 and CAP#2. No matter what we do in this example, we can't swap Objects between two lists with different wildcard capture types.

Sometimes, Wilcard capture error may happen due to the presumption of a type of the wildcard.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  static void displayString(List<? extends CharSequence> lt){
    
    //we can't presume that the type of lt is
    //a String even we specify String type
    //argument on the list in main()
    //this is unsafe
    for(String s : lt)
      System.out.println(s);
      
    //Use this instead since the upper
    //bound of lt is CharSequence
    //This presumption is safe
    //for(CharSequence s : lt)
    //  System.out.println(s.toString());
  }
  
  public static void main(String[]args){
    List<String> lt = Arrays.asList("A","B","C");
    SampleClass.displayString(lt);
  }
}
Generic Class/Interface in Type Parameter/Argument

A type parameter can have generic class or interface as types.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  //Generic class in type parameter. We use
  //type argument to specify the generic type
  //of ClassA. It means, "T" declared in 
  //getInstance() only accepts ClassA<String>
  static <T extends ClassA<String>>
  void getInstance(List<T> a){
    for(ClassA ca : a)
      System.out.println(ca.toString());
  }
  
  //Generic class in type parameter. "T"
  //before the extends is a parameter type
  //with Comparable<T> as upper bound.
  //It means, the parameter type only accepts
  //classes/interfaces that implements/extends
  //the Comparable<T> interface
  static <T extends Comparable<T>>
  void compare(T o1, T o2){
    if(o1.compareTo(o2) ==
       o2.compareTo(o1))
    System.out.println(o1.toString() + 
                       " is equal to " +
                       o2.toString());
    else
    System.out.println(o1.toString() + 
                       " is not equal to " +
                       o2.toString());
  }
  
  public static void main(String[]args){
    //Generic class as type argument
    List<ClassA<String>> lt =
    Arrays.asList(new ClassA<>("a1"),
                  new ClassA<>("a2"),
                  new ClassA<>("a3"));
    SampleClass.getInstance(lt);
    
    //Integer and String implement Comparable<T>
    //That' why they're valid arguments
    SampleClass.compare("abcd","ABCD");
    SampleClass.compare(33,100);
  }
}

class ClassA<T>{
  private T name;
  
  ClassA(T name){
    this.name = name;
  }
  
  @Override
  public String toString(){
    return name.toString();
  }
}
Generics Subtyping

In non-generic types, we can assign a type to another type if the types are compatible. However, this doesn't apply to generic types.
public class SampleClass{

  public static void main(String[]args){
    //valid
    CharSequence sc = new String("String");
    
    ClassA<CharSequence> a1 = null;
    ClassA<String> a2 = new ClassA<>();
    //invalid 'cause ClassA<CharSequence>
    //is not related to ClassA<String>
    a1 = a2;
  }
}

class ClassA<T>{
}
We can subtype generic classes or interfaces by using inheritance. However, we shouldn't forget to include type parameter declaration of parent class in a subclass.
public class SampleClass{

  public static void main(String[]args){
    
    //valid: ClassB<String> is a
    //subclass of InterfaceA<String>
    InterfaceA<String> ia = new ClassB<>();
    
    //valid: ClassB is a subclass of ClassA
    ClassA<Integer> a1 = new ClassB<>();
  }
}

interface InterfaceA<T>{
}
//implements interface
class ClassA<T> implements InterfaceA<T>{
}

//extends generic class
class ClassB<U> extends ClassA<U>{
}
If we don't include type parameter declaration of parent class in a subclass during inheritance, then the compiler will throw an "unchecked conversion" warning everytime we instantiate that subclass. "Unchecked conversion" will appear if we assign a raw type to a generic type.
public class SampleClass{

  public static void main(String[]args){
    
    //This will throw an "unchecked conversion" warning
    InterfaceA<String> ia = new ClassB<>();
    
    //This will throw an "unchecked conversion" warning
    //ClassA<Integer> a1 = new ClassB<>();
  }
}

interface InterfaceA<T>{
}
//implements interface
class ClassA<T> implements InterfaceA<T>{
}

//extends generic class. Type parameter of ClassA is
//omitted
class ClassB<U> extends ClassA{
}
Because we omitted the type parameter of ClassA in ClassB, ClassB inherited the raw type version of ClassA. So, ClassB is considered as raw type because it inherited the raw type version of ClassA.

If we want to relate a generic type to another generic type then, we will use the generic wildcard("?")
public class SampleClass{

  public static void main(String[]args){
    
    //This type inference is invalid if used with
    //wildcard(?)
    //ClassA<?> a1 = new ClassA<>("a1");
    
    ClassA<String> a1 = new ClassA<String>("a1");
    ClassA<Integer> a2 = new ClassA<Integer>(100);
    //Subtyping using wildcard(?)
    ClassA<?> a3 = a1;
    System.out.println(a3.getString());
    a3 = a2;
    System.out.println(a3.getString());
  }
}

class ClassA<T>{
  private T string;
  
  ClassA(T string){
    this.string = string;
  }
  
  String getString(){
    return string.toString();
  }
}
Type Erasure

Type erasure is the process of removing type information related to generics and other generic elements to ensure that no new classes are created for parameterized types. Because there are no new classes are created, generics incur no runtime overhead.
These are the Type Erasure procedures according to The Java™ Tutorials
  • Replace all type parameters in generic types with their bounds or Object if the type parameters are unbounded. The produced bytecode, therefore, contains only ordinary classes, interfaces, and methods.

  • Insert type casts if necessary to preserve type safety.

  • Generate bridge methods to preserve polymorphism in extended generic types.(This is not applicable in java11 and above)

Let's create an example to demonstrate the first and second procedures of type erasure. This is our sample code structure before erasure
public class SampleClass{
  
  static void displayName(ClassA<?> a){
    System.out.println(a.getName());
  }
  
  static <T>boolean compareNames(T n1, T n2){
    return n1.toString().equals(n2.toString());
  }
  
  public static void main(String[]args){
    
    ClassA<String> a1 = new ClassA<>("a1");
    ClassA<String> a2 = new ClassA<>("a2");
    String str = a1.getName();
    System.out.println(str);
    System.out.println(SampleClass.compareNames(a1,a2));
    SampleClass.displayName(a2);
  }
}

class ClassA<T extends CharSequence>{
  private T name;
  
  ClassA(T name){
    this.name = name;
  }
   
  T getName(){ return name; }
  
  void setName(T name){
    this.name = name;
  }
}
This is our sample code structure after type erasure.
public class SampleClass{
  
  //wildcard has been removed
  static void displayName(ClassA a){
    System.out.println(a.getName());
  }
  
  //Type parameter has been removed
  static boolean compareNames(Object n1, Object n2){
    return n1.toString().equals(n2.toString());
  }
  
  public static void main(String[]args){
    
    //Diamond operators and type arguments have been
    //removed
    ClassA a1 = new ClassA("a1");
    ClassA a2 = new ClassA("a2");
    
    //Implicit addition of explicit cast
    String str = (String)a1.getName();
    
    System.out.println(str);
    System.out.println(SampleClass.compareNames(a1,a2));
    SampleClass.displayName(a2);
  }
}

//Type parameter has been removed
class ClassA{
  private CharSequence name;
  
  ClassA(CharSequence name){
    this.name = name;
  }
   
  CharSequence getName(){ return name; }
  
  void setName(CharSequence name){
    this.name = name;
  }
}
In the example above, the diamond operators with type parameter,arguments,wildcard and blank diamond operators have been removed. Then, the types of instance variables, local variables and return types that use type parameters are going to be the first bound or I would to call it "base type", if you will.

If a type parameter is unbounded, then the first bound of that type parameter is the Object class. Otherwise, the first bound of that type parameter is the upper bound.

Now we know that generics only exist at compile time. Once all type checks are complete and the compilation process is done, generics will be removed.

Even we know what the type "T" is going to be after erasure. We still can't assign an actual type to "T".
public class SampleClass{

  public static void main(String[]args){
  }
}

class ClassA<T>{
  private T name;
  //error
  //private T num = 10;
  //error
  //private T obj = new Object();
  Object o = null;
  
  ClassA(T name){
    //valid
    this.name = name;
    //We can assign T to its
    //first bound or base type
    o = name;
  }
  
  T getName(){
    //error
    //return name.toString();
    return name;
  }

}
Even we know that the "T" in the example above is going to be Object type, we still can't assign actual types to "T" even those types can be implicitly upcasted or explicitly downcasted.

Bridge Method

Note: Starting java11, bridge methods are not generated anymore. If you're using java version prior to java11 then this topic may still be helpful.

Bridge method links nested classes/interfaces, Thus making nested classes have unrestricted access to their public and non-public members. In this topic, bridge method is created to link a parameterized parent class/interface to its subclass. Bridge method is one of the procedures of type erasure.
public class SampleClass{

  public static void main(String[]args){
    ClassA a1 = new ClassB(2);
	try{ a1.setObj("Name2"); }
	catch(Exception e){
		e.printStackTrace();
	}
	
  }
}

class ClassA<T>{
  private T obj;
  
  ClassA(T obj){
    this.obj = obj;
  }
  
  void setObj(T obj){
    System.out.println("setObj() in ClassA");
    this.obj = obj;
  }
}

class ClassB extends ClassA<Integer>{

  ClassB(Integer obj){
    super(obj);
  }
  
  @Override
  void setObj(Integer obj){
	super.setObj(obj);
  }
}
The code will compile but we will get a ClassCastException. Let's apply type erasure to our code above to see the bridge method.
public class SampleClass{

  public static void main(String[]args){
    ClassA a1 = new ClassB(2);
	try{ a1.setObj("Name2"); }
	catch(Exception e){
		e.printStackTrace();
	}
	
  }
}

class ClassA{
  private Object obj;
  
  ClassA(Object obj){
    this.obj = obj;
  }
  
  public void setObj(Object obj){
    System.out.println("setObj() in ClassA");
    this.obj = obj;
  }
}

class ClassB extends ClassA{

  ClassB(Integer obj){
    super(obj);
  }
  
  //This method is the bridge method.
  //This method is compiler-generated
  //Note: Starting java11, synthetic
  //method or bridge method is not
  //generated anymore
  public void setObj(Object obj){
    //This is the code that throws
    //ClassCastException
    //String can't be converted to
    //Integer
    setObj((Integer)obj);
  }
  
  //This overriding process
  //is a failure because setObj()
  //in this class and setObj()
  //in ClassA have differrent
  //method signatures
  //
  //Although, In newer versions of
  //java, this overriding method
  //won't throw a compilation error
  @Override
  public void setObj(Integer obj){
	super.setObj(obj);
  }
}
Reifiable and Non-Reifiable Types

Refiable types are types, which type information is available at run time. These types are raw types, non-generic types, primitives and types that use unbounded wildcard.

Non-Reifiable types are types, which type information is not completely available at run time due to type erasure. Generic types, except for unbounded wildcard, are non-reifiable types.
For example, List<String> and List<Integer>; at runtime, these types are indistinguishable from each other because, when the generic information of these types is removed, both of them will be a normal List object.

You might ask: Why unbounded wildcard<?> is reifiable?
<?> is reifiable 'cause no type information will be removed if <?> is removed due to type erasure.
Take a look at this example.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  //Before erasure
  <T>void meth1(T type){
  }
  
  //After erasure
  //"T" is gone we can't use "T"
  //type information at runtime
  /*
    void meth1(Object type){
  }
  */
  
  //Before erasure
  void meth2(List<? extends Number> lt){
    
  }
  
  //After erasure
  //<? extends Number> is gone
  //we can't use the upper bound information
  //at runtime
  /*
  void meth2(List lt){
    
  }
  */
  
  //Before erasure
  void meth3(List<?> lt){
    
  }
  
  //After erasure
  //<?> is gone
  //Though, <?> doesn't
  //have type information
  //that may be usable at runtime
  //So, this method is pretty much
  //unaffected by erasure
  /*
  void meth3(List lt){
    
  }
  */
  
  public static void main(String[]args){
    //Before erasure
    List<Integer> lt = Arrays.asList(1,2);
    
    //After erasure
    //<Integer> is gone 
    //and can't be used at runtime
    //List lt = Arrays.asList(1,2);
  }
}


Heap Pollution

Heap pollution is a situation where a variable with parameterized type refers to an object that doesn't have that parameterized type. Heap pollution occurs in some situations like mixing raw types and parameterized type, etc. Our program may throw unexpected exceptions due to heap pollution.

Variable Arguments(varargs) is a hotspot for heap pollution. When we use generic varargs, java will throw a warning regarding heap pollution.
public class SampleClass{
  
  static <T>void displayString(T... elem){
    for(Object o : elem)
       System.out.println(o.toString());
  }
  
  public static void main(String[]args){
  
    SampleClass.displayString("A",1,4.5f,"C");
  }
}
One of the reasons why varargs is prone to heap pollution is due to the fact that varargs is translated to arrays and arrays of parametertized types are not allowable in java; even we can use type parameter as type of arrays. This means that arrays elements can't be generic. That's why the compiler can't ensure type safety on arrays.

So, T... is equivalent to T[] and after type erasure, the array will be like this: Object[]

This example demonstrates an exception thrown using varargs.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  static void listArr(List<String>... lt){
     //valid 'cause Object is a superclass
     //of List. So, Object[] is compatible with
     //List[]
     Object[] obj = lt;
     
     obj[0] = Arrays.asList(new StringBuilder("Builder"));
     
     //This will throw a ClassCastException, since lt[]
     //reference is assigned to obj[] and obj[] first element
     //is changed to StringBuilder.
     String s = lt[0].get(0); 
  }
  
  public static void main(String[]args){
    SampleClass.listArr(Arrays.asList("String"),
                        Arrays.asList("Builder"));
  }
}
If you're confident that the the generic varargs that you're using is safe to use then, we can suppress the warning regarding generic varargs. To suppress the warning, we will use the @SafeVarargs or @SuppressedWarnings annotation.
public class SampleClass{
  
  //We can use SuppressedWarnings but
  //@SafeVarargs is more preferrable 
  //when suppressing varargs related
  //warning
  //@SuppressWarnings({"unchecked", "varargs"})
  @SafeVarargs
  static <T>void displayString(T... elem){
    for(Object o : elem)
       System.out.println(o.toString());
  }
  
  public static void main(String[]args){
  
    SampleClass.displayString("A",1,4.5f,"C");
  }
}
Generics Restrictions

If we want to use java generics effectively, we need to know some of its restrictions.

1.) Primitive types like int,char, etc. can't be used as type arguments.
import java.util.List;
import java.util.Arrays;
public class SampleClass{
  
  public static void main(String[]args){
    //invalid
    List<int> lt = Arrays.asList(1,2,3);
    //valid
    //List<Integer> lt = Arrays.asList(1,2,3);
  }
}
2.) Type parameters can't be instantiated.
public class SampleClass{
  
  static <T>void displayName(T name){
    //invalid
    T localName = new T();
    
  }
  
  public static void main(String[]args){
  }
}
3.) Static fields whose types are type parameters can't be declared. Type parameters declared in class/interface definition are non-static in order to prevent conflict between type parameters from different classes.
//This sample code won't compile but
//let's assume that this code is valid
public class SampleClass<T>{
  //This invalid in real life
  static T name;
  
  public static void main(String[]args){
    //since name variable is static. It's being shared
    //to every instance of SampleClass. If we instantiate
    //SampleClass multiple times then, the type of name
    //variable is going to be ambiguious
    //In this example, we don't know if the type of the name
    //variable is a String or Integer
    SampleClass<String> sc1 = new SampleClass<>();
    SampleClass<Integer> sc1 = new SampleClass<>();
  }
}
4.) Type parameters declared in class/interface can't be used in nested static blocks.
public class SampleClass<T>{
  //valid
  T tName;
  
  static class StaticClass<U>{
    //invalid
    //T staticName;
    //valid
    U uName;
    
    //invalid parameter
    /*
    static void meth(U param){
    }
    */
  }
  
  class MyClass{
    //valid
    T className;
  }
  
  //1st parameter is invalid
  /*
  static <S>void myMeth(T param1,S param2){
  }
  */
  
  //all parameters are valid
  <Y>void anotherMeth(T param1,Y param2){
  }
  
  public static void main(String[]args){
  }
}
5.)We can't use typecasting or instanceof with parameterized types.
import java.util.List;
import java.util.LinkedList;
import java.util.Arrays;
public class SampleClass{
  
  static <E>void displayList(List<E> lt){
    //invalid: instanceof can't be used with 
    //parameterized type
    /*
    if(lt instanceof LinkedList<String>)
      System.out.println("invalid");
    */
  }
  
  public static void main(String[]args){
    //List<String> lta = Arrays.asList("A","B");
    //Invalid: can't do upcast
    //List<CharSequence> ltb = lta;
    
    //This typecast is valid 'cause unbounded
    //wildcard is reifiable type
    List<?> lt1 = Arrays.asList("A","B");
    List<?> lt2 = lt1;
    
    //valid 'cause unbounded
    //wildcard is reifiable type
    if(lt2 instanceof List<?>)
      System.out.println("valid!");
      
    //In some cases, the compiler can cast parameterized
    //type 'cause the compiler knows that an argument type
    //is valid to be casted to that argument type
    List<String> lt3 = new LinkedList<>();
    LinkedList<String> linkedList = 
    (LinkedList<String>) lt3;
  }
}
6.)We can't create arrays of parameterized types.
import java.util.List;
import java.util.ArrayList;
public class SampleClass{

  public static void main(String[]args){
    //invalid: can't create arrays of 
    //parameterized types
    //List<String>[] lt1 = new ArrayList<>[2];
	
    //valid but with raw type warning
    List<String>[] lt2 = new ArrayList[2];
	
    //valid since unbounded wildcard is reifiable
    List<?>[] lt3 = new ArrayList<?>[2];
	
    //Let's pretend that array of parameterized
    //type is valid. If that's the case, then
    //our generic algorithm will be more prone
    //to ArrayStoreException
    /*
    //pretend this is a valid syntax
    Object[] lt = new List<String>[2]; 
    //ok
    lt[0] = new ArrayList<String>(); 
    //ArrayStoreException should be thrown
    //but it can't be detected 'cause
    //the compiler thinks this is valid
    //'cause Object is a superclass of
    //ArrayList
    lt[1] = new ArrayList<Integer>(); 
    */
  }
}
7.) Generic class can't extend Throwable and any subclasses of Throwable. Also, we can't use type parameter as type in catch block and throw keyword. However, we can use type parameter next to throws keyword.
public class SampleClass{
  
  /*
  static <T,U extends Exception>void castType(T type){
    try{
      String s = (String)type;
    }
    //error
    catch(U e){
    }
  }
  */
  
  //valid: type variable can be used next to throws keyword
  <T,U extends RuntimeException> void meth() throws U{
    throw new RuntimeException("Test!");
    //invalid
    //throw new U("Error");
  }
  
  public static void main(String[]args){
  }
}


//error
/*
class ClassA<T> extends Throwable{
}
*/

//error
/*
class ClassB<T> extends RuntimeException{
}
*/
8.)We can't overload generic methods with equal method signature after erasure.
import java.util.List;
public class SampleClass{
  
  //Invalid generic method overloading
  //Before erasure
  void meth(List<String> lt){
  }
  
  void meth(List<Integer> lt){
  }
  
  After Erasure
  /*
  void meth(List lt){
  }
  
  void meth(List lt){
  }
  */
  
  public static void main(String[]args){
  }
}

No comments:

Post a Comment