Java Generic

What


什么是泛型,看表面的意思,泛型就是指广泛的、普通的类型。在 Java 中是指把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型

ArrayList 就是使用泛型的典型案例,如下所示:

1
2
3
List<String> strList = new ArrayList<String>();
List<Integer> intList = new ArrayList<Integer>();
List<Double> doubleList = new ArrayList<Double>();

可以看到,通过菱形语法(’<>’)可以将 List 内元素的类型限定为 String、Integer 和 Double 类型。需要注意的是 <> 内的类型只能是引用类型,当然对于基本类型,可以使用对应的包装类型

Why


没有泛型会怎样?举个例子:实现两个能够设置点坐标的类,分别设置 Integer 类型的点坐标和 Float 类型的点坐标,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 设置 Integer 类型的点坐标
class IntegerPoint {
private Integer x; // 表示 X 坐标
private Integer y; // 表示 Y 坐标
public void setX(Integer x){
this.x = x;
}
public void setY(Integer y){
this.y = y;
}
public Integer getX(){
return this.x;
}
public Integer getY(){
return this.y;
}
}
// 设置 Float 类型的点坐标
class FloatPoint {
private Float x; // 表示 X 坐标
private Float y; // 表示 Y 坐标
public void setX(Float x) {
this.x = x;
}
public void setY(Float y) {
this.y = y;
}
public Float getX() {
return this.x;
}
public Float getY() {
return this.y;
}
}

大家可以发现一个问题:它们除了变量类型不一样(一个是 Integer 一个是 Float)以外,其它并没有什么区别。那我们能不能合并成一个呢?

答案是可以的,因为 Integer 和 Float 都是派生自 Object 的,我们用下面这段代码代替:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class ObjectPoint {
private Object x;
private Object y;
public void setX(Object x) {
this.x = x;
}
public void setY(Object y) {
this.y = y;
}
public Object getX() {
return this.x;
}
public Object getY() {
return this.y;
}
}

即全部都用 Object 来代替所有的子类。在使用的时候是这样的:

1
2
3
ObjectPoint integerPoint = new ObjectPoint();
integerPoint.setX(new Integer(100));
Integer integerX = (Integer) integerPoint.getX();

在设置的时候,使用 new Integer(100) 来新建一个 Integer 对象,然后在取值的时候,进行强制转换。由于设置的类型和强转的类型是一致的,所以不会出错。

同理,FloatPoint 的设置和取值也是类似的,代码如下:

1
2
3
ObjectPoint floatPoint = new ObjectPoint();
floatPoint.setX(new Float(100.12f));
Float floatX = (Float) floatPoint.getX();

但是上面的代码很容易发生运行时异常,且编译是不会报错。比如我们改成下面这样:

1
2
3
ObjectPoint floatPoint = new ObjectPoint();
floatPoint.setX(new Float(100.12f));
String floatX = (String) floatPoint.getX();

因为编译器也不知道你传进去的是什么,而 floatPoint.getX() 返回的类型是 Object,所以编译时,将 Object 强转成 String 是成立的。必然不会报错。而在运行时,floatPoint 实例中明明传进去的是 Float 类型的变量,非要把它强转成 String 类型,肯定会报类型转换错误的!

那有没有一种办法在编译阶段,即能合并成同一个,又能在编译时检查出来传进去类型不对呢?当然,这就是泛型。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Point<T> {
private T x;
private T y;
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
public T getY() {
return y;
}
public void setY(T y) {
this.y = y;
}
}

如果使用不当,编译器会提示错误信息。

这样代码就不需要强制转换,而且只要在编译时期没有出现警告,那么运行时期就不会出现 ClassCastException 异常。

Type & How


1. 泛型类

泛型类也就是把泛型定义在类上,这样用户在使用类的时候才把类型给确定下来。

1
2
3
4
5
6
7
8
9
10
11
class Point<T> {
private T x;
public T getX() {
return x;
}
public void setX(T x) {
this.x = x;
}
}

可以看到上面这个程序,在使用时如果定义了类型,那么在使用时就可以不用进行强制类型转换,直接就可以得到一个 T 类型的对象。

1
2
3
4
5
6
7
8
9
// IntegerPoint 使用
Point<Integer> p = new Point<Integer>() ;
p.setX(new Integer(100)) ;
System.out.println(p.getX());
// FloatPoint 使用
Point<Float> p = new Point<Float>() ;
p.setX(new Float(100.12f)) ;
System.out.println(p.getX());

