第十五章 泛型(干货)

泛型这一章不知道是因为翻译的原因还是什么,感觉《编程思想》讲的混乱无比。花了很多的时间研究泛型,现在力求用最简单的语言,最简明的示例把这一章说清楚。

前言

还是要来区分一下细节的概念,对理解泛型影响还是蛮大的。

1、<T>与不在<>中的T的区别

class A <T> {
    <T> T getT(T t) {
        return t;
    }
}
public class Demo1 {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        A<String> a = new A<String>();
        String s = a.getT("test");
        System.out.println(s);
    }

}

 输出结果:

test

结果分析:<T>这里的T叫类型参数,是在定义泛型方法、泛型类时使用的,它本质是参数。

getT(T t):这里面的T称为参数化类型,它是将原本的类型参数化了。

从中还能够看出,泛型方法与泛型类是相互独立的。泛型类中的T,在泛型方法中不可见。

2、[T、E、K、V]

其实类型参数用什么字母表示都无所谓,高兴的话可以A<WWW>,如果同学们看JDK文档的话,了解这些字母的含义还是有益处的。

T:代表一般的任意类

E:代表元素(Element),或者异常(Exception)

K:代表键(Key)

V:代表值(Value)

3、介绍一些学习泛型会用到的,但是jdk文档说的很让人迷惑的接口

(1)ParameterizedType 参数化类型,表示例如、Collection<String>。

(2)TypeVariable 就是类型参数,也就是Collection<E>和Map<K,V>中的E,K和V。

(3)GenericDeclaration 泛型声明,只有实现了这个接口的类才能够声明泛型,事实上,只有泛型方法(包括构造方法),泛型类。

  也就只有Class,Constructor,Method三个类实现了此接口。

如果前言很迷或也没有关系,有个印象就好。笔者也是从小白开始学习的,后文力求把令人困惑的地方说清楚。

一、泛型类与泛型接口

接口就是极度抽象的类,所以泛型类与泛型接口在本质上没有差别。

1、示例一:通过一个简单的实例了解泛型类与接口的用法

interface Interface<T> {
    void printClass(T t);
}

class Base1<T> implements Interface<T> {
    public void printClass(T t) {
        System.out.println(t.getClass().getName());
    }
}

class Base2 implements Interface<Integer>  {
    public void printClass(Integer t) {
        System.out.println(t.getClass().getName());
    }
}
public class Demo2 {

    public static void main(String[] args) {
        
        Base1<String> b1 = new Base1<String>();
        Base2 b2 = new Base2();
        b1.printClass("test");
        b2.printClass(1);
    }

}
View Code

 输出结果:

java.lang.String
java.lang.Integer

结果分析:

从这里可以看到继承或是实现一个泛型类的方式,基类带有类型参数<T>则子类比带,基类不带则子类不带。

二、泛型方法

1、示例一:静态方法要使用泛型能力必须成为泛型方法。

class C {
    static <T> String getNmae(T t) {
        return t.getClass().getName();
    }
}

public class Demo3 {
    public static void main(String[] arg) {
        C c = new C();
        System.out.println(C.getNmae(c));
    }

}
View Code

 输出结果:

chapter15.C

结果分析:静态方法是不可以访问所属泛型类的类型参数的,要拥有泛型能力必须成为泛型方法。如前言中所述,泛型方法与泛型类是相互独立的。如果泛型方法与泛型类类型参数同名,则泛型类的类型参数在泛型方法中不可见,判断是以泛型方法为主。泛型方通常不需要指明类型参数的实际类型,编译器会进行类型参数推断为我们找出实际类型。在特殊的时候也可以显示声明,比如,Arrays.<Integer>asList(1,2,3);。

三、类型擦除

1、类型擦除的定义

类型擦除:泛型的信息只存在于编译阶段,在进入虚拟机之前,与泛型相关的信息会全部被擦除。

通俗的讲,在虚拟机内部,泛型类、泛型方法与普通类、普通方法没有什么区别。泛型的引入也是为了兼容以前老旧的代码和类库。

2、类型擦除的解析

(1)一个经典问题

List<String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
        
System.out.println(l1.getClass() == l2.getClass());

这个程序输出的结果是true,原因是类型擦除。

让我们来通过RTTI看一下l1与l2在虚拟机中的类型是什么

import java.util.*;
public class Demo5 {

    public static void main(String[] args) {
        
        List<String> l1 = new ArrayList<String>();
        List<Integer> l2 = new ArrayList<Integer>();
        System.out.println("l1.getClass().getName() : " + l1.getClass().getName());
        System.out.println("l2.getClass().getName() : " + l2.getClass().getName());
    }

}

 输出结果:

l1.getClass().getName() : java.util.ArrayList
l2.getClass().getName() : java.util.ArrayList

结果分析:可以看到,l1与l2在虚拟机中没有任何的泛型信息,与普通的类引用是一样的。

(2)泛型类内的T在虚拟机中究竟代表什么讯息

