Java基础-泛型机制详解(一)为什么会引入泛型泛

java 泛型这个特性是从 jdk1.5 才开始加入的,因此为了兼容之前的版本,java 泛型的实现采取了 “伪泛型” 的策略,即 java 在语法上支持泛型,但是在编译阶段会进行所谓的 “类型擦除” (Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。

为什么会引入泛型

泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同的类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

引入泛型的意义在于:

  • 适用于多种数据类型执行相同的代码(代码复用)

我们通过一个例子来简述,可以先看下面的代码:

private static int add(int a, int b){
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

private static float add(float a, float b){
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

private static double add(double a, double b){
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}
复制代码

如果没有泛型,要实现不同类型类型的加法,每种类型都需要重载一个 add 方法,通过泛型,我们可以复用为一个方法:

private static <T extends Number> double add (T a,T b){
    System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
    return a.doubleValue() + b.doubleValue();
}
复制代码
  • 泛型中的类型在使用时指定,不需要强制类型转换 (类型安全,编译器会检查类型

看下这个例子:

List list = new ArrayList();
list.add("xxString");
list.add(100d);
list.add(new Person());
复制代码

我们在使用上述 list 中,list 中的元素都是 object 类型(无法约束其中的类型),所以在取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现 java.lang.ClassCastException 异常。

引入泛型,它将提供类型的约束,提供编译前的检查:

List<String> list = new ArrayList<>();
// list 中只能被放 String,不能放其他类型的元素。
复制代码

泛型的基本使用

TIP
我们通过一些例子来学习泛型的使用;泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法。

泛型类

  • 从一个简单的泛型类看起:
class Point<T>{ // 此处可以随便写标识符号,T 是 type 的简称。
    private T var; // var 的类型由 T 指定,即:由尾部决定
    public T getVar(){ //返回值的类型由外部决定
        return var;
    }
    
   public void setVar(T var){ // 设置的类型也由外部决定
       this.var = var;
   }
}

public class GenericsDemo06{
    public static void main(String args[]){
        Point<String> p = new Point<String>(); // 里面的 var 类型为 String 类型
        p.setVar("it"); // 设置字符串
        System.out.println(p.getVar().length); // 取得字符串的长度
    }
}
复制代码
  • 多元泛型
class Notepad<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;
    }
}

public class GenericsDemo09{
    public static void main(String args[]){
        Notepad<String, Integer> t = null; // 定义两个泛型类型的对象
        t = new Notepad<String, Integer>(); // 里面的key为String,value为Integer 
        t.setKey("汤姆"); // 设置第一个内容
        t.setValue(20); // 设置第二个内容
        System.out.println("姓名:" + t.getKey()); // 取得信息
        System.out.println("姓名:" + t.getValue()); // 取得信息

    }
}
复制代码

泛型接口

  • 简单的泛型接口
interface Info<T>{ // 在接口上定义泛型
	public  T getAar(); // 定义抽象方法,抽象方法的返回值就是泛型类型
}

class InfoImpl<T> implements Info<T>{ // 定义泛型借口
	private T var; // 定义属性
        public Infolmol(T var){ // 通过构造方法设置属性内容
            this.setVar(var);
        }
        public void setVar(T var){
           this.var = var; 
        }
        public T getVar(){
            return this.var;
        }  
}

public class GennericsDemo24{
    public static void main(String args[]){
        Info<String> i = null; // 声明接口对象
        i = new InfoImpl<String>("汤姆"); // 通过子类实例化对象
        System.out.println("内容:" + i.getVar());
    }
}
复制代码

泛型方法

  • 泛型方法,是在调用方法的时候指明泛型的具体类型。
  • 定义泛型方法语法格式

image.png

  • 调用泛型方法语法格式

image.png

说明一下,定义泛型方法时,必须在返回值前边加一个 ,来声明这是一个泛型方法,持有一个泛型 T,然后才可以用泛型 T 作为方法的返回值。

Class 的作用就是指明泛型的具体类型,而 Class 类型的变量 c,可以用来创建泛型类的对象。

为什么要用变量 c 来创建对象呢?既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去 new 一个对象,但是可以利用变量 c 的 newInstance 方法去创建对象,也就是利用反射创建对象。

泛型方法要求的参数是 Class 类型,而 Class.forName() 方法的返回值也是 Class,因此可以用 Class.forName() 作为参数。其中,forName() 方法中的参数是何种类型,返回的 Class 就是何种类型,返回的 Class 就是何种类型。在本例中, forName() 方法中传入的是 User 类的完整路径,因此返回的是 Class 类型的对象,因此调用泛型方法时,变量 c 的类型就是 Class,因此泛型方法中的泛型 T 就被指明为 User,因此变量 obj 的类型为 User。

当然,泛型方法不是仅仅可以有一个参数 Class,可以根据需要添加其他参数。

**为什么要使用泛型方法呢?**因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新 new 一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。

泛型的上下限

  • 先看下如下的代码,很明显是会报错的。
class A{}

Class B extends A{}

// 如下两个方法不会报错
public static void funA(A a){
    // ....
}
public static void funB(B b){
    funA(b);
    // ....
}

// 如下 funD 方法会报错
public static void funC(List<A> listA){
    // ....
}
public static void funD(List<B> listB){
    /* 未解析的编译问题:类型测试中的方法doPrint(List<A>)不适用于参数(List<B>)
    * Unresolved compilation problem: The method doPrint(List<A>) in the type test is not applicable for the arguments (List<B>)*/
    // funC(listB); 
    // ....
}
复制代码

那么如何解决呢?

为了解决泛型中隐含的转换问题,Java 泛型加入了类型参数的上下边界机制。<? extends A> 表示该类型参数可以是 A (上边界)或者 A 的子类类型。编译时擦除类型 A,即用 A 类型代替类型参数。这种方法可以解决开始遇到的问题,编译器知道类型参数的范围,如果传入的实例类型 B 是在这个范围内的话允许转换,这时只要一次类型转换就可以了,运行时会把对象当做 A 的实例看待。

public static void funC(? <extends A> listA){
    // ....
}
public static void funD(List<B> listB){
    funC(listB); // ok
    // ....
}
复制代码
  • 泛型上下限的引入

在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

上限

class Info<T extends Number>{ // 此处泛型只能是数字类型
    private T var; // 定义泛型变量
    public void setVar(T var){
        this.var = var;
    }
    public T getVar(){
        return this.var;
    }
    public String toString(){ // 直接打印
        return this.var.toString();
    }
}

public class demo1{
    public static void main(String args[]){
        Info<Integer> il = new Info<Integer>(); // 声明 Integer 的泛型对象
    }
}
复制代码

下限

class Info<T>{
    private T var; // 定义泛型变量
    public void setVar(T var){
        this.var = var;
    }
    public T getVar(){
        return this.var;
    }
    public String toString(){ // 直接打印
        return this.var.toString();
    }
}
public class GenericsDemo21{
    public static void main(String args[]){
        Info<String> i1 = new Info<String>(); // 声明 String 的泛型对象
        Info<Object> i2 = new Info<Object>(); // 声明 Object 的泛型对象
       i1.setVar("hello"); 
       i2.setVar(new Object());
       fun(i1);
       fun(i2);
    }
    
    public static void fun(Info<? super String> temp){ // 只能接收 String 或 Object 类型的泛型,String 类的父类只有 Object 类
        System.out.println(temp + ",");
    }
}
复制代码
  • 小结
<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类。
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类。

/* 使用原则《Effictive Java》
 * 为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限,消费者有下限。 */
1.如果参数化类型表示一个 T 的生产者,使用 <? extends T>;
2.如果它表示一个 T 的消费者,就使用 <? super T>;
3.如果即是生产又是消费,那么使用通配符就没有什么意义了,因为你需要的是精确的参数类型。
复制代码
  • 在看一个实际例子,加深印象。
private <E extends Comparable<? super E>> E max(List<? extends E> el){
    if(el == null){
        return null;
    }
    
    // 迭代器返回的元素属于 E 的某个子类型。
    Iterator<? extends  E> iterator = el.iterator();
    E result = iterator.next();
    while (iterator.hasNext()){
        E next = iterator.next();
        if(next.compareTo(result) > 0){
            result = next;
        }
    }
    return result;
}
复制代码

上述代码中的类型参数 E 的范围是 <E extends Comparable<? super E>>,我们可以分步查看:

  • 要进行比较,所以 E 需要是可比较的类,因此需要 extends Comparable<...> (注意这里不要和继承的 extends 搞混了,不一样)。
  • Comparable<? super E> 要对 E 进行比较,即 E 的消费者,所以需要用 super。
  • 而参数 List<? extends E> 表示要操作的数据是 E 的子类的列表,指定上限,这样容器才够大。

多个限制

使用 & 符号

public class Client{
    // 工资低于 2500 元的上班族并且独立站立的乘客车票打8折
    public static <T extends Staft & Passenger> void discount(T t){
        if(t.getSalary() < 2500 && t.isStanding()){
            System.out.prinln("恭喜你!您的车票打八折!");
        }
    }
    public static void main(String[] args){
        discount(new Me());
    }
}
复制代码

泛型数组

首先,我们泛型数组的相关的申明:

List<String>[] list1 = new ArrayList<String>[10]; // 编译错误,非法创建。
List<String>[] list2 = new ArrayList<?>[10]; // 编译错误,需要强制转换类型
List<String>[] list3 = (List<String>[])new ArrayList<?>[10]; // ok,但是会有警告
List<?>[] list4 = new ArrayList<String>[10]; // 编译错误,非法创建
List<?>[] list5 = new ArrayList<?>[10]; // ok
List<String>[] list6 = new ArrayList[10]; // ok,但是会有警告 
复制代码

那么通常我们如何用呢?

  • 讨巧的使用场景
public class GenericsDemo{
    public static void main(String args[]){
        Integer i[] = fun1(1,2,3,4,5,6); // 返回泛型数组
        fun2(i);
    }
    
    public static<T> T[] fun1(T... arg){ // 接收可变参数
        return arg; // 返回泛型数组
    }
    
    public static<T> void fun2(T param[]){ // 输出
        System.out.println("接收泛型数组:");
        for(T t : param){
            System.out.print(t + "、");
        }
    }
}
复制代码
  • 合理使用
puplic ArrayWithTypeToken(Class<T> type, int size){
    array = (T[])Array.newInstance(type, size);
}
复制代码

具体可以看后文解释。

深入理解泛型

我们通过泛型背后的类型擦除以及相关的问题来进一步理解泛型。

如何理解 Java 中的泛型是伪泛型?泛型中类型擦除?

java 泛型这个特性是从 JDK1.5 才开始加入的,因此为了兼容之前的版本,java 泛型的实现采取了**“伪泛型”的策略,即 Java 在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”**(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。理解类型擦除对于用好泛型是很有帮助对的,尤其是一些看起来“疑难杂症”的问题,弄明白了类型擦除也就迎刃而解了。

泛型的类型擦除原则是是:

  • 消除类型参数声明,即删除 <> 及其包围的部分。
  • 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为 Onject,如果存在上下界限定则根据子类替换原则取类型参数左边限定类型(即父类)。
  • 为了保证类型安全,必要时插入强制类型转换代码。
  • 自动产生“桥接方法”以保证擦除类型后的代码依然具有泛型的“多态性”。

那么如何进行擦除的呢?

参考自:softlab.sdut.edu.cn/blog/subaoc…

  • 擦除类定义中的类型参数 -无限制类型擦除。

当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为 Object,即形如 和 <?> 的类型参数都被替换为 Object。

image.png

  • 擦除类定义中的类型参数 -有限制类型擦除。

当类定义的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如 和 <? extends Number> 的类型被替换为 Number,<? super Number> 被替换为 Object。

image.png

  • 擦除方法中定义的类型参数

擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限类型参数为例。

image.png

如何证明类型的擦除呢?

我们通过两个例子证明 Java 类型的类型擦除。

  • 元素类型相等
public class Test{
    public static void main(String[] args){
        ArrayList<String> list1 = new ArrayList<String>();
        list1.add("abc");
        ArrayList<Integer> list2 = new ArrayList<Integer>();
        list1.add("123");
        System.out.println(list1.getClass() == list2.getClass()); // true
    }
}
复制代码
在这个例子中,我们定义了两个 ArrayList 数组,不过一个是 ArrayList<String> 泛型类型,只能存储字符串;一个是 ArrayList<Integer> 泛型类型,只能存储整数;最后,我们通过 list1 和 list2 对象的 getClass() 方法获取他们的类的信息,最后发现结果为 true。
由此说明泛型类型 String 和 Integer 都被擦除掉了,只剩下原始类型。
复制代码
  • 通过反射添加其他类型元素
public class Test {

	public static void main(String[] args) throws IllegalAccessException, IllegalArgumentException,
			InvocationTargetException, NoSuchMethodException, SecurityException {
		ArrayList<Integer> list = new ArrayList<>();
		list.add(1); // 这样调用 add 方法只能存储整形,因为泛型的实例为 Integer。

		list.getClass().getMethod("add", Object.class).invoke(list, "abc");

		for (int i = 0; i < list.size(); i++) {
			System.out.println(list.get(i));
		}

	}
}
复制代码
在程序中定义了一个 ArrayList 泛型类型实例化为 Integer 对象,如果直接调用 add() 方法,那么只能存储整数数据,不过当我们利用反射调用 add() 方法的时候,却可以存储字符串,这说明了 Integer 泛型实例在编译之后被擦除掉了,只保留了原始类型。
复制代码

如何理解类型擦除后保留的原始类型?

在上面,两次提到了原始类型,什么是原始类型?

原始类型 就是擦除去了泛型信息,最后在字节码中的类型变量的真正类型,无论何时定义一个泛型,相应的原始类型都会被自动提供,类型变量擦除,并使用其限定类型(无限定的变量用 Object)替换。

  • 原始类型 Object
public class Pair<T> {
	private T value;

	public T getValue() {
		return value;
	}

	public void setValue(T value) {
		this.value = value;
	}
}
复制代码

Pair 的原始类型为:

public Pair{
    private Object value;
    
    public Object getValue(){
        return value;
    }
    
    public void setValue(Object value){
        this.value = value;
    }
}
复制代码

因为在 Pair 中,T 是一个无限定的类型变量,所以用 Object 替换,其结果就是一个普通的类,如同泛型加入 Java 语言之前已经实现的样。在程序中可以包含不同类型的 Pair,如 Pair 或 Pair,但是擦除类型后他们就成为原始的 Pair 类型了。原始类型都是 Object。

在上面的章节,我们可以明白 ArrayList 被擦除类型后,原始类型也变为 Object,所以通过反射我们就可以存储字符串了。

如果类型变量有限定,那么原始类型就用第一个边界类型变量替换。

比如:Pair 这样声明的话:

public class Pair<T extends Comparable>{}
复制代码

那么原始类型就是 Comparable。

要区分原始类型和泛型变量的类型。

在调用泛型方法时,可以指定泛型,也可以不指定泛型。

  • 在不指定泛型的情况下,泛型变量的类型为该方法中的几种类型的同一父类的最小级,直到 Object.
  • 在指定泛型的情况下,该方法的几种类型必须是该泛型的实例的类型或者其子类。
public class Test{
    public static void mian(String[] args){
        /* 不指定泛型的时候 */
        int i = Test.add(1, 2); // 这两个参数都是 Integer,所以 T 为 Integer 类型。
        Number f = Test.add(1, 1.2); // 这两个参数一个是 Integer,一个是 Float,所以取同一父类的最小级,即 Number。
        Object o = Test.add(1, "abc");
        
        /* 指定泛型的时候 */
        int a = Test.<Integer>add(1, 2); // 指定了 Integer,所以只能为 Integer 类型或者其子类。
        int b = Test.<Integer>add(1, 1.2); // 编译错误,指定了 Integer,不能为 Float。
        Number c = Test.<Number>add(1, 2.2); // 指定为 Number,所以可以为 Integer 和 Float。
    }
    
    // 这是一个简单的泛型方法
    public static <T> T add(T x, T y){
        return y;
    }
}
复制代码

其实在泛型类中,不指定泛型的时候,也差不多,只不过这个时候的泛型为 Object,就比如 ArrayList 中,如果不指定泛型,那么这个 ArrayList 可以存储任意的对象。

  • Object泛型
public static main(String[] args){
    ArrayList list = new ArrayList();
    
    list.add(1);
    list.add("abc");
    list.add(new Date());
}
复制代码

如何理解泛型的编译期检查?

既然说类型变量会在编译的时候擦除掉,那为什么我们往 ArrayList 创建的对象中添加整数会报错呢?不是说泛型变量 String 会在编译的时候变成 Object 类型吗?为什么不能存别的类型呢?既然类型擦除了,如何保证我们只能使用泛型变量限定的类型呢?

Java 编译器是通过检查代码中泛型的类型,然后在进行类型擦除,在进行编译。

例如:

public static void main(String[] args){
    ArrayList<String> list = new ArrayList<String>();
    list.add("123");
    list.add(123); // 编译错误
}
复制代码

在上面的程序中,使用 add 方法添加一个整型,在 IDE 中,直接会报错,说明这是在编译之前的检查因为如果是在编译之后的检查,类型擦除后,原始类型为 Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。

那么,**这个类型检查是针对谁的呢?**我们先看看参数化类型和原始类型的兼容。

以 ArrayList 举例子,以前的写法:

ArrayList list = new ArrayList();
复制代码

现在的写法:

ArrayList<String> list = new ArrayList<String>();
复制代码

如果是与以前的代码兼容,各种引用传值之前,必然会出现如下的情况:

ArrayList<String> list1 = new ArrayList(); // 第一种情况
ArrayList list2 = new ArrayList<String>(); // 第二种情况
复制代码

这样是没有错误的,不过会有个编译时警告。

不过在第一种情况,可以实现与完全使用泛型参数一样的效果,第二种则没有效果。

因为类型检查就是编译时完成的,new ArrayList() 只是在内存中开辟了一个存储空间,可以存储任何类型对象,而真正设计类型检查的是它的引用,因为我们是使用它引用 list1 来调用它的方法,比如说调用 add 方法,所以 list1 引用能完成泛型类型的检查。而引用 list2 没有使用泛型,所以不行。

举例子:

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
        
        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); // 编译错误
       
        String str2 = new ArrayList<String>.get(0); // 返回类型就是 String
    }
}
复制代码

通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。

泛型中参数类型为什么不考虑继承关系?

在 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; //编译错误
复制代码

实际上在,在第四行代码的时候就会由于编译错误。那么,我们先假设它编译没错。那么当我们使用 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 转换为 Object。但是这样没有意义,泛型出现的原因就是为了解决类型转换的问题。

我们使用了泛型,如果还需要自己进行强转,这样违背了泛型设计的初衷。所以 Java 不允许这么干。
如果再次向 list2 中添加新的对象,那么 get() 的时候就不明确自己取得是什么类型的值。

所以,需要格外注意泛型中的引用传递的问题。