泛型

2019-08-26 05:50:49来源:博客园 阅读 ()

新老客户大回馈,云服务器低至5折

泛型

为什么要使用泛型?

在jdk1.5之前是没有泛型的,ArrayList的实现大致如下

 1 public class ArrayList{
 2     private Object[] elements;
 3     private int size;
 4     
 5     public Object get(int i) {
 6         return elements[i];
 7     }
 8     public void add(Object o) {
 9         elements[size++]=o;
10     }
11     public int getSize() {
12         return size;
13     }
14     public ArrayList() {
15         this.size=0;
16         elements = new Object[16];
17     }
18 }
 1 public class ArrayTest {
 2     public static void main(String[] args) {
 3         ArrayList list = new ArrayList();
 4         list.add(new String("String类型"));
 5         list.add(new Random());
 6         String str = (String) list.get(0);
 7         System.out.println(str);
 8         String str2 = (String) list.get(1);        // java.lang.ClassCastException: java.util.Random cannot be cast to java.lang.String
 9         System.out.println(str2);
10     }
11 }

使用这种方式会有两个问题

  1. 每次获取一个值的时候都需要进行强制类型转换
  2. 由于没有类型检查,可以向ArrayList中添加任意类型的对象,编译和运行都不会报错,但是在后面的调用我们可以看出,当将get()方法获取的值强制转换为一个Stringl类型时就会抛出一个异常

但是从JDK引进泛型之后,可以看出在获取一个值时不需要进行强制类型转换,同时当ArrayList试图添加一个非String类型的变量时,编译器也会给出一个错误,同时由于使用了泛型,也使程序变得更加可读.

 1 public class ArrayTest {
 2      public static void main(String[] args) {
 3           ArrayList<String> list = new ArrayList<String>();
 4           list.add(new String("String类型"));
 5           list.add(new Random());    //报错
 6           
 7           String str = list.get(0);
 8           System.out.println(str);
 9     }
10 }

如何使用泛型?

首先先了解一下泛型的有关术语

ArrayList<E> 称为泛型类型
ArrayList<E> 中的E称为类型形参
ArrayList<String> 称为泛型实例类型
ArrayList<String> 中的String称为类型实参
ArrayList 称为原始类型

定义一个泛型类

类中定义的类型形参指定方法的返回类型以及域和局部变量的类型

 1 public class SimpleGeneric<T>{
 2     public void setMax(T max) {
 3         this.max = max;
 4     }
 5 
 6     private T max;
 7     private T min;
 8     
 9     public SimpleGeneric() {
10         max = null;
11         min = null;
12     }
13 
14     public SimpleGeneric(T max, T min) {
15         this.max = max;
16         this.min = min;
17     }
18 
19     public T getMax() {
20         return max;
21     }
22 
23     public T getMin() {
24         return min;
25     }
26 
27     public void setMin(T min) {
28         this.min = min;
29     }
30     
31     
32 }

定义一个泛型接口

interface GenericInterface<T>{
    void getMax(T t);
}

普通类中也可以定义泛型方法

1 public class GenericMethodDemo{
2 
3     public static <T> T getMiddleElement(T... args) {
4         int middleIndex = args.length/2;
5         return args[middleIndex];
6     }
7 }

使用泛型

在声明和实例化泛型类型的变量时指定类型形参的类型

1 SimpleGeneric<String> demo = new SimpleGeneric<String>("aaa", "bbb");
2 System.out.println(demo.getMax()+"=" + demo.getMin());
3  // 调用泛型方法
4 
5  String middleString = GenericMethodDemo.<String>getMiddleElement("aaaa","bbb","ccc");
6  System.out.println(middleString);

 

 在jdk1.7以后可以在构造函数中省略类型实参,编译器会根据变量的类型推断出类型实参的值,

1 Integer middleInt = GenericMethodDemo.getMiddleElement(1,2,3,4);
2 System.out.println(middleInt);

