聊聊Java中的泛型

聊聊Java中的泛型

参考资料

下文如有错漏之处,敬请指正

一、概述

1. 泛型的定义

1.1 定义

官方:Java泛型是Jdk1.5中引入的一个新特性,其本质是参数化类型,也就是所操作的数据类型被指定为一个参数(type parameter),这种参数类型可以用在类、接口和方法的创建,分别称为泛型类、泛型接口和泛型方法。

所谓泛型指的就是在类定义的时候不设置类中属性或方法(返回值及其参数)的类型,使用泛型参数进行替代,当类使用的时候再定义具体类型。

泛型参数:T,其中T可以是(写)任意字符(a-ZA-Z_)

泛型的出现是为了解决类型转换的问题。

1.2 常见形式
  • 泛型类

    //	定义
    public class Message<T> {
       private T type;
    }
    
    //	使用
    public static void main(String[] args) {
    		
      	// 泛型参数的类型为 String
    		Message<String> stringMessage=new Message<>();
      	// 泛型参数的类型为 Integer
    		Message<Integer>integerMessage=new Message<>();
    	}
    
    
  • 泛型接口

    //	定义
    interface IMessage<T>{
      public void send(T t);
    }
    
    //	使用
    class MessageImpl implements IMessage<String>{
      			@Override
            public void pring(String t) {
                System.out.println(t);
            }
    }
    public static void main(String[] args) {
      IMessage message=new MessageImpl();
      message.print("hello world");
    }
    
  • 泛型方法

    //	定义
    class Message<T> {
    
    private T type;
    
    public Message(T type) {
       this.type = type;
    }
    
    // 泛型方法 
    // <T> 声明泛型参数
    //	T  返回的类型
    // 这里的T与Message<T>的T没有联系,这里的T也可以用其他字符(如E)来替代: 
    // private <E>  E sendMessage(E content){
    //   return (E) (type+":"+content);
    // }
    
    private <T>  T sendMessage(T content){
       return (T) (type+":"+content);
    }
    
    //	使用
    public static void main(String[] args) {
    
       Message<String> message=new Message<>("String");
       System.out.println(message.sendMessage("Hello World!"));
       //	输出结果:String:Hello World!
    }
    

2. 为什么需要泛型

如果没有泛型:

  • 不能限制元素类型

    期望Collection集合存放的全部是A对象,但实际存放B对象也不会有任何语法错误,因为Collection集合对元素的类型是没有任何限制的。

  • 读元素时要向下转型

    由于Collection集合不知道元素的实际类型是什么,仅仅知道是Object。在执行写入操作时需要向上转型,在读取数据后需要向下转型。

向上转型:子类转为父类 Parent parent=new Son(); (向上转型可以不用显式转型)

向下转型:父类转为子类 Son son=(Son) parent; (向下转型存在风险,只有父类是由子类向上转型后再向下转型是安全的)

有了泛型以后:

  • 代码更加简洁【不用强制类型转换】
  • 程序更加健壮【只要编译时期没有警告,那么运行时期就不会出现ClassCastException异常】
  • 可读性和稳定性【在编写集合的时候,限定了元素的类型】

3. 泛型的优点

  • 减少强制类型转换

  • 规范集合的元素类型,提高代码的安全性和可读性。

4. 泛型的使用

4.1 泛型类
class Car {
	private String brand;

	public Car(String brand) {
		this.brand = brand;
	}

	@Override
	public String toString() {
		return "Car{" +
				"brand='" + brand + '\'' +
				'}';
	}
}


class Plane {
	private String brand;

	public Plane(String brand) {
		this.brand = brand;
	}

	@Override
	public String toString() {
		return "Plane{" +
				"brand='" + brand + '\'' +
				'}';
	}
}

//  使用泛型定义交通工具的类型为泛型参数T
public class Vehicle<T> {
	private List<T> vehicleLists;

	public Vehicle() {
		this.vehicleLists = new ArrayList<>();
	}

	public void add(T vehicle) {
		vehicleLists.add(vehicle);
	}

	public void showAll() {
		for (T t : vehicleLists) {
			System.out.println(t);
		}
	}

	public static void main(String[] args) {

		//  定义交通工具的实际类型为汽车
		Vehicle<Car> carVehicle = new Vehicle<>();
		//	增加交通工具
		carVehicle.add(new Car("Benz"));
		carVehicle.add(new Car("Tesla"));

		//  如果传入的参数不是Car类型,则会编译错误
		//  carVehicle.add(new Plane("Virgin"));

		//	显示已有的交通工具
		carVehicle.showAll();
		/**
		 * 输出结果:
		 * Car{brand='Benz'}
		 * Car{brand='Tesla'}
		 */
	}
}

泛型类的实际类型在类实例化的时候确定。

4.2 泛型接口
 interface IMessage<T>{
        public void send(T t);
 }

对于泛型接口的子类有两种实现方法:

  1. 在子类定义的时候继续使用泛型,在使用时设置实际类型

    class MessageImpl<T> implements IMessage<T> {
    	@Override
    	public void send(T t) {
    		System.out.println(t);
    	}
    }
    public static void main(String[] args) {
    		//  子类定义时设置实际类型
    		IMessage<String> iMessage=new MessageImpl<>();
    		iMessage.send("Hello World");   //  Hello World
    	}
    
  2. 在子类定义的时候明确类型

    class MessageImpl implements IMessage<String>{
    	@Override
    	public void send(String t) {
    		System.out.println(t);
    	}
    }
    
    public static void main(String[] args) {
    		IMessage iMessage = new MessageImpl();
    		iMessage.send("Hello World");   //  Hello World
    	}
    

    泛型接口的实际类型在子类定义时确定或是在子类实例化时确定。

4.3 泛型方法
public class Demo {


   /**
    * 泛型方法
    *
    * @param args 形参
    * @param <T>  声明泛型参数
    * @return T   返回类型
    */
   public static <T> T[] func(T... args) {
      return args;
   }

   public static void main(String[] args) {
      //  设置泛型方法的形参和返回类型为 Integer
      Integer[] data = func(1, 2, 3, 4);
      for (int i : data) {
         System.out.print(i + " ");
      }
      /**
       * 输出结果:
       * 1 2 3 4
       */
      System.out.println();
      //  设置泛型方法的形参和返回类型为 String
      String[] data2 = func("Hello", "World", "!");
      for (String i : data2) {
         System.out.print(i + " ");
      }
      /**
       * 输出结果:
       * Hello World !
       */

   }

}

泛型方法的实际类型在方法运行时确定。

4.4 泛型实际类型的确定时机
  • 泛型类的实际类型在类实例化的时候确定。

  • 泛型接口的实际类型在子类定义时确定或是在子类实例化时确定。

  • 泛型方法的实际类型在方法运行时确定。

二、泛型的扩展

2.1 Java泛型的擦除与补偿

1、泛型参数的擦除与补偿

擦除:

带有泛型参数的JAVA程序被编译时,会按照泛型参数的擦除规则将泛型参数擦除为原始类型

List<String>会被擦除为原始类型List

补偿:

JAVA程序运行时,当获取某个带有泛型参数的变量,JVM会将该变量的原始类型转为其实际类型,即实现自动类型转换。

例如:

Pair.java

import java.io.Serializable;

public class Pair<T> {
	
	private  T value;

	public T getValue() {
		return value;
	}

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

	//    泛型方法
	public static <E> E getE(E e){
		return e;
	}

	public static void main(String[] args) {
		Pair<String> pair=new Pair<>();
		pair.setValue("10");
		String a=pair.getValue();

		int b=getE(10);
	}
}

反编译Pair.class:
在这里插入图片描述

通过反编译字节码文件,我们可以得出带有泛型参数的变量会被擦除成原始类型,并且会检查变量的实际类型用来判断是否可以进行强制类型转换(即类型安全),之后程序在运行时,JVM会自动给带有泛型参数的变量增加类型转换的指令,将原始类型转为实际类型

2、泛型参数的擦除规则
  • 若参数类型无限定<T>,那么原始类型就是 Object,即所有出现 T 的地方都用 Object 替换。
    • T obj 擦除后的原始类型为Object obj
    • List、List、List擦除后的原始类型为List
    • List[]擦除后的原始类型为List[]。
  • 若参数类型有限定<T extends E ><T super E >,那么原始类型就是E,即所有出现 T 的地方都用E替换.
    • List擦除后的原始类型为List。
    • List擦除后的原始类型为List。
  • 若参数类型有多个限定List< T exnteds XClass1 & XClass2 >,那么使用第一个边界类型XClass1作为原始类型。

例子1:

public static void main(String[] args) {
   List<String> list = new ArrayList<>();
   list.add("hello");
   String s = list.get(0);
}

经过编译器的擦除后,上面的代码与下面的代码是一致的

public static void main(String[] args)  {
   List list = new ArrayList();
   list.add("hello");
   String s = (String) list.get(0);
}

例子2:

public class Example{
    //  listMethod接收List参数,并进行重载
    public void listMethod(List<String> strList){}	//	擦除后的类型是 listMethod(List strList)
    public void listMethod(List<Integer> intList){}	//	擦除后的类型是 listMethod(List intList)
}

如上程序无法编译,编译时报错信息如下:

java: name clash: listMethod(java.util.List<java.lang.Integer>) and listMethod(java.util.List<java.lang.String>) have the same erasure

此错误的意思是说listMethod(List<Integer>)方法在编译时泛型参数擦除后的方法是listMethod(List intList),它与另外一个方法参数类型重复,不符合重载,通俗地说就是方法签名重复了。

3、泛型参数擦除的原因

Java程序经过编译后的字节码文件中已经没有泛型的任何信息了,也就是说一个泛型类和一个普通类在经过编译后都指向了同一字节码。比如Foo<T>类,经过编译后将只有一份Foo.class类,不管是Foo<String>还是Foo<Integer>引用的都是同一字节码。

Java之所以如此处理,有如下原因:

  • 避免类膨胀

    C++的泛型实现采用不同的实现类,而这会造成类膨胀问题,为此Java采用泛型参数擦除来解决此问题。

  • 避免JVM的大换血

    C++的泛型在运行期也有效,而Java的泛型在编译期就被擦除了,如果JVM也把泛型延续到运行期,那么JVM就需要进行大量的重构工作了。

  • 版本兼容

    在编译期擦除可以更好地支持原生类型(RawType),JDK1.5提出了泛型这个概念,但是JDK1.5以前是没有泛型的,也就是泛型是需要兼容JDK1.5以下的版本。

2.2 泛型参数擦除所带来的问题

因为种种原因,Java不能实现真正的泛型,只能使用泛型参数擦除来实现伪泛型,这样虽然不会有类型膨胀问题(真泛型实现是具有不同的实现类),但是也引起来许多新问题,所以,SUN公司对这些问题做出了种种限制,避免我们发生各种错误。

1、强制类型转换问题

Q:既然所有的泛型参数在编译后都会被替换为原始类型,那么为什么我们在获取的时候,不需要进行强制类型转换呢?

A:在编译器编译完程序后,当程序运行时,JVM会自动给带有泛型参数的变量增加类型转换的指令,将原始类型转为实际类型

例如:

一个泛型类Human

class Human<T> {  
    private T value;  
    public T getValue() {  
        return value;  
    }  
    public void setValue(T  value) {  
        this.value = value;  
    }  
}  

泛型参数擦除后,Human的原始类型:

class Human {  
    private Object value;  
    public Object getValue() {  
        return value;  
    }  
    public void setValue(Object  value) {  
        this.value = value;  
    }  
}

假设value实际类型是Date,在程序运行时期,当value被获取时,JVM就会把getValue()return value;转换成return (Date) value,同理,也会把this.value = value;转换为this.value = (Date)value;

2、引用传递的问题

Q1:既然说泛型参数在编译的时候会被擦除掉,那如何确保我们之前定义的泛型参数被使用(即限定泛型的类型)了呢?

A1:Java编译器通过先检查代码中泛型参数的实际类型(字节码指令是checkcast)即类型安全检查,如果类型安全就进行泛型参数擦除,否则编译错误。

例如:

public static  void main(String[] args) {  

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

在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明泛型参数在擦除前会先进行类型安全检查。

Q2:类型检查是针对谁的呢?

A2:类型检查就是针对引用的,而无关它真正引用的实例。

例如:

public class Test {  

    public static void main(String[] args) {  

        ArrayList<String> list1 = new ArrayList();  
        list1.add("1"); //	编译通过  
        list1.add(1);  //	编译错误  
        String str1 = list1.get(0); //	返回类型是String (get()的返回值会被补偿,即由JVM进行强制类型转换)  

        ArrayList list2 = new ArrayList<String>();  
        list2.add("1"); //	编译通过  
        list2.add(1);  //   编译通过  
        Object object = list2.get(0); //	返回类型是Object  

        new ArrayList<String>().add("11"); //	编译通过  
        new ArrayList<String>().add(22); 	//	编译错误   
    }  

}  

Java中,像下面形式的引用传递是不允许的:

ArrayList<String> list1 = new ArrayList<Object>(); //	编译错误  
ArrayList<Object> list2 = new ArrayList<String>(); //	编译错误

我们先看第一种情况,将第一种情况拓展成下面的形式:

ArrayList<Object> list1 = new ArrayList<Object>();  
list1.add(new Object());  
list1.add(new Object());  

ArrayList<String> list2 = list1; //	编译错误

实际上,在第4行代码的时候,就会有编译错误。那么,我们先假设它编译没错。当我们使用list2引用用get()方法取值的时候,返回的都是String类型的对象,可是它里面实际上已经被我们存放了Object类型的对象,这样就会有ClassCastException了。所以为了避免这种极易出现的错误,Java不允许进行这样的引用传递。(泛型的出现就是为了解决类型转换的问题)。

再看第二种情况,将第二种情况拓展成下面的形式:

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

ArrayList<Object> list2 = list1; //	编译错误

没错,这样的情况比第一种情况好的多,最起码,在我们用list2取值的时候不会出现ClassCastException,因为是从String转换为ObjectObject是一切类的父类。可是,这样做有什么意义呢?泛型的出现就是为了解决类型转换的问题。我们使用了泛型,到头来,进行读写操作还是要自己强转,违背了泛型设计的初衷。所以Java也不允许这么干。再说,如果往list2往里面add()新的对象,那么到时候取得时候,你怎么知道取出来的到底是String类型,还是Object类型的呢?

所以,要格外注意泛型中的引用传递的问题,Java为了保证运行期的安全性,必须保证泛型参数是固定的,所以它不允许一个泛型参数可以同时包含两种类型,即使是父子类关系也不行。

3、继承中方法重写的问题

Q:子类继承了泛型类且同时指定了泛型参数的具体类型,然后重写了含有泛型参数的方法。由于编译时泛型参数被擦除,导致原本的重写变成了重载,那么在运行时编译器就无法确定执行哪一个方法,如何解决多态冲突呢?

A:编译器生成了桥接方法来避免泛型参数擦除与多态发生冲突。

JAVA重写和重载的区别:

重写重载
实现方式子类对父类允许的方法进行重新编写对一个类里对同名的方法进行重新编写
多态确定运行时期编译时期
参数列表必须相同必须不同
返回值类型父类或其子类没有要求
  1. 重写是子类对父类允许的方法进行重新编写,重载是对一个类里对同名的方法进行重新编写
  2. 重写实现的是运行时的多态,而重载实现的是编译时的多态。
  3. 重写的方法参数列表必须相同;而重载的方法参数列表必须不同。
  4. 重写的方法返回值类型只能是父类类型或其子类类型,而重载的方法对返回值类型没有要求。

例子:

一个泛型类:

public class Utility<T>{

   public void doThings(T utility){
      System.out.println(utility.getClass()+"---do……");
   }
   
}

编译后(即泛型参数被擦除后)的泛型类:

public class Utility{

   public void doThings(Object utility){
      System.out.println(utility.getClass()+"---do……");
   }
   
}

一个类继承了泛型类,并重写了doThings()方法:

public class StringUtility extends Utility<String> {

		@Override
		public void doThings(String utility){
			System.out.println(utility.getClass()+"---do……");
		}
	}
	//	父类的方法:
public void doThings(Object utility){
      System.out.println(utility.getClass()+"---do……");
   }
   
	//	子类重写父类的方法:   
	@Override
		public void doThings(String utility){
			System.out.println(utility.getClass()+"---do……");
		}

观察上面的代码,我们发现子类方法的重写不符合方法重写的规则(重写规则要求方法参数列表必须相同),父类是Object,子类是String,自然是不符合重写,但是符合重载的规则,如果是重载的话,那么子类就存在两个doThings方法,一个参数是Object,一个参数是String,我们来验证一下。

public static void main(String[] args) {
   StringUtility stringUtility=new StringUtility();
   stringUtility.doThings(new String());	//  编译通过
   stringUtility.doThings(new Date()); 	    //  编译错误
}

结果不是重载,竟然是重写!可实际又不符合重写的规则,那究竟是怎样实现的呢?

答案就是:Java 编译器自动生成了一个桥接方法来确保子类型按预期工作。

public class StringUtility extends Utility<String> {

  
  	/**
   *	编译器自动生成的桥接方法,通过javap -c  StringUtility.class 就能看到
	 * 	public void doThings(java.lang.Object);
	 *     Code:
	 *        0: aload_0
	 *        1: aload_1
	 *        2: checkcast     #11                 // class java/lang/String
	 *        5: invokevirtual #12                 // Method doThings:(Ljava/lang/String;)V
	 *        8: return
	 */
  	// 等同于
		//  public void doThings(Object utility){
		// 	    doThings((String) utility)
		//  }
  
		@Override
		public void doThings(String utility){
			System.out.println(utility.getClass()+"---do……");
		}
	}

这个桥接方法实际上就是对父类中dothings(Object utility)的重写,并委托给原始dothings(String utility)方法,以此来避免泛型参数擦除与多态发生冲突。

4、不能用instanceof判断泛型参数
List<String> list=new ArrayList<String>();
System.out.println(list instanceof List<String>);//	编译错误

以上代码不能通过编译,因为泛型参数擦除后,List<String>不存在泛型信息String了。

5、泛型类中static关键字的问题

Q:为什么泛型类中的静态方法和静态属性不可以使用泛型参数?

A:因为泛型类的实际类型是在类实例化的时候才确定的,而用static修饰的变量是在类加载后就被初始化了,不需要使用实例对象来调用,那么连对象都没有创建,那又如何确定这个泛型类的实际类型呢??

public class Test<T> {    
    private static T one;   //编译错误    
    private static  T show(T one){ //编译错误    
        return null;    
    }    
}

静态泛型方法:

public class Test<T> {    
    public static <E> E show(E one){ //	编译正确   
        return null;    
    }    
}

这是一个静态泛型方法,该泛型方法中的<E>是声明泛型参数,而后面的不带尖括号的 E 是方法的返回值类型。

泛型方法的实际类型由方法运行时决定,因此编译正确。

6、不能在泛型类中初始化运行期不安全的泛型变量
public class Foo<T> {
   public T t = new T();//    编译错误
   public T[] tArray = new T[5];//    编译错误
   public List<T> tList = new ArrayList<>();//    编译通过
}

上面代码中ttArray变量经过泛型参数擦除后原始类型都是Object,按理来说不应该编译错误,但由于Java语言是一个强类型、编译型的安全语言,要确保运行期的稳定性和安全性。

T[] tArray = new T()因为不确定运行时期的实际类型是否具有无参构造器,所以编译错误。

T[] tArray = new T[5]因为Java数组必须明确知道内部元素的类型,而且会”记住“这个类型,每次往数组里插入新元素都会进行类型安全检查。由于泛型参数擦除,导致数组在运行时期拿不到实际类型(JVM自动强制类型转(T[])tArray是错误的),就会抛出java.lang.ArrayStoreException,因此编译错误。

List<T> tList = new ArrayList<>()因为ArrayList源码中elementData它容纳了ArrayList的所有元素,其类型是Object数组,因为Object是所有类的父类,数组又允许协变(允许父类接收子类),因此elementData数组可以容纳所有的实例对象。元素加入时向上转型为Object类型(E类型转为Object),取出时向下转型为E类型(Object转为E类型),因此编译通过。

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
  //	容纳元素的数组
   transient Object[] elementData;
 	//	无参构造器
   public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }
  
  	//	获取一个元素
    public E get(int index) {
        rangeCheck(index);
				//	返回前进行强制类型转换
        return (E) elementData[index];
    }
}

