Generics


In previous sections, we briefly talked about data structures, as a quick review, data structures are objects that are designed to hold data. We will now attempt to implement a data structure object to begin this section. Let’s start with the basic ones - a Pair object. The following piece of code is my first attempt to implement a Pair that holds two Strings.

            public class Pair{
                private String key;
                private String value;
                public Pair(String key, String value){
                    this.key = key;
                    this.value = value;
                }
                public String getKey(){
                    return key;
                }
                public String getValue(){
                    return value;
                }
            }

        

This Pair object has two instance variables that binds two String object together as a pair and we have getters and setters toi retrieve the data, thus we have successfully accomplished our task of implementing a Pair object that holds two Strings.
What problem do we have with the above code? what if your client tell you that I want to have a Pair object that holds two int? now we have to change the above code to this

            public class Pair{
                private int key;
                private int value;

                public Pair(int key, int value){
                    this.key = key;
                    this.value = value;
                }
                public int getKey(){
                    return key;
                }
                public int getValue(){
                    return value;
                }
            }
      

The altered code has been bolded. However, this implementation does not solve the problem, because there can potentially be an infinite different type of objects. There is no way we can provide that many different version of “Pair” to satisfy all the clients. How do we solve this then? Recall in primary school, or middle school, we learned basic algebra.

Let us assume that the user want the Pair object to hold a certain data type call K. We will denote this generic type at the class declaration enclosed with angle brackets <>. Once we declare this generic type, we can use this “assumed” type throughout the class. Hence the Pair object can be modified into the following.

            public class Pair<K>{
                private K key;
                private K value;
                public Pair(K key, K value){
                    this.key = key;
                    this.value = value;
                }
                public K getKey(){
                    return key;
                }
                public K getValue(){
                    return value;
                }
            }

Note that this class uses a generic type as part of its definition and hence we call this class a Generic class. Similar to algebra, you can name your unknown type to whatever you want, it doesn’t even have to be a single character. As a convention, there are a few characters frequently used when defining a generic class. T for type, E for element, K for key and V for value. With this class defined, now we can use the Pair object in main method like the following.

            public static void main(String[] args){
                Pair<String> pair1 = new Pair<>();
                Pair<Integer> pair2 = new Pair<>();
                ...
            }
       

This Pair object is now compatible with any different data types. The question Are we done with Pair? Unfortunately not. What if now the client want to hold a pair of two different data type, say, a String and a Scanner, our current implementation fails to satisfy the requirements. Are we out of options? Fortuately not, recall that middle school, we learned to define two unknowns x and y, we can do the same thing with generics here.
Let us auume that the user want the Pair object to hold the first data type to be called K and the second data type to be called V. The Pair object is now modified to

                public class Pair<K, V>{
                    private K key;
                    private V value;
                    public Pair(K k, V v){
                        this.key = key;
                        this.value = value;
                    }
                    public K getKey(){
                        return key;
                    }
                    public V getValue(){
                        return value;
                    }
                }

            
To use this object our main method needs to be modified to the following
            public static void main(String[] args){
                Pair<String, Scanner> pair1 = new Pair<>();
                Pair<Integer, Random> pair2 = new Pair<>();
                ...
            }

As you can see now, a generic class is like a template that fits into any user defined datatype, the class itself is merely a “generic template”, hence called generics! Generics are usually considered one of the harder topics to understand in java, but now you can tell others that, generics are just a bunch of algebra learned in middle school!

Bounding a generic type

At some point, you might want to design a class that limits what data type the user can input. From our example above, our Pair object can hold any data types. Hence, we will introduce two keywords to bound a generic type, extend and super
. The extend keyword limits how general the generic type can be. For example, if I change the Pair class to the following

                public class Pair <K extends Number>{
                    private K key;
                    private K value;
                    public Pair(K key, K value){
                        this.key = key;
                        this.value = value;
                    }
                    public K getKey(){
                        return key;
                    }
                    public K getValue(){
                        return value;
                    }
                }

                
By putting extends in the generic type, this limits what the user can put as a datatype in the pair object. In the above example, we made the upper bound of the generic type to be Number, this means the user can only input datatypes that conforms to Number. In other words, the user are only allowed to input Number or subclasses of Number as the datatype, any other datatypes like String input by the user will cause a compile error. Let’s look at some examples:
                    public static void main(String[] args){
                        Pair<Integer> p1 = new Pair<>(); // works
                        Pair<Double> p2 = new Pair<>(); // works
                        Pair<Number> p3 = new Pair<>(); //works
                        Pair<String> p4 = new Pair<>(); // compile error
                    }

The super keyword has the total opposite effect, it bounds the lower bound a generic type. Note that the super keyword cannot be used directly to the generic type variable it can only be used on a wild card or the notation “?”. We will introduce wild card in the next section.

Generics as a parameter

So far, we’ve used generic at the class level, but generics can also be found in method parameters like this.

            public static void method1(ArrayList>Number< list) {
                //…
            }
        

We can see that from the method definition, the method is looking for an ArrayList with generic type of Number. The generic type specified prevents user from inputting anything other than an ArrayList of Number. For instance, the snippet below would not work.

                    ArrayList>String< al = new ArrayList<>();
                    method1(al); // this would cause a compile error

                

It would not work because there is a type conflict between Number and String. The above example demonstrates a situation when the code requires the generic type of the Arraylist to be a Number, what if we want to allow “any” types? That’s when wildcard comes in handy. Wild Cards Generic types are user defined; wild cards are used whenever we have a type that is not defined by the user but needs a placeholder. In the previous paragraph, the word “any” is bolded because whenever you think about “any generic type…” that’s when the wildcard comes into play. Wildcard is noted as “?” in java. To continue with our question posed above, what if we want to allow “any” type as the parameter? We will define the method signature as the follow

            public static void method1(ArrayList list) {
                //…
            }

        

The question mark denotes the wildcard, indicating that the generic type can be of any data type.

Continuing with using generics in parameter, what if we change the main method body to the following:

            ArrayList<Integer> al2 = new ArrayList<>();
            method1(al2); // would this work?

        

Would this work? We know from inheritance that Integers are a type of number, so it might seem like it will work without causing error. However, we have to remember that we are actually dealing with ArrayList at this moment. ArrayList<Number> is not a superclass of ArrayList<Integer>, hence inheritance does not work over here. Hence the above snippet of code would not work, even though Integer is a type of Number.
In order to make it work, we have to specify that the parameter is allowed to take in any generic type that is Number or a subclass of number. To achieve this, we need to utilize both wildcards and generic bounds. The method parameter will be changed to the following:

            public static void method1(ArrayList <? extends Number> list) {
                ...
            }