当编译器推断类型参数时,如果遇到如下这种情况.编译器将6.78和0.5自动装箱生成两个Double类型对象,将179自动装箱生成一个Integer对象,然后寻找它们的共同父类它们的共同父类有 Number和Comparable,不同的编译器可能针对这种情况作出不同的处理结果,下面这种情况就推断出类型参数为Number,当然也可以指定类型参数为Comparable,但最好的解决方法是将所有的参数类型改为一致,如将179改为double类型

 1 Number middleElement = GenericMethodDemo.getMiddleElement(6.78,179,0.5);  //编译推断出类型实参为Number
 2 System.out.println(middleElement);
 3   
 4 Comparable middleElement2 = GenericMethodDemo.<Comparable>getMiddleElement(6.78,179,0.5);
 5 System.out.println(middleElement2);
 6 
 7 Double middleElement3 = GenericMethodDemo.getMiddleElement(6.78,179d,0.5);
 8 System.out.println(middleElement3++);
 9 

 类型变量的限定

我们使用如下的记法限制类型变量的范围:  <T extends Comparable> 表示T应该是Comparable的子类型,也可以添加多个限定,用&分隔每一个限定类型,如 <T extends Comparable & Serializable> 由于java中一个类只能有一个父类,但可以有多个超接口,因此,在限定时应该保证在类型限定时,如果有父类和父接口同时存在,应该把类做为限定列表的第一个,如 <T extends Number & Comparable>表示T应该是Number的子类,并实现了Comparable接口,下面的例子为了保证可以调用compareTo(Object o)方法,就要限定类型参数必须实现Comparable接口.

 1 public static <T extends Comparable> T min(T[] a) {
 2     if(a == null || a.length ==0)
 3         return null;
 4     T min = a[0];
 5     for(int i =1;i < a.length;i++) {
 6         if(min.compareTo(a[i]) > 0)
 7             min = a[i];
 8     }
 9     return min;
10 }

类型擦除

java虚拟机中所有的对象都属于普通类,也就是说,当我们定义一个泛型类型时,都会对应一个相应的原始类型,原始类型的名字就是删去类型参数,替换为限定类型(无限定的类型参数使用Object)的泛型类,这些工作都是由编译器完成的.通过反编译工具可以看出,SimpleGeneric泛型类擦除后的原始类型如下 

 1 public class SimpleGeneric<T>{
 2     public void setMax(T max) {
 3         this.max = max;
 4     }
 5 
 6     private T max;
 7     private T min;
 8     
 9     public SimpleGeneric() {
10         max = null;
11         min = null;
12     }
13 
14     public SimpleGeneric(T max, T min) {
15         this.max = max;
16         this.min = min;
17     }
18 
19     public T getMax() {
20         return max;
21     }
22 
23     public T getMin() {
24         return min;
25     }
26 
27     public void setMin(T min) {
28         this.min = min;
29     }
30     
31 }
View Code

(类型参数有限定的)泛型类型擦除后的原始类型如下

 1 public static <T extends Comparable> T min(T[] a) {
 2     if(a == null || a.length ==0)
 3         return null;
 4     T min = a[0];
 5     for(int i =1;i < a.length;i++) {
 6         if(min.compareTo(a[i]) > 0)
 7             min = a[i];
 8     }
 9     return min;
10 }
View Code

 

如果类型参数的限定不止一个(如下)时会发生什么情况呢?

 1 public static <T extends Serializable & Comparable> T max(T o1, T o2) {
 2     if (o1.compareTo(o2) > 0) {
 3         return o1;
 4     } else {
 5         return o2;
 6     }
 7 }
 8 public static <T extends Comparable & Serializable> T max(T o1 ,T o2){
 9      if(o1.compareTo(o2)>0) {
10          return o1;
11      }else {
12          return o2;
13      }
14 }

 当Comparable在前时,编译器在做类型擦除时会用Comparable替换类型参数T

