RoadMap
1. 什么是泛型
泛型是一种参数化类型的机制。它可以使得代码适用于各种类型,从而编写更加通用的代码,例如集合框架。
泛型是一种编译时类型确认机制。它提供了编译期的类型安全,
2. 泛型的优势
1,类型安全。 泛型的主要目标是提高 Java 程序的类型安全。通过知道使用泛型定义的变量的类型限制,编译器可以在一个高得多的程度上验证类型假设。没有泛型,这些假设就只存在于程序员的头脑中(或者如果幸运的话,还存在于代码注释中)。
2,消除强制类型转换。 泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
3,潜在的性能收益。 泛型为较大的优化带来可能。在泛型的初始实现中,编译器将强制类型转换(没有泛型的话,程序员会指定这些强制类型转换)插入生成的字节码中。但是更多类型信息可用于编译器这一事实,为未来版本的 JVM 的优化带来可能。由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改。所有工作都在编译器中完成,编译器生成类似于没有泛型(和强制类型转换)时所写的代码,只是更能确保类型安全而已。
3. 泛型类
定义泛型类 在 类名后面 加上<T> 表示这是个泛型类
public class Crate<T> {
private T contents;
public T emptyCrate() {
return contents;
}
public void packCrate(T contents) {
this.contents = contents;
}
}
这个泛型类型,可以在类内任何地方出现,
如 属性类型,方法的返回值,方法的参数类型。
在生成实例的时候 必须指定具体类型。
// create an instance with generic type
Crate<Elephant> crateForElephant = new Crate<>();
泛型数量可以是多个
public class SizeLimitedCrate<T, U> {
private T contents;
private U sizeLimit;
public SizeLimitedCrate(T contents, U sizeLimit) {
this.contents = contents;
this.sizeLimit = sizeLimit;
}
}
// create an instance with generic types
SizeLimitedCrate<Elephant, Integer> c1 = new SizeLimitedCrate<>()
泛型类命名规范
理论上来说,泛型的类型名字可以定义成任何你想要的。为了方便起见,提高可读性,JDK建议大家采用 单个大写字母,区分泛型与真实类名,同时提供了一些常用的建议泛型
E 表示一个元素
K 表示一个键值对的键
V 表示一个键值对的值
N 表示一个数字
T 表示一个通用类型
如果是多个通用类型,可以延续使用,S, U, V, .
3.1 多态下的泛型类
泛型类支持接口定义, 即定义一个泛型接口。
public interface Shippable<T> {
void ship(T t);
}
那么问题来了,这个泛型类怎么去实现?有三种方式可以实现
3.1.1 指定具体的泛型类型
在实现接口的同时 指定具体的类型 而不用泛型表示。
class ShippableRobotCrate implements Shippable<Robot> {
public void ship(Robot t) { }
}
3.1.2 继续泛化类型
在实现接口的同时,自己也变成泛化类,进一步,泛化下沉。
class ShippableAbstractCrate<U> implements Shippable<U> {
public void ship(U t) { }
}
3.1.3 没有泛型的实现
在实现接口的同时,不继续使用泛型,取而代之的是Object类型,这是个古老方法,主要是为了向前兼容,对那些没有泛型支持的兼容。
对于编译器而言,会抛出警告,但是会通过编译。
class ShippableCrate implements Shippable {
public void ship(Object t) { }
}
3.2 泛型类型参数的约束
1. 构造函数不能泛型, 如new T() 最终变成 new Object()
2. 不能使用静态类型的数组
3. 不能使用instanceof, 运行的时候泛型会被擦除
4. 不能使用基本类型作为泛型的参数,可以通过封装类如:Integer
5. 不能使用静态类型作为参数
4. 泛型方法
4.1 泛型方法的声明
泛型方法 与 泛型类有点类似,只是它作用与具体的方法,范围相对于泛型类 更小。
在定义方法的时候 在声明返回值的前面 使用<T> 来声明泛型方法。
public static <T> void sink(T t) { }
对于方法的返回类型,也可以是泛型或者是泛型类。
// 返回一个泛型
public static <T> T identity(T t) { return t; }
// 返回一个泛型类
public static <T> Crate<T> ship(T t) {
System.out.println("Preparing " + t);
return new Crate<T>();
}
同样的, 泛型方法支持多个 泛型类型
// 返回一个泛型
public static <T,U> T identity(T t,U u) { return t; }
4.2 泛型方法的调用
4.2.1 显示调用
调用具体的泛型方法时,需要指定具体类型
Box.<String>ship("package");
Box.<String[]>ship(args);
4.2.2 隐式调用
调用泛型方法的时候可以向正常的方法调用一样, java编译器会自动匹配泛型
Box.ship("package");
5 泛型擦除
泛型的出现帮助编译器能够在编译的时候,使用正确的类型。
实际上,编译器 是将所有的泛型替换为 Object,换句话说,代码编译之后,这些泛型都将被Object所取代。这么做的目的主要是为了兼容老版本的代码(非泛型)
public class Crate {
private Object contents;
public Object emptyCrate() {
return contents;
}
public void packCrate(Object contents) {
this.contents = contents;
}
}
也不用过于担心 这个泛型擦除,编译期会自动转型了那些被擦除了泛型 如:
当你调用方法: Robot r = crate.emptyCrate();
编译期 实际会编译出显示转型的代码
Robot r = (Robot) crate.emptyCrate();
6 与老代码合作
class Dragon {}
class Unicorn { }
public class LegacyDragons {
public static void main(String[] args) {
List unicorns = new ArrayList();
unicorns.add(new Unicorn());
printDragons(unicorns);
}
private static void printDragons(List<Dragon> dragons) {
for (Dragon dragon: dragons) { // ClassCastException
System.out.println(dragon);
}
}
}
虽然有了泛型擦除,但java 毕竟是动态强类型语言,在实际使用过程中,与老代码结合的使用也会出现问题。
7 泛型的通配与上下界
泛型的通配表示的是为知类型,通过? 表示
对一个泛型的通配有三种方式来使用它
类型 | 语法 | Example |
无界通配 | ? | List<?> l =new ArrayList<String>(); |
上界通配 | ? extends type | List<? extends Exception> l =new ArrayList<RuntimeException> (); |
下界通配 | ? super type | List<? super Exception> l =new ArrayList<Object>(); |
7.1 无界通配
java 是强类型语言, 所以,对于
List<Object> keywords = new ArrayList<String>();
是不能通过编译的, 如果使用了通配就可。
public static void printList(List<?> list) {
for (Object x: list) System.out.println(x);
}
public static void main(String[] args) {
List<String> keywords = new ArrayList<>();
keywords.add("java");
printList(keywords);
}
7.2 上界通配
假如 我们要设定一个继承关系的泛型
ArrayList<Number> list = new ArrayList<Integer>(); // DOES NOT COMPILE
List<? extends Number> list = new ArrayList<Integer>(); // compiled
上界通配表示 任何一个 Number的子类包括它自己都可以被匹配进来
public static long total(List<? extends Number> list) {
long count = 0;
for (Number number: list) count += number.longValue();
return count;
}
// 有了上界的泛型,在基于泛型擦除的机制,会将Object 强转成泛型上界
public static long total(List list) {
long count = 0;
for (Object obj: list) {
Number number = (Number) obj;
count += number.longValue();
}
return count;
}
需要注意的是,使用了上界通配的列表 是不能添加元素,从java的角度来看,编译期并不知道
添加的元素的具体是哪一个,因为任何extends type都可能。
static class Sparrow extends Bird { }
static class Bird { }
public static void main(String[] args) {
List<? extends Bird> birds = new ArrayList<Bird>();
birds.add(new Sparrow()); // DOES NOT COMPILE
birds.add(new Bird()); // DOES NOT COMPILE
}
7.3 下界通配
与上界通配类似,表示 任何一个 超类包括它自己都可以被匹配进来
public static void addSound(List<? super String> list) {
// lower bound
list.add("quack");
}
8 总结
使用场景
在使用泛型的时候可以遵循一些基本的原则,从而避免一些常见的问题。
- 在代码中避免泛型类和原始类型的混用。比如List 和List不应该共同使用。这样会产生一些编译器警告和潜在的运行时异常。当需要利用JDK 5之前开发的遗留代码,而不得不这么做时,也尽可能的隔离相关的代码。
- 在使用带通配符的泛型类的时候,需要明确通配符所代表的一组类型的概念。由于具体的类型是未知的,很多操作是不允许的。
- 泛型类最好不要同数组一块使用。你只能创建new List<?>[10]这样的数组,无法创建new List[10]这样的。这限制了数组的使用能力,而且会带来很多费解的问题。因此,当需要类似数组的功能时候,使用集合类即可。
- 不要忽视编译器给出的警告信息。
PECS 原则
- 如果要从集合中读取类型T的数据, 并且不能写入,可以使用 上界通配符(<?extends>)—Producer Extends。
- 如果要从集合中写入类型T 的数据, 并且不需要读取,可以使用下界通配符(<? super>)—Consumer Super。
- 如果既要存又要取, 那么就要使用任何通配符。