在某些情况下,我们确实需要使用泛型数组,那该如何处理呢?代码如下:

public class Foo<T> {
      //  不在初始化,由构造器初始化
      public T t;
      public T[] tArray;
      public List<T> tList = new ArrayList<>();
      //  构造器初始化
      public Foo(Class<?> type) {
         try {
            t=(T)type.newInstance();
            tArray=(T[]) Array.newInstance(type,5);
         } catch (Exception e) {
            e.printStackTrace();
         }
      }

      @Override
      public String toString() {
         return "Foo{" +
               "t=" + t +
               ", tArray=" + Arrays.toString(tArray) +
               ", tList=" + tList +
               '}';
      }
   }

   public static void main(String[] args) {
      Foo<String> foo =new Foo<>(String.class);
      foo.t="Hello";
      foo.tArray[0]="World";
      foo.tList.add("!");
      System.out.println(foo);
     //	  输出结果Foo{t=Hello, tArray=[World, null, null, null, null], tList=[!]}
      
   }
}

2.3 泛型数组

class GenericArray<T> {
   T[] tArray = new T[5];//    编译错误
  
  // 模拟JVM自动强制类型转 
  // public static void main(String[] args) {
  	// Object[] tArray = new Object[5];
		// String [] sArray=(String[])tArray; Exception in thread "main" java.lang.ClassCastException: [Ljava.lang.Object; cannot be cast to [Ljava.lang.String;
  	
