swiftR

泛型通过在编译时检测到更多的代码 bug 从而使你的代码更加稳定

泛型的作用

  • 在编译时更强的类型检查。
  • 消除强制类型转换

泛型的类型

例如一个简单的box类

1
2
3
4
5
public class box{
private Object object;
//set
//get
}

改成泛型之后

1
2
3
4
5
public class box<T>{
private T t;
//set
//get
}

所有的 Object 被 T 代替了。类型变量可以是非基本类型的的任意类型,任意的类、接口、数组或其他类型变量。

泛型类型参数和命名规范

1
2
3
4
5
6
E - Element (由 Java 集合框架广泛使用)
K - Key
N - Number
T - Type
V - Value
S,U,V 等. - 第二种、第三种、第四种类型

菱形

我们先来实例化一个泛型类

1
Box<Integer> box = new Box<Integer>();

Java SE 7 开始泛型可以使用空的类型参数集<>,编译器可以推断出泛型的类型,所以后面的Integer可以省略

1
Box<Integer> integerBox = new Box<>();

参数化类型

1
Box<Integer> integerBox = new Box<>(1);

原生类型

如果这个类是不要泛型那该怎么办呢,这时候就需要原生类型,也只有带泛型的类才具有原生类型

1
Box rawBox = new Box();

java为了保持向后兼容,可以将参数化类型赋值给原生类型,反之则报错,同时原生类型也不可调用带有泛型的方法

1
2
Box<String> stringBox = new Box<>();
Box rawBox = stringBox; // OK

警告显示原始类型绕过泛型类型检查,将不安全代码的捕获推迟到运行时。因此,开发人员应该避免使用原始类型。

泛型方法

类似于声明泛型类型,但类型参数的范围仅限于声明它的方法。允许使用静态和非静态泛型方法,以及泛型类构造函数。

1
2
3
4
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}

有界参数类型

对数字进行操作的方法可能只想接受Number或其子类的实例。这时,就需要用到有界类型参数。

1
2
3
4
public <U extends Number> void inspect(U u){
System.out.println("T: " + t.getClass().getName());
System.out.println("U: " + u.getClass().getName());
}

调用该方法是参数是有界的,必须是Number及其子类


如果要定义多个边界,使用&符号就好了

1
<T extends B1 & B2 & B3>

同时边界也可以是一个接口

泛型的继承和子类型

由于Integer是继承自Number的,下面的代码是ok的

1
2
3
4
public void someMethod(Number n) { /* ... */ }

someMethod(new Integer(10)); // OK
someMethod(new Double(10.1)); // OK

但如果是下面的方法呢

1
public void boxTest(Box<Number> n) { /* ... */ }

可能会想到使用Box获取Box作为参数,但这是不可以的
,因为Box和Box并不是Box的子类型。在使用泛型编程时,这是一个常见的误解,虽然Integer和Double是Number的子类型。

通配符

通配符(?)通常用于表示未知类型。

  • 作为参数,字段或局部变量的类型;
  • 作为返回类型。

上限有界通配符

使用上限有界通配符可以放宽对变量的限制
例如

1
2
3
4
5
6
7
8
9
10
11
12
13
public static double sumOfList(List<? extends Number> list){
double sum = 0.0;
for(Number n : list){
sum += n.doubleValue();
System.out.println(n.doubleValue());
}
return sum;
}

public static void main(String[] args) {

System.out.println(sumOfList(Arrays.asList(1,2.2,3)));
}

无界通配符

无界通配符类型通常用于定义未知类型,比如List<?>

  1. 需要使用Object类中提供的功能实现的方法
    1
    2
    3
    4
    5
    public static void printList(List<Object> list) {
    for (Object elem : list)
    System.out.println(elem + " ");
    System.out.println();
    }

printList只能打印一个Object实例列表,不能打印List,List,List等,因为它们不是List的子类型。

  1. 泛型类中不依赖于类型参数的方法
    1
    2
    3
    4
    5
    public static void printList(List<?> list) {
    for (Object elem: list)
    System.out.print(elem + " ");
    System.out.println();
    }

下限有界通配符

下限有界通配符将该类型限制为特定类型或者超类 语法为<? super A>

1
2
3
4
5
public static void addNumbers(List<? super Integer> list) {
for (int i = 1; i <= 10; i++) {
list.add(i);
}
}

通配符及其子类

由于泛型的继承关系不同于类的继承,下列代码会出错

1
2
List<B> listb = new ArrayList<>();
List<A> lista = listb; //error

即使B是继承自B的,但还是会出错,他们的继承关系如图

upload successful

如果这样写就没问题了

1
2
List<? extends Integer> intList = new ArrayList<>();
List<? extends Number> numList = intList; // OK. List<? extends Integer> is a subtype of List<? extends Number>

由于Integer是继承自Number的,numList是Number的列表集合,关系如图

upload successful

类型擦除

  • 如果类型参数是无界的,则用泛型或对象替换泛型类型中的所有类型参数。因此,生成的字节码仅包含普通的类\接口和方法。
  • 如有必要,插入类型铸件以保持类型安全。
  • 生成桥接方法以保留扩展泛型类型中的多态性。

擦除泛型类型

类型擦除过程中,java编译器将擦除所有类型参数,类型参数有界时将其替换为第一个绑定,如果类型参数为无界,则替换为Object。

1
2
class<T>{
}

T将会替换为Object

1
public class Node<T extends Comparable<T>> {

将会替换为第一个绑定类Comparable

擦除泛型方法

与类的泛型擦除类似

泛型的一些限制

无法使用基本类型实例化泛型

例如 不能使用int去代替Integer

1
Pair<Integer, Character> p = new Pair<>(8, 'a');

无法创建类型参数实例

1
2
3
4
public static <E> void append(List<E> list) {
E elem = new E(); // compile-time error
list.add(elem);
}

作为解决办法,可以通过反射来创建

1
2
3
4
5
6
public static <E> void append(List<E> list, Class<E> cls) throws Exception {
E elem = cls.newInstance(); // OK
list.add(elem);
}
List<String> ls = new ArrayList<>();
append(ls, String.class);

无法声明类型为类型参数的静态字段

1
2
3
public class test{
private static T os;
}

无法使用具有参数化类型的强制转换或instanceof

无法创建参数化类型的数组

无法创建、捕获或抛出参数化类型的对象

无法重载每个重载的形式参数类型擦除到相同原始类型的方法

例如

1
2
3
4
public class Example {
public void print(Set<String> strSet) { }
public void print(Set<Integer> intSet) { }
}

因为他们进行类型擦除后有相同的签名