为什么会引入泛型?

1
2
为了同时纳入多个参数类型,通过泛型指定的不同类型来控制形参具体限制的类型
适用于多种数据类型执行相同的代码,参考下列例子
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static int add(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}

private static float add(float a, float b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}

private static double add(double a, double b) {
System.out.println(a + "+" + b + "=" + (a + b));
return a + b;
}

如果没有泛型,则每种类型都需要重载一个add方法,如果使用泛型,可以复用为一个方法:

1
2
3
4
private static <T extends Number> double add(T a, T b) {
System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
return a.doubleValue() + b.doubleValue();
}

2.泛型可以约束参数类型,提供编译检查

1
2
List<String> list = new ArrayList<String>();
//这时候list中只能添加Sting类型的参数

泛型的基本使用

1
泛型的使用范围有泛型类,泛型接口,泛型方法

泛型类

  • 简单泛型类例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Point<T>{
    private T var;
    public T getVar(){ // 返回值的类型由外部决定
    return var ;
    }
    public void setVar(T var){ // 设置的类型也由外部决定
    this.var = var ;
    }
    public class GenericsDemo06{
    public static void main(String args[]){
    Point<String> p = new Point<String>() ; // 里面的var类型为String类型
    p.setVar("it") ; // 设置字符串
    System.out.println(p.getVar().length()) ; // 取得字符串的长度
    }
    }
    //Point中的T属性可以设置为String类型,也可以设置为其他类型

泛型接口

  • 简单的泛型接口:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    interface Info<T>{        // 在接口上定义泛型  
    public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型
    }
    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 ;
    }
    }
    public class GenericsDemo24{
    public static void main(String arsg[]){
    Info<String> i = null; // 声明接口对象
    i = new InfoImpl<String>("汤姆") ; // 通过子类实例化对象
    System.out.println("内容:" + i.getVar()) ;
    }
    }
    //泛型接口中有个抽象方法,返回类型是泛型类型,通过子类实例化中set设置的属性,将属性类型set到属性中

    泛型方法

    • 泛型方法语法格式

      1
      2
      3
      4
      5
      6
      7
      public <T> T getObject(Class<T> c){
      T t = c.newInstance();
      return t;
      }
      //<T>用于标识此方法为一个泛型方法
      //T为返回类型是一个泛型
      //c代表泛型参数,因为不确定参数类型,用c来替代
      • 泛型方法的调用

        1
        2
        3
        4
        Generic generic = new Generic();
        Object obj = generic.getObject(Class.forName("com.cnblogs.test.User"));
        //调用过程中参数使用Class.forName,因为class.forName()返回的Class(T)类型
        //此过程中指定了参数为"com.cnblogs.test.User"类型,所以此时obj的类型为User

泛型的上下界

1
2
3
4
5
6
7
8
9
<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

// 使用原则《Effictive Java》
// 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限
1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。

泛型数组

泛型数组相关的申明:

1
2
3
4
5
6
List<String>[] list11 = new ArrayList<String>[10]; //编译错误,非法创建 
List<String>[] list12 = new ArrayList<?>[10]; //编译错误,需要强转类型
List<String>[] list13 = (List<String>[]) new ArrayList<?>[10]; //OK,但是会有警告
List<?>[] list14 = new ArrayList<String>[10]; //编译错误,非法创建
List<?>[] list15 = new ArrayList<?>[10]; //OK
List<String>[] list16 = new ArrayList[10]; //OK,但是会有警告

使用场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class GenericsDemo30{  
public static void main(String args[]){
Integer i[] = fun1(1,2,3,4,5,6) ; // 返回泛型数组
fun2(i) ;
}
public static <T> T[] fun1(T...arg){ // 接收可变参数
return arg ; // 返回泛型数组
}
public static <T> void fun2(T param[]){ // 输出
System.out.print("接收泛型数组:") ;
for(T t:param){
System.out.print(t + "、") ;
}
}
}

深入理解泛型

如何理解Java中的泛型是伪泛型?泛型中类型擦除

Java泛型的实现采取了‘伪泛型’的策略,即在语法上支持泛型,但在编译阶段会进行”类型擦除”,将所有的泛型表示替换为具体的类型(对应的原生态类型),就像完全没有泛型一样.

泛型的类型擦除原则:

  • 消除参数声明,级<>及其包围部分

  • 根据类型上下界推断并替换有所有类型的原生态类型,如果参数类型是无限制则替换为Object,如果存在上下界,根据子类替换原则取类型参数的最左边限定类型(父类)

  • 为了保证类型安全,在必要时加上强转代码

  • 自动产生’桥方法’以保证类型擦除后的代码仍具有泛型的多态性

如何进行类型擦除?

img

img

如何证明类型擦除?