  //	}
  
}

上述创建泛型数组的方式是错误的,因为Java数组必须明确知道内部元素的类型,而且会”记住“这个类型,每次往数组里插入新元素都会进行类型检查。由于泛型参数擦除,导致数组在运行时期拿不到实际类型,(JVM自动强制类型转(T[])tArray是错误的)就会抛出java.lang.ArrayStoreException,因此编译错误。

Q:那要如何才能创建一个泛型数组呢?

A:利用反射

public class GenericArray <T> {
   T[] tArray;
   public GenericArray(Class<?> type) {
      tArray=(T[]) Array.newInstance(type,5);
   }
  
   public static void main(String[] args) {
		GenericArray array=new Foo(String.class);
		array.tArray[0]="HelloA";
		array.tArray[1]="HelloB";
		array.tArray[2]="HelloC";
		array.tArray[3]="HelloD";
		array.tArray[4]="HelloE";
		//	array.tArray[5]="HelloF"; java.lang.ArrayIndexOutOfBoundsException
		System.out.println(array.tArray[3]);//    HelloD

	}
}

2.4 泛型通配符

1、定义

Java泛型支持通配符,可以使用一个?表示任意类,也可以使用extends关键字表示某一个类(接口)的子类型,还可以使用super关键字表示某一个类(接口)的父类型。

