Chinaunix首页 | 论坛 | 博客
  • 博客访问: 30480281
  • 博文数量: 708
  • 博客积分: 12163
  • 博客等级: 上将
  • 技术积分: 8240
  • 用 户 组: 普通用户
  • 注册时间: 2007-12-04 20:59
文章分类

全部博文(708)

分类: Java

2008-10-09 15:30:09

对于如 List List< String > List ,其中 List 称为 parameterized type E 称为 (formal) type parameter String 称为 actual type argument List 称为 raw type

Generic 的逻辑意义

原有 java 的类型系统

Generic java 5 带来了新的类型,这使得 java 中的类型关系变得更加复杂,要弄清楚加入了 generic 后的类型关系就需要先弄清楚原先 java 中的类型系统。

首先,在类型的定义上,类之间不允许多重继承,类可以实现多个接口。

其次,在类型的使用上,每个变量都必须有明确的类型,变量只能指向相应类型(或相应类型的子类型)的对象,为了实现这一规则, compile time 会对所有的赋值操作做检测,而 runtime 则对所有的显示类型转换做检测。

最后,数组作为 java 中一个特殊的类型,如果 B extends A ,那么 B[] extends A[] ,当对数组 element 赋值时, runtime 会做检测,类型不符则抛 ArrayStoreException ,如下。由于有多重数组的出现,意味着 java 的类型系统种有无限种类型。

// B extends A

A a = new A();

A[] array = new B[1];

Array[0] = a; // ArrayStoreException

我认为理想状态下的 generic

首先,假设有 B extends A ,那么 B extends A B extends B ,并且 runtime 对使用到 parameter type 的输入参数做类型检测。这跟原先 java 类型系统中的 array 是一致的。与数组相同的还有,因为有如 B> B< B>> 等等类型的存在, generic 也可以无限增加可用类型。

其次,当 generic 跟继承连用时,(在不考虑接口的情况下)有三种新的形式: B extends A B extends A B extends A ,其中第三种情况意味着有 B extend A

现实中的 generic

       事实上,在 java 5 中,对于 B extends A B B 之间并不存在继承关系( invariant subtyping ),这跟数组( covariant subtyping )不同。之所以使用这种做法,我想有以下原因:

首先, java 5 compiler 使用 erasure 来支持 generic ,所有与 generic 相关的信息都不存在于 runtime (见下文中“ generic 的实现”),这就意味着 runtime 无法做如下的类型检测,而即便 runtime 有条件做类型检测,也势必影响代码的执行效率。

ArrayList strList = new ArrayList();

ArrayList objList = strList;

objList.add(new Object()); // runtime could not throw exception

其次,考虑下面的例子, B extends A ,有 B extends A ,如果使用 covariant subtyping ,又有 B extends B ,这意味着存在多重继承,而多重继承在 java 里面是不被允许的。值得注意的是,尽管数组使用 covariant subtyping ,但却不会导致多重继承,因为数组属于系统类型, java 并不允许数组被继承。

采用了 invariant subtyping 之后,假如有 A ,由于 A 不再是其他类型 A A 等类型的父类, 则无法声明可以指向所有 A 类型对象的变量。为了解决这一问题, java 1.5 引入了 wildcard ,声明为 A 类型的变量可以指向所有 A 类型的对象。需要注意的是, wildcard 跟继承是两种不同的关系,继承使类型间呈现树状的关系,类型为 B 的变量可以指向的对象类型必须在以 B 为根节点的子树中,而类型为 A 的变量可以指向的对象类型必须为类型树中 A 或与 A 平行的节点。最后, wildcard 跟继承结合使得 A 类型变量能够指向的对象类型必须在以 A A 平行的节点为根的所有子树中。

// A extends Object, B extends Object, C extends B, D extends B

A a; // instances of A, A, A can be assigned to this variable

B b; // instance of B, C, D can be assigned to this variable

Generic 的实现

加入了 generic java type safe

       保证 type safe ,其实就关键在于确保所有变量所指向的对象的类型必须是正确的。我认为在理想状态下,应该实现以下几点:首先,类型为 A 的变量所能指向的对象类型必须在以 A 为根节点的子树中;其次,类型为 wildcard 的变量,如 A ,所能指向的对象类型必须在以 A A 平行的节点为根的所有子树中;最后,所有的显式转换在 runtime 必须做类型判定。其中,前两点由 compiler 实现,最后一点由 jvm 实现,然而事实上, java 5 仅实现了前两点,而决定不在 runtime 做检测。

       Compile time generic type safe 主要包括 generic class generic method type safe ,以下分开讨论。

