Ordinary classes and methods work with specific types:either primitives or class types. If you are writing code that might be used across more types, this rigidity can be overconstraining.
One way that object-oriented languages allow generalization is through polymorphism. You can write (for example) a method that takes a base class object as an argument, and then use that method with any class derived from that base class. Now your method is a little more general and can be used in more places. The same is true within classes—anyplace you use a specific type, a base type provides more flexibility. Of course, anything but a final(Or a class with all private constructors) class can be extended, so this flexibility is automatic much of the time.
Sometimes, being constrained to a single hierarchy is too limiting. If a method argument is an interface instead of a class, the limitations are loosened to include anything that implements the interface—including classes that haven't been created yet. This gives the client programmer the option of implementing an interface in order to conform to your class or method. So interfaces allow you to cut across class hierarchies, as long as you have the option to create a new class in order to do so.
Sometimes even an interface is too restrictive. An interface still requires that your code work with that particular interface. You could write even more general code if you could say that your code works with "some unspecified type," rather than a specific interface or class.
This is the concept of generics, one of the more significant changes in Java SE5. Generics implement the concept of parameterized types, which allow multiple types. The term "generic" means "pertaining or appropriate to large groups of classes." The original intent of generics in programming languages was to allow the programmer the greatest amount of expressiveness possible when writing classes or methods, by loosening the constraints on the types that those classes or methods work with. As you will see in this chapter, the Java implementation of generics is not that broad reaching—indeed, you may question whether the term "generic" is even appropriate for this feature.
If you've never seen any kind of parameterized type mechanism before, Java generics will probably seem like a convenient addition to the language. When you create an instance of a parameterized type, casts will be taken care of for you and the type correctness will be ensured at compile time. This seems like an improvement.
However, if you've had experience with a parameterized type mechanism, in C++, for example, you will find that you can't do everything that you might expect when using Java generics. While using someone else's generic type is fairly easy, when creating your own you will encounter a number of surprises.One of the things I shall try to explain is how the feature came to be like it is.
This is not to say that Java generics are useless. In many cases they make code more straightforward and even elegant. But if you're coming from a language that has implemented a more pure version of generics, you may be disappointed. In this chapter, we will examine both the strengths and the
limitations of Java generics so that you can use this new feature more effectively.
Comparison with C++
The Java designers stated that much of the inspiration for the language came as a reaction to C++. Despite this, it is possible to teach Java largely without reference to C++, and I have endeavored to do so except when the comparison will give you greater depth of understanding.
Generics require more comparison with C++ for two reasons. First, understanding certain aspects of C++ templates (the main inspiration for generics, including the basic syntax) will help you understand the foundations of the concept, as well as—and this is very important—the limitations of what you can do with Java generics and why. The ultimate goal is to give you a clear understanding of where the boundaries lie, because my experience is that by understanding the boundaries, you become a more
powerful programmer. By knowing what you can't do, you can make better use of what you can do (partly because you don't waste time bumping up against walls).
The second reason is that there is significant misunderstanding in the Java community about C++ templates, and this misunderstanding may further confuse you about the intent of generics.
So although I will introduce a few C++ template examples in this chapter, I will keep them to a minimum.
Simple generics
One of the most compelling initial motivations for generics is to create container classes, which you saw in the Holding Your Objects chapter (you'll learn more about these in the Containers in Depth chapter). A container is a place to hold objects while you're working with them. Although this is also true of arrays, containers tend to be more flexible and have different characteristics than simple arrays. Virtually all programs require that you hold a group of objects while you use them, so containers are one of the most
reusable of class libraries.
Let's look at a class that holds a single object. Of course, the class could specify the exact type of the object, like this:
// : generics/Holderl.java class Automobile { } public class Holderl { private Automobile a; public Holderl(Automobile a) { this.a = a; } Automobile get() { return a; } } // /: -
But this is not a very reusable tool, since it can't be used to hold anything else.We would prefer not to write a new one of these for every type we encounter.
Before Java SE5, we would simply make it hold an Object:
//: generics/Holder2.java public class Holder2 { private Object a; public Holder2(Object a) { this.a = a; } public void set(Object a) { this.a = a; } public Object get() { return a; } public static void main(String[] args) { Holder2 h2 = new Holder2(new Automobile()); Automobile a = (Automobile) h2.get(); h2.set("Not an Automobile"); String s = (String) h2.get(); h2.set(1); // Autoboxes to Integer Integer x = (Integer) h2.get(); } } // /:-
There are some cases where you want a container to hold multiple types of objects, but typically you only put one type of object into a container. One of the primary motivations for generics is to specify what type of object a container holds, and to have that specification backed up by the compiler.
So instead of Object, we'd like to use an unspecified type, which can be decided at a later time. To do this, you put a type parameter inside angle brackets after the class name, and then substitute an actual type when you use the class. For the "holder" class, it looks like this, where T is the type parameter:
//: generics/Holder3.Java public class Holder3<T> { private T a; public Holder3(T a) { this.a = a; } public void set(T a) { this.a = a; } public T get() { return a; } public static void main(String[] args) { Holder3<Automobile> h3 = new Holder3<Automobile>(new Automobile()); Automobile a = h3.get(); // No cast needed // h3.set("Not an Automobile"); // Error // h3.set(l) ; // Error } } // /:-
Now when you create a Holders, you must specify what type you want to put into it using the same angle-bracket syntax, as you can see in main( ). You are only allowed to put objects of that type (or a subtype, since the substitution principle still works with generics) into the holder. And when you get a value out, it is automatically the right type.
That's the core idea of Java generics: You tell it what type you want to use, and it takes care of the details.
In general, you can treat generics as if they are any other type—they just happen to have type parameters. But as you'll see, you can use generics just by naming them along with their type argument list.
A tuple library
One of the things you often want to do is return multiple objects from a method call. The return statement only allows you to specify a single object,so the answer is to create an object that holds the multiple objects that you want to return. Of course, you can write a special class every time you encounter the situation, but with generics it's possible to solve the problem once and save yourself the effort in the future. At the same time, you are ensuring compile-time type safety.
This concept is called a tuple, and it is simply a group of objects wrapped together into a single object. The recipient of the object is allowed to read the elements but not put new ones in. (This concept is also called a Data Transfer Object (or Messenger.)
Tuples can typically be any length, but each object in the tuple can be of a different type. However, we want to specify the type of each object and ensure that when the recipient reads the value, they get the right type. To deal with the problem of multiple lengths, we create multiple different tuples. Here's
one that holds two objects:
// : net/mindview/uti1/TwoTuple.java package net.mindview.util; public class TwoTuple<A, B> { public final A first; public final B second; public TwoTuple(A a, B b) { first = a; second = b; } public String toString() { return "(" + first + ", " + second + ")"; } } // /:-
The constructor captures the object to be stored, and toString( ) is a convenience function to display the values in a list. Note that a tuple implicitly keeps its elements in order.
Upon first reading, you may think that this could violate common safety principles of Java programming. Shouldn't first and second be private,and only accessed with methods named getFirst( ) and getSecond( )?Consider the safety that you would get in that case: Clients could still read the objects and do whatever they want with them, but they could not assign first or second to anything else. The final declaration buys you the same safety,but the above form is shorter and simpler.
Another design observation is that you might want to allow a client programmer to point first or second to another object. However, it's safer to leave it in the above form, and just force the user to create a new
TwoTuple if they want one that has different elements.
The longer-length tuples can be created with inheritance. You can see that adding more type parameters is a simple matter:
//: net/mindview/uti1/ThreeTuple.java package net.mindview.util; public class ThreeTuple<A, B, C> extends TwoTuple<A, B> { public final C third; public ThreeTuple(A a, B b, C c) { super(a, b); third = c; } public String toString() { return "(" + first + ", " + second + ", " + third + ")"; } } // /: -
//: net/mindview/util/FourTuple.java package net.mindview.util; public class FourTuple<A, B, C, D> extends ThreeTuple<A, B, C> { public final D fourth; public FourTuple(A a, B b, C c, D d) { super(a, b, c); fourth = d; } public String toString() { return "(" + first + ", " + second + ", " + third + ", " + fourth + ")"; } }
//: net/mindview/uti1/FiveTuple.Java package net.mindview.util; public class FiveTuple<A, B, C, D, E> extends FourTuple<A, B, C, D> { public final E fifth; public FiveTuple(A a, B b, C c, D d, E e) { super(a, b, c, d); fifth = e; } public String toString() { return "(" + first + ", " + second + ", " + third + ", " + fourth + ", " + fifth + ")"; } } // /:-
To use a tuple, you simply define the appropriate-length tuple as the return value for your function, and then create and return it in your return statement:
package net.mindview.util; // : generics/TupleTest.Java import net.mindview.util.*; class Amphibian { } class Vehicle { } public class TupleTest { static TwoTuple<String, Integer> f() { // Autoboxing converts the int to Integer : return new TwoTuple<String, Integer>("hi", 47); } static ThreeTuple<Amphibian, String, Integer> g() { return new ThreeTuple<Amphibian, String, Integer>(new Amphibian(), "hi", 47); } static FourTuple<Vehicle, Amphibian, String, Integer> h() { return new FourTuple<Vehicle, Amphibian, String, Integer>( new Vehicle(), new Amphibian(), "hi", 47); } static FiveTuple<Vehicle, Amphibian, String, Integer, Double> k() { return new FiveTuple<Vehicle, Amphibian, String, Integer, Double>( new Vehicle(), new Amphibian(), "hi", 47, 11.1); } public static void main(String[] args) { TwoTuple<String, Integer> ttsi = f(); System.out.println(ttsi); // ttsi.first = "there"; // Compile error: final System.out.println(g()); System.out.println(h()); System.out.println(k()); } }
Because of generics, you can easily create any tuple to return any group of types, just by writing the expression.
You can see how the final specification on the public fields prevents them from being reassigned after construction, in the failure of the statement ttsi.first = "there".
The new expressions are a little verbose. Later in this chapter you'll see how to simplify them using generic methods.
A stack class
Let's look at something slightly more complicated: the traditional pushdown stack. In the Holding Your Objects chapter, you saw this implemented using a LinkedList as the net.mindview.util.Stack class (page 412). In that example, you can see that a LinkedList already has the necessary methods to create a stack. The Stack was constructed by composing one generic class (Stack<T>) with another generic class (LinkedList<T>). In that example,notice that (with a few exceptions that we shall look at later) a generic type is just another type.
Instead of using LinkedList, we can implement our own internal linked storage mechanism.
//: generics/LinkedStack.Java // A stack implemented with an internal linked structure. public class LinkedStack<T> { private static class Node<U> { U item; Node<U> next; Node() { item = null; next = null; } Node(U item, Node<U> next) { this.item = item; this.next = next; } boolean end() { return item == null && next == null; } } private Node<T> top = new Node<T>(); // End sentinel public void push(T item) { top = new Node<T>(item, top); } public T pop() { T result = top.item; if (!top.end()) top = top.next; return result; } public static void main(String[] args) { LinkedStack<String> lss = new LinkedStack<String>(); for (String s : "Phasers on stun!".split(" ")) lss.push(s); String s; while ((s = lss.pop()) != null) System.out.println(s); } }
The inner class Node is also a generic, and has its own type parameter.
This example makes use of an end sentinel to determine when the stack is empty. The end sentinel is created when the LinkedStack is constructed,and each time you call push( ) a new Node<T> is created and linked to the previous Node<T>. When you call pop( ), you always return the top.item,and then you discard the current Node<T> and move to the next one— except when you hit the end sentinel, in which case you don't move. That way,if the client keeps calling pop( ), they keep getting null back to indicate that the stack is empty.
RandomList
For another example of a holder, suppose you'd like a special type of list that randomly selects one of its elements each time you call select( ). When doing this you want to build a tool that works with all objects, so you use generics:
//: generics/RandomList.Java import java.util.*; public class RandomList<T> { private ArrayList<T> storage = new ArrayList<T>(); private Random rand = new Random(47); public void add(T item) { storage.add(item); } public T select() { return storage.get(rand.nextInt(storage.size())); } public static void main(String[] args) { RandomList<String> rs = new RandomList<String>(); for (String s : ("The quick brown fox jumped over " + "the lazy brown dog").split(" ")) rs.add(s); for (int i = 0; i < 11; i++) System.out.print(rs.select() + " "); } }
Generic interfaces
Generics also work with interfaces. For example, a generator is a class that creates objects. It's actually a specialization of the Factory Method design pattern, but when you ask a generator for new object, you don't pass it any arguments, whereas you typically do pass arguments to a Factory Method. The generator knows how to create new objects without any extra information.
Typically, a generator just defines one method, the method that produces new objects. Here, we'll call it next( ), and include it in the standard utilities:
The return type of next( ) is parameterized to T. As you can see, using generics with interfaces is no different than using generics with classes.
To demonstrate the implementation of a Generator, we'll need some classes. Here's a coffee hierarchy:
// : generics/coffee/Coffee.Java package generics.coffee; public class Coffee { private static long counter = 0; private final long id = counter++; public String toString() { return getClass().getSimpleName() + " " + id; } } // /: -
//: generics/coffee/Latte.java package generics.coffee; public class Latte extends Coffee {}
//: generics/coffee/Mocha.Java package generics.coffee; public class Mocha extends Coffee {} ///:-
//: generics/coffee/Cappuccino.java package generics.coffee; public class Cappuccino extends Coffee {} ///:-
//: generics/coffee/Americano.Java package generics.coffee; public class Americano extends Coffee {}
//: generics/coffee/Breve.Java package generics.coffee; public class Breve extends Coffee {}
Now we can implement a Generator < Coffee > that produces random different types of Coffee objects:
//: generics/coffee/CoffeeGenerator.java // Generate different types of Coffee: package generics.coffee; import java.util.*; import net.mindview.util.*; public class CoffeeGenerator implements Generator<Coffee>, Iterable<Coffee> { private Class[] types = { Latte.class, Mocha.class, Cappuccino.class, Americano.class, Breve.class, }; private static Random rand = new Random(47); public CoffeeGenerator() { } // For iteration: private int size = 0; public CoffeeGenerator(int sz) { size = sz; } public Coffee next() { try { return (Coffee) types[rand.nextInt(types.length)].newInstance(); // Report programmer errors at run time: } catch (Exception e) { throw new RuntimeException(e); } } class Coffeelterator implements Iterator<Coffee> { int count = size; public boolean hasNext() { return count > 0; } public Coffee next() { count--; return CoffeeGenerator.this.next(); } public void remove() { // Not implemented throw new UnsupportedOperationException(); } }; public Iterator<Coffee> iterator() { return new Coffeelterator(); } public static void main(String[] args) { CoffeeGenerator gen = new CoffeeGenerator(); for (int i = 0; i < 5; i++) System.out.println(gen.next()); for (Coffee c : new CoffeeGenerator(5)) System.out.println(c); } }
这里对Iterable的用法及其错误
The parameterized Generator interface ensures that next( ) returns the parameter type. CoffeeGenerator also implements the Iterable interface,so it can be used in a foreach statement. However, it requires an "end sentinel" to know when to stop, and this is produced using the second constructor.
Here's a second implementation of Generator<T>, this time to produce Fibonacci numbers:
package generics.coffee; //: generics/Fibonacci.java // Generate a Fibonacci sequence, import net.mindview.util.*; public class Fibonacci implements Generator<Integer> { private int count = 0; public Integer next() { return fib(count++); } private int fib(int n) { if (n < 2) return 1; return fib(n - 2) + fib(n - 1); } public static void main(String[] args) { Fibonacci gen = new Fibonacci(); for (int i = 0; i < 18; i++) System.out.print(gen.next() + " "); } }
Although we are working with ints both inside and outside the class, the type parameter is Integer. This brings up one of the limitations of Java generics:You cannot use primitives as type parameters. However, Java SE5 conveniently added autoboxing and autounboxing to convert from primitive types to wrapper types and back. You can see the effect here because ints are seamlessly used and produced by the class.
We can go one step further and make an Iterable Fibonacci generator. One option is to reimplement the class and add the Iterable interface, but you don't always have control of the original code, and you don't want to rewrite when you don't have to. Instead, we can create an adapter to produce the desired interface—this design pattern was introduced earlier in the book.
Adapters can be implemented in multiple ways. For example, you could use inheritance to generate the adapted class:
package generics.coffee; //: generics/IterableFibonacci.java // Adapt the Fibonacci class to make it Iterable. import java.util.*; public class IterableFibonacci extends Fibonacci implements Iterable<Integer> { private int n; public IterableFibonacci(int count) { n = count; } public Iterator<Integer> iterator() { return new Iterator<Integer>() { public boolean hasNext() { return n > 0; } public Integer next() { n--; return IterableFibonacci.this.next(); } public void remove() { // Not implemented throw new UnsupportedOperationException(); } }; } public static void main(String[] args) { for (int i : new IterableFibonacci(18)) System.out.print(i + " "); } }
To use IterableFibonacci in a foreach statement, you give the constructor a boundary so that hasNext( ) can know when to return false.
Generic methods
So far we've looked at parameterizing entire classes. You can also parameterize methods within a class. The class itself may or may not be generic—this is independent of whether you have a generic method.
A generic method allows the method to vary independently of the class. As a guideline, you should use generic methods "whenever you can." That is, if it's possible to make a method generic rather than the entire class, it's probably going to be clearer to do so. In addition, if a method is static, it has no access
to the generic type parameters of the class, so if it needs to use genericity it
must be a generic method.
To define a generic method, you simply place a generic parameter list before the return value, like this:
//: generics/GenericMethods.Java public class GenericMethods { public <T> void f(T x) { System.out.println(x.getClass().getName()); } public static void main(String[] args) { GenericMethods gm = new GenericMethods(); gm.f(""); gm.f(1); gm.f(1.0); gm.f(1.0F); gm.f('C'); gm.f(gm); } }
The class GenericMethods is not parameterized, although both a class and its methods may be parameterized at the same time. But in this case, only the method f( ) has a type parameter, indicated by the parameter list before the method's return type.
Notice that with a generic class, you must specify the type parameters when you instantiate the class. But with a generic method, you don't usually have to specify the parameter types, because the compiler can figure that out for you.This is called type argument inference. So calls to f( ) look like normal method calls, and it appears that f( ) has been infinitely overloaded. It will even take an argument of the type GenericMethods.
For the calls to f( ) that use primitive types, autoboxing comes into play,automatically wrapping the primitive types in their associated objects. In fact generic methods and autoboxing can eliminate some code that previously required hand conversion.
Leveraging type argument inference
One of the complaints about generics is that it adds even more text to your code. Consider holding/MapOfList.java from the Holding Your Objects chapter. The creation of the Map of List looks like this:
Map<Person, List<? extends Pet>> petPeople = new HashMap<Person, List<? extends Pet>>()
(This use of extends and the question marks will be explained later in this chapter.) It appears that you are repeating yourself, and that the compiler should figure out one of the generic argument lists from the other. Alas, it cannot, but type argument inference in a generic method can produce some simplification. For example, we can create a utility containing various static methods, which produces the most commonly used implementations of the various containers:
//: net/mindview/util/New.Java // Utilities to simplify generic container creation // by using type argument inference. package net.mindview.util; import java.util.*; public class New { public static <K, V> Map<K, V> map() { return new HashMap<K, V>(); } public static <T> List<T> list() { return new ArrayList<T>(); } public static <T> LinkedList<T> lList() { return new LinkedList<T>(); } public static <T> Set<T> set() { return new HashSet<T>(); } public static <T> Queue<T> queue() { return new LinkedList<T>(); } // Examples: public static void main(String[] args) { Map<String, List<String>> sis = New.map(); List<String> is = New.list(); LinkedList<String> Us = New.lList(); Set<String> ss = New.set(); Queue<String> qs = New.queue(); } } // /:-
In main( ) you can see examples of how this is used—type argument inference eliminates the need to repeat the generic parameter list. This can be applied to holding/MapOfList.java:
package net.mindview.util; //: generics/SimplerPets.java import typeinfo.pets.*; import java.util.*; import net.mindview.util.*; public class SimplerPets { public static void main(String[] args) { Map<Person, List<? extends Pet>> petPeople = New.map(); // Rest of the code is the same... } } // /:-
Although this is an interesting example of type argument inference, it's difficult to say how much it actually buys you. The person reading the code is required to parse and understand this additional library and its implications,so it might be just as productive to leave the original (admittedly repetitious)definition in place—ironically, for simplicity. However, if the standard Java library were to add something like the New.java utility above, it would make sense to use it.
Type inference doesn't work for anything other than assignment. If you pass the result of a method call such as New.map( ) as an argument to another method, the compiler will not try to perform type inference. Instead it will treat the method call as though the return value is assigned to a variable of type Object. Here's an example that fails:
//: generics/LimitsOflnference.java import typeinfo.pets.*; import java.util.*; public class LimitsOfInference { static void f(Map<Person, List<? extends Pet>> petPeople) { } public static void main(String[] args) { f (New.map()) ; // Does not compile } } // /:-
Explicit type specification
It is possible to explicitly specify the type in a generic method, although the syntax is rarely needed. To do so, you place the type in angle brackets after the dot and immediately preceding the method name. When calling a method from within the same class, you must use this before the dot, and when working with static methods, you must use the class name before the dot.The problem shown in LimitsOflnference.java can be solved using this syntax:
//: generics/Explici tTypeSpecificat ion.Java import typeinfo.pets.*; import java.util.*; import net.mindview.util.*; public class ExplicitTypeSpecification { static void f(Map<Person, List<Pet>> petPeople) { } public static void main(String[] args) { f(New.<Person, List<Pet>> map()); } } // /:-
Of course, this eliminates the benefit of using the New class to reduce the amount of typing, but the extra syntax is only required when you are not writing an assignment statement.
Varargs and generic methods
Generic methods and variable argument lists coexist nicely:
//: generics/GenericVarargs.Java import java.util.*; public class GenericVarargs { public static <T> List<T> makeList(T... args) { List<T> result = new ArrayList<T>(); for (T item : args) result.add(item); return result; } public static void main(String[] args) { List<String> ls = makeList("A"); System.out.println(ls); ls = makeList("A", "B", "C"); System.out.println(ls); ls = makeList("ABCDEFFHIJKLMNOPQRSTUVWXYZ".split("")); System.out.println(ls); } }
The makeList( ) method shown here produces the same functionality as the standard library's java.util.Arrays.asList( ) method.
A generic method to use with Generators
It is convenient to use a generator to fill a Collection, and it makes sense to "generify" this operation:
//: generics/Generators.java // A utility to use with Generators. import generics.coffee.*; import java.util.*; import net.mindview.util.*; public class Generators { public static <T> Collection<T> fill(Collection<T> coll, Generator<T> gen, int n) { for (int i = 0; i < n; i++) coll.add(gen.next()); return coll; } public static void main(String[] args) { Collection<Coffee> coffee = fill(new ArrayList<Coffee>(), new CoffeeGenerator(), 4); for (Coffee c : coffee) System.out.println(c); Collection<Integer> fnumbers = fill(new ArrayList<Integer>(), new Fibonacci(), 12); for (int i : fnumbers) System.out.print(i + ", "); } }
Notice how the generic method fill( ) can be transparently applied to both Coffee and Integer containers and generators.
A general-purpose Generator
Here's a class that produces a Generator for any class that has a default constructor. To reduce typing, it also includes a generic method to produce a BasicGenerator:
//: net/mindview/util/BasicGenerator.java // Automatically create a Generator, given a class // with a default (no-arg) constructor. package net.mindview.util; public class BasicGenerator<T> implements Generator<T> { private Class<T> type; public BasicGenerator(Class<T> type) { this.type = type; } public T next() { try { // Assumes type is a public class: return type.newInstance(); } catch (Exception e) { throw new RuntimeException(e); } } // Produce a Default generator given a type token: public static <T> Generator<T> create(Class<T> type) { return new BasicGenerator<T>(type); } } // /:-
This class provides a basic implementation that will produce objects of a class that (1) is public (because BasicGenerator is in a separate package, the class in question must have public and not just package access) and (2) has a default constructor (one that takes no arguments). To create one of these
BasicGenerator objects, you call the create( ) method and pass it the type token for the type you want generated. The generic create( ) method allows you to say BasicGenerator.create(MyType.class) instead of the more awkward new BasicGenerator<MyType>(MyType.class) .
For example, here's a simple class that has a default constructor:
// : generics/CountedObject.java public class CountedObject { private static long counter = 0; private final long id = counter++; public long id() { return id; } public String toString() { return "CountedObject " + id; } } // /: -
The CountedObject class keeps track of how many instances of itself have been created, and reports these in its toString( ).
Using BasicGenerator, you can easily create a Generator for CountedObject:
//: generics/BasicGeneratorDemo.Java import net.mindview.util.*; public class BasicGeneratorDemo { public static void main(String[] args) { Generator<CountedObject> gen = BasicGenerator .create(CountedObject.class); for (int i = 0; i < 5; i++) System.out.println(gen.next()); } }
You can see how the generic method reduces the amount of typing necessary to produce the Generator object. Java generics force you to pass in the Class object anyway, so you might as well use it for type inference in the create( ) method.
Simplifying tuple use
Type argument inference, together with static imports, allows the tuples we saw earlier to be rewritten into a more general-purpose library. Here, tuples can be created using an overloaded static method:
// : net/mindview/util/Tupie.java // Tuple library using type argument inference . package net.mindview.util; public class Tuple { public static <A, B> TwoTuple<A, B> tuple(A a, B b) { return new TwoTuple<A, B>(a, b); } public static <A, B, C> ThreeTuple<A, B, C> tuple(A a, B b, C c) { return new ThreeTuple<A, B, C>(a, b, c); } public static <A, B, C, D> FourTuple<A, B, C, D> tuple(A a, B b, C c, D d) { return new FourTuple<A, B, C, D>(a, b, c, d); } public static <A, B, C, D, E> FiveTuple<A, B, C, D, E> tuple(A a, B b, C c, D d, E e) { return new FiveTuple<A, B, C, D, E>(a, b, c, d, e); } } // /: -Here's a modification of TupleTest.java to test Tuple.java:
package net.mindview.util; //: generics/TupleTest2.Java import static net.mindview.util.Tuple.tuple; public class TupleTest2 { static TwoTuple<String, Integer> f() { return tuple("hi", 47); } static TwoTuple f2() { return tuple("hi", 47); } static ThreeTuple<Amphibian, String, Integer> g() { return tuple(new Amphibian(), "hi", 47); } static FourTuple<Vehicle, Amphibian, String, Integer> h() { return tuple(new Vehicle(), new Amphibian(), "hi", 47); } static FiveTuple<Vehicle, Amphibian, String, Integer, Double> k() { return tuple(new Vehicle(), new Amphibian(), "hi", 47, 11.1); } public static void main(String[] args) { TwoTuple<String, Integer> ttsi = f(); System.out.println(ttsi); System.out.println(f2()); System.out.println(g()); System.out.println(h()); System.out.println(k()); } }Notice that f( ) returns a parameterized TwoTuple object, while f2( ) returns an unparameterized TwoTuple object. The compiler doesn't warn about f2( ) in this case because the return value is not being used in a parameterized fashion; in a sense, it is being "upcast" to an unparameterized TwoTuple. However, if you were to try to capture the result of f2( ) into a parameterized TwoTuple, the compiler would issue a warning.
A Set utility
For another example of the use of generic methods, consider the mathematical relationships that can be expressed using Sets. These can be conveniently defined as generic methods, to be used with all different types:
// : net/mindview/util/Sets.jav a package net.mindview.util; import java.util.HashSet; import java.util.Set; public class Sets { public static <T> Set<T> union(Set<T> a, Set<T> b) { Set<T> result = new HashSet<T>(a); result.addAll(b); return result; } public static <T> Set<T> intersection(Set<T> a, Set<T> b) { Set<T> result = new HashSet<T>(a); result.retainAll(b); return result; } // Subtract subset from superset: public static <T> Set<T> difference(Set<T> superset, Set<T> subset) { Set<T> result = new HashSet<T>(superset); result.removeAll(subset); return result; } // Reflexive--everything not in the intersection: public static <T> Set<T> complement(Set<T> a, Set<T> b) { return difference(union(a, b), intersection(a, b)); } } // /:-The first three methods duplicate the first argument by copying its references into a new HashSet object, so the argument Sets are not directly modified.The return value is thus a new Set object.
The four methods represent mathematical set operations: union( ) returns a Set containing the combination of the two arguments, intersection( ) returns a Set containing the common elements between the two arguments, difference( ) performs a subtraction of the subset elements from the superset, and complement ( ) returns a Set of all the elements that are not in the intersection. To create a simple example showing the effects of these methods, here's an enum containing different names of watercolors:
For convenience (so that all the names don't have to be qualified), this is imported statically into the following example. This example uses the EnumSet, which is a Java SE5 tool for easy creation of Sets from enums . (You'll learn more about EnumSet in the Enumerated Types chapter.) Here,the static method EnumSet.range( ) is given the first and last elements of the range to create in the resulting Set:
package generics.watercolors; //: generics/WatercolorSets.j ava import static generics.watercolors.Watercolors.BRILLIANT_RED; import static generics.watercolors.Watercolors.BURNT_UMBER; import static generics.watercolors.Watercolors.CERULEAN_BLUE_HUE; import static generics.watercolors.Watercolors.VIRIDIAN_HUE; import static net.mindview.util.Print.print; import static net.mindview.util.Sets.complement; import static net.mindview.util.Sets.difference; import static net.mindview.util.Sets.intersection; import static net.mindview.util.Sets.union; import java.util.EnumSet; import java.util.Set; public class WatercolorSets { public static void main(String[] args) { Set<Watercolors> setl = EnumSet.range(BRILLIANT_RED, VIRIDIAN_HUE); Set<Watercolors> set2 = EnumSet.range(CERULEAN_BLUE_HUE, BURNT_UMBER); print("setl: " + setl); print("set2: " + set2); print("union(setl, set2): " + union(setl, set2)); Set<Watercolors> subset = intersection(setl, set2); print("intersection(setl, set2) : " + subset); print("difference(setl, subset): " + difference(setl, subset)); print("difference(set2, subset): " + difference(set2, subset)); print("complement(setl, set2): " + complement(setl, set2)); } }You can see the results of each operation from the output.
The following example uses Sets.difference( ) to show the method differences between various Collection and Map classes in java.util:
// : net/mindview/util/ContainerMethodDifferences.Jav a package net.mindview.util; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.PriorityQueue; import java.util.Queue; import java.util.Set; import java.util.SortedMap; import java.util.TreeMap; import java.util.TreeSet; public class ContainerMethodDifferences { static Set<String> methodSet(Class<?> type) { Set<String> result = new TreeSet<String>(); for (Method m : type.getMethods()) result.add(m.getName()); return result; } static void interfaces(Class<?> type) { System.out.print("Interfaces in " + type.getSimpleName() + ": "); List<String> result = new ArrayList<String>(); for (Class<?> c : type.getInterfaces()) result.add(c.getSimpleName()); System.out.println(result); } static Set<String> object = methodSet(Object.class); static { object.add("clone"); } static void difference(Class<?> superset, Class<?> subset) { System.out.print(superset.getSimpleName() + " extends " + subset.getSimpleName() + ", adds: "); Set<String> comp = Sets.difference(methodSet(superset), methodSet(subset)); comp.removeAll(object); // Don't show 'Object' methods System.out.println(comp); interfaces(superset); } public static void main(String[] args) { System.out.println("Collection: " + methodSet(Collection.class)); interfaces(Collection.class); difference(Set.class, Collection.class); difference(HashSet.class, Set.class); difference(LinkedHashSet.class, HashSet.class); difference(TreeSet.class, Set.class); difference(List.class, Collection.class); difference(ArrayList.class, List.class); difference(LinkedList.class, List.class); difference(Queue.class, Collection.class); difference(PriorityQueue.class, Queue.class); System.out.println("Map: " + methodSet(Map.class)); difference(HashMap.class, Map.class); difference(LinkedHashMap.class, HashMap.class); difference(SortedMap.class, Map.class); difference(TreeMap.class, Map.class); } } // /:-The output of this program was used in the "Summary" section of the Holding Your Objects chapter.
Anonymous inner classes
Generics can also be used with inner classes and anonymous inner classes. Here's an example that implements the Generator interface using anonymous inner classes:
package net.mindview.util; // : generics/BankTeller.java // A very simple bank telle r simulation . import generics.Generators; import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.Queue; import java.util.Random; class Customer { private static long counter = 1; private final long id = counter++; private Customer() { } public String toString() { return "Customer " + id; } // A method to produce Generator objects : public static Generator<Customer> generator() { return new Generator<Customer>() { public Customer next() { return new Customer(); } }; } } class Teller { private static long counter = 1; private final long id = counter++; private Teller() { } public String toString() { return "Teller " + id; } // A single Generator object: public static Generator<Teller> generator = new Generator<Teller>() { public Teller next() { return new Teller(); } }; } public class BankTeller { public static void serve(Teller t, Customer c) { System.out.println(t + " serves " + c); } public static void main(String[] args) { Random rand = new Random(47); Queue<Customer> line = new LinkedList<Customer>(); Generators.fill(line, Customer.generator(), 15); List<Teller> tellers = new ArrayList<Teller>(); Generators.fill(tellers, Teller.generator, 4); for (Customer c : line) serve(tellers.get(rand.nextInt(tellers.size())), c); } }Both Customer and Teller have private constructors, thereby forcing yo to use Generator objects. Customer has a generator( ) method that produces a new Generator<Customer> object each time you call it. You may not need multiple Generator objects, and Teller creates a single public generator object. You can see both of these approaches used in the fill( ) methods in main( ).
Since both the generator( ) method in Customer and the Generator object in Teller are static, they cannot be part of an interface, so there is no way to "generify" this particular idiom. Despite that, it works reasonably well with the fill( ) method.
We'll look at other versions of this queuing problem in the Concurrency chapter.
Building complex models
An important benefit of generics is the ability to simply and safely create complex models. For example, we can easily create a List of tuples:
//: generics/TupleList.java // Combining generic types to make complex generic types. import java.util.ArrayList; import net.mindview.util.FourTuple; public class TupleList<A, B, C, D> extends ArrayList<FourTuple<A, B, C, D>> { public static void main(String[] args) { TupleList<Vehicle, Amphibian, String, Integer> tl = new TupleList<Vehicle, Amphibian, String, Integer>(); tl.add(TupleTest.h()); tl.add(TupleTest.h()); for (FourTuple<Vehicle, Amphibian, String, Integer> i : tl) System.out.println(i); } }
Although it gets somewhat verbose (especially the creation of the iterator), you end up with a fairly powerful data structure without too much code.
Here's another example showing how straightforward it is to build complex models using generic types. Even though each class is created as a building block, the total has many parts. In this case, the model is a retail store with aisles, shelves and products:
//: generics/Store.java // Building up a complex model using generic containers. import generics.Generators; import java.util.ArrayList; import java.util.Random; import net.mindview.util.Generator; class Product { private final int id; private String description; private double price; public Product(int IDnumber, String descr, double price) { id = IDnumber; description = descr; this.price = price; System.out.println(toString()); } public String toString() { return id + ": " + description + ", price: $" + price; } public void priceChange(double change) { price += change; } public static Generator<Product> generator = new Generator<Product>() { private Random rand = new Random(47); public Product next() { return new Product(rand.nextInt(1000), "Test", Math.round(rand .nextDouble() * 1000.0) + 0.99); } }; } class Shelf extends ArrayList<Product> { public Shelf(int nProducts) { Generators.fill(this, Product.generator, nProducts); } } class Aisle extends ArrayList<Shelf> { public Aisle(int nShelves, int nProducts) { for (int i = 0; i < nShelves; i++) add(new Shelf(nProducts)); } } class CheckoutStand { } class Office { } public class Store extends ArrayList<Aisle> { private ArrayList<CheckoutStand> checkouts = new ArrayList<CheckoutStand>(); private Office office = new Office(); public Store(int nAisles, int nShelves, int nProducts) { for (int i = 0; i < nAisles; i++) add(new Aisle(nShelves, nProducts)); } public String toString() { StringBuilder result = new StringBuilder(); for (Aisle a : this) for (Shelf s : a) for (Product p : s) { result.append(p); result.append("\n"); } return result.toString(); } public static void main(String[] args) { System.out.println(new Store(14, 5, 10)); } } /* * Output: 258: Test, price: $400.99 861: Test, price: $160.99 868: Test, price: * $417.99 207: Test, price: $268.99 551: Test, price: $114.99 278: Test, price: * $804.99 520: Test, price: $554.99 140: Test, price: $530.99 ... */// :~
As you can see in Store.toString( ), the result is many layers of containers that are nonetheless type-safe and manageable. What's impressive is that it is not intellectually prohibitive to assemble such a model.
The mystery of erasure
//: generics/ErasedTypeEquivalence.java import java.util.ArrayList; public class ErasedTypeEquivalence { public static void main(String[] args) { Class c1 = new ArrayList<String>().getClass(); Class c2 = new ArrayList<Integer>().getClass(); System.out.println(c1 == c2); } } /* * Output: true */// :~
Array List < String > and Array List < Integer > could easily be argued to be distinct types. Different types behave differently, and if you try, for example, to put an Integer into an Array List < String >, you get different behavior (it fails) than if you put an Integer into an ArrayList< Integer > (it succeeds). And yet the above program suggests that they are the same type.
Here's an example that adds to this puzzle:
//: generics/LostInformation.java import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; class Frob { } class Fnorkle { } class Quark<Q> { } class Particle<POSITION, MOMENTUM> { } public class LostInformation { public static void main(String[] args) { List<Frob> list = new ArrayList<Frob>(); Map<Frob, Fnorkle> map = new HashMap<Frob, Fnorkle>(); Quark<Fnorkle> quark = new Quark<Fnorkle>(); Particle<Long, Double> p = new Particle<Long, Double>(); System.out .println(Arrays.toString(list.getClass().getTypeParameters())); System.out.println(Arrays.toString(map.getClass().getTypeParameters())); System.out.println(Arrays .toString(quark.getClass().getTypeParameters())); System.out.println(Arrays.toString(p.getClass().getTypeParameters())); } } /* * Output: [E] [K, V] [Q] [POSITION, MOMENTUM] */// :~
According to the JDK documentation, Class.getTypeParameters( ) "returns an array of TypeVariable objects that represent the type variables declared by the generic declaration..." This seems to suggest that you might be able to find out what the parameter types are. However, as you can see from the output, all you find out is the identifiers that are used as the parameter placeholders, which is not such an interesting piece of information.
The cold truth is:
There's no information about generic parameter types available inside generic code.
Thus, you can know things like the identifier of the type parameter and the bounds of the generic type—you just can't know the actual type parameter(s) used to create a particular instance. This fact, which is especially frustrating if you're coming from C++, is the most fundamental issue that you must deal with when working with Java generics.
Java generics are implemented using erasure. This means that any specific type information is erased when you use a generic. Inside the generic, the only thing that you know is that you're using an object. So List<String> and List< Integer> are, in fact, the same type at run time. Both forms are "erased" to their raw type, List. Understanding erasure and how you must deal with it will be one of the biggest hurdles you will face when learning Java generics, and that's what we'll explore in this section.
The C++ approach
Here's a C++ example which uses templates. You'll notice that the syntax for parameterized types is quite similar, because Java took inspiration from C++:
//: generics/Templates.cpp #include <iostream> using namespace std; template<class T> class Manipulator { T obj; public: Manipulator(T x) { obj = x; } void manipulate() { obj.f(); } }; class HasF { public: void f() { cout << "HasF::f()" << endl; } }; int main() { HasF hf; Manipulator<HasF> manipulator(hf); manipulator.manipulate(); } /* Output: HasF::f() ///:~
The Manipulator class stores an object of type T. What's interesting is the manipulate( ) method, which calls a method f( ) on obj. How can it know that the f( ) method exists for the type parameter T? The C++ compiler checks when you instantiate the template, so at the point of instantiation of Manipulator <HasF>, it sees that HasF has a method f( ). If it were not the case, you'd get a compile-time error, and thus type safety is preserved.
Writing this kind of code in C++ is straightforward because when a template is instantiated, the template code knows the type of its template parameters. Java generics are different. Here's the translation of HasF:
//: generics/HasF.java public class HasF { public void f() { System.out.println("HasF.f()"); } } ///:~
If we take the rest of the example and translate it to Java, it won't compile:
//: generics/Manipulation.java // {CompileTimeError} (Won't compile) class Manipulator<T> { private T obj; public Manipulator(T x) { obj = x; } // Error: cannot find symbol: method f(): public void manipulate() { obj.f(); } } public class Manipulation { public static void main(String[] args) { HasF hf = new HasF(); Manipulator<HasF> manipulator = new Manipulator<HasF>(hf); manipulator.manipulate(); } } ///:~
Because of erasure, the Java compiler can't map the requirement that manipulate( ) must be able to call f( ) on obj to the fact that HasF has a method f( ). In order to call f( ), we must assist the generic class by giving it a bound that tells the compiler to only accept types that conform to that bound.This reuses the extends keyword. Because of the bound, the following compiles:
//: generics/Manipulator2.java class Manipulator2<T extends HasF> { private T obj; public Manipulator2(T x) { obj = x; } public void manipulate() { obj.f(); } } ///:~
The bound <T extends HasF> says that T must be of type HasF or something derived from HasF. If this is true, then it is safe to call f( ) on obj.
We say that a generic type parameter erases to its first bound (it's possible to have multiple bounds, as you shall see later). We also talk about the erasure of the type parameter. The compiler actually replaces the type parameter with its erasure, so in the above case, T erases to HasF, which is the same as replacing T with HasF in the class body.
You may correctly observe that in Manipulations.Java, generics do not contribute anything. You could just as easily perform the erasure yourself and produce a class without generics:
//: generics/Manipulator3.java class Manipulator3 { private HasF obj; public Manipulator3(HasF x) { obj = x; } public void manipulate() { obj.f(); } } ///:~
This brings up an important point: Generics are only useful when you want to use type parameters that are more "generic" than a specific type (and all its subtypes)—that is, when you want code to work across multiple classes. As a result, the type parameters and their application in useful generic code will
usually be more complex than simple class replacement. However, you can't just say that anything of the form <T extends HasF> is therefore flawed. For example, if a class has a method that returns T, then generics are helpful,because they will then return the exact type:
//: generics/ReturnGenericType.java class ReturnGenericType<T extends HasF> { private T obj; public ReturnGenericType(T x) { obj = x; } public T get() { return obj; } } ///:~
You have to look at all the code and understand whether it is "complex enough" to warrant the use of generics.
We'll look at bounds in more detail later in the chapter.
Migration compatibility
To allay any potential confusion about erasure, you must clearly understand that it is not a language feature. It is a compromise in the implementation of Java generics, necessary because generics were not made part of the language from the beginning. This compromise will cause you pain, so you need to get
used to it early and to understand why it's there.
If generics had been part of Java l.o, the feature would not have been implemented using erasure—it would have used reification to retain the type parameters as first-class entities, so you would have been able to perform type-based language and reflective operations on type parameters. You'll see later in this chapter that erasure reduces the "genericity" of generics.Generics are still useful in Java, just not as useful as they could be, and the reason is erasure.
In an erasure-based implementation, generic types are treated as second-class types that cannot be used in some important contexts. The generic types are present only during static type checking, after which every generic type in the program is erased by replacing it with a non-generic upper bound. For example, type annotations such as List<T> are erased to List, and ordinary type variables are erased to Object unless a bound is specified.
The core motivation for erasure is that it allows generified clients to be used with non-generified libraries, and vice versa. This is often called migration compatibility. In the ideal world, we would have had a single day when everything was generified at once. In reality, even if programmers are only writing generic code, they will have to deal with non-generic libraries that were written before Java SE5. The authors of those libraries may never have the incentive to generify their code, or they may just take their time in getting
to it.
So Java generics not only must support backwards compatibility—existing code and class files are still legal, and continue to mean what they meant before—but also must support migration compatibility, so that libraries can become generic at their own pace, and when a library does become generic, it
doesn't break code and applications that depend upon it. After deciding that this was the goal, the Java designers and the various groups working on the problem decided that erasure was the only feasible solution. Erasure enables this migration towards generics by allowing non-generic code to coexist with
generic code.
For example, suppose an application uses two libraries, X and Y, and Y uses library Z. With the advent of Java SE5, the creators of this application and these libraries will probably, eventually, want to migrate to generics. Each of them, however, will have different motivations and constraints as to when that migration happens. To achieve migration compatibility, each library and application must be independent of all the others regarding whether generics are used. Thus, they must not be able to detect whether other libraries are or are not using generics. Ergo, the evidence that a particular library is using generics must be "erased."
Without some kind of migration path, all the libraries that had been built up over time stood the chance of being cut off from the developers that chose to move to Java generics. Libraries are arguably the part of a programming language that has the greatest productivity impact, so this was not an acceptable cost. Whether or not erasure was the best or only migration path is something that only time will tell.
The problem with erasure
So the primary justification for erasure is the transition process from non-generified code to generified code, and to incorporate generics into the language without breaking existing libraries. Erasure allows existing non-generic client code to continue to be used without change, until clients are ready to rewrite code for generics. This is a noble motivation, because it doesn't suddenly break all existing code.
The cost of erasure is significant. Generic types cannot be used in operations that explicitly refer to runtime types, such as casts, instanceof operations, and new expressions. Because all the type information about the parameters is lost, whenever you're writing generic code you must constantly be
reminding yourself that it only appears that you have type information about a parameter. So when you write a piece of code like this:
class Foo<T> { T var; }
it appears that when you create an instance of Foo:
Foo<Cat> f = new Foo<Cat>();
the code in class Foo ought to know that it is now working with a Cat. The syntax strongly suggests that the type T is being substituted everywhere throughout the class. But it isn't, and you must remind yourself, "No, it's just an Object," whenever you're writing the code for the class.
In addition, erasure and migration compatibility mean that the use of generics is not enforced when you might want it to be:
//: generics/ErasureAndInheritance.java class GenericBase<T> { private T element; public void set(T arg) { arg = element; } public T get() { return element; } } class Derived1<T> extends GenericBase<T> { } class Derived2 extends GenericBase { } // No warning class Derived3 extends GenericBase<?> {} // Strange error: // unexpected type found : ? // required: class or interface without bounds public class ErasureAndInheritance { @SuppressWarnings("unchecked") public static void main(String[] args) { Derived2 d2 = new Derived2(); Object obj = d2.get(); d2.set(obj); // Warning here! } } // /:~
Derived2 inherits from GenericBase with no generic parameters, and the compiler doesn't issue a warning. The warning doesn't occur until set( ) is called.
To turn off the warning, Java provides an annotation, the one that you see in the listing (this annotation was not supported in earlier releases of Java SE5):
@SuppressWarnings("unchecked")
Notice that this is placed on the method that generates the warning, rather than the entire class. It's best to be as "focused" as possible when you turn off a warning, so that you don't accidentally cloak a real problem by turning off warnings too broadly.
Presumably, the error produced by Derived3 means that the compiler expects a raw base class.
Add to this the extra effort of managing bounds when you want to treat your type parameter as more than just an Object, and you have far more effort for much less payoff than you get in parameterized types in languages like C++,Ada or Eiffel. This is not to say that those languages in general buy you more
than Java does for the majority of programming problems, but rather that their parameterized type mechanisms are more flexible and powerful than Java's.
The action at the boundaries
Because of erasure, I find that the most confusing aspect of generics is the fact that you can represent things that have no meaning. For example:
//: generics/ArrayMaker.java import java.lang.reflect.Array; import java.util.Arrays; public class ArrayMaker<T> { private Class<T> kind; public ArrayMaker(Class<T> kind) { this.kind = kind; } @SuppressWarnings("unchecked") T[] create(int size) { return (T[]) Array.newInstance(kind, size); } public static void main(String[] args) { ArrayMaker<String> stringMaker = new ArrayMaker<String>(String.class); String[] stringArray = stringMaker.create(9); System.out.println(Arrays.toString(stringArray)); } } /* * Output: [null, null, null, null, null, null, null, null, null] */// :~
Even though kind is stored as Class<T>, erasure means that it is actually just being stored as a Class, with no parameter. So, when you do something with it, as in creating an array, Array.newlnstance( ) doesn't actually have the type information that's implied in kind; so it cannot produce the specific
result, which must therefore be cast, which produces a warning that you cannot satisfy.
If we create a container instead of an array, things are different:
//: generics/ListMaker.java import java.util.*; public class ListMaker<T> { List<T> create() { return new ArrayList<T>(); } public static void main(String[] args) { ListMaker<String> stringMaker= new ListMaker<String>(); List<String> stringList = stringMaker.create(); } } ///:~The compiler gives no warnings, even though we know (from erasure) that the <T> in new ArrayList<T>( ) inside create( ) is removed—at run time there's no <T> inside the class, so it seems meaningless. But if you follow this idea and change the expression to new ArrayList( ), the compiler gives a warning.
Is it really meaningless in this case? What if you were to put some objects in the list before returning it, like this:
//: generics/FilledListMaker.java import java.util.*; public class FilledListMaker<T> { List<T> create(T t, int n) { List<T> result = new ArrayList<T>(); for(int i = 0; i < n; i++) result.add(t); return result; } public static void main(String[] args) { FilledListMaker<String> stringMaker = new FilledListMaker<String>(); List<String> list = stringMaker.create("Hello", 4); System.out.println(list); } } /* Output: [Hello, Hello, Hello, Hello] *///:~Even though the compiler is unable to know anything about T inside create( ), it can still ensure—at compile time—that what you put into result is of type T, so that it agrees with ArrayList<T>. Thus, even though erasure removes the information about the actual type inside a method or class, the compiler can still ensure internal consistency in the way that the type is used within the method or class.
Because erasure removes type information in the body of a method, what matters at run time is the boundaries: the points where objects enter and leave a method. These are the points at which the compiler performs type checks at compile time, and inserts casting code. Consider the following non-
generic example:
//: generics/SimpleHolder.java public class SimpleHolder { private Object obj; public void set(Object obj) { this.obj = obj; } public Object get() { return obj; } public static void main(String[] args) { SimpleHolder holder = new SimpleHolder(); holder.set("Item"); String s = (String)holder.get(); } } ///:~If we decompile the result with javap -c SimpleHolder, we get (after editing):
The set( ) and get( ) methods simply store and produce the value, and the cast is checked at the point of the call to get( ).
Now incorporate generics into the above code:
//: generics/GenericHolder.java public class GenericHolder<T> { private T obj; public void set(T obj) { this.obj = obj; } public T get() { return obj; } public static void main(String[] args) { GenericHolder<String> holder = new GenericHolder<String>(); holder.set("Item"); String s = holder.get(); } } ///:~The need for the cast from get( ) has disappeared, but we also know that the value passed to set( ) is being type-checked at compile time. Here are the relevant bytecodes:
The resulting code is identical. The extra work of checking the incoming type in set( ) is free, because it is performed by the compiler. And the cast for the outgoing value of get( ) is still there, but it's no less than you'd have to do yourself—and it's automatically inserted by the compiler, so the code you write (and read) is less noisy.
Since get( ) and set( ) produce the same bytecodes, all the action in generics happens at the boundaries—the extra compile-time check for incoming values, and the inserted cast for outgoing values. It helps to counter the confusion of erasure to remember that "the boundaries are where the action takes place."
Compensating for erasure
As we've seen, erasure loses the ability to perform certain operations in generic code. Anything that requires the knowledge of the exact type at run time won't work:
//: generics/Erased.java // {CompileTimeError} (Won't compile) public class Erased<T> { private final int SIZE = 100; public void f(Object arg) { if (arg instanceof T) { } // Error T var = new T(); // Error T[] array = new T[SIZE]; // Error T[] array = (T) new Object[SIZE]; // Unchecked warning } } // /:~
Occasionally you can program around these issues, but sometimes you must compensate for erasure by introducing a type tag. This means you explicitly pass in the Class object for your type so that you can use it in type expressions.
For example, the attempt to use instanceof in the previous program fails because the type information has been erased. If you introduce a type tag, a dynamic islnstance( ) can be used instead:
//: generics/ClassTypeCapture.java class Building {} class House extends Building {} public class ClassTypeCapture<T> { Class<T> kind; public ClassTypeCapture(Class<T> kind) { this.kind = kind; } public boolean f(Object arg) { return kind.isInstance(arg); } public static void main(String[] args) { ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<Building>(Building.class); System.out.println(ctt1.f(new Building())); System.out.println(ctt1.f(new House())); ClassTypeCapture<House> ctt2 = new ClassTypeCapture<House>(House.class); System.out.println(ctt2.f(new Building())); System.out.println(ctt2.f(new House())); } } /* Output: true true false true *///:~
The compiler ensures that the type tag matches the generic argument.
Creating instances of types
The attempt to create a new T( ) in Erased.java won't work, partly because of erasure, and partly because the compiler cannot verify that T has a default (no-arg) constructor. But in C++ this operation is natural, straightforward, and safe (it's checked at compile time):
//: generics/InstantiateGenericType.cpp // C++, not Java! template<class T> class Foo { T x; // Create a field of type T T* y; // Pointer to T public: // Initialize the pointer: Foo() { y = new T(); } }; class Bar {}; int main() { Foo<Bar> fb; Foo<int> fi; // ... and it works with primitives } ///:~
The solution in Java is to pass in a factory object, and use that to make the new instance. A convenient factory object is just the Class object, so if you use a type tag, you can use newlnstance( ) to create a new object of that type:
//: generics/InstantiateGenericType.java import static net.mindview.util.Print.*; class ClassAsFactory<T> { T x; public ClassAsFactory(Class<T> kind) { try { x = kind.newInstance(); } catch(Exception e) { throw new RuntimeException(e); } } } class Employee {} public class InstantiateGenericType { public static void main(String[] args) { ClassAsFactory<Employee> fe = new ClassAsFactory<Employee>(Employee.class); print("ClassAsFactory<Employee> succeeded"); try { ClassAsFactory<Integer> fi = new ClassAsFactory<Integer>(Integer.class); } catch(Exception e) { print("ClassAsFactory<Integer> failed"); } } } /* Output: ClassAsFactory<Employee> succeeded ClassAsFactory<Integer> failed *///:~
This compiles, but fails with ClassAsFactory<Integer> because Integer has no default constructor. Because the error is not caught at compile time,this approach is frowned upon by the Sun folks. They suggest instead that you use an explicit factory and constrain the type so that it only takes a class that
implements this factory:
//: generics/FactoryConstraint.java interface FactoryI<T> { T create(); } class Foo2<T> { private T x; public <F extends FactoryI<T>> Foo2(F factory) { x = factory.create(); } // ... } class IntegerFactory implements FactoryI<Integer> { public Integer create() { return new Integer(0); } } class Widget { public static class Factory implements FactoryI<Widget> { public Widget create() { return new Widget(); } } } public class FactoryConstraint { public static void main(String[] args) { new Foo2<Integer>(new IntegerFactory()); new Foo2<Widget>(new Widget.Factory()); } } ///:~
Note that this is really just a variation of passing Class<T>. Both approaches pass factory objects; Class<T> happens to be the built-in factory object, whereas the above approach creates an explicit factory object. But you get compile-time checking.
Another approach is the Template Method design pattern. In the following example, get( ) is the Template Method, and create( ) is defined in the subclass to produce an object of that type:
//: generics/CreatorGeneric.java abstract class GenericWithCreate<T> { final T element; GenericWithCreate() { element = create(); } abstract T create(); } class X {} class Creator extends GenericWithCreate<X> { X create() { return new X(); } void f() { System.out.println(element.getClass().getSimpleName()); } } public class CreatorGeneric { public static void main(String[] args) { Creator c = new Creator(); c.f(); } } /* Output: X *///:~
Arrays of generics
As you saw in Erased.java, you can't create arrays of generics. The general solution is to use an ArrayList everywhere that you are tempted to create an array of generics:
//: generics/ListOfGenerics.java import java.util.*; public class ListOfGenerics<T> { private List<T> array = new ArrayList<T>(); public void add(T item) { array.add(item); } public T get(int index) { return array.get(index); } } ///:~
Here you get the behavior of an array but the compile-time type safety afforded by generics.
At times, you will still want to create an array of generic types (the ArrayList, for example, uses arrays internally). Interestingly enough, you can define a reference in a way that makes the compiler happy. For example:
//: generics/ArrayOfGenericReference.java class Generic<T> {} public class ArrayOfGenericReference { static Generic<Integer>[] gia; } ///:~
The compiler accepts this without producing warnings. But you can never create an array of that exact type (including the type parameters), so it's a little confusing. Since all arrays have the same structure (size of each array slot and array layout) regardless of the type they hold, it seems that you should be able to create an array of Object and cast that to the desired array type. This does in fact compile, but it won't run; it produces a ClassCastException:
//: generics/ArrayOfGeneric.java public class ArrayOfGeneric { static final int SIZE = 100; static Generic<Integer>[] gia; @SuppressWarnings("unchecked") public static void main(String[] args) { // Compiles; produces ClassCastException: // ! gia = (Generic<Integer>[])new Object[SIZE]; // Runtime type is the raw (erased) type: gia = (Generic<Integer>[]) new Generic[SIZE]; System.out.println(gia.getClass().getSimpleName()); gia[0] = new Generic<Integer>(); // ! gia[1] = new Object(); // Compile-time error // Discovers type mismatch at compile time: // ! gia[2] = new Generic<Double>(); } } /* * Output: Generic[] */// :~
The problem is that arrays keep track of their actual type, and that type is established at the point of creation of the array. So even though gia has been cast to a Generic < Integer >[], that information only exists at compile time (and without the @SuppressWarnings annotation, you'd get a warning for
that cast). At run time, it's still an array of Object, and that causes problems. The only way to successfully create an array of a generic type is to create a new array of the erased type, and cast that. Let's look at a slightly more sophisticated example. Consider a simple generic wrapper around an array:
//: generics/GenericArray.java public class GenericArray<T> { private T[] array; @SuppressWarnings("unchecked") public GenericArray(int sz) { array = (T[]) new Object[sz]; } public void put(int index, T item) { array[index] = item; } public T get(int index) { return array[index]; } // Method that exposes the underlying representation: public T[] rep() { return array; } public static void main(String[] args) { GenericArray<Integer> gai = new GenericArray<Integer>(10); // This causes a ClassCastException: // ! Integer[] ia = gai.rep(); // This is OK: Object[] oa = gai.rep(); } } // /:~
As before, we can't say T[] array = new T[sz], so we create an array of objects and cast it.
The rep( ) method returns a T[], which in main( ) should be an Integer[]
for gai, but if you call it and try to capture the result as an Integer []
reference, you get a ClassCastException, again because the actual runtime
type is Object[].
相关推荐
《Thinking in Java》是Bruce Eckel的经典之作,第四版涵盖了Java编程语言的广泛主题,适合初学者和有经验的程序员。这本书深入浅出地讲解了Java的核心概念和技术,旨在帮助读者建立坚实的编程基础,并理解面向对象...
《Thinking in Java》是Java编程领域的一本经典著作,由Bruce Eckel撰写,深受程序员喜爱。这本书分为第三版和第四版,提供了英文版和中文版,适合不同语言背景的学习者。书中内容详实且深入,从基础知识到高级概念...
很抱歉,但根据您给出的信息,这似乎是一个音乐文件列表,而非与"Thinking in Java"相关的IT知识内容。"Thinking in Java"是一本著名的编程书籍,通常与Java编程语言的学习和实践相关。如果您的目标是获取这方面的...
《Thinking in Java》是Bruce Eckel的经典编程教材,它深入浅出地介绍了Java语言的核心概念和技术。这本书通过实例代码来讲解理论,使读者能够更好地理解和掌握Java编程。在这个压缩包中,我们很可能会找到与书中的...
《Thinking in Java》是Bruce Eckel的经典编程教材,它涵盖了Java语言的核心概念和技术,深受程序员和初学者喜爱。这本书分为第三版和第四版,每个版本都有其独特的知识点和更新内容。 第三版是针对Java 2 Platform...
《Thinking in Java(英文原版第4版)》作为一本经典Java编程思想书籍,其内容涵盖了面向对象的叙述方式,并针对Java SE5/6版本新增了示例和章节。本书适合作为初学者的入门教材,同时也包含了足够的深度,适合专业...
《Thinking in Java》会详细解释如何使用这些集合以及迭代器(Iterator)和泛型(Generics)的概念,这些对于编写高效且可维护的代码至关重要。 多线程是并发编程的基础,Java提供了强大的支持。书中会讨论线程的...
### 《Thinking in Java》第四版关键知识点综述 #### 一、书籍概述与评价 《Thinking in Java》第四版是一本备受推崇的经典Java编程教材,由Bruce Eckel撰写,适用于初学者及进阶读者。本书自出版以来,便成为众多...
《Thinking in Java》是Bruce Eckel的经典之作,第四版更是深受全球Java开发者喜爱的教材。这本书深入浅出地讲解了Java编程语言的核心概念和技术,旨在为初学者提供全面且深入的Java学习指导。 首先,书中的第一章...
《Thinking in Java 4th Edition with Annotated Solution Guide》是一本经典的Java编程教材,由Bruce Eckel撰写。这本书深入浅出地介绍了Java编程的核心概念和技术,对于初学者和经验丰富的开发者来说,都是一个...
《Thinking in Java》是Java编程领域的一本经典之作,由Bruce Eckel撰写,深受程序员喜爱。这本书深入浅出地介绍了Java语言的核心概念和技术,旨在帮助读者建立起坚实的基础,并提升编程思维能力。书中不仅包含了...
《Thinking in Java 4th 习题答案》涵盖了多个关键的Java编程概念和技术,包括深入的容器理解、输入/输出(I/O)处理、枚举类型(Enumerated Types)、注解(Annotations)以及并发编程(Concurrency)。这些章节是Java学习...
《Thinking In Java》是Bruce Eckel的经典之作,它深入浅出地介绍了Java编程语言的核心概念和技术,被广大程序员视为学习Java的权威指南。第四版更是加入了更多现代Java特性,如Generics、Annotations等,使读者能够...
Java 泛型详解 Java 中的泛型是 Java 5(JDK 1.5)中引入的一项新特性,旨在解决类型安全和代码重用的问题。泛型允许程序员对类型进行抽象,使得代码更加灵活和可维护。 泛型的优点: 1. 类型安全:泛型可以在...
《Thinking in Java》是Bruce Eckel的经典著作,它在Java编程领域享有极高的声誉,是许多初学者和专业开发者的必备参考书籍。该书的第三版中文电子版PDF提供了全面而深入的Java学习资源,旨在帮助读者理解Java语言的...
本资料 "[Java泛型和集合].(Java.Generics.and.Collections).Maurice.Naftalin&Philip.Wadler.文字版" 由知名专家Maurice Naftalin和Philip Wadler编著,提供了关于这些主题的深入理解。 **Java泛型** 是自Java...