当Serializable在前时,编译器在做类型擦除时会用Serializable替换类型参数T,这时由于o1是Serializable类型的,不能调用compareTo()方法,所以编译器处理这种情况时,会插入强制类型转换。

因此,为了提高效率,限定类型参数时应该把标记接口类型的放在类型限制列表尾端

桥方法

 1 class Animal2<T>{
 2     private T name;
 3     public void show(T a) {
 4         System.out.println("父类的show方法"+a);
 5     }    
 6     
 7     public T getName(){
 8         return name;
 9     }
10 }
11 class Cat2 extends Animal2<String>{
12     
13     public void show(String a) {
14         System.out.println("子类的show方法"+a);
15     }
16     public String getName() {
17         return "这是猫";
18     }
19 }

在编译器处理泛型方法时,会出现以下的情况,当Cat2继承了Animal2<T>并定义了和父类方法名相同的方法。类型擦除时,父类的T变为Object,也就是说方法 public void show(T a){...}变为 public void show(Object a){...}
而在子类中,存在两个show方法

public void show (String a){...}    //子类继承的父类的show方法
public void show(Object a){...}    //子类自己定义的show方法

show(Object a)和show(String a)应该是重载关系,但在多态调用中,当我们使用父类引用指向子类对象,并调用show方法时,按理应该调用的是父类的public void show(Object a)方法,但实际上是子类的public void show(String a)方法,这说明子类的show(String a)方法重写了父类的show(Object a)方法.
但是我们说过子类在重写父类的方法时,参数类型必须相同.那么编译器是如何处理这个问题的呢?

1 Animal2<String>  animal  = new Cat2();
2         animal.show("哈哈哈");        // 输出: 子类的show方法哈哈哈
3         
4 Animal2<String> animal2 = new Animal2<String>();
5         animal2.show("嘻嘻嘻");        // 输出:  父类的show方法嘻嘻嘻

通过反编译工具可以看出,编译器会在子类中生成一个新的方法,这个方法就是桥方法

public volatile void show(Object obj){
    show((String)obj);
}

桥方法重写了父类的show(Object obj)方法,并调用子类的show(String obj)方法,这样就可以保持调用具有多态性
再来看另一种情况,当发生类型擦除后,按照生成桥方法的思路,子类中会有两个getName()方法

public String getName(){...}
public volatile Object getName(){...}

但是我们平时编写这样的代码是不允许的,但是在虚拟机中,使用参数类型和返回类型确定一个方法,也就是说虚拟机是可以区分这是两个不同的方法,因此编译器在生成字节码文件的时候会生成两个不同的方法字节码。

这种生成桥方法的方式并不是只在泛型中出现,普通类中也可能会生成桥方法,如类B继承了类A.类B重写了父类的getObject()方法,但是子类的返回类型是父类返回类型的子类型,我们称A.getObject()和B.getObject()具有可协变的返回类型

 1 class A{
 2     public Object getObject() {
 3         return "父类";
 4     }
 5     
 6 }
 7 class B extends A{
 8     @Override
 9     public String getObject() {
10         return "子类";
11     }
12 }
View Code

当使用泛型时,编译器会在必要的时候插入类型强制转换.如下面的例子中,ArrayList一个使用了泛型,一个没有使用泛型.通过反编译工具可以看出,使用了泛型的ArrayList在调用get()方法时,进行了强制类型转换.

1 ArrayList list = new ArrayList();
2 list.add("String");
3 String string = (String) list.get(0);
4 System.out.println(string);
5         
6 ArrayList<String> list2 = new ArrayList<>();
7 list2.add("泛型类");
8 String string2 = list2.get(0);
9 System.out.println(string2);

 

 