Generic class type safe

       假设有以下的类:

public class A {};

public class B extends A {

public T obj;

}

public class C extends B {

public void set(T obj) { this. obj = obj; }

public T get() { return obj; }

}

对于类型为 C 的对象,能够指向它的变量的类型有: A B C B C 。对于类型为 A 的变量,通过该变量无法访问到任何与 T 相关的方法或对象变量,很显然在原有 java type safe 机制仍然有效;对于类型为 B C 的变量, compiler 对所有通过该变量所访问的方法( set get )或对象变量 (obj) 进行检测,所有涉及到 T 的赋值都必须满足 T=String ,则 type safe 得以保证。对于类型为 B C 的变量,通过该变量所访问的方法或对象变量,所有的输出值中 T 类型被替换成 T bound (见下文中“ type parameter 的限制”),所有输入值中由于 T 类型未知,所以不能接受任何变量赋值( null 除外)。在理想状态下,输入值中 T 类型应该也被替换成 T bound ,然后由 runtime 去做类型判定,但是由于 runtime 没有 generic 相关的任何信息

C strC = new C();

C c = strC;

// even if the following code pass compile time check, runtime could not throw exception

c.obj = new Object();

c.set(new Object());

// here’s a unexpected exception

String str = strC.obj;

str = strC.get();

generic class 的所有方法中, T 的类型被认为是其 bound 或者 bound 的某个子类。也就是说,首先, T 的变量只能指向类型为 T T 的子类的对象;其次,通过 T 的变量只能访问到其 bound 的方法和对象变量。假设以下代码存在于 C set 方法中:

public void set(T obj;) {

Object temp;

temp = obj; // ok

obj = temp; // ompile error

obj.toString(); // can access Object’s methods

}

Generic method type safe

Generic class 不同的是,在 generic method 中, actual type argument 并非指定的,而是由 compiler 推断出的( Inference )。 Compiler 通过对 generic method 中的输入变量的类型推断 type parameter 的类型,如果不能够得到一个 unique smallest type ,则被视为 compile error ,参考以下代码:

public void doublet(A a, A b) {};

// compile error, because String and Integer have both Comparable and Serializable as common supertypes

doublet(“abc”, 123);

wildcard generic method 同时使用时,有以下的特例:

public List test(List list) { return list; }

List wildcardList = new ArrayList();

wildcardList = test(wildcardList);

最后, generic method 中对 type parameter 的使用所必须遵循的规则跟上面所提到的 generic class 的方法中的规则是一样的。

Erasure 的实现方式

Java 5 compiler 中采用 erasure 来实现 generic ,经过 erasure 的处理,所有与 generic 相关的信息将被抹掉( erase ),同时在适当的位置插入显式类型转换,最终形成的 byte code java1.4 byte code 没有什么不一样。

首先, parameterized type ,被还原成其 non-parameterized type ,如 List 将变成 List

其次, type parameter 被替换成它的 bound ,如 T 将变成 Object (假如它的 upper bound Object )。

接着,对于方法类成员的返回值,如果其类型为 parameter type erasure 则会插入显式转换。如:

public class A {

public T get() { return null; }

}

A a = new A();

String temp = a.get();

// translate to

public class A {

public Object get() { return null; }

}

A a = new A();

String temp = (String) a.get();

最后 erasure 将在必要的时候插入 bridge method 。对于以下的代码

public class A {

private T obj;

public void set(T obj) { this.obj = obj; }

public T get() { return obj; }

}

public class B extends A {

public void set(String obj) {};

public String get() { return null;}

}

A a = new B();

a.set(“abc”);

String temp = a.get();

在没有 bridge method 存在的情况下,对于 a 的方法的调用将无法获得多态性的支持,原因是 B 中的方法的 signature A 的不同,所以不被 jvm 视为重载。这时候 erasure 必须在 B 中插入如下的 bridge method

public void set(Object obj) { set((String) obj);}

public Object get() { return get(); }

需要注意的是 get bridge method 在是编译不过的,因为 java 不允许这种形式的 overload ,事实上, bridge method 是直接在 byte code 中插入的。

最后值得注意的是, bridge method 只有在需要的时候被插入,如果 B 不重载 get set 方法,将不会有 bridge method 存在。

