Thinking in java——Generics

The mystery of erasure

As you begin to delve more deeply into generics, there are a number of things that won't initially make sense. For example, although you can say ArrayList.class, you cannot say ArrayList<Integer>.class. And 

consider the following: 

 

//: 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);// True
	}
} /*
 * Output: true
 */// :~

ArrayList<String> and ArrayList<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 ArrayList<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()));
	}
}

 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. 

 

 

你可能感兴趣的:(Thinking in java——Generics)