下面来看看泛型是怎么定义及使用的吧。

  • Point<T>
    即在类名后面加一个尖括号,括号里是一个大写字母。这里写的是 T,其实这个字母可以是任何大写字母,意义是相同的。
  • T
    这个 T 表示派生自 Object 类的任何类,比如 String、Integer、Double 等等。这里要注意的是,T 一定是派生于 Object 类的。为方便起见,大家可以在这里把 T 当成 String,即 String 在类中怎么用,那 T 在类中就可以怎么用!所以它可以定义变量,作为返回值,作为参数传入。
1
2
3
4
5
6
7
8
9
10
// 定义变量
private T x ;
// 作为返回值
public T getX(){
return x ;
}
// 作为参数
public void setX(T x){
this.x = x ;
}

使用方式参考上面,不再表述。

2. 泛型变量

上在我们只定义了一个泛型变量T,那如果我们需要传进去多个泛型要怎么办呢?只需要类似下面这样就可以了:

1
2
class MorePoint<T, U> {
}

也就是在原来的 T 后面用逗号隔开,写上其它的任意大写字母即可。想加几个就加几个,比如我们想加五个泛型变量,那应该是这样的:

1
2
class MorePoint<T, U, A, B, C> {
}

举个例子,我们在 Point 上再另加一个字段 name,也用泛型来表示,那要怎么做?代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MorePoint<T,U> {
private T x;
private T y;
private U name;
public void setX(T x) {
this.x = x;
}
public T getX() {
return this.x;
}
// 代码省略...
public void setName(U name){
this.name = name;
}
public U getName() {
return this.name;
}
}

使用方式如下:

1
2
3
4
// 使用
MorePoint<Integer, String> morePoint = new MorePoint<Integer, String>();
morePoint.setName("xian");
Log.d(TAG, "morPont.getName:" + morePoint.getName());

从上面的代码中,可以明显看出,就是在新添加的泛型变量 U 用法与 T 是一样的。上面提到任意一个大写字母都可以,他们的意义是完全相同的,但为了提高可读性,大家还是用有意义的字母比较好,一般来讲,在不同的情境下使用的字母意义如下:

  • E — Element,常用在 java Collection 里,如:List、Iterator、Set
  • K, V — Key, Value,如:代表 Map 的键值对;
  • N — Number,数字;
  • T — Type,类型,如 String,Integer 等等。

使用约定俗成的大写字母,可以提高可读性。

3. 泛型接口

在接口上定义泛型与在类中定义泛型是一样的,代码如下:

1
2
3
4
5
interface Info<T> {
// 定义抽象方法,抽象方法的返回值就是泛型类
public T getVar(); 型
public void setVar(T x);
}

使用方式一:非泛型子类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class InfoImpl implements Info<String> {
private String var ;
public InfoImpl(String var) {
this.setVar(var) ;
}
@Override
public void setVar(String var) {
this.var = var ;
}
@Override
public String getVar() {
return this.var ;
}
}
public class Test {
public void main(String arsg[]) {
InfoImpl i = new InfoImpl("xian");
System.out.println(i.getVar()) ;
}
};

InfoImpl 不是一个泛型类,因为他类名后没有 。然后在这里我们将 Info 中的泛型变量 T 定义填充为了 String 类型。所以在重写时 setVar() 和 getVar() 时,IDE 会也我们直接生成 String 类型的重写函数。在使用时,传进去 String 类型的字符串来构造 InfoImpl 实例,然后调用它的函数即可。

使用方法二:泛型子类

在方法一中,我们在类中直接把 Info 接口给填充好了,但我们的类,是可以构造成泛型类的,那我们利用泛型类来构造填充泛型接口会是怎样呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
interface Info<T> {
public T getVar();
public void setVar(T var);
}
class InfoImpl<T> implements Info<T> {
private T var ;
public InfoImpl(T var) {
this.setVar(var) ;
}
public void setVar(T var) {
this.var = var ;
}
public T getVar() {
return this.var ;
}
}
public class Test {
public static void main(String arsg[]) {
InfoImpl<String> i = new InfoImpl<String>("xian");
System.out.println(i.getVar()) ;
}
}

