java开发必备技能之Java泛型

news2024/11/16 7:26:23

简介

泛型的优点

1、泛型的本质是为了参数化类型,也就是在在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型,很明显这种方法提高了代码的复用性(比如List因为使用泛型可以添加任意类型的对象,而不需要针对每一种类型创建对应的一种List)

2、泛型的引入提高了安全性,泛型提供了编译时类型安全检测机制,该机制允许开发者在编译时检测到非法的类型。(感觉解释的有点勉强,这句话的意思是:比如List,在new 的时候我指定为String的,那么给list中添加元素时不是String类型的就不能添加,否则编译的时候会直接报异常,但是如果直接存在StringList,那不也一样。。。)

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
 * <p>
 * 功能描述:
 */
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<String> 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 <U> CompletableFuture<U> thenComposeAsync(
        Function<? super T, ? extends CompletionStage<U>> fn) {
        return uniComposeStage(asyncPool, fn);
    }

    public <U> CompletableFuture<U> thenComposeAsync(
        Function<? super T, ? extends CompletionStage<U>> fn,
        Executor executor) {
        return uniComposeStage(screenExecutor(executor), fn);
    }

    public CompletableFuture<T> whenComplete(
        BiConsumer<? super T, ? super Throwable> action) {
        return uniWhenCompleteStage(null, action);
    }

    public CompletableFuture<T> 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
 * <p>
 * 功能描述:
 */
public class Test_GenericClass {
    public static void main(String[] args) {
        test();
    }

