第8章 泛型程序设计

使用泛型程序设计的原因

泛型程序设计意味着编写的代码可以对多种不同类型的对象重用

定义简单泛型类

class Pair
{
    private T first;
    private T second;

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

    public T getFirst()
    {
        return first;
    }
    public T getSecond()
    {
        return second;
    }

    public void setFirst(T newValue)
    {
        first=newValue;
    }
    public void setSecond(T newValue)
    {
        second=newValue;
    }
}

这里的Pair类引入了一个类型变量T,用<>括起来,放在类名的后面。
类型变量在整个类定义中用于指定方法的返回类型以及字段和局部变量的类型。

Java库的类型变量表示:
  • 变量E表示集合的元素类型
  • K和V分别表示表的键和值的类型
  • T(必要时还可以用相邻的字母U和S)表示“任意类型"
    可以用具体的类型替换类型变量来实例化泛型类型,例如:
Pair

泛型类相当于普通类的工厂

泛型方法

泛型方法可以在普通类中定义,也可以在泛型类中定义。

class ArrayA
{
    public static T getMiddle(T...a)
    {
        return a[a.length/2];
    }
}

类型变量的限定

如果在定义泛型方法时,想要指定T所属的类,可以限制T只能是实现了某种接口的类。可以通过对类型变量T设置一个限定来实现这一点:

public static  T min(T[] a)

这种记法表示T应该是限定类型(bounding type)的子类型(subtype)。T和限定类型可以示类,也可以是接口。
一个类型变量或通配符可以有多个限定,如:
T extends Comparable & Serializable
限定类型用“&”分隔,而逗号用来分割类型变量。
在Java的继承中,可以根据需要拥有多个接口超类型,但最多有一个限定可以是类。如果有一个类作为限定,它必须是限定列表中的第一个限定。

泛型代码和虚拟机

虚拟机没有泛型类型对象——所有对象都属于普通类

类型擦除

无论何时定义一个泛型类型,都会自动提供一个相应的原始类型(raw type)。这个原始类型的名字就是去掉类型参数后的泛型类型名。类型变量会被擦除(erased),并替换为其限定类型(或者,对于无限定的变量则替换为Object)。
原始类型用第一个限定来替换类型变量,如果没有给定限定,就替换为Object。因此,为了提高效率,应该将标签(tagging)接口(即没有方法的接口)放在限定列表的末尾。

Java泛型的转换

编写一个泛型方法调用时,如果擦除了返回类型,编译器会插入强制类型转换。

对于Java泛型的转换,需要记住以下几个事实:
  • 虚拟机中没有泛型,只有普通的类和方法
  • 所有的类型参数都会替换为它们的返回类型
  • 会合成桥方法来保持多态
  • 为保持类型安全性,必要时会插入强制类型转换

限制与局限性

大多数限制都是由类型擦除引起的

不能用基本类型实例化类型参数

例如:没有Pair,只有Pair
其原因在于类型擦除。擦除之后,Pair类含有Object类型的字段,而Object不能存储double值。(这也是为什么之前的泛型数组不能用基本类型的原因)

运行时类型查询只适用于原始类型

虚拟机中的对象总有一个特定的非泛型类型。因此,所有的类型查询只产生原始类型。

不能创建参数化类型的数组

Java不支持泛型类型的数组
数组会记住它的元素类型,如果试图存储其他类型的元素,就会抛出一个ArrayStore-Exception异常。
但是对于泛型类型,擦除会使这种机制无效。
只是不允许创建这些数组,而声明类型为Pair[]的变量仍是合法的。不过不能用new Pair[10]初始化这个变量。

Varargs警告

虽然Java不支持泛型类型的数组,但是可以向参数个数可变的方法传递一个泛型类型的实例。例如:

//这是一个参数个数可变的方法
public static  void addAll(Collection coll,T...ts)
    {
        for(T t : ts) coll.add(t);
    }

在调用这个方法的时候,Java虚拟机必须建立一个Pair数组,这样就违反了前面Java不支持泛型类型的数组的错误。不过对于这种情况,规则有所放松,你只会得到一个警告,而不是错误。

@SafeVarargs
    public static  void addAll(Collection coll,T...ts)
    {
        for(T t : ts) coll.add(t);
    }

@SafeVarargs只能用于声明为static、final或(Java9中)private的构造器和方法。所有其他方法都可能被覆盖,使得这个注解没有什么意义。
可以使用@SafeVarargs注解来消除创建泛型数组的有关限制

不能实例化类型变量

不能在类似new T(...)的表达式中使用类型变量。

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

因为类型擦除会将T变成Object,而你肯定不希望调用new Object()。
Java8之后,最好的方法是让调用者提供一个构造器表达式。

//Supplier是一个函数式接口,表示一个无参数而且返回类型为T的函数
    public static  Pair makePair(Supplier constr)
    {
        return new Pair<>(constr.get(),constr.get());
    }

    //解决方法:让调用者提供一个构造器表达式。
    Pair p = Pair.makePair(String::new);

还可以通过反射调用Constructor.newInstance方法来构造泛型对象。(比较传统的方法)

//通过反射调用Constructor.newInstance方法来构造泛型对象
    public static  Pair makePair2(Class cl)
    {
        try {
            return new Pair<>(cl.getConstructor().newInstance(),
                    cl.getConstructor().newInstance());
        }
        catch (Exception e){
            return null;
        }
    }
Pair p = makePair2(String.class);

不能构造泛型数组