总结:

  1. 虚拟机中没有泛型,都是普通的类和方法.Java的泛型只是在编译期间存在,当泛型检查通过,编译器在生成.class文件时,会进行类型擦除,类型参数都用它们的限定类型替换
  2. 为了保证多态,编译器会生成桥方法来解决泛型擦除带来的重载冲突
  3. 为了保证类型安全,编译器会在它认为必要的时候插入类型强制转换

 使用泛型时会遇到的坑,这些问题大多数都是泛型擦除引起的

1. 不能使用基本类型来实例化类型形参

当使用基本类型如double来实例化类型参数时 Animal<double>,发生擦除后原来的类型参数会变成Object类型,而Object类型不能存储double类型的值,因此在这种情况下可以使用相应的包装器类型或使用独立的类和方法来处理

Animal2<double> list = new Animal2<>();        // 编译报错

2.在运行时使用instanceof查询类型只能适用于泛型类的原始类型

由于发生了类型擦除,在虚拟机中的每个对象对应一个非泛型类 因此在查询某一个对象是否属于某个泛型类时,使用instanceof其实就是查询某一个对象是否是某一个泛型类型的原始类型,既然这 编译器在检查到 a instanceof Animal<Double>这种情况时会得到一个编译错误,因为这种情况下添加类型参数没有意义.最终生成的字节码都是 a instanceof Animal这样的.下面的例子也可以看出str和dou1都返回的是原始类型Animal2.class

 1 Animal2<Double> dou1 =  new Animal2<>();
 2 if(dou1 instanceof Animal2) {
 3     System.out.println("cat是Animal2类型");
 4 }
 5 Animal2<Double> dou2 =  new Animal2<>();
 6 if(dou2 instanceof Animal2<Double>) {        // 编译报错
 7     System.out.println("cat是Animal2类型");
 8 }
 9 Animal2<String> str =  new Animal2<>();
10 System.out.println(str.getClass().getName());        // Animal2
11 System.out.println(dou1.getClass().getName());  // Animal2

 

同样,在编译期使用强制类型转换会得到一个警告,为什么不报错而出现一个警告 这是由于使用instanceof是在运行时查询类型的,结果可以确定,而使用强制类型转换会在编译器生成字节码之前进行类型检查,编译器不能确定obj是否是Animal2<Double>类型的, obj在虚拟机中可以是任意实例化参数的Animal,在生成字节码时,泛型类型会擦除变为原始类型,也就是说,凡是<>里面的东西都不会出现在字节码文件中.那么我们写代码为什么要加上类型参数?

1 Animal2 obj = new Animal2();
2 System.out.println(obj.getName());        // null
3 Animal2<Double>  p = (Animal2<Double>)obj;
4 System.out.println(p.getName());        // null

 

这是为了让编译器检查参数化类型是否满足类型参数的限定.所以java的泛型是一种伪泛型,只在编译期间起作用,当生成字节码时泛型就不存在了.为什么要这样设计 在C++中,每个模板的实例化都会产生不同的类型,这种现象称为"模板代码膨胀".而java中就不会发生这种问题

3.不能创建参数化类型的数组,但可以声明参数化类型的数组

java中子类数组的引用可以转换为超类数组的引用,而不需要采用强制类型转换,如String[]类型的引用可以转换为Object[]类型的引用,但是基本数据类型的数组不可以转换为超类数组的引用,因为基本数据类型的数组中存放的是值而不是引用.如 int[]类型的数组中存放元素是int值,而转为Object[]类型的时,Object[]数组中的每个元素应该存储的是一个引用.所以编译器会给出一个错误

1 int[] intarr = new int[10];
2 Object[] objarr=intarr;        //编译报错
3         
4 String[] array = new String[10];
5 Object[] objArray = array;        // 可以

但是不是有自动装箱机制吗?难道不能把int类型的值包装为Integer类型,这样Object[]中的元素就可以引用了.
这是因为自动装箱时实际上在堆中生成了新的Integer对象,这样Object[]数组的引用指向的就不是原来的int[]数组了.鉴于这种情况,编译器禁止将基本数组类型引用转换为超类数组引用.虽然不可以将基本类型数组的引用转换为超类数组的引用.但是可以将基本类型数组的引用转换为Object类型的引用.

