泛型

泛型

泛型就是把类型Type作为参数使用,就是类型参数。可以理解为参数化的类型Type(Class,Interface等等)

1.为什么要用到泛型?

泛型是为了解决什么问题出现的?
为了了解这个,下面我们举个箱子(Box)的例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Box{
    private Object obj;
    public Box(){
    }
    public void add(Object obj){
        this.obj = obj;
    }
    public Object get(){
        return obj;
    }
}

这是单纯表示箱子的一个类。
Add方法接受到什么对象,就在内容保存什么对象。当get方法调用时,再把那个对象返回。

我们在想象一下调用的情况:

1
2
3
4
5
6
public static void main(String[] args){
    Box b = new Box();
    b.add(123);
    Integer i = (Integer) b.get();
    System.out.println(i);
}

往Box的Add方法里传一个数字123,再通过get方法取出来。
强制转换为Integer,作为Integer的变量输出。

1
2
3
4
5
6
public static void main(String[] args){
    Box b = new Box();
    b.add("Hello");
    String s = (String) b.get();
    System.out.println(s);
}

直到现在基本上没有什么问题。
但我们再看看下面的例子。

1
2
3
4
5
6
public static void main(String[] args){
    Box b = new Box();
    b.add(123);
    String s = (String) b.get();
    System.out.println(s);
}

同样编译成功,但执行结果呢?

抛出了下面的异常:
generics5

理由应该是显而易见的。即使有不对的地方,编译还是通过了但执行时却失败了。
这样想写出稳定的代码是非常难的
还有各种各样的类型・・・
结局就是,Add方法设了什么值进去,get方法就要取什么类型的值
那为了安全起见,如果Integer用的箱子,我们写一个叫IntegerBox类
String用的箱子,我们写一个叫StringBox类
Long用的箱子,我们写一个叫LongBox类
Boolean用的箱子,我们写一个叫BooleanBox类等等等等・・・

 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
public class IntegerBox{
    private int obj;
    public Box(){
    }
    public void add(int obj){
        this.obj = obj;
    }
    public int get(){
        return obj;
    }
}

public class StringBox{
    private String obj;
    public Box(){
    }
    public void add(String obj){
        this.obj = obj;
    }
    public String get(){
        return obj;
    }
}

public class LongBox{
    private long obj;
    public Box(){
    }
    public void add(long obj){
        this.obj = obj;
    }
    public long get(){
        return obj;
    }
}

按照上面的例子,是可以解决可以编译但不能执行的不稳定代码的问题,
但每种类型都要去写一个这个箱子,是不是太麻烦?也不太符合现实中的情况?

2.泛型登场

generics21

把Object类型,用T置换,再在类名后加入<T>,就变成了class Box<T>
可以理解为这个Box类是针对类型T的类定义的类。
可以代替[IntegerBox、 StringBox・・・]的多会重复定义。 使用泛型定义Box,要传入类型时,可以是:Box、Box这样的形式。

generics22

往Int的Box里传入String的这种错误,在编译时就可以直接检查出来。
同样取值的时候,也没有必要做强制转换了。 泛型是类型安全的。

generics23

3.命名规则

在此介绍一下,定义泛型类型是惯用的标记法。
定义泛型类时,为了和通常的变量明确区分出来,一般使用一个大写字母。

使用像下面这样的大写字母

文字 意义
E 元素(Java collection框架内广泛使用)
K key
N 数值
T 类型Type
V
S,U,V 第二,第三个的类型

虽然无论用什么字母,都是可以编译的,
但请按照惯例遵守命名规则,这样可以写出容易理解的代码

4.子类型

再来了解一下子类型的问题。
所谓子类型,换言之就是子类或者派生类了。
请注意:泛型里面,和通常的对象的动作是不一样的

从Person类派生了一个SalesPerson类。
这是,SalesPerson对象(实例)可以很安全的转换为Person类,因为SalesPerson类是Person类派生出来的, SalesPerson也是Person。

像这种向父类的类型转换时安全的。叫做向上转换。
那在泛型里面,会是什么情况呢?

1
2
Box<String> sbox = new Box<String>();
Box<Object> obox = sbox;

上面的例子中,把Box类型的sbox赋给了Box变量。

