- Forward


Wildcards in Parameterized Classes/Interfaces
in Java


Prof. David Bernstein
James Madison University

Computer Science Department
bernstdh@jmu.edu

Print

Review
Back SMYC Forward
  • An Observation:
    • We can treat any object generically using the type Object (since it is an ancestor of all classes) but this isn't "type safe"
  • Parameterized Classes:
    • Class/interface declarations can be parameterized
    • Attributes and methods can use the parameterized type as if it were an actual type
  • What This Accomplishes:
    • We can tell the compiler what kind of Object is appropriate
Review (cont.)
Back SMYC Forward
  • A Common Need:
    • When constructing the parameterized class, you want to restrict the types that can be used as type arguments (i.e., place a bound/limit/restriction on the classes that can be "bound to")
  • The Solution:
    • List the parameter name followed by extends, followed by, what are unfortunately called, the bounding types (separated by an &)
  • A Notes:
    • The term extends was probably a bad choice since any class that either extends the bounding type or implements the bounding type can be used (which is why there can be more than one)
    • Only one class can be a bounding type and it must appear first in the list; the list can contain multiple interfaces
Topics of this Lecture
Back SMYC Forward
  • Some Terminology and Process Details:
    • Reifiable/Non-Reifiable Types
    • Type Erasure
    • Heap Pollution
  • Wildcards:
    • Covariance: ? extends Type
    • Contravariance: ? super Type
Reification
Back SMYC Forward
  • Common Meaning:
    • Making a thing real; bringing a thing into being
  • Type Systems:
    • A type is reifiable if information about it is fully available at run-time
Type Erasure
Back SMYC Forward
  • Defined:
    • The replacement of the type parameter in a generic type with its bound (if bounded) or Object (if unbounded)
  • Rationale:
    • Byte code was not changed when parameterized classes were introduced
    • Eliminate run-time overhead
  • Implications:
    • Type information is not available at run-time (so, for example, parameterized collections are not reifiable)
    • Some limitations must be imposed at compile-time
Heap Pollution
Back SMYC Forward
  • Defined:
    • When a variable of a type refers to an object that is not of that type
  • Possible Causes:
    • Mixing raw and parameterized types
    • Unchecked casts
    • Type erasure
Review of Arrays
Back SMYC Forward
  • Primitive Type or Reference Type?
    • Arrays are reference types
      Expand
  • Reifiable?
    • Arrays are reifiable
      Expand
Review of Arrays (cont.)
Back SMYC Forward
Assignment of Elements
Number[] n = new Number[10]; n[0] = Integer.valueOf(1); // An Integer "is a" Number n[1] = Double.valueOf(1.0); // A Double "is a" Number
Review of Arrays (cont.)
Back SMYC Forward
Assignment of Arrays
Number[] n = new Number[10]; Integer[] i = new Integer[5]; // Won't compile because a Number[] "is not a" Integer[] i = n; // Will compile because an Integer[] "is a" Number[] n = i;
Review of Arrays (cont.)
Back SMYC Forward
Compile-Time vs. Run-Time
Number[] n; Integer[] i = new Integer[10]; i[0] = Integer.valueOf(5); i[1] = Integer.valueOf(6); n = i; // Will compile and run n[2] = Integer.valueOf(7); // Will compile but will throw an ArrayStoreException at run-time // to prevent heap pollution n[3] = Double.valueOf(7.5);
Review of Arrays (cont.)
Back SMYC Forward
  • Conclusions:
    • Because arrays are reifiable reference types, some checks can be performed at run-time
  • So?
    • Because parameterized collections are non-reifiable reference types, some behavior must be prevented at compile-time (more on this later)
Motivating Covariance
Back SMYC Forward
  • An Example:
    • We want to write a Statistics class that can, among other things, find the maximum and minimum of a data set
  • A Seemingly Workable Design:
    • Use an Ordered interface to characterize the data
    • Write min() and max() methods that are passed List<Ordered> objects
Motivating Covariance (cont.)
Back SMYC Forward

The Ordered Interface

javaexamples/parameterized/wildcards/v1/Ordered.java
 
Motivating Covariance (cont.)
Back SMYC Forward

The Statistics Class

javaexamples/parameterized/wildcards/v1/Statistics.java
 
Motivating Covariance (cont.)
Back SMYC Forward
  • The Problem with this Design:
    • An invocation of max() that is passed a List of objects that implements Ordered (e.g., List<Person> where Person implements Ordered) will not compile because the List class must ensure that its elements are Person objects, not Ordered objects (a type of class invariance)
  • What's Needed:
    • A way to relax the restrictions
Using Covariance
Back SMYC Forward
  • Wildcards:
    • ? represents an unknown type
  • Upper Bounded Wildcards:
    • Limits the unknown type to be a specialization (or realization) of the bound
  • Syntax:
    • ? extends B where B, the upper bound, can be a class or interface (and "extends" means "extends or implements")
Using Covariance (cont.)
Back SMYC Forward
  • One Interpretation of "Upper Bound":
    • Create a UML class model with Object (the root node) at the top and descendants at lower levels
    • The bound is as far up the tree as you can go
  • Another Interpretation of "Upper Bounds":
    • A class defines a set of objects
    • The bound is the largest allowed set/class
Using Covariance (cont.)
Back SMYC Forward

A Revised Statistics Class

javaexamples/parameterized/wildcards/v2/Statistics.java
 
Using Covariance (cont.)
Back SMYC Forward
  • Recall:
    • Type erasure makes parameterized collections non-reifiable
  • Implication:
    • They are restricted to be "read only" when upper bounded