1 int[] intArr = new int[6];
2 Object obj = intArr;   //可以

虽然可以将子类数组引用转换为超类数组的引用,但是会有一定的问题如 String[] 类型数组的引用array 转化为Object[] 类型的引用objArray ,在objArray[]中添加一个非String类型(如Integer)的元素时,编译会通过(但是将一个Integer类型添加到String[]数组中显然是不合法的),这种情况运行时会抛出一个 java.lang.ArrayStoreException

1 String[] array = new String[10];
2 Object[] objArray = array;        // 可以
3 objArray[0] = "String类型";        //运行不报错
4 objArray[1] = new Integer(2);        // 抛出一个异常

实际上,所有类型的数组都会记住创建它们时的元素类型,可以将一个类型兼容的引用存储到数组中,当将一个类型不兼容的引用存储到数组中时,会发生两种情况

  • 如果这个数组的引用可以兼容要添加元素的引用,编译不会报错,但是运行时会抛出一个ArrayStoreException
  • 如果这个数组的引用不可以兼容要添加元素的引用,编译器会报错(这种情况下编译器可以检查出来类型不兼容)

下面讨论泛型数组的情况

SimpleGeneric<Number>[] arr= new SimpleGeneric<Number>[10];    //编译报错:Cannot create a generic array of SimpleGeneric<Number>
SimpleGeneric<String>[] simpleArray;        //声明可以,编译通过

类型擦除后arr的类型是SimpleGeneric[](SimpleGeneric是前面例子中定义的类),可以在数组中存储一个类型兼容的引用,也可以将它的引用转换为超类数组的引用但前面说过,数组会记住它的元素类型,如果存取其他类型的引用,会抛出一个ArrayStoreException异常.但在泛型类中,由于类型擦除会使这种机制失效, 可以在数组中添加其他参数化类型的对象,这样会导致SimpleGeneric<String>[] 的数组中存放了SimpleGeneric<Cat>类型的元素,显然这是不合法的,所以java不允许创建参数化类型的数组 

怎么解决这种问题呢?

1.可以声明通配类型的数组,然后进行类型强制转换,这样编译不会报错,但会给出一个警告,因为其结果是不安全的,假如在arr2[0]的位置存储了一个SimpleGeneric<Cat>类型的元素对arr2[0].getMax()调用一个String类的方法,就会抛出一个ClassCastException异常

1 SimpleGeneric<String>[] arr2 = (SimpleGeneric<String>[]) new SimpleGeneric<?>[5];
2 Object[] objarray2 = arr2;
3 objarray2[0] = new SimpleGeneric<Cat>(new Cat("1"),new Cat("2"));        //编译器可以通过,运行也不抛出ArrayStoreException异常
4 arr2[0].getMax().charAt(0);        // 抛出一个ClassCastException异常 (generic.Cat cannot be cast to java.lang.String)

 

2. 使用ArrayList<SimpleGeneric<String>>来收集参数化类型对象,当添加一个非SimpleGeneric<String>类型的时候,编译器会报错,这是由编译器检查处理的,与类型擦除无关.

1 ArrayList<SimpleGeneric<String>>  list = new ArrayList<SimpleGeneric<String>>();
2 list.add(new SimpleGeneric<Cat>());            //编译报错
3 list.add(new SimpleGeneric<String>());

 4.使用注解消除Varargs警告

Java中不支持泛型类型的数组,但是在下面这种情况下,当调用addAll方法时,java虚拟机会建立一个SimpleGeneric类型的数组,这违背了前面所说的规定,对于这种情况,编译器的规则会放松,只会得到一个警告,可以使用注解@SuppressWarnings("unchecked")或@SafeVarargs来抑制这个警告