2、extends关键字

extends关键字又称为泛型上界,List<? extends E> list表示list集合元素中的类型是E类型(或是其子类型),通常在泛型结构只参与读操作则限定上界。

public static <E> void read(List<? extends E> list){
   for (E e : list) {
      System.out.println(e);
   }
}


public static void main(String[] args) {
   ArrayList<String> arrayList = new ArrayList<>();
   arrayList.add("A");
   arrayList.add("B");
   arrayList.add("C");
   read(arrayList);
   /**
    * 输出结果:
    * A
    * B
    * C
    */
}

那可以使用List<? super E> list吗?

不能,我们不知道list到底存放的是什么元素,只能推断出是E类型的父类(或是E类型,下同,不再赘述),但问题是E类型的父类是什么呢?只有运行时才知道,那么编码期无法推断,也就完全无法操作了。当然,可以把它当作是Object类来处理,需要时再转换成E类型,但这完全违背了泛型的初衷。

那可以使用List<E> list吗?

可以,但不建议这样做,因为译器会推断出类型是Object,编译时就不会给出unchecked警告。

3、super关键字

super关键字又称为泛型下界,List<? super E> list表示list集合元素中的类型是E(或其父类),通常在泛型结构只参与写操作则限定下界。

public static  void write(List<? super Number> list){
 	 
   list.add(123);
   
   list.add(1.2f);
 
   list.add(1.2d);
}

