Java—泛型

泛型出现的原因

Java的泛型是在JDK1.5开始才加上的。在此之前的Java是没有泛型的。
没有泛型的Java使用起来给人感觉非常的笨重,为了体会泛型带来的好处,
来看看如果没有泛型,我们将如何写代码,以下是样例。

List list = new ArrayList();
list.add(1);
list.add("Hello");
list.add("World");
list.add(2);
// 现在将list中Integer类型的数据求和,并输出结果
int res = 0;
for (Object obj : list) {
    if (obj instanceof Integer) {
        res += (Integer)obj;
    }
}
System.out.println(res); // 输出:3

由样例我们可以看出,没有泛型 -> 可以存储任意类型 -> 使用Object类型存储 -> 取出使用时需判断并强转类型。
以上流程可见其繁琐,这与当今愈见简洁的编程发展方向背道而驰。所以便有了Java泛型的出现。

泛型的演化

JDK1.5加入了泛型之后,Java代码的编写也开始变得简洁明了了。我们来试图使用泛型对上面的代码进行优化,以下是样例。

List list = new ArrayList();
list.add(1);
// list.add("Hello"); // 编译报错,不能在一个List中存入String
// list.add("World"); // 同上
list.add(2);
// 现在将list中Integer类型的数据求和,并输出结果
int res = 0;
for (Integer i : list) {
    res += i; // 本身就是Integer类型不需要类型判断和类型强转
}
System.out.println(res); // 输出:3

由优化后的样例可以看出,泛型的基本功能就是限制容器中能够存储的数据的类型。既然存任意类型会让程序在输出的时候需要大量的类型判断与类型强转,那索性就限制容器只能存放一种类型(包含其子类),这样就从源头解决了,写大量冗余代码的问题。

然而,只是这样的话真的没有问题吗?我们来看看下面这段代码。

List list1 = new ArrayList(); // 编译报错,List不能强转为List
List list2 = new ArrayList(); // 编译通过
List list3 = new ArrayList<>(); // 编译通过
list3.add(1);
list3.add("2");

为啥会有上面的现象呢。我们分析一下。

第一行,我们假设编译通过,来看看有什么问题。申明类型是Object,而实例类型是Integer。申明类型表示list可以存放的类型,而实例类型是想处理的类型。可以存放的类型与想处理的类型不一致,这就违反了泛型出现的初衷,泛型就是就是为了解决存放类型与实际想处理类型不一致导致需要大量冗余代码问题的。矛盾,所以编译报错。
第二行,同理可知,想处理的类型是Integer,可以存储的类型未指定。由于申明类型未指定所以不检测泛型,编译通过。
第三行,由第一行代码的结论可知,使用泛型时,申明类型与实际类型必须一致。既然如此,右边的类型一定等于左边的类型,那右边索性就不写了,由此有了这种省略的写法。

解释完上述问题后,会发现这样的泛型并不能满足泛型特性的使用需求,这无疑使用起来变得麻烦,虽然不用写大量的类型判断等代码,但作为方法参数时也没办法接收泛型为子类的对象了,样例如下。

// 定义方法
void test(List list) { /* do something */ }
// 其它代码省略
List list = new ArrayList<>();
// test(list); // 编译报错
List list1 = new ArrayList<>();
for (Integer i : list) {
    list1.add(i);
}
test(list1);

难道为了传参还需要把list中的Integer取出一个个放入List

我们先来看看什么是通配符,以及通配符有什么特性。

// 不使用通配符
List list1 = List.of(1, 2); // 编译通过
list1.add("1"); // 编译通过
list1.add(2); // 编译通过
// 使用通配符?

List list2 = List.of(1, 2);
// list1.add("1"); // 编译报错
// list1.add(2); // 编译报错
for (Object obj : list2) { // 默认使用Object接收
    // ...

}

为什么会有上述现象?通配符?代表着不知道是何类型,那么就有了两个可能的发展放心,一是为了存储方便,使其可以存储任何类型的数据;二是为了元素取出后使用方便,使其不可以再存储任何类型的数据。因为Java泛型的基本职责是限制容器里存储的类型,所以这里不再能追加存储任何类型。