通过两个例子来证明

  • 原始类型相等

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    public class Test {

    public static void main(String[] args) {

    ArrayList<String> list1 = new ArrayList<String>();
    list1.add("abc");

    ArrayList<Integer> list2 = new ArrayList<Integer>();
    list2.add(123);

    System.out.println(list1.getClass() == list2.getClass()); // true
    }
    }

    在此例子中,定义了两个ArraList数组,通过泛型限制参数为String,Integer.最后我们通过对list1和list2的getClass()方法获取类信息,最后发现结果为true,说明泛型类型被擦除了,只剩下原始类型

  • 通过反射,添加其他类型的元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class Test {

    public static void main(String[] args) throws Exception {

    ArrayList<Integer> list = new ArrayList<Integer>();

    list.add(1); //这样调用 add 方法只能存储整形,因为泛型类型的实例为 Integer

    list.getClass().getMethod("add", Object.class).invoke(list, "asd");

    for (int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
    }
    }
    }

    List限制泛型类型为Integer对象,直接调用add()方法,只能添加Integer类型.如果通过反射调用add()方法时,却可以添加字符串,说明Interger泛型在编译之后被擦除了,只留下原始类型

什么是原始类型?

原始类型就是类型擦除后替换成的类型,无边界就是Object,有边界<? extends X> 那么原始类型就是X. <? super X> 那么原始类型就是Object

如何理解泛型的编译期检查?

既然说类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量String会在编译的时候变为Object类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?

Java编译器是先通过检查代码中泛型的类型,然后在进行类型擦除,再进行编译.

例如:

1
2
3
4
5
6
public static  void main(String[] args) {  

ArrayList<String> list = new ArrayList<String>();
list.add("123");
list.add(123);//编译错误
}

在编写代码时,IDE会检查出类型错误.

类型检查针对的是谁?

1
2
ArrayList<String> list1 = new ArrayList(); //第一种 情况
ArrayList list2 = new ArrayList<String>(); //第二种 情况

第一种,有效果,而第二种则没有效果.

因为检查是在编译时完成的,new ArrayList()只是在内存中开辟出一个存储空间,可以存储任何类型对象,而真正涉及类型检查的是它的引用,因为我们是用list1来调用方法,比如add()方法,所以list1引用能完成泛型类型的检查.而引用list2没有使用泛型,所以不行

如何理解泛型的多态?泛型的桥接方法

类型擦除会造成多态的冲突,看个例子

一个泛型类:

1
2
3
4
5
6
7
8
9
10
11
12
class Pair<T> {  

private T value;

public T getValue() {
return value;
}

public void setValue(T value) {
this.value = value;
}
}

子类继承他:

1
2
3
4
5
6
7
8
9
10
11
12
class DateInter extends Pair<Date> {  

@Override
public void setValue(Date value) {
super.setValue(value);
}

@Override
public Date getValue() {
return super.getValue();
}
}

在子类中,将父类Pair<Date>泛型类型为Date,将父类的泛型类型限定为Date,那么get和set两个方法的参数都为Date类型.

而且@Override标签也没有问题,而实际上…

分析:实际上,类型擦除后,父类的泛型类型都变为了原始类型Object,所以父类编译后变为

1
2
3
4
5
6
7
8
9
10
11
class Pair {  
private Object value;

public Object getValue() {
return value;
}

public void setValue(Object value) {
this.value = value;
}
}

而这时,子类的方法参数还是Date类型,父类的类型是Object,参数类型都不一样,这如果是在普通的继承关系中,根本不是重写,是重载! 在main方法中测试:

1
2
3
4
5
public static void main(String[] args) throws ClassNotFoundException {  
DateInter dateInter = new DateInter();
dateInter.setValue(new Date());
dateInter.setValue(new Object()); //编译错误
}

确实是重写了.那么为什么会这样呢?

我们本意是将父类Pair泛型类型变为Date类型.由子类重写参数类型为Date的两个方法,实现继承中的多态.

由于类型擦除,Date会变为原始类型,我们本意是重写,实现多态,类型擦除后变为了重载.JVM知道你的本意,但他不能直接实现,但是它是如何做得呢?

桥方法

用javap -c className反编译DateInter子类的字节码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class com.tao.test.DateInter extends com.tao.test.Pair<java.util.Date> {  
com.tao.test.DateInter();
Code:
0: aload_0
1: invokespecial #8 // Method com/tao/test/Pair."<init>":()V
4: return

public void setValue(java.util.Date); //我们重写的setValue方法
Code:
0: aload_0
1: aload_1
2: invokespecial #16 // Method com/tao/test/Pair.setValue:(Ljava/lang/Object;)V
5: return

public java.util.Date getValue(); //我们重写的getValue方法
Code:
0: aload_0
1: invokespecial #23 // Method com/tao/test/Pair.getValue:()Ljava/lang/Object;
4: checkcast #26 // class java/util/Date
7: areturn

public java.lang.Object getValue(); //编译时由编译器生成的桥方法
Code:
0: aload_0
1: invokevirtual #28 // Method getValue:()Ljava/util/Date 去调用我们重写的getValue方法;
4: areturn

public void setValue(java.lang.Object); //编译时由编译器生成的桥方法
Code:
0: aload_0
1: aload_1
2: checkcast #26 // class java/util/Date
5: invokevirtual #30 // Method setValue:(Ljava/util/Date; 去调用我们重写的setValue方法)V
8: return
}

