java中泛型浅析——泛型擦除及extends和super通配符

文章目录

  • 1. 泛型的定义、作用及类型擦除
  • 2. 泛型集合
  • 3. 泛型通配符
    • 3.1 无限通配符
    • 3.2 上限通配符
    • 3.3 下限通配符

1. 泛型的定义、作用及类型擦除

   先看一段维基百科上对Java中范泛的定义:

 Generics are a facility of generic programming that were added to the       
 Java programming language in 2004 within version J2SE 5.0. They 	    	
 were designed to extend Java's type system to allow "a type or method 	 
 to operate on objects of various types while providing compile-time type 
 safety. The aspect compile-time type safety was not fully achieved,
 since it was shown in 2016 that it is not guaranteed in all cases.

   重点看第二句话,意思的泛型扩展了Java的类型系统,允许一种类型或方法操作在各种各样的数据类型上,并在编译时提供安全检查。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。但是JVM并不知道泛型的存在,经过编译器的处理,即类型擦除,同一般的类和方法并没有本质的区别。

举例就很容易明白上面的话,

  1. 参数化类型
//其中T的类型为类类型即可,
class FirstGeneric{
		private T myT
		FirstGeneric(T t){
			this.myT = t;
		}
		public static void main(String[] args){
			FirstGeneric g1= new FirstGeneric<>("abc");
		    FirstGeneric g2 = new FirstGeneric<>(1);
			//可以定义更多的泛型实例
		}
}
  1. 类型检查
//类型检查
public class TypeCheck{
	public static void main(String[] args){
		List strList = new ArrayList<>();   //(1)
		//List strList = new ArrayList();   //(2)
        strList.add("aaa");
        strList.add("bbb");
        strList.add(3); //编译时这步会报错,但如果注释(1),取消(2)处的注释,只有在运行执行下面一行代码时,会报错。
        strList.forEach(str -> System.out.println(((String)str).length()));
	}
}
  1. 泛型擦除
    泛型并没有定义一中新的类型,只是在编译的时候会执行相应的语句检查,经过编译器处理后,与一般类型没有区别。
List l1 = new ArrayList<>();
List l2 = new ArrayList<>();
List l3 = new ArrayList();
System.out.println(l1.getClass());  
System.out.println(l2.getClass());
System.out.println(l3.getClass());
System.out.println(l1.getClass() == l2.getClass());

//输出:
class java.util.ArrayList
class java.util.ArrayList
class java.util.ArrayList
true

2. 泛型集合

   普通集合:即Jdk1.5之前的集合,一个集合中可以放入任何的类元素,往其中加入元素时,会丢失类型信息,通通变成了Object类型,在取出其中的元素,要进行强制转换。

List list = new ArrayList();
list.add("abc");
((String)list.get(0)).length(); // 需要进行强制转换
System.out.println(strList.get(0).getClass());  //能够输出正确的类型信息
list.add(1); //会自动转换成对应的类类型

   泛型集合:当不使用通配符时(下文将会阐述),除了null元素外,只能存储相同类型的元素,否则将会报错。

List list = new ArrayList<>();
list.add("abc");
list.get(0).length(); //可以正常调用
list.add(1); //编译时就报错

3. 泛型通配符

   有了泛型,可以让写出的代码更加清晰可读,也能够在编译阶段就能发现某些异常。但编译器对泛型也有着较为严格的限制,考虑如下的例子:

