浅谈java的泛型

什么是泛型

这个概念很抽象,举个例子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

image

这样把本来运行时候发生的错误直接提前到编译期,减少了代码出错的风险,这就泛型的好处之一

泛型擦除

真泛型

泛型存在于编译器和运行期

伪泛型

泛型仅仅存在于编译器

真泛型的代表有c#,关于真泛型不在本章类容里。学过java的人都知道java是伪泛型,如何验证呢?这也很简单

java可以方法重载,重载的原则是方法名一样,参数不同。

    public void fun(List<Integer> integers) {        
    }

    public void fun(List<String> strings) {
    }

根据重载的原则,List<Integer>!=List<String>重载应该是成立的。然鹅...

image

这里报错了,报错信息存在里两个相同的方法。以上的代码写到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>();应该也没问题吧。然鹅...

image

这里报错了,因为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的子类>();//间接子类

你以为这样就没问题了吗

image

调用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是会报错的。

image

总结

不变类型的泛型的直接使用上应该是没什么难的地方,主要是协变和逆变的地方。说实话我对这玩意还是有些晕,而且越想越晕。这里总结的一下关于型变的特点,大家在用的时候记住这个特点就好了。

协变:正向的继承关系,只能读取数据不能修改数据,java中使用? extends,kotlin中使用out,协变的型变点只能作为返回值使用

逆变:与协变相反,逆向的继承关系,只能修改数据,不能读取数据,java中使用? super,kotlin中使用in,逆变的型变点只能作为参数使用

不变:不存在继承关系,既能修改数据也能读取数据,型变点既可以当参数也可以当返回值

相关资料:

Kotlin 的泛型
java 泛型详解-绝对是对泛型方法讲解最详细的
协变与逆变

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容