解决方法也类似于构造泛型对象的解决方法
1.让用户提供数组构造器表达式
2.利用反射

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

不能在静态字段或方法中引用类型变量。

不能抛出或捕获泛型类的实例

既不能抛出也不能捕获泛型类的对象
不过,在异常规范中使用类型变量是允许的。

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

/**
 *  这是一个从Task到Runnable的适配器,它的run方法可以抛出任意异常。
 */

public interface Task
{
    void run() throws Exception;

    //这个方法被调用的时候可以取消对检查型异常的检查
    @SuppressWarnings("unchecked")
    static  void throwAs(Throwable t) throws T
    {
        throw (T) t;
    }

    static Runnable asRunnable(Task task)
    {
        return () ->
        {
            try
            {
                task.run();
            }
            catch (Exception e)
            {
                //这里调用方法,编译器会认为e是一个非检查型异常
                Task.throwAs(e);
            }
        };
    }
}
/**
 *  这个程序运行了一个线程,它会抛出一个检查型异常
 *
 *  正常情况下,必须捕获一个Runable的run方法中的所有检查型异常,把它们“包装”到非检查型异常中,
 *  因为run方法声明为不抛出任何检查型异常
 *
 *  在这里我们只是抛出异常,并“哄骗”编译器,让它相信这不是一个检查型异常
 */

public class Test
{
    public static void main(String[] args)
    {
        var thread = new Thread(Task.asRunnable(() ->
            {
                //Thread.sleep 方法声明为抛出一个InterruptedException,我们不再需要捕获这个异常
                Thread.sleep(1000);
                System.out.println("Hello,World!");
                throw new Exception("Check this out!");
            }));
        thread.start();
    }
}

泛型类型的继承规则

无论S与T有什么关系,通常,Pair与Pair没有任何关系。

通配符类型

通配符概念

在通配符类型中,允许类型参数发生变化。
例如:Pair表示任何泛型Pair类型,它的类型参数是Employee的子类,如Pair,但不是Pair
注意,需要区分安全的访问器方法和不安全的更改器方法。

通配符的超类型限定

? super Manager 这个通配符限制为Manager的所有超类型。
直观地讲,带有超类型限定的通配符允许你写入一个泛型对象,而带有子类型想定的通配符允许你读取一个泛型对象。

无限定通配符

Pair

通配符捕获

通配符捕获只有在非常限定的情况下才是合法的。编译器必须能够保证通配符表示单个确定的类型。

通配符测试程序

package pair3;

import pair2.Pair;
import EmployeeUsed.*;

public class PairTest3
{
    public static void main(String[] args)
    {
        var ceo = new Manager("Gus Greedy", 800000, 2003, 12, 15);
        var cfo = new Manager("Sid Sneaky", 600000, 2003, 12, 15);
        var buddies = new Pair(ceo,cfo);
        printBuddies(buddies);

        ceo.setBonus(1000000);
        cfo.setBonus(500000);
        Manager[] managers = {ceo,cfo};

        var result = new Pair();
        minmaxBonus(managers, result);
        System.out.println("first: " + result.getFirst().getName() +
                ", second: " + result.getSecond().getName());
        maxminBonus(managers, result);
        System.out.println("first: " + result.getFirst().getName() +
                ", second: " + result.getSecond().getName());
    }

    //带有子类型限定的通配符允许你读取一个泛型对象
    public static void printBuddies(Pair p)
    {
        Employee first = p.getFirst();
        Employee second = p.getSecond();
        System.out.println(first.getName() + " and " + second.getName() + "are buddies.");
    }

    //带有超类型限定的通配符允许你写入一个泛型对象
    public static void minmaxBonus(Manager[] a, Pair result)
    {
        if (a.length == 0) return;
        Manager min = a[0];
        Manager max = a[0];
        for (int i = 1; i < a.length; i++)
        {
            if (min.getBonus() > a[i].getBonus()) min = a[i];
            if (max.getBonus() < a[i].getBonus()) max = a[i];
        }
        result.setFirst(min);
        result.setSecond(max);
    }

    //带有超类型限定的通配符允许你写入一个泛型对象
    public static void maxminBonus(Manager[] a, Pair result)
    {
        minmaxBonus(a, result);
        PairAlg.swapHelper(result);
    }
}

class PairAlg
{
    // 使用无限定通配符来测试一个对组是否包含一个null引用,这里不需要实际的类型
    // 通过将hasNulls转换成泛型方法,可以避免使用通配符类型,然而可读性较差
    // public static boolean hasNulls(Pair p)
    public static boolean hasNulls(Pair p)
    {
        return p.getFirst() == null || p.getSecond() == null;
    }

    //通配符捕获
    public static void swap(Pair p) {swapHelper(p);}

    //swapHelper方法的参数T捕获通配符
    public static  void swapHelper(Pair p)
    {
        T t = p.getFirst();
        p.setFirst(p.getSecond());
        p.setSecond(t);
    }
}

反射与泛型

泛型Class类

现在Class类是泛型的。例如,String.class实际上是一个Class类的对象。

虚拟机中的泛型类型信息

虽然java虚拟机会进行泛型擦除,但是擦除的类仍然保留原先泛型的微弱记忆。原始的Pair类知道它源于泛型类Pair,尽管一个Pair类型的对象无法区分它是构造为Pair还是Pair.
你可以重写构造实现者声明的泛型类和方法的所有有关内容。但是你不会知道对于特定的对象和或方法调用会如何解析类型参数。

你可能感兴趣的:(第8章 泛型程序设计)