单纯的通配符会不会有什么限制,或者不方便的地方呢?一起来看看下面这段代码。

// 定义一个方法,使用通配符
int sum(List list) {
    int res = 0;
    for(Object obj : list) {
        res += (Integer)obj;
    }
    return res; // 返回集合中元素的和
}
// 省略调用位置的方法定义等
int sum = sum(List.of("2", "1", "3")); // 编译通过
// 上述代码执行后RuntimeException

由上述案例可知,单纯的通配符使用起来并不方便,而且容易出现意料之外的bug,为了解决这个问题,Java又拓展了两个新的性质:通配符的上界与下界。

语法:
变量赋值时,实例的泛型允许使用Integer以及Integer的子类

使用通配符上界对上文的代码进行优化。

// 定义一个方法,使用通配符上界
int sum(List list) {
    // list.add(4); // 编译报错
    int res = 0;
    for(Integer i : list) { // 默认使用Integer接收
        res += i;
    }
    return res; // 返回集合中元素的和
}
// 省略调用位置的方法定义等
// int sum = sum(List.of("2", "1", "3")); // 编译报错
int sum = sum(List.of(2, 1, 3)); // 编译通过

由上述案例可知,虽然还是通配符,虽然还是不知道是啥类型,但知道一个限制,是Integer类型或Integer的子类(虽然没有子类),并且不能追加元素。原因是定义当前容器的实例所有的元素都得是Integer或者其子类的其中一种类型,所有元素都得是这种类型,这点符合泛型的基本职能。又因为并不知道具体类型,即便知道一定是Integer及其子类也不能确定添加一个任意的Integer或者其子类的元素是不是当前实例类型或者其子类。所以干脆不允许添加。

有上界就有下界,接下来我们看看通配符的下界。

语法:
变量赋值时,实例的泛型允许使用Integer以及Integer的父类

// 定义一个方法,使用通配符上界
int sum(List list) {
    list.add(4); // 存入一个Integer的元素,通过
    int res = 0;
    for(Object obj : list) { // 默认使用Object接收
        res += (Integer)obj;
    }
    return res; // 返回集合中元素的和
}
// 省略调用位置的方法定义等
int sum = sum(new ArrayList()); // 编译通过

由上述案例可知,使用通配符下界又能向容器里添加元素。这什么情况?其实不难理解,因为实例的泛型必须为Integer或者Integer的父类,那么我们添加Integer类型(包括子类)时满足Java泛型,也就不会报错可以正常使用了。

Java的泛型并非真正的泛型,只存在于编译阶段,编译过后则不再保留泛型。虽然可以通过反射获取类中定义的泛型信息,但对象中并没有,见样例。

List list1 = new ArrayList<>();
List list2 = new ArrayList<>();
System.out.println(list1.getClass().getSimpleName()); // 输出:List
System.out.println(list2.getClass().getSimpleName()); // 输出:List

上述输出都是List而非List

泛型的使用

泛型类是泛型最常见的使用方式。
定义容器中存放的数据的类型。
将类型参数化,有调用方来决定容器中的数据使用怎样的类型,如案例。

class Data { // 只能制定Number及其子类
    T t; // 传入Integer则T为Integer
    public Data(T t) {
        this.t = t;
    }
}

泛型方法的特性是可以在方法定义时同时定义泛型(与反省类一样可以多个)。
并且该泛型是独立于泛型类与其他泛型方法的泛型的,换而言之就是泛型方法所定义的泛型既不影响泛型类,也不影响其他的泛型方法,反之也不会受到泛型类与其他泛型方法的影响。详见样例。

class Test {
    T t; // 成员变量可以使用泛型
    void test(T t) {
        System.out.println("泛型类");
    }
     void test(T t) { // 使用泛型方法中定义的T, 并且与使用类定义泛型的同名方法相互重载
        System.out.println("泛型方法");
    }
     void test() {
        T t = (T)Integer.valueOf(10); // 类的泛型也可以在局部变量中使用
        Function f = (o) -> (R)o.toString();
        R r = f.apply(t); // 方法的泛型也可以在局部变量中使用
    }
    //  void test(E e) {
    //    System.out.println("泛型方法");
    // }
    /*
         void test(E e)与第6行的方法重复,编译报错
     */
}
// 省略其他代码
Test test = new Test<>();
// test.test(new Object()); // 编译报错,泛型方法无需指定会被自动推定出类型
test.test(new Object()); // 输出:泛型方法
test.test(1); // 输出:泛型类

