java泛型看这一篇就够了

Java泛型

泛型的使用

定义泛型的接口和类

泛型就是允许在定义类、接口、方法时使用类型形参,这个类型形参(泛型)将在声明变量、创建对象、调用方法时动态地指定。

  • 定义泛型的接口

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    //定义接口指定泛型形参,该形参名为E
    public interface List<E>{

    //在该接口中,E可作为类型使用
    void add(E x);
    Iterator<E> iterator();
    }


    //定义接口时指定一个泛型形参,该形参为E
    public interface Iterator<E>{
    //在该接口里E完全可以作为类型使用
    E next();
    boolean hasNext();
    }
    //定义该接口时指定了两个泛型形参,其形参名为K,V
    public interface Map<K,V>{
    //在该接口里K,V完全可以作为类型使用
    Set<K> keySet();
    V put(K key,V value);
    }

    对于List<E>来说,如果E形参传入String类型实参,则产生了一个新的类型:List<String>类型,可以把List<E>中所有 E 的地方都换成String。虽然程序只定义了一个List<E>接口,但是实际使用时可以产生无数多个List接口。**List<String>绝对不会被替换成ListString,系统没有进行源码复制,二进制代码中也没有,哪里都没有。**

  • 定义泛型的类

    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
    public class Apple<T> {
    //使用T类型定义实例变量
    private T info;
    public Apple() {
    }

    //使用T类型来定义构造器
    public Apple(T info) {
    this.info = info;
    }

    public T getInfo() {
    return info;
    }

    public void setInfo(T info) {
    this.info = info;
    }

    public static void main(String[] args) {

    //由于传给T形参的是String,所以构造器参数只能是String
    Apple<String> a1 = new Apple<>("苹果");
    System.out.println(a1.getInfo());

    //由于传给T形参的是Double,所以构造器参数只能是Double或double
    Apple<Double> a2 = new Apple<>(3.14);
    System.out.println(a2.getInfo());
    }
    }

    上面程序在使用时就可以生成Apple<String>,Apple<Double>等多个形式的逻辑子类(物理上并不存在)

  • 泛型类生成子类的注意事项

    首先,当我们继承这些泛型类时,父类不能包含泛型形参。也就是说下面的书写方式是错误的。

    public class A extends Apple<T>{}这是错误的写法,以下几种写法是正确的。

    1
    2
    3
    public class A extends Apple<String>{}
    public class B extends Apple{}
    public class c<T> extends Apple<T>{}

    当使用第一种的时候,父类中所有带有 T 的方法都会变成 String,当子类需要重写父类的方法时候要尤为注意。

    如果是第二种的话,父类中带T的方法都会变成Object。

  • 并不存在泛型类

    1
    2
    3
    4
    5
    //分别创建List<String>对象和List<Integer>对象
    List<String> l1 = new ArrayList<>();
    List<Integer> l2 = new ArrayList<>();
    //调用getClass方法来比较两个类是否相等
    System.out.println(l1.getClass()==l2.getClass());

    运行上面的泛型类,可以发现输出True。不管为泛型形参传入哪一个种类型实参,对于Java来说,他们都是同一个类,内存中也只占用一块内存空间,因此在静态方法、静态初始化块、静态变量(类相关)的声明和初始化中不允许使用泛型形参。

类型通配符

感受一下一种神奇的错误

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ArrayErr {
public static void main(String[] args) {
//定义一个Integer数组
Integer[] ia = new Integer[5];

//可以把一个Integer[]数组赋值给Number[]变量
Number[] na = ia;

//下面带啊吗编译正常,但运行时会引发异常ArrayStoreException
//因为0.5不是Integer
na[0] = 0.5;
}
}

java早期设计中,允许Integer[]数组赋值给Number[]变量存在缺陷,因此在设计泛型的时候进行了改进,不再允许List给List变量。换个说法来讲就是假如你在类中使用泛型,那么你这个类方法只能接受这一种类别。

1
2
3
4
5
public void test(List<Object> c){
for(int i=0;i<c.size;i++){
System.out.println(c.get(i));
}
}

你可能会希望这个参数能够传入所有类型的list,但是很遗憾。只有List才能作为这个方法的参数, List也不能。

所以这就需要能表示各种泛型List的父类,类型通配符是一个问号(?),也就是说写成如下形式是正确的。

1
2
3
4
5
public void test(List<?> c){
for(int i = 0;i<c.size();i++){
System.out.println(c.get(i));
}
}

设定类型通配符的上限

当直接使用List<?>这种形式时,表示这个List集合可以是任何泛型List的父类。有时候我们不希望它是所有泛型的父类,因为好像没啥用。有时候我们想限定它是一种类的子类,这样我们就可以用父类的方法了。

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
36
37
38
39
40
41
42
43
44
45
package test;

import java.util.ArrayList;
import java.util.List;

public class Canvas{

//同时在画布上绘制多个形状
public void drawAll(List<? extends Shape> shapes) {
for(Shape s: shapes) {
s.draw(this);
}
}

public static void main(String[] args) {
List<Circle> circleList = new ArrayList<>();
circleList.add(new Circle());
Canvas c = new Canvas();
c.drawAll(circleList);

}
}
abstract class Shape {
public abstract void draw(Canvas c);
}

//定义Shape的子类Circle
class Circle extends Shape{

public void draw(Canvas c) {
//实现画图方法,以打印字符串来模拟画图方法实现
System.out.println("在画布"+ c+ "上画一个圆");

}

}

//定义Shape的子类Rectangle
class Rectangle extends Shape{

public void draw(Canvas c) {
System.out.println("把一个矩形画在画布"+c+"上");
}
}

这个程序重点关注List<? extends Shape> shapes注意一定要这么写,这个参数才能接受Shape的两个实现类作为其参数。假如你写成List<Shape> shapes则只能接受Shape这个抽象类的实例对象。不过? extends Shape,它表示Shape未知的子类,程序无法确定这个类型是什么,所以无法将任何对象添加到这种集合中。

总之这种指定通配符上限的集合,只能从集合中取元素(取出的元素总是上线的类型或其子类),不能向集合中添加元素,而且我们还可以为泛型形参设置上限

1
2
3
4
//表明T类型必须是Number类或其子类,其必须实现java.io.Serializable接口
public class Apple<T extends Number & java.io.Serializable>{
...
}

对于更广泛的泛型类来说,指定通配符上限就是为了支持类型型变。 比如 Foo 是 Bar 的子类,这样 A< Foo> 就 相当于 A<? extends Bar> 的子类, 可以将 A< Foo> 赋值给 A<? extends Bar> 类型 的 变量,这种型变方式被称为协变。

