Java基础深度总结:泛型

不怕路长,只怕志短。

1.泛型概述

泛型是Java SE 1.5的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

  • 在Java SE 1.5之前,没有泛型的情况的下,通过对类型Object的引用来实现参数的“任意化”,但要做显式的强制类型转换,且在强制类型转换时可能出现错误。
  • 在Java SE 1.5时,使用泛型可在编译阶段进行编译类型检查,保证了类型转换的安全性,并且所有的强制转换都是自动和隐式的。
public class GenericAndObjectTest {

    public static void main(String[] args) {
         Holder1 h1 = new Holder1("String");
        String str1  = (String) h1.getObj();
        //Integer num1 = (Integer) h1.getObj(); // 运行时报错:ClassCastException

        Holder2<String> h2 = new Holder2<String>("String");
        String str2 = h2.getT();
        //Integer num2 = h2.getT(); // 编译时报错:Incompatible types.
    }
}

class Holder1{

    private Object obj;

    public Holder1(Object obj) {
        this.obj = obj;
    }

    public Object getObj() {
        return obj;
    }
}

class Holder2<T>{

    private T t;

    public Holder2(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }
}

上面的代码中分别定义了一个可以存放Object类型的容器Holder1,和一个使用泛型的容器Holder2,使用泛型的容器Holder2自动进行类型转换,并将可能出现的错误转移到编译时期。

注意:

  • < T >:泛型标识
  • T:泛型类型

Tips:

  • Java泛型是向前兼容的。
  • 泛型的设计初衷是为了减少类型转换错误产生的安全隐患。
2.泛型类型命名规范
  • 通用泛型类型:一般有T、S、U、V等,若需要多个通用泛型,依次定义即可。
  • 集合元素泛型类型:E
  • 映射中泛型类型:映射-键:K;映射-值:V
  • 数值泛型类型:N
3.泛型的定义和使用

泛型按照使用情况可以分为 3 种。

  • 泛型类
  • 泛型接口
  • 泛型方法
3.1 泛型类

泛型类就是具有一个或多个泛型类型的类,定义泛型类时,需要将 < T >泛型标识置于类名后。

class Holder<T>{

    private T t;
    
    public Holder(T t) {
        this.t = t;
    }

    public void setT(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }

}

当你创建泛型类的实例时,通过<>来指定泛型类型,那么 T 就会被“替换”成对应的类型(实际上不是,原因会在类型擦除时讲到)。以Integer为例:

 Holder<Integer> holder = new Holder<Integer>(new Integer(3));
class Holder{

    private Integer t;
    
    public Holder(Integer t) {
        this.t = t;
    }

    public void setT(Integer t) {
        this.t = t;
    }

    public Integer getT() {
        return t;
    }

}

使用泛型类需要注意:

  • T代表任意的类型(Object及其子类),但不能使用基本数据类型作为泛型类型。
  • 不能定义静态泛型类型变量,也不能在静态方法中定义泛型参数。
 private static T t; //cannot be referenced from a static context
 public static void fun(T t){} //cannot be referenced from a static context
3.2 泛型接口

泛型也可用于接口,例如生成器:

interface Generator<T>{
	T next();
}

接口中使用泛型与类中使用泛型没什么区别,需要注意的是,接口中的成员默认是static final的,因此在接口中不能有泛型成员变量。

3.3 泛型方法

泛型方法是被泛型标识修饰的方法,泛型方法的定义格式:

[作用域修饰符] <泛型类型标识> [返回类型] 方法名(参数列表){}

示例:

public class GenericMethodTest<T> {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }
    
    public void fun(T t) {    
    }

    public <E> void fun2(E e) { 
    }
}

注意:fun(T t) 不是泛型方法,fun2(E e)才是。

泛型方法有以下特点:

  • 参数列表中用到的泛型类型,需要在返回类型之前通过<>定义。
  • 泛型方法中的泛型类型只与实参类型有关,而与泛型类中的泛型类型无关。
public class GenericMethodTest<T> {

    private T t;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

    public void fun(T t) {
        System.out.println(t);
    }

    public <E> void fun2(E e) {
        System.out.println(e);
    }

    public <T> void fun3(T t) {
        System.out.println(t);
    }


    public static void main(String[] args) {
        GenericMethodTest<String> gmt = new GenericMethodTest<>();
        gmt.fun("abc");
        //gmt.fun(new Integer(3));// compile error
        System.out.println("--------");
        gmt.fun2("abc");
        gmt.fun2(new Integer(3));
        System.out.println("--------");
        gmt.fun3("abc");
        gmt.fun3(new Integer(3));
    }
}
输出:
abc
--------
abc
3
--------
abc
3

