Java 便利贴之泛型

作者: 樱木天亥 | 来源:发表于2018-10-06 15:43 被阅读20次

为什么要使用泛型?

Java 语言以严谨著称,但在设计的时候忽略了「泛型」这个重要的概念,增加了使用者的责任,使用者需要记住每个元素的类型,同时还得强制转型,编译器无法帮忙,在运行时才会抛出 Class Cast 异常。

泛型是什么?

Java 5 以后,引入了参数化类型(parameterized type),Java 的参数化类型被称为泛型(Generic)。所谓泛型,就是允许在定义类、接口、方法时使用类型形参,这个类型形参(或叫泛型)将在声明变量、创建对象、调用方法时动态地指定(即传入实际的类型参数,也可称为类型实参)。

深入泛型

上述代码中,向 List 中添加了一个字符串和整数,看起来没问题,但是在使用的时候,我们必须要知道第一个元素是字符串类型,第二个元素是整型,并且还得强制转换,否则运行时会报错。这大大增加了使用者的责任,编译器无法帮忙,只有在运行时才会抛出 Class Cast 异常。

那么怎么解决这个问题呢?答案就是使用泛型

可以看看上面代码,其中的 T 我们可以把它想象成一个占位符,将来可以传入任意类型,如 Integer、String 等。同时我们也可以看到上面泛型的定义也比较简单,除了尖括号中的内容——这就是泛型的实质:允许在定义类、接口和方法时声明泛型形参,泛型形参在整个接口、类体内可当成类型使用,几乎所有可使用普通类型的地方都可以使用这种泛型形参。

可是说来轻巧,那么泛型又是如何实现的呢?

每次实例化一个泛型/模板类都会生成一个新的类。例如模板类是 List,用 int、double、string、Employee 分别去实例化,那么在编译的时候,就会生成 4 个新类,如 List_int、List_double、List_string、List_Employee。

PS   包含泛型声明的类型可以在定义变量、创建对象时传入一个类型实参。从而可以动态地生成无数多个逻辑上的子类,但这种子类在物理上是不存在的。所以即使生成了很多新类,但也不必担心系统会膨胀爆炸。

因为 Java 中对此使用的是擦除法,简单来说就是一个参数化的类型经过擦除后会去除参数,如 ArrayList<T> 会被擦除为 ArrayList。我们传入的 String、Integer 等非但没有消失,反而都变成了 Object,如 ArrayList<Integer>被擦除成了原始的 ArrayList。

但是问题又来了,我们使用泛型是为了避开强制转型而减少使用者的责任,而现在类型被擦除了,都变成了 Object,本来可以写成  Integer i = list1.get(0);,但现在被擦除成了 Object,我们该怎么处理呢?

其实很简单,就是在编译的时候做一点手脚,加一个自动的转型,如  Integer i = (Integer)list1.get(0);

泛型方法

前面说的是泛型类,但是对于一些静态方法来说,怎么将其变成泛型呢?其实很简单,就是将泛型类型 <T> 移到方法上去。如下例子:

很显然,这个静态方法是求最大值的,也就是说需要对 List 中的元素比较大小。但是如果传入的 T 没有实现 Comparable 接口,那么就没法比较大小。所以我们需要做一个类型的限制,限制传入的类型 T 必须是 Comparable 的子类,否则编译器报错。如下:

不过除了使用 extends 关键字之外,Java 泛型的限制符还支持 super,实际上为了更灵活使用,上面的 Comparable<T> 应该写成 Comparable<? super T>。

泛型和继承

类图
代码清单

从上述类图和代码中,我们发现当传递一个 ArrayList<Apple>时编译会出错,可是这到底是怎么回事儿呢? print() 方法能接收的参数不是 ArrayList<Fruit> 吗?

其实这里存在一个误区,就是 Apple 虽然是 Fruit 的子类,但是 ArrayList<Apple> 却不是 ArrayList<Fruit> 的子类,实际上它们之间是没有任何关系的,不能执行转型操作,所以在调用 print() 方法的时候会报错。

【注】如果 Foo 是 Bar 的一个子类型(子类或者子接口),而 G 是具有泛型声明的类或接口,G<Foo> 并不是 G<Bar> 的子类型。

可是为什么不能将 ArrayList<Apple> 转换成 ArrayList<Fruit> 呢?那是因为如果可以这么做,那么我们不但可以向这个 Lsit 中加入 Apple,还能加入 Orange,泛型就被破坏了。

那么对于这个问题要怎么解决呢?可以通过引用通配符的方式加以解决,可以将方法的输入参数修改如下:



也就是说,传进来的参数只要是 Fruit 或者 Fruit 的子类都可以,这样一来就可以接收 ArrayList<Fruit>、ArrayList<Apple> 和 ArrayList<Orange> 这样的参数了。但是有一点需要注意的是,这种带通配符的 List 仅表示它是各种泛型 List 的父类,并不能向其中添加元素,比如如下代码就会引起编译错误。这是因为程序无法确定集合是哪种类型,所以只能对集合进行读操作,而不能向其中添加数据。



本文参考

《码农翻身》
《疯狂Java讲义》
《一文读懂Java泛型中的通配符?》(趣谈编程)

相关文章

  • Java 便利贴之泛型

    为什么要使用泛型? Java 语言以严谨著称,但在设计的时候忽略了「泛型」这个重要的概念,增加了使用者的责任,使用...

  • 夯实 Java 基础1 - 泛型的使用与原理

    Java 泛型为我们写安全易读的通用程序提供了便利,Go 也将在 2.0 引入泛型。下面首先介绍 Java 泛型的...

  • Java泛型教程

    Java泛型教程导航 Java 泛型概述 Java泛型环境设置 Java泛型通用类 Java泛型类型参数命名约定 ...

  • Java中的泛型/范型

    维基百科中关于Java泛型的描述 Java 泛型的参数只可以代表类,不能代表个别对象。由于Java泛型的类型参数之...

  • Java高级语言特性之泛型

    Java高级语言特性之泛型 Java泛型(generics)是JDK 5中引入的一个新特性,泛型提供了编译时类型安...

  • Java基础之泛型

    Java基础之泛型 泛型基本介绍 简单介绍在jdk1.6之前,还没有使用到泛型,Java类型分为原始类型、复杂类型...

  • 第二十八课:泛型

    泛型出现之前 泛型出现之后 Java深度历险(五)——Java泛型

  • Kotlin 泛型

    说起 kotlin 的泛型,就离不开 java 的泛型,首先来看下 java 的泛型,当然比较熟悉 java 泛型...

  • java泛型中类型擦除的一些思考

    java泛型 java泛型介绍 java泛型的参数只可以代表类,不能代表个别对象。由于java泛型的类型参数之实际...

  • 详解Java泛型之4——一个例子理解泛型带来的好处

    前面我介绍了关于泛型、通配符以及泛型擦除的相关知识点,大家可以参考以下文章: 详解Java泛型之1——入门泛型必懂...

网友评论

    本文标题:Java 便利贴之泛型

    本文链接:https://www.haomeiwen.com/subject/tdshaftx.html