public static void main(String[] args) {
    Collection<SimpleGeneric<String>> collection = new ArrayList<>(); 
    SimpleGeneric<String> p1 = new SimpleGeneric<>();
    SimpleGeneric<String> p2 = new SimpleGeneric<>();
    addAll(collection,p1,p2);        
}
    
@SafeVarargs
public static <T> void addAll(Collection<T> coll,T... elements){
    for (T e : elements) {
        coll.add(e);
    }
}

 5.不能实例化类型变量和构造泛型数组

当使用 new T(),new T[10],T.class时,由于类型擦除,变为new Object(),new Object[10],Object.class.我们的本意肯定不希望使用 new Object(), new Object[10]和Object.class,因此这种写法是非法的

1 T p = new T();        //编译报错
2 T[] arr = new T[10];    //编译报错
3 Class<T> c1 = T.class;            // 编译报错

 6.泛型类中静态上下文中类型变量无效,

就是说下面这种写法是错误的,因为泛型类的实际类型参数是在创建对象时指定的,当调用泛型静态方法时 ,类型参数还未确定.

 1 public class GenericStaticDemo<T>{
 2     private static T singInstance;    // 编译报错
 3     public static T getSingInstance() {    //编译报错
 4         return singInstance;
 5     }
 6     // 这样是可以的,因为这个方法的类型参数不是泛型类的类型参数,而是自己的类型参数
 7     public static <T> void show(T o){
 8         System.out.println(o);
 9     }
10 }    

 7.不能抛出或捕获泛型类的实例

java中不能抛出泛型类型或捕获泛型类的实例,甚至拓展Throwable都是不合法的.这是因为抛出异常和捕获异常是发生在程序运行期间,而在运行期间,泛型已经被擦除.但可以在异常声明中使用类型变量,使用这种方式可以消除编译器对受查异常的检查,如这样 Exception t = new Exception();  Demo.doWork(t); 调用下面的方法时,编译会认为这是一个运行时异常,从而通过编译.

 1 // 这样写是合法的
 2 @SuppressWarnings("unchecked")
 3 public static <T extends Throwable> void doWork(Throwable t) throws T {
 4     try {
 5         System.out.println(t);
 6     }catch(Throwable e) {
 7         e.printStackTrace();
 8         throw (T)t;
 9     }
10 }

 8.泛型类型擦除后可能会发生冲突

在下面中的类中定义了泛型方法public boolean equals(T value),当发生类型擦除后.GenericConstraintDemo6中会有两个一样的equals(Object)方法,所以编译器会给出一个错误,解决的办法是修改引起冲突的方法名

1 public class GenericConstraintDemo6<T>{
2         
3     public boolean equals(T value) {
4         return false;
5         
6     }
7 }    

泛型的规范指出:要想支持擦除的转换,就需要强行限制一个类或类型变量不能同时成为两个接口类型(这两个接口是同一接口的不同参数化)的子类.什么意思呢?
举个例子,Maxable是一个泛型接口,Person实现了Maxable<Person>接口.而Person的子类实现Maxable<Student>接口时,会发生一个冲突,Maxable<Student>和Maxable<Person>就是同一个接口的不同参数化.Person在实现Maxable<Student>接口时,会生成一个桥方法 public volatile boolean getMax(Object obj),同理,在Student类实现Maxable<Student>接口也可能产生一个桥方法 public volatile boolean getMax(Object obj),两个方法发生了冲突.但是当不采用泛型时,就不会产生冲突.

 1 interface Maxable<T>{
 2     public boolean getMax(T o) ;
 3 }
 4 class Person implements Maxable<Person>{
 5 
 6     @Override
 7     public boolean getMax(Person o) {
 8         return false;
 9     }
10 }
11 // 报错.Student类同时成为两个接口类型Maxable<Student>和Maxable<Person>的子类
12 class Student extends Person implements Maxable<Student>{
13     @Override
14     public boolean getMax(Student o) {
15         return false;
16     }
17 }

