Java Generics - 7. Upper and Lower Bounds

This is part 7 of the 9 part series on Java Generics 



Prev Topic

Topics



Next Topic



Java Generics - Upper and Lower Bounds

This is going to be a really long blog, so please bear with me.

Declaring Generic Variables

You can also use generics to declare the variables. In other work you can choose which types can be assigned to a variable. The rules are more or less the same as for methods. You cannot define a type variable, like you could for the methods. Thus the declaration below makes no sense.


        // invalid
       private <E extends Fruit> E var1 = null;
      
       // invalid
       private <? extends Fruit> var2 = null;


It does make sense if the type E is declared at the class level. Thus the ones below are all a valid declarations.


  public class GenericVariables<E extends Fruit> {
      
       E var = null;
      
       public void foo(){
              E localVar = null;
       }
      
       public E get(){
              return var;
       }
  }


We have indirectly seen how variables are declared in the code above. It is very similar to the way we declare the method parameters. Note that the generics follow strict typing so a Bean<Fruit> cannot be used to refer a Bean<Mango> even though Mango is a Fruit, and Fruit can be used to refer Mango.


       // valid
       Bean<Mango> mBean1 = new Bean<>();
       Bean<Mango> mBean2 = new Bean<Mango>();
      
       // Mango cannot be initialized with Fruit and vice-versa
       // Type mismatch: cannot convert from Bean<Mango> to Bean<Fruit>          
       Bean<Fruit> mBean3 = new Bean<Mango>(); // Error
      
       // even though Fruit can be used to refer Mango       
       Fruit f = new Mango();     // valid

       // Error – this is invalid too
       Bean<Mango> mBean4 = new Bean<Fruit>();


If you wish to refer to any type of Bean you can use wildcard. If you wish to use any type of bean use the wildcard ‘?’ in the diamond – Bean<?>. If you want to refer any Mango, use Bean<? extends Mango> type of reference. Again since all Fruit cannot be Mango (they can be Grape too), the last declaration gives compiler error.


       Bean<?> wBean1 = new Bean<>();
       Bean<?> wBean2 = new Bean<Fruit>();
       Bean<?> wBean3 = new Bean<Mango>();
       Bean<?> wBean4 = new Bean<Alphonso>();
       Bean<?> wBean5 = new Bean<Integer>();
      
       Bean<? extends Mango> wmBean1 = new Bean<>();
       Bean<? extends Mango> wmBean2 = new Bean<Mango>();
       Bean<? extends Mango> wmBean3 = new Bean<Alphonso>();
       // compiler error
       // Type mismatch: cannot convert from Bean<Fruit> to Bean<? extends Mango>
       Bean<? extends Mango> wmBean4 = new Bean<Fruit>();


The right side of the decaration cannot have a wildcard. The programmers must be sure what type of objects they want to initialize. This is the type of problem generics were created to solve in the first place. Thus in the declaration below, the first one is valid, as anything which extends mango is afterall a mango. Compare it with the wmBean3 variable above. The second declaration is invalid because you are not specifying what would be the type of Bean which you are trying to initialize.


       // valid
       Bean<? extends Mango> wmBean5 = new Bean<Mango>();
       // Cannot instantiate the type Bean<? extends Mango>
       Bean<? extends Mango> wmBean6 = new Bean<? extends Mango>();
      

These would be clearer when we discuss type erasure section.

Upper and Lower Bounding

We encountered earlier the use of wildcard with extends - <? extends Fruit>. The variable having the Bean<? extends Mango> can refer any Bean which if type Mango (i.e. the class Mango and all classes extending Mango).


       Bean<? extends Mango> wmBean1 = new Bean<>();
       Bean<? extends Mango> wmBean2 = new Bean<Mango>();
       Bean<? extends Mango> wmBean3 = new Bean<Alphonso>();


This is an example of upper bounding in Java. If you initialize the bean with only Mango like Bean<Mango>, you can only reference it with a Mango. Attachning an Alphonso bean Bean<Alphonso> would result in error. In the code below, the third line would throw an error but not the last one. Staring at the code below will automatically make it clear.


       Bean<Mango> mb = new Bean<>();
       Bean<Alphonso> ab = new Bean<>();
       mb = ab; // error
             
       Bean<? extends Mango> emb = new Bean<>();      
       emb = ab; // all is well  