对于协变的泛型而言,它只能调用泛型类型作为返回值类型的方法( 编译器会将该方法返回值当 成通配符上限的类型;而不能调用泛型类型作为参数的方法。 口诀 是:协变只出不进!

设定类型通配符的下限

与上限刚好相反,你限定这个参数是该类的父类或是与其相同。假设自己实现一个工具方法:实现将src集合中的元素赋值到dest集合的功能,因为dest集合可以保存src集合中的所有元素,所以dest集合可以保存src集合中的所有元素,所以dest集合元素的类型应该是src集合元素类型的父类。

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
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

public class MyUtils {

//下面dest集合元素的类型必须与src集合元素的类型相同,或者是其父类
public static<T> T copy(Collection<? super T>dest,Collection<T> src) {
T last = null;
for(T ele:src) {
last = ele;

//逆变的泛型集合添加元素是安全的
dest.add(ele);
}
return last;
}

public static void main(String[] args) {
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<Integer>();
li.add(5);
//此处可准确地知道最后一个被复制的元素是Integer类型
//src集合元素的类型相同
Integer last = copy(ln,li);
System.out.println(ln);
}
}

Java 也允许指定通配符的下限,通配符的下限用<? super 类型> 的方式来指定,通配符下限的作用与通配符上限的作用恰好相反。 指定通配符的下限就是为了支持类型 型变。 比如 Foo 是 Bar 的子类, 当程序需要一个 A<? super Foo> 变量时,程序可以将 A< Bar>、 A< Object> 赋值给 A<? super Foo> 类型的变量,这种型变方式被称为逆变。

泛型方法

泛型方法的定义

泛型方法的定义体与之前的差不多,大多数时候都可以用泛型方法来代替类型通配符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.ArrayList;
import java.util.Collection;

public class GenericMethodTest {

//声明一个泛型方法,该泛型方法中带有一个T泛型形参
static <T> void fromArrayToCollection(T[] a,Collection<T> c) {
for(T o:a) {
c.add(o);
}
}

public static void main(String[] args) {
Object[] oa= new Object[100];
Collection<Object> co = new ArrayList<>();

//下面代码中T代表Object类型
fromArrayToCollection(oa, co);

//其他String、Integer也是同样的
}
}

不过泛型方法允许泛型形参被用来表示方法的一个或多个参数之间的类型依赖关系,就像如上的方法中,限定了两个参数之间的关系,如果我们定义的方法没有这样类型的依赖关系,就不应该用泛型方法。

类型通配符和泛型方法显著的区别:类型通配符既可以在方法签名中定义形参的类型,也可以用来定义变量的类型;但是泛型方法中的泛型形参必须在对应的方法中显式声明。

菱形语法与泛型构造器

Java 也允许在构造器签名中声明泛型形参,这样就产生了所谓的泛型构造器。一旦定义了泛型构造器, 接下来 在调用构造器时, 就不仅可以让 Java 根据数据参数的类型来“ 推断” 泛型形参的类型,而且程序员也可以显式地为构造器中的泛型形参指定实际的类型。

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
class Foo{
public <T> Foo(T t){
System.out.println(t);
}
}


public class GenericConstrucor {
public static void main(String[] args) {

//泛型构造器中的T类型为String
new Foo("疯狂哥哥讲义");

//泛型构造器中的T类型为Integer
new Foo(200);

//显式指定泛型构造器中的T类型为String
//床给Foo构造器的实参也是String对象,完全正确
new <String>Foo("疯狂弟弟讲义");

//显式指定泛型构造器中的T类型为String
//但传给Foo构造器的实参是Double对象,下面代码出错
//new <String>Foo(12.3);
}
}

泛型方法重载

1
2
3
4
public class MyUtils{
public static <T> void copy(Collection<T> dest,Collection<? extends T> src){}
public static <T> T copy(Collection<? super T> dest,Collection<T> src){}
}

不要写出令人迷惑的代码:上述代码列表存在一定的区别,但是这区别不是很明确。假如两个参数类型都是Collection的话就会引起编译错误,因为编译器无法确定想调用哪个方法。

1
2
3
4
//会引起错误的代码
List<Number> ln = new ArrayList<>();
List<Integer> li = new ArrayList<>();
MyUtils.copy(ln,li);

擦除和转换

  • 擦除:把一个具有泛型信息的对象赋给另一个没有泛型信息的变量时,所有<>之间的类型信息都会丢失。

    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
    class BoBo<T extends Number>{
    T size;
    public BoBo() {}

    public BoBo(T size) {
    this.size = size;
    }

    public T getSize() {
    return size;
    }

    public void setSize(T size) {
    this.size = size;
    }

    }
    public class ErasureTest {
    public static void main(String[] args) {
    BoBo<Integer> a = new BoBo<>();

    //a的getSize()方法返回Integer
    Integer as = a.getSize();

    //把 a对象赋值给其他变量会丢失尖括号里的信息
    BoBo b = a;

    //b只知道size的类型是Number
    Number size = b.getSize();

    //下面代码将引起错误
    Integer size1 = b.getSize();
    }
    }
  • 转换:你可以直接把一个List对象赋值给一个List<String>。编译器会提示你未经检查的转换,有可能会引发运行时异常ClassCastException

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import java.util.ArrayList;
    import java.util.List;

    public class ErasureTest2 {
    public static void main(String[] args) {
    List<Integer> li = new ArrayList<>();
    li.add(6);
    li.add(9);
    List list = li;

    //下面的代码引起“未经检查的转换警告”
    List<String> ls = list;

    //但只要访问ls里的元素,下面代码将引起运行时异常ClassCastException
    System.out.println(ls.get(0));
    }
    }

泛型与数组

Java泛型有一个很重要的设计原则—如果一段代码在编译时没有提出未经严查的转换警告,则程度在运行时不会引发ClassCastException

正是因为这个原因:所以数组元素的类型不能包含泛型变量或泛型形参。假如是允许的话,那么下面的代码一定会引发ClassCastException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.util.ArrayList;
import java.util.List;

public class GenericAndArray {
public static void main(String[] args) {

//下面的代码实际上是不允许的
List<String>[] lsa = new ArrayList<String>[];

//将lsa向上转型为Object[]类型的变量
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(3);

//将List<Integer>对象作为oa的第二个元素
//下面代码没有任何警告
oa[1] = li;

//下面代码也不会有任何警告,但会引发ClassCastException异常
String s = lsa[1].get(0);
}
}

如果改成以下形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.ArrayList;
import java.util.List;

public class GenericAndArray {
public static void main(String[] args) {

//修改过的
List<String>[] lsa = new ArrayList[10];
//这么写也是允许的:List<String>[] lsa = new ArrayList<?>[10];


//将lsa向上转型为Object[]类型的变量
Object[] oa = lsa;
List<Integer> li = new ArrayList<>();
li.add(3);

//将List<Integer>对象作为oa的第二个元素
//下面代码没有任何警告
oa[1] = li;

//下面代码也不会有任何警告,但会引发ClassCastException异常
String s = lsa[1].get(0);
}
}

这样的话List<String>[] lsa = new ArrayList[10];会有编译警告未经检查的转换,即编译器并不保证这段代码是类型安全的。但正因为编译器给出了警告,所以完全可能出现这种异常,与此类似如下代码也是不允许的。

1
2
3
4
<T> T[] makeArray(Collection<T> coll){
//下面代码导致编译错误
return new T[coll.size()]
}

由于类型变量在运行时并不存在,而编译器无法确定实际类型是什么,因此编译器会报错。


 目录