10分钟让你彻底搞懂Java泛型!!

发布时间:2024年01月23日

前言

最近在学习中,我对于泛型这个知识点其实一直比较模糊,对于源码的各种通配符“?”,“T”,“S”,"R"等,都不了解其含义,更加不要说怎么去使用泛型。所以,我专门写了一篇文章,来从头开始透彻的分析Java中的泛型,并结合项目实际的应用场景,让我自己和读者都可以更加深刻的来理解泛型。我的这篇文章,采用的是问题引导式,层层递进,我相信只要读者可以跟着我的思路走,必定可以对泛型有一个新的认识。

什么是泛型?为什么引入泛型?

在谈泛型之前,我们先来看一段JDK5之前没有泛型的代码

public static void main(String[] args) {
        ArrayList list = new ArrayList();
        list.add(521);//添加 Integer 类型元素
        list.add("wan");//添加 String 类型元素
        list.add(true);//添加 Boolean 类型元素
        list.add('a');//添加 Character 类型元素
        Object item1 = list.get(0);//只能用 Object 接受元素
        list.forEach(item -> {
            //使用 item,这里的 item 类型是 Object,由于不知道 item 的确切类型,我们需要判断之后强转
            if (item instanceof Integer) {
                //执行业务...
            } else if (item instanceof String) {
                //执行业务...
            } else if (item instanceof Boolean) {
                //执行业务...
            } //继续判断类型...
        });
    }

没有泛型的时候,我们声明的List集合默认是可以存储任意类型元素的,乍一看你可能还会觉得挺好,这样功能强大,啥类型都可以存储。但是......在开发的时候由于不知道集合中元素的确切类型,遍历的时候我们拿到的item其实都是Object类型,如果要使用就必须强转,强转就必须得判断当前元素的具体类型,否则直接使用强转很可能会发生类型转换异常。这样就会让开发很不方便,每次都要额外做判断工作。

总结起来就不一句话:它不安全!

那么你可能已经想到了,我们在业务中不要把全部数据都存放在一个List就行了,在代码中定义多个List分类型使用。就像这样:

public static void main(String[] args) {
        ArrayList listInteger = new ArrayList();
        ArrayList listString = new ArrayList();
        ArrayList listBoolean = new ArrayList();
        //...这样就可以在不同的 list 中存入对应的类型数据
        //——————————————————————分割线————————————————————
        listString.add(121);//即使如此它还是无法限制,只能起到提示作用
    }

不知道你们可不可以想到这种方法,反正我是想到了哈哈哈,我一开始还以为这个方法很nice,但是当我了解到更多时,我发现这个方法其实是一坨勾丝,治标不治本。我们声明了listString是想让它只存储String类型,但是其实它仍然可以存储非String类型的其它类型的数据,而且更为重要的是这种类型转换异常通常只有在运行时才会被发现。我们需要一种机制能够强制性的让我们只能存储对应类型的元素,否则编译就不通过,所以泛型出现了。

事实上,泛型也是我们刚刚的思路,就是在实例化的时候给集合分配一个类型,限定一个集合只能存储我们分配的类型的元素。比如像这样子:

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("wan");
        list.add(521);//编译报错,只能存储 String 元素
        String str = list.get(0);//直接用 String 类型接受元素
        list.forEach(item -> {
            //这里 item 类型就是 String
        });
    }

有了泛型的指定,我们声明的list就只能存储规定类型String,当存储其他类型的元素时,编译器就会直接给我们报错(可以在IDEA开发环境中看看add方法,提示的参数类型就是String),这样雷类型不匹配的问题就在编译时就可以检查出来,而不会在运行时才抛出异常。而且当我们进行遍历,获取元素等操作时,get方法返回值就是String类型的。

所以总结一下泛型的好处

(1)编译期类型安全

(2)避免了强制类型转换运行时异常

(3)同一个类可以操作多种数据类型,代码复用

其实到这里我还没有给泛型下定义,别急,看完下一节泛型类再说。

泛型类

说到泛型类,最典型的例子就是上面我们说的ArrayList了。你有没有想过,为什么给ArrayList指定泛型后,就只能存储指定的类型,get(0)获取元素返回值就是指定的那个泛型类型?你看一下ArrayList源码就知道了:

