浅谈 Java 中的泛型

文章目录
  1. 1. 简介泛型
  2. 2. Java 泛型
  3. 3. 参考文献

不积跬步,无以至千里;不积小流,无以成江海。

Java 中的泛型,在我们的 Android 开发中,用的也是非常多。用好用活泛型,可以大大地增加代码的效率和灵活性。Team 里即将离职的 Mentor 泛型用得蛮溜的,“临渊羡鱼,不如退而结网”,本期就来谈一谈 Java 中的泛型编程。

简介泛型

泛型程序设计是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化 (Instantiate) 时作为参数指明这些类型。各种程序设计语言和其编译器、运行环境对泛型的支持均不一样。比如,我们常见的 Java, C# 和 Swift 称之为泛型;Scala 称之为参数多态;C++ 称之为模板,等等。此外,具有广泛影响的 1994 年版的《Design Patterns》一书将其称为参数化类型。

泛型的定义主要有以下两种:

1.在程序编码中一些包含类型参数的类型,也就是说泛型的参数只可以代表类,不能代表个别对象。(这是当今较常见的定义)

2.在程序编码中一些包含参数的类。其参数可以代表类或对象等。(现在大多把这称作模板)

不论使用哪个定义,泛型的参数在真正使用泛型时,都必须作出指明。一些强类型程序语言支持泛型,其主要目的是加强类型安全及减少类转换的次数

Java 泛型的参数只可以代表类,不能代表个别对象。Java 泛型的类型参数,其实际类型在编译时会被消除,故无法在运行时得知其类型参数的类型,并且,无法直接使用基本值类型作为泛型类型参数。此外,Java 编译程序在编译泛型时会自动加入类型转换的编码,故运行速度不会因为使用泛型而加快。

由于运行时会消除泛型的对象实例类型信息等,此类缺陷经常被人诟病,Java 及 JVM 的开发方面也尝试解决这个问题,比如 Java 通过在生成字节码时添加类型推导辅助信息,从而可以通过反射接口获得部分泛型信息。通过改进泛型在 JVM 的实现,使其支持基本值类型泛型和直接获得泛型信息等。

以上来源于维基百科。

Java 泛型

Java 泛型应用是 Java 核心基础之一,从 Java 5 开始引入泛型。实际上,在 Java Collection 中使用泛型是一件很简单的事情,此外,泛型还有很多意想不到的作用。

I. 引入

Java 泛型的应用可以提高代码的复用性,同时,泛型提供了类型检查,减少了数据的类型转换,保证了类型安全,看下面泛型保证类型安全的例子:

1
2
3
4
5
6
List list = new ArrayList();
list.add("abc");
list.add(new Integer(1)); // 能通过编译
for (Object object : list) {
System.out.println((String) object); // 抛出 ClassCastException 异常
}

代码中,尝试将一个 Integer 转换为 String,在运行期会抛出 ClassCastException。来看看 Java 5 开始的 Collection 的用法:

1
2
3
4
5
6
List<String> list = new ArrayList<>();
list.add("abc");
//list.add(new Integer(1)); // 无法通过编译
for (String string : list) {
System.out.println(string);
}

代码中,List 的创建增加了类型参数 String,故只能向 List 中添加 String 类型对象,添加其他对象会抛出编译异常;同时,foreach 循环不需要再添加任何强制类型转换,也就移除了运行时的 ClassCastException 异常。

II. 泛型类

先看一个原始的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Gen {

private Object mObject;

public Object getObject() {
return mObject;
}

public void setObject(Object object) {
mObject = object;
}

public static void main(String[] args) {
Gen gen = new Gen();
gen.setObject("abc");
String str = (String) gen.getObject(); // 类型转换,或会引起运行时 ClassCastException
}
}

原始类的定义,易引发 ClassCastException,且看使用泛型类重新定义 Gen,即使用 <> 指定泛型参数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Gen<T> {

T mObject;

public T getObject() {
return mObject;
}

public void setObject(T object) {
mObject = object;
}

public static void main(String[] args) {
Gen<String> gen1 = new Gen<>();
gen1.setObject("abc");
// gen.setObject(10); // 无法通过编译
String str = gen1.getObject(); // 不需要类型转换
//----------------------------
Gen gen2 = new Gen(); // 原始类型
gen2.setObject("abc");
gen2.setObject(10); // 能通过编译,自动装箱将 10 转化为 Integer 对象
Integer integer = (Integer) gen2.getObject(); // 强制类型转换
}
}

main() 方法里,使用泛型类型 Gen,不再需要强制类型转换,也就移除了运行时的 ClassCastException。为了对照,定义一个没有使用泛型类型的 gen2,这时,会默认使用 Object 代替,因此,gen2 可以设置 String 和 Integer 类型。注意,尽量避免出现这种情况,否则,用到强制类型转换,也伴随着运行时的 ClassCastException。

注意:可以使用 @SuppressWarnings(“rawtypes”) 抑制编译器弹出警告。

III. 泛型接口

接口的泛型应用和类的泛型应用很类似,如下:

1
2
3
4
5
6
public interface List<E> {

void add(E x);

Iterator<E> iterator();
}

又如下:

1
2
3
4
5
6
public interface Iterator<E> {

E next();

boolean hasNext();
}

类似地,可以将此应用到自定义的类与接口中。此外,注意到,可以使用多个泛型参数来定义类与接口,比如 Map;同时,泛型类型也可以作为一个参数来使用,比如,常会使用到的new HashMap<String, List<String>>()形式。

IV. 泛型的命名规范

刚接触泛型时,会对尖括号里的字母感到奇怪。其实,为了与 Java 关键字区分开,Java 泛型参数只是使用一个大写字母来定义。各种常用泛型参数的意义如下:

E — Element,常用在 Java Collection 里,如:List,Iterator 和 Set 等;

K, V — Key 和 Value,代表 Map 的键值对;

N — Number,代表数字;

T — Type,代表类型,如 String,Integer 等;

S, U, V 等 — 第二、第三、第四种类型,和 T 的用法一样。

V. 泛型方法与构造函数

有时候,并不希望整个类都被泛型化,这时,可以只在某个方法上应用泛型。由于构造函数是一种特殊的方法,故也可以在构造函数上应用泛型。方法上应用泛型和调用泛型方法的实例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class GenMethod {

public static <T> void fromArrayToCollection(T[] array, Collection<T> collection) {
for (T t : array) {
collection.add(t);
}
}

public static void main(String[] args) {
Object[] objects = new Object[100];
Collection<Object> collection = new ArrayList<>();
GenMethod.<Object>fromArrayToCollection(objects, collection);
}
}

代码不多,要注意的地方不少。

其一:定义方法所用的泛型参数需要在修饰符之后添加上,如 public static ,若有多个泛型,定义成 或者

其二:不建议在泛型变量中添加其他类型,否则会引起编译错误(或隐含错误),如下:

1
2
3
4
5
6
public static <T> void fromArrayToCollection(T[] array, Collection<T> collection) {
for (T t : array) {
collection.add(t);
// collection.add(new Object()); // 无法通过编译
}
}

其三:调用方法 GenMethod.fromArrayToCollection(array, collection) 时,方法前声明泛型类型 Object,由于编译器可以推断该泛型类型,故也可以写成:

1
GenMethod.fromArrayToCollection(array, collection);

VI. 泛型参数的界限

有时候,会希望泛型类型只是某一部分类型,比如操作数据的时候,会希望是 Number 或其子类类型,也就是给泛型参数添加一个界限,定义形式为:

<T extends BoundingType>

T 表示 BoundingType 的子类型。T 和 BoundingType 可以是类或接口。注意,此处的extends表示子类型,不等同于继承,可以调用 BoundingType 上相对应的方法,此类形式如public static <T extends Integer> void method()

泛型参数可以添加多个限制范围如下:

<T extends A & B & C>

一个泛型参数可以有多重限制范围,使用&分隔,且限制范围至多有一个类。若用一个类作为限定,它必须是限定列表中的第一个,否则会产生编译异常。如下:

1
2
3
4
5
class A {/* ... */}
interface B {/* ... */}
interface C {/* ... */}

class D <T extends A & B & C> {/* ... */}

VII. 泛型方法与泛型参数界限的综合

若设计一个方法,统计在一个数组中比指定元素大的个数:

1
2
3
4
5
6
7
8
9
public static <T> int countBigger(T[] array, T elem) {
int count = 0;
for (T t : array) {
if (t > elem) { // 无法通过编译
++count;
}
}
return count;
}

操作符 “>” 只能用在基本数据类型 (byte, char 等),无法用来比较类对象之间的大小(除非实现了 Comparable 接口)。解决办法如下:

添加一个界限 Comparable

1
2
3
public interface Comparable<T> {
public int compareTo(T t);
}

然后,更改代码如下:

1
2
3
4
5
6
7
8
9
public static <T extends Comparable<T>> int countBigger(T[] array, T elem) {
int count = 0;
for (T t : array) {
if (t.compareTo(elem) > 0) { // 能通过编译
++count;
}
}
return count;
}

VIII. 泛型的继承与子类型

1.Integer 是 Number 的子类,但 Box 并不是 Box 的子类。

2.泛型的 “extends” 与继承的 “extends” 不同,泛型的 “extends” 后可以是一个类(如 T extends Number),也可以是一个接口(如 T extends List)。

3.泛型的 “extends” 代表子类型,而不是子类,或许可以将其等同于 “extends (继承)” 和 “implement” 的并集。

4.泛型里,也存在子类型,前提是其泛型参数的限制并没有改变。

至此,关于 Java 中的泛型总结完毕。

本人才疏学浅,如有疏漏错误之处,望读者中有识之士不吝赐教,谢谢。

1
Email: [email protected] / WeChat: Wolverine623

您也可以关注我个人的微信公众号码农六哥第一时间获得博客的更新通知,或后台留言与我交流

参考文献

1.http://peiquan.blog.51cto.com/7518552/1302898