那可以使用List<? extends Number> list吗?

public static  void write(List<? extends Number> list){
 	 
   //	编译失败
		list.add(123);
}

不能,编译失败。

? extends Number的意思是,允许Number所有的子类(包括自身)作为泛型参数。

list.add(123)编译错误是因为泛型参数擦除后,list集合元素中的类型是Number(与实际引用new ArrayList<Integer>()无关),而编译器不知道123Integer类型,或是Double类型,还是Number类型等,由于Java泛型要求运行期只能是有一个具体类型,因此编译器无法确定泛型的实际类型,编译失败。

在此种情况下,只有一个元素是可以add进去的:null值,这是因为null是一个万用类型,它可以是所有类的实例对象,所以可以加入到任何列表中。

4、注意

如果一个泛型结构即用作读操作又用作写操作,那该如何进行限定呢?不限定,使用确定的泛型类型即可,如List。

2.4 协变和逆变

1、定义

Java中,协变和逆变是指宽类型和窄类型在某种情况下(如参数、泛型、返回值)替换的特性,简单地说,协变是用一个窄类型替换宽类型,而逆变则是用一个宽类型覆盖窄类型

2、协变

用一个窄类型替换宽类型

	class Father {

	}

	class Son extends Father {

	}

	public static void main(String[] args) {
		Father father = new Son();
	}