由于 runtime 缺乏 generic 相关的信息而导致的各种限制

1.       通过 wildcard 类型的变量访问方法及对象变量受到限制(如上文所述)。

2.       type parameter 相关的显式转化无法保证 type safe ,同时 compiler 会有 warning

List list = new ArrayList();

List strList = (List) list; // warning

public T test1(Object obj) { return (T) obj; } // warning

public T[] test2(Object[] objs) { return (T[]) objs; } // warning

3.       在创建某些类型对象时受到限制。

public T test1(T sample) { return new T(); } // compile error

public T[] test2(T sample) { return new T[0]; } // compile error

值得注意的是,即便提供了 actual type argument ,依然无法创建 parameterized type 的数组:

// compile error, but assumes that compiler allow to create such kind of array

List[] lists = new List[1];

List intList = new List();

intList.add(1);

Object[] objs = lists;

objs[0] = intList; // runtime could not throw an ArrayStoreException for this

String temp = lists[0].get(0); // unexpected error

通过 Class 能够创建 T 的对象, Class 的奥妙在于,一方面它能够通过 compiler 的检测,另一方面,它本身携带的信息也足以让 runtime 得以创建 T 的对象。

public T create(Class c) { return c.newInstance(); };

String temp = create(String.class);

4.       不得不插入 bridge method (如上文所述)。

5.       使用 instanceof 时受到限制。

List list = new ArrayList();

boolean temp;

temp = list instanceof List; // compile error

temp = list instanceof List; // ok

6.       使用 reflection 时存在安全隐患。

public class A {

public T obj;

}

public class B {

public A a;

}

A a = new A();

B b = new B();

B.class.getField(“a”).set(a); // everything ok

String temp = b.a.obj; // unexpected error

7.       Generic class 中的 type parameter 在其静态方法及静态变量中无法使用。

generic 中对 type parameter 的限制

使用 extends 关键字

对于如 A generic class ,可以使用 extends 来进一步限制 T 所能代表的类型。如:

public class A { … }

A objA; // compile error

A strA; // compile error

A numA; // ok

A intA; // ok

这里, extends 意味着 T 必须是 Number 或者 Number 的子类,以下是对于 extends 更为复杂的使用。

public class B> {…}

public class C extends B { … }

public class D extends C { … }

C c = new C();

D d = new D();

B bc; // ok

bc = c;

bc = d;

B bd; // compile error

B b; // compile error

这里,显然 B 是非法的,对于 B ,虽然 D 继承了 C ,但是把 D 替换到“ T extends B ”中,显然“ D extends B ”不成立,所以 B 也是非法的,与此类似的是 java.lang 里面的“ Enum> ”,这一声明保证了,假设有类 Test ,它不是 Enum 类型(编译器保证只有使用 enum 关键字创建时才能满足 Enum 类中对 T 的限制),那么无法声明 Enum 类型的变量。

public class E { … }

E e1; // ok

E e2; // ok

E e3; // compile error

这里, extends 用来限制不同的 type parameter(T S) 之间的关系。

最后,需要注意的是,在 generic class 里面通过使用 extends type parameter 进行限制所导致的结果是删除了一部分类型(如上述的 E ),而并非阻止这一类型的对象的创建,而在 generic method 里面使用 extends 则在于阻止某些类型的对象作为输入参数,如:

public void test(List list) {}

test(new ArrayList()); // ok

test(new ArrayList()); // compile error

关于 wildcard – “?”

在不使用 super 关键字的时候, ”?” 可以理解成 parameter type 的匿名形式,事实上,这些 ”?” 都可以转变成用 parameter type 表示。

public void test(List list) {}

public void test(List list) {}

“?” 只能作为 parameterized type actual type argument 使用,同时由于 ”?” 是匿名的形式,编译器并不会认为出现两次的 A 要求相同的 type parameter

public void test(? obj) {} // compile error

public void test1(List list1, List list2) {}

public void test2(List list1, List list2) {}

test1(new ArrayList(), new ArrayList()); // compile error

test2(new ArrayList(), new ArrayList()); // ok

使用 super 关键字

对于 T super A super 表示 T 必须是 A 或者 A 的父类,相比起 extends ,对 super 的使用有更多的限制。考虑以下代码:

public void test(T obj) {} // assumes that there’s no compile error

test(new Object()); // this looks reasonable