import java.lang.reflect.*;

class Erasure<T> {
    T t;
}
public class Demo6 {

    public static void main(String[] args) {
        Erasure<String> e = new Erasure<String>();
        Field[] f = e.getClass().getDeclaredFields();
        System.out.print("FiledName : " + f[0].getName());
        System.out.println(" FieldType : " + f[0].getType());
    }

}

 输出结果:

FiledName : t FieldType : class java.lang.Object

结果分析:可以看到T在虚拟机中的类型是class java.lang.Object而不是String。

 总结:现在我们可不可这样说,在虚拟机中,Erasure<String>被擦除成原生类型Erasure,Erasure泛型类内的T被擦除成Object。这么说没有问题,但是少了一种情况,边界。

四、泛型引发的动作

class A <T> {
    T t;
    void set(T t1) {
        t = t1;
    }
    T get() {
        return t;
    }
}
public class Demo1 {

    public static void main(String[] args) {
        // TODO Auto-generated method stub
        A<String> a = new A<String>();
        a.set("test");
        String s = a.get();
        System.out.println(s);
    }

}

 输出结果:

 test

 结果说明:上文不是说过类型擦除会将类内部的T擦成Object吗,但是这里get()返回的是确切的类型String啊。原因是编译器在编译这部分代码时,会在get()方法调用之后,插入类型转换(String)Object,将Object转换成String。泛型引发的动作,即为对传进来的值进行额外的编译期检查,对传出去的值进行转型。

五、边界

同学们鸭,千万千万不要把边界与通配符的上下限弄混淆了呀。区别放到后面说哦。

1、边界基本用法

边界:用于限定传递给类型参数T的类型,<T extends X>X以及X的子类可以传递给类型参数T,用于定义泛型方法、泛型类。

将上面代码稍改一下:

class Erasure<T extends Number> {
    T t;
}
public class Demo6 {

    public static void main(String[] args) {
        Erasure<Integer> e = new Erasure<Integer>();
        Field[] f = e.getClass().getDeclaredFields();
        System.out.print("FiledName : " + f[0].getName());
        System.out.println(" FieldType : " + f[0].getType());
    }

}

输出结果:

FiledName : t FieldType : class java.lang.Number

结果分析:惊了,这里居然有类型信息了。事实上,泛型擦除是擦除到上边界的,这里擦除到了Number。Object是所有类的基类,所以Erasure<T>类内部的T擦除到了上边界Object。注意,泛型中不准Erasure<T super Integer>这样定义泛型类的,没有下边界这一说。

 这下咱们可以下结论了

总结:在虚拟机中,Erasure<T>被擦除成Erasure(原生类型),Erasure<T>类内部的T被擦除到上边界。

六、通配符"?"

1、无限定通配符

1.1无限定通配符的引入

这样写会报编译错误吗?

class Base {}
class Derived extends Base{}

List<Derived> l1 = new ArrayList<Base>();
List<Base> l2 = l1;

解释说明:肯定报编译错误。因为List<Base>根本就不是List<Derived>的基类,在编译器看来,他们根本就是不同的类型。类型擦除以后,在虚拟机中他们就是一样的类型。但是在实际编程中确实有这样的需求,想要一种引用可以指向多种类型的对象。所以就引入了通配符。

List<?> l = l1;
l = l2;

 这样写就都可行。

1.2无限定通配符的限制

示例:

import java.util.*;

public class Demo4 {

    public static void main(String[] args) {
        List<?> l = new ArrayList<Integer>(Arrays.asList(1,2,3,4,5));
        //l.add(6);//报参数不合适的错误
        l.add(null);
        //Integer i = l.get(0);//报类型不匹配
        Object o = l.get(0);
        System.out.println(o);
        System.out.println(l);
        System.out.println("l.size() = " + l.size());
    }

}

 输出结果:

1
[1, 2, 3, 4, 5, null]
l.size() = 6

结果分析:由于"?"代表未知的类型,所以List<?>容器中存放的就是指向未知类型的引用,故而向容器中添加任何类型的数据都是禁止的。但,任何的引用都可以赋值为null,所以添加null值是准许的。同样道理,从List<?>容器中读出来的也是未知类型,所以使用Object对象才能够接收。这里的未知类型是针对编译器而言的,虚拟机是知道实际类型的,报错指的也是通不过编译。所以可以借助反射,绕过编译器去做很多编译器不准的事。

1.3"T"与"?"的区别

T:是一个参数

?:通配符,但终究代表一个具体类型

class Hold<T> {}

Hold<?> h;

 分析:T相当于一个参数,用于定义泛型类、泛型方法。可以将?传递给T,上限通配符,下限通配符是一样的道理。

2、有上限的通配符

2.1有上限通配符<? extends X>

 示例:

import java.util.*;

class Father {}
class Son extends Father {}

public class Demo7 {