在这个类中,我们构造了一个泛型类 InfoImpl,然后把泛型变量 T 传给了 Info,这说明接口和泛型类使用的都是同一个泛型变量。然后在使用时,就是构造一个泛型类的实例的过程,使用过程也不变。

使用泛型类来继承泛型接口的作用就是让用户来定义接口所使用的变量类型,而不是像方法一那样,在类中写死。

那我们稍微加深点难度,构造一个多个泛型变量的类,并继承自 Info 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class InfoImpl<T,K,U> implements Info<U> {
private U var ;
private T x;
private K y;
public InfoImpl(U var) {
this.setVar(var) ;
}
public void setVar(U var) {
this.var = var ;
}
public U getVar() {
return this.var ;
}
}

在这个例子中,我们在泛型类中定义三个泛型变量 T、K、U,并且把第三个泛型变量 U 用来填充接口 Info。所以在这个例子中 Info 所使用的类型就是由 U 来决定的。代码如下:

1
2
InfoImpl<Integer, Double, String> i = new InfoImpl<>("xian");
System.out.println(i.getVar()) ;

4. 泛型函数

上面我们讲解了类和接口的泛型使用,下面我们再说说,怎么单独在一个函数里使用泛型。新建一个普通的类 StaticFans,然后在其中定义了两个泛型函数:

1
2
3
4
5
6
7
8
9
10
public class StaticFans {
// 静态函数
public static <T> void staticMethod(T a) {
Log.d("xian", "staticMethod: " + a.toString());
}
// 普通函数
public <T> void otherMethod(T a) {
Log.d("xian", "otherMethod: " + a.toString());
}
}

上面分别是静态泛型函数和常规泛型函数的定义方法,与以往方法的唯一不同点就是在返回值前加上 来表示泛型变量。其它没什么区别。使用方法如下:

1
2
3
4
5
6
// 静态方法
StaticFans.staticMethod("Xian");
// 常规方法
StaticFans staticFans = new StaticFans();
staticFans.otherMethod(123);

5. 泛型返回值

有时,我们需要将泛型变量返回,比如下面这个函数:

1
2
3
4
public static <T> List<T> parseArray(String response, Class<T> object) {
List<T> modelList = JSON.parseArray(response, object);
return modelList;
}

6. 泛型数组

在写程序时,大家可能会遇到类似 String[] list = new String[8]; 的需求,这里可以定义 String 数组,当然我们也可以定义泛型数组,泛型数组的定义方法为 T[],与 String[] 是一致的,下面看看用法:

1
2
3
4
5
6
7
8
public static <T> T[] fun1(T... arg) {
return arg ; // 返回泛型数组
}
// 使用
public static void main(String args[]) {
Integer[] i = fun1(1,2,3,4,5,6) ;
Integer[] result = fun1(i) ;
}

通配符


1. 无界

“?”可以接收任何类型。

1
2
3
4
5
public void processElements(List<?> elements){
for(Object o : elements){
System.out.println(o);
}
}

2. 上界

这种情况下能够接收 A 类或者 A 类的子类。

1
2
3
4
5
public void processElements(List<? extends A> elements){
for(A a : elements){
System.out.println(a.getValue());
}
}

A 类也可以换成接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface Comparable<T> {
public boolean compareTo(T i);
}
// 添加上 extends Comparable 之后,就可以 Comparable 里的函数了
public static <T extends Comparable> T min(T... a) {
T smallest = a[0];
for (T item : a) {
if (smallest.compareTo(item)) {
smallest = item;
}
}
return smallest;
}

通配多个类:

1
2
3
4
5
6
public static <T extends Fruit & Serializable> String getFruitName(T t) {
return t.getName();
}
public static <T extends Comparable & Serializable, U extends Runnable> T foo(T t, U u) {
}

3. 下界

接收 A 类或者 A 类的父类:

1
2
3
4
5
public static void insertElements(List<? super A> list){
list.add(new A());
list.add(new B());
list.add(new C());
}

泛型通配符 < ? extends T > 来接收返回的数据,此写法的泛型集合不能使用 add 方 法, 而 < ? super T > 不能使用 get 方法,做为接口调用赋值时易出错。

当我们使用 extends 时,我们可以读元素,因为元素都是 T 类或子类,可以放心的用 T 类拿出。当使用 super 时,可以添加元素,因为都是 T 类或父类,那么就可以安全的插入 T 类。

参考链接:JAVA 泛型夯实 JAVA 基础 —— 泛型详解