概述
在复杂的项目中,bugs总是相伴其一生。通过详细的计划,编码和测试可以减少bug的存在,但是,他们还是会以某种形式,在某个地方溜进你的程序中,而且随着项目越来越复杂和庞大,这种情况就越明显。
好在,一些bug更容易检测出来,这就是编译时bug。如果在编译时就会报错,这时你可以根据报错信息找到代码到位置并修复。但运行时bug,就比较麻烦。因为运行时bug不会立即出问题,而是等程序上线跑起来之后才出问题,而且往往出问题的地方和造成bug的地方相去甚远。
所以,说白了,泛型就是要给你的代码增加健壮性,尽可能在编译时就可以检测出来一些bug来。
为什么用泛型
简单说,泛型就是定义类时,让类型变成参数。
类型参数让你可以重复利用相同的代码,但可以有不同的输入类型。
与普通方法中的参数不同的是,方法中的是输入是变量的值,这里的输入是参数是类型。
使用泛型的代码可以带来以下的好处
- 强类型检查
编译器会做强类型检查,修复编译错误可比修复运行时错误要更容易。
- 省去类型转换
下面没用泛型的代码,需要显示的类型转换
List list = new ArrayList();list.add("hello");String s = (String) list.get(0);
而使用泛型重写后,就不需要自己类型转换(cast)了
List list = new ArrayList();list.add("hello");String s = list.get(0); // no cast
- 可以写出泛型的算法
例如Java集合框架,在使用泛型后,集合就可以应用在不同的类型上了。
如何定义泛型类型
泛型类型generic type就是一个泛型的类或接口,通过类型参数化了。
下面用Box类来说明这个概念。
简单的box类
Box是一个类,为了可以操作任何类型的对象,所用的属性的类型是Object。
public class Box { private Object object; public void set(Object object) { this.object = object; } public Object get() { return object; }}
由于它的方法接受和返回的是Object对象,你可以想传什么就传什么,传原始类型都可以。
这样就没有办法在编译阶段做检查。
这时有可能set了一个Integer,而get时以为是一个String,出现运行时错误。
Box box = new Box(); box.setObject(123); Object object = (String) box.getObject();// Exception in thread "main" java.lang.ClassCastException: // java.lang.Integer cannot be cast to java.lang.String System.out.println(object);
泛型版本的Box类
下面祭出泛型版本的Box类,看是如何解决这个问题的。
泛型类定义的形式如下
class name { /* ... */ }
类型参数的部分跟在类名后面,用尖括号括起来,这表示类型参数(也叫类型变量)就是T1,... Tn,类型变量可以在类中的任何地方使用。
下面是用泛型重写后的Box类
public class BoxT { // T 代表 Type的意思 T object; public T getObject() { return object; } public void setObject(T object) { this.object = object; }}
可以看到,以前Object出现的地方被T所代替了。 类型变量可以是任何的非原始类型,包括任何类,任何接口,任何数组,甚至是另一个类型变量。
这里演示一下使用泛型后和上面的区别
public static void main(String[] args) { BoxT b = new BoxT(); b.setObject(123); // 变化一,由于实例化时,已经指定了 类型的实际参数是Integer, // 因此这里就只能set一个Integer,不能传入其他的类型, // 编译器会帮助检查错误 Integer integer = b.getObject(); // 变化二,这里不需要强转了,因为编译器能判断这里类型就是Integer。 System.out.println(integer); }
类型参数名的习惯叫法
通常类型参数名是单个的大写字母,这样做的原因就是为了区分类型变量和普通的类名和接口名。
常用的参数名有
- E - Element (used extensively by the Java Collections Framework) 广泛的用于集合框架。
- K - Key
- N - Number
- T - Type
- V - Value
- S,U,V etc. - 2nd, 3rd, 4th types
如何实例化一个泛型类
需要将类定义中的T替换成一个具体类型,如Integer
Box integerBox;
就像是传了一个参数一样。
一般在术语上,type parameter是类中定义的形式参数,type argument是实例化时传的实际参数。
Box也叫参数化的类型。
实例化的代码
Box integerBox = new Box();
钻石操作符 Diamond
JDK7以后,只要编译器可以根据上下文来推断出类型参数,在初始化时,就不必要将类型传给构造函数,使用空的尖括号就可以,这种尖括号也称为“钻石操作符”,如此,上面的实例化代码可以写成下面这样。
Box integerBox = new Box<>();
在new后面的类上,就不用指定T的类型了,但是<>还是保留着,这个非常必要,如果<>也去掉的话,就会变成下面要讲到的raw类型了。
多个类型参数
上面的类中只有一个类型参数,其实泛型类型可以有多个类型参数,如下面的代码
public class OrderedPair implements Pair { private K key; private V value; public OrderedPair(K key, V value) { this.key = key; this.value = value; } @Override public K getKey() { return key; } @Override public V getValue() { return value; }}
下面实例化了两个OrderedPair类。
Pair p1 = new OrderedPair("Even", 8);Pair p2 = new OrderedPair("hello", "world");
同样由于编译器可以推断类型,同样可以使用钻石操作符,上面的实例化可简化为:
Pair p1 = new OrderedPair<>("Even", 8);Pair p2 = new OrderedPair<>("hello", "world");
参数化的类型
泛型类的类型参数本身可以是另一个参数化的泛型类型,如
OrderedPair> p = new OrderedPair<>("primes", new Box());
泛型的raw类型
像上面说的,如果在实例化时,丢掉了<>,不向泛型类传递实际的类型会怎么样呢?
如果对于泛型的类,没有提供类型的实际参数,这就是泛型类的raw类型(raw type),使用raw类型,本质上退化成了泛型出现之前的代码,也就是Object的情况。
例如
Box rawBox = new Box();List box = new ArrayList();
注意,普通的类,即非泛型的类不是raw类型。
raw type 返回的是Object,这里为了向前兼容,将参数化的类型赋值给 raw 类型是合法的。
Box stringBox = new Box<>();Box rawBox = stringBox; // OK
但如果将一个raw类型,赋值给一个参数化的类型,将会收到一个警告。
List box = new ArrayList(); // warning: unchecked conversion
另外 如果使用raw type调用泛型方法,也会得到一个警告。
List box = new ArrayList(); box.add("1"); warning: unchecked invocation to add(T)
Unchecked Error Messages
那这些警告是什么意思呢?还记得编译器可以通过泛型来做强类型检查吗?
但当使用了raw type时,编译器没有足够的信息来做类型检查,所以就会报一个unchecked 的警告。
所以尽量不要使用raw type。
本文就介绍到这里,下一篇Java中的泛型generics从0到1(中)的内容包括 泛型的方法,有界的类型参数,泛型通配符,有上界的通配符,类型擦除等。
本文为《Java中的泛型系列》第一篇,其他还有包括
Java中的泛型generics从0到1(上)
Java中的泛型generics从0到1(中)
Java中的泛型generics从0到1(下)
Java中的泛型generics从0到1(甲)
Java中的泛型generics从0到1(乙)
Java中的泛型generics从0到1(丙)
Java中的泛型generics从0到1(丁)