    public static void main(String[] args) {
        List<? extends Father> l = new ArrayList<Father>(Arrays.asList(new Son()));
        Father f = new Father();
        //l.add(f);//编译错误,参数类型不合适
        l.add(null);
        f = l.get(0);
        System.out.println(f);
    }

}

 输出结果:

 chapter15.Son@22f71333

结果分析:还是想要在这里强调,通配符虽然可以代表很多类型,但是在引用赋值以后,其实就是一种具体类型了。(就像支付宝集五福,万能福变成敬业福)。谈论的泛型的类型都是针对编译而言的,虚拟机根本就不知道泛型。上限通配符也是不能够写入的,道理同上述。但是它可以读出,List<? extends Father>这样写,就是在告诉编译器,List容器中的引用类型肯定是Father的子类,用一个Father类型的引用肯定能够接受List方法的返回值。

 3、有下限的通配符

3.1有下限的通配符<? super X>

class Father {}
class Son extends Father {}

public class Demo7 {

    public static void main(String[] args) {
        List<? super Son> l = new ArrayList<Father>(Arrays.asList());
        Son s = new Son();
        //l.add(new Father());//编译错误类型不合适
        l.add(s);
        l.add(null);
        Object o = l.get(0);
        System.out.println(l);
        System.out.println(o);
    }

}

 输出结果:

[chapter15.Son@22f71333, null]
chapter15.Son@22f71333

结果分析:可以看到下限通配符有了写的能力,List<? super Son>这样写是在告诉编译器,List中的引用一定是Son及其祖先类型的,放心把Son及其子类写入Llist。编译器并不知道从此List中读出数据的类型,只能用Object接。

4、边界与通配符的区别

边界<T extends X>:在定义泛型类,泛型方法时使用,用于限制传入类型参数T的数据类型必须是X及其子孙型。

通配符:在声明域时使用,是一个具体的类型,<? extends X>用于限定传递给引用的对象其泛型类型必须是X及其子孙型,其余同理。

import java.util.*;

class Father {}
class Son extends Father {}
class Hold<T extends Father> {
    List<? extends T> l;
    Hold(List<? extends T> l1) {
        l = l1;
    }
    List<? extends T> get() {
        return l;
    }
}
public class Demo7 {

    public static void main(String[] args) {
        List<Son> l = new ArrayList<Son>(Arrays.asList());
        Hold<Son> h = new Hold<Son>(l);
        Son s = new Son();
        l.add(s);
        l.add(null);
        List<?> l2 = h.get();
        System.out.println(l2);
    }

}

 输出结果:

[chapter15.Son@3498ed, null]

结果分析:这个示例有点绕,但还是能够体现出他们的区别。同样这里也体现了extends在不同环境下的不同含义。

 七、擦除的补偿

泛型的擦除会产生很多的限制,所以要采取一些措施来弥补这些缺陷。

1、缺陷:任何在运行时需要知道确切类型信息的操作都无法完成

class Erasure<T> {
    public static void f(Object o) {
        if(o instanceof T) { //Error
            T var = new T(); //Error
            T[] array = new T[10]; //Error
            T[] array1 = (T[])new Object[10]; //Unchecked warning
        }
    }
}

 2、对o instanceof T进行补偿(换一种方式实现此功能)

public class Demo8 {

    public static void main(String[] args) {
        Integer i = 1;
        System.out.println(Integer.class.isInstance(i));
    }

}

结果分析:可以使用Class对象自带的动态的isInstance()方法。

3、创建类型实例

class ClassAsFactory<T> {
    T x;
    T create(Class<T> kind) {
        try {
            x = kind.newInstance();
            System.out.println("Create succeeded");
            return x;
        }catch(Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}
public class Demo8 {

    public static void main(String[] args) {
        ClassAsFactory<Demo8> c = new ClassAsFactory<Demo8>();
        c.create(Demo8.class);
    }

}
View Code

 结果分析:还是通过Class对象的newInstance()方法创建。

4、创建泛型数组

原则上是不能够创建泛型数组的,数组会跟踪自己的实际类型,类型擦除以后数组没办法跟踪自己的实际类型。

成功创建泛型数组的唯一方式:创建一个被类型擦除的数组,然后对其转型。

T[] array = (T[]) new Object[10];

 事实上,这样会得到一个Unchecked警告。

八、泛型中要注意的问题

1、基本类型是不能够传递给类型参数的

List<int> \\错误

 2、一个类不可实现同一个泛型接口的两种变体

interface Payable<T> {}
class Employee implements Payable<Employee> {}
class Hourly extends Employee implements Payable<Hourly> {}

 编译通不过

3、转型警告

(T)object

 4、重载

void f(List<A> l) {}
void f(List<B> l) {}

 由于类型擦除的原因,List<A>与List<B>是同样类型,虚拟机没办法根据参数区别重载方法。

5、自限定类型

class SelfBounded<T extends SelfBounded<T>> {}
class A extends SelfBounded<A> {}

猜你喜欢

转载自www.cnblogs.com/mgblogs/p/11487990.html