java中的泛型(1)
本博客参考 << thinking in java >> 第15章泛型,记录整理我的学习笔记,结合了自己的思考和理解.
泛型使用
- 使用类型参数,用尖括号括住,放在类名/接口后面.如果有多个参数,只需要用逗号隔开即可.
public class Holder<T>{
private T a;
public void set(T a){ this.a = a;}
}
- 基本类型不能作为类型参数,但是java具备自动打包和自动拆包的功能,可以很方便地在基本类型和其相应的包装器类型之间的转换.
- 泛型方法:只需要将泛型参数列表置于返回值之前.例如
public <T> void f(T x){...}
,但是,是否拥有泛型方法和这个类是否是泛型没有关系.使用泛型类的时候,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指定参数类型,编译器会找出具体的类型.如果调用泛型参数时传入基本类型,那么自动打包机制就会介入. - 泛型方法的显式类型说明:在点操作符与方法名之间插入尖括号,然后把类型置于尖括号中,如果是定义在该方法的类的内部,必须在点操作符之前使用this关键字,如果是static方法,必须在点操作符之前加上类名.但一般在编写非赋值语句时才会使用到这种语法.
- 泛型还可以用于内部类和匿名内部类,假设有一个使用了泛型的类
Generator<T>
, 那么考虑一个方法返回一个Generator<Customer>
的对象:
public static Generator<Customer> G
generator(){
return new Generator<Customer>(){
public Customer next(){
return new Customer();
}
}
擦除
- 疑问引入:考虑一下代码:
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2); // output : true
发现输出居然为true!原来:在泛型代码内部,无法获得任何有关泛型参数类型的信息.java泛型是用擦除来实现的,这意味着在你是用泛型的时候,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象.故上面的两种类型都被擦除为”原生”类型.
- 由于擦除,下面的ObjectOne.java代码就无法编译通过:
ObjectOne.java
public class ObjectOne<T> {
private T obj;
ObjectOne(T obj){
this.obj = obj;
}
void test(){
obj.f(); // can't resolve method f()
}
public static void main(String[] args) {
ObjectTwo t = new ObjectTwo();
ObjectOne<ObjectTwo> o = new ObjectOne<>(t);
}
}
ObjectTwo.java
public class ObjectTwo {
void f(){
}
}
java无法将test()中能够在obj上调用f()这一需求映射到ObjectTwo中拥有f()的事实,为了调用f(),我们必须协助泛型类,给定泛型类的界限,我们需要使用extends关键字,将ObjectOne<T>
替换为ObjectOne<T extends ObjectTwo>
就可以了,这表明T必须具有类型ObjectTwo或者是从ObjectTwo导出的类型(同理,如果在泛型类中使用属性,例如T.attr
,那么这个也必须说明<T extends X>
, 其中X类中含有属性x,这个X也是相当于协助泛型类).但是你可以看出,这并没有使用到泛型的好处,要真是这样,为什么干脆把类中的T直接替换为ObjectTwo的类型呢?当然,如果这个类中是要返回一个T类型,那么泛型才能够派上用场,因为它可以返回一个确切是T类型的而不是T的导出类型.
3. 泛型类型只有在静态类型检查期间才会出现,在此之后,程序中的所有泛型类型都会被擦除,替换为他们的非泛型上界,例如LIst被擦除为List,而普通的类型变量在没有指定边界的情况下将会被擦除为Object.
擦除的补偿(针对java中泛型的限制的一些解决方法)
- 擦除丢失了在泛型代码中执行某些操作的能力,任何在运行时需要知道的确切类型信息的操作都将无法进行:
public class ObjectOne<T> {
private final int SIZE = 100;
public void f(Object obj){
if (obj instanceof T){ } // error
T var = new T(); // error
T[] array = new T[SIZE]; // error
T[] array2 = (T[]) new Object[SIZE]; // unchecked case
}
}
上面的实例中instanceof的尝试失败了,原因是它的类型信息已经被擦除了,如果引入类型标签,那么就可以动态使用isInstance()方法了.(即你需要将T换成Class<T> obj
, 引入一个obj对象)
public class ClassTypeCapture<T>{
Class<T> kind;
public ClassTypeCapture(Class<T> kind){
this.kind = kind;
}
public boolean f(Object arg){
return kind.isInstance(arg);
}
- 上述可以看到new T()将因为擦除而无法实现,而且编译器不能验证T具有默认的(无参)构造器,虽然在c++中这种操作是很自然和安全的.解决方法:传递一个Class类型的工厂对象,使用getConstrutor().newInstance()方法创建实例.
//c++ version : work perfectly
template<class T> class Foo{
T x;
T* y;
public:
Foo(){
y = new T();
}
};
class Bar(){};
int main(){
Foo<Bar> fb; // ok!
Foo<int> fi; // 即使是基本类型也没毛病
// java version
public class ClassAsFactory<T>{
T x;
public ClassAsFactory(Class<T> kind){
try{
x = kind.getConstructor().newInstance();
} catch (Exception e){
throw new RuntimeException();
}
}
}
但是上述java的代码还是存在问题,如果调用new ClassAsFactory<Integer>()
,会由于Integer没有默认的构造函数而失败,但这个错误不是在编译器捕获到的,sun建议使用显式的工厂,并限制其类型,使得只能够实现了这个工厂的类.(我的理解就是,你不能要生产什么类型的对象就把这个对象的Class对象扔过来,有可能这个对象像Integer一样没有默认的无参构造器,因此这个工厂只能够是一个接口的存在,你需要生产什么对象就要实现这个工厂接口,定义你自己的生成方法,所以你可以发现,其实你的对象在你定义的工厂中已经创建出来了)解决方法2:
public interface Factory<T> {
T create();
}
public class Foo2<T> {
private T x;
public <F extends Factory<T>> Foo2(F factory){
x = factory.create();
}
}
public class IntegerFactory implements Factory<Integer> {
@Override
public Integer create() {
return new Integer(0);
}
}
public class Widget {
public static class Factory implements general.Factory<Widget>{
@Override
public Widget create() {
return new Widget();
}
}
}
于是想要生成你的对象,只需要先写一个工厂类实现了Factory接口,实现你这个类的create方法,然后将这个工厂类传递给Foo2类,这个Foo2类就包含你想要生成的对象.(此时此刻我已经懵逼了,c++几行的东西在java下搞居然要这么麻烦).另外一种解决方法使用设计模式中的模板方法:
abstract class GenericWithCreate<T> {
final T element;
GenericWithCreate(){
element = create();
}
abstract T create();
}
class X{}
public class Creator extends GenericWithCreate<X> {
@Override
X create() {
return new X();
}
}
这种方法将create步骤分离出来了,使用一个指定类型的Creator来生成对象.
泛型数组
- 正如上面所看到的,不能创建泛型数组.一般的解决方法是使用ArrayList.
public class ListOfGenerics<T>{
private List<T> array = new ArrayList<T>;
public void add(T item){ array.add(item);}
public T get(int index){ array.get(index);}
}
- 先考虑如下代码:
public class ArrayOfGeneric {
static final int SIZE = 100;
static Generic<Integer>[] gia;
public static void main(String[] args) {
// gia = (Generic<Integer>[]) new Object[SIZE]; // uncheck cast. Produce classCastException.
// gia = new Generic<Integer>[SIZE]; error
gia = (Generic<Integer>[]) new Generic[SIZE]; // uncheck cast
System.out.println(gia.getClass().getSimpleName());
gia[0] = new Generic<>();
}
}
有一点让人疑惑,就是数组无论他们的持有类型如何,都具有相同的结构(数组槽位的尺寸和数组布局),看起来你可以通过创建一个Object数组并将其转型为希望的数组类型,事实上这可以编译但是不可以运行,会出现classCastException.这个问题在于数组将会跟踪他们的实际类型,而这个类型是在数组创建时确定的,在运行时,它们仍然是Object数组,而这将会引发问题.成功创建泛型数组的唯一方法是创建一个被擦除类型的新数组,然后对其转型,正如gia = (Generic<Integer>[]) new Generic[SIZE]这一句
还有一种实现方法,利用泛型数组包装器,而且最好在集合内部使用Object[]
public class GenericArray<T>{
private Object[] array;
public GenericArray(int sz){
array = new Object[sz];
}
public void put(int index, T item){
array[index] = item;
}
public T get(int index){ return (T)array[index];}
public T[] rep(){ return (T[]) array;} // warning : unchecked cast
现在内部表示是Object[]而不是T[],get()被调用时,它将对象转型为T,这实际上是正确的类型.但是如果你调用rep(),他还是尝试将Object[]转型为T[],这仍然是不正确的,将在编译器发生警告,在运行时发生异常.因此,没有任何方式可以推翻底层的数组类型,他只能是Object[]
对于新代码,应该传递一个类型标记:
public class GenericArrayWithToken <T>{
private T[] array;
public GenericArrayWithToken(Class<T> type, int sz){
array = (T[]) Array.newInstance(type, sz);
}
public void put(int index, T item){
array[index] = item;
}
public T get(int index){
return array[index];
}
public T[] rep(){
return array;
}
public static void main(String[] args) {
GenericArrayWithToken<Integer> gai = new GenericArrayWithToken<>(Integer.class, 10);
Integer[] ia = gai.rep();
ia[0] = 1;
System.out.println(ia[0]);
}
}
类型标记Class 被传递到构造器中,以便从擦除中恢复.
边界
- 边界使得你可以用于泛型的参数类型上设置限制条件.由于擦除移除了类型信息,所有用无边界泛型参数调用的方法只是那些可以用Object调用的方法,但如果能够将这个参数限制为某个类型子集,那么你就可以用这些类型子集来调用方法.
- 使用extends和super关键字进行参数类型的限制
<T extends ClassName>
表示类型是ClassName的类型或者是它的导出类型,super意义同样易得.如果需要extends/super多个ClassName,使用&
符号进行连接. - 通配符:
先展示数组的一种特殊行为:可以向导出类型的数组中赋予基类型的数组引用:
//definition
class Fruit{}
class Apple{}
class Joanthan extends Apple{}
class Orange extends Fruit{}
在考虑一下语句:
Fruit[] f = new Apple[10];
f[0] = new Apple();
f[1] = new Joanthan();
try {
f[0] = new Fruit();
} catch (Exception e} {System.out.println(e);}
try {
f[1] = new Orange()'
} catch (Exception e){System.out.println(e);}
结果是编译没有问题,但是运行起来两个try块都会抛出异常.原因也很简单:实际的数组类型是Apple[],你应该只能够在其中放置Apple或者Apple的子类,这在编译器或者是运行时都是没有问题的.但是编译器也允许你将Fruit或者它的导出类(如Orange)放入这个数组中,由于它是一个Fruit的引用.但是运行时的数组机制知道它处理的实际是Apple[],因此会在数组中放置异构类型时抛出异常.
然后,我们试图用泛型容器来代替泛型数组List<Fruit> f = new ArrayList<Apple>()
编译器就会报出错误,Apple的list根本就不是Fruit的list,即使Apple是Fruit的子类.
于是我们想引入通配符,List<? extends Fruit> f = new ArrayList<Apple>
,但是注意,这实际上并不意味着可以持有任何类型的Fruit,通配符引用的是明确的类型,但是现在你并不知道它的实际类型,既然如此,那就不能安全地向其中添加对象,所以现在你连Apple,Fruit也加不进去.但是如果你创建了一个Holder<Apple>
,虽然不可以向上转型为Holder<Fruit>
,但是可以向上转型为Holder<? extends Fruit?
.如果调用get(),它就会返回一个Fruit.
public class Holder<T>{
private T value;
public Holder(){}
public Holder(T val){ value = val;}
public void set(T val){ value = val;}
public T get() { return value;}
public boolean equals(Object obj){ return value.equals(obj);}
public static void main(String[] args){
Holder<Apple> apple = new Holder<Apple>(new Apple());
Holder<? extends Fruit> fruit = apple; // ok
Fruit p = fruit.get();
Apple d = (Apple) fruit.get();
}
}
- 使用超类型通配符super,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定
<? extends MyClass>
.
List<? super Apple> a = new ArrayList<>();
a.add(new Apple());
a.add(new LittleApple()); // littleApple为Apple的子类
//a.add(new Fruit()); error
a是Apple的某种基类型的list,那么可以知道向其中添加Apple或者LittleApple是安全的,但是添加Fruit是不安全的,因为你无法确定Fruit就是这个基类或者这个基类的导出类.
5. 先看书本上一段代码:
public class GeneralReading {
static <T> T readExact(List<T> list){
return list.get(0);
}
static List<Apple> apples = Arrays.asList(new Apple());
static List<Fruit> fruits = Arrays.asList(new Fruit());
static void f1(){
Apple a = readExact(apples);
Fruit f = readExact(fruits);
f = readExact(apples);
}
static class Reader<T>{
T readExact(List<T> list){
return list.get(0);
}
}
static void f2(){
Reader<Fruit> fruitReader = new Reader<>();
Fruit f = fruitReader.readExact(fruits);
//Fruit a = fruitReader.readExact(apples); error
}
static class CovariantReader<T>{
T readCovariant(List<? extends T> list){
return list.get(0);
}
}
static void f3(){
CovariantReader<Fruit> fruitCovariantReader = new CovariantReader<>();
Fruit f = fruitCovariantReader.readCovariant(fruits);
Fruit a = fruitCovariantReader.readCovariant(apples);
}
}
可以看到:①f1()是一个静态泛型方法,它的返回值可以有效适应每一个方法调用.②Reader是一个泛型类,创建这个类的实例时要为这个类确定参数,f2()中创建的时候确定的参数是Fruit,虽然传入的List<Apple>
可以产生Fruit,但是FruitReader不允许这么做.③CovariantReader类的方法将接受List<? extends T>
,因此在列表中读取一个T就是安全的.
下面先本人根据理解稍微总结一下
6. (add)List,List<?>,List<? extends Object>
:①首先要明确,正如之前所提到的,’?’并不代表容器可以容纳任意类型,它是确定的,只是我们不知道它的准确类型(或者说不知道容器的下界).②本人实践得出,它们三者是可以相互赋值的,而且它们的get方法都是正常运行的.③但是,正如前面所提到,既然涉及’?’的我们都不知道它的具体类型信息(或者说它的下界),所以List<?>,List<? extends Object>
都不可以add任何的对象,但是List相当于List<Object>
,它可以添加任何的对象.(相反,由于List<? super ClassType>
我们知道它的下界为ClassType,那么它至少可以添加ClassType类型或者ClassType的子类型)
7. (get)对于List,List<?>,List<? super ClassType>
,我们都不知道容器的上界是什么类型的,所以调用get方法返回值为Object;而对于List<ClassType>,List<? extends ClassType>
,我们都清楚容器的上界,所以调用get方法返回值为ClassType.