泛型的继承规则

在泛型中的继承和子父类之间的关系可能会很难理解,下面使用举例的方式说明这些规则

  1. ArrayList<Number>和ArrayList<Integer>是子父类关系吗?实际上,ArrayList<Number>和ArrayList<Integer>两者没有任何关系,但是ArrayList<Number>是List<Number>的子类,也就是说,只有相同的类型实参的泛型类的对象之间才存在继承关系
  2. 泛型类也可以像普通类一样扩展或实现其他类或接口,如List<T>扩展Collection<T>接口,ArrayList<T>实现List<T>接口,
  3. 泛型也可以使用多态,如List<Number>可以引用ArrayList<Integer>类型的对象
1 ArrayList<Number> arr1 = new ArrayList<>();
2 ArrayList<Number>arr3 = arr2;        // 报错,arr3和arr2并没有任何关系
3         
4 ArrayList<Number> arr2 = new ArrayList<>();
5 List<Number> arr4 = arr2;                // 可以,List<Number>是ArrayList<Number>的父类

 通配符类型

由于泛型ArrayList<Number>和ArrayList<Integer>之间不存在继承关系,当将一个ArrayList<Integer>类型的实参传递给一个ArrayList<Number>类型形参的方法(注意,这不是泛型方法)时,就会出现一个编译错误.

也就是说,下面的getMax()方法只能接受ArrayList<Number>类型的实参,这样有很大的局限性.所以引入了通配符类型,通配符类型也是一种类型形参的实例化.但是可以发生变化.

 1 public static void main(String[] args) {
 2     ArrayList<Integer> list = new ArrayList<Integer>();
 3     getMax(list);            //编译报错  The method getMax(ArrayList<Number>) in the type GenericWildcard is not applicable for the arguments (ArrayList<Integer>)
 4     ArrayList<Number> list2 = new ArrayList<Number>();
 5     getMax(list2);            //编译通过
 6         
 7 }
 8 public static Number getMax(ArrayList<Number> arrlist) {
 9     Number max = arrlist.get(0);
10     for (int i = 1; i < arrlist.size(); i++) {
11         if(max.doubleValue()<arrlist.get(i).doubleValue()) {
12             max = arrlist.get(i);
13         }
14     }
15     return max;
16 }

 

子类限定通配符 (? extends 类型)

当使用 <? extends Number>限定泛型类时,允许类型参数变化,它的类型参数可以是Number本身或Number的子类,此时ArrayList<? extends Number>类型的引用plist就可以引用 类型实参是"Number本身或它的子类"的参数化ArrayList类型的引用list,但是当plist调用add方法时,会出现一个错误,但是调用get方法可以正常运行.这是为什么呢?

ArrayList<? extends Number>中相当有下面两个方法(这两个方法实际并不存在,是为了更好的说明原因而想象出来的)

public ? extends Number get(int index)    //这个方法的返回值是一个Number或它子类的引用,将它的引用赋值给Number是合法的,所以编译器不会报错
public boolean add(? extends Number e)    // 编译器检查后会知道这个方法将接收一个Number类型的子类,但具体是什么类型,插入是否安全,编译器不清楚,因此编译器将拒绝传递任何特定的类型
 1 public class ExtendDemo {
 2     public static void main(String[] args) {
 3         ArrayList<Integer> list= new ArrayList<Integer>();
 4         list.add(0);
 5         list.add(1);
 6         list.add(2);
 7         System.out.println(list);
 8         
 9         
10         printArrayList(list);
11         ArrayList<? extends Number>  plist = list;
12         plist.add(new Double(12.5));        // 报错
13         plist.add(new Integer(10));                //报错
14         for (int i = 0; i < plist.size(); i++) {
15             System.out.println(plist.get(i));
16         }
17         System.out.println(list);
18     }
19     
20     public static void printArrayList(ArrayList<? extends Number> arr) {
21         for (Number number : arr) {
22             System.out.println(number);
23         }
24         arr.add(new Integer(11));        //报错
25     }
26 }

