原标题:Java泛型可行与不可行
喜欢就 关注我们吧!
泛型基础
理解
一般情况,一个类的属性,或者一个方法的参数/返回值都需要在编写代码时声明基本类型或者自定义类型,但有时候无法在编写代码时使用现有的类来表达参数类型或者返回值类型,这时候就需有一种方式可以表达下面的意思:这里需要一个类,它满足这些要求就可以了,具体是什么类可以在使用这个类或方法时指定。Java中这种方式就是泛型。但是java泛型在使用上有很多限制,使用时要注意,同时注意泛型主义上的理解,Java中泛型的声明使用更多
作用
一定程序上继承与接口就可以完成上面的功能,但泛型有很多额外的作用
在泛型出现之前,如果一个方法不能确定方法的返回值类型,或者根据入参可以确定多种类型返回值类型,那么这个方法就只能返回Object ,有了泛型之后,在方法返回正确的值后,会自动转为具体的类型,而这在代码上没有额外的代码,而且这种转换很安全
上面例子编译之后再反编译回来 make 方法是这样的
publiccom.zoro.thinkinginjava.four. TupleTest<String, String> make{
returnnewcom.zoro.thinkinginjava.four.TupleTest((T) “a”, (R) “b”);
}
再看一个调用时的代码
publicclassTupleMain{
publicstaticvoidmain(String[] args){
TupleTest<Apple, Orange> tuple = newTupleTest<>( newApple, newOrange);
Orange orange = tuple.getR;
}
}
反编译之后
publicclassTupleMain{
publicstaticvoidmain(String[] args){
TupleTest<Apple, Orange> tuple = newTupleTest( newApple, newOrange);
Orange orange = (Orange)tuple.getR;
}
}
可以看到自动对参数进行了转型,所以编译器不会产生转型警告
书写泛型代码的主要困难是因为泛型在运行时被擦除,所以在运行期没有泛型类的具体信息,这意味着泛型参数看上去就借一个Object类,什么都干不了,需要注意以下方法
不能通过不同的泛型参数进行方法重载,但是可以使用 <R extends List<?>>给泛型参数添加边界重载方法
publicclassOverLoadTest{
public<T> voidtest(T t){ }
// 因为T与R没有设置边界在运行时 T与R 都是类似Object,所以不能通过方法签名区分这两个方法
// public <R> void test(R r) { }
// 这样是可以的 因为R一定会是一个List的子类,List与Object(T)是有区别的,就可以通过方法签名区分了
public<R extends List<?>> voidtest(R r){ }
}
可以使用 extends 限定泛型类型的边界,可以是多个(&连接),类写在前面,限定边界之后在泛型方法或者类的内部就可以使用边界类上的方法了
publicclassWildCardTest< TextendsList< String> & Iterable< String> & InterfaceA<?>> {
publicvoidtest(T t){
t.add( “”); // List接口的方法
t.iterator; // Iterable接口的方法
t.testMethod; // InterfaceA方法
}
}
interfaceInterfaceA< T> {
// void add(T t); // List接口也有同样方法签名的方法,所以在 同时将 List与InterfaceA设置为上边界时List与InterfaceA的泛型参数要兼容,否则也会出错
voidtestMethod;
}
通配符
通配符在泛型中的应用是为了解决下面的问题:有一个容器的泛型是基类的变量,想要将一个泛型是子类的容器赋值给这个变量,编译器是不允许的;因为运行时会将泛型擦除,一旦将一个泛型是子类的容器赋值给泛型是基类的容器变量,在运行时就可以将一个这个基类的其他子类对象放入这个窗口,造成在取出对象时的类型不安全,所以编译期不允许这样赋值;
publicclassWildCardTest< TextendsList< String> & Iterable< String> & InterfaceA<?>> {
publicstaticvoidmain(String[] args){
List<InterfaceA<String>> list ;
List<Impl> impls = newArrayList<>;
// list = impls;
// 将 impls赋值给 list是不可以的,原因:
// 1. 编译期 List<InterfaceA<String>> 与 List<Impl>是不同的且不能向上转型
// 2. 一旦允许这样赋值,那么之后 的操作会出现类型问题,比如此例,将一个ArrayList<Impl> 赋值给 List<InterfaceA>变量list,
// 那么之后可以向list 中add 一个 Impl2对象,Impl2与Impl不兼容
}
}
interfaceInterfaceA< T> {}
classImplimplementsInterfaceA< String> {}
classImpl2implementsInterfaceA< String> {}
容器的这一特点与数组不同,子类数组对象可以赋值给基类数组变量(类似向上转型),但是在运行期jvm 可以知道数组元素中的对象类型是哪个具体子类,所以如果将数组中元素赋值时,如果不是原数组中的类型,会报错(ArrayStoreException)
publicclassWildCardTest2{
publicstaticvoidmain(String[] args){
InterfaceA<?>[] arr1 = newImpl[ 3];
arr1[ 0] = newImpl;
//会报错
//arr1[2] = new Impl2;
// 兼容的类型可以
InterfaceA<?>[] arr2 = newInterfaceA[ 4];
arr2[ 0] = newImpl;
arr2[ 0] = newImpl2;
}
}
为了保证类型安全,又可以将子类泛型容器赋值给基类泛型变量,可以使用通配符(单一边界,extends 后面只能有一个类型)
通配符的困难之处
当一个类在声明时使用了<? extends Fruit> 这种泛型,而这个类的写法如同下面这样
classTestClass< T> {
publicvoidtest(T t){
// somecode
}
publicvoidtest2(Object o){
// somecode
}
}
在使用时
TestClass<? extends Fruit> f = newTestClass<Apple>;
这样写会出现的问题是不能调用test(T)方法了,因为test 需要的是一个具体的Fruit 的子类,例子中应该是Applie,但 ? extends Fruit 代表的不仅仅是 Apple 这一种子类,也可能是 orange 。如果调用时真的用orange 类型实例做为能数,类型就不安全,所以test(T)方法不能用了;但是 test2(Object)还可以用
逆变
逆变指的是 < ? super Apple>这种写法,这种写法的特性与 <? extends Apple> 的写法的特性是相反的。上面的例子,泛型入参方法不能用了,而逆变的特性是入参可以是任何Apple 的子类,注意是子类,不是基类,因为Apple 的基类有多种,如果编译器允许传入基类,就会存在风险,但是传入子类就不会有风险,因为子类可以转型为Apple 类,Apple 类可以算是Apple的基类;
publicclassWildcardTest4{
publicstaticvoidmain(String[] args){
List<? superApple> appleList = newArrayList<Fruit>;
List<? superApple> appleList2 = newArrayList<Apple>;
List<? superApple> appleList3 = newArrayList<>;
// 前三种情况都可以,但是这种不可以
// List<? super Apple> appleList4 = new ArrayList<BigApple>;
// 不可以
//appleList3.add(new Orange);
appleList3.add( newApple);
appleList3.add( newBigApple);
// 虽然字面上是 任何 Apple 的父类,但是Apple父类很多,不能确定类型,所以实际上任何Apple 的父类都不行
//appleList3.add(new Fruit);
// 只能Object 接
Object a = appleList3.get( 1);
}
}
classFruit{}
classOrangeextendsFruit{}
classAppleextendsFruit{}
classBigAppleextendsAppleimplementsRunnable{
@Override
publicvoidrun{
}
}
classSmallAppleextendsApple{}
逆变的困难之处在于方法的返回值,它的返回值只能用Object 类型的变量接受
无界通配符
两个功能
- 这里想用泛型代码来编写,这里并不是要用原生的类型,但是当前情况下,泛型参数可以持有任何类型
- 当有个地方需要多个泛型参数,但你只能确定一部分时可以使用无界通配符 ? 例: Map<String, ?>
- 当一个地方要求泛型,如果你没有给出泛型,会有警告,但使用无界通配符会消除警告
无界通配符与原生类型是不一样的,以List和List<?>为例,List代表持有任何Object类型的List,List<?>代表具有某种特定类型的的非原生List,但目前不确定是什么类型;
下面例子显示这种区别
publicclassWildcardTest5{
publicstaticvoidmain(String[] args){
List list = newArrayList;
list.add( newApple); // 有警告,但是不会编译报错
Object o = list.get( 0);
List<?> list1 = newArrayList<>;
// list1.add(new Apple);// 不可这样写,编译报错
}
}
总结
在使用泛型时,时刻都要想着,我这样定义泛型,编译器为了保证泛型安全,这里我只能接受什么样的类型;方法的返回值会是什么样的;同时要想着这里是否会发生转型
公众号聊天窗口回复关键词“干货”,获取更多技术干货内容~
分布式定时任务调度框架实践 2021-05-04
复杂分布式架构下的计算治理之路:计算中间件Linkis 2021-05-03
摆脱主机环境限制,随心所欲编译Android源码 2021-05-02
责任编辑: