3-1-泛型
泛型数组列表
ArrayList是一个有类型参数的泛型类。例如ArrayList<Employee>
,在添加删除元素时,它能自动地调整数组容量,而不需要为此编写任何代码
声明数组列表ArrayList
声明和构造一个保存Employee对象的数组列表
Array<Employee> staff = new ArrayList<Employee>();
java10后,可以使用var关键字
var staff = new ArrayList<Employee>();
不使用var关键字的话,也可以省去右边的类型参数
Array<Employee> staff = new ArrayList<>();
- 如果使用var声明ArrayList,就不要使用菱形语法
// not ok
//下面的语句会自动生成一个ArrayList<Object>
var elements = new ArrayList<>();
java的老版本中,使用Vector实现动态数组,但现在的ArrayList类更加高效,没有理由再使用Vector类
Java的ArrayList类似于C++的vector模板,但C++的vector模板为了便于访问元素重载了[]运算符。Java没有运算符重载,所以Java中只能调用显式的方法
ArrayList与vector都是泛型类型
包装器
尖括号中的类型不允许是基本类型,于是对基本类型就需要使用包装器进行包装
要注意的是ArrayList<Inteager>
的效率要远低于int[],所以这种方法只在程序员认为操作的方便性比执行效率更重要的时候,才会考虑对较小的集合使用这种构造。
自动装箱
list.add(3);
将自动变换成
list.add(Integer.valueOf(3));
这种变换过程称为自动装箱
泛型类
泛型类和泛型方法有类型参数 <参数> ,这使得它可以准确地描述用特定类型实例化时会发生什么
在有泛型类之前,程序员使用 Object类继承 编写适用多种类型的代码,这过程中要进行强制类型转换。它很繁琐,而且不安全。
//Object继承,当获取一个值时必须进行强制类型转换
ArrayList files = new ArrayList();
...
String filename = (String)files.get(0);
而且,这里没有错误检查,可以向数组列表中添加任何类的值。
只有当调用产生时,强制类型转换失败时才会产生一个错误。
于是有了一个更好的解决方案:泛型与类型参数
泛型意味着编写的代码可以对多种不同类型的对象重用。例如ArrayList类使得你不用为收集String类和File类对象而分别编写不同的类
泛型类是在实例化一个对象时指定他的类型。
var files = new ArrayList<String>;
这使得代码具有更好的可读性,更安全,形式更简便。
现在,编译器可以检查,防止你插入错误类型的对象。
泛型类(generic)就是有一个或者多个类型变量的类.形如:
public class Pair<T>{
private final T first;
private final T second;
public Pair(){
first = null;
second = null;
}
public Pair(T first,T second){
this.first = first;
this.second = second;
}
public T getFirst(){return first}
}
可以用具体的类型替换类型变量来实例化(instantiate)泛型类型:
public class Pair<String>{
private final String first;
private final String second;
public Pair(){
first = null;
second = null;
}
public Pair(String first,String second){
this.first = first;
this.second = second;
}
public String getFirst(){return first}
}
换句话来说,泛型类相当于普通类的工厂。
表面上看,Java的泛型类类似于C++的模板类template,但这两种机制存在本质的区别,本章中会加以介绍
泛型方法
可以定义一个带有类型参数的方法。当你调用这个方法时指定它的类型
class ArrayAlg{
public static <T> T getMiddle(T... a){
return a[a.length/2];
}
}
其中类型变量放在修饰符(static)后面,且在返回类型T的前面
这个方法是在普通类中定义的而不是在泛型类中。
泛型方法可以在普通类中定义,也可以在泛型类中定义。
下面给出一个示例,观察泛型方法的应用
package pair2;
import java.time.*;
class ArrayAlg {
/**
* Gets the minimum and maximum of an array of objects of type T.
*
* @param a an array of objects of type T
* @return a pair with the min and max values, or null if a is null or empty
*/
public static <T extends Comparable> Pair<T> minmax(T[] a) {
if (a == null || a.length == 0) {
return null;
}
// 传参T[] a
// <T extends Comparable>
T min = a[0];
T max = a[0];
for (int i = 1; i < a.length; i++) {
if (min.compareTo(a[i]) > 0) {
min = a[i];
}
if (max.compareTo(a[i]) < 0) {
max = a[i];
}
}
// return Pair<T>
return new Pair<>(min, max);
}
}
调用泛型方法
当调用一个泛型方法时,可以把具体类型包围在尖括号中,放在方法名前面:
String middle = ArrayAlh.<String>getMiddle("John","Q","Public");
在这种情况下(实际上也是常见的情况)下,方法调用中可以省略<String>
类型参数。编译器有足够的信息推断出你想要的方法。它会把参数的类型与泛型类型T进行匹配。推断出T是String
即调用时,可以简单地调用:
String middle = ArrayAlh.getMiddle("John","Q","Public");
几乎所有情况下,泛型方法的类型推导都能正常工作。只有偶尔的情况,编译器会报出错误。看下面这个例子
double middle = ArrayAlh.getMiddle(3.14,1729,0);
给出的错误信息会很晦涩。解释这个代码,编译器会有两种方式,而且两种方式都合法。
1.编译器先把参数自动装箱为1个Double对象和2个Integer对象
2.编译器寻找这些类的共同超类型
3.编译器会找到两个超类,Number和Comparable接口
4.特别的是,Comparable接口本身也是一个泛型类型。
这样的话,可以采取的补给措施就是把所有的参数都写成double值。
- C++中,要将类型参数放在方法名后面,这样做容易出现二义性。所以Java将其放在修饰符和返回值类型的中间。
在<>中可以加入限定类型,即使用extends关键字,它表示T是右边限定条件的子类型,右边的限定条件可以是类,也可以是接口.
使用extends关键字是因为设计者不想再引入多余的关键字。
<T extends BoundingTyoe>
表示T应该是限定类型(bounding type)的子类型(subtype).
如果有多个类型限定 中间用 & 分隔,而如果有多个类型参数T,中间用 , 分隔
import java.io.Serializable;
<T extends Comparable & Serializable,V extends Comparable>
- Java不支持泛型类型的数组,需要使用一些方法来解除限制,这些会在下面的内容中学到
Java泛型:泛型类、泛型接口和泛型方法来源
根据《Java编程思想 (第4版)》中的描述,泛型出现的动机在于:
有许多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创建容器类。
泛型类来源
容器类应该算得上最具重用性的类库之一。先来看一个没有泛型的情况下的容器类如何定义:
public class Container {
private String key;
private String value;
public Container(String k, String v) {
key = k;
value = v;
}
public String getKey() {
return key;
}
public void setKey(String key) {
this.key = key;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}
Container类保存了一对key-value键值对,但是类型是定死的,也就说如果我想要创建一个键值对是String-Integer类型的,当前这个Container是做不到的,必须再自定义。那么这明显重用性就非常低。
当然,我可以用Object来代替String,并且在Java SE5之前,我们也只能这么做,由于Object是所有类型的基类,所以可以直接转型。
但是这样灵活性还是不够,因为还是指定类型了,只不过这次指定的类型层级更高而已,有没有可能不指定类型?有没有可能在运行时才知道具体的类型是什么?
所以,就出现了泛型。
public class Container<K, V> {
private K key;
private V value;
public Container(K k, V v) {
key = k;
value = v;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
}
在编译期,是无法知道K和V具体是什么类型,只有在运行时才会真正根据类型来构造和分配内存。可以看一下现在Container类对于不同类型的支持情况:
public class Main {
public static void main(String[] args) {
Container<String, String> c1 = new Container<String, String>("name", "findingsea");
Container<String, Integer> c2 = new Container<String, Integer>("age", 24);
Container<Double, Double> c3 = new Container<Double, Double>(1.1, 2.2);
System.out.println(c1.getKey() + " : " + c1.getValue());
System.out.println(c2.getKey() + " : " + c2.getValue());
System.out.println(c3.getKey() + " : " + c3.getValue());
}
}
输出:
name : findingsea
age : 24
1.1 : 2.2
泛型接口来源
在泛型接口中,生成器是一个很好的理解,看如下的生成器接口定义:
public interface Generator<T> {
T next();
}
然后定义一个生成器类来实现这个接口:
public class FruitGenerator implements Generator<String> {
private final String[] fruits = new String[]{"Apple", "Banana", "Pear"};
@Override
public String next() {
Random rand = new Random();
return fruits[rand.nextInt(3)];
}
}
调用:
public class Main {
public static void main(String[] args) {
FruitGenerator generator = new FruitGenerator();
System.out.println(generator.next());
System.out.println(generator.next());
System.out.println(generator.next());
System.out.println(generator.next());
}
}
输出:
Banana
Banana
Pear
Banana
泛型方法来源
一个基本的原则是:无论何时,只要你能做到,你就应该尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛化,那么应该有限采用泛型方法。下面来看一个简单的泛型方法的定义:
public class Main {
public static <T> void out(T t) {
System.out.println(t);
}
public static void main(String[] args) {
out("findingSea");
out(123);
out(11.11);
out(true);
}
}
可以看到方法的参数彻底泛化了,这个过程涉及到编译器的类型推导和自动打包,也就说原来需要我们自己对类型进行的判断和处理,现在编译器帮我们做了。
这样在定义方法的时候不必考虑以后到底需要处理哪些类型的参数,大大增加了编程的灵活性。
再看一个泛型方法和可变参数的例子:
public class Main {
public static <T> void out(T... args) {
for (T t : args) {
System.out.println(t);
}
}
public static void main(String[] args) {
out("findingSea", 123, 11.11, true);
}
}
输出和前一段代码相同,可以看到泛型可以和可变参数非常完美的结合。
泛型代码与虚拟机
虚拟机里没有泛型类型对象--所有的对象都属于普通类。于是我们要将泛型类转变为普通的类,这称为"类型擦除“
类型擦除
无论何时定义一个泛型类型,都会自动提供一个相应的原始类型,这个原始类型的名字就是擦除类型参数后的泛型类型名。
具体来说,类型变量会自动地被擦除,被替换为其限定类型(extends
),如果没有限定的类型变量,就会被替换为Object.
例如上面例子中Pair<T>
在擦除类型变量<T>
后,会替换为Object
经过上面说的类型擦除后,结果就成为一个虚拟机认识的普通的类。
更具体来说,原始类型会使用第一个限定(extends)来替换类型变量。如果没有限定则会使用Object。
所以,为了提高效率,应该讲标签(tagging)接口放到限定列表的末尾,例如Serializable与Comparable都是限定条件,前者就属于一个标签接口,
表现形式是,有时调用方法,会传入Comparable,这时需要强制类型转换为Serializable
转换泛型表达式
编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。
Pair<Employee> buddies = ...;
Employee buddy = buddies.getFirst();
getFirst擦除类型后返回类型是Object,编译器就会自动插入一个强制类型转换,将其转换为Employee.
转换泛型方法
对于下面的标准形式的泛型方法:
//对于 <T extends Comparable> T man(T[] a)
//其中 <T extends Comparable> 是方法中的泛型类型T
//T是返回类型
//T[] a 是泛型的类型参数,表示传入一个形参T[]a
public static <T extends Comployee> T min(T[] a){
...
}
擦除类型后,会转换成下面的方法:
public static Comparable min(Comparable[] a)
编译器自动生成桥方法
虚拟机中,会由 参数类型和返回类型 共同指定一个方法。(注意,在编译器中,两个方法有相同的类型参数是不合法的,会报错,但虚拟机能正确处理这个情况)
即虚拟机可以为两个仅返回类型不同的方法生成字节码,虚拟机能够正确地处理这种情况
Java泛型类型的转换,有以下几个要点:
- 虚拟机中没有泛型,只有普通的类和方法。
- 所有的类型参数都会替换为它们的限定类型。
- 和合成桥方法来保持多态性。
- 为保持类型安全性,必要时候会自动插入强制类型转换。
调用遗留代码
限制与局限性
不能用基本类型实例化类型参数
运行时类型查询只适用于原始类型
不能创建参数化类型的数组
Varargs警告
不能实例化类型变量
不能构造泛型数组
泛型类的静态上下文中类型变量无效
不能抛出胡噢噢噢捕获泛型类的实例
可以取消对检查性异常的检查
注意擦除后的冲突
泛型类型的继承规则
泛型通配符
反射与泛型
- 下一节: java集合