第8章 泛型程序设计

本文详细探讨了Java中的泛型,包括自定义泛型类和方法、泛型与虚拟机的关系、泛型的限制和局限性,以及泛型类型的继承规则。通过类型参数和通配符的使用,阐述了泛型在类型检查、多态和类型安全中的作用,同时揭示了类型擦除对泛型的影响,如桥方法的生成。并讨论了泛型在数组、类型变量限制等方面的问题和解决方案。
摘要由CSDN通过智能技术生成

8.1 自定义泛型类和泛型方法

Java5中,泛型的引入成为Java程序设计语言发行以来最显著的变化。Java引入泛型类之前,泛型程序设计是用继承实现的。泛型类维护一个Object的引用,使用的时候进行强制类型转换。

这种方法带来的问题主要有两个:

  1. 当获取一个值时必须进行强制类型转换。
  2. 没有类型检查,可以向泛型类里添加任何类型。只有当运行的时候才会报错。

泛型提供了一个很好的解决方案:类型参数。

泛型类就是有一个或者多个类型变量的类。泛型方法就是有一个或者多个类型变量的方法。

在Java中,使用变量E表示集合的元素类型。使用K和V分别表示表的键和值的类型。使用T、U、S表示任意类型。

泛型类或者方法定义的时候,可以采用关键字extends对泛型进行限定。要求传入泛型类或者方法的类型必须是某个接口或者某个类的子类。当有多个限定类或者接口的时候,使用&连接。

下面是一个简单的泛型程序:

import java.io.Serializable;
import java.util.Objects;

public class UniversalTest {
    public static void main(String[] args){
        //泛型方法的调用可以直接像常规方法一样,也可以用<T>指明类型  <T>functionName(T args)
        System.out.println(minMax("string", "b", "c", "d"));
        var r =minMax(1.0, 2.0, 5.12, 10.0);
        System.out.println(r);
    }

    //泛型方法的声明格式是:public static <T> T FunctionName(T args)
    //<T>表明这是一个泛型方法  T是返回值    args是泛型参数
    //下面这个泛型方法含参数较多。注意分析,因为返回的UniversalPair类中包裹的泛型T必须实现那两个接口,所以你传进来的泛型T也要实现那两个接口。
    //所以这里的三个T表达的是同一种类型。
    public static <T extends Comparable & Serializable> UniversalPair<T> minMax(T ...a){
        if (a == null || a.length == 0) return null;
        T min = a[0];
        T max = a[0];
        for (T aa : a){
            if (min.compareTo(aa) > 0) min = aa;
            if (max.compareTo(aa) < 0) max = aa;
        }
        return new UniversalPair<>(min, max);
    }
}

//定义一个泛型类时要在后面用<T>指明该类为泛型类
class UniversalPair<T extends Comparable & Serializable> implements Comparable, Serializable{

    private T first = null;
    private T second = null;

    public UniversalPair(T first, T second) {
        this.first = first;
        this.second = second;
    }

    public T getSecond() {
        return second;
    }

    public void setSecond(T second) {
        this.second = second;
    }

    public T getFirst() {
        return first;
    }

    public void setFirst(T first) {
        this.first = first;
    }


    @Override
    public int compareTo(Object o) {
        var no = (UniversalPair) Objects.requireNonNull(o);
        if (this.getFirst().compareTo(no.getFirst()) > 0) return 1;
        else if (this.getFirst().compareTo(no.getFirst()) < 0) return -1;
        else if (this.getSecond().compareTo(no.getSecond()) > 0) return 1;
        else if (this.getSecond().compareTo(no.getSecond()) < 0) return -1;
        else return 0;
    }

    @Override
    public String toString() {
        return "UniversalPair{" +
                "first=" + first +
                ", second=" + second +
                '}';
    }
}

8.2 泛型代码和虚拟机

