Sunday, August 27, 2023

Generics in Java

Rio:  Guys, today Mr Archie would be telling us about the concept of Generics in Java. I am sure this lecture is going to clear all your doubts around Generics. You may come up with your queries as we move along. You have the floor now, Mr Archie.

Mr Archie: Thank you, Rio. So guys, most of you must have already heard about the term Generics in Java.


Rio: Yes, Mr Archie.


Mr Archie: Good, so have you ever wondered where do we use Generics in Java?


Audience is silent.


Mr Archie: Generics help us in implementing generic code which can work with different data types. They help us in type checking of the java objects at the compile time rather than facing any casting related issues at run time. Let me try to explain this using an example here.

Suppose you want to create a class to store and print a string. so you would be doing something like this and you can create an object of this class and call print method as below.
class StringPrinter {
private String toPrint;
StringPrinter(String toPrint){
this.toPrint = toPrint;
}
public void print(){
System.out.println(toPrint);
}
}
public class Generics {
public static void main(String[] args) {
StringPrinter printer = new StringPrinter("Anshul");
printer.print();
}
}

Suppose later at some point of time you need to create another class which you should be able to use to print an integer. You can create another class almost like the above one

public class IntegerPrinter {
private Integer toPrint;
IntegerPrinter(Integer toPrint) {
this.toPrint = toPrint;
}
public void print() {
System.out.println(toPrint);
}
}
public class Generics {
public static void main(String[] args) {
StringPrinter stringPrinter = new StringPrinter("Anshul");
stringPrinter.print();
IntegerPrinter integerPrinter = new IntegerPrinter(12);
integerPrinter.print();
}
}

If you take a look carefully, the code is almost duplicate for the two classes viz. StringPrinter and IntegerPrinter.


This duplicity can easily be removed using Generics. We can create a parameterised class as below

