Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1798336
  • 博文数量: 438
  • 博客积分: 9799
  • 博客等级: 中将
  • 技术积分: 6092
  • 用 户 组: 普通用户
  • 注册时间: 2012-03-25 17:25
文章分类

全部博文(438)

文章存档

2019年(1)

2013年(8)

2012年(429)

分类: Java

2012-03-25 22:56:53

java里也有范型的概念,可以自定义范型类、范型接口、范型内部类和范型方法等:


  1. class MyGeneric<T> { public T value; }
  2. class MyOtherGeneric<A, B> { void f(A a, B b) {} }
  3. interface MyGenericInterface<T> { T getT(); }
  4. class NormalOuter {
  5.     class GenericInner<T> {}
  6.     public <T> void GenericMethod(T value) {}
  7. }

注意范型不能接受基本类型作为参数,但是可以使用包装类。比如不能使用MyGeneric但可以使用MyGeneric
java中的范型使用了一种“擦除”的机制,即范型只在编译期存在,编译器去除了(不是替代)范型参数,而java虚拟机根本不知道范型的存在。带有不同的范型参数的范型类在运行时都是同一个类:
  1. MyGeneric<Integer> mg1 = new MyGeneric<Integer>();
  2. MyGeneric<Double> mg2 = new MyGeneric<Double>();
  3. boolean b = mg1.getClass() == mg2.getClass(); // true;

因为范型类在运行时最终都会被擦除成普通的类,所以不能定义两个类名相同,而范型参数不同的类:
  1. class MyGeneric<T> { public T value; }
  2. // class MyGeneric { public T value; } // The type MyGeneric is already defined

更悲惨的是,在运行时是无法确切知道范型参数的实际类型。接着上面的代码:
  1. MyOtherGeneric<Integer, Double> mog = new MyOtherGeneric<Integer, Double>();
  2. TypeVariable<?>[] types = mog.getClass().getTypeParameters();
  3. System.out.println(Arrays.toString(types)); // [A, B]

getTypeParameters这个方法只是返回范型参数的占位符,不是具体的某个类型。

java的范型在定义时不是假定范型参数支持哪些方法,尽管C++是可以做到的。看下面的代码:


  1. class MyGeneric<T> {
  2.     // public void GuessT(T t) { t.f(); } // error: The method f() is undefined for the type T
  3. }

如果要让上面的代码编译通过,则需要使用到边界:
  1. class SupportT { void f() {} }
  2. class MyGeneric<T extends SupportT> {
  3.     public void GuessT(T t) { t.f(); }
  4. }

java中之所以用擦除的方式来实现范型,而不是像C++一样根据不同的范型参数具现化不同的类,是由于java语言在发布初期并不支持范型,在发布很长时间后才开始引入范型,为了很原来的类库兼容才使用了擦除。擦除会把参数T换成Object。

因为擦除,在范型类里是不能创建范型参数类型的对象,因为编译器不能保证T拥有默认构造器:


  1. class ClassMaker<T> {
  2.     T create() {
  3.         // return new T(); // error: Cannot instantiate the type T
  4.     }
  5. }

要创建一个范型对象,可以利用Class类型:
  1. class ClassMaker<T> {
  2.     ClassMaker(Class<T> c) { type = c; }
  3.     T create() {
  4.         T t = null;
  5.         try { t = type.newInstance(); }
  6.         catch (InstantiationException e) { e.printStackTrace(); }
  7.         catch (IllegalAcces***ception e) { e.printStackTrace(); }
  8.         return t;
  9.     }
  10.     private Class<T> type;
  11. }

同样,如果要创建范型数组的话:
  1. class ClassMaker<T> {
  2.     ClassMaker(Class<T> c) { type = c; }
  3.     T[] createArray(int size) {
  4.         // return new T[size]; // error: Cannot create a generic array of T
  5.         // return (T[]) new Object[size]; // success!
  6.         return (T[])Array.newInstance(type, size); // success!
  7.     }
  8.     private Class<T> type;
  9. }


范型的边界(extends)可以加入多个限定,但只能是一个类和多个接口:


  1. interface face1 {}
  2. interface face2 {}
  3. class class1 {}
  4. class Bound<T extends class1 & face1 & face2> {}

在子类还能加入更多的限定:
  1. interface face3 {}
  2. class BoundDerived<T extends class1 & face1 & face2 & face3> extends Bound<T> {}

试试把一个子类的数组赋给父类数组的引用:


  1. class Base1 {}
  2. class Derived1 extends Base1 {}
  3. Base1[] base = new Derived1[3];
  4. base[0] = new Base1(); // java.lang.ArrayStoreException

父类数组的引用实际指向一个子类的数组,当给元素赋值时,传入一个父类对象对得到一个异常,因为它期望的是一个子类的类型。
如果你希望在编译期时就发现这样的错误,而不是等到异常发生时才发现,可以使用范型:
  1. // ArrayList alb = new ArrayList(); 
  2. // Type mismatch: cannot convert from ArrayList to ArrayList

这样你在编译时就会发现这个错误。

有时候你可能就是希望一个合法的向上转型,这时可以使用通配符


  1. ArrayList<? extends Base1> aleb = new ArrayList<Derived1>(); // success!
  2. // aleb.add(new Base1()); 
  3. // error: The method add(capture#3-of ? extends Base1) in the type 
  4. // ArrayList is not applicable for the arguments (Object)
  5. // aleb.add(new Derived1()); error
  6. // aleb.add(new Object()); // error
  7. aleb.add(null); // success
  8. Base1 b = aleb.get(0); // success