由上述样例可知,泛型方法也可以重载,但如果只是使用的泛型(T,E,R等)不同入参列表的数量、顺序都相同会被认为方法重复定义。泛型方法中定义的泛型与泛型类的泛型重复时,泛型方法使用方法定义的泛型。

上述的泛型方法是成员方法,静态方法能不能也使用泛型呢?

class Test {
    // static void staticTest(T t) {
    //     System.out.println("静态方法");
    // }
    /*
        事实上上面这种写法会编译报错,
        原因是泛型类定义的泛型属于实例,只会在实例化的时候被指定
        而静态方法是属于类的,当我们用类名调用静态方法的时候泛型还未指定
        有的朋友就会说了:那静态方法还可以用对象调用呀,创建对象的时候不是指定了吗?
        确实如此,但这经不起推敲,类名调用未指定,对象调用已指定,
        为了添加一个泛型的性质,难道要让使用了泛型的静态方法都必须经由对象调用吗?
        这难道不是违背了静态方法出现的初衷吗?只有一份,隶属于类,推荐使用类名调用等。
        所以这种方式被舍弃了。
        那么,静态方法就不能用泛型了吗?是吗,我们接着看。
     */
    static  void staticTest(T t) { // 编译通过
        System.out.println("静态泛型方法");
    }
    static  void staticTest(T t) {
        Function f = (o) -> (R)o.toString();
        R r = f.apply(t); // 静态方法中的局部变量也可以使用方法的泛型。
    }
}

如上述,静态方法不可以直接使用泛型类中定义的泛型,如要使用泛型必须定义为静态泛型方法。

多说一句,结合程序的设计如无必要则尽量使用泛型方法而非泛型类。

与普通类间继承相比,泛型类的继承只是多了泛型的指定而已,见样例。

class A {}
class B extends A {} // 泛型类继承泛型类时,子类定义需包含父类的泛型
class C extends A {} // 非泛型类继承泛型类时,子类定义需指定父类泛型的实际类型

interface D {}
interface E implements D {} // 泛型接口实现泛型接口时,子接口定义需包含父接口的泛型
interface F implements D {} // 非泛型接口实现泛型接口时,子接口定义需指定父接口泛型的实际类型

由于Java对象的实例顺序是先父类后子类,所以子类必须包含父类的泛型或者指定父类泛型的实际类型。否则,实例过程中无法指定父类泛型。

  • 泛型的演化
  • 泛型的基本职能:统一容器中的元素类型。
  • 泛型通配符:实例的泛型没有限定,可以为任何类型,但不能添加新元素。
  • 泛型通配符上界:实例的泛型限定为指定类型及其子类,且不能添加新元素。
  • 泛型通配符下界:实例的泛型限定为指定类型及其父类,且能添加新元素。
  • 泛型的类型擦除:Java的泛型只存在于编译阶段,编译过后泛型便不复存在。
  • 泛型的使用
  • 泛型类:将容器中部分数据的类型定义为参数,将类型指定延迟到实例化。
  • 泛型方法:不使用泛型类的泛型,而是定义独属于当前方法的泛型,是的程序设计更为灵活。
  • 静态泛型方法:由于类与实例的关系,不能使用类的泛型,必须在方法中定义泛型并使用。
  • 泛型类继承泛型类:子类需包含父类泛型。(接口同理)
  • 非泛型类集成泛型类:子类需指定父类泛型。(接口同理)

本文中比较难懂的部分就是通配符的上下界,建议结合实践来消化这部分内容。