public class Test{
    static void testFun(List list){
    	//do something
    }
    public static void main(String[] args) {
    	List plantList = new ArrayList<>();
       List fruitList = new ArrayList<>();
       List appleList = new ArrayList<>();
       List animalList = new ArrayList<>();
       Test myTest = new Test();
       myTest.testFun(appleList); // (1)
       myTest.testFun(animalList);  //(2) 
       myTest.testFun(plantList);  //(3) 
    }
}
class Plant{
	 void fun(){
        System.out.println("plant");
    }
}
class Fruit extends Plant{
	@Override
    void fun(){
        System.out.println("furit");
    }
}
class Apple extends Fruit{
    @Override
    void fun(){
        System.out.println("apple");
    }
    void appleFun(){
    }
}
class Orange extends Fruit{
    @Override
    void fun(){
        System.out.println("origin");
    }
}
class Animal{
}

   上例中(1) (2) (3)处的代码将会引发异常,可见虽然Apple是Fruit的子类,但List并不是List 的子类,两者之间并没有直接的联系,但上述代码中想实现的功能,在实际中也很常见,如果为了每一种具体的泛型,都定义一个处理函数,那么太麻烦了。还好通过使用泛型通配符,可以达到我们的目的,不过在操作上也有相应的限制。
Java泛型中总共有三种型式的通配符:

  1. :无限通配符;
  2. :上限通配符,其中T表示某种具体的类型,下同;
  3. :下限通配符;

下文的展开都是基于上述代码,尤其要关注testFun函数。

3.1 无限通配符

将上面的testFun函数修改为如下:

void testFun(List list){
	if (!list.isEmpty() && (list.get(0) instanceof Fruit)) {
            ((Fruit)list.get(0)).fun();
        }
}

   该函数需要一个List集合,但对传过来的集合中的元素类型没有限制,所以主函数中的(1) (2) (3)处的代码都可以顺利运行。但也正因为如此,不能向其加入元素,即不能调用add()方法,因为编译器对泛型集合操作有较为严格的限制,除了null元素例外,只能向集合中加入同一类型的元素,在真正运行之前,并不能确定集合中的元素类型,所以编译器干脆就不允许向其中添加元素。对于这种类型的参数,只能取其中的元素,并且默认的元素类型是Object,因此,如果需要使用其中元素的所含且不存在Object类型中的方法,需要进行强制类型转换,为了防止出现转换异常,之前应该进行类型检查。

3.2 上限通配符

将上面的testFun函数修改为如下:

void testFun(List list){
	if (!list.isEmpty()) {
            list.get(0).fun();
            if(list.get(0) instance Apple){
				((Apple)list.get(0)).appleFun();
			}
     }
}

   此时形参要求传过来的实参中的元素类型必须是Fruit或其子类,所含主函数中(1)处可以通过,(2)和(3)处依然报错。如果集合不为空,可以直接调用类中的fun函数,这很好理解,毕竟传过来的集合中的元素都是Fruit的子类,所以,其中一定含有该函数。但如果要调用其子类的函数,则需要经过强制转;并且不能向参数list中添加元素,此时并不知道其中所含元素的具体类型,有可能是Apple类型,也有可能是Orange类型,而两者之间也没有直接联系,因此添加元素的行为编译器是禁止的。

3.3 下限通配符

   继续修改testFun函数;

void testFun(List list){
	if(!list.isEmpty()){
		Object object = list.get(0);
		if(object instanceof Fruit){
			((Fruit)list.get(0)).fun();
		}
	}
	list.add(new Apple());
	list.add(new Orange());
	//list.add((Fruit)new Plant());  //这里只是示意,实际上这步代码在运行时会报错
}

   此时形参要起传过来的实参中的元素类型必须是Fruit或其父类,因此(3)处代码顺利运行,(1) (2)两次会报错,由于集合中所含的元素类型可能的上限是Object类型,而每种类型都是它的子类,因此list中取出的元素类型都为Object,此时若要调用Fruit中的函数,必须进行强制转换;同时,这种情况允许向集合中添加元素,但该元素必须是Fruit的子类,因为其子类总是可以安全且隐式的向上转型为父类(及祖先类),但在函数体内不能直接向list中添加Fruit 的父类,除非显示的进行类型转换,考虑list中的元素类型为Fruit,如果允许添加它的父类,就必须要进行类型转换,但向下转型必须要显式进行,如上图所示,但注意的是,只有父类所指向的对象是子类时,才能转换成功。

参考:
https://blog.csdn.net/briblue/article/details/76736356
维基百科

你可能感兴趣的:(java中泛型浅析——泛型擦除及extends和super通配符)