可以发现带有通配符参数的范型类,它的所有带范型参数的方法调用都不能通过编译,比如上例中的ArrayList.add(T)。甚至连Object类型都不能接受,只能接受null。不带范型参数的方法可以调用,如ArrayList.get。这是因为编译器不知道应该接受什么类型,所以干脆就什么类型都不接受。如果你希望你的范型类在参数是通配符的时候,它的某些方法仍然能被调用,则定义方法的参数类型为Object,而非范型类型T。

与通配符相对的是超类型通配符,即
  1. ArrayList<? super Derived1> alsb = new ArrayList<Base1>();
  2. alsb.add(new Derived1()); //success
  3. // alsb.add(new Base1()); // error: 
  4. // The method add(capture#4-of ? super Derived1) in the type 
  5. // ArrayList is not applicable for the arguments (Base1)
  6. Object d = alsb.get(0); // return an Object

可以看到在接受参数时限制放宽了,因为编译器知道范型的下界,只要是Derived类或它的子类都是合法的。但是在返回时,它只能返回Object类型,因为它不能确定它的上界。

无界通配符,即,与原生类型(非范型类)大体相似,但仍有少许不同:
  1. ArrayList<?> al_any = new ArrayList<Base1>();
  2. // al_any.add(new Object()); // error:
  3. // The method add(capture#5-of ?) in the type 
  4. // ArrayList is not applicable for the arguments (Object)
  5. Object obj = al_any.get(0); // return an Object

  6. ArrayList al_raw = new ArrayList<Base1>();
  7. al_raw.add(new Object()); // warning: 
  8. // Type safety: The method add(Object) belongs to the raw type ArrayList. 
  9. // References to generic type ArrayList should be parameterized
  10. Object obj2 = al_raw.get(0);

调用add时,无界通配符范型不接受任何类型的参数,而原生类型可以接受Object类型但会发出一个警告。


有一种看起来很诡异的范型参数定义,即自限定的类型。它是这样的形式:


  1. class SelfBounded<T extends SelfBounded<T>> {
  2.     void f(T t) {}
  3. }

看起来很复杂,先把范型参数提出来研究下:T extends SelfBounded,它表明了一种类型,这种类型是SelfBounded的子类,而SelfBounded的范型参数恰恰就是这个类型本身。举个例子:
  1. class Derived2 extends SelfBounded<Derived2> {}

Derived2这个类就是以Derived2为参数的SelfBounded范型的子类,而SelfBounded类只能接受如Derived2这样特殊的类型作为范型参数。T extends SelfBounded其实是限定了一个继承关系,下面的定义是不合法的:

  1. class OtherType {}
  2. class Derived3 extends SelfBounded<OtherType> {} // error: 
  3.   // Bound mismatch: The type OtherType is not a valid substitute 
  4.   // for the bounded parameter > of the type SelfBounded

因为OtherType并不是SelfBounded的子类,所以不能作为SelfBounded的合法范型参数。

自限定类型一般用于参数协变。Java是支持返回值协变的:


  1. class BaseFace {
  2.     Base1 getObject() {
  3.         System.out.println("BaseFace");
  4.         return new Base1();
  5.     }
  6. }

  7. class DerivedFace extends BaseFace {
  8.     Derived1 getObject() {
  9.         System.out.println("DerivedFace");
  10.         return new Derive1();
  11.     }
  12. }

子类的同名方法覆盖了父类的方法,不是重载。下面的代码可以证明:
  1. void test(DerivedFace df) {
  2.     Derived1 d1 = df.getObject(); // output: DerivedFace
  3.     BaseFace bf = df;
  4.     Base1 b1 = bf.getObject(); // output: DerivedFace
  5. }

有趣的是,你可以调用DerivedFace.class.getMethods()看看,Derived类一共定义了两个方法,和普通的覆盖又有不同。

尽管支持返回值协变,Java并不支持参数协变。改写父类和子类的定义:


  1. class BaseFace {
  2. setObject(Base1 b) { 
  3. System.out.println("BaseFace"); 
  4. }
  5. }

  6. class DerivedFace extends BaseFace {
  7. void setObject(Derived1 d) {
  8. System.out.println("DerivedFace");
  9. }
  10. }

  11. void test(DerivedFace df) {
  12.     Derived1 d1 = new Derived1();
  13.     df.setObject(d1); // output: DerivedFace
  14.     df.setObject((Base1)d1); // output: BaseFace set
  15.     BaseFace bf = df;
  16.     bf.setObject(d1); // output: BaseFace
  17. }

自限定类型很好的支持了参数协变。
  1. class BaseFace2<T extends BaseFace2<T>> { 
  2. void setObject(T t) {} 
  3. }
  4. class DerivedFace2 extends BaseFace2<DerivedFace2> 
  5. {
  6. }
  7. void test(DerivedFace2 df, BaseFace2 bf) {
  8.     df.setObject(df); // Output: DerivedFace
  9.     // df.setObject(bf); // error: 
  10. // The method setObject(DerivedFace2) in the type BaseFace2 
  11. // is not applicable for the arguments (BaseFace2)
  12.     bf.setObject(bf); // Output: BaseFace
  13.     bf.setObject(df); // Output: BaseFace        
  14. }

可以看到自限定类型的子类只能接受子类作为参数,而且方法是被覆盖的,而不是重载。


Java容器的插入方法并不检查类型,所以很可能会插入错误的类型:


  1. List l_raw = new ArrayList<Integer>();
  2. l_raw.add(new Double(3)); // success

Collections提供了检查类型的方法,比如checkedList返回一个在插入时检查类型的List:
  1. List<Integer> l = Collections.checkedList(new ArrayList<Integer>(), Integer.class);
  2. List l_raw = l;
  3. l_raw.add(new Double(3)); // throws exception!

在checked容器里试图插入不合法的类型会抛出异常。


阅读(1549) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~