//类定义
    public class ArrayList<E> extends AbstractList<E> implements List<E>
    
    //添加元素方法
    public boolean add(E e) {...}
    
    //获取元素方法
    public E get(int index) {...}

可以看到ArrayList在定义的时候制定了一个泛型<E>,并且下面的添加元素,获取元素等方法都是对这个E进行操作,我当时其实对这个比较懵.....这个E是什么鬼?其实这个E就是我们实例化ArrayList的时候指定的类型,当我们指定String,add方法的形参和get方法的返回值就是String类型。当我们指定Integer,add方法的形参和get方法的返回值就是Integer类型。这样一来,ArrayList这个类就被参数化了,当实例化ArrayList时传入不同的泛型就可以操作不同的类型。

当然我们也可以在一个类中个定义多个泛型参数,比如HashMap

public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>

说到这里,是时候回答一下前面我抛出的一个问题了,就是泛型的定义是什么?其实,泛型的本质就是把类型参数化,所操作的数据类型被指定为参数,根据动态传入进行处理。

自定义泛型类

上面我们看到的是JDK源码中泛型类,当然我们自己也可以定义泛型类,最常见的 就是我们曾经封装的 web应用后端返回结果。

public class ResultHelper<T> implements Serializable {
        private T data;
        private boolean success;
        private Integer code;
        private String message;

        private ResultHelper() {}

        public static <T> ResultHelper<T> wrapSuccessfulResult(T data) {
            ResultHelper<T> result = new ResultHelper<T>();
            result.data = data;
            result.success = true;
            result.code = 0;
            return result;
        }
    }

这是做项目经常会用的工具类,封装一个这样的数据结构给前端(不过听说现在大部分企业已经不这么用了)。这样一来,我的wrapSuccessfulResult 泛型方法的参数泛型T可以接受调用者任何参数。无论是订单数据还是商品数据等等,都可以封装。

泛型方法

有时候开发中我们会有这样一个需求,根据方法传入的参数类型不同,返回不同的返回值类型。前面所说的自定义泛型类wrapSuccessfulResult 方法就是典型的泛型方法,它只有一个泛型参数,我们还可以使用多个泛型参数:

public static <E, T> List<T> convertListToList(List<E> input, Class<T> clzz) {
        List<T> output = Lists.newArrayList();
        if (CollectionUtils.isNotEmpty(input)) {
            input.forEach(source -> {
                T target = BeanUtils.instantiate(clzz);
                BeanUtils.copyProperties(source, target);
                output.add(target);
            });
        }
        return output;
    }

例如上面这个方法(不用太过注重方法体),它的作用就是把一个类型E的List转换为另一个类型T的List。这里的 E和T都是开放的,随便调用者传递什么类型。

无界泛型通配符 “?”

"?" 代表不确定的类型,比如我大二的时候曾经写过一个后端的程序,那个程序里面有一个订单列表/详情的需求,我们都知道订单有待发货、待收货、已退款等不同页面,不同状态页面的数据又不一样。如果把所有字段都放在一个类中,那样的设计太难受了,代码看起来更难受(实际上那个程序刚开始就是这么干的,后来是我改的)。比如待收货有发货时间,待支付就没有,如果你用一个有发货时间字段的类来作为待支付详情的数据结构,那就不合适。所以我写了一个父类 AppOrderResponse 把所有页面共有的字段(订单id、订单编号、订单状态、下单时间等)放在父类,其他独有的再扩展子类,继承关系为:

这样根据不同状态返回对应的类型数据就行了,比如待发货列表就返回 AppOrderWaitDeliveredResponse 泛型类型,待收货列表就返回 AppOrderDispatchedResponse 泛型类型,不过我们一个接口要同时返回四种可能的类型,这该怎么办呢?也许你可能会这么想:

public IPage<AppOrderResponse> list(Page<AppOrderResponse> page, AppOrderQueryRequest request) {
        IPage<AppOrderDispatchedResponse> demo1 = new Page<>();
        IPage<AppOrderWaitDeliveredResponse> demo2 = new Page<>();
        return demo1;//报错
        return demo2;//报错
    }

