欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java泛型的重要目的:别让猫别站在狗队里

程序员文章站 2022-05-27 14:56:07
《Java编程思想》第四版足足用了75页来讲泛型——厚厚的一沓内容,很容易让人头大——但其实根本不用这么多,只需要一句话:我是一个泛型队列,狗可以站进来,猫也可以站进来,但最好不要既站猫,又站狗! 01、泛型是什么 泛型,有人拆解这个词为“参数化类型”。这种拆解其实也不好理解,还是按照沉默王二的意思 ......

Java泛型的重要目的:别让猫别站在狗队里

《java编程思想》第四版足足用了75页来讲泛型——厚厚的一沓内容,很容易让人头大——但其实根本不用这么多,只需要一句话:我是一个泛型队列,狗可以站进来,猫也可以站进来,但最好不要既站猫,又站狗!

01、泛型是什么

泛型,有人拆解这个词为“参数化类型”。这种拆解其实也不好理解,还是按照沉默王二的意思来理解一下吧。

现在有一只玻璃杯,你可以让它盛一杯白开水,也可以盛一杯二锅头——泛型的概念就在于此,制造这只杯子的时候没必要在说明书上定义死,指明它只能盛白开水而不能盛二锅头!

可以在说明书上指明它用来盛装液体,但最好也不要这样,弄不好用户想用它来盛几块冰糖呢!

这么一说,你是不是感觉不那么抽象了?泛型其实就是在定义类、接口、方法的时候不局限地指定某一种特定类型,而让类、接口、方法的调用者来决定具体使用哪一种类型的参数。

就好比,玻璃杯的制造者说,我不知道使用者用这只玻璃杯来干嘛,所以我只负责造这么一只杯子;玻璃杯的使用者说,这就对了,我来决定这只玻璃杯是盛白开水还是二锅头,或者冰糖。

02、什么时候用泛型

我们来看一段简短的代码:

public class cmower {

    class dog {
    }

    class cat {
    }

    public static void main(string[] args) {
        cmower cmower = new cmower();
        map map = new hashmap();
        map.put("dog", cmower.new dog());
        map.put("cat", cmower.new cat());

        cat cat = (cat) map.get("dog");
        system.out.println(cat);
    }

}

这段代码的意思是:我们在map中放了一只狗(dog),又放了一只猫(cat),当我们想从map中取出猫的时候,却一不留神把狗取了出来。

这段代码编译是没有问题的,但运行的时候就会报classcastexception(狗毕竟不是猫啊):

exception in thread "main" java.lang.classcastexception: com.cmower.java_demo.sixteen.cmower$dog cannot be cast to com.cmower.java_demo.sixteen.cmower$cat
    at com.cmower.java_demo.sixteen.cmower.main(cmower.java:20)

为什么会这样呢?

1)写代码的程序员粗心大意。要从map中把猫取出来,你不能取狗啊!

2)创建map的时候,没有明确指定map中要放的类型。如果指定是要放猫,那肯定取的时候就是猫,不会取出来狗;如果指定是要放狗,也一个道理。

第一种情况不太好解决,总不能把程序员打一顿(我可不想做一个天天背锅的程序员,很重的好不好);第二种情况就比较容易解决,因为map支持泛型(泛型接口)。

public interface map<k,v> {
}

注:在java中,经常用t、e、k、v等形式的参数来表示泛型参数。

t:代表一般的任何类。
e:代表 element 的意思,或者 exception 异常的意思。
k:代表 key 的意思。
v:代表 value 的意思,通常与 k 一起配合使用。

既然map支持泛型,那作为map的实现者hashmap(泛型类)也支持泛型了。

public class hashmap<k,v> extends abstractmap<k,v>
    implements map<k,v>, cloneable, serializable {
    
}

其中的put方法(泛型方法)是这样定义的:

public v put(k key, v value) {
  return putval(hash(key), key, value, false, true);
}

好了,现在使用泛型的形式来定义一个只能放cat的map吧!

public class cmower {

    class dog {
    }

    class cat {
    }

    public static void main(string[] args) {
        cmower cmower = new cmower();
        map<string, cat> map = new hashmap<>();
//      map.put("dog", cmower.new dog()); // 不再允许添加
        map.put("cat", cmower.new cat());

        cat cat = map.get("cat");
        system.out.println(cat);
    }
}

当使用泛型定义map(键为string类型,值为cat类型)后:

1)编译器就不再允许你向map中添加狗的对象了。

2)当你从map中取出猫的时候,也不再需要强制转型了。