因为String也是Object,那这种也是正确的吗?
但是实际上,这个代码是编译出错的。

如果通过Box可以操作Box对象的话,
那么往Box加入Integer型等别的类型,就有混乱了。
这样的话,对于强类型来说,泛型就没有什么意义了。

所有结论就是,虽然String是 Object 的子类,但是Box 却不是Box的子类。

5.泛型方法和构造函数

下例告诉我们怎么定义泛型方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class Box<T> {
    T o;

    public Box() {
    }

    public static <T> T getrMiddle(T[] a) {
        return a[a.length / 2];
    }
}

getMiddle方法接受T类型的数组,并返回数据中间的元素。当然这个元素也是T类型的。
可以像下面这样调用泛型方法。

1
2
3
4
5
6
7
public static void main(String[] args) {
    String[] cars = { "Toyota", "Nissan", "Honda" };
    System.out.println(Box.<String> getMiddle(cars));
}

//执行结果如下:
Nissan

此时,编译器可以根据传入参数判断出类型是什么,所以方法前的可以省略。
下面代码和上面是一样的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static void main(String[] args) {
    String[] cars = { "Toyota", "Nissan", "Honda" };
    System.out.println(Box.<String> getMiddle(cars));
    System.out.println(Box.getMiddle(cars));

    Integer[] carIds = { 1, 2, 3 };
    System.out.println(Box.getMiddle(carIds));
}

//执行结果如下:
Nissan
Nissan
2

6.边界类型(Bounded Types)

 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 Person {
    String name;

    public Person(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

public class SalesPerson extends Person {
    int empID;

    public SalesPerson(int empID, String name) {
        super(name);
        this.empID = empID;
    }

    public int getEmpID() {
        return empID;
    }
}

再尝试执行下面的方法:

1
2
3
4
5
public static <T extends Person> void showName(T[] a) {
    for(Person p  a) {
        System.out.println(p.getName());
    }
}

接受T类型的数组,再遍历这个数组元素调用getName方法。

为了正确执行这个代码,传入的参数必须是Person类,或是它的子类。
因为Person定义了getName方法,所有的子类就都有了。
如果是String对象,就没有getName方法,会导致处理失败。

像这样想给作为泛型的类型加入限制的时候,我们就可以指定了。
下面方法的定义的红框。

generics63

<T extends Person>的表述中,T表示是Person派生的类。
像这样可以限制的类型,就叫边界类型(Bounded Types)。

7.通配符

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public static void main(String[] args) {
    SalesPerson p = new SalesPerson(123, "Keisuke");
    Box<SalesPerson> b = new Box<SalesPerson>();
    b.add(p);
    foo(b);
}

public static void foo(Box<SalesPerson> bp) {
    // do sth.
}

这个例子中foo方法只接受Box<SalesPerson>类型的参数。
如果要接受Person所有的派生类的话,要如下:

1
2
3
public static void foo(Box<? extends Person> bp) {
    // do sth.
}

如果你想要可以传入这个类的任何子类,就要使用[?]作为通配符

8.Java类库中的泛型类

标准的Collection的接口,已经都泛型化了。
Collection<V>、List<V>、Set<V> 和 Map<K,V>


!!! Question 1. List<Object>List<String> 之间,有何关系?
2. List<String>ArrayList<String>之间,有何关系?
3. 下面的两个,正确吗?

1
2
3
4
5
    Integer[] intArray = new Integer[10];
    Number[] numberArray = intArray;

    List<Integer> intList = new ArrayList<Integer>();
    List<Number> numberList = intList;
4. 下面两个一样吗?有什么区别?
1
2
    public static void foo(Box<T extends Person> bp) {
    public static void foo(Box<? extends Person> bp) {

総括

  • 泛型,就是参数化的类,接口,方法等。
  • 参数,只能是引用型,不能是基本型。
  • 编译时可以安全检查,可以自动转换Cast,提高代码重复利用率
  • 参数不一样的泛型类,是没有关系的。
  • 参数可以有多个。
  • 参数的继承可以使用<T extends superclass>:边界类型 (Bounded Types)
  • 参数也支持通配符。如:Class<?> classType = Class.forName("java.lang.String");