    public static void test(){
        /**
         * 1、泛型的类型参数只能是类类型(包括自定义类),不能是简单数据类型(比如int,long这些)
         * 2、传入的实参类型需与泛型的类型参数类型相同,即为这里的Integer。
         * 3、new 后面的泛型参数可以省略
         */
        Generic<Integer> genericInteger1 = new Generic<Integer>(123);
        Generic<Integer> genericInteger = new Generic<>(123);

        Generic<String> genericString = new Generic<String>("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<T> {
    //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<K, V> {       // 此处指定了两个泛型类型
    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
  • 注意泛型类中,属性和方法使用泛型类的方式(参照Generic)
  • 定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

还是以上面的泛型类为例进行测试

 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<T>{        // 在接口上定义泛型  
    public T getVar() ; // 定义方法,方法的返回值就是泛型类型  
} 

当实现泛型接口的类,未传入泛型实参时:

/**
 * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中
 * 即:class InfoImpl<T> implements Info<T>
 * 如果不声明泛型,如:class InfoImpl implements Info<T>,编译器会报错:"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<T>
 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型
 * 即:InfoImpl01<T>,public String getVar();中的的T都要替换成传入的String类型。
 */
class InfoImpl01 implements Info<String> {   // 定义泛型接口的子类
    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中,泛型类和接口的定义非常简单,但是泛型方法就比较复杂了。

泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型。不要和泛型类混淆,即使都使用T,但意义是不一样的

 1、public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
 2、只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 3、<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 4、<T> 后面的这个T,代表这个方法的返回值类型
 5、与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。

最简单的一个泛型方法

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 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
     * 2、只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
     * 3、<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
     * 4、<T> 后面的这个T,代表这个方法的返回值类型
     * 4、与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
     */
    public <T> T genericMethod(T a) {

        return a;
    }
}

基本用法(非泛型类中的泛型方法)

下面来细说一下泛型方法

首先说一个误区

class Generic01<T> {
    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
 * <p>
 * 功能描述:
 */
public class Test_GenericMethod {

    public static void main(String[] args) {
        Test_GenericMethod test_genericMethod = new Test_GenericMethod();
        Generic01<Integer> generic01 = new Generic01<>(123);

        Generic01<String> generic02 = new Generic01<>("AAAAA");

        test_genericMethod.genericMethod_test01(generic01);
        test_genericMethod.genericMethod_test02(generic02, "我是T");

        test_genericMethod.Method01(generic01);
    }

    /**
     * 说明:
     * 1、public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。
     * 2、只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
     * 3、<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
     * 4、<T> 后面的这个T,代表这个方法的返回值类型
     * 4、与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
     */
     //这里特别注意,形参T a 中的大T是泛型方法中定义的T,而不是泛型类中定义的T
    public <T> T genericMethod(T a) {
        return a;
    }

    /**
     * 1、这才是一个真正的泛型方法。
     * 2、首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T。
     * 3、这个T可以出现在这个泛型方法的任意位置.泛型的数量也可以为任意多个
     *   这里注意一点就是Generic01<T> 中的T 和  <T> T 后面的T是同一个
     */
    public <T> T genericMethod_test01(Generic01<T> generic01) {
        System.out.println("我是genericMethod_test01:" + generic01.getKey());
        T test = generic01.getKey();
        return test;
    }
    /**
    * <T,V> 定义这个方法为泛型方法,并且可以使用两个泛型类型 T和V
    */
    public <T, V> T genericMethod_test02(Generic01<T> generic01, V value) {
        System.out.println("我是genericMethod_test02:" + generic01.getKey() + "==> value:" + value);

        T test = generic01.getKey();
        return test;
    }


    //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic<Number>这个泛型类做形参而已。
    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>,也表明了这是一个可以处理泛型的类型的泛型方法。
     * 但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。
     */
//    public <T> T showKeyName(Generic01<E> 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
 * <p>
 * 功能描述:
 */
public class Test_GenericMethod01 {
    public static void main(String[] args) {
        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest<Fruit> generateTest = new GenerateTest<Fruit>();
        //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<T> {
    
    public void show_1(T t) {
        System.out.println(t.toString());
    }


    /**
     * 1、在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
     * 2、由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别
     泛型方法中识别的泛型。
     */
    public <E> void show_3(E t) {
        System.out.println(t.toString());
    }


    /**
     * 1、在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T
     * 不是同一种类型。也就是说main函数中使用的时候也可以是不一样的泛型类型
     */
    public <T> void show_2(T t) {
        System.out.println(t.toString());
    }
}

这里只要注意记住一点:就是定义了泛型方法之后,形参T就是和泛型方法中的T一致,不要和泛型类中的混淆
结果:
在这里插入图片描述

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 <T> 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<T> extends Exception{
}

2、

package keyAndDifficultPoints.Generic.Minutiae;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: youthlql-吕
 * @Date: 2020/10/15 22:28
 * <p>
 * 功能描述:
 */
public class Test_Minutiae1 {
}
class Order<T> {

    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 <E> List<E> copyFromArrayToList(E[] arr){

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

        for(E e : arr){
            list.add(e);
        }
        return list;

    }
}
class SubOrder extends Order<Integer> {//SubOrder:不是泛型类


    public static <E> List<E> copyFromArrayToList(E[] arr) {

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

        for (E e : arr) {
            list.add(e);
        }
        return list;

    }

}

class SubOrder1<T> extends Order<T> {//SubOrder1<T>:仍然是泛型类
}

泛型数组 (可了解)

package keyAndDifficultPoints.Generic;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: youthlql-吕
 * @Date: 2020/10/15 12:10
 * <p>
 * 功能描述: 测试泛型数组
 */
public class Test_GenericArray {

    public static void main(String[] args) {
        test02();
    }

    public static void test() {
        //编译错误
//        List<String>[] ls = new ArrayList<String>[10];
    }


    public static void test01() {
        //这样声明是正确的
        List<?>[] ls = new ArrayList<?>[10];
        ls[1] = new ArrayList<String>();

        //这样写编译就报错了
//        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<Integer> li = new ArrayList<Integer>();
        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<String>[] ls = new ArrayList[10];
        ls[0] = new ArrayList<String>();
        ls[1] = new ArrayList<String>();

        ls[0].add("x");

    }

}

sun文档

泛型在继承方面的细节

虽然类A是类B的父类,但是G 和G二者不具备子父类关系,二者是并列关系。下面是例子
在这里插入图片描述

直接看代码注释

 /*
    1. 泛型在继承方面的体现

      虽然类A是类B的父类,但是G<A> 和G<B>二者不具备子父类关系,二者是并列关系。

       补充:类A是类B的父类,A<G> 是 B<G> 的父类

     */
    @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,? 是这样约定的:

  • ? 表示不确定的 java 类型
  • T (Type) 表示具体的一个java类型
  • K V (Key Value) 分别代表java键值中的Key Value
  • E (element) 代表Element

比较难的就是通配符,下面就着重讲一下

’ ? '无界通配符

基本用法

List<Animal> listAnimals

但是如果用通配符的话:

List<? extends Animal> listAnimals

为什么要使用通配符而不是简单的泛型呢?通配符其实在声明局部变量时是没有什么意义的,但是当你为一个方法声明一个参数时,它是非常重要的。

package keyAndDifficultPoints.Generic;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: youthlql-吕
 * @Date: 2020/10/15 21:25
 * <p>
 * 功能描述: 泛型通配符测试
 */
public class Test_Wildcard_Character {

    public static void main(String[] args) {
        List<Dog> 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<Animal> 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<A>和G<B>是没有关系的,二者共同的父类是:G<?>
     */

    @Test
    public void test3() {
        List<Object> list1 = null;
        List<String> list2 = null;

        List<?> list = null;

        list = list1;
        list = list2;
        //编译通过
//        print(list1);
//        print(list2);


        //
        List<String> list3 = new ArrayList<>();
        list3.add("AA");
        list3.add("BB");
        list3.add("CC");
        list = list3;
        //添加(写入):对于List<?>就不能向其内部添加数据。
        //除了添加null之外。
//        list.add("DD");
//        list.add('?');

        list.add(null);

        //获取(读取):允许读取数据,读取的数据类型为Object。
        Object o = list.get(0);
        System.out.println(o);

    }

extends和super上下界

上界通配符 < ? extends E>

上结:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类。

在类型参数中使用 extends 表示这个泛型中的参数必须是 E 或者 E 的子类,这样有两个好处:

  • 如果传入的类型不是 E 或者 E 的子类,编译不成功
  • 泛型中可以使用 E 的方法,要不然还得强转成 E 才能使用

下界通配符 < ? super E>

下界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object

在类型参数中使用 super 表示这个泛型中的参数必须是 E 或者 E 的父类。

举例

 /*
    3.有限制条件的通配符的使用。
        ? extends A:
                G<? extends A> 可以作为G<A>和G<B>的父类,其中B是A的子类

        ? super A:
                G<? super A> 可以作为G<A>和G<B>的父类,其中B是A的父类

     */
    @Test
    public void test4() {

        List<? extends Person> list1 = null; //[-无穷,Person]
        List<? super Person> list2 = null;  //[Person,+无穷]

        List<Student> list3 = new ArrayList<Student>();
        List<Person> list4 = new ArrayList<Person>();
        List<Object> list5 = new ArrayList<Object>();

        list1 = list3;
        list1 = list4;
//        list1 = list5;

//        list2 = list3;
        list2 = list4;
        list2 = list5;

        
        //下面的东西很奇怪

        //读取数据:
        list1 = list3;
        Person p = list1.get(0);
        //编译不通过
        //Student s = list1.get(0);

        list2 = list4;
        Object obj = list2.get(0);
        编译不通过
//        Person obj = list2.get(0);

        //写入数据:
        //编译不通过
//        list1.add(new Student());

        //编译通过
        list2.add(new Person());
        list2.add(new Student());

    }
}

class Person {
}

class Student extends Person {
}

##? 和 T 的区别

?和 T 都表示不确定的类型,区别在于我们可以对 T 进行操作,但是对 ? 不行,比如如下这种 :

// 可以
T t = operate();

// 不可以
? car = operate();

简单总结下:

T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义,?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。

区别1:通过T来确保泛型参数的一致性

package keyAndDifficultPoints.Wildcard_Character;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: youthlql-吕
 * @Date: 2020/10/16 11:28
 * <p>
 * 功能描述:
 */
public class Test_difference {

    public static void main(String[] args) {
        List<Integer> integerList = new ArrayList<>();
        List<Float> floatList = new ArrayList<>();

        //编译报错
//        test(integerList, floatList);
        //编译通过
        test1(integerList, floatList);


        //编译通过
        test(integerList, integerList);
        test1(integerList, integerList);

    }

    // 通过 T 来 确保 泛型参数的一致性
    public static <T extends Number> void test(List<T> dest, List<T> src){

    }

    //通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型
    public static void test1(List<? extends Number> dest, List<? extends Number> src){

    }
}

区别2:T可以通过&进行多重限定

public class Test_difference {

    public static void main(String[] args) {


        /*---------------------测试多重限定符---------------------*/
        ArrayList list = new ArrayList<>();
        ArrayDeque deque = new ArrayDeque<>();
        LinkedList<Object> linkedList = new LinkedList<>();

        //多重限定时,在编译的时候取最小范围或共同子类

        test2(list);
//        test3(list); 编译报错

        //编译报错
//        test2(deque);
//        test3(deque);

        //编译通过
        test2(linkedList);
        test3(linkedList);


    }


    //可以进行多重限定
    public static <T extends List & Collection> void test2(T t) {

    }

    //可以进行多重限定
    public static <T extends Queue & List> void test3(T t) {

    }

    //编译报错,无法进行多重限定
//    public static <? extends List & Collection> void test4(List<T> dest, List<T> src){
//
//    }

}

区别3:?通配符可以使用超类限定而T不行

类型参数 T 只具有 一种 类型限定方式:

T extends A

但是通配符 ? 可以进行 两种限定:

? extends A
? super A

关于反射和泛型的一点东西

package keyAndDifficultPoints.Wildcard_Character;

/**
 * @Author: youthlql-吕
 * @Date: 2020/10/16 12:09
 * <p>
 * 功能描述: 泛型反射
 */
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> T createInstance(Class<T> 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<T> clazz2;
}

class D<T> {
    public Class<?> clazz;
    // 不会报错
    public Class<T> clazzT;
}

以下内容,如有时间可以了解一下

泛型原理(泛型擦除)

类型擦除简介

​ Java的泛型是伪泛型,为什么说Java的泛型是伪泛型呢?因为在编译期间,所有的泛型信息都会被擦除掉,我们常称为泛型擦除

​ Java中的泛型基本上都是在编译器这个层次来实现的,在生成的Java字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,编译器在编译的时候去掉,这个过程就称为类型擦除。

​ 如在代码中定义的List<object>List<String>等类型,在编译后都会编程List,JVM看到的只是List。而由泛型附加的类型信息对JVM来说是不可见的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现类型转换异常的情况。类型擦除也是Java的泛型实现方法与C++模版机制实现方式之间的重要区别。

可以通过两个例子,来证明java泛型的类型擦除。

例1:

   @Test
    public void test() {
        List<String> stringList = new ArrayList<String>();
        stringList.add("my");
        List<Integer> integerList = new ArrayList<Integer>();
        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<Integer> list = new ArrayList<Integer>();
        //这样调用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
 * <p>
 * 功能描述:
 */
public class Test_principle02 {
    public static void main(String[] args) {
    }
}
class Test_Generic<T> {
    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 <T:Ljava/lang/Object;>Ljava/lang/Object;
// declaration: keyAndDifficultPoints/principle/Test_Generic<T>
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
  <init>()V
   L0
    LINENUMBER 13 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()V
    RETURN
   L1
    LOCALVARIABLE this LkeyAndDifficultPoints/principle/Test_Generic; L0 L1 0
    // signature LkeyAndDifficultPoints/principle/Test_Generic<TT;>;
    // declaration: keyAndDifficultPoints.principle.Test_Generic<T>
    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<TT;>;
    // declaration: keyAndDifficultPoints.principle.Test_Generic<T>
    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<TT;>;
    // declaration: keyAndDifficultPoints.principle.Test_Generic<T>
    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<T extends List & Collection>

我们还是看字节码(后面如无必须,只截取部分字节码)

// class version 52.0 (52)
// access flags 0x20
// signature <T::Ljava/util/List;:Ljava/util/Collection;>Ljava/lang/Object;
// declaration: keyAndDifficultPoints/principle/Test_Generic1<T extends java.util.List, java.util.Collection>
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 <T::Ljava/util/Collection;:Ljava/util/List;>Ljava/lang/Object;
// declaration: keyAndDifficultPoints/principle/Test_Generic1<T extends java.util.Collection, java.util.List>
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
 * <p>
 * 功能描述:
 */
public class Test_principle04 {

    public static void main(String[] args) {


        /*---------------------测试多重限定符---------------------*/
        List list = new ArrayList<>();
        Queue queue = new ArrayDeque<>();
        LinkedList<Object> linkedList = new LinkedList<>();

		 //多重限定时,在编译的时候取最小范围或共同子类
        test2(list);
//        test3(list); 编译报错
        test4(list);


        //编译报错
//        test2(deque);
//        test3(deque);
//        test4(queue);

        //编译通过
        test2(linkedList);
        test3(linkedList);
        test4(linkedList);


    }


    //可以进行多重限定
    public static <T extends List & Collection> void test2(T t) {

    }

    //可以进行多重限定
    public static <T extends Queue & List> void test3(T t) {

    }

    //可以进行多重限定
    public static <T extends Collection & List> void test4(T t) {

    }

    //编译报错,无法进行多重限定
//    public static <? extends List & Collection> void test4(List<T> dest, List<T> src){
//
//    }

}

首先来看一下字节码

// class version 52.0 (52)
// access flags 0x21
public class keyAndDifficultPoints/principle/Test_principle04 {

  // compiled from: Test_principle04.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 11 L0
    ALOAD 0
    INVOKESPECIAL java/lang/Object.<init> ()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.<init> ()V
    ASTORE 1
   L1
    LINENUMBER 18 L1
    NEW java/util/ArrayDeque
    DUP
    INVOKESPECIAL java/util/ArrayDeque.<init> ()V
    ASTORE 2
   L2
    LINENUMBER 19 L2
    NEW java/util/LinkedList
    DUP
    INVOKESPECIAL java/util/LinkedList.<init> ()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<Ljava/lang/Object;>;
    // declaration: java.util.LinkedList<java.lang.Object>
    MAXSTACK = 2
    MAXLOCALS = 4

  // access flags 0x9
  // signature <T::Ljava/util/List;:Ljava/util/Collection;>(TT;)V
  // declaration: void test2<T extends java.util.List, java.util.Collection>(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 <T::Ljava/util/Queue;:Ljava/util/List;>(TT;)V
  // declaration: void test3<T extends java.util.Queue, java.util.List>(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 <T::Ljava/util/Collection;:Ljava/util/List;>(TT;)V
  // declaration: void test4<T extends java.util.Collection, java.util.List>(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.<Integer>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<String> arrayList=new ArrayList<String>();
		arrayList.add("123");
		arrayList.add(123);//编译错误
	}

类型擦除后,原始类型为Object,是应该运行任意引用类型的添加的。可实际上却不是这样,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。

那么,这么类型检查是针对谁的呢?我们来看例子:

public static void main(String[] args) {
        ArrayList<String> arrayList = new ArrayList<String>();
        arrayList.add(1); //编译报错

        ArrayList<String> arrayList1 = new ArrayList(); //第一种 情况
        arrayList1.add(1); //编译报错

        ArrayList arrayList2 = new ArrayList<String>();//第二种 情况
        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<T> {
    //key这个成员变量的类型为T,T的类型由外部指定
    private T var;

    public T getVar() {
        return var;
    }

    public void setVar(T 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);
    }
}

实际上,从他们的@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<Integer>{
    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<Ljava/lang/Integer;>;
// declaration: keyAndDifficultPoints/principle/MyGeneric extends keyAndDifficultPoints.principle.Generic<java.lang.Integer>
public class keyAndDifficultPoints/principle/MyGeneric extends keyAndDifficultPoints/principle/Generic  {

  // compiled from: MyGeneric.java

  // access flags 0x1
  public <init>()V
   L0
    LINENUMBER 9 L0
    ALOAD 0
    INVOKESPECIAL keyAndDifficultPoints/principle/Generic.<init> ()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只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。

所以,虚拟机巧妙的使用了巧方法,来解决了类型擦除和多态的冲突。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/707842.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《PyTorch深度学习实践》第八讲 加载数据集

b站刘二大人《PyTorch深度学习实践》课程第八讲加载数据集笔记与代码&#xff1a;https://www.bilibili.com/video/BV1Y7411d7Ys?p8&vd_sourceb17f113d28933824d753a0915d5e3a90 Dataset用于构造数据集&#xff0c;该数据集能够支持索引 DataLoader用于从数据集中拿出一个…

深入理解计算机系统(3)_计算机指令

深入理解计算机系统系列文章目录 第一章 计算机的基本组成 1. 内容概述 2. 计算机基本组成 第二章 计算机的指令和运算 3. 计算机指令 4. 程序的机器级表示 5. 计算机运算 6. 信息表示与处理 第三章 处理器设计 7. CPU 8. 其他处理器 第四章 存储器和IO系统 9. 存储器的层次…

金融基础知识(三):期权

1.认购期权与认沽期权 认购期权和认沽期权都是交易所常见的期权合约。 认购期权&#xff08;Call Option&#xff09;是一种给予持有人以在未来某个时间或特定事件发生时购买底层标的资产的权利。认购期权的持有人在行权日&#xff08;Expiration Date&#xff09;可以按照期…

B/S架构的C#云检验系统源码 实验室信息管理系统源码

科技的飞速发展为实验室信息管理带来了新机遇&#xff0c;云计算技术的应用更是为实验室信息管理打开了新的大门。云 LIS 实验室信息管理系统&#xff0c;作为一种新型的信息化管理方案&#xff0c;已经在多个实验室的信息化管理中得到应用&#xff0c;并且具有广阔的应用前景。…

Python3 命名空间和作用域 | 菜鸟教程(十七)

目录 一、命名空间 &#xff08;一&#xff09;简介 1、命名空间(Namespace)是从名称到对象的映射&#xff0c;大部分的命名空间都是通过 Python 字典来实现的。 2、命名空间提供了在项目中避免名字冲突的一种方法。 3、各个命名空间是独立的&#xff0c;没有任何关系的&a…

访问者模式(Vistor)

定义 访问者是一种行为设计模式&#xff0c;它能将算法与其所作用的对象隔离开来。 前言 1. 问题 假如你的团队开发了一款能够使用巨型图像中地理信息的应用程序。图像中的每个节点既能代表复杂实体&#xff08;例如一座城市&#xff09;&#xff0c; 也能代表更精细的对象…

Nginx【Docker(安装Nginx、Nginx服务启停控制、全局块、events块、HTTP块)】(二)-全面详解(学习总结---从入门到深化)

目录 Docker安装Nginx Nginx服务启停控制 Nginx配置指令详解_全局块 Nginx配置指令详解_events块 Nginx配置指令详解_HTTP块 Docker安装Nginx 拉取官方的Nginx镜像 [rootlocalhost ~]# docker pull nginx 以下命令使用 Nginx 默认的配置来启动一个 Nginx 容器实例&#xf…

小驰私房菜_28_Qcom Camx相关名词

(Qcom 7325平台) CSID = Camera Serial Interface Decoder module IPE = Image Processing Engine IFE (x3) = Image Front End IFE_lite (x2) BPS = Bayer processing segment (for Snapshot) IPE = Image Processing Engine VPU = Video Processing Unit (CODEC) DP…

matplotlib布局模式

栅格布局 import matplotlib.pyplot as plt import numpy as np plt.figure("OBJ")x np.linspace(-np.pi, np.pi, 1000) cosy np.cos(x) siny np.sin(x) y x * 0.5 timesy x ** 2 # 创建九宫格 gs plt.GridSpec(3, 3) # 第0-1行&#xff0c;第2列 plt.subplot…

Eclipse中有用的快捷键

Eclipse中有的快捷键自己记不清楚&#xff0c;但用起来又很方便&#xff0c;遇到了就放在这边备忘。 【CtrlO】快速定位某个类中的属性、方法 有时候&#xff0c;一个类中的属性、方法比较多&#xff0c;想用快捷键快速查找&#xff0c;提升效率。 举例&#xff1a;我想查找…

MYSQL-聚合函数及分组查询

常用聚合函数 COUNT() 求有多少行 SUM() 求和 AVG() 求平均值 MIN() 求最小值 MAX() 求最大值 举个栗子 SELECT AVG(price) FROM products WHERE price_id > 10; 这行代码就是在求id大于10的价格的平均值 AVG(price)表示求price列的平均值 执行逻辑为 先由WHERE…

Mock在接口测试中的实际应用

关于Mock测试 01、含义和目的 1、 什么是mock测试&#xff1f; Mock 测试就是在测试过程中&#xff0c;对于某些不容易构造&#xff08;如 HttpServletRequest 必须在Servlet 容器中才能构造出来&#xff09;或者不容易获取的比较复杂的对象&#xff08;如 JDBC 中的ResultSe…

chatgpt赋能python:下载完Python,如何进入编辑器

下载完Python&#xff0c;如何进入编辑器 Python是一门高级编程语言&#xff0c;具有简单易懂、易于学习、可拓展性强等特点&#xff0c;被广泛应用于Web应用、桌面应用、科学计算、人工智能等众多领域。如果你已经下载并安装了Python&#xff0c;那么接下来如何进入编辑器呢&…

uniapp智慧停车场系统微信小程序h5、APP源码 智能停车系统源码 安装搭建部署教程

【APP】: flutter(原生混合框架&#xff0c;不是web封装&#xff0c;原生应用&#xff0c;一套代码直接生成原生Android和ios应用)&#xff0c;既不损失性能&#xff0c;也能降低开发成本 【小程序/h5/公众号】&#xff1a;uni-app(底层框架Vue) 【后台管理】&#xff1a;vue-e…

DeepSpeed-Chat 打造类ChatGPT全流程 笔记一

这篇文章主要是对DeepSpeed Chat的功能做了一些了解&#xff0c;然后翻译了几个主要的教程了解了一些使用细节。最后在手动复现opt-13b做actor模型&#xff0c;opt-350m做reward模型进行的chatbot全流程训练时&#xff0c;踩了一些坑也分享出来了。最后使用训练后的模型做servi…

计算机组成原理(课堂测验3次)

3、同步通信与异步通信的主要区别是什么&#xff0c;说明通信双方如何联络。 同步通信和异步通信的主要区别是&#xff1a;前者有公共时钟线&#xff0c;所有设备按统一的时序、同一的传输周期进行信息传输&#xff0c;通信双方按约定好的时序联络&#xff1b;后者没有公共时钟…

探秘直链网盘:高效传输、便捷分享的存储利器!

什么是直链网盘&#xff1f; 直链网盘是一种用于存储和共享文件的在线服务。它为用户提供了一个方便的方式来存储和访问他们的文件&#xff0c;而无需依赖本地存储设备。直链网盘的主要特点是它们可以生成直接下载链接&#xff0c;允许用户快速下载文件&#xff0c;而不需要进…

使用 Sigstore 签名的 Elastic Stack 容器镜像!

作者&#xff1a;Maxime Greau 软件供应链攻击不断增加。 这就是为什么这个主题是安全领导者的首要任务。 在这方面&#xff0c;这篇博文重点介绍了使用 Sigstore 对 Elastic Stack 容器镜像进行签名的新功能&#xff0c;以便&#xff1a; 保护 Elastic 软件供应链工作流程为…

java面试Day14

1.如何使用 Redis 实现一个排行榜&#xff1f; Redis实现排行榜是Redis中一个很常见的场景&#xff0c;主要使用的是ZSet进行实现&#xff0c;下面是为什么选用ZSet&#xff1a; 有序性&#xff1a;排行榜肯定需要实现一个排序的功能&#xff0c;在Redis中有序的数据结构有List…

Tauri:跨平台探索之旅

一、简介 Tauri 是一个跨平台 GUI 框架&#xff0c;与 Electron 的思想基本类似。都是属于跨平台技术的解决方案 优缺点快速分析 我们一般会把tauri作为 Electron 的替代方案&#xff0c;electron优点咱们不看&#xff0c;这里就提两个electron比较明显的问题&#xff1a; 安装…