虚拟机没有泛型类型对象——所有的对象都属于普通类。当传入泛型类时,虚拟机编译时会把没有限定的泛型参数<T>转换成Object对象,这叫做类型擦除。会将有限定的<T extends A & B>转换成A对象。注意虚拟机对于有限定的泛型类型,总是会在编译的时候把泛型转换成第一个限定类型。因此,对于含有标记型接口的限定类型的泛型,要把标记接口放到最后。不然在运行的时候,虚拟机还得将标记接口的类进行强制类型转换成非标记接口的类。

例如,Pair<T>在虚拟机编译后的原始类型如下:

public class Pair{
    private Object first;
    ...
    public Pair(Object first, Object second){
        this.first = first;
        this.second = second;
    }
    ...
}

这个时候如果出现泛型表达式,如:

Pair<Empliyee> buddies= ...;
Employee buddy = buddies.getfirst();

在虚拟机编译时会进行类型擦除,并加入类型转换。

Pair buddyies = ...;
Employee buddy = (Employee) buddies.getfirst();

当获取一个泛型属性的时候,也会进行强制类型转换。

再比如以下泛型方法:

public static <T extends Comparable> T min(T[] a)

在虚拟机编译进行类型擦除后会变成:

public static Comparable min(Comparable[] a)

类型擦除也会带来多态实现上的问题,解决方法是引入一个桥方法。例如以下代码:

class DateInterval extends Pair<LocalDate>{
    public void setSecond(LocalDate second)
        ...
}

这段代码类型擦除后变成:

class DateInterval extends Pair{
    public void setSecond(LocalDate second)
        ...
    public void setSecond(Object second)
}

为什么会出现一个setSecond(Object second)方法?

这个方法是一个桥方法,因为泛型导致了子类继承的时候没法确定重写方法的类型。这样在发生多态调用的时候就没法调用子类重写的方法。而引入桥方法之后就完美的解决了这个问题,可以在子类的桥方法中调用重写后的方法。这样看来桥方法才是父类的相同方法的重写方法。

桥方法在方法重写的时候也有应用。例如,子类重写父类方法的时候,方法的返回值类型可以使用父类返回值类型的子类,这个内部就是用桥方法实现的。

总之,Java的泛型转换有以下几点:

  • 虚拟机中没有泛型,只有普通的类和方法。
  • 所有的类型参数都会替换成他们的限定类型
  • 使用桥方法来保持多态
  • 为了保持类型的安全性,必要时会插入强制类型转换。

8.3 泛型的限制和局限性

Java泛型使用的时候需要慎重的考虑限制和局限性。大多数限制和局限性都是类型擦除引起的,所以认真的考虑类型擦除很重要。

  1. 不能用基本类型实例化泛型类的泛型参数T

    考虑Pair<int>类型擦除后会变成Pair<Object>。而int是基本类型,其不是Object的子类。但是Pair<Int>是合法的。

  2. 不能实例化类型变量T

    考虑以下代码:

    public Pair(){
        first = new T();
        second = new T();
    }
    

    类型擦除之后都变成了new Object(),显然这不是我们想要的结果。

    Java9之后的解决方法是让调用者提供一个构造器表达式。

    public static <T> Pair<T> makePair(Supplier<T> constr){
        return new Pair<>(conster.get(), conster.get());
    }
    

    而调用的时候可以传入一个构造器引用:

    Pair<String> p = Pair.makePair(String::new)
    
  3. 运行时类型查询只能用于原始类型。

    • 考虑a instanceof Pair<T>或者a instanceof Pair<String>,其类型擦除之后都变成了Pair,所以类型检查只可用于查看a是否是Pair的子类。
    • 另一个T.getClass或者T.Class的使用,这两个在Java类型擦除之后都变成了Object.Class。包括Pair<String>.Class也是没有意义的,类型擦除之后都是Pair.Class。
  4. 不能创建泛型类的数组

    考虑var table = new Pair<String>[10]类型擦除之后会变成var table = new Pair<String>[10]数组table是Pair类型的,但是其内部存储的T类型的值全部擦除成了Object。这样会导致数组记住他的元素类型是Object,如果再存入其他类型就会抛出ArrayStore-Exception类型。

  5. 不能创建类型变量T的数组

    如同不能实例化类型变量T一样,也不能创建类型变量T的数组。如果要创建可以参考实例化类型变量T一样传入一个数组构造器String[]:new,Java的数组类型也都是Object的实例。

  6. Varargs警告

    考虑可变参数列表的方法function(T …a),其内部实现是将a变成了数组的形式传给了方法。由于T是泛型参数,如果传递String等非泛型类是没有问题的。但是当传递泛型类例如Pair<String>就会创建泛型类的数组,这几产生了之前提到的问题。不过对于这种情况编译器的规则会比较放松,会返回一个警告而不是错误。

    可以采用两种方法抑制这个警告:

    • 使用注解@SuppressWarnings(“unchedked”)抑制警告。
    • 使用注解@SafeVargrs注解方法,对于任何只需读取参数数组元素的、声明中有static、final或(Java9)private的方法,都可以使用这个注解。

    注意补充一点,所有的类名型的调用Pair<T>都会在编译器中擦除为Pair,而所有参数类型的Pair<T>都会在编译器中擦除为Pair<Object>。

    判断擦除之后的结果,关键是看这个调用的含义。

  7. 泛型类的静态上下文中类型变量无效

    不能在静态字段或方法中引用类型变量T。因为编译器擦除之后都会是Object,而且静态字段和方法是在类加载的时候就创建的,之后就不可改变了。所以之后的调用相当于全程在使用Object类型。

    public class Singleton<T>{
        private static T singleInstance;  //error
        public static getSingleInstance(){  //error
            ...
        }
    }
    
  8. 不能抛出活捕捉泛型类的实例

  9. 可以取消对检查型异常的检查

  10. 注意擦除后的冲突

