泛型(Generic)总结

泛型(Generic)总结

一、泛型的引入

泛型这个概念的出现,根本目的是解决在“通用方法”中使用“通用类型”的问题。

泛型的本质是参数类型化,也就是将数据类型也指定为一个参数。

1-1、使用Object

如果需要一个存储不同的对象的列表类,必须要应对所有类型的对象,不使用泛型的话,最好的解决办法就是使用Object类型(因为天地万物继承自Object)。

public class GenericDemo {

    private static final Logger LOGGER = LogManager.getLogger(GenericDemo.class);

    private GenericDemo(int n) {
        this.arr = new Object[n];
    }

    private Object[] arr;

    public void set(int i, Object o) {
        this.arr[i] = o;
    }

    public Object get(int i) {
        return this.arr[i];
    }

    public static void main(String[] args) {
        GenericDemo arr = new GenericDemo(3);
        arr.set(0, "泛型");
        // 必须强制类型转换
        String n = (String) arr.get(0);
        LOGGER.info(n);
    }

}

这种做法的缺点是必须做强制类型转换(cast),这种转换要求开发者对实际参数类型可以预知。

而编译器无法对强制类型转换错误进行提示,这种错误必须在运行时才显现出来,属于安全隐患。

1-2、使用泛型

如果使用泛型的话,好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,可以提高代码的重用率。

以下使用泛型来重写:

public class GenericDemo<T> {

    private static final Logger LOGGER = LogManager.getLogger(GenericDemo.class);

    private GenericDemo(int n) {
        this.arr = new Object[n];
    }

    private Object[] arr;

    public void set(int i, T o) {
        this.arr[i] = o;
    }

    @SuppressWarnings("unchecked")
    public T get(int i) {
        return (T) this.arr[i];
    }

    public static void main(String[] args) {
        GenericDemo<String> arr = new GenericDemo<>(3);
        arr.set(0, "泛型");
        String n = arr.get(0);
        LOGGER.info(n);
    }

}

实际上我们每天都会写的

Map<String> map = new HashMap<>();

就是JDK提供的容器类的泛型方面的应用。

(在get方法内部仍然存在强制类类型转换,实际上HashMap、ArrayList内部也同样存在强制类型转换。但是泛型把问题封装了起来,对于使用者来说完全不用关心强制类型转换引发的问题)

1-3、小结

在需要编写可以应用于多种类型的代码时,可以使用泛型编写出更“泛化”的代码,这些代码对于它们能够作用的类型具有更少的限制。

在上例中,泛型起到的作用(好处):

  1. 规范、简化代码: 不用在每次get操作时候都要做强制类型转换
  2. 良好的可读性:GenericDemo arr 声明能明确 GenericDemo 中存储的数据类型
  3. 安全性(类型安全):使用了泛型机制后,编译器能在set操作中检测传入的参数是否为T类型,同时检测get操作中返回值是否为T类型,将错误在编译阶段解决掉

泛型对于java做出的做大贡献,就是对于容器类的改进。我们现在常用的容器类,Map、List甚至包括Collection、Iterable这些容器最基础的接口都使用了泛型。

同时注意,泛型只是一个超级好用的语法糖,并不是万能的,只是解决使用 Object 类型时笨拙、安全性差的问题。

二、泛型基础知识

泛型分为泛型类、泛型接口和泛型方法

2-1、泛型类

例子中的GenericDemo就是一个泛型类,将表示类型参数的符号放在类名后面,即可在全类中使用该类型:

public class GenericDemo<T>

2-2、泛型接口