public class Printer<T> {
T toPrint;
Printer(T toPrint){
this.toPrint = toPrint;
}
public void print(){
System.out.println(toPrint);
}
}
public class Generics {
public static void main(String[] args) {
Printer<String> stringPrinter = new Printer<>("Anshul");
stringPrinter.print();
Printer<Integer> integerPrinter = new Printer<>(12);
integerPrinter.print();
} 

Jamie (among audience): Wao thats cool!.

Mr Archie: Yes its, indeed, a cool feature provided by Java. Not only classes we can also create generic methods using Generics.
public class Printer<T> {
T toPrint;
Printer(T toPrint){
this.toPrint = toPrint;
}
public void print(){
System.out.println(toPrint);
}
public T get(){
return toPrint;
}
}

If you would notice carefully you would see that the method get is returning a generic Type and we can call this method to get the respective datatypes

public class Generics {
public static void main(String[] args) {
Printer<String> stringPrinter = new Printer<>("Anshul");
String str = stringPrinter.get();
System.out.println(str);
Printer<Integer> integerPrinter = new Printer<>(12);
Integer integer = integerPrinter.get();
System.out.println(integer);
}
}
Rio: Can we use primitive data types in generics?
Mr Archie: Generics work with Reference Types only. You can not use primitive data types.

Rio: Thanks!

Jamie: Mr Archie, I had once heard about bounded and Unbounded Generic Types. Can you please explain those concepts, as well, here?

Mr Archie: Yes Jamie, I was about to explain those. In the above example the Type parameter can take any argument. So this will be called an unbounded generic type. In all we can categorise generics into 2 different types
  • Unbounded Generic Types: The unbounded generic types can take any known reference type <T> or a wildcard <?> if the type is unknown. 
  • Bounded Generic Types: In case, for known reference types, if you want to create a boundary of  the types that will be  accepted by the generic class/method. we can use bounded generic types.
Rio: So you mean, if we want to restrict the types that will be accepted by generic class/methods we can do that? So how is that possible?

Mr Archie: Let me explain you in better way. Till this time, all the examples, I gave above, were using unbounded or simple generic type where in we were not limiting the types that could be used with the Generic class. We could use any reference type with the above generic class e.g. String, Integer, Animal, Airplane, Student, any. There may come scenarios where you might need to restrict your generic class to accept only limited reference types. Bounded Generic Types help us to achieve this requirement. They are further of 2 types
  • Upper Bounded
  • Lower Bounded
Jamie: Examples Mr Jamie, please?

Mr Archie: Suppose you want to make your generic class accept only type T and its subtypes, you will go for Upper Bounded Generic Type. Let me explain it using the above example only.

The generic class Printer above, can accept any reference type, even say an Animal class, if exists.
public class Animal {
private String name;
Animal(String name) {
this.name = name;
}
@Override
public String toString() {
return "Animal{" +
"name='" + name + '\'' +
'}';
}
}
public class Generics {
public static void main(String[] args) {
Printer<String> stringPrinter = new Printer<>("Anshul");
String str = stringPrinter.get();
System.out.println(str);
Printer<Integer> integerPrinter = new Printer<>(12);
Integer integer = integerPrinter.get();
System.out.println(integer);
Printer<Animal> aPrinter = new Printer<>(new Animal("Cat"));
System.out.println(aPrinter);
}

}

If we want this Printer class to accept Number or its subtypes only, we can use Upper Bounded Generic via extends keyword as below
public class Printer<T extends Number> {
T toPrint;
Printer(T toPrint){
this.toPrint = toPrint;
}
public void print(){
System.out.println(toPrint);
}
public T get(){
return toPrint;
}

public String toString(){
return toPrint.toString();
}
After doing so, the Printer class would not be able to accept either String or Animal type, and you will get compile time error saying "Type parameter is not within bounded" while trying to do so.

and similarly if we want this Printer class to accept Animals or its subtypes only, we can change it to
public class Printer<T extends Animal> {
T toPrint;
Printer(T toPrint){
this.toPrint = toPrint;
}
public void print(){
System.out.println(toPrint);
}
public T get(){
return toPrint;
}
public String toString(){
return toPrint.toString();
}
}
Lower Bounded Generic Type parameter is, however, not supported at class level but rather at method level only. This is because wildcard ? can not be used at class level. It makes no sense using it at the class level. Let me try to explain the above statements.

If we could have, anyhow, been able to create a class Printer with ? type parameter, we would have been in, no way, able to determine the type to be used for declaring instance variable for that or using them inside any method. With classes you need some identifier unlike that with methods where you don't. This is quite confusing for now. I will try to cover this topic in some other lecture. For now just remember that we can not use super keyword in type parameter while declaring a generic class.
Rio: Sure, no worries. We will take a note of this.

Mr Archie: Now for using super keyword in case of lower bounded generics, let me try to give an example here. Suppose we have two different lists, one of Number type and another of integer type. We want to create a generic method that works on both of them, if we try to do this way
public class Generics {
public static void main(String[] args) {
List<Number> listNumber =Arrays.asList(1, 2);
List<Integer> listInteger =Arrays.asList(1, 2);
List<Object> listObject =Arrays.asList(1, 2);
print(listNumber);
print(listInteger);
print(listObject);
}
private static void print(List<Integer> listNumber) {

}
private static void print(List<Number> listNumber) {

}
private static void print(List<Object> listNumber) {

}
}
We will get a compile time error saying "both methods have same erasure" as Integer already extends Number class in java. How can we write generic method that supports both Integer and its super classes. We can in this case use lower bounded generics. This required generic method can be written as
public class Generics {
public static void main(String[] args) {
List<Number> listNumber =Arrays.asList(1, 2);
List<Integer> listInteger =Arrays.asList(1, 2);
List<Object> listObject =Arrays.asList(1, 2);
print(listNumber);
print(listInteger);
print(listObject);
}
private static void print(List<? super Integer> listNumber) {
listNumber.stream().forEach(System.out::println);
}
}
Rio: Mr Archie, so how can we determine where we should use upper bound generics and where we should go for lower bound generics?

Mr Archie: A very simple get-put rule if we can to get something out, go for Upper Bound and if we want to set something in, go for Lower Bound.

One more topic I think can be covered here today, is about variance.

Rio: Oh still more left in there. Please go ahead, quite interesting.

Mr Archie: Variance is the assignment compatibility between generic classes and methods.

Lets take an example, we have an array of animals and a list of animals. We have two methods one with Object[] as parameter and another with List<Object> as parameter.
public class Generics {
public static void main(String[] args) {
Animal[] arrayAnimal = new Animal[10];
arrayAnimal[0] = new Animal("monkey");
arrayAnimal[1] = new Cat("cat");
arrayAnimal[2] = new Dog("dog");
List<Animal> listAnimal = new ArrayList<>();
listAnimal.add(new Animal("monkey"));
listAnimal.add(new Cat("cat"));
listAnimal.add(new Dog("dog"));
}
private static void doStuff(Object[] objects) {
System.out.println(objects);
}
private static void doStuff(List<Object> objects) {
System.out.println(objects);
}
}
If we try to pass the arrayAnimal to method accepting Object[] this is acceptable but if we try to pass listAnimal to method accepting List<Object>, we get compile time error. This is because arrays of animal is subtype of array of object but list of animal is not a subtype of list of object. This is called invariant method.

To fix this issue, we can change the method to use wildcard ? to allow list of any type and this will make the method bi-variant.
public class Generics {
public static void main(String[] args) {
List<Animal> listAnimal = new ArrayList<>();
listAnimal.add(new Animal("monkey"));
listAnimal.add(new Cat("cat"));
listAnimal.add(new Dog("dog"));
doStuff(listAnimal);
}
private static void doStuff(List<?> objects) {
System.out.println(objects);
}
}
Another method to fix this issue, we can change the method a bit to allow objects of  type T (Animal in this case) and objects of subtypes of T to make it covariant. This is nothing but using concept of upper bound generics.
public class Generics {
public static void main(String[] args) {
Animal[] arrayAnimal = new Animal[10];
arrayAnimal[0] = new Animal("monkey");
arrayAnimal[1] = new Cat("cat");
arrayAnimal[2] = new Dog("dog");
List<Animal> listAnimal = new ArrayList<>();
listAnimal.add(new Animal("monkey"));
listAnimal.add(new Cat("cat"));
listAnimal.add(new Dog("dog"));
doStuff(arrayAnimal);
doStuff(listAnimal);
}
private static void doStuff(Object[] objects) {
System.out.println(objects);
}
private static void doStuff(List<? extends Animal> objects) {
System.out.println(objects);
}
}
Similarly for understanding contra-variance, lets take an example of scenario used above
public class Generics {
public static void main(String[] args) {
List<Number> listNumber =Arrays.asList(1, 2);
List<Integer> listInteger =Arrays.asList(1, 2);
List<Object> listObject =Arrays.asList(1, 2);
print(listNumber);
print(listInteger);
print(listObject);
}
private static void print(List<? super Integer> listNumber) {
listNumber.stream().forEach(System.out::println);
}
Here the print method has bound the input parameters to be of type T (Integer in this case) and its superclasses viz. Number as well as Object to make it contra-variant.

Rio: Wao, very nicely explained!

Mr Archie: One more thing that just came into my mind is Type Erasure Process. This is in-fact the process by which a compiler replaces all the generic parameters in generic class with actual ones. Compiler follows these rules in this process
  • For bounded type parameters, bounded types are inserted.
  • For unbounded type parameters, Object class is inserted.
  • To preserve type safety, type casts are introduced t
  • To preserve polymorphism in extended generic type classes, bridge methods are generated.
Thats all for today guys. We will meet soon again to discuss some other interesting topic. Thank you everyone.

Rio: Thanks, Mr Archie for this wonderful session today. This indeed helped us in understanding the concept of Generics in Java. See you soon in another session.


No comments:

Post a Comment

SpringBoot Application Event Listeners

When a spring boot application starts few events occurs in below order ApplicationStartingEvent ApplicationEnvironmentPreparedEvent Applicat...