什么是泛型
这个概念很抽象,举个例子List<View> list = new ArrayList<>();
View就是List的泛型,表示这个List只能存放View类型或者View子类的对象。这么说也有些笼统,我直接把百科的介绍拿过来吧,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数,可以用在类,接口,方法中。看完本篇文章你就会明白泛型到底是是什么。
泛型的好处
举例
List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
String item = (String)arrayList.get(i);
}
用了无数遍的例子,这里的List没有指定泛型,那默认的泛型就是Object
所以这段代码在编译时完全没问题。运行程序肯定会崩溃。这里存放的是Object
类型,使用的时候以String
类型使用。但是实际上添加了一个Integer类型的100,所以取出时会有类型强转错误。
修改一下代码
List<String> arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for(int i = 0; i< arrayList.size();i++){
String item = (String)arrayList.get(i);
}
好嘛,修改完编辑器直接报错。大家应该都知道为什么报错。声明了String
类型的List
却存放了一个100
这样把本来运行时候发生的错误直接提前到编译期,减少了代码出错的风险,这就泛型的好处之一
泛型擦除
真泛型
泛型存在于编译器和运行期
伪泛型
泛型仅仅存在于编译器
真泛型的代表有c#,关于真泛型不在本章类容里。学过java的人都知道java是伪泛型,如何验证呢?这也很简单
java可以方法重载,重载的原则是方法名一样,参数不同。
public void fun(List<Integer> integers) {
}
public void fun(List<String> strings) {
}
根据重载的原则,List<Integer>!=List<String>
重载应该是成立的。然鹅...
这里报错了,报错信息存在里两个相同的方法。以上的代码写到c#中就不会报错。
再看一个例子
List<Integer> integers = new ArrayList<>();
List<String> strings = new ArrayList<>();
if (integers.getClass().equals(strings.getClass())) {
System.out.println("类型相同");
}
以上代码运行会输出日志,也可以证明java是伪泛型,泛型在运行时会擦除。以上的 List<Integer>
List<String>
最后都会变成List
泛型的使用
泛型方法
现在有这样的需求,传入两个对象,返回较大的对象。伪代码如下
public Object getMax(Object a, Object b) {
//……比较大小
return a;
}
public void fun() {
int a = 1;
int b = 2;
Object max = getMax(a, b);
}
传入两个Object对象,比较大小后返回较大的。直接写的话返回值肯定也是Object。其实这样很不好,在fun方法中调用时候传入了1和2,返回的却是个Object。很明显这里返回int类型会更好。修改一下代码
public <T> T getMax(T a, T b) {
//……比较大小
return a;
}
public void fun() {
int a = 1;
int b = 2;
int max = getMax(a, b);
}
在类上也要声明T public class JavaTest<T>
这里使用T代表泛型,getMax方法声明参数类型<T >
,这样调用的时候传入的参数是什么类型就会返回什么类型
泛型类
public class JavaTest<T> {
private T t;
public JavaTest(T t) {
this.t = t;
}
public T getT() {
return t;
}
}
//声明JavaTest对象的时候指定泛型类型,传入的t参数的类型必须要一致
JavaTest<String> javaTest1 = new JavaTest<>("String");
JavaTest<Integer> javaTest2 = new JavaTest<>(1);
JavaTest<Boolean> javaTest3 = new JavaTest<>(true);
协变,逆变和不变
协变
先看一段代码
public class Father {
}
public class Son extends Father{
}
Father father = new Father();
Son son = new Son();
father = son;
List<Father> fathers = new ArrayList<Son>();
Son是继承于Father的,根据多态father = son;
这是完全没问题的,那List<Father> fathers = new ArrayList<Son>();
应该也没问题吧。然鹅...
这里报错了,因为Java的泛型是不变性质的,也就是在List<Father>
和List<Son>
的类型不一致。Java中子类的泛型类型不属于父类泛型类型的子类。在这个例子里就是 List<Son>
并不是List<Father>
的子类。
这种把子类的泛型对象赋值给父类的泛型的引用叫协变,因为java的泛型擦除,所以不支持协变。但是实际使用中又会遇到这样的需求。
这里就要使用 通配符 ? extends
List<? extends Father> fathers = new ArrayList<Son>();
上界通配符,表示这个list是个未知的类型。extends Father 限制了未知类型的上界限,虽然是未知类型,但是必须是Father的子类。
所以以下的情况都是可以用的:
List<?extends Father> list1 = new ArrayList<Father>(); //本身
List<?extends Father> list2 = new ArrayList<Son>(); //直接子类
List<?extends Father> list3 = new ArrayList<Son的子类>();//间接子类
你以为这样就没问题了吗
调用add方法报错了,使用? extends Father
虽然解除了协变的限制,却又带来了新的限制
List<? extends Father>
的泛型是个未知泛型,只是限制了必须是Father的子类。所以fathers.get();
得到的肯定是fathers的。当然也可以强转成fathers的子类
使用add方法,既然List<? extends Father>
是Father的子类的未知类型,那它可能是List<Father>
也可能是List<Son>
。如果是List<Son>
的话就不能添加Father了。编辑器根本不知道List的实际类型也就无法确定add(father)
是否正确。所以干脆不让用add方法。
那这样的协变又有什么用呢???
public void fun1(List<? extends Father> list) {
for (int i = 0; i < list.size(); i++) {
list.get(i).toString();
}
}
以上的场景中fun1方法接受一个Father子类的list,然后遍历调用toString方法。这时你有个List<Son>
依然也是可以调用这个方法的。如果不使用? extends
就无法调用。在遇到只需要读取数据不修改数据数据的时候就可以使用? extends
让java支持协变
由于这种限制,使用协变的泛型只能提供数据而不能修改数据。所以Java的协变是向外提供数据的一方,被称为生产者 Producer
逆变
和? extends
对应有? super
下界通配符,与上界通配符对应,这里 super 限制了? 的子类。
List<? super Son> list = new ArrayList<Father>();
super限制了泛型的下界,必须要满足 引用 super 对象 这个条件( son super Father
)即 后边的泛型类型必须是前面的泛型类型的父类,正好与协变返过来。
以下这些写法都是可以的
List<? super Son> list1 = new ArrayList<Son>(); //本身
List<? super Son> list2 = new ArrayList<Father>(); //直接父类
List<? super Son> list3 = new ArrayList<Object>(); //间接父类
同样使用? super
实现了逆变,也带来了别的限制。限制也正好与? extends
相反
List<? super Son> sons = new ArrayList<Father>();
sons.add(new Son());
Object object = sons.get(0);
同理,?表示未知类型。这里的泛型只要是 Son 的父类就可以,所以add一个Son是可以的。
调用get方法,泛型无法确定具体的类型,只能向上取值,取到最大的值就是Object,如果你足够自信的活当然可以强转成Son,实际使用上肯定存在强转风险。
那..逆变又有什么用?
public void fun2(List<? super Father> list) {
Son son = new Son();
list.add(son);
}
fun2接受一个泛型? super Father
的list的,将内部创建的Son对象添加到list中。
这时你有一个Father类型的List的,只是想在Father类型中添加一个Son的数据,根据多态的特性是完全合理的,语法上就可以使用? super
来解决这个问题
Java逆变的特性确定它只能修数据不能获取获取,通常只拿来添加数据,往List中添加数据,这种泛型类型也叫消费者 Consumer
不变
不变是最好理解的,Java默认的泛型就是不变类型。即引用和对象并不存在什么继承关系
小结
关于Java的协变和逆变也被总结成PECS 法则:Producer-extends, Consumer-super
说直白点就是,从数据流来看,extends是限制数据来源的(生产者),而super是限制数据流入的(消费者)。例如上面例子中,使用<? extends Father> 限制存放的是Father类型的及其子类,所以调用get方法一定能得到Father,但因为具体类型不明确,无法调用add方法。使用<? super Father>限制了泛型是Fathe及其父类,也就限制了add方法必须添加Father以及其父类,也因为具体类型不明确,调用get方法时候会向上取最大兼容的类型,也就是Object。
kotlin中的泛型
kotlin完全兼容java,所以泛型的特点也都继承自java。kotlin也是伪泛型,泛型的写法也都类似,同样也有协变和逆变的问题。
in
out
Kotlin使用关键字 out
来支持协变,等同于 Java 中的上界通配符 ? extends
( <? extends Father> = <out Father> )
Kotlin使用关键字 in
来支持逆变,等同于 Java 中的下界通配符
? super
( <? super Father> = <in Father> )
kotlin中泛型的使用:
class Producer<T> {
fun produce(): T {
...
}
}
val producer: Producer<out TextView> = Producer<Button>()
val textView: TextView = producer.produce() // 相当于 'List' 的 `get`
class Consumer<T> {
fun consume(t: T) {
...
}
}
val consumer: Consumer<in Button> = Consumer<TextView>()
consumer.consume(Button(context)) // 相当于 'List' 的 'add'
kotlin 泛型与java不同的地方
通配符* :
泛型中使用* 和 Java中使用通配符?一样
java中单独使用?相当于 ?extends Object
kotlin使用*相当于out Any
reified 关键字:
在Java和Kotlin中都不能检查一个对象是否是T类型
fathers instanceof T //java 会报错的
100 is T //kotlin 也会报错的
这个问题在Java中通过添加一个Class<T> 类型的参数 来解决
public<T> void check(Object item, Class<T> type) {
if (type.isInstance(item)) {
System.out.println(item);
}
}
Kotlin中也可以这么做,但是还有另外一个方法。
在inline函数中配合使用reified关键字
inline fun <reified T> printIfTypeMatch(item: Any) {
if (item is T) {
println(item)
}
}
类声明处使用out和in
在类的声明时候使用out和in,也就定位了这个类是用来输入还是输出。
class Producer<out T> {
fun produce(): T {
...
}
}
val producer: Producer<TextView> = Producer<Button>() // 这里不写 out 也不会报错
class Consumer<in T> {
fun consume(t: T) {
...
}
}
val consumer: Consumer<Button> = Consumer<TextView>() // 这里不写 in 也不会报错
型变点:
在类中使用out或者in,型变点就是这个泛型的类型,也就是T
协变时,型变点只能作为返回值使用
逆变时,型变点只能作为参数使用
如果遇到协变时型变点要作为参数使用,或者逆变时型变点要作为返回值使用。可以使用@UnsafeVariance解除限制(仅仅是解除编辑器报错,并不会影响协变逆变对数据读取修改的特性)
例
interface KotlinGenericity<out T> {
fun get():T
fun add(t:@UnsafeVariance T)
}
上例中add方法中如果不使用@UnsafeVariance是会报错的。
总结
不变类型的泛型的直接使用上应该是没什么难的地方,主要是协变和逆变的地方。说实话我对这玩意还是有些晕,而且越想越晕。这里总结的一下关于型变的特点,大家在用的时候记住这个特点就好了。
协变:正向的继承关系,只能读取数据不能修改数据,java中使用? extends
,kotlin中使用out
,协变的型变点只能作为返回值使用
逆变:与协变相反,逆向的继承关系,只能修改数据,不能读取数据,java中使用? super
,kotlin中使用in
,逆变的型变点只能作为参数使用
不变:不存在继承关系,既能修改数据也能读取数据,型变点既可以当参数也可以当返回值
相关资料: