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);
            // {[email protected]}
        } 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虽然支持泛型的向上转型(使用 extends 通配符),但我们却无法向其中添加任何东西——编译器并不知道宠物小屋里要住的是小猫还是小狗,或者其他宠物,因此干脆什么都不让住。

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

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



推荐阅读:

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

原文地址:https://www.cnblogs.com/qing-gee/p/10342858.html

时间: 2024-08-29 11:08:01

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

2017.4.5 Java 泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型. 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数. 假定我们有这样一个需求:写一个排序方法,能够对整形数组.字符串数组甚至其他任何类型的数组进行排序,该如何实现? 答案是可以使用 Java 泛型. 使用 Java 泛型的概念,我们可以写一个泛型方法来对一个对象数组排序.然后,调用该泛型方法来对整型数组.浮点数数组.字符串数组等进行排

Java 泛型的约束与局限性

Java 泛型的约束与局限性 @author ixenos 不能用基本类型实例化类型参数 不能用类型参数代替基本类型:例如,没有Pair<double>,只有Pair<Double>,其原因是类型擦除.擦除之后,Pair类含有Object类型的域,而Object不能存储double值.这体现了Java语言中基本类型的独立状态. 运行时类型查询只适用于原始类型(raw type) 运行时:通常指在Classloader装载之后,JVM执行之时 类型查询:instanceof.getC

Java泛型读书笔记 (二)

关于Java泛型擦除后,继承一个泛型类带来的问题 有如下泛型类Pair: public class Pair<T> { private T second; private T first; public Pair() { first = null; second = null; } public Pair(T first, T second) { this.first = first; this.second =second; } public T getFirst() { return fi

jdk学习之路---java泛型

1:java泛型的好处是可以实现类型检查,比较安全,可以实现功能的扩展化,增强功能. 2:泛型的概念就是参数化的类型,相当于方法中的形式参数一样,先做一个占位符然后再在使用的时候传递真正的参数进去. 3: package generic; /** * 自定义泛型类 * @author PC * */ public class GenericModel <T>{ private T data; public GenericModel(){ } public GenericModel(T data

Java泛型学习笔记--Java泛型和C#泛型比较学习(一)

总结Java的泛型前,先简单的介绍下C#的泛型,通过对比,比较学习Java泛型的目的和设计意图.C#泛型是C#语言2.0和通用语言运行时(CLR)同时支持的一个特性(这一点是导致C#泛型和Java泛型区别的最大原因,后面会介绍).C#泛型在.NET CLR支持为.NET框架引入参数化变量支持.C#泛型更类似C++模板,可以理解,C#泛型实际上可以理解为类的模板类.我们通过代码实例来看C# 2.0泛型解决的问题,首先,我们通过一个没有泛型的迭代器的代码示例说起,代码实现如下: interface

Java泛型详解(转)

文章转自  importNew:Java 泛型详解 引言 泛型是Java中一个非常重要的知识点,在Java集合类框架中泛型被广泛应用.本文我们将从零开始来看一下Java泛型的设计,将会涉及到通配符处理,以及让人苦恼的类型擦除. 泛型基础 泛型类 我们首先定义一个简单的Box类: public class Box { private String object; public void set(String object) { this.object = object; } public Stri

Java 泛型(Generics) 综述

一. 引子 一般的类和方法,只能使用具体类型:要么是基本类型,要么是自定义类型.如果要编写可以应用于多种类型的代码,这种刻板的限制对代码的束缚就会很大. 多态算是一种泛化机制,但对代码的约束还是太强(要么继承父类,要么实现接口). 有许多原因促成了泛型的出现,而最引人注目的一个原因,就是为了创造容器类.(泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性) 例如,在 Java 实现加入泛型前,ArrayList 只维护一个 Object 类型的数组: publ

Java泛型笔记

Java泛型的目的:   通用性,可应用于多种类型,而不是一个具体的接口或类.   JAVA1.5的补充.  用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性. Java泛型的核心概念:  告诉编译器想使用什么类型,然后编译器帮你处理一切细节. Java 泛型的局限性:  1,不能使用基本类型作为类型参数. -Java泛型方法 :  能使用泛型方法,尽量使用泛型方法.  static的方法,不能访问泛型类的类型参数. 定义泛型方法:将泛型参数列表定义在返回值之前  public

Java 泛型详解

在Java SE1.5中,增加了一个新的特性:泛型(日本语中的总称型).何谓泛型呢?通俗的说,就是泛泛的指定对象所操作的类型,而不像常规方式一样使用某种固定的类型去指定.泛型的本质就是将所操作的数据类型参数化,也就是说,该数据类型被指定为一个参数.这种参数类型可以使用在类.接口以及方法定义中. 一.为什么使用泛型呢? 在以往的J2SE中,没有泛型的情况下,通常是使用Object类型来进行多种类型数据的操作.这个时候操作最多的就是针对该Object进行数据的强制转换,而这种转换是基于开发者对该数据