OCP Exam Series III – Generics

Introduction

Generics enable types (classes and interfaces) to be parameters when defining classes, interfaces, and methods. It enables you to re-use the same code. In this section, we will cover Generic Classes, Generic Interfaces, Generic Methods, and Bounded Type Parameters.

Type Parameter Naming Conventions

By convention, type parameter names are single, uppercase letters. The most commonly used type parameter names are:

E - Element (used extensively by the Java Collections Framework)
K - Key
N - Number
T - Type
V - Value
S,U,V etc. - 2nd, 3rd, 4th types

Generic Classes

Generic classes are very useful if you need to use different classes as the type parameter. You can create a generic class with a generic data type T. For example, the Food class has a generic type variable declared after the name of the class. It means that you can create Food instances using different data types

public class Food<T> {
    private T ingredients;    
    public T getIngredients(){
        return ingredients;
    }
    public void setIngredients(T ingredients) {
        this.ingredients = ingredients;
    }
}

Let’s create a food instance with String data type

Food<String> food = new Food<>();
food.setIngredients("protein: 200 gr; carb: 300, fat: 75");

Maybe you changed your mind and decided to create another class that holds the ingredients

class Ingredients {
   private String name;
   private int calories;
   Ingredients(String name, int calories) {
       this.name = name;
       this.calories = calories;
   }
}

Then, you can create a food instance using Ingredients data type

    Ingredients ingredients = new Ingredients("protein", 200);
    Food<Ingredients> ingredientsFood = new Food<>();
    ingredientsFood.setIngredients(ingredients);

Let’s look at the next example. Why don’t we create a food instance with the Map data type? Don’t worry if you don’t understand the syntax. We will cover the Map interface in the next section

    Food<Map<String, Integer>> foodAsMap = new Food<>();
    Map<String, Integer> foods = new HashMap<String,Integer>();
    foods.put("protein", 200);
    foods.put("carb", 300);
    foods.put("fat", 75);
    foodAsMap.setIngredients(foods);

Generic classes can have two type parameters as well. In this example, we defined FoodMultipleType generic class that has two parameters. K for a map key, V for a map value

    public class FoodMultipleType<K, V> implements FoodPair<K, V> {
        private K name;
        private V calories;
    
        public FoodMultipleType(K name, V value) {
            this.name = name;
            this.calories = value;
        }
    
        @Override
        public K getName() { return name; }
    
        @Override
        public V getCalories() { return calories; }
    }

To use this class, we can implement the following:

     FoodPair<String, Integer> tuna = new FoodMultipleType<String, Integer>("tuna", 205);
     FoodPair<String, Integer> brownRice = new FoodMultipleType<String, Integer>("brown rice", 214);

Generic Interfaces

An interface can also declare a formal type parameter. For example, the List interface uses a generic type.

    public interface List<E> extends Collection<E> {
        int size();
        boolean isEmpty();
        boolean contains(Object var1);
        ...
    }

You can also define your own interface. Let’s create a Diet interface. There are three approaches to implement the Diet interface.

    public interface Diet<T> {
        void printDietList(T t);
    }

Approach 1: Specify the generic type in the class

The following concrete class says that it deals only with LowCarbHighProtein class

    class LowCarbDiet implements Diet<LowCarbHighProtein> {
        @Override
        public void printDietList(LowCarbHighProtein t) {
            //do something with t
        }
    }

Approach 2: Create a generic class

The following concrete class allows the caller to specify the type of the generic

class LowCarbAbstractDiet<U> implements Diet<U> {
    @Override
    public void printDietList(U u) { // do something}
}

Approach 3: Not use generics at all

This is the old way of writing code. It generates a compiler warning about LowCarbDietSimple being a raw type, but it compiles

    class LowCarbDietSimple implements Diet {
        @Override
        public void printDietList(Object o) {}
    }

Generic Methods

Generic methods are methods that introduce their own type parameters. The following generic method compares the two FoodMultipleType objects

    public static <K, V> boolean compare(FoodMultipleType<K, V> p1, FoodMultipleType<K, V> p2) {
        return p1.getName().equals(p2.getName()) &amp;&amp;
                p1.getCalories().equals(p2.getCalories());
    } 

Let’s see how we use the compare method

FoodPair<String, Integer> tuna = new FoodMultipleType<String,Integer>("tuna",205);
FoodMultipleType<String, Integer> brownRice = new FoodMultipleType<String,Integer>("brown rice",214);
Util.<String,Integer>compare((FoodMultipleType<String, Integer>) tuna,brownRice);

Bounded Type Parameters

We sometimes need to restrict the type arguments for the methods. Bounded type parameters serve for that purpose.

Unbounded

The unbounded wildcard type is specified using the wildcard character (?), for example, List<?>. This is called a list of unknown type. This method prints a list of any type.

    private static void unboud(List<?> list) {
        for (Object elem : list)
            System.out.print(elem + " ");
        System.out.println();
    }

Lower Bound

A lower bounded wildcard is expressed using List<? super ClassName>, such as List<? super Integer>. This method works on lists of Integer and the supertypes of Integer, such as Integer, Number, and Object

    private static void lowerBound(List<? super Integer> list) {
        for (int i = 0; i < 10 ; i++) {
            list.add(i);
        }
        System.out.println(list);
    }

Upper Bound

An upper bounded wildcard is specified using List<? extends ClassName>, List<? extends Number>. This method works on lists of Number and the subtypes of Number, such as Integer, Double, and Float.

     private static double upperBound(List<? extends Number> list) {
         double s = 0.0;
         for (Number n : list)
             s += n.doubleValue();
         return s;
     }

Let’s look at how these methods are called. Note that we can pass Integer and Double list to the upperBound method. On the other hand, lowerBound method only accepts Integer and its super classes. The undound method doesn’t have a restriction.

     List<Integer> intList = Arrays.<Integer>asList(1, 2, 3);
     System.out.println(upperBound(intList)); // Output: 6.0

     List<Double> doubleList = Arrays.<Double>asList(4.3, 7.1, 2.4);
     System.out.println(upperBound(doubleList)); // Output: 13.799999999999999

     lowerBound(intList); // Output: 0 1 2
     //lowerBound(doubleList); // compile error

     unboud(intList); // Output: 1 2 3
     unboud(doubleList); // Output: 4.3 7.1 2.4 

Conclusion

In this section, we covered Generic Classes, Generic Interfaces, Generic Methods, and Bounded Type Parameters. You can find the source code on GitHub.

Leave a Reply

Your email address will not be published. Required fields are marked *