public interface GenericInterface<T>{

对接口的实现:

public class Generic implements GenericInterface<String>{

平时碰到的最常见的泛型接口,应该是用于比较的Comparator(同时也是一个函数式接口)

public interface Comparator<T> {

2-3、泛型方法

可以对一个方法单独进行泛型处理:

public <T> T genericMethod(T v){
或者
public <U,V> U genericMethod(V v){
  • 第二行中 U、V 分别代表方法的返回类型和参数类型,因为有两种,所以使用 <U,V> 进行说明
  • 对于泛型类,如果需要使用多种参数的话,可以写成: public class GenericDemo<U,V>
  • 泛型方法可以定义在泛型类当中,也可以定义在一个普通类当中

2-4、泛型符号的使用习惯

常使用E表示集合的元素类型, K和V分别表示关键字和值的类型, T(以及U,S等)表示任意类型

2-5、类型变量的限定

有时候,需要对泛型的类型进行一定的限制,比如它必须是某个父类的子类,或者必须实现了某个接口。

2-5-1、extends
  1. 对于要求实现接口, 或者继承自某个父类, 统一使用extends关键字 (不使用implements关键字)

  2. 限定类型之间用 “&” 分隔

  3. 如果限定类型既有超类也有接口,则:父类限定名必须放在前面,且至多只能有一个(接口可以有多个)。这个书写规范和类的继承和接口的实的规则是一致的(不允许类多继承,但允许接口多继承 ; 书写类的时候类的继承是写在接口实现前面的)

使用例(T必须是SuperClass的子类,且实现了Comparable接口):

public class Foo<T extends SuperClass&Comparable> {
}
2-5-2、super

要求 T 必须是某类型的父类(或者是父类的父类)

2-5-3、限定类型的好处

一个好处是:限定类型以后,我们可以确定泛型类型必然具有父类中的共通方法。就好像我们可以放心地对所有类型使用 .toSring() 方法,因为这是 Object 类中的方法。

这样就可以对 T类型 使用其父类中存在的方法,只是需要使用反射来实现(具体方法参看第四章)。

三、类型参数与无界通配符<?>

3-1、区别使用

首先要区分开两种不同的场景:

  • 声明一个泛型类或泛型方法。这种情况下要使用类型参数“”
  • 使用泛型类或泛型方法。这种情况下要使用无界通配符“<?>”。具体解释,就是对已经存在的泛型,我们不想给她一个具体的类型做为类型参数,我们可以给她一个不确定的类型作为参数,(前提是这个泛型必须已经定义)

的作用是保证所有出现T的地方,指代的都是同一种类型,是一种约束。

<?>的作用是保证能在容器类里面放入各种不同类型的元素(顺便说,?的英文是wildcard:通配符)。通配符?是不能用来声明泛型的。以下声明直接报错:
public class GenericDemo<?>  // 报错
<?>的使用例:用<?>声明List容器的变量类型,然后用一个实例对象给它赋值。
List<?> list = new ArrayList<String>();

或者

public static void showObj(List<?> list) {
    for (Object object : list) {
        System.out.println(object);
    }
}
小结(伪代码)
List<T> aa = new ArrayList<T>();  // 正确
List<?> aa = new ArrayList<T>();  // 正确
List<?> aa = new ArrayList<?>();  // 报错,ArrayList里面必须是一种确定类型
List<Object> aa = new ArrayList<Object>();  // 正确
List<?> aa = new ArrayList<Object>();  // 正确

注意 <?> 和 是完全不同的概念。List<?> 表示未知类型的列表,而 List 表示任意类型的列表——不管是哪一种,都算是已知的。

3-1-附、List 和 List 之间的区别?

List是原始类型。List使用了泛型。

  • 编译时编译器不会对原始类型进行类型安全检查,但是会检查泛型
  • 可以把任何带参数的类型传递给原始类型 List,但却不能把 List< String> 传递给接受 List< Object> 的方法,因为泛型的不可变性,会产生编译错误。(泛型的不可变性参看 6-4 )

3-2、List<?>(以及其他容器类)的坑

使用List<?>这个写法时,通配符会捕获具体的String类型,但编译器不叫它String,而是起个临时的代号,比如”CAP#1“。

以后再也不能往list里存任何元素,包括String。唯一能存的就是null。

在Java集合框架中,对于参数值是未知类型的容器类,只能读取其中元素,不能向其中添加元素。

因为,其类型是未知,所以编译器无法识别添加元素的类型和容器的类型是否兼容,而null不涉及类型问题,所以是唯一的例外

List<?> list = new ArrayList<String>();

list.add("hello");    //ERROR
list.add(111);    //ERROR

//argument mismatch; String cannot be converted to CAP#1
//argument mismatch; int cannot be converted to CAP#1

另外如果拿List<?>做参数的话

class Box<T>{
    private List<T> item;
    public List<T> get(){return item;}
    public void set(List<T> t){item=t;}
    // 只是把item先用get()方法读出来,然后再用set()方法存回去
    public void getSet(Box<?> box){box.set(box.get());}
}

会报错:

error: incompatible types: Object cannot be converted to CAP#1

原因同样是通配符box<?>.set()的参数类型被编译器捕获,命名为CAP#1,和box<?>.get()返回的Object对象无法匹配。

解决方法,是给getSet()方法写一个辅助函数:

class Box<T>{
    private List<T> item;
    public List<T> get(){return item;}
    public void set(List<T> t){item=t;}
    //helper()函数辅助getSet()方法存取元素
    public void getSet(Box<?> box){helper(box);}
    public <V> void helper(Box<V> box){box.set(box.get());}
}

3-3、无界通配符的类型限定

在一些情况下,<?>需要配合extends或者super来使用。

在《Effective Java》中有如下的总结:

image

更通俗地总结一下,就是:

  • <? super E> 用于灵活写入,主要目的是统一使用父类的容器,使得对象可以写入父类型的容器。或者用于比较,使得父类型的比较方法可以应用于子类对象。
  • <? extends E> 用于灵活读取,使得方法可以读取 E 或 E 的任意子类型的容器对象。
  • 如果既是生产又是消费,那使用通配符就没什么意义了,因为需要的是精确的参数类型

四、泛型的使用限制以及部分变通方法

4-1、不能实例化类型变量

如T obj = new T ();  // 报错, 提示: Type parameter 'T' cannot be instantiated directly

解决方法:使用反射创建泛型实例

public class GenericObj<T> {
  private T obj;
  public GenericObj(Class<T> c){
      try {
        obj = c.newInstance(); // 利用反射创建实例
      } catch (Exception e) {
        e.printStackTrace();
      }
    }
}

public class Test {
  public static void main (String args[]) {
    GenericObj<String> go = new GenericObj<> (String.class);
  }
}

Class类本身就是泛型, 而String.class是Class的实例

4-2、不能实例化泛型数组

如T [] arr = new T[3];// 报错, 提示: Type parameter 'T' cannot be instantiated directly

不过只是单纯声明的话是允许的,例如 T [] arr

解决方法一:创建Object类型的数组,然后获取时转型为T类型:

public class GenericArray<T>  {
  private Object [] arr;
  public GenericArray(int n) {
    this.arr = new Object[n];
  }
  public void set (int i, T o) {
    this.arr[i] = o;
  }
  public T get (int i) {
    return (T)this.arr[i];
  }
}

解决方法二: 使用反射机制中的Array.newInstance方法创建泛型数组

public class GenericArray<T> {
  private T [] arr;
  public GenericArray(Class<T> type, int n) {
    arr = (T[])Array.newInstance(type, n); // 利用反射创建泛型类型的数组
  }
  public void set (int i, T o) {
    this.arr[i] = o;
  }
  public T get (int i) {
    return (T)this.arr[i];
  }
}

public class Test {
  public static void main (String args[]) {
  GenericArray<String> genericArr = new GenericArray<>(String.class, 5);
  genericArr.set(0, "abcdefg");
  System.out.println(genericArr.get(0)); 
  }
}

解决方法三:可以声明通配类型的数组, 然后做强制转换

Foo<Node> [] f =(Foo<Node> [])new Foo<?> [3]; 

4-3、不能在泛型类的静态上下文中使用类型变量

public class Foo<T> {
  private static T t;
  public static T get () { // 报错: 'Foo.this' can not be referenced from a static context
    return T;
  }
}

原因在于静态变量,不需要创建对象即可调用;而对于泛型类,对象不创建的话无法确定泛型是哪种类型。二者的要求矛盾所以编译禁止通过。

在非泛型类的静态泛型方法中是可以使用类型变量的。

4-4、不能使用基本类型的值作为类型变量的值

Foo<int> node = new Foo<int> (); // 报错

必须使用封装类型

4-5、泛型类不能继承exception

// ERROR:Generic class may not extend java.lang.throwable
public class GenericException<T> extends Exception {
}

扩展Throwable也是不合法的

public class Foo {
  public static  <T extends Throwable> void doWork () {
    try {
      // ERROR: Cannot catch type parameters
    }catch (T t) {
    }
  }
}

但是以下写法可以通过

public class Foo {
  public static  <T extends Throwable> void doWork  (T t) throws T {
    try {
      // ...
    }catch (Throwable realCause) {
      throw  t;
    }
  }
}

五、泛型与模板模式

5-1、模板模式简要回顾

所谓模板模式,就是把做事情的流程整理好,共通方法提炼出来,由个体实施的方法空出来,谁要实施谁自己填空。

比如,要实现生成、上传合同。先定义好模板(抽象类),就是每一步要干啥,自己能干的自己干,自己干不了的让各个具体需要合同的需求方自己做。

→ 其实这一步就算是在完成骨架类。

public abstract class Abstract合同{
    public void createProcess(){
        查询数据();
        读取合同模板();
        替换合同模板中的关键字();
        生成合同PDF文件();
        签章();
        信息入库();
    }
    
    protected abstract void 查询数据();
    private void 读取合同模板(){...};    
    protected abstract void 替换合同模板中的关键字();
    private void 生成合同PDF文件(){...};
    private void 签章(){...};
    private void 信息入库(){...};
    
}

要生成采购合同的时候,只需要完成跟自己业务实际相关的方法即可:

public class 采购合同 extends Abstract合同{

    @Override
    protected void 查询数据() {
        "select A,B,C from T_Contract"
        ...
    }

    @Override
    protected void 替换合同模板中的关键字() {
        strContractContent.replaceAll("#contract_no#","采购合同编号001-01");
        ...
    }

}

5-2、加入泛型

5-1 的例子中,只是简单地用抽象类、抽象方法对于公共行为和不明确的行为进行了隔离。
实际上情况往往要复杂的多。

比如查询出来的数据一定是一个List,要对List做遍历分别生成合同,而每种合同返回的数据类型一定是不一样的。也就是说除了不明确的行为以后,还出现了不明确的对象类型。

这就需要引入泛型来解决问题。比较完整的伪代码如下:

public abstract class Abstract合同<T>{
    public void createProcess(){
        List<T> contractDatas = 查询数据();
        for (T data : contractDatas) {
            读取合同模板();
            替换合同模板中的关键字(data);
            生成合同PDF文件();
            签章();
            信息入库();
        }
    }
    
    protected abstract List<T> 查询数据();
    private void 读取合同模板(){...};    
    protected abstract void 替换合同模板中的关键字(T t);
    private void 生成合同PDF文件(){...};
    private void 签章(){...};
    private void 信息入库(){...};

}

5-3、小结

模板模式的特点:

  • 抽象类:控制程序总体流程(骨架);实现共通(确定)的方法
  • 实现类:继承抽象类,实现具体差异部分的逻辑

模板模式的技术实现:

  • 用抽象方法隔离不明确的行为
  • 用泛型代表不明确的对象类型

六、泛型擦除

6-1、什么是泛型擦除

泛型这个概念,只存在于编译器中。而不存在于虚拟机(JVM)中。

意思是说,编译器对带有泛型的java代码进行编译时,会去执行类型检查和类型推断,然后生成普通的不带泛型的字节码,供JVM接收并执行。

这个过程就叫做泛型擦除。

下面通过反射,向List类型的容器中添加Integer元素,来证明:只要能想办法绕开编译器检查,泛型的约束?不存在的

    public static void main(String... args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        List<String> strList = new ArrayList<>(3);
        strList.add("A1");
        strList.add("B2");
        // 这样写是一定会报编译错的: strList.add(333);  所以使用反射
        strList.getClass().getMethod("add",Object.class).invoke(strList,333);
        strList.forEach(System.out::println);
    }

在最后一句打上断点,会发现333作为Integer类型已经被成功添加到了strList里面,但是在print的时候,仍然会报类型转换异常:java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

那么就仍然使用反射的方法绕开泛型的类型检查

Integer c = (Integer)strList.getClass().getMethod("get",int.class).invoke(strList,2);
System.out.println(c);
        
运行结果:333        
小结:对于JVM来说,泛型信息是不可见的。

6-2、擦除的过程

Java 编译器会将泛型代码中的类型完全擦除,使其变成原始类型。

然后在代码中加入类型转换,将原始类型转换成想要的类型。

这些操作都是编译器在后台进行的,以保证类型安全。

所以说泛型就是一个语法糖,对于实际运行不产生任何影响。

看一个例子:

public class ErasureTest<T> {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

    public static void main(String... args) {
        ErasureTest<String> a = new ErasureTest<>();
        a.setT("abc");
        System.out.println(a.getT());
    }

}

使用 javap -c 命令查看这段代码的字节码

(javap用法点这里)

  public T getT();
    Code:
       0: aload_0
       1: getfield      #2                  // Field t:Ljava/lang/Object;
       4: areturn

  public void setT(T);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field t:Ljava/lang/Object;
       5: return

  public static void main(java.lang.String...);
    Code:
       0: new           #3                  // class com/puhuijia/helloStudy/ErasureTest
       3: dup
       4: invokespecial #4                  // Method "<init>":()V
       7: astore_1
       8: aload_1
       9: ldc           #5                  // String abc
      11: invokevirtual #6                  // Method setT:(Ljava/lang/Object;)V
      14: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
      17: aload_1
      18: invokevirtual #8                  // Method getT:()Ljava/lang/Object;
      21: checkcast     #9                  // class java/lang/String
      24: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      27: return

注意看main()方法

标号9的那一行,abc被创建出来时是String,但是马上就开始按照Object进行处理。

标号21以前的处理,使用的类型都是Object。

到了标号21的那一行,进行了“checkcast”,将Object进行了强制类型转换,变成String。

(checkcast checks that the top item on the operand stack (a reference to an object or array) can be cast to a given type)

总之,这再度印证了:所有的活都是Object干的;强制类型转换是打死也躲不开的;泛型只是让你舒服一些,把所有脏活累活都藏起来了。

6-3、java擦除的特点

  • C++ 中泛型的实例化会为每一种类型都产生一套不同的代码,这就是所谓的代码膨胀。

  • java 中并不会产生这个问题。虚拟机中并没有泛型类型对象,所有的对象都是普通类。

java 不同类型都能使用一套代码,就是因为采用了泛型擦除机制,字节码中根本就没有类型。

实际上,擦除机制的出现,主要目的是为了JDK新老版本在泛型上的兼容性问题。

6-4、擦除带来的一些问题

6-4-1、类型信息丢失

由于泛型擦除机制的存在,在运行期间无法获取关于泛型参数类型的任何信息,自然也就无法对类型信息进行操作;例如:instanceof 、创建对象等

这是“4-1、不能实例化类型变量”的原因

6-4-2、类型擦除对于多态的影响

看下面这个例子,正常来说,这两个方法的参数不同,应该被辨识成重载,但是编译器报错:

    void method(List<Integer> a) {
    }

    void method(List<String> b) {
    }
    
Error:java: name clash: method(java.util.List<java.lang.String>) and method(java.util.List<java.lang.Integer>) have the same erasure    

错误信息是说两个方法的参数在擦除之后完全一致(have the same erasure),都是List,所以就不是重载,而是产生了冲突。

6-4-3、泛型在父类子类继承时造成的一个影响
6-4-3-1、问题提出

首先创建一个简单的使用泛型的父类:

public class GsuperClass<T> {
    private T t;
    public T getT() {return t;}
    public void setT(T t) {this.t = t;}
}

然后子类:

public class GchildClass extends GsuperClass<String>{
    private String childString;
    @Override
    public String getT() {
        return this.childString;
    }
    @Override
    public void setT(String s) {
        this.childString = s;
    }
}

现在一个让人疑惑的问题是:子类中的getT和setT真的是重写?(Override)

编译器认为这两个方法是重写,因为不加@Override注解的话会直接报警。

但是,以set方法为例,父类中的setT(T t)经过类型擦除以后是setT(Object t);

子类中的set方法参数是String类型,也就是说方法名相同但是参数不同,这难道不算是重载?(overloading)

6-4-3-2、正常情况下的表现

普通类的话,下面这样显然是合法的:

public class CommonClass {
    public void setT (Object t){System.out.println("object");}
    public void setT (String s){System.out.println("String");}
    public static void main(String[] args) {
        CommonClass c = new CommonClass();
        c.setT(new Object());
        c.setT("123");
    }
}

结果:
object
String
Process finished with exit code 0

推及到继承上面,如果在一个普通的父类里面定义

public void setT(Object t) {this.t = t;}

在其子类里面定义

public void setT(String t) {this.childString = t;}

的话,也是完全行得通的,子类就拥有了两个setT方法(重载)。

6-4-3-3、分析

但是,在使用了泛型以后就完全不同了。如果尝试在 GchildClass 里面调用我认为有可能存在的重载方法时,编译直接报错:

public class GchildClass extends GsuperClass<String>{
    private String childString;
    @Override
    public String getT() {
        return this.childString;
    }
    @Override
    public void setT(String t) {
        this.childString = t;
    }
    public static void main(String[] args) {
        GchildClass child = new GchildClass();
        child.setT("123");
        child.setT(new Object()); // ERROR:参数与方法类型不匹配
        System.out.println(child.getT());
    }
}

下面分析子类字节码,看看编译器和jvm到底干了什么见不得人的交易。

public class GchildClass extends GsuperClass<java.lang.String> {
  public com.puhuijia.quartz.base.GchildClass();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method com/puhuijia/quartz/base/GsuperClass."<init>":()V
       4: return

  public java.lang.String getT();
    Code:
       0: aload_0
       1: getfield      #2                  // Field childString:Ljava/lang/String;
       4: areturn

  public void setT(java.lang.String);
    Code:
       0: aload_0
       1: aload_1
       2: putfield      #2                  // Field childString:Ljava/lang/String;
       5: return

  public void setT(java.lang.Object);
    Code:
       0: aload_0
       1: aload_1
       2: checkcast     #10                 // class java/lang/String
       5: invokevirtual #6                  // Method setT:(Ljava/lang/String;)V
       8: return

  public java.lang.Object getT();
    Code:
       0: aload_0
       1: invokevirtual #8                  // Method getT:()Ljava/lang/String;
       4: areturn
}

生成了两套get、set方法。

而且参数是Object的调用了参数是String的方法。

这里使用了桥接模式,相当于说jvm自己暗地里完成了对于setT(Object)的重写。

6-4-3-4、彩蛋:关于重载的定义问题

java编译器对于重载的定义不包括返回值。

也就是说两个方法名、参数列表一致的方法,不管返回值是什么,都不可以同时存在,不视为重载;

但是对于jvm来说,上例中存在两个 getT: String getT() 和 Object getT(),

这显然不符合java的语法定义,但是却符合jvm标准。

6-4-4、用泛型擦除来解释 “4-5、泛型类不能继承exception”

如果以下代码可以通过

public class GenericException<T> extends Exception {
}

那么就会出现这样的情况:

try{
}catch(GenericException<String> e1){
}catch(GenericException<Integer> e2){
}

泛型擦除以后,两个catch就都会变成 GenericException,因此规定泛型类不能继承exception

6-4、擦除导致的泛型不可变性

对于泛型来说,其相同的容器类之间不存在任何的父类子类关系。

也就是说:

  1. 不管 class A extends B ; 还是 class B extends A

  2. List《A》与List《B》之间不存在任何父类子类关系。

这称之为不可变性。

与不可变性相对应的概念是 协变、逆变:

  • 协变:如果 A 是 B 的父类,并且 A 的容器(比如 List< A>) 也是 B 的容器(List< B>)的父类,则称之为协变的(父子关系保持一致)

  • 逆变:如果 A 是 B 的父类,但是 A 的容器 是 B 的容器的子类,则称之为逆变

Java 中数组是协变的,泛型是不可变的。

6-4-1、用<?>来解决不可变性造成的问题
class Fruit {}
class Apple extends Fruit {}

class Plate<T>{
    private T item;
    public Plate(T t){item=t;}
    public void set(T t){item=t;}
    public T get(){return item;}
}    

像下面这样使用水果盘子放苹果,是会引发编译错误的,因为根据上面的不可变性,容器之间不存在继承关系,无法向上溯型:

Plate<Fruit> p=new Plate<Apple>(new Apple());   // ERROR

解决方法如下:

Plate<? extends Fruit> p=new Plate<Apple>(new Apple());

(完)

发布了33 篇原创文章 · 获赞 19 · 访问量 3173

猜你喜欢

转载自blog.csdn.net/zmflying8177/article/details/99695158