超类限定通配符 (? super 类型)

当使用超类型限定泛型实例类型时,<? super Number>表示ArrayList<? super Number>可以引用任意类型实参是"Number或它的父类"的 ArrayList泛型实例类型的引用,如下面例子
中的plsit引用list.那么按照分析子类型限定的方法来分析一下,ArrayList<? super Number>中相当于有以下个方法

public boolean add(? super Number e)    //编译器无法知道具体的类型是啥,所以不能接受Number的父类型Object类型的参数,所以编译报错,但是可以接收Number类型和它的子类型的引用.
public ? super Number get(int index)    // 当调用这个方法时,不能确定返回对象的类型,因此只能使用Object接收

 

 1 public class SuperDemo {
 2     public static void main(String[] args) {
 3         ArrayList<Object> list = new ArrayList<Object>();
 4         list.add(0);
 5         list.add(1);
 6         show(list);
 7         System.out.println(list);
 8         ArrayList<? super Number> plist = list;
 9         plist.add(new Object());        // 报错,plist只能添加Number和它的子类
10         plist.add(new Integer(5));
11         for (int i = 0; i <plist.size(); i++) {
12             Object object = plist.get(i);            
13             System.out.print(object+",");
14         }
15         
16     }
17     public static void show(ArrayList<? super Number> arr) {
18         for (int i = 0; i <arr.size(); i++) {
19             Object object = arr.get(i);
20             System.out.print(object+",");
21         }
22         arr.add(new Object());    //报错,arr只能添加Number和它的子类
23     }
24 }

无限定通配符 (?)

就是说 ArrayList<?>类型的变量 plist可以引用ArrayList实例类型变量,如 ArrayList<Object>,ArrayList<Integer>等.此时ArrayList中相当有两个方法

public boolean add(? e)    //编译器无法知道具体的类型是啥,所以不能添加元素,编译产生一个错误
public ? get(int index)    // 当调用这个方法时,不能确定返回对象的类型,因此只能使用Object接收

 

public class NoWildcardDemo {
    public static void main(String[] args) {    
        ArrayList<Object> list = new ArrayList<Object>();
        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list2.add(0);
        list2.add(1);
        
        ArrayList<?> plist=list;
        ArrayList<?> plist2 = list2;
        plist.add(new Object());        //编译报错
        plist2.add(new Integer(11));        // 编译报错
        plist2.add(null);                    //唯一可以编译通过的
        Object object = plist.get(0);        //只能用Objcet接受
        Object object2 = plist.get(0);    // 只能用Object接受
    }
}

 总结:

  1. 当使用子类型限定的通配符时,可以从泛型对象中读取,但不可以写入
  2. 当使用超类型限定的通配符时,可以向泛型对象中写入,但不可以读取
  3. 当使用无限定的通配符时,既不可以从泛型对象中读取,也不可以向泛型对象中写入(null除外)

与其纠结编译器如何去处理一些奇怪的代码,不如确保我们不去写这些代码.下面是编写泛型代码的一些技巧

  1. 使用通配符类型 作为一个引用变量的声明,或者作为一个函数的形参声明,而不用来进行类的定义和实例化
  2. 使用类型参数  (java库中,通常用E表示集合的元素类型,K和V表示Map中键和值的类型.T(U/S)表示任意类型)来定义泛型类和泛型方法
  3. 创建泛型类的对象或调用泛型方法时,要么指明类型实参,要么要编译去自行推断

 


原文链接:https://www.cnblogs.com/liyeye/p/11361478.html
如有疑问请与原作者联系

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇:java秒杀系列(2)- 页面静态化技术

下一篇:springboot 返回json和xml