是五月呀!

Java泛型

Java范型有点类似与C++中的模板类或者模板方法,对类型进行抽象。

1. 泛型是什么

举一个JDK中的例子:

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

这里创建了三个List,分别存储String、Integer和Double。
在调用List的addget方法时,会对类型进行检查。
这里ArrayList将内存存储的类型参数化,各种类型的变量都可以组装成对应的List,而不必针对每个类型分别实现一个构建ArrayList的类。

2. 没有泛型会怎样

我们实现两个能够设置点坐标的类,分别设置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
//设置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
4
5
6
7
ObjectPoint integerPoint = new ObjectPoint();
integerPoint.setX(new Integer(100));
Integer integerX=(Integer)integerPoint.getX();
ObjectPoint floatPoint = new ObjectPoint();
floatPoint.setX(new Float(100.12f));
Float floatX = (Float)floatPoint.getX();

特别注意取值时需要强制类型转换。
但问题来了:注意,注意,我们这里使用了强制转换,我们这里setX()和getX()写得很近,所以我们明确的知道我们传进去的是Float类型,那如果我们记错了呢?
比如我们改成下面这样,编译时会报错吗:

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

不会!!!我们问题的关键在于这句:

1
String floatX = (String)floatPoint.getX();

强制转换时,编译不会出错。因为编译器也不知道你传进去的是什么,而floatPoint.getX()返回的类型是Object,所以编译时,将Object强转成String是成立的。必然不会报错。
而在运行时,则不然,在运行时,floatPoint实例中明明传进去的是Float类型的变量,非要把它强转成String类型,肯定会报类型转换错误的!
那有没有一种办法在编译阶段,即能合并成同一个,又能在编译时检查出来传进去类型不对呢?当然,这就是泛型。

3. 泛型类

我们先看看泛型的类是怎么定义的:

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
//定义
class Point<T>{// 此处可以随便写标识符号
//定义变量
private T x ;
private T y ;
public void setX(T x){//作为参数
this.x = x ;
}
public void setY(T y){
this.y = y ;
}
public T getX(){//作为返回值
return this.x ;
}
public T getY(){
return this.y ;
}
}
//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());

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

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
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;
}
}
//使用
MorePoint<Integer,String> morePoint = new MorePoint<Integer, String>();
morePoint.setName("harvic");
System.out.println("morPont.getName:" + morePoint.getName());

4. 泛型接口

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

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

4.1 使用方法一:非泛型类

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 GenericsDemo24{
public void main(String arsg[]){
InfoImpl i = new InfoImpl("harvic");
System.out.println(i.getVar()) ;
}
}

要清楚的一点是InfoImpl不是一个泛型类!因为他类名后没有
然后在在这里我们将Info中的泛型变量T定义填充为了String类型。所以在重写时setVar()和getVar()时,IDE会也我们直接生成String类型的重写函数。

4.2 使用方法二:泛型类

在方法一中,我们在类中直接把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 GenericsDemo24{
public static void main(String arsg[]){
InfoImpl<String> i = new InfoImpl<String>("harvic");
System.out.println(i.getVar()) ;
}
}

5. 泛型函数

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

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

1
2
3
4
5
6
7
8
//静态方法
StaticFans.StaticMethod("adfdsa");//使用方法一
StaticFans.<String>StaticMethod("adfdsa");//使用方法二
//常规方法
StaticFans staticFans = new StaticFans();
staticFans.OtherMethod(new Integer(123));//使用方法一
staticFans.<Integer>OtherMethod(new Integer(123));//使用方法二

6. 使用Class传递泛型类Class对象

有时,我们会遇到一个情况,比如,我们在使用JSON解析字符串的时候,代码一般是这样的

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

这段代码的意义就是根据SuccessModel解析出List的数组。
那现在,我们把下面这句组装成一个泛型函数要怎么来做呢?
首先,我们应该把SuccessModel单独抽出来做为泛型变量,但parseArray()中用到的SuccessModel.class要怎么弄呢?
先来看代码:

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

注意到,我们用的Class object来传递类的class对象,即我们上面提到的SuccessModel.class。
这是因为Class也是一泛型,它是传来用来装载类的class对象的,它的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Instances of the class {@code Class} represent classes and
* interfaces in a running Java application. An enum is a kind of
* class and an annotation is a kind of interface. Every array also
* belongs to a class that is reflected as a {@code Class} object
* that is shared by all arrays with the same element type and number
* of dimensions. The primitive Java types ({@code boolean},
* {@code byte}, {@code char}, {@code short},
* {@code int}, {@code long}, {@code float}, and
* {@code double}), and the keyword {@code void} are also
* represented as {@code Class} objects.
*/
public final class Class<T> implements Serializable {
}

7. 类型绑定

有时候,我们在泛型类或者泛型函数中希望泛型参数能够是某个接口的实现,这样我们可以调用接口方法。