上面的例子充分说明了泛型方法中的泛型类型与泛型类中的泛型类型无关,即使泛型方法中的泛型类型与泛型类中的泛型类型相同。泛型类是通过实例化确定具体类型,泛型方式通过方法调用确定具体类型。

为了避免混淆,它们应该使用不同的泛型类型名称

原则:如果使用泛型方法可以取代整个类泛化,那么就应该尽量使用泛型方法。

4.有界泛型类型

通过有界泛型类型,可以作用于类或接口,限制允许实例化的泛型类型,也可以作用于方法,限制泛型方法实参的泛型类型。

  • 继承父类:< T extends ClassA > 限定该泛型类型必须是ClassA或ClassA的子类。
  • 实现某接口: < T extends InterfaceB > 限定该泛型类型必须是InterfaceB的实现类。
  • 多重边界:< T extends ClassA & InterfaceB & InterfaceC…>

多重边界是根据Java单继承多实现接口的特点来限定的:

  • 可限定多个接口(则必须是同时实现这些接口的实现类),但最多只有一个限定类。
  • 若限定了一个类,则必须是限定列表中的第一个。
class Fu{}
class Zi extends Fu{}
class Sun extends Zi implements I1,I2{}
interface I1{}
interface I2{}

class GenericHolder<T extends Fu>{

    private T t;

    public GenericHolder(T t) {
        this.t = t;
    }

    public void put(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }

    public <E extends Zi> void test(E e) {
        System.out.println(e);
    }

    public <E extends Zi & I1 & I2> void test2(E e) {
        System.out.println(e);
    }
}

class GenericHolder2 <T extends Zi & I1 & I2>{
    
    private T t;

    public GenericHolder2(T t) {
        this.t = t;
    }

    public void put(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }
}

public class GenericBoundedTest {

    public static void main(String[] args) {

        GenericHolder<Fu> holder = new GenericHolder<Fu>(new Zi());
        holder.test(new Zi());
        holder.test2(new Sun());
        GenericHolder2<Sun> holder2 = new GenericHolder2<Sun>(new Sun());
    }
}

有界泛型类型的好处:

  • 在一定程度上限制了泛型类型,提高了程序安全性。
  • 定义了边界,则可以调用泛型类型变量的父类或父接口的方法,更加灵活。

Tip:
无界泛型类型:< T > 等同于有界泛型类型:< T extends Object >,因此在无界泛型类型中,也可以调用泛型类型变量的Object方法。

5.泛型实现原理:类型擦除(重点)

泛型类型擦除:泛型在编译阶段,生成的字节码中不包含泛型类型,只保留原始类型,也就是说泛型类和普通类在 Java 虚拟机中没什么不同。

  • 无界的泛型类型擦除之后的原始类型是Object类型。
  • 有界泛型类型擦除之后的原始类型是父类型
  • 多重有界泛型类型擦除之后的原始类型是第一个父类型
class A<T> {
    private T t;
}

class B<T extends Fu> {
    private T t;
}

class C<T extends I1 & I2>{
    private T t;
}

class Fu{}
class Zi extends Fu{}
class Sun extends Zi implements I1,I2{}
interface I1{}
interface I2{}

public class GenericErasureTest {

    public static void main(String[] args) throws Exception{
        
        // A : 无界泛型
        A<String> a1 = new A<>();
        A<Integer> a2 = new A<>();
        System.out.println(a1.getClass() == a2.getClass());
        System.out.println(a1.getClass().getDeclaredField("t"));
        System.out.println(a2.getClass().getDeclaredField("t"));
        System.out.println("------------");

		// B : 有界泛型
        B<Zi> b1 = new B<>();
        System.out.println(b1.getClass().getDeclaredField("t"));
        System.out.println("------------");
		
		// C :多重有界泛型
        C<Sun> c1 = new C<>();
        System.out.println(c1.getClass().getDeclaredField("t"));
        System.out.println("------------");
    }
}
输出:
true
private java.lang.Object A.t
private java.lang.Object A.t
------------
private Fu B.t
------------
private I1 C.t
------------

Tip:
由于泛型会在编译阶段进行类型擦除,为了保证类型转换的安全性,编译器会在编译前检查类型合法性,最后由编译器自动、隐式的进行强制类型转换。

6.类型擦除的问题