Original: https://www.cnblogs.com/buzuweiqi/p/16637299.html
Author: buzuweiqi
Title: Java—泛型

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/584790/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Nginx 负载均衡

    Nginx简单实现网站的负载均衡 地址:http://www.cnblogs.com/alvin_xp/p/4161162.html Original: https://www.c…

    Java 2023年5月30日
    074
  • java实现空心金字塔

    前言 最近在学习java,遇到了一个经典打印题目,空心金字塔,初学者记录,根据网上教程,有一句话感觉很好,就是先把麻烦的问题转换成很多的简单问题,最后一一解决就可以了,然后先死后活…

    Java 2023年6月13日
    0116
  • 密码学入门

    原创:打码日记(微信公众号ID:codelogs),欢迎分享,转载请保留出处。 简介 在信息安全领域,一般会遇到”窃听”、”篡改”、…

    Java 2023年6月7日
    091
  • SpringCloud微服务之Ribbon负载均衡(一)

    什么是微服务?什么是SpringCloud? 微服务是一种架构的模式,它提倡将一个应用程序划分成很多个微小的服务,服务与服务之间相互协调、相互配合。每个服务运行都是一个独立的进程,…

    Java 2023年6月7日
    088
  • SpringBoot学习(十一)创建自己的自动配置和Kotlin支持

    一、创建自己的自动配置 如果您在开发共享库的公司工作,或者在开源或商业库上工作,您可能希望开发自己的自动配置。自动配置类可以绑定在外部jar中,并且仍然可以通过Spring Boo…

    Java 2023年5月30日
    082
  • 根据表结构自动生成JavaBean,史上最强最专业的表结构转JavaBean的工具(第12版)

    目录: 第1版:http://blog.csdn.net/vipbooks/article/details/51912143 第2版:http://blog.csdn.net/vi…

    Java 2023年6月9日
    0105
  • 运用Spring Aop,一个注解实现日志记录

    运用Spring Aop,一个注解实现日志记录 1. 介绍 我们都知道Spring框架的两大特性分别是 IOC (控制反转)和 AOP (面向切面),这个是每一个Spring学习视…

    Java 2023年6月8日
    094
  • 实用向—总结一些唯一ID生成方式

    Redis Incr 命令会将 key 中储存的数字值增一。如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。 这里以jedis为例提供两种…

    Java 2023年6月9日
    091
  • Spring 源码(17)Spring Bean的创建过程(8)Bean的初始化

    在实例化 createInstance时大致可以分为三种方式进行实例化: 使用 Supplier 进行实例化,通过 BeanFactoryPostProcessor对 BeanDe…

    Java 2023年6月14日
    097
  • java_抽象类和接口

    1.抽象类: 1.抽象类之所以被称为抽象类,就是因为它包含有抽象方法,只要含有抽象方法的类就叫抽象类。 2.抽象类中可以没有抽象方法,也可以抽象方法和非抽象方法共存。 3.抽象类和…

    Java 2023年6月5日
    0126
  • Java代码中System.currentTimeMillis()方法具有什么功能呢?

    转自:http://java265.com/JavaCourse/202111/1749.html 下文笔者讲述System.currentTimeMillis()方法的具体功能,…

    Java 2023年6月15日
    0102
  • Day13 note

    super注意点: 1、super调用父类的构造方法,必须在构造方法的第一行 2、super必须只能出现在子类的方法或者构造方法中 3、super和this不能同时调用构造方法对比…

    Java 2023年6月5日
    085
  • 【Android】线程池原理及Java简单实现

    线程池简介 多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。 假设一个服务器完成一项任务所需时间为: T1 创建线程…

    Java 2023年5月29日
    081
  • JavaCV的摄像头实战之七:推流(带声音)

    借助JavaCV,完成本地摄像头和麦克风数据推送到媒体服务器的操作,并用VLC验证 欢迎访问我的GitHub 这里分类和汇总了欣宸的全部原创(含配套源码):https://gith…

    Java 2023年6月8日
    0103
  • Golang多线程垂直输出字符串

    [本文出自天外归云的博客园] 三个字符串,abc,def,ghi,请用多线程顺序输出:adg,beh,cfi 抛砖引玉,我的代码如下: go;gutter:true; packag…

    Java 2023年5月29日
    081
  • 1-Java基础

    一、基础常识 软件:及一些列按照特定顺序组织的计算机数据和指令的集合。分为:系统软件和应用软件。系统软件:windows , mac os , linux ,unix,androi…

    Java 2023年6月7日
    088
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球