03、类型擦除

有人说,java的泛型做的只是表面功夫——泛型信息存在于编译阶段(狗队在编译时不允许站猫),运行阶段就消失了(运行时的队列里没有猫的信息,连狗的信息也没有)——这种现象被称为“类型擦除”。

来,看代码解释一下:

public class cmower {

    class dog {
    }

    class cat {
    }

    public static void main(string[] args) {
        cmower cmower = new cmower();
        map<string, cat> map = new hashmap<>();
        map<string, dog> map1 = new hashmap<>();
        
        // the method put(string, cmower.cat) in the type map<string,cmower.cat> is not applicable for the arguments (string, cmower.dog)
        //map.put("dog",cmower.new dog());
        
        system.out.println(map.getclass());
        // 输出:class java.util.hashmap
        system.out.println(map1.getclass());
        // 输出:class java.util.hashmap
    }

}

map的键位上是cat,所以不允许put一只dog;否则编译器会提醒the method put(string, cmower.cat) in the type map<string,cmower.cat> is not applicable for the arguments (string, cmower.dog)。编译器做得不错,值得点赞。

但是问题就来了,map的class类型为hashmap,map1的class类型也为hashmap——也就是说,java代码在运行的时候并不知道map的键位上放的是cat,map1的键位上放的是dog。

那么,试着想一些可怕的事情:既然运行时泛型的信息被擦除了,而反射机制是在运行时确定类型信息的,那么利用反射机制,是不是就能够在键位为cat的map上放一只dog呢?

我们不妨来试一试:

public class cmower {

    class dog {
    }

    class cat {
    }

    public static void main(string[] args) {
        cmower cmower = new cmower();
        map<string, cat> map = new hashmap<>();
        
        try {
            method method = map.getclass().getdeclaredmethod("put",object.class, object.class);
            
            method.invoke(map,"dog", cmower.new dog());
            
            system.out.println(map);
            // {dog=com.cmower.java_demo.sixteen.cmower$dog@55f96302}
        } catch (nosuchmethodexception | securityexception | illegalaccessexception | illegalargumentexception | invocationtargetexception e) {
            e.printstacktrace();
        }
    }

}

看到没?我们竟然在键位为cat的map上放了一只dog!

注:java的设计者在jdk 1.5时才引入了泛型,但为了照顾以前设计上的缺陷,同时兼容非泛型的代码,不得不做出了一个折中的策略:编译时对泛型要求严格,运行时却把泛型擦除了——要兼容以前的版本,还要升级扩展新的功能,真的很不容易!

04、泛型通配符

有些时候,你会见到这样一些代码:

list<? extends number> list = new arraylist<>();
list<? super number> list = new arraylist<>();

?和关键字extends或者super在一起其实就是泛型的高级应用:通配符。

我们来自定义一个泛型类——pethouse(宠物小屋),它有一些基本的动作(可以住进来一只宠物,也可以放出去):

public class pethouse<t> {
    private list<t> list;

    public pethouse() {
    }

    public void add(t item) {
        list.add(item);
    }

    public t get() {
        return list.get(0);
    }
}

如果我们想要住进去一只宠物,可以这样定义小屋(其泛型为pet):

pethouse<pet> pethouse = new pethouse<>();

然后,我们让小猫和小狗住进去:

pethouse.add(new cat());
pethouse.add(new dog());

如果我们只想要住进去一只小猫,打算这样定义小屋:

pethouse<pet> pethouse = new pethouse<cat>();

但事实上,编译器不允许我们这样定义:因为泛型不直接支持向上转型。该怎么办呢?

可以这样定义小屋:

pethouse<? extends pet> pethouse = new pethouse<cat>();

也就是说,宠物小屋可以住进去小猫,但它必须是宠物(pet或者pet的子类)而不是一只野猫。

但很遗憾,这个宠物小屋实际上住不了小猫,看下图。

Java泛型的重要目的:别让猫别站在狗队里

这是因为java虽然支持泛型的向上转型(使用 extends 通配符),但我们却无法向其中添加任何东西——编译器并不知道宠物小屋里要住的是小猫还是小狗,或者其他宠物,因此干脆什么都不让住。

看到这,你一定非常疑惑,既然pethouse<? extends pet>定义的宠物小屋什么也不让住,那为什么还要这样定义呢?

(我暂时也没有想到合适的场景,你知道吗?)


推荐阅读:

java如何在运行时识别类型信息?
掌握java字符串的这6点常识,你就可以怼面试官了