father变量发生了协变,father变量是父类,而其赋值却是子类实例,也就是用窄类型替换了宽类型。这也叫多态,两者同含义,在Java世界里“重复发明”轮子的事情多了去了。

3、逆变

用一个宽类型覆盖窄类型

	class Father {
  	public void func(Integer a){}
	}

	class Son extends Father {
		public void func(Number a){}
	}

子类的func方法的参数类型比父类要宽,此时就是一个逆变方法,子类方法扩大了父类方法的输入参数。

4、Java泛型不支持协变和逆变
泛型不支持协变
public static void main(String[] args) {
   //  数组支持协变
   Number[] array=new Integer[10];
   //  编译失败,泛型不支持协变
   List<Number> list=new ArrayList<Integer>();
}

ArrayListList的子类型,IntegerNumber的子类型,由于Java为了保证运行期的安全性,必须保证泛型参数类型是固定的,所以它不允许一个泛型参数可以同时包含两种类型,即使是父子类关系也不行。泛型不支持协变,但可以使用extends关键字来模拟协变,代码如下所示

public static void main(String[] args) {
   //  编译通过,Number的子类型或本身都可以是泛型参数类型
   List<? extends Number> list=new ArrayList<Integer>();
   //  编译失败
   list.add(1);
}

? extends Number的意思是,允许Number所有的子类(包括自身)作为泛型参数,这里看着就像是把一个Integer类型的ArrayList赋值给了Number类型的List,其外观类似于使用一个窄类型替换一个宽类型。

