- 文章优先发布在Github,其它平台会晚一段时间,文章纠错与更新内容只在Github:https://github.com/youthlql/JavaYouth
- 转载须知:转载请注明GitHub出处,让我们一起维护一个良好的技术创作环境。
- 如果你要提交 issue 或者 pr 的话建议到 Github 提交。笔者会陆续更新,如果对你有所帮助,不妨Github点个Star~。你的Star是我创作的动力。
1、泛型的本质是为了参数化类型,也就是在在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型,很明显这种方法提高了代码的复用性。
2、泛型的引入提高了安全性,泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。。
3、在没有泛型的情况的下,通过对类型 Object 的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。
那么泛型的好处就是在编译的时候能够检查类型安全,并且所有的强制转换都是自动和隐式的。
public class GlmapperGeneric<T> {
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
public static void main(String[] args) {
// do nothing
}
/**
* 不指定类型
*/
public void noSpecifyType(){
GlmapperGeneric glmapperGeneric = new GlmapperGeneric();
glmapperGeneric.set("test");
// 需要强制类型转换
String test = (String) glmapperGeneric.get();
System.out.println(test);
}
/**
* 指定类型
*/
public void specifyType(){
GlmapperGeneric<String> glmapperGeneric = new GlmapperGeneric();
glmapperGeneric.set("test");
// 不需要强制类型转换
String test = glmapperGeneric.get();
System.out.println(test);
}
}
再举例子说明一下
不安全举例
package keyAndDifficultPoints.Generic;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: youthlql-吕
* @Date: 2020/10/15 16:09
*
* 功能描述:
*/
public class Test_Safe {
public static void main(String[] args) {
test();
}
public static void test() {
List arrayList = new ArrayList();
arrayList.add("aaaa");
arrayList.add(100);
for (int i = 0; i < arrayList.size(); i++) {
String s = (String) arrayList.get(i);
System.out.println(s);
}
}
}
结果:
aaaa
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
at keyAndDifficultPoints.Generic.Test_Safe.test(Test_Safe.java:25)
at keyAndDifficultPoints.Generic.Test_Safe.main(Test_Safe.java:16)
很明显的一个类型转换错误。ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。
泛型提高安全性
将上面的代码稍微改一下
public static void test01(){
List arrayList = new ArrayList<>();
arrayList.add("aaaa");
//下面代码编译时就直接报错了
arrayList.add(100);
for (int i = 0; i < arrayList.size(); i++) {
String s = (String) arrayList.get(i);
System.out.println(s);
}
}
通过泛型来提前检测类型,编译时就通不过。
我们看一下比较常用的JUC包
public CompletableFuture thenComposeAsync(
Function super T, ? extends CompletionStage> fn) {
return uniComposeStage(asyncPool, fn);
}
public CompletableFuture thenComposeAsync(
Function super T, ? extends CompletionStage> fn,
Executor executor) {
return uniComposeStage(screenExecutor(executor), fn);
}
public CompletableFuture whenComplete(
BiConsumer super T, ? super Throwable> action) {
return uniWhenCompleteStage(null, action);
}
public CompletableFuture whenCompleteAsync(
BiConsumer super T, ? super Throwable> action) {
return uniWhenCompleteStage(asyncPool, action);
}
这些都大量的用到了泛型,如果不把泛型学好,想真正深入源码了解一些东西,可能就完全看不懂了。
泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。
最普通的泛型类:
package keyAndDifficultPoints.Generic;
/**
* @Author: youthlql-吕
* @Date: 2020/10/15 16:38
*
* 功能描述:
*/
public class Test_GenericClass {
public static void main(String[] args) {
test();
}
public static void test(){
/**
* 1、泛型的类型参数只能是类类型(包括自定义类),不能是简单数据类型(比如int,long这些)
* 2、传入的实参类型需与泛型的类型参数类型相同,即为这里的Integer。
* 3、new 后面的泛型参数可以省略
*/
Generic genericInteger1 = new Generic(123);
Generic genericInteger = new Generic<>(123);
Generic genericString = new Generic("my");
System.out.println(genericInteger.getVar());
System.out.println(genericString.getVar());
}
}
/**
* 1、此处T虽然可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
* 但是为了代码的可读性一般来说:
* K,V用来表示键值对
* E是Element的缩写,常用来遍历时表示
* T就是Type的缩写,常用在普通泛型类上
* 2、还有一些不常见的U,R啥的
*/
class Generic {
//key这个成员变量的类型为T,T的类型由外部指定
private T var;
public Generic(T var) { //泛型构造方法形参key的类型也为T,T的类型由外部指定
this.var = var;
}
public T getVar() { //泛型方法getKey的返回值类型为T,T的类型由外部指定
return var;
}
}
class MyMap { // 此处指定了两个泛型类型
private K key; // 此变量的类型由外部决定
private V value; // 此变量的类型由外部决定
public K getKey() {
return this.key;
}
public V getValue() {
return this.value;
}
public void setKey(K key) {
this.key = key;
}
public void setValue(V value) {
this.value = value;
}
};
结果:
123
my
Process finished with exit code 0
还是以上面的泛型类为例进行测试
public static void test01() {
Generic generic = new Generic("我是字符串");
Generic generic1 = new Generic(123);
Generic generic2 = new Generic(123.123);
Generic generic3 = new Generic(false);
System.out.println(generic.getVar());
System.out.println(generic1.getVar());
System.out.println(generic2.getVar());
System.out.println(generic3.getVar());
}
结果:
我是字符串
123
123.123
false
Process finished with exit code 0
没有报错,正确输出了。
泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:
interface Info{ // 在接口上定义泛型
public T getVar() ; // 定义方法,方法的返回值就是泛型类型
}
当实现泛型接口的类,未传入泛型实参时:
/**
* 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 即:class InfoImpl implements Info
* 如果不声明泛型,如:class InfoImpl implements Info,编译器会报错:"Unknown class"
*/
class InfoImpl<T> implements Info<T> { // 定义泛型接口的子类
private T var;
public InfoImpl(T var) {
this.setVar(var);
}
public void setVar(T var) {
this.var = var;
}
public T getVar() {
return this.var;
}
}
当实现泛型接口的类,传入泛型实参时:
/**
* 传入泛型实参时:
* 定义一个是先烈实现这个接口,虽然我们只创建了一个泛型接口Info
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
* 即:InfoImpl01,public String getVar();中的的T都要替换成传入的String类型。
*/
class InfoImpl01 implements Info { // 定义泛型接口的子类
private String var;
public InfoImpl01(String var) {
this.setVar(var);
}
public void setVar(String var) {
this.var = var;
}
public String getVar() {
return this.var;
}
}
在java中,泛型类和接口的定义非常简单,但是泛型方法就比较复杂了。
泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型。
最简单的一个泛型方法
public class Test_GenericMethod {
public static void main(String[] args) {
Test_GenericMethod test_genericMethod = new Test_GenericMethod();
Integer integer = test_genericMethod.genericMethod(12);
System.out.println(integer);
}
/**
* 说明:
* 1、public 与 返回值中间非常重要,可以理解为声明此方法为泛型方法。
* 2、只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3、表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4、 后面的这个T,代表这个方法的返回值类型
* 4、与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public T genericMethod(T a) {
return a;
}
}
下面来细说一下泛型方法
首先说一个误区
class Generic01 {
private T key;
public Generic01(T key) {
this.key = key;
}
/**
* 1、这个虽然在方法中使用了泛型,但这并不是一个泛型方法。这只是类中一个普通的
* 成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。所以在这个方法中才
* 可以继续使用 T 这个泛型。
*/
public T getKey() {
return key;
}
/**
* 1、这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
* 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。
*/
// public E setKey(E key) {
// this.key = key;
// }
}
基本用法(非)
package keyAndDifficultPoints.Generic;
/**
* @Author: youthlql-吕
* @Date: 2020/10/15 17:46
*
* 功能描述:
*/
public class Test_GenericMethod {
public static void main(String[] args) {
Test_GenericMethod test_genericMethod = new Test_GenericMethod();
Generic01 generic01 = new Generic01<>(123);
Generic01 generic02 = new Generic01<>("AAAAA");
test_genericMethod.genericMethod_test01(generic01);
test_genericMethod.genericMethod_test02(generic02, "我是T");
test_genericMethod.Method01(generic01);
}
/**
* 说明:
* 1、public 与 返回值中间非常重要,可以理解为声明此方法为泛型方法。
* 2、只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3、表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4、 后面的这个T,代表这个方法的返回值类型
* 4、与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public T genericMethod(T a) {
return a;
}
/**
* 1、这才是一个真正的泛型方法。
* 2、首先在public与返回值之间的必不可少,这表明这是一个泛型方法,并且声明了一个泛型T。
* 3、这个T可以出现在这个泛型方法的任意位置.泛型的数量也可以为任意多个
*/
public T genericMethod_test01(Generic01 generic01) {
System.out.println("我是genericMethod_test01:" + generic01.getKey());
T test = generic01.getKey();
return test;
}
public T genericMethod_test02(Generic01 generic01, V value) {
System.out.println("我是genericMethod_test02:" + generic01.getKey() + "==> value:" + value);
T test = generic01.getKey();
return test;
}
//这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic这个泛型类做形参而已。
public void Method01(Generic01 extends Number> generic01) {
System.out.println(generic01.getKey());
}
//这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
//同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
public void Method02(Generic01> generic01) {
System.out.println(generic01.getKey());
}
/**
* 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class 'E' "
* 虽然我们声明了,也表明了这是一个可以处理泛型的类型的泛型方法。
* 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
*/
// public T showKeyName(Generic01 generic01, T t) {
// return t;
// }
}
结果:
我是genericMethod_test01:123
我是genericMethod_test02:AAAAA==> value:我是T
123
Process finished with exit code 0
当然这并不是泛型方法的全部,泛型方法可以出现杂任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下。
package keyAndDifficultPoints.Generic;
/**
* @Author: youthlql-吕
* @Date: 2020/10/15 20:14
*
* 功能描述:
*/
public class Test_GenericMethod01 {
public static void main(String[] args) {
Apple apple = new Apple();
Person person = new Person();
GenerateTest generateTest = new GenerateTest();
//apple是Fruit的子类,所以这里可以
generateTest.show_1(apple);
//编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
//generateTest.show_1(person);
//使用这两个方法都可以成功
generateTest.show_2(apple);
generateTest.show_2(person);
//使用这两个方法也都可以成功
generateTest.show_3(apple);
generateTest.show_3(person);
}
}
abstract class GenericFruit {
}
class Fruit {
@Override
public String toString() {
return "fruit";
}
}
class Apple extends Fruit {
@Override
public String toString() {
return "apple";
}
}
class Person {
@Override
public String toString() {
return "Person";
}
}
class GenerateTest {
public void show_1(T t) {
System.out.println(t.toString());
}
/**
* 1、在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
* 2、由于泛型方法在声明的时候会声明泛型,因此即使在泛型类中并未声明泛型,编译器也能够正确识别
泛型方法中识别的泛型。
*/
public void show_3(E t) {
System.out.println(t.toString());
}
/**
* 1、在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T
* 不是同一种类型。也就是说main函数中使用的时候也可以是不一样的泛型类型
*/
public void show_2(T t) {
System.out.println(t.toString());
}
}
结果:
apple
apple
Person
apple
Person
Process finished with exit code 0
再看一个泛型方法和可变参数的例子:
public class Test_GenericMethod02 {
public static void main(String[] args) {
print("123",753,123.12);
}
//必须是三个点
public static void print(T... args) {
for (T t : args) {
System.out.println(t);
}
}
}
结果:
123
753
123.12
Process finished with exit code 0
静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。
即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。
public class StaticGenerator<T> {
/**
* 1、如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
* 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
* 如:public static void show(T t){..},此时编译器会提示错误信息:
"StaticGenerator cannot be refrenced from static context"
* 2、泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系。换句话说,
* 泛型方法所属的类是不是泛型类都没有关系。
* 3、泛型方法,可以声明为静态的。原因:泛型参数是在调用方法时确定的。并非在初始化类时确定,所以无所谓
*/
public static <E> List<E> copyFromArrayToList(E[] arr){
ArrayList<E> list = new ArrayList<>();
for(E e : arr){
list.add(e);
}
return list;
}
}
可能合上面的有一些重复
1、泛型异常类
//异常类不能声明为泛型类,编译报错
class MyException extends Exception{
}
2、
package keyAndDifficultPoints.Generic.Minutiae;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: youthlql-吕
* @Date: 2020/10/15 22:28
*
* 功能描述:
*/
public class Test_Minutiae1 {
}
class Order {
String orderName;
int orderId;
//类的内部结构就可以使用类的泛型
T orderT;
public Order(){
//编译不通过
// T[] arr = new T[10];
//编译通过
T[] arr = (T[]) new Object[10];
}
public Order(String orderName,int orderId,T orderT){
this.orderName = orderName;
this.orderId = orderId;
this.orderT = orderT;
}
//如下的三个方法都不是泛型方法
public T getOrderT(){
return orderT;
}
public void setOrderT(T orderT){
this.orderT = orderT;
}
@Override
public String toString() {
return "Order{" +
"orderName='" + orderName + '\'' +
", orderId=" + orderId +
", orderT=" + orderT +
'}';
}
//静态方法中不能使用类的泛型。
// public static void show(T orderT){
// System.out.println(orderT);
// }
public void show(){
//编译不通过
// try{
//
//
// }catch(T t){
//
// }
}
/**
* 2、泛型方法:在方法中出现了泛型的结构,泛型参数与类的泛型参数没有任何关系。换句话说,
* 泛型方法所属的类是不是泛型类都没有关系。
* 3、泛型方法,可以声明为静态的。原因:泛型参数是在调用方法时确定的。并非在初始化类时确定,
* 所以无所谓
*/
public static List copyFromArrayToList(E[] arr){
ArrayList list = new ArrayList<>();
for(E e : arr){
list.add(e);
}
return list;
}
}
class SubOrder extends Order {//SubOrder:不是泛型类
public static List copyFromArrayToList(E[] arr) {
ArrayList list = new ArrayList<>();
for (E e : arr) {
list.add(e);
}
return list;
}
}
class SubOrder1 extends Order {//SubOrder1:仍然是泛型类
}
package keyAndDifficultPoints.Generic;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: youthlql-吕
* @Date: 2020/10/15 12:10
*
* 功能描述: 测试泛型数组
*/
public class Test_GenericArray {
public static void main(String[] args) {
test02();
}
public static void test() {
//编译错误
// List[] ls = new ArrayList[10];
}
public static void test01() {
//这样声明是正确的
List>[] ls = new ArrayList>[10];
ls[1] = new ArrayList();
//这样写编译就报错了
// ls[1].add(1);
}
/**
* 下面是sun官方文档里写的。其实不用太纠结,平时泛型虽然用的多,但也不会用的这么奇葩。
*/
public static void test02(){
List>[] lsa = new List>[10]; // OK, array of unbounded wildcard type.
Object o = lsa;
Object[] oa = (Object[]) o;
List li = new ArrayList();
li.add(new Integer(3));
oa[1] = li; // Correct.
Integer i = (Integer) lsa[1].get(0); // OK
System.out.println(i);
}
//正确
public static void test03() {
List[] ls = new ArrayList[10];
ls[0] = new ArrayList();
ls[1] = new ArrayList();
ls[0].add("x");
}
}
sun文档
直接看代码注释
/*
1. 泛型在继承方面的体现
虽然类A是类B的父类,但是G 和G二者不具备子父类关系,二者是并列关系。
补充:类A是类B的父类,A 是 B 的父类
*/
@Test
public void test1() {
/**
* 下面是有继承关系,所以可以赋值
*/
Object obj = null;
String str = null;
obj = str;
Object[] arr1 = null;
String[] arr2 = null;
arr1 = arr2;
/**
* 下面属于并列关系,无继承关系。无法赋值
*/
//编译不通过
// Date date = new Date();
// str = date;
List<Object> list1 = null;
List<String> list2 = new ArrayList<String>();
//此时的list1和list2的类型不具有子父类关系
//编译不通过
// list1 = list2;
/*
反证法:
假设list1 = list2;
list1.add(123);导致混入非String的数据。出错。
*/
}
@Test
public void test2() {
AbstractList<String> list1 = null;
List<String> list2 = null;
ArrayList<String> list3 = null;
list1 = list3;
list2 = list3;
List<String> list4 = new ArrayList<>();
}
我们在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T,E,K,V,? 等等,下面来详细讲一下这些通配符。
本质上都是通配符没啥区别,只不过是编码时的一种约定俗成的东西(可以说提高了代码可读性)。比如上述代码中的 T ,我们可以换成 A-Z 之间的任何一个大小写字母都可以,并不会影响程序的正常运行,但是如果换成其他的字母代替 T ,在可读性上可能会弱一些。通常情况下,T,E,K,V,? 是这样约定的:
比较难的就是?
通配符,下面就着重讲一下
List listAnimals
但是如果用通配符的话:
List extends Animal> listAnimals
为什么要使用通配符而不是简单的泛型呢?通配符其实在声明局部变量时是没有什么意义的,但是当你为一个方法声明一个参数时,它是非常重要的。
package keyAndDifficultPoints.Generic;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: youthlql-吕
* @Date: 2020/10/15 21:25
*
* 功能描述: 泛型通配符测试
*/
public class Test_Wildcard_Character {
public static void main(String[] args) {
List dogList = new ArrayList<>();
test(dogList);
test1(dogList);
}
static void test(List extends Animal> animals) {
System.out.println("test输出:");
for (Animal animal : animals) {
System.out.print(animal.toString() + "-");
}
}
static void test1(List animals) {
System.out.println("test1输出:");
for (Animal animal : animals) {
System.out.print(animal.toString() + "-");
}
}
}
class Animal {
@Override
public String toString() {
return "Animal";
}
}
class Dog extends Animal {
@Override
public String toString() {
return "Dog";
}
}
test1()
在编译时就会飘红
所以,对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 > ),表示可以持有任何类型。像 test()
方法中,限定了上界,但是不关心具体类型是什么,所以对于传入的 Animal 的所有子类都可以支持,并且不会报错,而test1()
就不行。
/*
2. 通配符的使用
通配符:?
类A是类B的父类,G和G是没有关系的,二者共同的父类是:G>
*/
@Test
public void test3() {
List
上结:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:
下界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object
在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。
/*
3.有限制条件的通配符的使用。
? extends A:
G extends A> 可以作为G和G的父类,其中B是A的子类
? super A:
G super A> 可以作为G和G的父类,其中B是A的父类
*/
@Test
public void test4() {
List extends Person> list1 = null; //[-无穷,Person]
List super Person> list2 = null; //[Person,+无穷]
List list3 = new ArrayList();
List list4 = new ArrayList();
List
##? 和 T 的区别
?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ? 不行,比如如下这种 :
// 可以
T t = operate();
// 不可以
? car = operate();
简单总结下:
T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。
package keyAndDifficultPoints.Wildcard_Character;
import java.util.ArrayList;
import java.util.List;
/**
* @Author: youthlql-吕
* @Date: 2020/10/16 11:28
*
* 功能描述:
*/
public class Test_difference {
public static void main(String[] args) {
List integerList = new ArrayList<>();
List floatList = new ArrayList<>();
//编译报错
// test(integerList, floatList);
//编译通过
test1(integerList, floatList);
//编译通过
test(integerList, integerList);
test1(integerList, integerList);
}
// 通过 T 来 确保 泛型参数的一致性
public static void test(List dest, List src){
}
//通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型
public static void test1(List extends Number> dest, List extends Number> src){
}
}
public class Test_difference {
public static void main(String[] args) {
/*---------------------测试多重限定符---------------------*/
ArrayList list = new ArrayList<>();
ArrayDeque deque = new ArrayDeque<>();
LinkedList linkedList = new LinkedList<>();
//多重限定时,在编译的时候取最小范围或共同子类
test2(list);
// test3(list); 编译报错
//编译报错
// test2(deque);
// test3(deque);
//编译通过
test2(linkedList);
test3(linkedList);
}
//可以进行多重限定
public static void test2(T t) {
}
//可以进行多重限定
public static void test3(T t) {
}
//编译报错,无法进行多重限定
// public static extends List & Collection> void test4(List dest, List src){
//
// }
}
类型参数 T 只具有 一种 类型限定方式:
T extends A
但是通配符 ? 可以进行 两种限定:
? extends A
? super A
package keyAndDifficultPoints.Wildcard_Character;
/**
* @Author: youthlql-吕
* @Date: 2020/10/16 12:09
*
* 功能描述: 泛型反射
*/
public class Test_Reflect {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
A a = createInstance(A.class);
B b = createInstance(B.class);
}
/**
* 这样写明显是要安全很多的
*/
public static T createInstance(Class clazz) throws IllegalAccessException, InstantiationException {
return clazz.newInstance();
}
public static void getA(String path) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
A a = (A) Class.forName("keyAndDifficultPoints.Wildcard_Character.A").newInstance();
//很明显下面的这行代码是错的,但是写代码的时候你不知道path是哪个
// B b = (B)Class.forName("keyAndDifficultPoints.Wildcard_Character.A").newInstance();
System.out.println(a.toString());
}
}
class A {
String name;
@Override
public String toString() {
return "我是对象A";
}
}
class B {
String name;
@Override
public String toString() {
return "我是对象B";
}
}
class C {
//所以当不知道声明什么类型的 Class 的时候可以定义一 个Class>。
public Class> clazz1;
//因为T没有声明,所以编译报错
// public Class clazz2;
}
class D {
public Class> clazz;
// 不会报错
public Class clazzT;
}
Java的泛型是伪泛型,为什么说Java的泛型是伪泛型呢?因为在编译期间,所有的泛型信息都会被擦除掉,我们常称为泛型擦除。
Java中的泛型基本上都是在编译器这个层次来实现的,在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,编译器在编译的时候去掉,这个过程就称为类型擦除。
如在代码中定义的List
和List
等类型,在编译后都会编程List,JVM看到的只是List。而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别。
可以通过两个例子,来证明java泛型的类型擦除。
例1:
@Test
public void test() {
List stringList = new ArrayList();
stringList.add("my");
List integerList = new ArrayList();
integerList.add(123);
System.out.println(stringList.getClass() == integerList.getClass());
}
结果:
true
Process finished with exit code 0
在这个例子中,我们定义了两个List,不过一个是List泛型类型,只能存储字符串。一个是List泛型类型,只能存储整形。最后,我们通过stringList对象和integerList对象的getClass方法获取它们的类的信息,最后发现结果为true。说明泛型类型String和Integer都被擦除掉了,只剩下了原始类型。
例2:
@Test
public void test01() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
List list = new ArrayList();
//这样调用add方法只能存储整形,因为泛型类型的实例为Integer
list.add(1);
//这样写编译就会报错
// list.add("my");
//通过反射的方式则可以存储String
list.getClass().getMethod("add", Object.class).invoke(list, "my");
for (int i = 0; i < list.size(); i++) {
System.out.println(list.get(i));
}
}
结果:
1
my
Process finished with exit code 0
在程序中定义了一个List泛型类型,如果直接调用add方法,那么只能存储整形的数据。不过当我们利用反射调用add方法的时候,却可以存储字符串。这说明了Integer泛型实例在编译之后被擦除了,只保留了 原始类型。
1、在上面,几次提到了原始类型。什么是原始类型?原始类型就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除,并使用其限定类型(无限定的变量用Object替换)。
例3:
package keyAndDifficultPoints.principle;
/**
* @Author: youthlql-吕
* @Date: 2020/10/16 23:01
*
* 功能描述:
*/
public class Test_principle02 {
public static void main(String[] args) {
}
}
class Test_Generic {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
下面我们用IDEA的工具,查看这个类的字节码信息。我把完整的字节码复制在下方:
// class version 52.0 (52)
// access flags 0x20
// signature Ljava/lang/Object;
// declaration: keyAndDifficultPoints/principle/Test_Generic
class keyAndDifficultPoints/principle/Test_Generic {
// compiled from: Test_principle02.java
// access flags 0x2
// signature TT;
// declaration: T
private Ljava/lang/Object; value
// access flags 0x0
()V
L0
LINENUMBER 13 L0
ALOAD 0
INVOKESPECIAL java/lang/Object. ()V
RETURN
L1
LOCALVARIABLE this LkeyAndDifficultPoints/principle/Test_Generic; L0 L1 0
// signature LkeyAndDifficultPoints/principle/Test_Generic;
// declaration: keyAndDifficultPoints.principle.Test_Generic
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
// signature ()TT;
// declaration: T getValue()
public getValue()Ljava/lang/Object;
L0
LINENUMBER 17 L0
ALOAD 0
GETFIELD keyAndDifficultPoints/principle/Test_Generic.value : Ljava/lang/Object;
ARETURN
L1
LOCALVARIABLE this LkeyAndDifficultPoints/principle/Test_Generic; L0 L1 0
// signature LkeyAndDifficultPoints/principle/Test_Generic;
// declaration: keyAndDifficultPoints.principle.Test_Generic
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
// signature (TT;)V
// declaration: void setValue(T)
public setValue(Ljava/lang/Object;)V
L0
LINENUMBER 21 L0
ALOAD 0
ALOAD 1
PUTFIELD keyAndDifficultPoints/principle/Test_Generic.value : Ljava/lang/Object;
L1
LINENUMBER 22 L1
RETURN
L2
LOCALVARIABLE this LkeyAndDifficultPoints/principle/Test_Generic; L0 L2 0
// signature LkeyAndDifficultPoints/principle/Test_Generic;
// declaration: keyAndDifficultPoints.principle.Test_Generic
LOCALVARIABLE value Ljava/lang/Object; L0 L2 1
// signature TT;
// declaration: T
MAXSTACK = 2
MAXLOCALS = 2
}
可以明显的看到泛型T被替换成了Object。
因为在Test_Generic中,T是一个无限定的类型变量,所以用Object替换。其结果就是一个普通的类,如同泛型加入java变成语言之前已经实现的那样。在程序中可以包含不同类型的Test_Generic,如Test_Generic或Test_Generic,但是,擦除类型后它们就成为原始的Test_Generic类型了,原始类型都是Object。
从上面的那个例2中,我们也可以明白List被擦除类型后,原始类型也变成了Object,所以通过反射我们就可以存储字符串了。
2、如果类型变量有限定,那么原始类型就用第一个边界的类型变量来替换。
比如Test_Generic这样声明
class Test_Generic1
我们还是看字节码(后面如无必须,只截取部分字节码)
// class version 52.0 (52)
// access flags 0x20
// signature Ljava/lang/Object;
// declaration: keyAndDifficultPoints/principle/Test_Generic1
class keyAndDifficultPoints/principle/Test_Generic1 {
// compiled from: Test_principle03.java
// access flags 0x2
// signature TT;
// declaration: T
private Ljava/util/List; value
会发现T变成了List
如果顺序变一下
class Test_Generic1<T extends Collection & List>
字节码就变了
T变成了Collection
// class version 52.0 (52)
// access flags 0x20
// signature Ljava/lang/Object;
// declaration: keyAndDifficultPoints/principle/Test_Generic1
class keyAndDifficultPoints/principle/Test_Generic1 {
// compiled from: Test_principle03.java
// access flags 0x2
// signature TT;
// declaration: T
private Ljava/util/Collection; value
也就是说在进行字节码编译的时候是使用离T最近的一个类型。
在上文说到&的多重限定时
package keyAndDifficultPoints.principle;
import java.util.*;
/**
* @Author: youthlql-吕
* @Date: 2020/10/16 23:30
*
* 功能描述:
*/
public class Test_principle04 {
public static void main(String[] args) {
/*---------------------测试多重限定符---------------------*/
List list = new ArrayList<>();
Queue queue = new ArrayDeque<>();
LinkedList linkedList = new LinkedList<>();
//多重限定时,在编译的时候取最小范围或共同子类
test2(list);
// test3(list); 编译报错
test4(list);
//编译报错
// test2(deque);
// test3(deque);
// test4(queue);
//编译通过
test2(linkedList);
test3(linkedList);
test4(linkedList);
}
//可以进行多重限定
public static void test2(T t) {
}
//可以进行多重限定
public static void test3(T t) {
}
//可以进行多重限定
public static void test4(T t) {
}
//编译报错,无法进行多重限定
// public static extends List & Collection> void test4(List dest, List src){
//
// }
}
首先来看一下字节码
// class version 52.0 (52)
// access flags 0x21
public class keyAndDifficultPoints/principle/Test_principle04 {
// compiled from: Test_principle04.java
// access flags 0x1
public ()V
L0
LINENUMBER 11 L0
ALOAD 0
INVOKESPECIAL java/lang/Object. ()V
RETURN
L1
LOCALVARIABLE this LkeyAndDifficultPoints/principle/Test_principle04; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 17 L0
NEW java/util/ArrayList
DUP
INVOKESPECIAL java/util/ArrayList. ()V
ASTORE 1
L1
LINENUMBER 18 L1
NEW java/util/ArrayDeque
DUP
INVOKESPECIAL java/util/ArrayDeque. ()V
ASTORE 2
L2
LINENUMBER 19 L2
NEW java/util/LinkedList
DUP
INVOKESPECIAL java/util/LinkedList. ()V
ASTORE 3
L3
LINENUMBER 22 L3
ALOAD 1
INVOKESTATIC keyAndDifficultPoints/principle/Test_principle04.test2 (Ljava/util/List;)V
L4
LINENUMBER 24 L4
ALOAD 1
INVOKESTATIC keyAndDifficultPoints/principle/Test_principle04.test4 (Ljava/util/Collection;)V
L5
LINENUMBER 33 L5
ALOAD 3
INVOKESTATIC keyAndDifficultPoints/principle/Test_principle04.test2 (Ljava/util/List;)V
L6
LINENUMBER 34 L6
ALOAD 3
INVOKESTATIC keyAndDifficultPoints/principle/Test_principle04.test3 (Ljava/util/Queue;)V
L7
LINENUMBER 35 L7
ALOAD 3
INVOKESTATIC keyAndDifficultPoints/principle/Test_principle04.test4 (Ljava/util/Collection;)V
L8
LINENUMBER 38 L8
RETURN
L9
LOCALVARIABLE args [Ljava/lang/String; L0 L9 0
LOCALVARIABLE list Ljava/util/List; L1 L9 1
LOCALVARIABLE queue Ljava/util/Queue; L2 L9 2
LOCALVARIABLE linkedList Ljava/util/LinkedList; L3 L9 3
// signature Ljava/util/LinkedList;
// declaration: java.util.LinkedList
MAXSTACK = 2
MAXLOCALS = 4
// access flags 0x9
// signature (TT;)V
// declaration: void test2(T)
public static test2(Ljava/util/List;)V
L0
LINENUMBER 44 L0
RETURN
L1
LOCALVARIABLE t Ljava/util/List; L0 L1 0
// signature TT;
// declaration: T
MAXSTACK = 0
MAXLOCALS = 1
// access flags 0x9
// signature (TT;)V
// declaration: void test3(T)
public static test3(Ljava/util/Queue;)V
L0
LINENUMBER 49 L0
RETURN
L1
LOCALVARIABLE t Ljava/util/Queue; L0 L1 0
// signature TT;
// declaration: T
MAXSTACK = 0
MAXLOCALS = 1
// access flags 0x9
// signature (TT;)V
// declaration: void test4(T)
public static test4(Ljava/util/Collection;)V
L0
LINENUMBER 54 L0
RETURN
L1
LOCALVARIABLE t Ljava/util/Collection; L0 L1 0
// signature TT;
// declaration: T
MAXSTACK = 0
MAXLOCALS = 1
}
test4()
方法里离T最近的是Collection
,那么T在编译后就被Collection
代替了。那按理来说
test4(queue);
1、这里我们传一个Collection的实现类Queue,也应该是可以的啊,但是为什么报错了呢?注意一点报错报的是编译错误,泛型提供编译前检测机制,也就是说在没运行前,泛型规定了多重限定时,在编译的时候取最小范围或共同子类
。
2、那实际上到底可以不可以传Queue呢?根据之前的讲解,我相信大家已经有了结论。实际上是可以的,只不过要跳过编译检测机制,通过反射来放Queue。
在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object。在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。
class Test {
public static void main(String[] args) {
//不指定泛型的时候
int a1 = add(1, 2); //这两个参数都是Integer,所以T为Integer类型
Number b1 = add(1, 1.2);//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Number
Object c1 = add(1, "my");//这两个参数一个是Integer,以风格是Float,所以取同一父类的最小级,为Object
//指定泛型的时候
int a = Test.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类
// int b = Test.add(1, 2.2);//编译错误,指定了Integer,不能为Float
Number c = Test.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float
}
//这是一个简单的泛型方法
public static <T> T add(T x, T y) {
return x;
}
}
public static void main(String[] args) {
ArrayList arrayList=new ArrayList();
arrayList.add("123");
arrayList.add(123);//编译错误
}
类型擦除后,原始类型为Object,是应该运行任意引用类型的添加的。可实际上却不是这样,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。
那么,这么类型检查是针对谁的呢?我们来看例子:
public static void main(String[] args) {
ArrayList arrayList = new ArrayList();
arrayList.add(1); //编译报错
ArrayList arrayList1 = new ArrayList(); //第一种 情况
arrayList1.add(1); //编译报错
ArrayList arrayList2 = new ArrayList();//第二种 情况
arrayList2.add(1);
}
通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
因为类型擦除的问题,所以所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?
我么来看一下List的get()方法:
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
E elementData(int index) {
return (E) elementData[index];
}
可以看到基本各个类都已经自动帮你转了。
这个其实是类型擦除引起的最大的问题了。
public class Test_principle05 {
public static void main(String[] args) {
}
}
class Generic {
//key这个成员变量的类型为T,T的类型由外部指定
private T var;
public T getVar() {
return var;
}
public void setVar(T var) {
this.var = var;
}
}
class MyGeneric extends Generic{
@Override
public Integer getVar() {
return super.getVar();
}
@Override
public void setVar(Integer var) {
super.setVar(var);
}
}
实际上,从他们的@Override标签中也可以看到,在子类中重写这两个方法一点问题也没有,实际上是这样的吗?
分析:
泛型擦除后,父类是下面这样子
class Generic {
//key这个成员变量的类型为T,T的类型由外部指定
private Object var;
public Object getVar() {
return var;
}
public void setVar(Object var) {
this.var = var;
}
}
子类还是这样
class MyGeneric extends Generic<Integer>{
@Override
public Integer getVar() {
return super.getVar();
}
@Override
public void setVar(Integer var) {
super.setVar(var);
}
}
先来分析setValue方法,父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果实在普通的继承关系中,根本就不会是重写,而是重载。
重载(Overload):首先是位于一个类之中或者其子类中,具有相同的方法名,但是方法的参数不同,返回值类型可以相同也可以不同。
(1):方法名必须相同。
(2):方法的参数列表一定不一样。
(3):访问修饰符和返回值类型可以相同也可以不同。
重写(override):一般都是表示子类和父类之间的关系,其主要的特征是:方法名相同,参数相同,但是具体的实现不同。
重写的特征:
(1):方法名必须相同,返回值类型必须相同
(2):参数列表必须相同
(3):访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为public,那么在子类中重写该方法就不能声明为protected。
(4):子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为private和final的方法。
(5):构造方法不能被重写
我们来测试下到底是重载还是重写
public static void main(String[] args) {
MyGeneric myGeneric = new MyGeneric();
myGeneric.setVar(new Integer(1));
myGeneric.setVar(new Object());//编译错误
}
如果是重载的话,第四行代码是不会报错的,因为调的是不同的重载方法。但是发现编译报错了,也就是说没有参数是Object的这样的重载函数。所以说是重写了,导致MyGeneric对象只能调用自己重写的方法。
为什么会这样呢?
原因是这样的,我们传入父类的泛型类型是Integer,Generic,我们的本意是将泛型类变为如下:
class Generic {
//key这个成员变量的类型为T,T的类型由外部指定
private Integer var;
public Integer getVar() {
return var;
}
public void setVar(Integer var) {
this.var = var;
}
}
然后再子类中重写参数类型为Date的那两个方法,实现继承中的多态。
可是由于种种原因,虚拟机并不能将泛型类型变为Integer,只能将类型擦除掉,变为原始类型Object。这样,我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突。JVM知道你的本意吗?知道,可是它能直接实现吗,不能。如果真的不能的话,那我们怎么去重写我们想要的Integer类型参数的方法啊。
JVM采用了一个特殊的方法,来完成这项功能,那就是桥方法。
我们对下面这个类进行编译,看其字节码
public class MyGeneric extends Generic{
public static void main(String[] args) {
}
@Override
public Integer getVar() {
return super.getVar();
}
@Override
public void setVar(Integer var) {
super.setVar(var);
}
}
字节码:
// class version 52.0 (52)
// access flags 0x21
// signature LkeyAndDifficultPoints/principle/Generic;
// declaration: keyAndDifficultPoints/principle/MyGeneric extends keyAndDifficultPoints.principle.Generic
public class keyAndDifficultPoints/principle/MyGeneric extends keyAndDifficultPoints/principle/Generic {
// compiled from: MyGeneric.java
// access flags 0x1
public ()V
L0
LINENUMBER 9 L0
ALOAD 0
INVOKESPECIAL keyAndDifficultPoints/principle/Generic. ()V
RETURN
L1
LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x9
public static main([Ljava/lang/String;)V
L0
LINENUMBER 12 L0
RETURN
L1
LOCALVARIABLE args [Ljava/lang/String; L0 L1 0
MAXSTACK = 0
MAXLOCALS = 1
// access flags 0x1
public getVar()Ljava/lang/Integer; //这是我们重写的getVar()方法
L0
LINENUMBER 16 L0
ALOAD 0
INVOKESPECIAL keyAndDifficultPoints/principle/Generic.getVar ()Ljava/lang/Object;
CHECKCAST java/lang/Integer
ARETURN
L1
LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public setVar(Ljava/lang/Integer;)V 这是我们重写的setVar()方法
L0
LINENUMBER 20 L0
ALOAD 0
ALOAD 1
INVOKESPECIAL keyAndDifficultPoints/principle/Generic.setVar (Ljava/lang/Object;)V
L1
LINENUMBER 21 L1
RETURN
L2
LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L2 0
LOCALVARIABLE var Ljava/lang/Integer; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1041
public synthetic bridge setVar(Ljava/lang/Object;)V //编译时由编译器生成的桥方法
L0
LINENUMBER 9 L0
ALOAD 0
ALOAD 1
CHECKCAST java/lang/Integer
INVOKEVIRTUAL keyAndDifficultPoints/principle/MyGeneric.setVar (Ljava/lang/Integer;)V
RETURN
L1
LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L1 0
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1041
public synthetic bridge getVar()Ljava/lang/Object; //编译时由编译器生成的桥方法
L0
LINENUMBER 9 L0
ALOAD 0
INVOKEVIRTUAL keyAndDifficultPoints/principle/MyGeneric.getVar ()Ljava/lang/Integer;
ARETURN
L1
LOCALVARIABLE this LkeyAndDifficultPoints/principle/MyGeneric; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
}
从编译的结果来看,我们本意重写setValue和getValue方法的子类,竟然有4个方法。最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。
所以,虚拟机巧妙的使用了巧方法,来解决了类型擦除和多态的冲突。