(1)类型擦除导致了有关参数的类型信息全部丢失了,因此下列和运行时的类型信息相关的操作都是不合法的

  • 泛型不可实例化。
  • 不能使用instanceof进行运行时类型检查。
  • 不能创建泛型数组
  • 泛型作为参数不能重载
class Erased<T> {

    private final int size = 10;
    private T t;

    public void test(Object o) {

        T t1 = new T();			 // Error
        if (o instanceof T) {}   // Error
        T[] array = new T[size]; // Error
        List<Integer>[] lists = new List<>[size]; // Error
    }

    void test(List<Integer> list){}
    //Error 类型擦除之后,两个方法是一样的参数列表,无法重载。
    void test(List<Double> list){}
}

(2)利用Java反射可以跳过类型检查

class Holder<T>{

    private T t;

    public Holder() {}

    public void setT(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }
}

class GenericReflexTest{

    public static void main(String[] args) throws Exception{
        Holder<String> holder = new Holder<>();
        Class<Holder> clz = Holder.class;
        Method setT = clz.getDeclaredMethod("setT", Object.class);// 无界泛型类型擦除后的原始类型为Object
        setT.invoke(holder,new Integer(3));

        //String t = holder.getT(); //java.lang.Integer cannot be cast to java.lang.String
        Object o = holder.getT();
        System.out.println(o + "---" + o.getClass());// 3---class java.lang.Integer
    }

}
7.通配符

通配符的作用就是在泛型实例化时,指定泛型中的类型范围。

请看如下代码:Apple 继承自 Fruit

List<Apple> appleList = new ArrayList<>();
List<Fruit> fruitList = appleList; //Error : Incompatible types

虽然 Fruit 是 Apple 的父类,但 List< Fruit > 和 List< Apple >之间却没有继承关系,所以示例代码编译不通过,但有时就是想让两个容器之间产生联系,通配符正是起到这样的作用。

Java泛型中的通配符有三种:

  • 无边界通配符:< ? >
  • 上边界通配符:< ? extends T >
  • 下边界通配符:< ? super T >

Tip:
通配符和有界泛型类型十分相似,它们最大的区别就是,通配符用于泛型实例化时指定泛型中的类型范围;而有界泛型类型用于定义泛型类、接口或者方法,是用来限定泛型类型的。

7.1 上边界通配符:< ? extends T >
/**
 *              Fruit
 *            /       \
 *         Apple     Orange
 */
class Fruit{
}

class Apple extends Fruit{
}

class Orange extends Fruit{
}

class Holder<T>{

    private T t;

    public Holder() {}

    public void setT(T t) {
        this.t = t;
    }

    public T getT() {
        return t;
    }
}

public class GenericWildcardTest {

    public static void main(String[] args) {

        Holder<Apple> appleHolder = new Holder<>();
        appleHolder.setT(new Apple());

        Holder<Orange> orangeHolder = new Holder<>();
        orangeHolder.setT(new Orange());

        Holder<? extends Fruit> fruitHolder;

        fruitHolder = appleHolder; //OK

        fruitHolder = orangeHolder; //OK
    }
}

使用上边界通配符使得 Holder<? extends Fruit>成为了Holder< Apple >和 Holder< Orange >的基类。

7.2 无边界通配符:< ? >

无边界通配符:< ? > 等同于 上边界通配符:< ? extends Object >

7.3 下边界通配符:< ? super T >
public class GenericWildcardTest {

    public static void main(String[] args) {

        Holder<Apple> appleHolder = new Holder<>();
        appleHolder.setT(new Apple());

        Holder<Orange> orangeHolder = new Holder<>();
        orangeHolder.setT(new Orange());

        Holder<Fruit> fruitHolder = new Holder<>();
        fruitHolder.setT(new Fruit());

        Holder<? super Apple> superAppleHolder;
        superAppleHolder = appleHolder;     // OK
        superAppleHolder = fruitHolder;     // OK
        //superAppleHolder = orangeHolder;  //Error : Incompatible types

    }
}

与上边界通配符不同,Holder<? super Apple>表示,Holder< Apple > 及其父类是 Holder<? super Apple>的基类,而 Holder< Orange >不是。

7.4 上下边界通配符的副作用
  • 上边界通配符:可以从父类型获取数据(向上转型),不能写入数据。
  • 下边界通配符:可以通过子类型写入数据(向上转型),通过Object获取数据。

Tip:

  • 如果你需要一个只读容器,用它来生产 T,那么使用? extends T。
  • 如果你需要一个只写容器,用它来消费 T,那么使用 ? super T。
  • 如果需要同时读取以及写入,那么我们就不能使用通配符了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值