对Java Generic相关知识的总结

来源:互联网 发布:http 127.0.0.1 mac 编辑:程序博客网 时间:2024/05/16 15:15
 
对于如 List<E> 、 List< String > 、 List ,其中 List<E> 称为 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<T> extends A ,那么 B<Object> extends A 、 B<String> extends B<Object> ,并且 runtime 对使用到 parameter type 的输入参数做类型检测。这跟原先 java 类型系统中的 array 是一致的。与数组相同的还有,因为有如 B<B<String>> 、 B< B<B<String>>> 等等类型的存在, generic 也可以无限增加可用类型。
其次,当 generic 跟继承连用时,(在不考虑接口的情况下)有三种新的形式: B<T> extends A 、 B extends A<String> 、 B<T> extends A<T> ,其中第三种情况意味着有 B<String> extend A<String> 。
现实中的 generic
       事实上,在 java 5 中,对于 B<T> extends A , B<Object> 跟 B<String> 之间并不存在继承关系( invariant subtyping ),这跟数组( covariant subtyping )不同。之所以使用这种做法,我想有以下原因:
首先, java 5 compiler 使用 erasure 来支持 generic ,所有与 generic 相关的信息都不存在于 runtime (见下文中“ generic 的实现”),这就意味着 runtime 无法做如下的类型检测,而即便 runtime 有条件做类型检测,也势必影响代码的执行效率。
ArrayList<String> strList = new ArrayList<String>();
ArrayList<Object> objList = strList;
objList.add(new Object()); // runtime could not throw exception
其次,考虑下面的例子, B<T> extends A<T> ,有 B<String> extends A<String> ,如果使用 covariant subtyping ,又有 B<String> extends B<Object> ,这意味着存在多重继承,而多重继承在 java 里面是不被允许的。值得注意的是,尽管数组使用 covariant subtyping ,但却不会导致多重继承,因为数组属于系统类型, java 并不允许数组被继承。
采用了 invariant subtyping 之后,假如有 A<T> ,由于 A<Object> 不再是其他类型 A<String> 、 A<Integer> 等类型的父类, 则无法声明可以指向所有 A<T> 类型对象的变量。为了解决这一问题, java 1.5 引入了 wildcard ,声明为 A<?> 类型的变量可以指向所有 A<T> 类型的对象。需要注意的是, wildcard 跟继承是两种不同的关系,继承使类型间呈现树状的关系,类型为 B 的变量可以指向的对象类型必须在以 B 为根节点的子树中,而类型为 A<?> 的变量可以指向的对象类型必须为类型树中 A<Object> 或与 A<Object> 平行的节点。最后, wildcard 跟继承结合使得 A<?> 类型变量能够指向的对象类型必须在以 A<Object> 及 A<Object> 平行的节点为根的所有子树中。
// A<T> extends Object, B extends Object, C extends B, D extends B
A<?> a; // instances of A<Object>, A<String>, A<Integer> 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<Object> 及 A<Object> 平行的节点为根的所有子树中;最后,所有的显式转换在 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<T> extends A {
public T obj;
}
public class C<T> extends B<T> {
public void set(T obj) { this. obj = obj; }
public T get() { return obj; }
}
对于类型为 C<String> 的对象,能够指向它的变量的类型有: A 、 B<String> 、 C<String> 、 B<?> 、 C<?> 。对于类型为 A 的变量,通过该变量无法访问到任何与 T 相关的方法或对象变量,很显然在原有 java 的 type safe 机制仍然有效;对于类型为 B<String> 、 C<String> 的变量, 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<String> strC = new C<String>();
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 <A> 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 <T> List<T> test(List<T> list) { return list; }
List<?> wildcardList = new ArrayList<String>();
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<String> 将变成 List 。
其次, type parameter 被替换成它的 bound ,如 T 将变成 Object (假如它的 upper bound 是 Object )。
接着,对于方法类成员的返回值,如果其类型为 parameter type , erasure 则会插入显式转换。如:
public class A<T> {
public T get() { return null; }
}
A<String> a = new A<String>();
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<T> {
private T obj;
public void set(T obj) { this.obj = obj; }
public T get() { return obj; }
}
public class B extends A<String> {
public void set(String obj) {};
public String get() { return null;}
}
A<String> 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<String>();
List<String> strList = (List<String>) list; // warning
public <T> T test1(Object obj) { return (T) obj; } // warning
public <T> T[] test2(Object[] objs) { return (T[]) objs; } // warning
3.       在创建某些类型对象时受到限制。
public <T> T test1(T sample) { return new T(); } // compile error
public <T> 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<String>[] lists = new List<String>[1];
List<Integer> intList = new List<Integer>();
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> 能够创建 T 的对象, Class<T> 的奥妙在于,一方面它能够通过 compiler 的检测,另一方面,它本身携带的信息也足以让 runtime 得以创建 T 的对象。
public T create(Class<T> c) { return c.newInstance(); };
String temp = create(String.class);
4.       不得不插入 bridge method (如上文所述)。
5.       使用 instanceof 时受到限制。
List<String> list = new ArrayList<String>();
boolean temp;
temp = list instanceof List<String>; // compile error
temp = list instanceof List<?>; // ok
6.       使用 reflection 时存在安全隐患。
public class A<T> {
public T obj;
}
public class B {
public A<String> a;
}
A<Integer> a = new A<Integer>();
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<T> 的 generic class ,可以使用 extends 来进一步限制 T 所能代表的类型。如:
public class A<T extends Number> { … }
A<Object> objA; // compile error
A<String> strA; // compile error
A<Number> numA; // ok
A<Integer> intA; // ok
这里, extends 意味着 T 必须是 Number 或者 Number 的子类,以下是对于 extends 更为复杂的使用。
public class B<T extends B<T>> {…}
public class C extends B<C> { … }
public class D extends C { … }
C c = new C();
D d = new D();
B<C> bc; // ok
bc = c;
bc = d;
B<D> bd; // compile error
B<Object> b; // compile error
这里,显然 B<Object> 是非法的,对于 B<D> ,虽然 D 继承了 C ,但是把 D 替换到“ T extends B<T> ”中,显然“ D extends B<D> ”不成立,所以 B<D> 也是非法的,与此类似的是 java.lang 里面的“ Enum<E extends Enum<E>> ”,这一声明保证了,假设有类 Test ,它不是 Enum 类型(编译器保证只有使用 enum 关键字创建时才能满足 Enum 类中对 T 的限制),那么无法声明 Enum<Test> 类型的变量。
public class E<T, S extends T> { … }
E<Number, Number> e1; // ok
E<Number, Integer> e2; // ok
E<Number, String> e3; // compile error
这里, extends 用来限制不同的 type parameter(T 、 S) 之间的关系。
最后,需要注意的是,在 generic class 里面通过使用 extends 对 type parameter 进行限制所导致的结果是删除了一部分类型(如上述的 E<Number,String> ),而并非阻止这一类型的对象的创建,而在 generic method 里面使用 extends 则在于阻止某些类型的对象作为输入参数,如:
public <T extends String> void test(List<T> list) {}
test(new ArrayList<String>()); // ok
test(new ArrayList<Object>()); // compile error
关于 wildcard – “?”
在不使用 super 关键字的时候, ”?” 可以理解成 parameter type 的匿名形式,事实上,这些 ”?” 都可以转变成用 parameter type 表示。
public void test(List<? extends String> list) {}
public <T extends String> void test(List<T> list) {}
“?” 只能作为 parameterized type 的 actual type argument 使用,同时由于 ”?” 是匿名的形式,编译器并不会认为出现两次的 A<?> 要求相同的 type parameter 。
public void test(? obj) {} // compile error
public <T> void test1(List<T> list1, List<T> list2) {}
public void test2(List<?> list1, List<?> list2) {}
test1(new ArrayList<Object>(), new ArrayList<String>()); // compile error
test2(new ArrayList<Object>(), new ArrayList<String>()); // ok
使用 super 关键字
对于 T super A , super 表示 T 必须是 A 或者 A 的父类,相比起 extends ,对 super 的使用有更多的限制。考虑以下代码:
public <T super Number> 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<? super Number> list) {}
其次, super 不能用于限制非匿名的 parameter type ,显然如果可以这样的话,就会出现上述代码中的错误。这就决定了 super 只能与 ”?” 连用。
Generic 的向前兼容
下面以 List<E> 为例,为了向前兼容原先的代码,允许使用 List 这样的 raw type 。在语义上, List 跟 List<Object> 是一致的,然而在语法上 List 跟 List<?> 更为类似,并且编译器允许原先在 List 使用原先在 List<?> 上禁止的某些操作(对于 List<?> 为 compile error 的操作在 List 上仅仅是 warning )。
首先,在类型转换上, List 跟 List<?> 是等价的, List 类型可以赋给任何指定了 actual type argument 的 parameterized type (如 List<String> ),而 List<?> 则不行(除非使用显式转换)。
List list;
List<?> wildcardList;
List<String> 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<SecureChannel> cs;
}
由于 LinkedList<SecureChannel> 在 runtime 会退化成 LinkedList ,恶意的代码很容易在 cs 里面放入其他类型的 Channel 。 GJ 中建议使用 type specialization 来解决这一问题,但是由于 java 5 的 generic 仅在需要的时候插入 bridge 方法,所以这一方法在 java 5 中是无效的。
在方法参数中使用 extends 、 super 关键字的技巧
考虑如下代码:
public class A<T> {
private T obj;
public void set(T obj) { this.obj = obj; }
public T get() { return obj; }
}
public class Test<T> {
public void test(A<T> a) { … }
}
A<Object> a = new A<Object>();
A<Number> numA = new A<Number>();
A<Integer> intA = new A<Integer>();
Test<Number> test = new Test<Number>();
test.test(a); // compile error (1)
test.test(numA); // ok
test.test(intA); // compile error (2)
对于 test 方法,如果方法内仅需要调用 A<T> 的 set 方法(即仅需要用到输入值为 T 类型的方法,注意,这不包括如 List<T> 这种类新),使用 A<? super T> 代替 A<T> 可能会更为合适,这使得 (1) 得以编译通过,由于仅需要调用 A<T> 的 set 方法, A<Object>.set(Object) 显然比 A<Number>.set(Number) 允许更多的类型,从而使 A<Object> 可以替换 A<Number> 。相似的,如果 test 方法内仅需要调用 A<T> 个 get 方法,则使用 A<? extends T> 代替 A<T> 可能会更合适,这使得 (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 <T> 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 extends Object & Comparable<? super T>> T max1(Collection<? extends T> coll)
public static <T extends Object & Comparable<? super T>> T max2(Collection<T> coll)
参考资料
GJ- Making the future safe for the past: Adding Genericity to the JavaTM Programming Language
Generics in the Java Programming Language
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 别人用我的手机号码注册微信怎么办 电脑此网站的安全证书有问题怎么办 起诉离婚开庭时被告不到场怎么办 商标35类被别人注册了怎么办 商标被注销后被别人注册怎么办 电商35类商标被抢注怎么办 血小板太低怎么办可以吃水果吗? 微信好友删除了只记得昵称怎么办 优酷会员1080p很卡怎么办 电脑最下面的任务栏不显示怎么办 ea账号保留的问题忘了怎么办 微博超级话题账号被屏蔽怎么办 梦幻西游手机将军令号码换了怎么办 文档的内容被锁定无法修改怎么办 用户没有权限访问u盘时该怎么办? 电脑找不到ip地址连不上网怎么办 商标提前被别人注册微博昵称怎么办 扫码注册显示手机号被占用怎么办 vivo手机屏锁密码忘了怎么办 手机号注册微博手机号不用了怎么办 微博更换手机号被别人注册过怎么办 怎么有个qq注册验证码怎么办 12306手机丢了密码忘了怎么办 教师考试注册时邮箱填写错误怎么办 计算机二级注册时邮箱填错了怎么办 注册时执业范围填错了怎么办 百度号被盗了申诉不回来怎么办 斗鱼直播伴侣显示分类不可用怎么办 斗鱼黑名单显示网络加载失败怎么办 平台登录验证码只能显示一半怎么办 想改微博密码但是忘了原密码怎么办 微博账号密码都忘了怎么办 新浪博客忘记邮箱和密码忘了怎么办 新浪助学贷款邮箱密码忘了怎么办 客厅的父母乐忘记密码了怎么办 微博密码忘记了手机号也换了怎么办 想改密码旧密码忘了怎么办 商标转让转让方不配合做补证怎么办 肇事后责任方拒赔怎么办久草www 我出车祸了对方逃逸找不到人怎么办 肇事车辆怀疑是故意伤人怎么办啊