But wait !! This is not the only thing with upperbounding. Consider this, you actually cannot call the method ‘set’ in the Bean<E> class. You can do this with Java.


       Bean<Mango> mb = new Bean<>();
       mb.set(new Mango());
       mb.set(new Alphonso());

 But the declaration below would thow an error - The method set(capture#2-of ? extends Mango) in the type Bean<capture#2-of ? extends Mango> is not applicable for the arguments (Mango)


       Bean<? extends Mango> meb = new Bean<>();
       meb.set(new Mango());      // error    
       meb.set(new Alphonso());   // error


Now this is kind of strange – after all an Alphonso is a Mango (class Alphonso extends class Mango). Let us try this with the class ArrayList. And we see that they too give an error when we try to add values to the list of Number. The error is similar - The method add(capture#4-of ? extends Number) in the type List<capture#4-of ? extends Number> is not applicable for the arguments (Integer).


       List<? extends Number> nList = new ArrayList<>();
       Integer i = new Integer("4");
       nList.add(i); // error


What is going on here? Well, that reason is, if you are allowed to add an Integer as well as Double (both are afterall a Number) after the code is complied and the type erased (coming later), then you would not know what did you add unless you do a specific type checking (or type casting) when you use it. You really won’t know what would be doing in the code. It is allowing you to add different types in a list and generics were created to avoid this problem in the first place. In fact it is quite similar to the code without generics if it allows you to do it.

       List nList = new ArrayList();
       Integer i = new Integer("4");
       nList.add(i); // no error, but no generics


The java compiler therefore prevents it. But you can use this construct to fetch the values as a number as it would be clearly type safe. Thus you can use this decalarion as a read-only list, where you can read the elements as a number, but are not allowed to add to it. Thus the below construct is actually allowed.

      
       // somewhere someone created a number list
       // and you add all types of number to it
       List<Number> nList = new ArrayList<>();
       nList.add(new Integer(4));
       nList.add(new Double(1.5));
       nList.add(new Long(2000));
             
       // and you can then read it as <? extends Number> somewhere else
       // preventing it from being messed up by addition
       // notice you can do this assignment
       List<? extends Number> exNumList = nList;
             
       // you can read it as a Number but not as Integer
       Number n1 = exNumList.get(1);
       Integer iNum = exNumList.get(1); // error - Type mismatch: cannot convert from capture#3-of ? extends Number to Integer


Well, if we can get a read-only list, can I also have a write-only one? Can there be a list that can be used to only write a value? Yes. Welcome the keyword super.


       public void writeOnly(List<? super Number> nList) {
             
              // all are valid
              nList.add(new Integer(4));
              nList.add(new Double(1.5));
              nList.add(new Long(2000));
             
              // cannot read it though :(
              // error
              Number n = nList.get(1);                
       }


You might wonder, why will you use such a construct? Actually it is quite handy when you are writing methods where you want to prevent the values from being read. What if you want to both read and write to the list? The answer is simple, just no not use wildcards.  


       public void readAndWrite(List<Number> nList) {
             
              // all are valid
              nList.add(new Integer(4));
              nList.add(new Double(1.5));
              nList.add(new Long(2000));
             
              // all are valid    
              Number n1 = nList.get(1);
              Number n2 = nList.get(2);               
       }


The bounding with super is also known as lower bounding. These upper and lower bounding can be used to the “in” and “out” parameter in a method. Suppose you want to read some elements in a list and add it to another. The best way to declare such a method is given below that avoids errors in the code.


       public static void copy(List<? extends Number> in, List<? super Number> out) {
              for (Number n : in) {
                     out.add(n);
              }
       }



Difference between wild card and a typed element


What is the difference between using <? extends Fruit> and <E extends Fruit>? Which one should be used? In the above example we saw that the two cases are equivalent and either can be used, but it is usually not the case in all cases. Here are the nuggets of wisdom, distilled for you.
  • Type parameter bounds can specify multiple bounds which you cannot do with a wildcard. Thus the declaration <E extends Fruit & Exportable> is valid but not <? extends Fruit & Exportable>

       public <E extends Fruit & Exportable> void doSomething1(List<E> e){
              // do something
       }
      
       // error
       public  void doSomething2(List<? extends Fruit & Exportable> e){
              // do something
       }

  • You cannot add elements with ‘extends’ wildcards – in the declaration below the second method declaration would give errors – something we have seen above.  
             
       public static <E extends Number> List<E> createList1(E e1, E e2){
              List<E> ls = new ArrayList<>();
              ls.add(e1);
              ls.add(e2);
              return ls;          
       }
             
       public static List<? extends Number> createList2(Number e1, Number e2){
              List<? extends Number> ls = new ArrayList<>();
              ls.add(e1); // error
              ls.add(e2); // error
              return ls;          
       }

  • You cannot have types with lowerbounds but only with wildcards. Thus the declaration <? super Fruit> is valid but <E super Fruit> is not.
  • You should avoid using wildcard as the method return type. This sometimes forces the users to use wildcards in the objects being declared. Just follow a simple rule – use wildcards only as method parameters and the typed parameters as the return types so that the user of your API does not have to worry about wildcards. Wildcards should generally be used by the API programmer and not by the API user.  Consider the code below for the two createList methods declared above. The method which returns a wildcard should be accepted by a variable which has a wildcard. The only reason you’s like to return a wildcard in the method, is to lock it for further addition (remember the upperbounding constraints). If java would have allowed the third declaration to be valid then we could add elements to the list with the upperbound constrains which as we explored above is not right.

       List<Number> nList1 = createList1(3, 4);
       List<? extends Number> nExList1 = createList2(3, 4);
             
       // invalid - compiler error
       // Type mismatch: cannot convert from List<capture#3-of ? extends Number>
       // to List<Number>
       List<Number> nList2 = createList2(3, 4);
       // valid
       List<? extends Number> nExList2 = createList2(3, 4);






Prev Topic

Topics



Next Topic

Comments

Popular posts from this blog

Java Functional Programming : 3. The java.util.function Library

Java Generics - 3. Multiple bounding