Java第10章 泛型

泛型的目的是为了实现类型的通用性,那为什么不用 Object 向上转型的方法呢?如果集合里面数据很多,某一个数据转型出现错误,在编译期是无法发现的。但是在运行期会发生java.lang.ClassCastException。泛型一方面让我们只能往集合中添加一种类型的数据,同时可以让我们在编译期就发现这些错误,避免运行时异常的发生,提升代码的健壮性。

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数

对于泛型,只是允许程序员在编译时检测到非法的类型而已。但是在运行期时,其中的泛型标志会变化为 Object 类型

泛型方法

泛型方法既可以存在于泛型类中,也可以存在于普通的类中。如果使用泛型方法可以解决问题,那么应该尽量使用泛型方法

定义泛型方法的规则:

  • 所有泛型方法声明都有一个类型参数声明部分(由尖括号分隔),该类型参数声明部分在方法返回类型之前

    public static < E > void printArray( E[] inputArray ){}

  • 每一个类型参数声明部分包含一个或多个类型参数,参数间用逗号隔开。一个泛型参数,也被称为一个类型变量,是用于指定一个泛型类型名称的标识符。

  • 类型参数能被用来声明返回值类型,并且能作为泛型方法得到的实际参数类型的占位符。

  • 泛型方法体的声明和其他方法一样。注意类型参数只能代表引用型类型,不能是原始类型(像int,double,char的等)

    1
    2
    3
    Pair<int, char> p = new Pair<>(8, 'a'); // compile-time error
    Pair<Integer, Character> p = new Pair<>(8, 'a');
    Pair<Integer, Character> p = new Pair<>(Integer.valueOf(8), new Character('a'));
  • 不能实例化类型参数

    1
    2
    3
    4
    public static <E> void append(List<E> list) {
    E elem = new E(); // compile-time error
    list.add(elem);
    }
  • 不能将静态成员的类型声明为类型参数

  • 如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法

  • 不能创建类型参数数组

    1
    List<Integer>[] arrayOfLists = new List<Integer>[2]; // compile-time error
  • 类型参数不能进行catch、throw等异常处理

有界的类型参数: 限制被允许传递到一个类型参数的类型种类范围。要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界

public static <T extends Comparable<T>> T maximum(T x, T y, T z){}

泛型类

泛型类的声明和非泛型类的声明类似,除了在类名后面添加了类型参数声明部分。因为他们接受一个或多个参数,这些类被称为参数化的类或参数化的类型。最典型的就是各种容器类,如:List、Set、Map

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Box<T> {  
private T t;

public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
//调用
Box<Integer> integerBox = new Box<>(); //右边尖括号无需再声明类型,因为编译器可以进行推断
// 参数还可以是参数化类型
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
// 没有实际参数是原生类型
Box rawBox1 = new Box();
// 可以将参数化类型赋值给原生类型,但不能反过来
Box rawBox2 = integerBox; // ok
Box<Integer> intBox = rawBox1; // error
// 原生类型绕过了泛型类型检查,因此要避免使用
rawBox1.set(8); // warning: unchecked invocation to set(T)

泛型类最常用的使用场景就是“元组”的使用。我们知道方法return返回值只能返回单个对象。如果我们定义一个泛型类,定义2个甚至3个类型参数,这样我们return对象的时候,构建这样一个“元组”数据,通过泛型传入多个对象,这样我们就可以一次性方法多个数据了。

泛型接口

定义:

1
2
3
public interface Generator<T> {
public T next();
}

泛型接口未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。

1
2
3
4
5
6
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}

如果泛型接口传入类型参数时,实现该泛型接口的实现类,则所有使用泛型的地方都要替换成传入的实参类型。

1
2
3
4
5
6
class DataHolder implements Generator<String>{
@Override
public String next() {
return null;
}
}

泛型类或接口可以被继承,如: interface PayloadList<E,P> extends List<E> { }

类型通配符

容器中的类型之间存在继承关系,但是两个容器之间是不存在继承关系的。因此需要用到通配符?

The wildcard can be used in a variety of situations: as the type of a parameter, field, or local variable; sometimes as a return type
The wildcard is never used as a type argument for a generic method invocation, a generic class instance creation, or a supertype

类型通配符一般是使用?代替具体的类型参数。例如 List<?> 在逻辑上是List,List 等所有List<具体类型实参>的父类 。无界通配符 意味着可以使用任何对象,因此使用它类似于使用原生类型。原生类型可以持有任何类型,而无界通配符修饰的容器持有的是某种具体的类型此处’?’是类型实参,而不是类型形参

public static void getData(List<?> data) {}

含通配符上限, 用 extends,如 public static void getUperNumber(List<? extends Number> data) {} 如此定义就是通配符泛型值接受Number及其下层子类类型。但是set() 方法会失效—— 可能是类型擦除的原因?get()方法仍可用,可能是因为获取出来的我们都可以隐式的转为其基类(或者Object基类)。所以上界描述符Extends适合频繁读取的场景。

多通配:<T extends B1 & B2 & B3> 类要在接口的前面

通配符下限, 用 super , 如 List<? super Number> 表示类型只能接受Number及其三层父类类型,如 Object 类型的实例

下界通配符<? super T>不影响往里面存储,但是读取出来的数据只能是Object类型。原因是:下界通配符规定了元素最小的粒度,必须是T及其基类,那么我往里面存储T及其派生类都是可以的,因为它都可以隐式的转化为T类型。但是往外读就不好控制了,里面存储的都是T及其基类,无法转型为任何一种类型,只有Object基类才能装下。

PECS原则

  • 上界<? extends T>不能往里存,只能往外取,适合频繁往外面读取内容的场景。

  • 下界<? super T>不影响往里存,但往外取只能放在Object对象里,适合经常往里面插入数据的场景。

泛型擦除

Java语言泛型在设计的时候为了兼容原来的旧代码,Java的泛型机制使用了“擦除”机制。

编译器虽然会在编译过程中移除参数的类型信息,但是会保证类或方法内部参数类型的一致性。泛型参数将会被擦除到它的第一个边界(边界可以有多个,重用 extends 关键字,通过它能给与参数类型添加一个边界)。在运行过程中,编译器事实上会把类型参数替换为它的第一个边界的类型。如果没有指明边界,那么类型参数将被擦除到Object。最后需要写入时,编译器会进行一次类型转换

1
2
3
4
5
Class<?> class1=new ArrayList<String>().getClass();
Class<?> class2=new ArrayList<Integer>().getClass();
System.out.println(class1); //class java.util.ArrayList
System.out.println(class2); //class java.util.ArrayList
System.out.println(class1.equals(class2)); //true

由以上例子可以看出,Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型

泛型类型不能显式地运用在运行时类型的操作当中,例如:转型、instanceof 和 new。因为在运行时,所有参数的类型信息都丢失了。 解决办法

命名传统

• E - Element (used extensively by the Java Collections Framework)
• K - Key
• N - Number
• T - Type
• V - Value
• S,U,V etc. - 2nd, 3rd, 4th types