All about Generics: Part 1

Generics are used to parameterize types. A piece of code can be written generically enough so that we can fit a variety of types in it.  Being a strongly type-safe language, Java requires to specify types in all aspects of its code, methods, fields, interfaces, etc.

However, generics is one of the tricky ways to make it possible to use type parameterization so that one particular portion or entirely of a class or their subclass can support a variety of types if an entity such as class, interface, or method that operates on a parameterized type is said to be a generic entity.

Benefits of Generics

Type safety

Using generics helps us define a type to consume in and produce from, where it is used. This means we can be sure of adding a specific type of data in a Collection type for example (List) and get the exact same type. This saves us from getting ClassCastException and from lots of development pain.

Type erasure

Type erasure means, all the additional information included while using Java Generics, is removed from bytecode while generated. In bytecode, it will be old java syntax prior to Java 5.

Alternatively, we can say, we will be enforced to use a type in compile-time, but the types are discarded at runtime.

Where we can use Generics

  • Using Generics with wildcards
    • Unbounded wildcards
    • Bounded wildcards
      • Upper bounded wildcards
      • Lower bounded wildcards
  • Arrays with Generic types
  • Using Generic types as parameters of a class or an interface.
  • Using Generic types with method or constructor definition.

Components of Generics

  1. <> – Diamond operator – we put a generic type inside it.
  2. T – known as a Generic type. We can actually use any character or word instead of T here. usually used as
  3. ? – known as a wildcard. We use it like
  4. extends – Used to set Upper Bound of Generic type. This means we can not use any parent class of this upper-bounded class.
  5. super – Used to set Lower Bound of Generic type. We cannot use any child class of this lower bounded class.

Let’s start our today’s discussion to explain using Generics with wildcards.

Using Generics with wildcards

We have talked about the wildcard above. Wildcard represents an unknown type. The followings are examples of wildcard parameterized generics.

Collection
List
Comparator 

Wildcard can be used in the following situations:

  • To define the type for a generic containing parameter
void addAll(List objects) {}
  • To define the type for a generic containing field or local variable
Set numList = Set.of(3, 100_000_000_000_000L, 2.5F, 2.7);
  • To define an unknown type for a generic containing return type. But it is better to make the type-specific.
ResponseEntity getAll() { 
 //
 // code goes here
 //
}

Unbounded wildcard parameterized type

A generic type that does not contain any boundary of the upper or lower limit only contains wildcard as type.

ArrayList  list = new ArrayList();  
//or
ArrayList  list = new ArrayList();  
//or
ArrayList  list = new ArrayList(); 

Bounded wildcard parameterized type

Bounded wildcards have 2 types – upper-bounded and lower bounded. These bounds put some restrictions on the range of classes that can be used as generic types. We usually achieve the upper bound limit by extends and the lower bound limit by super keyword.

Upper bounded wildcard

Let’s assume, we have a method, that takes a parameter of List. The list items can have different instances of a class, that has multiple child classes. Now, we don’t know, at which point of time which child class instance list will be passed through the method parameter. In such a case, we can simply set the upper-bounded wildcard.

List ints = Arrays.asList(1,2,3,4,5); 
System.out.println(sum(ints)); 

// and the sum method 

private static Number sum (List numbers){ 
    double s = 0.0; 
    for (Number n : numbers) 
        s += n.doubleValue(); 
    return s; 
}
Lower bounded wildcard

When we want to set a lower limit of a class hierarchy, and we don’t want to add any instance of any class below to that hierarchy, we use super keyword to make this lower bounded wildcard. This applies for a method parameter.

To demonstrate, let us create 3 classes – Fruit, Apple and AsianApple

class Fruit {
    @Override
    public String toString() {
        return "Any fruit!!";
    }
}

class Apple extends Fruit {
    @Override
    public String toString() {
        return "This is an Apple !!";
    }
}

class AsianApple extends Apple {
    @Override
    public String toString() {
        return "This is an AsianApple !!";
    }
}

We will also define a method that receives a List as a parameter.

public static void printApples(List apples)
{
    System.out.println(apples);
}

Now observe the following example. When we pass a list of AsianApple in the method printApples(…), it gives us compile-time error. But when we try to pass a list of Fruit , which is the super of Apple, it accepts it without any error.

The interesting thing to observe, we can add AsianApple to the 2nd list, and it is acceptable. But it is impossible to pass a list wholly of an unsupported type.

List basket1 = new ArrayList<>();
basket1.add(new AsianApple()); 
printApples(basket1); // Compilation error 

List basket2 = new ArrayList<>(); 
basket2.add(new AsianApple()); 
basket2.add(new Apple()); 
basket2.add(new Fruit()); 
printApples(basket2);

Now, let’s see some limitations and exceptional cases of extends and super.

Limitations and exceptional cases
  • We can create a list like List and instantiate the list. But when we try to add elements in it, it will give us compilation error.
Set appleSet = new HashSet<>(); 
appleSet.add(new Apple()); // Compilation error 
appleSet.add(new AsianApple()); // Compilation error

The reason is that we actually don’t need to mention extends If we define a list of Apple, it simply can accept any child of Apple type.

  • However, we can define something like the following when we define it in one go.
List basket = List.of(new Apple(), new AsianApple());
    
    // Because we are populating all objects first, 
    // then assigning the list in the left side object.
    // So the above code satisfies the below valid condition
    
    List basket = new ArrayList();

    List basket1 = List.of(new Apple(), new AsianApple(), new Fruit(), new Object(), 123, 12.45);

The reason is that when we define with List.of(…) it actually accepts objects and produces a list of Object. And List is a type of List.

  • Notice the following example:
List basket = new ArrayList<>(); 
basket.add(new Apple()); //Successful 
basket.add(new AsianApple()); //Successful 
// basket.add(new Fruit()); //Compile time error 
// basket.add(new Object()); //Compile time error

The interesting fact here is, we were supposed to be able to add any supertype of Apple to the list, but it seems to happen the opposite. When we try to add items in a list demonstrated above, it only accepts the children of the supertype.

PECS [Producer extends, Consumer super]

  • If we want to produce items, e.g.: iterate over a list or get items from a list, we use extends.

The reason is that we cannot know which subtype or Fruit our collection will hold. So it is safe to get the object from the list and store it in a Fruit type.

  • If we want to add items to our collection, we use super.

The reason is that we don’t care what is already stored in the collection. As long as we are adding a Fruit and its subtype in the collection, the compiler doesn’t complain.

Where we cannot use Generic types

  • With static fields
public class GenericsExample { 
    private static T member; //This is not allowed 
}
  • We cannot instantiate any generic type
public class GenericsExample { 
    public GenericsExample(){ 
        new T(); // Not allowed here
    } 
}
  • We cannot refer to primitive types with Generic type
final List ids = new ArrayList<>(); //Not allowed 
final List ids = new ArrayList<>(); //Allowed
  • We cannot create Generic exception classes
// causes compiler error 
public class GenericException extends Exception {}

That’s all for this article. In our next article, we will discuss on Arrays with GenericsUsing Generic type as parameter of a class and Using Generic types with method or constructor definition.

Thank you for your patience and taking time to read this long article

The post All about Generics: Part 1 appeared first on foojay.