泛型
1. 概述
- 泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。Collection,List 这个就是类型参数,即泛型。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。Java语言引入泛型的好处是安全简单。
- 在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是一个安全隐患。
- 泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,以提高代码的重用率
1.1 举例
// 例1.
class Cache {
Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
/*
* Cache cache = new Cache();
* cache.setValue(134);
* int value = (int) cache.getValue();
* cache.setValue("hello");
* String value1 = (String) cache.getValue();
* 使用的方法也很简单,只要我们做正确的强制转换就好了。
但是,泛型却给我们带来了不一样的编程体验。
*/
class Cache<T> {
T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}*/
/*这就是泛型,它将 value 这个属性的类型也参数化了,这就是所谓的参数化类型。再看它的使用方法。
* Cache<String> cache1 = new Cache<String>();
* cache1.setValue("123");
* String value2 = cache1.getValue();
*
* Cache<Integer> cache2 = new Cache<Integer>();
* cache2.setValue(456);
* int value3 = cache2.getValue();
* */
// 例2.
@Test
public void genericTest1(){
List arrayList = new ArrayList();
arrayList.add(100);
arrayList.add(200);
//类型不安全
arrayList.add("aaa");
for (Object o : arrayList) {
//强转
int item = (Integer) o;
System.out.println(item);
}
}
1.2 在集合中的使用
@Test
public void genericTest2(){
List<Integer> arrayList = new ArrayList<Integer>();
arrayList.add(100);
arrayList.add(200);
//编译时就会进行检查,保证数据安全
//arrayList.add("aaa");
for (Integer o : arrayList) {
//避免了强转操作
int item = o;
System.out.println(item);
}
}
1.3 泛型的特性
- 只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除
- 在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦除,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
2. 泛型的定义和使用
泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法
2.1 泛型类
/*
1.如果一个类被 <T>的形式定义,那么它就被称为是泛型类。
2.此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
T 代表一般的任何类。
E 代表 Element 的意思
K 代表 Key 的意思。
V 代表 Value 的意思,通常与 K 一起配合使用。
S 代表 Subtype 的意思,文章后面部分会讲解示意。
3.泛型类还可以这样接受多个类型参数Generic<T,E,v>
*/
public class Generic<T> {
//field这个成员变量的类型为T,T的类型由外部指定
private T field;
//泛型构造方法形参field的类型也为T,T的类型由外部指定
public Generic(T field) {
this.field = field;
}
//泛型方法getField的返回值类型为T,T的类型由外部指定
public T getField() {
return field;
}
}
//泛型类的使用
@Test
public void demo1(){
//在实例化泛型类时,必须指定T的具体类型
//泛型的类型参数只能是类类型(包括自定义类),不能是简单类型,int--Integer
//泛型不指定,可以是任意类型
Generic<Integer> integerGeneric = new Generic<Integer>(123);
//jdk1.7泛型的简化操作
Generic<String> stringGeneric = new Generic<>("泛型");
System.out.println("integerGeneric--field:"+integerGeneric.getField());
System.out.println("stringGeneric--field:"+stringGeneric.getField());
}
2.2 泛型接口
泛型接口与泛型类的定义及使用基本相同
//定义一个泛型接口
public interface Generator<T> {
public T get();
}
//使用
/**
* 1.未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
* 如果不声明泛型,如:class ColorGenerator implements Generator<T>,编译器会报错
*/
public class ColorGenerator<T> implements Generator<T> {
@Override
public T get() {
return null;
}
}
/**
* 2.传入泛型实参时:
* 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
*/
public class ColorGenerator implements Generator<String> {
private String[] colors = {"red","blue","green"}; //测试使用
@Override
public String get() { //方法返回值 T 要换成 String
Random random = new Random();
return colors[random.nextInt(3)];
}
}
2.3 泛型方法
//说明:
/* 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
* 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
* 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
* 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
*/
public class GenericMethod<T>{
/**
泛型类,是在实例化类的时候指明泛型的具体类型;
泛型方法,是在调用方法的时候指明泛型的具体类型.
*/
public <T> void demo1(T t){
}
/**
声明的类型参数<E>,其实也是可以当作返回值的类型的。
为了避免混淆,如果在一个泛型类中存在泛型方法,那么两者的类型参数最好不要同名。
*/
public <E> E demo2(E e){
return null;
}
private T field;
//此方法为泛型类的成员方法,但不是泛型方法!
public T getField() {
return field;
}
}
2.3.1 泛型方法-可变参数
public <T> void demo3(T...args){
for (T arg : args) {
System.out.println(arg);
}
}
2.3.2 泛型方法-静态方法
public class StaticGeneric<T> {
//类加载在类实例化之前,获得类上定义的泛型需要类实例化
//public static void show(T t){};
//泛型方法能使方法独立于类而产生变化---使用泛型方法
public static <T> void show(T t){};
}
2.4 泛型的边界
我们简单的学习了泛型类、泛型接口和泛型方法。我们都是直接使用<T>
这样的形式来完成泛型类型的声明。有的时候,类、接口或方法需要对类型变量加以约束。
- 为泛型类添加边界:(传入的类型实参必须是指定类型的子类型)
public class Demo5<T extends Person1> {
public void add(T t){};
public static void main(String[] args) {
Demo5<Person1> demo1 = new Demo5<>();
Demo5<Student1> demo2 = new Demo5<>();
Demo5<Xstu> demo53 = new Demo5<>();
}
}
class Person1{}
class Student1 extends Person1{}
class Xstu extends Student1{}
- 为泛型方法添加边界:
public static <T> T get(T t1,T t2) {
if(t1.compareTo(t2)>=0);//编译错误
return t1;
}
在编译之前,也就是我们还在定义这个泛型方法的时候,我们并不知道这个泛型类型T,到底是什么类型,所以,只能默认T为原始类型Object。所以它只能调用来自于Object的那几个方法,而不能调用compareTo
方法。
可我的本意就是要比较t1和t2,怎么办呢?这个时候,就要使用类型限定,对类型变量T设置限定(边界)来做到这一点。
我们知道,所有实现Comparable接口的方法,都会有compareTo方法。所以,可以对做如下限定:
public static <T extends Comparable> T get(T t1,T t2) { //添加类型限定
if(t1.compareTo(t2)>=0);
return t1;
}
-
类型限定在泛型类、泛型接口和泛型方法中都可以使用,不过要注意下面几点:
-
不管该限定是类还是接口,统一都使用关键字 extends
-
可以使用&符号给出多个限定,比如
public static <T extends Comparable&Serializable> T get(T t1,T t2)
-
如果限定既有接口也有类,那么类必须只有一个,并且放在首位置
public static <T extends Object&Comparable&Serializable> T get(T t1,T t2)
-
在定义泛型类和方法时,只能用extends,只有在使用通配符声明泛型变量时,才可以使用super关键字
-
3. 通配符的使用
我们知道String是Object的一个子类,那么问题来了,在使用List作为形参的方法中,能否使用List的实例传入呢?在逻辑上类似于List和List是否可以看成具有父子关系的泛型类型呢? 当然很显然它俩就只是平级关系
由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
如何让它俩兼容,也就是说我们需要一个在逻辑上可以表示同时是List和List父类的引用类型。由此类型通配符应运而生。
类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。(当然上述方法也可以使用泛型方法,不过需要强转)
通配符有 3 种形式。
-
<?>
被称作无限定的通配符。 可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。
上面的代码中,方法内的参数是被无限定通配符修饰的 Collection 对象,它隐略地表达了一个意图或者可以说是限定,那就是 testWidlCards() 这个方法内部无需关注 Collection 中的真实类型,因为它是未知的。所以,你只能调用 Collection 中与类型无关的方法。
<?>
提供了只读的功能,也就是它删减了增加具体类型元素的能力,只保留与具体类型无关的功能。它不管装载在这个容器内的元素是什么类型,它只关心元素的数量、容器是否为空 -
<? extends T>
被称作有上限的通配符。 -
<? super T>
被称作有下限的通配符。
@Test
public void demo2(){
//1.定义
List<? extends Person> list1 = null;
List<? super Person> list2 = null;
//2.
List<Person> personList = new ArrayList<>();
List<Student> studentList = new ArrayList<>();
List<Object> objectList = new ArrayList<>();
list1 = personList;
list1 = studentList;
//list1 = objectList;
list2 = personList;
//list2 = studentList;
list2 = objectList;
Person person = list1.get(0);
Object object = list2.get(0);
list2.add(new Person());
list2.add(new Student());
}
4. 关于泛型数组
查看sun的说明文档,在java中是**”不能创建一个确切的泛型类型的数组”**的。也就是说下面的这个例子是不可以的:
List<Integer>[] li1 = new ArrayList<Integer>[];
List<Boolean> li2 = new ArrayList<Boolean>[];
/*
这两行代码是无法在编译器中编译通过的。原因还是类型擦除后带来的影响。
List<Integer>和 List<Boolean>在 jvm 中等同于List<Object>,所有的类型信息都被擦除,
程序也无法分辨一个数组中的元素类型具体是 List<Integer>类型还是 List<Boolean>类型。对于这样的情况,可 以在编译期提示代码有类型安全问题,比没有任何提示要强很多。
*/
而使用通配符创建泛型数组是可以的,如下面这个例子:
List<?>[] lis3 = new ArrayList<?>[10];
/*采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。*/
5. 类型擦除 Type erasure
5.1 类型擦除
Java的泛型是伪泛型,这是因为Java在编译期间,所有的泛型信息都会被擦掉,正确理解泛型概念的首要前提是理解类型擦除。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除。
Java 引入泛型擦除的原因是避免因为引入泛型而导致运行时创建不必要的类和向下兼容
// 例1:
public class GenericType {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2); //true
}
}
尽管ArrayList<String>
和ArrayList<Integer>
看上去是不同的类型,但是上面的程序会认为它们是相同的类型。ArrayList<String>
和ArrayList<Integer>
在运行时事实上是相同的类型。这两种类型都被擦除成它们的“原生”类型,即ArrayList
。无论何时,编写泛型代码时,必须提醒自己“它的类型被擦除了”。
// 例2:
public class Test2 {
public static void main(String[] args) throws Exception {
ArrayList<Integer> arrayList = new ArrayList<Integer>();
arrayList.add(1);//这样调用add方法只能存储整形,因为泛型类型的实例为Integer
arrayList.getClass().getMethod("add", Object.class).invoke(arrayList, "aaa");
for (int i=0;i<arrayList.size();i++) {
System.out.println(arrayList.get(i)); //[1,aaa]
}
}
在程序中定义了一个ArrayList
泛型类型实例化为Integer的对象,如果直接调用add方法,那么只能存储整形的数据。不过当我们利用反射调用add方法的时候,却可以存储字符串。这说明了Integer泛型实例在编译之后被擦除了,只保留了原始类型。
5.2 类型擦除后保留的原始类型(raw type)
什么是原始类型?原始类型(raw type)就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型。无论何时定义一个泛型类型,相应的原始类型都会被自动地提供。类型变量被擦除,并使用其限定类型(无限定的变量用Object)替换。
//例1:无限定的泛型类
//泛型擦除后的原始类型
class Generic<T> { class Generic {
private T value; private Object value;
public T getValue() { public Object getValue() {
return value; return value;
} }
public void setValue(T value) { public void setValue(Object value) {
this.value = value; this.value = value;
} }
} }
//例2:如果类型变量有限定,那么原始类型就用第一个边界的类型变量来替换。
public class Generic<T extends Comparable& Serializable> {} // 原始泛型为 Comparable
//为了提高效率,应该将标签接口(即没有方法的接口)放在边界限定列表的末尾。
注意:在调用泛型方法的时候,可以指定泛型,也可以不指定泛型。在不指定泛型的情况下,泛型变量的类型为 该方法中的几种类型的同一个父类的最小级,直到Object。在指定泛型的时候,该方法中的几种类型必须是该泛型实例类型或者其子类。
public class Test{
public static void main(String[] args) {
/**不指定泛型的时候*/
int i=Test2.add(1, 2); //这两个参数都是Integer,所以T为Integer类型
Number number = Test2.add(1, 1.2);//取同一父类的最小级,为Number
Object o=Test2.add(1, "asd");//取同一父类的最小级,为Object
/**指定泛型的时候*/
int a=Test2.<Integer>add(1, 2);//指定了Integer,所以只能为Integer类型或者其子类
Number c=Test2.<Number>add(1, 2.2); //指定为Number,所以可以为Integer和Float
}
//这是一个简单的泛型方法
public static <T> T add(T x,T y){
return y;
}
}
6. 类型擦除引起的问题及解决办法
6.1 泛型类型只有在静态类型检查期间才出现
- java编译器是通过先检查代码中泛型的类型,然后再进行类型擦除,再进行编译的。
public static void main(String[] args) {
ArrayList<String> arrayList=new ArrayList<String>();
arrayList.add("123");
arrayList.add(123); //编译错误
}
使用add方法添加一个整形,就会报错,说明这就是在编译之前的检查。因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该运行任意引用类型的添加的。可实际上却不是这样,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。
-
那么类型检查是针对谁的呢?因为Java最开始的版本是不支持泛型的,为了兼容以前的库而不得不使用擦除法,从而向下兼容,以ArrayList为例,看如何兼容
//之前写法 ArrayList list1 = new ArrayList(); //现在写法 ArrayList<String> list2 = new ArrayList<String>(); //兼容后,会出现以下两种情况 ArrayList list3 = new ArrayList<String>(); //1. ArrayList<String> list4 = new ArrayList(); //2. /* 1.两种情况都没有错误,不过第一种情况没有泛型的效果,第二种其实也就是JDK1.7后泛型的写法 2.究其原因,本来类型检查就是编译时完成的。new ArrayList()只是在内存中开辟一个存储空间,可以存储任何的类型对象。而真正涉及类型检查的是它的引用 */
举例:
public class TestGeneric {
public static void main(String[] args) {
ArrayList<String> arrayList1=new ArrayList();
arrayList1.add("1");//编译通过
arrayList1.add(1);//编译错误
String str1=arrayList1.get(0);//返回类型就是String
ArrayList arrayList2=new ArrayList<String>();
arrayList2.add("1");//编译通过
arrayList2.add(1);//编译通过
Object object=arrayList2.get(0);//返回类型就是Object
new ArrayList<String>().add("11");//编译通过
new ArrayList<String>().add(22);//编译错误
String string=new ArrayList<String>().get(0);//返回类型就是String
}
}
通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。
6.2 自动类型转换
因为类型擦除的问题,所有的泛型类型变量最后都会被替换为原始类型。这样就引起了一个问题,既然都被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?看下ArrayList和get方法:
返回结果之前会根据泛型变量进行强转(checkcast
类型转换检查,不在get方法里强转,是在调用的地方强转的)
6.3 类型擦除与多态的冲突与解决方法
现在有一个泛型类与它的子类
class Pair<T> {
private T value;
public T getValue() {
return value;
}
public void setValue(T value) {
this.value = value;
}
}
class DatePair extends Pair<Date> {
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
}
问题:我们在子类中重写这两个方法一点问题也没有,实际上,从他们的@Override标签中也可以看到,实际上是这样的吗?
分析:
//实际上,类型擦除后,父类的的泛型类型全部变为了原始类型Object,所以父类编译之后会变成下面的样子:
class Pair {
private Object value;
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
}
//再看子类的两个重写的方法的类型:
@Override
public void setValue(Date value) {
super.setValue(value);
}
@Override
public Date getValue() {
return super.getValue();
}
//先来分析setValue方法,父类的类型是Object,而子类的类型是Date,参数类型不一样,这如果是在普通的继承关系/中,根本就不是重写,而是方法的重载。
//为什么会这样呢?我们的本意是进行重写,实现多态。可是类型擦除后,只能变为了重载。这样,类型擦除就和多态有了冲突,结果是可以正常运行。原因是编译器为了维护这种重写的原则,为我们提供了一个解决方案:桥方法,在DatePair类中自动生成了一个桥方法:
查看 DatePair.class
字节码文件
public class com/generic2/DatePair extends com/generic2/Pair {
// compiled from: DatePair.java
// access flags 0x1
public <init>()V
L0
LINENUMBER 10 L0
ALOAD 0
INVOKESPECIAL com/generic2/Pair.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/generic2/DatePair; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1
public setValue(Ljava/util/Date;)V
L0
LINENUMBER 14 L0
ALOAD 0
ALOAD 1
INVOKESPECIAL com/generic2/Pair.setValue (Ljava/lang/Object;)V
L1
LINENUMBER 15 L1
RETURN
L2
LOCALVARIABLE this Lcom/generic2/DatePair; L0 L2 0
LOCALVARIABLE value Ljava/util/Date; L0 L2 1
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1
public getValue()Ljava/util/Date;
L0
LINENUMBER 20 L0
ALOAD 0
INVOKESPECIAL com/generic2/Pair.getValue ()Ljava/lang/Object;
CHECKCAST java/util/Date
ARETURN
L1
LOCALVARIABLE this Lcom/generic2/DatePair; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
// access flags 0x1041
public synthetic bridge setValue(Ljava/lang/Object;)V
L0
LINENUMBER 10 L0
ALOAD 0
ALOAD 1
CHECKCAST java/util/Date
INVOKEVIRTUAL com/generic2/DatePair.setValue (Ljava/util/Date;)V
RETURN
L1
LOCALVARIABLE this Lcom/generic2/DatePair; L0 L1 0
MAXSTACK = 2
MAXLOCALS = 2
// access flags 0x1041
public synthetic bridge getValue()Ljava/lang/Object;
L0
LINENUMBER 10 L0
ALOAD 0
INVOKEVIRTUAL com/generic2/DatePair.getValue ()Ljava/util/Date;
ARETURN
L1
LOCALVARIABLE this Lcom/generic2/DatePair; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1
}
从编译的结果来看,我们本意重写setValue
和getValue
方法的子类,竟然有4个方法,其实不用惊奇,最后的两个方法,就是编译器自己生成的桥方法。可以看到桥方法的参数类型都是Object
,也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的桥方法。而打在我们自己定义的setvalue
和getValue
方法上面的@Oveerride
只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。这样就避免了在重写的时候我们还能调用到父类的方法所以,编译器使用synthetic bridge method 桥方法,来解决了类型擦除和多态的冲突。
这个时候我们发现 自己写的getValue
和桥方法getValue
类有点颠覆我们的常识了,难道一个类中允许出现方法签名相同的多个方法?
- 不能在同一个类中写两个方法签名(方法名+参数列表)相同的方法
- JVM 会用方法名、参数类型和返回类型来确定一个方法,所以针对方法签名相同的两个方法,返回值类型不相同的时候,JVM是能分辨的
7. 泛型之自限定类型
Java泛型中,有一个经常性出现的惯用写法
class SelfBounded<T extends SelfBounded<T>> {}
* SelfBounded类接受泛型参数T,而T由一个边界类限定,这个边界就是拥有T作为其参数的SelfBounded,看起来是一种无限循环。先给出结论:**这种语法定义了一个基类,这个基类能够使用子类作为其参数、返回类型、作用域**。为了理解这个含义,我们从一个简单的版本入手。
// BasicHolder.java
public class BasicHolder<T> {
T element;
void set(T arg) { element = arg; }
T get() { return element; }
void f() {
System.out.println(element.getClass().getSimpleName());
}
}
class Subtype extends BasicHolder<Subtype> {}
// CRGWithBasicHolder.java
public class CRGWithBasicHolder {
public static void main(String[] args) {
Subtype st1 = new Subtype(), st2 = new Subtype();
st1.set(st2);
Subtype st3 = st1.get();
st1.f();
}
}
/* 程序输出
Subtype
*/
/* class Subtype extends BasicHolder<Subtype> {}这样用,就构成自限定了。从定义上来说,它继承的父类的类型参数是它自己。从使用上来说,Subtype对象本身的类型是Subtype,且Subtype对象继承而来的成员(element)、方法的形参(set方法)、方法的返回值(get方法)也是Subtype了(这就是自限定的重要作用)。这样Subtype对象就只允许和Subtype对象(而不是别的类型的对象)交互了 */
- 新类Subtype接受的参数和返回的值具有Subtype类型而不仅仅是基类BasicHolder类型。所以自限定类型的本质就是:基类用子类代替其参数。这意味着泛型基类变成了一种其所有子类的公共功能模版,但是在所产生的类中将使用确切类型而不是基类型。
class SelfBounded<T extends SelfBounded<T>> {//自限定类型的标准用法
//...
T element;
SelfBounded<T> set(T arg) {
element = arg;
return this;
}
T get() { return element; }
}
class A extends SelfBounded<A> {}
public class SelfBounding {
public static void main(String[] args) {
A a = new A();//a变量只能与A类型变量交互,这就是自限定的妙处
SelfBounded<A> b = new SelfBounded<A>();
}
}
-
JDK源码里自限定的应用 – Enum
java中使用
enum
关键字来创建枚举类,实际创建出来的枚举类都继承了java.lang.Enum。也正因为这样,所以enum
不能再继承别的类了。其实enum
就是java的一个语法糖,编译器在背后帮我们继承了java.lang.Enum。public enum WeekDay { Mon("Monday"), Tue("Tuesday"), Wed("Wednesday"), Thu("Thursday"), Fri( "Friday"), Sat("Saturday"), Sun("Sunday"); private final String day; private WeekDay(String day) { this.day = day; } public static void printDay(int i){ switch(i){ case 1: System.out.println(WeekDay.Mon); break; case 2: System.out.println(WeekDay.Tue);break; case 3: System.out.println(WeekDay.Wed);break; case 4: System.out.println(WeekDay.Thu);break; case 5: System.out.println(WeekDay.Fri);break; case 6: System.out.println(WeekDay.Sat);break; case 7: System.out.println(WeekDay.Sun);break; default:System.out.println("wrong number!"); } } public String getDay() { return day; } public static void main(String[] args) { WeekDay a = WeekDay.Mon; } }
java.lang.Enum的定义(自限定标准写法)
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {}
反编译查看,发现确实WeekDay做到了自限定,因为继承来的成员和方法的类型都被限定成WeekDay它自己了。
// class version 52.0 (52)
// access flags 0x4031
// signature Ljava/lang/Enum<Lcom/generic3/WeekDay;>;
// declaration: com/generic3/WeekDay extends java.lang.Enum<com.generic3.WeekDay>
public final enum com/generic3/WeekDay extends java/lang/Enum {
// compiled from: WeekDay.java
// access flags 0x4019
public final static enum Lcom/generic3/WeekDay; Mon
// access flags 0x4019
public final static enum Lcom/generic3/WeekDay; Tue
// access flags 0x4019
public final static enum Lcom/generic3/WeekDay; Wed
// access flags 0x4019
public final static enum Lcom/generic3/WeekDay; Thu
// access flags 0x4019
public final static enum Lcom/generic3/WeekDay; Fri
// access flags 0x4019
public final static enum Lcom/generic3/WeekDay; Sat
// access flags 0x4019
public final static enum Lcom/generic3/WeekDay; Sun
// access flags 0x12
private final Ljava/lang/String; day
// access flags 0x101A
private final static synthetic [Lcom/generic3/WeekDay; $VALUES
// access flags 0x9
public static values()[Lcom/generic3/WeekDay;
...
分析一下java.lang.Enum
这么设计的好处:
-
Enum
作为一个抽象类,我们使用enum
关键字创建出来的枚举类实际都是Enum
的子类,因为class Enum<E extends Enum<E>>
的类定义是这种标准的自限定类型,所以编译器直接生成的类必须是WeekDay extends java.lang.Enum<WeekDay>
(即先创建一个符合边界条件的实际类型,但创建的同时又继承Enum
本身)。 - 正因为编译器生成的枚举类都是
Enum
的子类,结合上条分析,每种Enum
子类的自限定类型都是Enum
子类自身。这样WeekDay的实例就只能和WeekDay的实例交互(星期几和星期几比较),Month的实例就只能和Month的实例交互(月份和月份比较)。
8. 类型擦除后如何获得泛型参数
- 泛型擦除的时候,不会将
元数据
结构(类,属性,方法(结构)返回值及形参)泛型擦除,故可直接通过反射获取泛型类型。
public class Test<T> {
private T data;
private Set<String> set = new HashSet<>();
public <T> String isBoolean(List<Boolean> data) {
Map<String, String> map = new HashMap<>();
map.put("hello", "world");
map.put("你好", "世界");
System.out.println(map.get("hello"));
return "";
}
//查看反编译文件
public static void main(String[] args) throws NoSuchMethodException {
//获取Test.class类的class对象
Class<?> testClass = Test.class;
//获取类的属性字段
Field[] declaredField = testClass.getDeclaredFields();
//暴力解除,可以访问私有变量
Field.setAccessible(declaredField, true);
System.out.println("属性名:参数类型:参数泛型类型");
for (Field field : declaredField) {
String name = field.getName();
Class<?> type = field.getType();
Type genericType = field.getGenericType();
System.out.println(name + ":" + type + ":" + genericType);
}
System.out.println("方法形参的泛型类型");
Method method = testClass.getMethod("isBoolean", List.class);
ParameterizedType parameterType = (ParameterizedType) method.getGenericParameterTypes()[0];
System.out.println(parameterType.getActualTypeArguments()[0]); //获取第一个
System.out.println("方法返回值的泛型类型");
Class<?> returnType = method.getReturnType();
System.out.println(returnType);
// ParameterizedType returnType = (ParameterizedType) method.getGenericReturnType();
// System.out.println(returnType.getActualTypeArguments()[0]);
}
}
/*
属性名:参数类型:参数泛型类型
data:class java.lang.Object:T
set:interface java.util.Set:java.util.Set<java.lang.String>
方法形参的泛型类型
class java.lang.Boolean
方法返回值的泛型类型
class java.lang.String
*/
-
泛型类型只会在
类、字段以及方法形参
内保存其签名(Signature
),在方法实参
不作任何保留而统统擦除。文章来源:https://www.toymoban.com/news/detail-427079.html我们可以通过匿名类,以子类的方式把主类的
Signature
保存下来,从而获取到实参的泛型类型。文章来源地址https://www.toymoban.com/news/detail-427079.html
public class Demo<T> {
private T data;
public boolean isBoolean(List<T> data){
Class<? extends List> aClass = data.getClass();
//获取父类的泛型对象
Type genericSuperclass = aClass.getGenericSuperclass();
//输出 返回一个表示此类型的实际类型参数的数组 Type对象。
Type type = ((ParameterizedType) genericSuperclass).getActualTypeArguments()[0];
System.out.println(type);
return true;
}
public static void main(String[] args) {
Demo demo = new Demo();
List<String> list = new ArrayList<String>(){}; //使用匿名类
demo.isBoolean(list);
}
}
- 在
Google
的Gson
,阿里的FastJson
中,使用了比较多捕获泛型实参的方法,基本都是通过创建一个匿名类来获取的。FastJson
的com.alibaba.fastjson.TypeReference<T>
的源码:
protected TypeReference() {
Type superClass = this.getClass().getGenericSuperclass();
Type type = ((ParameterizedType)superClass).getActualTypeArguments()[0];
Type cachedType = (Type)classTypeCache.get(type);
if (cachedType == null) {
classTypeCache.putIfAbsent(type, type);
cachedType = (Type)classTypeCache.get(type);
}
this.type = cachedType;
}
到了这里,关于Java泛型<T>的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!