本意重写SetValue和GetValue方法,但是居然有4个方法,最后两个方法就是桥方法.桥方法的参数为Object,真正覆盖父类两个方法就是看不见的这两个桥方法.而桥方法的内部实现,就是去调用我们自己重写的那两个方法.虚拟机巧妙的使用了桥方法,来解决类型擦除和多态的冲突

setValue()方法是为了解决类型发出与多态之间的冲突,而getValue()具有普遍的意义.

另外一点:子类中的桥方法Object getValue()Date getValue()是同时存在的,如果是常规的两个方法,他们签名一样,如果我们自己编写Java代码,这样的代码无法通过编译器检查,但是虚拟机允许这样做,因为虚拟机通过参数类型和返回类型来确定一个方法,序偶已编译器为了实现泛型的多态允许自己做这个看起来’不合法’的事情,然后交给虚拟器识别,

如何理解基本类型不能作为泛型类型?

比如,我们没有ArraList 只有ArrayList

why?

因为当我们类型擦除后变为ArrayList,但是Object不能存储int值,只能引用Integer值.

如何理解泛型类型不能实例化?

我们可以看到如下代码会在编译器中报错:

1
T test = new T(); // ERROR

因为Java编译期没办法确定泛型参数化类型,找不到类字节码文件,自然就不行了,另外T被类型擦除为Object,如果可以New T()就变为了new Object,就是去了本意了.

如何理解泛型类中的静态方法和静态变量?

泛型类中的静态方法和静态变量不可以使用泛型类所声明的分析类型参数

因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用.对象都没创建,怎么确定泛型参数时什么类型,所以是错误的.

如何理解异常中使用泛型?

  • 不能抛出也不能补货泛型类的对象 泛型类扩展Throwable都不合法

    1
    public class Problem<T> extends Exception {}

    异常是在运行时捕获的,在编译的时候泛型信息全部都回被擦除掉,假设上面编译可行

    1
    2
    3
    try{} 
    catch(Problem<Integer> e1) {}
    catch(Problem<Number> e2) {}

    那么在类型擦除之后,catch都变为原始类型Object,两个catch的类型变为一样

    1
    2
    3
    try{} 
    catch(Problem<Object> e1) {}
    catch(Problem<Object> e2) {}

    这样显然不行

  • 不能在catch语句中使用使用泛型变量

    1
    2
    3
    4
    5
    6
    7
    8
    9
    public static <T extends Throwable> void doWork(Class<T> t){
    try {

    } catch(T e) { //编译错误

    } catch(IndexOutOfBounds e) {

    }
    }

    根据异常捕获原则,子类在前面,父类在后面.上面就违背了此原则.

如何获取泛型的参数类型?

既然类型被擦除了,那么如何获取泛型的参数类型?可以通过反射获取泛型

java.lang.reflect.Type是Java中所有类型的公共高级接口, 代表了Java中的所有类型. Type体系中类型的包括:数组类型(GenericArrayType)、参数化类型(ParameterizedType)、类型变量(TypeVariable)、通配符类型(WildcardType)、原始类型(Class)、基本类型(Class), 以上这些类型都实现Type接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class GenericType<T> {
private T data;

public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}

public static void main(String[] args) {
GenericType<String> genericType = new GenericType<String>() {};
Type superclass = genericType.getClass().getGenericSuperclass();
//getActualTypeArguments 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]
Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
System.out.println(type);//class java.lang.String
}
}

其中 ParameterizedType:

1
2
3
4
5
6
7
8
9
10
public interface ParameterizedType extends Type {
// 返回确切的泛型参数, 如Map<String, Integer>返回[String, Integer]
Type[] getActualTypeArguments();

//返回当前class或interface声明的类型, 如List<?>返回List
Type getRawType();

//返回所属类型. 如,当前类型为O<T>.I<S>, 则返回O<T>. 顶级类型将返回null
Type getOwnerType();
}
泛型机制总结
https://jisheng.xyz/2022/11/23/%E6%B3%9B%E5%9E%8B%E6%9C%BA%E5%88%B6%E6%80%BB%E7%BB%93/
作者
寂笙
发布于
2022-11-23
更新于
2023-05-24
许可协议
CC BY-NC-SA 4.0
打赏
  • wechat
    wechat
  • alipay
    alipay