java和kotlin中的泛型上界(extends)下界(super)的理解

首先申明,本文不是讲解泛型的基础概念和用法,网上有很多普及类的博客,写的都很不错,本文的目的只是为了帮助大家理解泛型里面比较难懂上界和下界的概念。

通过本文的学习,你能了解到:

1.为什么要用泛型?泛型的作用是什么?

2.java泛型的上界和下界,以及协变和逆变的理解。

3.上界和下界在kotlin的使用。

下面正式开始介绍。

一、泛型的作用

private void test() {
    //1.jdk5之前
    List list = new ArrayList();
    list.add(new Apple());
    Apple apple = (Apple) list.get(0);

    //2.jdk5之后
    List<Apple> list1 = new ArrayList<>();
    list1.add(new Apple());
    Apple apple1 = list1.get(0);

    //3.
    Apple apple2 = new Apple();
    List<Apple> list2 = new ArrayList<>();
    Apple apple3 = find1(apple2, list2);
}

<T> T find1(T item, List<T> list) {
    for (T t : list) {
        if (t.equals(item)) {
            return t;
        }
    }
    return null;
}

//3.1
Apple find2(Apple item, List<Apple> list) {
    for (Apple t : list) {
        if (t.equals(item)) {
            return t;
        }   
    }
    return null;
}

对以上三个例子分别解释如下:

(1)jdk5之前集合的用法,因为没有泛型的约束,List里面可以存放任意类型,取出的数据也需要强转。

(2)jdk5开始集合的用法,List中只能存放泛型约束的类型,取出的数据也不需要强转。

(3)find方法用到了泛型的类型推断,方法参数是什么类型,返回的就是什么类型。

使用<>两个尖括号是泛型的一个很明显的特征。我们一般都会认为只有类有实例化,其实泛型也有实例化,<>里面我们传入的类型就相当于给泛型实例化,比如上面例子中List<Apple>。find这个方法其实也叫泛型方法,因为方法最前面申明了一个泛型T。

在回答泛型的作用之前大家可以先思考一个问题:我们不用泛型是不是也能正常的写代码?当然可以!要不然jdk5之前的项目都是怎么来的?就比如上面的2、3两个例子,我们都可以不用泛型来实现。例2可以用例1代替,find1方法可以用find2方法代替。之所以我们用了find1和例2,是因为方便!没错,泛型的作用就是为了方便我们写代码,让我们写的代码更安全,写的过程更舒服。

二、java中的泛型

java中的泛型比较困扰我们的,大家有没有被困扰我不知道,反正我是困扰了很久。是啥呢?就是上界下界

先看下面这个例子。

public class Fruit {

}

public class Apple extends Fruit {

}

//1.这里我们很容易理解,也是我们最常用的用法
Fruit fruit = new Apple();
//2.这里会报错,编译不通过
ArrayList<Fruit> list = new ArrayList<Apple>();

第1句话很容易理解,创建一个子类对象赋值给父类型,这个就是java里面的多态,肯定是没问题的。第2句话就报错,这是为什么呢?百思不得其解。在解释之前先下一个结论:类具有继承性,但是作为泛型参数的时候是没有继承关系的,也就不允许像第2句话那样赋值。不仅是jdk自带的一些泛型类,我们自定义的类比如Shop<Fruit> shop = new Shop<Apple>(),这样写也是不允许的。

其实我们可以换个思路想想,如果List<Fruit> list = new ArrayList<Apple>();这句话编译器不报错会怎么样呢?

首先我们要了解一个知识点,java里面的泛型擦除。java代码在运行时所有的 T 以及尖括号里的东西都会被擦除,也就是说上面代码在运行时会变成List list = new ArrayList()

如果第2句话不报错会怎么样?

List<Fruit> list = new ArrayList<Apple>();
list.add(new Banana());
list.add(new Apple());
list.add(new XXX())

如果第一句话不报错,那么就可以通过这种写法向list中插入各种类型的Fruit子类。那List不是乱套了!别人取的时候怎么知道取的是啥玩意呢?所以java干脆就在初始化的时候直接编译报错,不允许你那样写。

List<Fruit> list = new ArrayList<Fruit>();
List<Apple> list = new ArrayList<Apple>();