7.1 extends

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

1
<T extends BoundingType>

此定义表示T应该是BoundingType的子类型(subtype)。T和BoundingType可以是类,也可以是接口。另外注意的是,此处的”extends“表示的子类型,不等同于继承。
一定要非常注意的是,这里的extends不是类继承里的那个extends!两个根本没有任何关联。在这里extends后的BoundingType可以是类,也可以是接口,意思是说,T是在BoundingType基础上创建的,具有BoundingType的功能。目测是Java的开发人员不想再引入一个关键字,所以用已有的extends来代替而已。
我们假设,我们有很多种类的水果,需要写一个函数,打印出填充进去水果的名字:
为此,我们先建一个基类来设置和提取名字:

1
2
3
4
5
6
7
8
9
10
class Fruit {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

然后写个泛型函数来提取名字:

1
2
3
public static <T extends Fruit> String getFruitName(T t){
return t.getName();
}

从这段代码也可以看出,类型绑定有两个作用:

  1. 对填充的泛型加以限定
  2. 使用泛型变量T时,可以使用BoundingType内部的函数。

有关绑定限定的用法,其实我们可以同时绑定多个绑定,用&连接,比如:

1
2
3
public static <T extends Fruit & Serializable> String getFruitName(T t){
return t.getName();
}

7.2 通配符

1
2
3
4
5
6
Point<?> point;
point = new Point<Integer>(3,3);
point = new Point<Float>(4.3f,4.3f);
point = new Point<Double>(4.3d,4.90d);
point = new Point<Long>(12l,23l);

无边界通配符?则只能用于填充泛型变量T,表示通配任何类型!!!!再重复一遍:?只能用于填充泛型变量T。它是用来填充T的!!!!只是填充方式的一种!!!
构造泛型实例时,如果省略了填充类型,则默认填充为无边界通配符!如下写法是对等的:

1
2
Point point3 = new Point(new Integer(23),new Integer(23));
Point<?> point3 = new Point(new Integer(23),new Integer(23));

7.2.1 通配符?的extends绑定

从上面我们可以知道通配符?可以代表任意类型,但跟泛型一样,如果不加以限定,在后期的使用中编译器可能不会报错。所以我们同样,要对?加以限定。
同样,通配符?可以用extends绑定范围。

1
Point<? extends Number> point;

此时,当将T填充为String和Object时,赋值给point就会报错!
这里虽然是指派生自Number的任意类型,但new Point();也是可以成功赋值的,这说明包括边界自身。
再重复一遍:无边界通配符只是泛型T的填充方式,给他加上限定,只是限定了赋值给它(比如这里的point)的实例类型。
如果想从根本上解决乱填充Point的问题,需要从Point泛型类定义时加上:

注意:利用<? extends Number>定义的变量,只可取其中的值,不可修改。
正因为point的类型为 Point<? extends Number> point,那也就是说,填充Point的泛型变量T的为<? extends Number>,这是一个什么类型?未知类型!!!怎么可能能用一个未知类型来设置内部值!这完全是不合理的。也就是说,它可能是Integer,也可能是Float,那么就不能往里面写数据。
但取值时,正由于泛型变量T被填充为<? extends Number>,所以编译器能确定的是T肯定是Number的子类,编译器就会用Number来填充T。

7.2.2 通配符?的super绑定

如果说 <? extends XXX>指填充为派生于XXX的任意子类的话,那么<? super XXX>则表示填充为任意XXX的父类!
我们先写三个类,Employee,Manager,CEO,分别代表工人,管理者,CEO

1
2
3
4
5
6
7
8
class CEO extends Manager {
}
class Manager extends Employee {
}
class Employee {
}

然后,如果我这样生成一个变量:

1
List<? super Manager> list;

它表示的意思是将泛型T填充为<? super Manager>,即任意Manager的父类;也就是说任意将List中的泛型变量T填充为Manager父类的List变量,都可以赋值给list。
new ArrayList(),new ArrayList()都是正确的,而new ArrayList()却报错,当然是因为CEO类已经不再是Manager的父类了。所以会报编译错误。

注意:super通配符实例内容:能存不能取。
由于list是List<? super Manager>类型,那么list中可以填充Manager和Manager的子类,它们一定是<? super Manager>的子类。

1
list.add(new Employee()); //编译错误

但是想添加Employee是不可以的,因为Employee不一定是<? super Manager>的子类。比如用Manager填充了T。
而从list中取出来的数据是Object类型的,需要强制类型转换。
总结 ? extends 和 the ? super 通配符的特征,我们可以得出以下结论:

  • 如果你想从一个数据类型里获取数据,使用 ? extends 通配符(能取不能存)
  • 如果你想把对象写入一个数据结构里,使用 ? super 通配符(能存不能取)
  • 如果你既想存,又想取,那就别用通配符。