Java-泛型

泛型

什么是java泛型?

本质:参数化类型
即所操作的数据类型被指定为一个参数。
分泛型类、泛型接口、泛型方法

类型擦除

Java 泛型基本上都是在编译器这个层次来实现的。

在生成 Java 字节代码中(source–>bytecode 过程:编译过程)是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数会被编译器在编译的时候去掉,这就是类型擦除。(但会保证类或方法内部参数类型的一致性。即类名旁边带的 E,其内部方法使用到的 E,类型是一致的)

List<Object>List<String>等类型,在编译之后都会变成 List(泛型类对象的读取(类型转换)和写入(类型检查)的位置,编译器会自动帮我们添加约束)

缺陷:泛型类型不能显式地运用在运行时类型的操作当中,例如**转型instanceofnew**。(运行时,所有参数的类型信息都丢失了)

擦除的补偿

  1. 类型判断问题
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
class Building{}
class House extends Building{}

/**
* 类型判断器
*/
public class ClassTypeCapture{
Class kind;
public ClassTypeCapture(Class kind){
this.kind = kind;
}
public boolean f(Object arg){
return kind.isInstance(arg);
}

public static void main(String[] args){
ClassTypeCapture ctt1 = new ClassTypeCapture(Building.class);
System.out.println(ctt1.f(new Building()));
System.out.println(ctt1.f(new House()));

ClassTypeCapture ctt2 = new ClassTypeCapture(House.class);
System.out.println(ctt2.f(new Building()));
System.out.println(ctt2.f(new House()));
}
}

//output
//true
//true
//false
//true
  1. 创建类的实例

不能 ``new T()`原因,不能确定类型,不能确定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
27
28
29
30
31
//使用显式地工厂模式

interface IFactory{
T create();
}
class Foo2{
private T x;
public <T> Foo2(F factory){
x = factory.create();
}
}
class IntegerFactory implements IFactory{
@Override
public Integer create(){
return new Integer(0);
}
}
class Widget{
public static class Factory implements IFactory{
@Override
public Widget create(){
return new Widget();
}
}
}
public class FactoryConstraint{
public static void main(String[] args){
new Foo2(new IntegerFactory());
new Foo2(new Widget.Factory());
}
}

为什么需要泛型?

安全简单。可将运行时错误提前到编译时错误。
泛型之前是用Object的引用来实现参数的“任意化”,这种如果强制转化错误只能在运行时发现。

设计泛型的关键目的是在成员之间提供有意义的约束,这些成员可以是:类的实例成员、类的方法、函数参数和函数返回值。

通配符和上下界

通配符 ?(只能用在方法上,不能定义在类上会报错)

上界 ? extends T(生产者Producer)只能读,不能写(除了null);可以接收T及其所有子类类型的数据,这里T可以是类也可以是接口

下界 ? super T(消费者Consumer)只能写,不能读;可以接收T及其所有超类类型的数据。

类型擦除将会擦除到它的第一个边界(边界可以有多个)。编译器事实上会把类型参数替换为它的第一个边界的类型。如果没有指明边界,类型将被擦除到 object

PECS:Producer extends Consumer super

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String args[]) {
List<Number> nums = new ArrayList<>();
List<Integer> integers = new ArrayList<>();
//Integer是Number的子类,但是List<Integer>并不是List<Number>的子类,故不能直接赋值
//nums = integers; //此处编译器会报错
//但是通过通配符?List<? extends Number>就可以赋值了
List<? extends Number> numbers = new ArrayList<>();
//? extends Number定义上界为Number,所以变量numbers可以接受Number和Number的所有子类赋值
numbers = integers;
List<Float> floats = new ArrayList<>();
numbers = floats;

//在集合中使用泛型通配符,要记得PECS规则,生产者只能读取,不能写入(除了null)
numbers.add(null); //编译器不会报错
numbers.add(999); //编译器会报错
//上界通配符无法写入,但是可以正常读取
Number num = numbers.get(0);
//如果要获取的类型是Number的子类,则必须使用强制类型转换
int i = (Integer)numbers.get(0);
}

null是所有引用类型都有的元素,所以上界(生产者)可以add成功

引入泛型之后的类型系统增加了两个维度:

  1. 类型参数自身的继承体系结构 (如 List<String>List<Object>

  2. 泛型类或接口自身的继承体系结构(如List接口继承自Collection接口)

**相同类型参数泛型类的关系取决于泛型类自身的继承体系结构 ** ( 即List<String>可以替换Collection<String>(Liskov 替换原则) )

当泛型类的类型声明中使用了通配符的时候,其子类型可以在两个维度上分别展开。

如对Collection<? extends Number>来说,其子类型可在 Collection 这个维度上展开,即 List<? extends Number>Set<? extends Number>等。也可以在 Number这个维度上展开,即Collection<Double>Collection<Integer>等。如此循环下去,ArrayList<Long>HashSet<Double>等也算是 Collection<? extends Number>的子类型

泛型的命名规范

  • E - Element,常用在 Collection 里,如:List<E>Iterator<E>Set<E>

  • K,V - Key,Value,代表 Map 的键值对

  • N - Number,数字

  • T - Type,类型

  • String,Integer 等 - S,U,V etc.

元组(tuple)类库

同list可用于数据存储,包含多个数据。可同时存储不同类型的数据类型。

1
2
3
4
5
6
7
8
9
10
11
class TwoTuple<A,B>{
public final A first;
public final B second;
public TwoTuple(A a, B b){
first = a;
second = b;
}
public String toString(){
return "(" + first + "," + second +")";
}
}

协变、逆变、不变

PECS:Producer-extendsConsumer-super

  1. 对于协变? extends T,只能get(),即作为生产者(Producer)
  2. 对于逆变? super T,只能set(),即作为消费者(Consumer)

可变性是一种类型安全的方式,将一个对象当做另一个对象来使用。若不能将一个类型替换为另一个类型,那么这个类型称之为:不变量
协变与逆变是相互对立的

  • 若某个返回类型可由其派生类替换,则此类型是支持协变的。(修饰返回值。把子类指向父类的关系)
  • 若某个参数类型可由其基类替换,则此类型是支持逆变的。(修饰传入参数。把父类指向子类的关系)

怎么自定义泛型接口、泛型类?

当声明或者实例化一个泛型的对象时,必须指定类型参数的值:

1
Map<String, String> map = new HashMap<String, String>();

数组是协变的
泛型不是协变的
例:List不是List的父类型

泛型若是协变的会违反泛型提供的类型安全。
例:

1
2
3
4
List<Integer> intList = new ArrayList<Integer>();
List<Number> numberList = new ArrayList<Number>();//invalid
numberList.add(new Float(3.14));
上面的代码就会让您把不是Integer的东西放进intList中

怎么定义泛型方法?