list.add(123)编译错误是因为泛型参数擦除后,list的类型是Number(与实际引用``new ArrayList()无关),而编译器不知道123Integer类型,或是Double类型,还是Number类型等,由于Java`泛型要求运行期只能有一个具体类型,因此编译器无法确定泛型的实际类型,编译失败。

在此种情况下,只有一个元素是可以add进去的:null值,这是因为null是一个万用类型,它可以是所有类的实例对象,所以可以加入到任何列表中。

泛型不支持逆变

Java虽然可以允许逆变存在,但在对类型赋值上是不允许逆变的,你不能把一个父类实例对象赋值给一个子类类型变量,泛型自然也不允许此种情况发生了,但是它可以使用super关键字来模拟逆变实现,代码如下所示

public static void main(String[] args) {
   //  编译通过,Integer的父类型或本身都可以是泛型参数类型
   List<? super Integer> list=new ArrayList<Number>();
   list.add(1);
}

? super Integer的意思是可以把所有Integer父类型(自身、父类)作为泛型参数,这里看着就像是把一个Number类型的ArrayList赋值给了Integer类型的List,其外观类似于使用一个宽类型覆盖一个窄类型,它模拟了逆变的实现。

5、总结

Java的泛型是不支持协变和逆变的,只是能够实现协变和逆变。

2.5 泛型的使用顺序

List<T>List<?>List<Object>这三者都可以容纳所有的对象,但使用的顺序应该是首选List<T>,次之List<?>,最后选择List<Object>

List<T>
  • T是一个明确的类型,List<T>表示List集合中的元素都为T类型,具体类型在运行期决定。
  • List<T>可以进行读写操作,因为它的类型是固定的T类型,在编码期不需要进行任何的转型操作。
List<?>
  • ?表示的是任意类型,List<?>表示List集合中的元素可以是任意类型。

  • List<?>是只读类型,可以进行删除操作,因为删除操作与泛型类型无关。

    不能进行读写操作,因为List<?>在编码期间无法确定容纳元素的实际类型是什么,就无法校验类型是否安全了,也就无法进行读写操作。而且List<?>读取出的元素都是Object类型的,客户端可以进行手动转型,也因此List<?>常用于泛型方法的返回值。

List<Object>
  • Object表示的是所有类类型,List<Object>表示List集合中的元素可以是Object类型,即可容纳所有类型。
  • List<Object>可以进行读写操作,但是它执行写入操作时需要向上转型,在读取数据后需要向下转型,而这就失去了泛型存在的意义了。(泛型的出现就是为了解决类型转换的问题)

综上,有固定类型首选<T>,只进行只读操作就选<?>,最后才选<Object>

2.6 泛型的多重限定边界

1、定义

格式:<T extends XClass1 & XClass2 &XClassN >

使用&符号设定多重边界,指定泛型参数T必须是XClass1XClass2XClass……XClassN的共有子类型,此时T就具有了所有限定的方法和属性。

Java的泛型中,可以使用&符号关联多个上界并实现多个边界限定,而且只有上界才有此限定,下界没有多重限定的情况。因为多个下界,编码者可自行推断出具体的类型,无须编译器推断了。

2、例子

模拟一款游戏有VIP玩家和普通玩家,VIP玩家具有出场特效、皮肤加成、武器加成等福利,且对攻击有50%的加成。

interface Show {
   public Boolean isShowing();
}

interface Skin {
   public Boolean isSKinBonus();
}

interface Weapon {
   public Boolean isWeaponBonus();
}


//  模拟普通玩家
class Role1 {

}

//  模拟VIP过期了
class Role2 implements Show, Skin, Weapon {

   @Override
   public Boolean isShowing() {
      return false;
   }

   @Override
   public Boolean isSKinBonus() {
      return false;
   }

   @Override
   public Boolean isWeaponBonus() {
      return false;
   }
}

//  模拟VIP玩家
class Role3 implements Show, Skin, Weapon {

   @Override
   public Boolean isShowing() {
      return true;
   }

   @Override
   public Boolean isSKinBonus() {
      return true;
   }

   @Override
   public Boolean isWeaponBonus() {
      return true;
   }
}


public class Test {

   public static <T extends Show & Skin & Weapon> void attackBonus(T t) {
      if (t.isShowing() && t.isSKinBonus() && t.isWeaponBonus()) {
         System.out.println(t.getClass().getSimpleName() + " 攻击加成50%");
      } else {
         System.out.println(t.getClass().getSimpleName() + " VIP已过期");
      }
   }

   public static void main(String[] args) {
      //  编译错误,不符合泛型多重限定边界
      attackBonus(new Role1());
     
      //	 编译通过
      attackBonus(new Role2());
      attackBonus(new Role3());
   }


}

三、总结

泛型是JDK1.5提出的一个新特性,让我们在类定义的时候可以不设置属性或方法参数的类型,当类使用的时候在定义实际类型

由于JAVA为了避免类膨胀、对JVM 进行换血且兼容JDK1.5以前的版本,采用在程序编译时期对泛型参数进行擦除(擦除:按照泛型参数的擦除规则将泛型参数擦除为原始类型),在程序运行时期对带有泛型参数的变量进行补偿(补偿:JVM会将该变量的原始类型转为其实际类型)的方式实现。

Java泛型参数擦除也带来了不少新问题,因此SUN公司对这些问题做出了种种限制,避免我们发生各种错误。例如:

  • 泛型参数必须是固定的,不允许一个泛型参数可以同时包含两种类型,即使是父子类关系也不行。
  • 编译器生成了桥接方法来避免泛型参数擦除与多态发生冲突。
  • 不能用instanceof判断泛型参数。
  • 泛型类中的静态方法和静态属性不可以使用泛型参数。
  • 不能在泛型类中初始化运行期不安全的泛型变量。
  • Java的泛型是不支持协变和逆变的,只是能够实现协变和逆变。

泛型的使用顺序是有固定类型首选<T>,只进行只读操作就选<?>,最后才选<Object>

泛型的作用是解决类型转换的问题,常用于集合类型。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值