8.4 泛型类型的继承规则

泛型的继承规则注意两点:

  • 如果A是B的父类,Pair<A>和Pair<B>无父子关系,两者是没有区别的类。
  • 泛型的原始类型可以继承,例如List<T>是ArrayList<T>的父类。

8.5 通配符––“?”

8.5.1 带限定的通配符

已经有泛型了,类型变量T可以表示所有类型,为什么又要引入通配符“?”呢?

类型参数T不管怎么说,他都是代表一种类型,在方法调用的时候一旦确定了就不能改变了。为了解决这一问题,所以引入了通配符,允许类型参数发生变化。

假设A是B的父类,那么通配符语法Pair<? extends A>,这个表示?是所有A的子类。它的方法如下:

? extends A getFirst()  //返回类型为A
void setFirst(? extends A)  //error

这样将不可以调用setFirst,编译器只知道是A的子类型,但是不知道具体是什么类型,所以编译器拒绝传递任何特定的类型。

而getFirst就不一样了,将getFirst的返回值赋给一个A的引用是完全合法的。

同样,对于通配符语法Pair<? super B>这个表示?是B的所有超类。

? super B getFirst()  //返回值为Object
void setFirst(? super B)  //传入类型必须为B

这样当调用getFirst的时候,返回值赋给B的超类,但是不知道赋给B的哪一级别的超类,因为可能是多层次继承关系,最终只能赋值给Object类。

而对于setFirst就不一样了,传入的是B的超类,但是不知道具体的类型。所以只能接受参数类型为B的对象。

综上

  • 带有超类型限定的通配符super,允许你写入一个泛型对象。
  • 带有子类型限定的通配符extends,允许你读取一个泛型对象。

8.5.2 无限定通配符

还可以使用根本无限定的通配符。

同样的分析方法,分析Pair<?>,它有以下方法:

? getFirst()
void setFirst(?)

getFirst的返回值会赋给一个Object对象,而setFirst则完全不能调用。

8.5.3 通配符捕获

通配符不是类型变量,因此,不能在编写代码中使用“?”作为一种类型。

下面是一种通配符捕获技巧解决的通用交换对组元素的方法。

public static void swap(Pair<?> p){
    swapHelper(p);
}

public static <T> void swapHelper(Pair<T> p){
    T t = p.getFirst();
    p.setFirst(p.getSecond());
    p.setSecond(t);
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值