像这样左右泛型参数类型保持一致,这样是没问题的。

那如何让上面那句话不报错呢?答案是:

List<? extends Fruit> list = new ArrayList<Apple>();

这样写意思是说我要求泛型的参数只要是Fruit子类就行,左边的<? extends Fruit>是约束泛型,右边的<Apple>是实例化泛型。

这种写法叫java的上界,也叫协变。就是说你实例化泛型的时候传的类型不要超过Fruit,是Fruit或者Fruit的子类都行,你别整个Object类型。

这时候有人会说了,我要整这么麻烦干嘛呢?我左右泛型保持一致不就得了,尽搞些花样,费脑。说的也没错,你可以不用,但是你不能阻止别人这样写:

List<Apple> appleList = new ArrayList<>();
List<Banana> bananaList = new ArrayList<>();

getWeight(appleList);


float getWeight(List<? extends Fruit> list) {
    float total = 0;
    for (Fruit fruit : list) {
        total += fruit.getWeight();
    }
    return total;
}

有没有发现方便的地方?按照getWeight方法那样的申明,我们可以传入任意泛型子类的集合进去,是不是贼噶方便。

另外我们用List<? extends Fruit> list来申明一个变量时,list只允许get不允许set的,也就是你只能读不能向list中插入数据。想想也好理解,因为如果我允许你往里面插入数据,你就可以插入任何Fruit的子类,那我就不知道取出来的是什么东西了。

既然有上界,那肯定也有下界。我们先看这句话:

List<Apple> list = new ArrayList<Fruit>();

父类哪能赋值给子类呢,这不很明显有问题嘛。

java也有办法让它不报错:

List<? super Apple> list = new ArrayList<Fruit>();

这种写法叫java的下界,也叫逆变。就是说,泛型类型我已经定义了最低限制了,不能低于Apple了,你实例化泛型的时候传Apple或者Apple的父类都行,你别整个Apple的子类,青苹果、红苹果之类的。

那有人有疑问了,我为什么要搞这么麻烦,上面的上界我还能理解,还能找到使用它的理由,那这个下界有个毛的用哦。确实这种很少用,至少我开发了这么多年几乎是没怎么用过这种写法。

但是下面这种用法还是有的:

public class Apple extends Fruit {
    public void addToList(List<? super Apple> list) {
        list.add(this);
    }
}


List<Fruit> list1 = new ArrayList<>();
List<Apple> list2 = new ArrayList<>();
Apple apple = new Apple();
apple.addToList(list1);
apple.addToList(list2);

addToList方法中允许传入一个list,list的泛型类型可以是Apple,也可以是Apple的父类Fruit,甚至可以是Object。

另外我们用List<? super Apple> list来申明一个变量时,list只允许set不允许get的,也就是你只能向list中插入数据而不能读,插入的数据也只能是Apple。我们可以这样理解,因为参数list能传入Apple或者Apple的父类型作为泛型参数,我即使能取,我也不知道我取的是Apple还是Fruit还是Object。

其实无论是上界还是下界,协变还是逆变,主要是为了方便我们写代码,用了之后能省事,当然你也可以不用。

为了帮助我们理解记忆,Joshua Bloch 在他的著作《Effective Java》中创造了PECS这个术语,producer extends、consumer super。意思是extends修饰的是生产者,super修饰的是消费者。

最后总结一下,在java中:

(1)? extends修饰的泛型类型只允许读不允许写。——生产者producer。

(2)? super修饰的泛型类型只允许写不允许读。——消费者consumer。

三、kotlin中的泛型

其实理解了java泛型,我们再来看kotlin中的泛型就很好理解了。

List<? extends Fruit> list = new ArrayList<>();
val list: ArrayList<out Fruit> = ArrayList<Apple>()

List<? super Apple> list = new ArrayList<>();
val list: ArrayList<in Apple> = ArrayList<Fruit>()

(1)kotlin中out修饰的对应java中的extends,表示生产者,只允许读。可以理解为我只想从里面拿东西。

(2)kotlin中in修饰的对应java中的super,表示消费者,只允许写。可以理解为我只想给你传东西。

以上就是关于泛型的理解和介绍,还是要多用,用的多了就会突然有一天茅塞顿开。

原创不易,转载请注明出处:https://www.longdw.com。