Loading

【CoreJava】泛型程序设计

8 泛型程序设计

8.1 泛型的发生

在泛型之前,大家为了复用代码,使用的是继承机制,以 ArrayList 举例,最开始:

public class ArrayList {
    private Object[] elementData;
    public Object get(int i);
    ...
}

这种做法有两个问题:

  1. 获取的值必须强转
  2. 没有类型检查,任何类型的对象都可以往里面塞

为了解决这两个问题,引入了类型参数,产生了泛型:

ArrayList<String> files = new ArrayList<>();

8.2 定义泛型类

以下泛型类引入了类型变量 T,并用尖括号包围起来,一个泛型类可以有多个类型变量。

一般的,类型变量都为大写形式,E 表示集合的元素类型,K 和 V 分别表示表的关键字与值的类型,T、U 和 S 表示任何类型。

public class Pair<T> {
   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 first) {
      this.first = first;
   }

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

8.3 泛型方法

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

定义泛型方法就像定义泛型类一样,将类型参数放在修饰符的后面即可。

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

调用时只需要指名具体的类型,绝大多数情况下不需要指名,编译器能够推断出类型:

String middle = ArrayAlg.<String>getMiddle(words);

如果所给定的参数不是统一的,那么编译器将会自动找寻它们的公共超类(类或者接口)。

8.4 限定类型变量

泛型的出现可以实现代码的复用,但是,有些代码往往仅限于一部分类型的元素使用,比如:

public static <T> T min(T[] a) {
    if (a null || a.length = 0) return null;
    T smallest = a[0];
    for (int i = 1; i < a.length; i ++)
        if (smallest.compareTo(a[i ]) > 0) smallest = a[i ];
    return smallest;
}

上述方法仅仅适用于实现了 Comparable 的元素类型 T,但 Client 往往不会注意这些(除非注解说明)。为了解决这个问题,引入类型限定,将方法签名更改成:

pulic static <T extends Comparable & Serializable> T min(T[] a)

其中,& 符号用来表示多重限定的并列,逗号则用来分隔类型变量。

8.5 泛型代码和虚拟机

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

8.5.1 类型擦除

如果泛型类的泛型参数(泛型变量)存在限定(extends),那么类将会被擦除还原成对应的限定,如果没有,则被擦除为 Object。

当调用泛型方法时,编译器将会自动插入类型转换,比如

String first = myPair.getFIrst();

将会被这样执行(大致原理):

Object object = myPair.getFIrst();
String first = (String) object;

桥方法,就是一个类继承自一个泛型类方法,因为类型擦除的原因,可能会与多态发生冲突。

8.6 约束和局限性

不能用基本类型实例化类型参数,不能创建 Pair<int> 的对象,因为类型擦除后的 Object 不能引用 int 基本类型变量。

使用 instanceof 判断一个对象是否是泛型类型将会抛出编译器错误,而对泛型使用强制类型转换则会得到警告:

if (a instanceof Pair<String>)
if (a instanceof Pair<T>)
    
Pair<String> p = (Pair<String>) a	// warning
    
stringPair.getClass() == employeeClass.getClass();	// equal

无法初始化数组泛型类型数组,但是可以声明变量,这是因为数组需要一个明确的数据类型,但是类型擦除后的类型是 Object,可以存储任意类型的对象,导致数组抛出 ArrayStoreException 的异常。所以我们往往使用 List 来收集参数化类型对象:

Pair<String>[] stringPairs;	// OK
new Pair<String>[10];	// NO!

@SafeVarargs 注解来压制多参方法的问题。

public static <T> void addAll(Collections coll, T... ts) {
	for (t : ts) coll.add(t);
}

// 调用
Col1ection<Pair<String» table = . . .;
Pair<String> pairl = . . .;
Pair<String> pair2 = . .
addAll (table, pairl, pair2);

这时候,系统就不得不为我们创建 Pair<String> 类型的数组,前面又说道 Java 不允许创建参数化类型对象数组,虽然这种调用不会导致系统抛出异常,但是会收到警告,为了压制警告,可以使用两种注解:

@SuppressWamings("unchecked")
@SafeVarargs

不能实例化类型变量。

下面的构造器就是错误的,不能使用类型变量 T 来作为表达式的成分,因为类型擦除后,T 就变成了 Object,不符合我们原本的愿望。

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

而最好的解决办法就是提供一个构造器表达式:

Pair<String> p = Pair.makePair(String::new);

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

makePair 方法接收一个Supplier<T>, 这是一个函数式接口, 表示一个无参数而且返回类型为 T 的函数。为了得到类型变量的对象,可以使用反射的方法来实现,但这种奇技淫巧还是不用为好:

public static <T> Pair<T> makePair(Class<T> cl) {
    try {
        return new Pair<>(cl.newInstance, cl.newInstance);
    } catch (Exception ex) {
        return null;
    }
}

Pair.makePair(String.class)

String.class 将会生成 Class<String> 的实例,进一步生成 Pair<String> 的实例。

8.7 泛型类型的继承规则

无论 S 和 T 有什么联系,Pair<S> 和 Pair<T> 是没有联系的!除非泛型类之间就已经存在了继承实现关系,比如 ArrayList<T> 实现了 List<T>,那么 ArrayList<String> 就可以被 List<String> 引用。

8.8 通配符类型

泛型虽好,倘若一定需要指定类型变量来使用泛型(没有 8.7 中提到的更为直接的继承关系),那就不是那么友好,因此引入了通配符类型。

8.8.1 通配符概念

public static void printBuddies(Pair<Employee> p) {
    Employee first = p.getFirst();
    Employee second = p.getSecond();
    Systefn.out.println(first.getName() + " and " + second.getNameQ + " are buddies.");
}

这个方法只能被 Pair<Employee> 调用,Pair<Manager> 无法享用。很不人性化,而下面的改进则解决了这个问题

public static void printBuddies(Pair<? extends Eiployee> p) {
    Employee first = p.getFirst();
    Employee second = p.getSecond();
    Systefn.out.println(first.getName() + " and " + second.getNameQ + " are buddies.");
}

其继承关系变成了这样,那么类型变量为 Manager 和 Employee 的泛型实例就可以是用这个方法了,因为它们的泛型类都继承自 Pair<? extends Employee>

image-20220314110717672

8.8.2 通配符的超类型限定

直观地讲,带有超类型限定的通配符可以向泛型对象写入,带有子类型限定的通配符可以从泛型对象读取。

8.8.3 无限定通配符

Pair<?> 和 Pair 原始类型似乎相同,但它们最大的区别就是:可以用任意Object 对象调用原始 Pair 类的 setObject 方法,但是 Pair<?> 不能。

使用 Pair<?> 的好处在于能够普适快速读取的应用,比如判断一个 Pair 中是否含有 null:

public static boolean hasNulls(Pair<?> p) {
	return p.getFirstO = null || p.getSecondO =null;
}

// 上面的方法可以换成以下写法,但是明显上面的方法可读性更强,更加简便
public static <T> boolean hasNulls(Pair<T> p)

8.8.4 捕获通配符

通过辅助的泛型方法来实现通配符的捕获。

还有一点要注意的就是,使用 extends 和 super 修饰通配符的时候的 get 和 set 的问题。

8.9 反射和泛型

引入了泛型类之后,CLass 类也是泛型的,String.class 实际上是 Class<String> 的一个对象(唯一对象)。

posted @ 2022-03-14 18:57  槐下  阅读(27)  评论(0编辑  收藏  举报