文章目录
什么是泛型
泛型是指将类型泛化的编程方式,通过泛型编写的程序可以实现类型解耦,从而达到一段代码作用于多种类型的目的。Java通过类型变量
类型变量
类型变量是指类型参数,通常用<TypeVariable1,TypeVariable2>
的形式表示,在泛型代码的内部它往往被当作第二类型处理,一个结构可以有多个类型变量,它们之间用逗号隔开,在定义不同结构时它的位置也不尽相同。
类型变量的位置
在类中的位置
class Position<T> extends Genericity implements Supplier
在接口中的位置
interface Position<T> extends Supplier
在方法中的位置
public <T>void test(){}
类型变量的约束<T extends BoundingType>
extends
为约束绑定符,使用它意味着类型变量T必须实现或继承了BoundingType
,BoundingType
为约束类型,它可以是类也可以是接口,一个类型变量可以有多个约束类型从而组成约束列表,它们中间用&
分隔,但约束列表中只能有一个是类且必须放在约束列表的最前面。
泛型擦除
泛型是jdk5加入的新特性,为了履行代码永远向前兼容的承诺,java开发人员使用了类型擦除实现泛型。泛型擦除是指类型变量只会在编译前起作用,当java代码编译为字节码后字节码内是不带任何类型变量信息的,在源代码内的所有类型变量都会被擦除到约束列表的边界。
public class Genericity {
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<>();
ArrayList objects = new ArrayList();
System.out.println(strings.getClass()==objects.getClass());//true
}
}
虽然擦除后的字节码内不含有任何泛型信息,但泛型信息还是被保留在了字节码文件中,因此我们可以使用反射获取泛型信息:
public class Genericity {
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<>();
System.out.println(Arrays.toString(strings.getClass().getTypeParameters()));//[E]
}
}
无约束列表的类型擦除
无约束列表的类型变量会被擦除为Object类型,比如:
class className<T>{
T a;
};
会被擦除为
class className{
Object a;
};
有约束列表的类型擦除
当类型变量有约束列表时,擦除机制会擦除到约束列表的第一个约束类型
class className<T extends type1&type2>{
T a;
};
会被擦除为
class className{
type1 a;
};
如果我们在使用时传入的类型变量是实现或继承了type2,但擦除机制会擦除到type1,那么此时编译器就会自动进行必要的类型转换。
裸类型
裸类型是指不带类型变量的类型,裸类型是所有该类型泛化实例的共同父类型。编译后泛型类型都会变为裸类型,只有在元素访问时插入类型转型代码。
泛型缺陷
在使用泛型时,我们要随时想着泛型擦除后的类型是什么,如果擦除前擦除后代码的逻辑都没有发生改变,那么就可以放心的使用泛型,代码逻辑的改变就是泛型的缺陷,
基本类型不能作为类型变量
因为基本类型擦除后不能转为Object。
运行时不能获取泛型的类型信息
instanceof
public class Genericity<T>{
public static void main(String[] args) {
Genericity<String> stringGenericity = new Genericity<>();
if (stringGenericity instanceof Genericity<Integer>){//编译错误
System.out.println("我要自杀");
}
}
}
本来是想判断stringGenericity
是否属于Genericity<Integer>
类型,很显然不是,那么我就不会自杀,但泛型擦除后代码就变成了
public class Genericity{
public static void main(String[] args) {
Genericity stringGenericity = new Genericity();
if (stringGenericity instanceof Genericity){
System.out.println("我要自杀");
}
}
}
很显然这绝对会导致我死亡,所以instanceof
后不能跟类型变量或泛型。
new
public class Genericity<T>{
T a=new T();//error
T[] as=new T[10];//error
public static void main(String[] args) {
Genericity<String> stringGenericity = new Genericity<String>();//right
Genericity<String>[]genericities=new Genericity<String>[10];//error
}
}
第二行擦除后会变成
Object a =new Object();
实例化Object是毫无意义的,况且在编译前来讲我们也并不知道T是否有无参构造函数以及构造函数是否为public。
第五行擦除后会变成
Genericity stringGenericity = new Genericity();
逻辑没有发生变化,所以是可行的,之所以没有发生变化是因为在进入和退出边界处的动作没有变化。
至于有关数组与泛型结合产生的错误见下文
泛型与数组
在java中数组也是一种对象,它的超类也是Object,但是并没有见过它们的构造函数。
数组与集合的比较
public static void main(String[] args) {
ArrayList<String> strings = new ArrayList<String>();
String []strs=new String[10];
}
在实例化一个集合时我们需要给出两种数据类型,一种是集合的类型,一种是集合所持数据的类型,但是在实例化数组时我们只是指定它所持有数据的类型,这是因为当我们在实例化一个数组时jvm在底层创建了数组对象并进行实例化,由于这个数组对象没有重写Object的toString方法,我们可以打印一下看看它的数据类型
System.out.println(strs);//[Ljava.lang.String;@6e8dacdf
分号前面就是这个数组对象的类型,至于它具体什么意思可以参考一下字节码文件,结论就是数组的类型就是一个左中括号加上数组所持有数据的类型。
协变数组类型
public static void main(String[] args) {
Object[] objs=new String[10];
}
将一个String数组实例赋值给一个Object数组引用没有报错,这是使用多态技术吗?我们可以验证一下
System.out.println(new Object[0].getClass()==new String[0].getClass());//false
结果是false,显然这并不是多态,而是一种协变数组类型,所谓协变数组类型是指一个持有基类数组类型的数组可以指向一个持有子类数组类型的数组,但是这会造成一个问题:基类数组类型所持有数据的类型和子类数组类型所持有数据的类型是不同的,但是java想到了这个问题数组会记住最初实例化时所持有数据的类型,如在上例中我们只能传入String对象,当传入一个Object对象时在运行时就会报错。
为什么需要协变数组类型
在没有泛型之前,如果没有协变数组类型,就好比所有的类都没有基类,那么我们将为每一种数组类型实现一些功能完全相同只有类型不同的方法,显然这是不现实的,而有了协变数组类型,我们只需要使用Object[]类型定义一个通用方法即可,而出现了泛型之后,按道理说就不需要协变数组类型,但是为了以前代码的兼容性,不得不继续保留协变数组类型而不允许数组使用泛型。
解密数组与泛型
那么有人要问了,为什么不能使协变数组类型和泛型数组共存呢?因为存在以下一种及其特殊的情况,其中的注释极为重要,最后的结论是:本来以为这两者结合起来是强强联合,但没想到彻底抵消了全部的类型检查(编译时和运行时)。
public static void main(String[] args) {
/**
* 由于lists最初持有的数据类型为List<Integer>,是一个泛型
* 泛型的作用致使在编译时只能向lists添加ArrayList<Integer>类型的数据
*
* lists.add(new ArrayList<Integer>)//right
* lists.add(new ArraysList<Long>)//error
*/
ArrayList<Integer>[] lists = new ArrayList<Integer>[10];
/**
* 我们使用协变数组类型指向该数组,这时以下两行添加语句就会通过编译,
* 也就是说斜边数组类型使泛型编译时数据类型检查功能失效
*
* lists.add(new ArrayList<Integer>)//right
* lists.add(new ArraysList<Long>)//right
*
* 即使上面两行添加代码会通过编译,但由于数组会记住最初实例化时所持有数据的类型而在运行时报错,
* 但不幸的是数组记住的是ArrayList<Integer>泛型,
* 而擦除会使此泛型变为运行时的ArrayList类型,
* 那么这样上面两行添加语句也不会在运行时报错了,因为他们也都被擦除为ArraysList类型,
* 也就是说泛型让数组的记忆功能失效了.
*
* 本来以为这两者结合起来是强强联合,但没想到彻底抵消了全部的类型检查(编译时和运行时)
*/
Object[] objs=lists;
}
泛型与异常
try {
throw new MyException();
}catch (MyException<String> e){//error
}catch (MyException<Integer> e){//error
}
如果上述代码存在的话,那么擦除后catch语句内的异常的类型都是一样,这是不允许的,不过不用担心这种问题,因为java根本不允许一个泛型类继承异常类。
泛型与方法重载
泛型与多态
通配符
通过泛型我们已经可以将类型泛化,但是我们怎么将类型变量也泛化呢?这就需要使用通配符了。使用通配符的目的是实现一种泛型类型的协变类型机制,比如ArrayList<Number> numbers = new ArrayList<Integer>();
这句赋值语句是完全错误的,但是通过通配符我们可以使两个泛型之间建立某种类似协变类型的向上转型关系,通配符的本质是将类型变量在编译前就转换为某一范围内的某个类型,也就是代表类型变量的泛化程度,一旦类型变量泛化,那么在泛型代码内部将会有很多限制。
<? extends A>
它的含义是:类型变量可能是任何A或从A继承的某个类型,类型A称为类型变量的上界
对泛型代码外部的影响
ArrayList<? extends Number> numbers = new ArrayList<Integer>();//right
对泛型代码内部的影响
编译器对此类型变量所知的唯一信息就是它的上界,所以在进入边界动作发生时,编译器将阻止含有类型变量的泛型持有任何类型的数据,但在退出边界动作发生时,编译器会将泛型所持有的类型全部转换为它的上界。
public static void main(String[] args) {
ArrayList<? extends Number> numbers = new ArrayList<Integer>();
numbers.add(1);//error
Number number = numbers.get(0);//right
}
<?super A>
它的含义是:类型变量可能是A或A基类的类型,类型A称为类型变量的下界。
对泛型代码外部的影响
ArrayList<? super Integer> numbers = new ArrayList<Number>();
对泛型代码内部的影响
编译器对此类型变量所知的唯一信息就是它的下界,所以在进入边界动作发生时,编译器将允许含有类型变量的泛型持有类型变量下界及以下类型的数据,但在退出边界动作发生时,编译器会将泛型所持有的类型全部转换为Object。
public static void main(String[] args) {
ArrayList<? super Integer> numbers = new ArrayList<Number>();
numbers.add(1);//right
Object object = numbers.get(0);//right
Integer integer = (Integer)object//right
}
<?>
它的含义是类型变量可以是任何类型
对泛型代码外部的影响
public class Genericity<T>{
public static void main(String[] args) {
Genericity<String> stringGenericity = new Genericity<String>();//right
if (stringGenericity instanceof Genericity<Integer>){//编译错误
System.out.println("我要自杀");
}
if (stringGenericity instanceof Genericity<?>){//right
System.out.println("我要自杀");
}
Genericity<String>[] genericities1 = new Genericity<String>[10];//error
Genericity<?>[] genericities2 = new Genericity<?>[10];//right
}
}
对泛型代码内部的影响
编译器对此类型变量一无所知,所以在进入边界动作发生时,编译器将阻止含有类型变量的泛型持有任何类型的数据,但在退出边界动作发生时,编译器会将泛型所持有的类型全部转换为Object。
public static void main(String[] args) {
ArrayList<?> arrayList = new ArrayList();
arrayList.add(1);//error
Object o = arrayList.get(0);//right
}
自限定类型
class A<T extends A<T>>
它想实现的效果就是当其他类要继承A类时,要想使用A类的类型变量,必须如下定义:
class B extends A<B>
这么做的意义在于,自限定类型可以实现协变参数类型
//正常情况下
public class Genericity{
public static void main(String[] args) {
B b = new B();
A a = new A();
a.print(a);//right
a.print(b);//right
b.print(a);//right
b.print(b);//right
Arrays.stream(b.getClass().getDeclaredMethods()).forEach((method)->{
System.out.println(method.getName());//null
});
}
}
class A {
public void print(A arg){
System.out.println(arg.getClass().getSimpleName());
}
}
class B extends A{
}
//自限定类型情况下
public class Genericity{
public static void main(String[] args) {
B b = new B();
A a = new A();
a.print(a);//right
a.print(b);//right
b.print(a);//error
b.print(b);//right
Arrays.stream(b.getClass().getDeclaredMethods()).forEach((method)->{
System.out.println(method.getName());//null
});
}
}
class A <T extends A<T>>{
public void print(T arg){
System.out.println(arg.getClass().getSimpleName());
}
}
class B extends A<B>{
}