test(1); // Integer is not super class of Number, so compiler should reject this, but Integer is also an object, why would a method accept object as valid argument but not integer?

可以看到, super 限制对象类型必须是某个类或其父类,而继承则允许父类的变量接受子类的对象,这两者是互相抵触的,所以对 super 的时候有以下的限制:

首先, super 只能在 parameterized type actual type argument 中作为限制条件使用,在这个时候,它并不和继承相抵触。如:

public void test(List list) {}

其次, super 不能用于限制非匿名的 parameter type ,显然如果可以这样的话,就会出现上述代码中的错误。这就决定了 super 只能与 ”?” 连用。

Generic 的向前兼容

下面以 List 为例,为了向前兼容原先的代码,允许使用 List 这样的 raw type 。在语义上, List List 是一致的,然而在语法上 List List 更为类似,并且编译器允许原先在 List 使用原先在 List 上禁止的某些操作(对于 List compile error 的操作在 List 上仅仅是 warning )。

首先,在类型转换上, List List 是等价的, List 类型可以赋给任何指定了 actual type argument parameterized type (如 List ),而 List 则不行(除非使用显式转换)。

List list;

List wildcardList;

List strList;

 

list = wildcardList;

list = strList;

wildcardList = list;

wildcardList = strList;

 

strList = list; // warning

strList = wildcardList; // compile error

其次, compiler 不允许通过 List 访问任何输入参数与 E 相关的方法,而 List 则仅给出 wanring

list.add(“abc”); // waring

wildcardList.add(“abc”); // compile error

其他

关于 GJ

GJ 是使 java 支持 generic 的一个开源项目, java 5 generic 是参照 GJ 实现的, GJ 的语法以及实现方式基本上跟目前 java 5 generic 相同,当然在某些细节方面 java 5 generic 做了改动,研究 GJ 有利于更好的理解 java 5 generic

Security Implications

考虑以下的代码:

public class SecureChannel extends Channel {

public String read();

}

public class A {

public LinkedList cs;

}

由于 LinkedList runtime 会退化成 LinkedList ,恶意的代码很容易在 cs 里面放入其他类型的 Channel GJ 中建议使用 type specialization 来解决这一问题,但是由于 java 5 generic 仅在需要的时候插入 bridge 方法,所以这一方法在 java 5 中是无效的。

在方法参数中使用 extends super 关键字的技巧

考虑如下代码:

public class A {

private T obj;

public void set(T obj) { this.obj = obj; }

public T get() { return obj; }

}

public class Test {

public void test(A a) { … }

}

A a = new A();

A numA = new A();

A intA = new A();

Test test = new Test();

test.test(a); // compile error (1)

test.test(numA); // ok

test.test(intA); // compile error (2)

对于 test 方法,如果方法内仅需要调用 A set 方法(即仅需要用到输入值为 T 类型的方法,注意,这不包括如 List 这种类新),使用 A 代替 A 可能会更为合适,这使得 (1) 得以编译通过,由于仅需要调用 A set 方法, A.set(Object) 显然比 A.set(Number) 允许更多的类型,从而使 A 可以替换 A 。相似的,如果 test 方法内仅需要调用 A get 方法,则使用 A 代替 A 可能会更合适,这使得 (2) 得以编译通过。

关于 type parameter 的命名规范

推荐使用精简同时有意义的名称,如 E for element T for type (最好是单个字母),同时避免使用任何小写字母以使得 type parameter 能够从一般的类还有接口名称中被区分出来。如果需要同时使用多个 type parameter ,则考虑使用邻近的几个不同字母,如 T S 。如果在某个类中已经使用了某个字母作为 type parameter ,则在其 generic method 以及 nested class 中避免使用同样的字母。

Generic method 采用 inference 所产生的问题

public interface I {}

public class A implements I {}

public class B{}

public void test(T a, T b);

test(new A(), new B()); // ok

当代码改动 B 也需要实现接口 I 的时候:

public class B implements I {}

test(new A(), new B()); // compile error

仍然搞不懂的地方

对于类似 java.util.Collections max 方法,经过我的试验以下两种声明方式所能接受的类型是一样的,不明白它为什么要用前者。

public static > T max1(Collection coll)

public static > T max2(Collection coll)

参考资料

GJ- Making the future safe for the past: Adding Genericity to the JavaTM Programming Language

Generics in the Java Programming Language

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