一网打尽Java中的泛型机制
这次我们来学习一下Java8中的泛型,泛型是Java语言中的一个高级特性,所以我们必须对泛型有所了解,才能更加的深入了解Java语言,了解源码。
由于泛型机制的知识实在是太过于庞大,所以我这么只能分成几篇文章来好好说一下。
-
泛型的基本概念
- 什么是Java的泛型
- Java中的泛型
- 泛型服务对象是谁?
- 出入泛型需要知道的一些概念
- 为什么要有泛型?
- 泛型、参数化类型、原生类型、类型参数的概念认识
- 什么是Java的泛型
-
深入了解泛型
- 原生类型(raw type)和参数化类型(Parameterized type)
- 泛型的擦除机制
- 泛型的边界
- 泛型的类型推导
-
泛型的方使方式
- 泛型类与泛型接口中
- 泛型方法中
-
泛型面向对象之泛型类
- 什么是泛型类?
- 应用时出现的一些问题
- 泛型类的应用
-
泛型面向对象之泛型接口
- 什么是泛型接口
- 应用时出现的一些问题
- 泛型接口的应用
-
泛型面向对象之泛型方法
- 什么是泛型方法
- 应用时出现的一些问题
- 泛型方法的应用
-
泛型的使用限制
- 泛型不能使用原始类型
- 不能使用泛型进行实例化
- 泛型不能在类的静态域中使用
- 不能使用泛型去Instanceof比较
- 泛型不能转换类型
- 泛型不能应用在数组上
- 泛型不能重载
-
泛型的扩展知识
- 原生类型和参数化类型之间的问题
- 可变参数和泛型方法
泛型的基本概念
什么是Java的泛型?
Java中的泛型:
-
泛型是Java 1.5加入的新特性,是Java的一个高级特性。
-
Java的泛型就是允许在定义类,接口,方法时使用类型参数的一个概念,这个类型参数将在声明变量,创建对象,调用方法时被动态指定。
-
Java的泛型它实现了参数化类型的概念,也就是将我们所操作的数据类型指定为一个参数,既类型参数,最显著的特征就是可以使我们的代码得到复用,可以应用于多种类型。
-
但Java中的泛型又不同于C++中的泛型(模板),因为Java的泛型最终会被擦除,所以在一种角度上可以说Java中的泛型是伪泛型,C++中的才是真泛型。
泛型所面向的对象:
泛型可以应用在类、接口和方法的创建中,应用了泛型的类、接口、方法分别称为泛型类、泛型接口、泛型方法。
- 泛型类
- 泛型接口
- 泛型方法
出入泛型需要知道的一些概念
为什么要有泛型?
- 类型安全 :泛型的主要目标是提高 Java 程序的类型安全。通过类型参数,编译器可以在一个更高的层面上去验证类型是否正确。没有泛型,这些假设就只存在于程序员的头脑中
- 消除强制类型转换:泛型的一个附带好处是,消除源代码中的许多强制类型转换。这使得代码更加可读,并且减少了出错机会。
- 提高代码复用性: 我们不需要再因为类型的不同而在不同的方法做同样的处理,只要为该方法或类应用泛型就可以了。
当然还有如《Java编程思想》所说,促进Java泛型出现的很重要原因之一是为了创造容器类
为什么要有泛型? - 作者:@pyj—
泛型、参数化类型、原生类型、类型参数的概念认识
-
参数化类型:
参数化类型(Parameterized type)就是将类型参数化,把类型变成数学中的一个x,x可以任何的类型 -
泛型:
泛型(Generic type)就是参数化类型(Parameterized type),是一个东西,只是不同的说法。泛型实现了类型参数化的概念,如List<E>
就是一个泛型 -
原生类型
原生类型(Raw type)就是引入泛型之后出现的一个概念,是相对泛型(参数化类型)而言的。为了区别实现了泛型和没有实现泛型的代码实现。如泛型List<String> list
的原生类型就是List list
,List list
的参数化类型就是List<String> list
-
类型参数:
类型参数也是引入泛型之后的一个概念,如果说泛型是一种思想,那么类型参数就是一个有具体概念的东西。如下代码public class List<E> {}
。E就是一个类型参数,它就是数学中函数的x。通俗点讲E就是一个参数,这个参数可以是任意类型,可以使Integer,可以使Integer[],也可以使String,所以叫类型参数,即类型的参数。
为了避免混淆,下面采用更严格的术语来描述:
下面表格的概念来源effective Java 第二版
术语 | 代码示例 |
---|---|
原生类型 | List |
泛型 | List<E> |
形式类型参数 | E |
参数化类型 | List<String> |
实际参数化类型 | String |
通常情况下泛型和参数化类型可以说是一个意思。
深入了解泛型
原生类型(raw type)和参数化类型(Parameterized type)
原生类型和参数化类型的概念:
引入了泛型之后,Java就引入了两种类型的概念:
- 原生类型
- 参数化类型。
因为引入了泛型,为了更好的区别应用了泛型的类,接口,方法和没有应用泛型的类,接口,方法。我们把应用了泛型的称为参数化类型(Parameterized type),既泛型(generic type),而参数化类型对应自己没应用泛型时的状态称为原生类型(raw type)
原生类型和参数化类型在代码中的区别:
//调用类时体现
List<String> list1; //这就是一个参数化类型(Parameterized type)
List list2; //这就是一个原生类型(raw type)
List list3 = new ArrayList<String>(); //这是一个原生类型变量指向了一个参数化类型对象
//类定义中体现
public class<T> Demo{} //参数化类型类Class<T>
class Demo1{} //原生类型类Demo1
public class Demo{
//方法中体现
public <T> void show(T t){} //参数化类型
public void show(){} //原生类型
}
参数化类型就是针对接口、类、方法而言的。因为泛型就是面向于接口、类和方法的应用。
参数化类型(泛型)的泛型参数表达式中可以有大致5种形式:
最后我们来说一下Java泛型参数表达式的5种表现形式
- 开放参数化类型(open generic type)
- 封闭参数化类型(closed generic type)
- 无界通配符类型(unbounded wildcard type)
- 上界通配符类型(upper bounded wildcard type)
- 下界通配符类型(lower bounded wildcard type)
List<E> list; //开放参数化类型
List<String> list; //封闭参数化类型
List<?> list; //无界限通配符类型
List<? extens xxx> list; //上界限通配符类型
List<? super xxx> list; //无界限通配符类型
原生类型和参数化类型之间的关系
一个原生类型运用了泛型机制之后就成了参数化类型,所以我们可以看出他们是有联系的。但一个类型虽然被泛型化了,我们还可以把它当作非泛型化(原生类型)的类型用。
public class Demo<T>{
public static void main(String[] args){
Demo demo = new Demo();
}
}
//我们可以看到Demo类定义时运用了泛型,是一个参数化类型。从Demo变成了Demo<T>.
//但实际在调用的时候,却发现,我们可以直接Demo demo = new Demo(),而不是Demo<T> demo = new Demo<T>();
出现这样的原因,就R大所说,是当年Java1.5引进泛型时为了兼容1.5版本前没有泛型实现的代码。比如说在1.5版本前实现的一些第三方类库等等,都是没有泛型实现的。没有泛型实现的第三方类库已经被使用Java语言开发的人广泛应用了。1.5后,如果泛型机制不能往前兼容,就会让开发第三方类库(很庞大)人和使用第三方类库的人烦恼。所以如果参数化类型不能当原生类型使用,就会导致该功能被人唾弃,竟然出了一个不能向下兼容的垃圾功能,so…emmm
小结:
所以,这里主要就是向说明泛型的引入之后,会延伸出两个概念。这两个概念是不对等的,但是可以向下兼容罢了。
在C++中List<String>
本身就是一个类型,是跟List
不同的类型。我们就可以对比成Java中两个不同的类,是会被编译成两个Class文件的。但事实上Java不会这么做,因为Java的泛型是会擦除的。List<String>
和List
最终都只是个List
泛型中的擦除机制
Java中的泛型与C++中的泛型不同,C++的泛型是一个真泛型,既List<String>
和List是两个不同的类型。但是Java中,编译之后的泛型信息都会被擦除,所以List<String>
和List<String>
和List实际都是List类型。
为什么会有擦除呢?
Java的泛型就是在C++的泛型中启发的,那么为什么Java不设计真正的泛型,既跟C++一样的泛型模式呢?Java设计擦除的主要原因Java1.5才出现泛型机制,就是为了兼容,为了从非泛型代码从泛型代码转变的过程,既不破坏现有类库的情况下,将泛型融入Java语言中。擦除使得现有的非泛型代码能够不通过改变的情况下继续使用。如果Java从第一个版本就支持了泛型,那么Java实现肯定是一个类型与C++的泛型,可以这么说擦除机制是一种曲线救国路线,只是为了更好的平衡语言性能和兼容性
代码中泛型的擦除
class<T> Demo{
T t;
show(T t){}
public static void main(String[] args){
Demo<String> demo = new Demo<>();
}
}
从代码的Demo<String> demo = new Demo<>();
这句中,泛型语法一直在强烈的提示你,里面的T被替换成了String。但是事实并非如此,T会被擦除,也不是String去替换,而是所有类型的超类Object。所以实际情况是,你必须提前自己“那不是一个String,那只是一个Object”。只是编译器最后会帮我们实现类型转换,从Object转换为String
泛型机制中的边界
有了边界,我们可以确定泛型的范围,比如说
/**
* 泛型类Demo,类型参数E是继承于Son的
* 这就给了类型参数一个边界,也就是上界,最多只能是一个Son类
*/
public class Demo<E extends Son>{
public void do(E e){
e.doSomeing(); //因为有了边界,所以我们就才可以e.doSomeing()
//没有边界,编译器就不知道你的e到底是什么类型,也不知道你有什么方法
}
}
class class Son{
public void doSomeing(){}
}
通配符
- 上界通配符
- 下届通配符
- 无界通配符
通配符传送门 =>【Java学习笔记系列】深入学习Java泛型机制中的通配符
类型推导
- 泛型类、泛型接口的类型推导
- 泛型方法的类型推导
类型推断在Java5,7,8的变化:
场景一(Java7升级):
在Java5中,类型推断机制比较弱,不成熟,所以存在一些诟病,比如
ArrayList<String> list = new ArrayList<String>();
ArrayList<String> list = new ArrayList<>(); //Java5-6中,这样的形式时无法通过编译的,7开始可以
首先我们看到第一种定义方式,你是不是也会想,我们在定义变量时已经声明了参数类型,为什么实例化时还要声明一次,这是不是有点多余,而且写起来也很麻烦呢?编译器不是应该从我定义变量时的参数类型声明中就能推断出我实例化的是什么类型了吗?多写一次真的很麻烦呀。只能说当时的编译器的确对类型推断支持的不太够。
所以在Java7后,就开始支持泛型类在实例化时空尖扩号的书写方式
//由定义变量时声明的类型参数就能推断出实例对象的类型参数是什么,所以不需要我们自己填写
ArrayList<String> list = new ArrayList<>();
原理:
原理是整个代码的上下文环境,编译器得知了泛型类声明时已经声明“我需要String
类型”,所以类型推导出实例化的对象的参数类型也肯定是String
,所以编译器都已经知道了,我们也就不用在实例化时显式地写出来类型参数了。但你必须要记住的是不能少写了**<>,类型推断只是帮你省略了尖括号里面的内容。但不能少了尖括号。如果你少了<>,那是原生类型和参数化类型**之间的兼容性交流了,而不属于泛型的类型推断问题。
场景二(Java8升级):
在Java8中泛型中,引入了一个概念叫目标类型(Target Type),用于代替之前的上下文环境,这个目标类型是什么呢?有什么作用呢?
目的类型就是你所声明的类型,作用就是编译器能够根据目标类型来进行准确的类型推导,并且拓宽到可以根据方法的参数声明来进行类型推导。我们来看下面这个例子:
public class Test {
public static <T> ArrayList<T> getList(){
return new ArrayList<T>();
}
public static void main(String[] args) {
ArrayList<String> list = getList();
}
}
在Java5~Java7中,上面的方式都是可以实现的(当然8也可以),并不会报错。<T> getList()
是一个泛型方法,返回一个ArrayList<T>
泛型类实例对象。5~7中,编译器可以根据上下文环境来类型推导,ArrayList<String> list = getList()
整句话中,上文ArrayList<String> list
定义list变量声明了需要的类型参数为String
,而下文就是getList()
方法,它返回的是一个ArrayList<T>
类型,所以编译器可以根据上下文结合做出类型推导,根据前面需要的类型参数为String
,而推断出getList()
方法所操作的T是也必然是一个String
,所以最后编译器已经得知getList()
方法的类型参数T的具体类型是String
,返回的自然也是一个ArrayList<String>
类型的对象。所以这里不会出现问题。但是5~7不支持对方法的参数声明进行类型推断,代码如下
public class Test {
public static <T> ArrayList<T> getList(){
return new ArrayList<T>();
}
public static void fun(ArrayList<String> list){} //定义了个静态方法,里面放了个有类型参数的参数
public static void main(String[] args) {
//第一次方式
ArrayList<String> list = Test.getList();
fun(list); //ok Java5 ~ 8 pass
//第二种方式
fun(Test.getList()); //Java5 ~ 7 error , Java8 pass
//The method fun(ArrayList<String>) in the type Test
//is not applicable for the arguments (ArrayList<Object>)
}
}
第一种方式,跟前面说的差不多,我们得到list,是一个ArrayList<String>
的对象,其类型参数为String
,把这个对象传入fun()
方法中,与方法参数列表中的参数所声明的类型参数也匹配,都是String
,所以编译器通过,不报错。
第二种方式,我们直接将getList()
方法返回的实例对象传入fun()
方法中,这就出现一个问题了,在Java8之前的版本中,会报错,提示方法声明中要的参数是ArrayList<String>
类型,但你却给我传了个ArrayList<Object>
,这不符合要求啊。为什么会这样呢?这是因为8之前的编译器并不能很好的支持对方法的参数声明进行类型推导。所以编译器无法知道getList()
方法的T到底是一个什么类型?所以就只能返回一个所有类的超类Object
类型。很明显String
不等于Object
。但也是可以解决的,可以通过下面的方式告诉泛型方法我具体需要的类型是什么。
fun(Test.<String>getList());
而在Java8中,有了目标类型的概念,用于代替所谓的上下文环境,且对方法的参数声明的类型推导进行了支持。所以我就不用这么麻烦总要显式的告诉你,我到底需要什么类型,因为方法参数中ArrayList<String>
是目标类型,所以要传过了那自然也要是String。所以编译器根据目标类型进行类型推导,让getList()方法知道自己所操作的T的具体类型是String,所以getList()直接返回了我们想要的ArrayList<String>
对象,而不是ArrayList<Object>
小结:
Java5 - 7 支持泛型类定义时,由上下文环境中通过类型推导得知方法返回的T的具体类型是什么
ArrayList<String> = getList();
Java7 改进了之前的类型推导,使得泛型类实例化时可以根据上下文环境进行类型推导,得知类型参数的具体类型,而不需要在定义变量时和对象实例化时都指定参数类型,减少啰嗦冗余的代码。
//before
ArrayList<String> = new ArrayList<String>();
//now
ArrayList<String> = new ArrayList<>();
Java8又改进了类型推导,引进了目标类型的概念(也就是之前说的上下文环境的改进型),使得方法的参数声明可以作为目标类型进行类型推导。
fun(ArrayList<String> list){}
//before
fun(Test.<String>getList());
//now
fun(Test.getList());
泛型的使用方式
一、泛型类与泛型接口中
接口跟跟差不多,就只说类
类定义时:
⑴类名右边用尖括号括着类型参数,这就定义了一个泛型类
public class Demo<T>{} //此时尖括号内部为类型参数,类型参数为开放泛型符号T
⑵类定义的内部
用类定义时的类型参数作为一个类型去定义变量
public class Demo<T>{
T t; //用泛型类的类型参数T定义一个成员变量
public void show1(){ T t;} //用泛型类的类型参数T定义一个局部变量
public void show2(T t){} //用泛型类的类型参数T当做方法形参
public T show3(T t){} //用泛型类的类型参数T当返回值类型
//方法声明中用类型参数作为形参的类型参数
public void test1(List<T>){} //此时尖括号内部为泛型类的类型参数T
public void test2(List<?>){} //此时尖括号内部为泛型表达式,表达式为无界通配符
public void test3(List<? extends Animal>){} //此时尖括号内部为泛型表达式,表达式为上界通配符
public void test4(List<? super Animal>){} //此时尖括号内部为泛型表达式,表达式为下界通配符
public void test3(List<? extends T>){} //此时尖括号内部为泛型表达式,表达式为上界通配符
public void test4(List<? super T>){} //此时尖括号内部为泛型表达式,表达式为下界通配符
}
类调用时:
⑴定义变量
可以用开放标识符或具体类型或通配符去定义一个泛型变量
List<T> list0; //定义变量时,类型参数可以为泛型类或类型方法的类型参数T
List<String> list1 ; //定义变量时, 类型参数可以限定为具体的类型。允许实例化ArrayList<String>
List<?> list2; //定义变量时,类型参数为无界通配符。不允许实例化ArrayList<?>
List<? extends Object> list3; //定义变量时,类型参数为无界通配符。不允许实例化ArrayList<? extends Object>
List<? super Object> list4; //定义变量时,类型参数为无界通配符。不允许实例化ArrayList<? super Object>
⑵实例化对象
只能用开放标识符或具体类型做为类型参数去实例化对象
new ArrayList<T>(); //实例化对象时,类型参数可以为泛型类或类型方法的类型参数T
new ArrayList<String>(); //实例化对象时,限定类型参数为具体的类型,实例化时,类型参数不允许为通配符
二、泛型方法中
public <E> void show1(){E t;} //用泛型方法的类型参数E定义一个局部变量
public <E> void show2(E t){} //用泛型方法的类型参数E当做方法形参
public <E> E show3(E e){} //用泛型方法的类型参数E当返回值类型
public <E> void test1(List<E>){}
public <E> void test2(List<?>){} //此时尖括号内部为泛型表达式,表达式为无界通配符
public <E> void test3(List<? extends Animal>){} //此时尖括号内部为泛型表达式,表达式为上界通配符
public <E> void test4(List<? super Animal>){} //此时尖括号内部为泛型表达式,表达式为下界通配符
public <E> void test3(List<? extends E>){} //此时尖括号内部为泛型表达式,表达式为上界通配符
public <E> void test4(List<? super E>){} //此时尖括号内部为泛型表达式,表达式为下界通配符
总结一下:
所以我们得出几条结论:
- 在定义泛型类时,类名尖括号内容只允许为开放泛型符号或者开放泛型符号配合上下界通配符,如没有意义的标识符
T
,T extends Object
- 通配符只能用类进行在定义变量时的尖括号内。常用于定义变量或方法形参中。
- 在定义变量时,尖括号内可以是开放泛型符号、具体类型、通配符
- 在实例化时,尖括号内只能是开发泛型符号和具体类型
泛型面向对象之泛型类
泛型类的概念
什么是泛型类?使用了泛型机制的类,我们就称为泛型类,JDK中也有很多例子使用了泛型。比如说HashMap
public class HashMap<K,V> extends AbstractMap<K,V> //HashMap源码
implements Map<K,V>, Cloneable, Serializable
可以看到在类的实例化过程中,我们给其变量和实例对象使用了封闭的参数化类型。
应用时出现的一些问题?
匿名内部类应用泛型机制
泛型类的应用
元组类库
元组(Tuple):
什么是一个元组呢?通常,我们在开发中常常有这样的做法,通过一次的函数调用返回多个对象。但是return语句只能返回一个对象,那怎么办呢?我们通过创建一个传输对象来封装我们想要返回的多个对象。这样的传输对象的概念就是一个元组(Tuple),也称数据传送对象或者信使
应用:
在没有泛型之前,我们有这种元组的需求时,代码如下
public class TwoTuple{
String param1;
String param2;
}
以上是封装了两个String对象的元组,用于给需要的人封装两个String数据进去,然后返回该TwoTuple类对象。可是这里有个问题,要是我要封装的数据不是String类型怎么办?比如说一个是String,一个是Integer类型。那就只能再创建一个元组。
public class TwoTuple2{
String param1;
Integer param2;
}
此时,我想返回的数据不想要一个Integer和一个String了,而是两个Integer类型时,那怎么办?那没办法,只能再创建一个元组。当然也会有人说可以通过命令两个Object对象的方式,比如
public class TwoTuple3{
Object param1;
Object param2;
}
的确这可以解决上面提到的问题,但是这是没有泛型之前的做法,有了泛型机制之后,我们可以怎么做呢?
public class TwoTuple4 <A,B> {
A param1;
B param2;
}
以上应用了开放的泛型机制,从此只要是封装两个变量的需要,不用管你想传入的类型是什么,都可以返回把数据封装进该元组,进行使用。
链表的应用
以下是链表中结点的结构类。你可以想象以下,平时我们使用的LinkedList集合,它的底层数据结构就是一个链表。因为集合中存放的对象是可以不一致的。总部不能LinkedList集合中为每一个类型都实现了一个对应的结点类吧,这很冗余也很啰嗦。所以为了配合这种需求,要怎么来解决这个问题呢?那就必须要用到我们的泛型了。
//链表中的结点
public class Node<T> {
public T data; //数据域
public Node<T> next; //指针域
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public Node() {
this(null,null);
}
}
泛型面向对象之泛型接口
泛型接口的概念
什么是泛型接口?使用了泛型机制的接口就是一个泛型接口,通常泛型接口被用在各个类的生产器中,。同样,JDK中也有很多生动的例子,如HashMap所实现的Map接口
public interface Map<K,V> //Map接口
泛型接口的应用
生成器
应用时出现的一些问题?
一、泛型接口在继承中要注意的问题:
- 实现泛型接口的类必须实现泛型,否则会编译报错。
- 继承泛型接口的子接口也必须实现泛型,否则也会编译报错
//泛型接口
public interface Generator<T> {
}
//实现泛型接口的类也必须实现泛型<T>,否则会编译报错
class Demo implements Generator<T> { //error ,T cannot be resolved to a type
}
//同理,泛型接口的子接口也必须实现泛型
interface Genertor2<T> extends Generator<T>{ //error,T cannot be resolved to a type
}
泛型面向对象之泛型方法
什么是泛型方法?
应用了泛型机制的方法就是泛型方法,比如下面的show()方法就是一个泛型方法
public <T> void show(T t){
return t;
}
应用时出现的一些问题?
一、普通方法在泛型类中使用类型参数和泛型方法的区别
二、静态方法只能使用静态泛型类的类型参数?
三、非泛型类的构造方法也可以使用泛型称为泛型构造方法
泛型的使用限制
泛型不能使用原始类型
public class Box<T> {
private T t;
public void add(T t) {
this.t = t;
}
public T get() {
return t;
}
public static void main(String[] args){
//Box<int> box1 = new Box<>(); //编译错误,类型参数的类型不能是基本类型
Box<Integer> box2= new Box<>(); //success,Integer是一个类
}
}
泛型是针对类、接口、方法而言的,且应用泛型时,可接受参数也只能是一个类,既类型参数是不允是基本类型的,必须使用其包装类型 - 类。
不能使用泛型进行实例化
不能使用泛型实例化的意思是,不能把类型参数当类型去实例化,如E e = new E()
。而非不能使用ArrayList<String> list = new ArrayList<>()
去实例化。我们看下面的代码:
public class Box<T> {
public static <E> void add() {
//E item = new E(); //编译错误,Cannot instantiate the type E
}
public static void main(String[] args){
Box<String> box = new Box<>(); //ok,因为实例化的是Box,而不是String。
}
}
我们来分析一下为什么不能使用类型参数当类型去实例化**
- 第一个原因是类型会被擦除,虚拟机并不知道T的具体类型是什么。
- 第二个原因是因为类型参数是一个未知的类型,谁能确定该类型是否能被实例化,如果类型参数T的具体类型是一个不可被实例化的类型(比如构造函数为私有的类)呢?或该类型并没有无参构造方法呢?那运行时岂不是会报错?那要怎么解决?
所以为了避免这些问题,不允许使用类型参数进行实例化。
创建类型对象(弥补new实例):
但是要实现这样的方式,也是可以的,可以通过反射来获得,因为反射是可以获得私有的构造方法的。
public static <T> void add(Box<T> box, Class<T> clazz) {
T item = clazz.newInstance(); // OK
}
也是通过自行传入class类型去实例化
泛型不能在类的静态域中使用
我们来看一下这段代码输出的结果是什么?
ArrayList<String> list1 = new ArrayList<String>();
ArrayList<Integer> list2 = new ArrayList<Integer>();
System.out.println(list1.getClass() == list2.getClass());
如果你说是false,那就错啦。在C++中,list1和list2的确不是一个类型。但在Java中不是这样,Java的泛型是伪泛型,所有泛型信息最终都是会擦除的。所以list1和list2的本质还是一个ArrayList。
所以说,我们最后得出的结论是list1
和list2
实际上是同一个类的不同实例对象。我们在了解了这个知识的前提下再来想想为什么泛型不能在类的静态域中使用?
public class Demo<T>{
//静态变量
static T t1; //error
T t2; //success
//静态代码块
static {
T t; //error
}
//静态方法
//Cannot make a static reference to the non-static type T
static void show(T t){} //error
//静态泛型方法
static <E> void show(E e){} //success
}
我们可以看到Demo
类是一个参数化类型,其类型参数是一个开放泛型标识符T。也就是说Demo这个类在实例化时,我可以有个很多种用法,比如
Demo<String> demo1 = new Demo<>();
Demo<Integer> demo2 = new Demo<>();
可是我们再来想想,一个类的静态域的所有东西都是该类的所有对象所共享的,也就是说如果static T t1
如果能通过编译,t1就是demo1和demo2对象所共享的变量。那么问题来了,t1
到底是String
类型还是Integer
类型呢?所以这就会导致一个严重的冲突。
可是你又会说为什么静态泛型方法的E就可以用,就可以通过编译,这又是为什么呢?因为E是泛型方法所定义的,不是属于类的。如果你在静态泛型方法中使用了T,那自然也是会报错的
不能使用泛型去Instanceof比较
//第一种
if("abc" instanceof T){ } //编译错误
//Cannot perform instanceof check against type parameter T.
//Use its erasure Object instead since further generic type information will be erased at runtime
//第二种
Box<Integer> box = new Box<Integer>;
if(box instanceof Box<Integer>){} //编译错误
//Cannot perform instanceof check against parameterized type Box<Integer>.
//Use the form Box<?> instead since further generic type information will be erased at runtime
第一种:
因为Java的泛型是伪泛型,最后编译器是会擦除泛型的信息的,所以类型参数T是会被擦除的。所以运行时虚拟机根本无法得知T的具体类型,因为以及被擦除了。所以一个"abc" 是否是 T 类型,这根本无法判断。所以不允许
第二种:
第二种也一样,在运行时,根本不存在参数化类型,既泛型。也即是根本不存在Box<Integer>
这个类型,因为编译器都会把它擦除,运行时都只有是原生类型,所以无法判断。只能if(box instanceof Box){}
弥补泛型无法instanceof的措施:
public class InstanceOf<T> {
Class<T> classType; //存放类型
public InstanceOf(Class<T> classType) {
this.classType = classType;
}
public boolean isInstanceOf(Object o){
return this.classType.isInstance(o); //检测o的类型是否是classType类型
}
//test
public static void main(String[] args) {
//虽然无法获得类型参数的具体类型,但是我们可以在实例化时传入其类型。
InstanceOf<Integer> instance= new InstanceOf<>(Integer.class);
System.out.println(instance.isInstanceOf(new String()));
System.out.println(instance.isInstanceOf(new Integer(1)));
}
}
因为类型参数T最后被会擦除,所以我们可以通过自行传入T的具体类型去判断,达到曲线救国
泛型不能转换类型
这里说的泛型不能进行转换的意思是,同属一个类,但其泛型的类型参数不同,它们之间是不能互相转换的,如下代码
//实例化一个ArrayList<Integer>泛型类实例
ArrayList<Integer> intList = new ArrayList<Integer>();
//将ArrayList<Integer>强转给ArrayList<String>变量
ArrayList<String> strList = (ArrayList<String>)intList; //error,Cannot cast from ArrayList<Integer> to ArrayList<String>
这里主要的疑问是,ArrayList<Integer>
和ArrayList<String>
的本质都是ArrayList类型,那为什么不能转换呢?
我们在上面说过,Java的泛型是个伪泛型,虽然ArrayList<Integer>
和ArrayList<String>
的本质都是ArrayList类型。但是编译器为了类型安全的判断,他们虽然本质是一样的,但是在这个层面上,是可以当做两个不同的类型。一个内部存储Integer的List怎么能被转换成一个内存存储String的List呢?
记住: 这是泛型与泛型之间的事情,如果说是原生类型和泛型之间就是另一个层面了
另外就如我说所以不要搞混淆,那是两个类型参数不同的泛型(参数化类型)不能**相互转换,**而不是只要用泛型去转换。以下我们就用泛型的类型参数去转换一个数据类型,让它转型为我们类型参数T的类型。
public class Converter<T> {
T t;
public T show(Object o){
return (T)o; //可以转换,但是会获得编译警告,Type safety: Unchecked cast from Object to T
}
}
泛型不能应用在数组上
不是实例化泛型数组
List<String>[] strList; //定义一个泛型数组变量,编译期间是ok的
List<String>[] strLis = (List<String>[]) new Object[20]; //编译期也是ok,但运行期间实际是会报运行异常的。
List<String>[] strList = new ArrayList<String>[10]; //这句话是报编译错误的,不允我们实例化泛型数组
由上面我们可以发现,其实实际上Java是支持泛型数组的。但是为什么不能使用的,只是Java规范了不让我们使用泛型数组而已。因为使用泛型数组可能会导致一些安全问题,所以索性就禁止使用了。
安全问题,我们来看下面得代码:
//伪代码,真实情况是无法实现的
List<String>[] strList = new ArrayList<String>[10]; // not really allowed,假设能通过
Object[] objects = (Object[]) strList; //将泛型数组强转成一个Object数组
List<Integer> intList = new ArrayList<Integer>(); //定义一个泛型集合List<Integer>
intList.add(new Integer(3)); //插入一个Integer数据
objects[1] = intList; // 警告,将Integer泛型集合赋值给Object数组第二个元素
String str = objects[1].get(0); // 错误,run-time error - ClassCastException
重点最后两句,objects对象其实是一个List<String>
数组,但因为Java并不存在真正的泛型,所以Object本质只是一个List
数组,所以objects的第二元素才可以指向List<Integer>
类型的intList,而不报错。但最后从objects[1]位置把其中的数据取出,并赋值给String则报错。因为objectArrays[0]本身指向的是一个List<String>
类型,但取出来的却是一个List<Integer>
类型,这就出现了类型冲突了。
//实际编译后的代码是这么执行的,有个(String)强转,因为指向的是List<String>类型的数组
String str = (String)objectArrays[0].get(0);
(String)Integer所以报错。
但是:
List<String>[] strList = new ArrayList[10];
List<?>[] strList1 = new ArrayList<?>[10];
这两种情况是可以实现的,Java只是不允许我们实例化泛型数组,但可以实现无界限的泛型数组
泛型不能重载
public class Demo{
//error,Erasure of method print(List<String>) is the same as another method in type Demo
public void print(List<String> stringList) { }
public void print(List<Integer> integerList) { }
}
我们都知道方法重载是根据方法的参数列表来决定的,但是List<String> stringList
和List<Integer> integerList
这两个参数的类型到底算一样吗?虽然编译上,为了类型安全问题,让他们看起来不是同一个类型。但是编译之后,泛型信息给擦除,它们就会露出他们的本质。既他们就是同一个类型List。所以编译器才会提示Demo类具有两个同名且参数列表相等的重复函数。
泛型的扩展知识
原生类型和参数化类型之间的问题
我们首先来看这个几段代码,我们发现了什么?
场景一:
public class Demo<T>{
public static void main(String[] args){
Demo demo = new Demo();
}
}
//我们可以看到Demo类定义时运用了泛型,是一个参数化类型。从Demo变成了Demo<T>.
//但实际在调用的时候,却发现,我们可以直接Demo demo = new Demo(),而不是Demo<x> demo = new Demo<x>();
这个问题我们上面已经讨论过了,一个参数化类型是可以当成原生类型使用的,为了兼容问题。就如我们的ArrayList<E>
一样
//ArrayList的源码
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
//使用
ArrayList<String> strList = new ArrayList<String>(); //当参数化类型ArrayList<E>来使用
ArrayList list = new ArrayList(); //当原生类型ArrayList来使用
场景二:
//这里将一个原生类型的ArrayList实例对象赋值给了一个参数化类型的List变量
List<String> list3 = new ArrayList();
//这里将一个参数化类型的ArrayList实例对象赋值给一个原生类型的List变量
List list4 = new ArrayList<String>();
以上两种定义方式否会出现编译时警告问题
//将参数化类型指向原生类型时会提示,需要将原生类型修改为参数化类型
ArrayList is a raw type. References to generic type ArrayList<E> should be parameterized
//将原生类型指向参数化类型会提示,需要参数化类型对象的引用应该要被参数化
List is a raw type. References to generic type List<E> should be parameterized
所以从上面我们可以知道,原生类型和参数化类型可以相互赋值。会出现这种情况,跟问题一是一样的,也是为了兼容性问题。既然Java已经允许了一个参数化类型可以当原生类型来使用,那自然对一个原生类型变量指向一个参数化类型对象或一个参数化类型变量指向一个原生类型对象都是可以实现的。但是编译器会提示警告,不推荐这么使用。
这时候我们就会想,既然不会报错,可以通过编译,**那么这两种不同的定义方式在代码上的实现会有什么区别吗?**我们来看一下下面的代码:
public class RawTest {
public static void main(String[] args) {
List<String> list1 = new ArrayList(); //参数化指向原生
List list2 = new ArrayList<String>(); //原生指向参数化
//传入int类型
list1.add(1); //编译错误,The method add(int, String) in the type List<String> is not applicable for the arguments (int)
list2.add(1); //仅有警告,Type safety: The method add(Object) belongs to the raw type List. References to generic type List<E> should be parameterized
System.out.println(list2);
}
}
由上我们可知:
- 当参数化类型变量指向原生类型对象时,list对象的数据也只能是参数化式定义的类型String,不能是别的,这跟正常的情况下是一样的。
- 当原生类型变量指向参数化类型对象时,list对象的数据可以是参数化类型时的String类型,同时也可以接收其他类型的数据
所以我们总结一下。关于造成这个不同行为的标准是看左边的变量是否有参数化,如果等式左边的变量被参数化了,那么可接收的类型就只能是参数化的类型。其他情况随意。
场景三:
public class Test {
public static void main(String[] args) {
ArrayList<Integer> intList = new ArrayList<Integer>(); //一个封闭的参数化类型
ArrayList<String> strList = new ArrayList<String>(); //一个封闭的参数化类型
ArrayList list = intList; //将一个指定参数为Integer的参数化类型引用赋值给一个原生类型的变量
list.add("str"); //赋予String对象
System.out.println(list); //output: [str] String类型
list = strList; //将一个指定参数为String的参数化类型引用赋值给一个原生类型的变量
list.add(124); //赋予Integer对象
System.out.println(list); //output: [124] Integer类型
//但这样就不行了,Type mismatch: cannot convert from ArrayList<String> to ArrayList<Integer>
intList = strList //error,编译错误
//同理是不行的
ArrayList<Integer> intList = new ArrayList<String>();
}
}
因为前面两种的出现是为了解决原生类型和参数化类型的兼容性问题。而两个参数化类型之间则不存在兼容问题,而第三、四种赋值方式的两个参数化类型的类型参数都不一致,一个是Integer,一个是String。那自然不允许赋值啦。
归结为什么会出现上面的问题,根本原因就是Java的泛型不是真正的泛型,在下面这个链接中R大评论中,也就对这个行为的进行了说明。Java不能实现真正泛型的原因? - @作者:RednaxelaFX 的评论
我们再来看看这么一个情况,如果你说那是Integer和String之间没有关系,即使是类型参数之间具有继承关系那也是不行的,如下
//Animal是Dog的父类
ArrayList<Animal> animalList = new ArrayList<Animal>();
ArrayList<Dog> dogList = new ArrayList<Dog>();
//error ,cannot convert from ArrayList<Dog> to ArrayList<Animal>
animalList = dogList;
这也是会报错的,因为Animal是Dog的父类。但是List<Animal>
这个泛型集合List不是List<Dog>
泛型结合的父类。只是他们之间的数据是父子关系而已。
可变参数和泛型方法
参考资料
- 《Java编程思想》
- Java 泛型,了解这些就够用了。 - 作者:@逃离沙漠
- 为什么要有泛型? - 作者:@pyj—
- java 泛型详解-绝对是对泛型方法讲解最详细的,没有之一 - 作者:@VieLei
- 三句话总结JAVA泛型通配符(PECS) - 作者:@蚁方阵
- Java总结篇系列:Java泛型 - 作者:Windstep
- Java技术----Java泛型详解 - 作者:Java初级码农
- 黑马程序员——参数化类型与原始化类型
- Java不能实现真正泛型的原因? - @作者:RednaxelaFX 的评论
- Java 8 新特性之泛型的类型推导
- java泛型基础、子类泛型不能转换成父类泛型–未完待续 - 作者@:lijingran
- Java1.5泛型指南中文版(Java1.5 Generic Tutorial)