将返回值定义为 IPage<AppOrderResponse> 类型,这样我就可以返回各种类型了,但其实这样会报错。AppOrderResponse 是 AppOrderWaitDeliveredResponse 的父类,但是 IPage<AppOrderResponse> 并不是 IPage<AppOrderWaitDeliveredResponse> 的父类,泛型中的继承不是你想的那样。此时就需要使用通配符 "?" 来解决。

public IPage<?> list(Page<AppOrderResponse> page, AppOrderQueryRequest request) {
        return appOrderService.list(page, request);
    }

这样一来我们使用通配符 "?" 之后返回任何 IPage 泛型都可以。但是这显然不合适,因为我们的类型是 AppOrderResponse ,不可能允许程序返回一个不属于 AppOrderResponse 的泛型类型。所以我们可以使用泛型的上界下界来控制。

泛型的上界下界

正如上面订单列表的例子,我们应该限定返回值的泛型仅为 AppOrderResponse 或者其子类,可以这么写:

public IPage<? extends AppOrderResponse> list(Page<AppOrderResponse> page, AppOrderQueryRequest request) {
        return appOrderService.list(page, request);
    }

这种写法叫做指定泛型的上界(上限),我们不能直接用 IPage<AppOrderResponse> 表示上界,但是可以使用 IPage<? extends AppOrderResponse>

其实这从 extends 和 super 关键字很容易理解:

IPage<? extends AppOrderResponse> //表示泛型最高类型是 AppOrderResponse,只能是它或它的子类
    IPage<? super AppOrderResponse> //表示泛型最低类型是 AppOrderResponse。只能是它或它的父类

泛型通配符 "?" 和 T、E、R、K、V 的区别

我相信这是广大同学最容易混淆的地方,毕竟源码中到处都是这些通配符,也看不出有什么区别。其实 T、E、R、K、V 对于程序运行没有区别,定义泛型的时候用 A-Z 中任何一个字母都可以,只不过我们上面的几个是约定俗成的,也算一种规范。

泛型擦除

所谓的泛型擦除其实很简单,简单来说就是泛型只在编译时起作用,运行时泛型还是被当成 Object 来处理,示例代码:

public static void main(String[] args) {
        ArrayList<String> list = new ArrayList<>();
        list.add("wan");//add 方法形参类型为 String
        String s = list.get(0);//get方法返回值类型为 String
        ArrayList<Integer> list2 = new ArrayList<>();
        System.out.println("list 和 list2 类型相同吗:" + (list.getClass() == list2.getClass()));//true 两个 ArrayList 是同一个类型的
        Method[] methods = list.getClass().getMethods();
        for (Method method : methods) {
            method.setAccessible(true);
            if (method.getName().equals("add")) {
                Class<?>[] parameterTypes = method.getParameterTypes();
                if (parameterTypes.length == 1) {
                    for (Class<?> parameterType : parameterTypes) {
                        System.out.println("add(E e) 形参 E 的类型为:" + parameterType.getName());//泛型的参数 E 运行时是 Object 类型
                    }
                }
            } else if (method.getName().equals("get")) {
                Class<?> returnType = method.getReturnType();
                System.out.println("E get(int index) 的返回值 E 的类型为:" + returnType.getName());
            }
        }
    }

运行结果如下:

list 和 list2 在运行时类型相同吗:true
add(E e) 形参 E 在运行时的类型为:java.lang.Object
E get(int index) 的返回值 E 在运行时的类型为:java.lang.Object

可以看到我们实例化 ArrayList 时虽然传入不同的泛型,但其实它们仍然还是同一个类型。对于 add 方法的形参和 get 方法的返回值,按道理说我们指定的泛型是 String 那么打印出来应该是 String 才对,但是这里运行时得到的却都是 Object,所以这就足以证明了,泛型在编译期起作用,运行时一律被擦除当做 Object 看待,这就是泛型擦除。

结语

泛型这个东西理解起来其实真的很简单,难的是如何把它用好,这个需要很强的编程功底、设计模式,我的建议是多看 JDK 源码、框架源码,看大牛是如何在框架中使用的。

文章来源:https://blog.csdn.net/qq_54432917/article/details/135771171
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。