Using Covariance (cont.)
Back SMYC Forward
Mimicking the Array Example with Lists
List<? extends Number> n = new ArrayList<Number>(); List<Integer> i = new ArrayList<Integer>(); i.add(Integer.valueOf(5)); i.add(Integer.valueOf(6)); // Will compile because Integer "is a" ? extends Number n = i; // Can't be checked at run-time because of type erasure // so must be prevented at compile time (i.e., won't compile // because "the type is not applicable for the argument") n.add(Double.valueOf(7.5));
Using Covariance on More Complicated Types
Back SMYC Forward
  • Motivation:
    • The compareTo() method is not type safe because it only ensures that this and other are both Ordered (but not necessarily of the same type)
    • This can be improved by parameterizing Ordered
  • Added Benefit:
    • The max() and min() methods can now return an object of appropriate type
  • A Complication:
    • The parameterized type of the interface gets more complicated
Using Covariance on More Complicated Types (cont.)
Back SMYC Forward

A Revised Ordered Interface

javaexamples/parameterized/wildcards/v3/Ordered.java
 
Using Covariance on More Complicated Types(cont.)
Back SMYC Forward

A Further Revised Statistics Class

javaexamples/parameterized/wildcards/v3/Statistics.java
 
Motivating Contravariance
Back SMYC Forward
  • An Example:
    • We want to write a method that populates a collection with the bounding rectangles of some enemies in a video game
  • Existing Components:
    • A Rectangle class
    • A Shape interface that is realized by the Rectangle class
  • An Adequate Design:
    • Make both the formal and actual parameters List<Rectangle>
Motivating Contravariance (cont.)
Back SMYC Forward
The Initial Implementation
// The method public void populateBoundsList(List<Rectangle> bounds) { // In an appropriate loop { Rectangle r; // Construct the Rectangle bounds.add(r); } } // The invoker finder.populateBoundsList(new ArrayList<Rectangle>());
Motivating Contravariance (cont.)
Back SMYC Forward
  • An Issue:
    • An invoker may not need all of the capabilities of the Rectangle class, it may only need the capabilities of the Shape interface
  • An Incorrect "Fix":
    • Make the formal parameter a List<Shape>
Motivating Contravariance (cont.)
Back SMYC Forward
The Incorrect "Fix"
// The method public void populateBoundsList(List<Shape> bounds) { // In an appropriate loop { Rectangle r; // Construct the Rectangle bounds.add(r); } } // The new invoker will compile finder.populateBoundsList(new ArrayList<Shape>()); // The old invoker will not compile finder.populateBoundsList(new ArrayList<Rectangle>());
Motivating Contravariance (cont.)
Back SMYC Forward
  • A Thought:
    • Use an upper bound wildcard
  • An Alternative Incorrect "Fix":
    • Make the formal parameter a List<? extends Shape>
Motivating Contravariance (cont.)
Back SMYC Forward
The Incorrect "Fix"
// The method public void populateBoundsList(List<? extends Shape> bounds) { // In an appropriate loop { Rectangle r; // Construct the Rectangle // Won't compile because bounds is "read only" to prevent // heap pollution bounds.add(r); } } // The new invoker will compile finder.populateBoundsList(new ArrayList<Shape>()); // The old invoker will compile finder.populateBoundsList(new ArrayList<Rectangle>());
Motivating Contravariance (cont.)
Back SMYC Forward
  • What's Needed:
    • A way to indicate that the formal parameter can be an ancestor of the given type
  • In Other Words:
    • A lower bounded wildcard
Using Contravariance
Back SMYC Forward
  • Lower Bounded Wildcards:
    • Limits the unknown type to be a generalization of the bound
  • Syntax:
    • ? super B where B is the lower bound
Using Contravariance (cont.)
Back SMYC Forward
  • One Interpretation of "Lower Bound":
    • Create a UML class model with Object (the root node) at the top and descendants at lower levels
    • The bound is as far down the tree as you can go
  • Another Interpretation of "Lower Bounds":
    • A class defines a set of objects
    • The bound is the smallest allowed set/class
Motivating Contravariance (cont.)
Back SMYC Forward
The Example Revisited
// The flexible version of the method public void populateBoundsList(List<? super Rectangle> bounds) { // In an appropriate loop { Rectangle r; // Construct the Rectangle bounds.add(r); } } // The new invoker will compile because it is assured that // the elements will implement Shape finder.populateBoundsList(new ArrayList<Shape>()); // The old invoker will compile because it is assured that // the elements will be Rectangle objects finder.populateBoundsList(new ArrayList<Rectangle>());
Using Contravariance (cont.)
Back SMYC Forward
  • Recall:
    • Type erasure makes parameterized collections non-reifiable
  • Implication:
    • They are restricted to be "write only" when lower bounded
Using Contravariance (cont.)
Back SMYC Forward
A Different Version of the Example with Lists
List<Integer> i = new ArrayList<Integer>(); List<Number> n = new ArrayList<Number>(); List<? super Integer> other; other = i; // Will compile because i can treat the element as an Integer other.add(Integer.valueOf(1)); // Won't compile because the element could be any ancestor // of Integer Integer i2i = other.get(0); Number i2n = other.get(0); other = n; // Will compile because n can treat the element as a Number other.add(Integer.valueOf(1)); // Won't compile because the element could be any ancestor // of Integer Integer n2i = other.get(0); Number n2n = other.get(0);
Putting it All Together
Back SMYC Forward
  • PECS:
    • From the standpoint of a method that is passed a collection: "producer extends, consumer super"
  • Explained:
    • If the collection produces objects of type T (i.e., the method gets objects from the collection) then it can be of type <? extends T> (because it can produce more specialized objects)
    • If the collection consumes objects of type T (i.e., the method adds objects to the collection) then it can be of type <? super T> (because it can consume more generalized objects)
There's Always More to Learn
Back -