20190920 On Java8 第二十章 泛型

第二十章 泛型

多态的泛化机制:

  1. 将方法的参数类型设为基类;
  2. 方法以接口而不是类作为参数;
  3. 使用泛型;

泛型实现了参数化类型

简单泛型

Java 泛型的核心概念:你只需告诉编译器要使用什么类型,剩下的细节交给它来处理。

钻石写法:

GenericHolder<Bob> h3 = new GenericHolder<>();

一个元组类库

元组,是将一组对象直接打包存储于其中的一个单一对象。

一个堆栈类

public class LinkedStack<T> {
	private static class Node<U> {
		U item;
		Node<U> next;

		Node() {
			item = null;
			next = null;
		}

		Node(U item, Node<U> next) {
			this.item = item;
			this.next = next;
		}

		boolean end() {
			return item == null && next == null;
		}
	}

	private Node<T> top = new Node<>(); // End sentinel

	public void push(T item) {
		top = new Node<>(item, top);
	}

	public T pop() {
		T result = top.item;
		if (!top.end())
			top = top.next;
		return result;
	}

	public static void main(String[] args) {
		LinkedStack<String> lss = new LinkedStack<>();
		for (String s : "Phasers on stun!".split(" "))
			lss.push(s);
		String s;
		while ((s = lss.pop()) != null)
			System.out.println(s);
	}
}

RandomList

public class RandomList<T> extends ArrayList<T> {
	private Random rand = new Random(47);

	public T select() {
		return get(rand.nextInt(size()));
	}

	public static void main(String[] args) {
		RandomList<String> rs = new RandomList<>();
		Arrays.stream(("The quick brown fox jumped over " + "the lazy brown dog").split(" ")).forEach(rs::add);
		IntStream.range(0, 11).forEach(i -> System.out.print(rs.select() + " "));
	}
}

泛型接口

泛型也可以应用于接口。参考 java.util.function.Supplier

Java 泛型的一个局限性:基本类型无法作为类型参数。

泛型方法

可以参数化类中的方法。类本身可能是泛型的,也可能不是,不过这与它的方法是否是泛型的并没有什么关系。

泛型方法独立于类而改变方法。作为准则,请“尽可能”使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂。

对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 类型参数推断

变长参数和泛型方法

泛型方法和变长参数列表可以很好地共存

参考 java.util.Arrays#asList

@SafeVarargs
public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}

@SafeVarargs 注解保证我们不会对变长参数列表进行任何修改,这是正确的,因为我们只从中读取。如果没有此注解,编译器将无法知道这些并会发出警告。

一个 Set 工具

public class Sets {
    /**
     * 返回并集
     * @param a
     * @param b
     * @param <T>
     * @return
     */
    public static <T> Set<T> union(Set<T> a, Set<T> b) {
        Set<T> result = new HashSet<>(a);
        result.addAll(b);
        return result;
    }

    /**
     * 返回交集
     * @param a
     * @param b
     * @param <T>
     * @return
     */
    public static <T> Set<T> intersection(Set<T> a, Set<T> b) {
        Set<T> result = new HashSet<>(a);
        result.retainAll(b);
        return result;
    }

    /**
     * 返回 从 superset 中减去 subset 的元素
     * @param superset
     * @param subset
     * @param <T>
     * @return
     */
    public static <T> Set<T> difference(Set<T> superset, Set<T> subset) {
        Set<T> result = new HashSet<>(superset);
        result.removeAll(subset);
        return result;
    }

    /**
     * 返回所有不在交集中的元素
     * @param a
     * @param b
     * @param <T>
     * @return
     */
    public static <T> Set<T> complement(Set<T> a, Set<T> b) {
        return difference(union(a, b), intersection(a, b));
    }
}

复杂模型构建

泛型的一个重要好处是能够简单安全地创建复杂模型。这将产生一个功能强大的数据结构,而无需太多代码。

泛型擦除

当你开始更深入地钻研泛型时,会发现有大量的东西初看起来是没有意义的。例如,尽管可以说 ArrayList.class,但不能说成 ArrayList<Integer>.class

class Frob {
}

class Fnorkle {
}

class Quark<Q> {
}

class Particle<POSITION, MOMENTUM> {
}

public class LostInformation {

    public static void main(String[] args) {
        List<Frob> list = new ArrayList<>();
        Map<Frob, Fnorkle> map = new HashMap<>();
        Quark<Fnorkle> quark = new Quark<>();
        Particle<Long, Double> p = new Particle<>();
        System.out.println(Arrays.toString(list.getClass().getTypeParameters()));   // [E]
        System.out.println(Arrays.toString(map.getClass().getTypeParameters()));   // [K,V]
        System.out.println(Arrays.toString(quark.getClass().getTypeParameters()));   // [Q]
        System.out.println(Arrays.toString(p.getClass().getTypeParameters()));   // [POSITION,MOMENTUM]
    }

}

根据 JDK 文档,Class.getTypeParameters() “返回一个 TypeVariable 对象数组,表示泛型声明中声明的类型参数...” 这暗示你可以发现这些参数类型。但是正如上例中输出所示,你只能看到用作参数占位符的标识符,这并非有用的信息。

残酷的现实是:在泛型代码内部,无法获取任何有关泛型参数类型的信息。

Java 泛型是使用擦除实现的。这意味着当你在使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。因此,List<String>List<Integer> 在运行时实际上是相同的类型。它们都被擦除成原生类型 List

边界 <T extends HasF> 声明 T 必须是 HasF 类型或其子类。泛型类型参数会擦除到它的第一个边界(可能有多个边界)。我们还提到了类型参数的擦除。编译器实际上会把类型参数替换为它的擦除,就像上面的示例,T 擦除到了 HasF,就像在类的声明中用 HasF 替换了 T 一样。

这提出了很重要的一点:泛型只有在类型参数比某个具体类型(以及其子类)更加“泛化”——代码能跨多个类工作时才有用。因此,类型参数和它们在有用的泛型代码中的应用,通常比简单的类替换更加复杂。但是,不能因此认为使用 <T extends HasF> 形式就是有缺陷的。例如,如果某个类有一个返回 T 的方法,那么泛型就有所帮助,因为它们之后将返回确切的类型:

迁移兼容性

泛型擦除是 Java 实现泛型的一种妥协,因为泛型不是 Java 语言出现时就有的,所以就有了这种妥协。

擦除减少了泛型的泛化性。泛型在 Java 中仍然是有用的,只是不如它们本来设想的那么有用,而原因就是擦除。

在基于擦除的实现中,泛型类型被当作第二类类型处理,即不能在某些重要的上下文使用泛型类型。泛型类型只有在静态类型检测期间才出现,在此之后,程序中的所有泛型类型都将被擦除,替换为它们的非泛型上界。例如, List<T> 这样的类型注解会被擦除为 List,普通的类型变量在未指定边界的情况下会被擦除为 Object

擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库,反之亦然。这经常被称为 迁移兼容性 。在理想情况下,所有事物将在指定的某天被泛化。在现实中,即使程序员只编写泛型代码,他们也必须处理 Java 5 之前编写的非泛型类库。这些类库的作者可能从没想过要泛化他们的代码,或许他们可能刚刚开始接触泛型。

擦除的问题

擦除的代价是显著的。泛型不能用于显式地引用运行时类型的操作中,例如转型、instanceof 操作和 new 表达式。因为所有关于参数的类型信息都丢失了,当你在编写泛型代码时,必须时刻提醒自己,你只是看起来拥有有关参数的类型信息而已。

擦除和迁移兼容性意味着,使用泛型并不是强制的,例如 new ArrayList()

边界处的动作

因为擦除移除了方法体中的类型信息,所以在运行时的问题就是 边界:即对象进入和离开方法的地点。这些正是编译器在 编译期执行类型检查 并插入转型代码的地点。

补偿擦除

obj instanceof T 无法使用,类型标签可以使用动态 isInstance() ,编译器来保证类型标签与泛型参数相匹配

class Building {
}

class House extends Building {
}

public class ClassTypeCapture<T> {
    Class<T> kind;

    public ClassTypeCapture(Class<T> kind) {
        this.kind = kind;
    }

    public boolean f(Object arg) {
        return kind.isInstance(arg);
    }

    public static void main(String[] args) {
        ClassTypeCapture<Building> ctt1 = new ClassTypeCapture<>(Building.class);
        System.out.println(ctt1.f(new Building()));     // true
        System.out.println(ctt1.f(new House()));     // true
        ClassTypeCapture<House> ctt2 = new ClassTypeCapture<>(House.class);
        System.out.println(ctt2.f(new Building()));     // false
        System.out.println(ctt2.f(new House()));     // true
    }
}

创建类型的实例

new T() 无法使用,可以使用 newInstance() 创建该类型的新对象

泛型数组

对于新代码,请传入类型标记。

public class GenericArrayWithTypeToken<T> {
    private T[] array;

    @SuppressWarnings("unchecked")
    public GenericArrayWithTypeToken(Class<T> type, int sz) {
        array = (T[]) Array.newInstance(type, sz);
    }

    public void put(int index, T item) {
        array[index] = item;
    }

    public T get(int index) {
        return array[index];
    }

    // Expose the underlying representation:
    public T[] rep() {
        return array;
    }

    public static void main(String[] args) {
        GenericArrayWithTypeToken<Integer> gai = new GenericArrayWithTypeToken<>(Integer.class, 10);
        // This now works:
        Integer[] ia = gai.rep();
    }
}

边界

由于擦除会删除类型信息,因此唯一可用于无限制泛型参数的方法是那些 Object 可用的方法。但是,如果将该参数限制为某类型的子集,则可以调用该子集中的方法。为了应用约束,Java 泛型使用了 extends 关键字。

泛型声明中的 extends 只能有一个具体类,可以有多个接口。

通配符

class Fruit {
}

class Apple extends Fruit {
}

class Jonathan extends Apple {
}

class Orange extends Fruit {
}

public class CovariantArrays {

    public static void main(String[] args) {
        Fruit[] fruit = new Apple[10];
        fruit[0] = new Apple(); // OK
        fruit[1] = new Jonathan(); // OK
        // 运行时类型是 Apple[] 而不是 Fruit[] 或者 Orange[]:
        try {
            // 编译通过,运行报错
            fruit[0] = new Fruit();
        } catch (Exception e) {
            System.out.println(e);  // java.lang.ArrayStoreException: generics.Fruit
        }
        try {
            // 编译通过,运行报错
            fruit[0] = new Orange();
        } catch (Exception e) {
            System.out.println(e);  // java.lang.ArrayStoreException: generics.Orange
        }
    }
}
// List<Fruit> flist = new ArrayList<Apple>();  // 编译错误
List<? extends Fruit> flist = new ArrayList<Apple>();

AppleList 在类型上不等价于 FruitList,即使 Apple 是一种 Fruit 类型。

逆变

超类型通配符 可以声明通配符是由某个特定类的任何基类来界定的,方法是指定 <?super MyClass> ,或者甚至使用类型参数: <?super T>(尽管你不能对泛型参数给出一个超类型边界;即不能声明 <T super MyClass> )。

List<? extends Apple> extendsApples = new ArrayList<>();
// extendsApples.add(new Fruit());     // 编译报错
// extendsApples.add(new Apple());     // 编译报错
// extendsApples.add(new Jonathan());     // 编译报错

List<? super Apple> superApples = new ArrayList<>();
// superApples.add(new Fruit());     // 编译报错
superApples.add(new Apple());
superApples.add(new Jonathan());

List<? extends Apple>,你可以读作“一个具有任何从 Apple 继承的类型的列表”。然而,这实际上并不意味着这个 List 将持有任何类型的 Apple。通配符引用的是明确的类型,因此它意味着“某种 flist 引用没有指定的具体类型”。

无界通配符

无界通配符 <?> 看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型。

List 实际上表示“持有任何 Object 类型的原生 List ”,而 List<?> 表示“具有某种特定类型的非原生 List ,只是我们不知道类型是什么。”

捕获转换

有一种特殊情况需要使用 <?> 而不是原生类型。如果向一个使用 <?> 的方法传递原生类型,那么对编译器来说,可能会推断出实际的类型参数,使得这个方法可以回转并调用另一个使用这个确切类型的方法。它被称为捕获转换,因为未指定的通配符类型被捕获,并被转换为确切类型。

public class CaptureConversion {
    static <T> void f1(Holder<T> holder) {
        T t = holder.get();
        System.out.println(t.getClass().getSimpleName());
    }

    static void f2(Holder<?> holder) {
        f1(holder); // Call with captured type
    }

    
    public static void main(String[] args) {
        Holder raw = new Holder<>(1);

        f1(raw);
        // warning: [unchecked] unchecked method invocation:
        // method f1 in class CaptureConversion
        // is applied to given types
        //     f1(raw);
        //       ^
        //   required: Holder<T>
        //   found: Holder
        //   where T is a type-variable:
        //     T extends Object declared in
        //     method <T>f1(Holder<T>)
        // warning: [unchecked] unchecked conversion
        //     f1(raw);
        //        ^
        //   required: Holder<T>
        //   found:    Holder
        //   where T is a type-variable:
        //     T extends Object declared in
        //     method <T>f1(Holder<T>)
        // 2 warnings

        f2(raw); // No warnings
        Holder rawBasic = new Holder();

        rawBasic.set(new Object());
        // warning: [unchecked] unchecked call to set(T)
        // as a member of the raw type Holder
        //     rawBasic.set(new Object());
        //                 ^
        //   where T is a type-variable:
        //     T extends Object declared in class Holder
        // 1 warning

        f2(rawBasic); // No warnings
        // Upcast to Holder<?>, still figures it out:
        Holder<?> wildcarded = new Holder<>(1.0);
        f2(wildcarded);
    }
}
/* Output:
Integer
Integer
Object
Double
*/

f1() 中的类型参数都是确切的,没有通配符或边界。在 f2() 中,Holder 参数是一个无界通配符,因此它看起来是未知的。但是,在 f2() 中调用了 f1(),而 f1() 需要一个已知参数。这里所发生的是:在调用 f2() 的过程中捕获了参数类型,并在调用 f1() 时使用了这种类型。

注意,不能从 f2() 中返回 T,因为 T 对于 f2() 来说是未知的。捕获转换十分有趣,但是非常 受限

问题

在使用 Java 泛型时会出现的各类问题

任何基本类型都不能作为类型参数

解决方法是使用基本类型的包装器类以及自动装箱机制。如果创建一个 ArrayList<Integer>,并将基本类型 int 应用于这个集合,那么你将发现自动装箱机制将自动地实现 intInteger 的双向转换——因此,这几乎就像是有一个 ArrayList<int> 一样

实现参数化接口

一个类不能实现同一个泛型接口的两种变体,由于擦除的原因,这两个变体会成为相同的接口。

package generics;

interface Payable<T> {
}

class Employee implements Payable<Employee> {
}

// 编译报错
// class Hourly extends Employee implements Payable<Hourly> {}

// 'generics.Payable' cannot be inherited with different type arguments: 'generics.Employee' and 'generics.Hourly'

转型和警告

使用带有泛型类型参数的转型或 instanceof 不会有任何效果。

有时,泛型没有消除对转型的需要,这就会由编译器产生警告,而这个警告是不恰当的。

你会被强制要求转型,但是又被告知不应该转型。为了解决这个问题,必须使用 Java 5 引入的新的转型形式,即通过泛型类来转型:

public class ClassCasting {
    @SuppressWarnings("unchecked")
    public void f(String[] args) throws Exception {
        ObjectInputStream in = new ObjectInputStream(new FileInputStream(args[0]));
        // Won't Compile:
        // List<Widget> lw1 = List<>.class.cast(in.readObject());   // 编译失败
        List<Widget> lw2 = List.class.cast(in.readObject());
        // List<Widget> lw3 = List<Widget>.class.cast(in.readObject());  // 编译失败
        List<Widget> lw4 = (List<Widget>) List.class.cast(in.readObject());
    }
}

重载

下面的程序是不能编译的,即使它看起来是合理的:

// generics/UseList.java
// {WillNotCompile}
import java.util.*;

public class UseList<W, T> {
    void f(List<T> v) {}
    void f(List<W> v) {}
}

因为擦除,所以重载方法产生了相同的类型签名。

因而,当擦除后的参数不能产生唯一的参数列表时,你必须提供不同的方法名:

// generics/UseList2.java

import java.util.*;

public class UseList2<W, T> {
    void f1(List<T> v) {}
    void f2(List<W> v) {}
}

幸运的是,编译器可以检测到这类问题。

基类劫持接口

假设你有一个实现了 Comparable 接口的 Pet 类:

// generics/ComparablePet.java

public class ComparablePet implements Comparable<ComparablePet> {
    @Override
    public int compareTo(ComparablePet o) {
        return 0;
    }
}

尝试缩小 ComparablePet 子类的比较类型是有意义的。例如,Cat 类可以与其他的 Cat 比较:

// generics/HijackedInterface.java
// {WillNotCompile}

class Cat extends ComparablePet implements Comparable<Cat> {
    // error: Comparable cannot be inherited with
    // different arguments: <Cat> and <ComparablePet>
    // class Cat
    // ^
    // 1 error
    public int compareTo(Cat arg) {
        return 0;
    }
}

不幸的是,这不能工作。一旦 Comparable 的类型参数设置为 ComparablePet,其他的实现类只能比较 ComparablePet

// generics/RestrictedComparablePets.java

public class Hamster extends ComparablePet implements Comparable<ComparablePet> {

    @Override
    public int compareTo(ComparablePet arg) {
        return 0;
    }
}
// Or just:
class Gecko extends ComparablePet {
    public int compareTo(ComparablePet arg) {
        return 0;
    }
}

Hamster 显示了重新实现 ComparablePet 中相同的接口是可能的,只要接口完全相同,包括参数类型。然而正如 Gecko 中所示,这与直接覆写基类的方法完全相同。

自限定的类型

Self-Bounded Types

在 Java 泛型中,有一个似乎经常性出现的惯用法,它相当令人费解:

class SelfBounded<T extends SelfBounded<T>> { // ...

它强调的是当 extends 关键字用于边界与用来创建子类明显是不同的。

古怪的循环泛型

不能直接继承一个泛型参数,但是,可以继承在其自己的定义中使用这个泛型参数的类

package generics;

class GenericType<T> {
}

public class CuriouslyRecurringGeneric extends GenericType<CuriouslyRecurringGeneric> {
}

这里意思是:“我在创建一个新类,它继承自一个泛型类型,这个泛型类型接受我的类的名字作为其参数。”当给出导出类的名字时,这个泛型基类能够实现什么呢?好吧,Java 中的泛型关乎参数和返回类型,因此它能够产生使用导出类作为其参数和返回类型的基类。它还能将导出类型用作其域类型,尽管这些将被擦除为 Object 的类型。

本质是基类用导出类替代其参数。这意味着泛型基类变成了一种其所有导出类的公共功能的模版,但是这些功能对于其所有参数和返回值,将使用导出类型。也就是说,在所产生的类中将使用确切类型而不是基类型。

自限定

class SelfBounded<T extends SelfBounded<T>> {
    T element;

    SelfBounded<T> set(T arg) {
        element = arg;
        return this;
    }

    T get() {
        return element;
    }
}

class A extends SelfBounded<A> {
}

// B b = new B();
// A ba = b.get();
class B extends SelfBounded<A> {
} // Also OK

class C extends SelfBounded<C> {
    C setAndGet(C arg) {
        set(arg);
        return get();
    }
}

class D {
}

// 编译失败
// 如果将 SelfBounded 类声明修改为
// class SelfBounded<T> 可以支持
// class E extends SelfBounded<D> {}

自限定限制只能强制作用于继承关系。如果使用自限定,就应该了解这个类所用的类型参数将与使用这个参数的类具有相同的基类型。这会强制要求使用这个类的每个人都要遵循这种形式。

还可以将自限定用于泛型方法:

public class SelfBoundingMethods {
    static <T extends SelfBounded<T>> T f(T arg) {
        return arg.set(arg).get();
    }

    public static void main(String[] args) {
        A a = f(new A());
        // B b = f(new B());    // 编译报错
    }
}

这可以防止这个方法被应用于除上述形式的自限定参数之外的任何事物上。

动态类型安全

因为可以向 Java 5 之前的代码传递泛型集合,所以旧式代码仍旧有可能会破坏你的集合。Java 5 的 java.util.Collections 中有一组便利工具,可以解决在这种情况下的类型检查问题,它们是:静态方法 checkedCollection()checkedList()checkedMap()checkedSet()checkedSortedMap()checkedSortedSet()。这些方法每一个都会将你希望动态检查的集合当作第一个参数接受,并将你希望强制要求的类型作为第二个参数接受。

受检查的集合在你试图插入类型不正确的对象时抛出 ClassCastException ,这与泛型之前的(原生)集合形成了对比,对于后者来说,当你将对象从集合中取出时,才会通知你出现了问题。在后一种情况中,你知道存在问题,但是不知道罪魁祸首在哪里,如果使用受检查的集合,就可以发现谁在试图插入不良对象。

package generics;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CheckedList {
    static class Pet {

    }

    static class Dog extends Pet {

    }

    static class Cat extends Pet {

    }

    @SuppressWarnings("unchecked")
    static void oldStyleMethod(List probablyDogs) {
        probablyDogs.add(new Cat());
    }

    public static void main(String[] args) {
        List<Dog> dogs1 = new ArrayList<>();
        oldStyleMethod(dogs1); // Quietly accepts a Cat
        List<Dog> dogs2 = Collections.checkedList(new ArrayList<>(), Dog.class);
        try {
            oldStyleMethod(dogs2); // checkedList 抛出异常
        } catch (Exception e) {
            System.out.println("Expected: " + e);
        }
        // Derived types work fine:
        List<Pet> pets = Collections.checkedList(new ArrayList<>(), Pet.class);
        pets.add(new Dog());
        pets.add(new Cat());
    }
}
/* Output:
Expected: java.lang.ClassCastException: Attempt to
insert class typeinfo.pets.Cat element into collection
with element type class typeinfo.pets.Dog
*/

泛型异常

由于擦除的原因,catch 语句不能捕获泛型类型的异常,因为在编译期和运行时都必须知道异常的确切类型。泛型类也不能直接或间接继承自 Throwable(这将进一步阻止你去定义不能捕获的泛型异常)。但是,类型参数可能会在一个方法的 throws 子句中用到。

interface Processor<T, E extends Exception> {
    void process(List<T> resultCollector) throws E;
}

混型

Mixins

术语混型随时间的推移好像拥有了无数的含义,但是其最基本的概念是混合多个类的能力,以产生一个可以表示混型中所有类型的类。这往往是你最后的手段,它将使组装多个类变得简单易行。

混型的价值之一是它们可以将特性和行为一致地应用于多个类之上。如果想在混型类中修改某些东西,作为一种意外的好处,这些修改将会应用于混型所应用的所有类型之上。正由于此,混型有一点面向切面编程 (AOP) 的味道,而切面经常被建议用来解决混型问题。

与接口混合

一种更常见的推荐解决方案是使用接口来产生混型效果,就像下面这样:

package generics;

import java.util.Date;

interface TimeStamped {
    long getStamp();
}

class TimeStampedImp implements TimeStamped {
    private final long timeStamp;

    TimeStampedImp() {
        timeStamp = new Date().getTime();
    }

    @Override
    public long getStamp() {
        return timeStamp;
    }
}

interface SerialNumbered {
    long getSerialNumber();
}

class SerialNumberedImp implements SerialNumbered {
    private static long counter = 1;
    private final long serialNumber = counter++;

    @Override
    public long getSerialNumber() {
        return serialNumber;
    }
}

interface Basic {
    void set(String val);

    String get();
}

class BasicImp implements Basic {
    private String value;

    @Override
    public void set(String val) {
        value = val;
    }

    @Override
    public String get() {
        return value;
    }
}

class Mixin extends BasicImp implements TimeStamped, SerialNumbered {
    private TimeStamped timeStamp = new TimeStampedImp();
    private SerialNumbered serialNumber = new SerialNumberedImp();

    @Override
    public long getStamp() {
        return timeStamp.getStamp();
    }

    @Override
    public long getSerialNumber() {
        return serialNumber.getSerialNumber();
    }
}

public class Mixins {
    public static void main(String[] args) {
        Mixin mixin1 = new Mixin(), mixin2 = new Mixin();
        mixin1.set("test string 1");
        mixin2.set("test string 2");
        System.out.println(mixin1.get() + " " + mixin1.getStamp() + " " + mixin1.getSerialNumber());
        System.out.println(mixin2.get() + " " + mixin2.getStamp() + " " + mixin2.getSerialNumber());
    }
}

Mixin 类基本上是在使用委托,因此每个混入类型都要求在 Mixin 中有一个相应的域,而你必须在 Mixin 中编写所有必需的方法,将方法调用转发给恰当的对象。这个示例使用了非常简单的类,但是当使用更复杂的混型时,代码数量会急速增加。

使用装饰器模式

前面的示例可以被改写为使用装饰器:

package generics.decorator;

import java.util.Date;

class Basic {
    private String value;

    public void set(String val) {
        value = val;
    }

    public String get() {
        return value;
    }
}

class Decorator extends Basic {
    protected Basic basic;

    Decorator(Basic basic) {
        this.basic = basic;
    }

    @Override
    public void set(String val) {
        basic.set(val);
    }

    @Override
    public String get() {
        return basic.get();
    }
}

class TimeStamped extends Decorator {
    private final long timeStamp;

    TimeStamped(Basic basic) {
        super(basic);
        timeStamp = new Date().getTime();
    }

    public long getStamp() {
        return timeStamp;
    }
}

class SerialNumbered extends Decorator {
    private static long counter = 1;
    private final long serialNumber = counter++;

    SerialNumbered(Basic basic) {
        super(basic);
    }

    public long getSerialNumber() {
        return serialNumber;
    }
}

public class Decoration {
    public static void main(String[] args) {
        TimeStamped t = new TimeStamped(new Basic());
        TimeStamped t2 = new TimeStamped(new SerialNumbered(new Basic()));
        //- t2.getSerialNumber(); // Not available
        SerialNumbered s = new SerialNumbered(new Basic());
        SerialNumbered s2 = new SerialNumbered(new TimeStamped(new Basic()));
        //- s2.getStamp(); // Not available
    }
}

产生自泛型的类包含所有感兴趣的方法,但是由使用装饰器所产生的对象类型是最后被装饰的类型。也就是说,尽管可以添加多个层,但是最后一层才是实际的类型,因此只有最后一层的方法是可视的,而混型的类型是所有被混合到一起的类型。因此对于装饰器来说,其明显的缺陷是它只能有效地工作于装饰中的一层(最后一层),而混型方法显然会更自然一些。因此,装饰器只是对由混型提出的问题的一种局限的解决方案。

与动态代理混合

可以使用动态代理来创建一种比装饰器更贴近混型模型的机制。通过使用动态代理,所产生的类的动态类型将会是已经混入的组合类型。

package generics;

import onjava.Tuple2;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.Map;

import static onjava.Tuple.tuple;

class MixinProxy implements InvocationHandler {
    Map<String, Object> delegatesByMethod;

    @SuppressWarnings("unchecked")
    MixinProxy(Tuple2<Object, Class<?>>... pairs) {
        delegatesByMethod = new HashMap<>();
        for (Tuple2<Object, Class<?>> pair : pairs) {
            for (Method method : pair.a2.getMethods()) {
                String methodName = method.getName();
                // The first interface in the map
                // implements the method.
                if (!delegatesByMethod.containsKey(methodName))
                    delegatesByMethod.put(methodName, pair.a1);
            }
        }
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        String methodName = method.getName();
        Object delegate = delegatesByMethod.get(methodName);
        return method.invoke(delegate, args);
    }

    @SuppressWarnings("unchecked")
    public static Object newInstance(Tuple2... pairs) {
        Class[] interfaces = new Class[pairs.length];
        for (int i = 0; i < pairs.length; i++) {
            interfaces[i] = (Class) pairs[i].a2;
        }
        ClassLoader cl = pairs[0].a1.getClass().getClassLoader();
        return Proxy.newProxyInstance(cl, interfaces, new MixinProxy(pairs));
    }
}

public class DynamicProxyMixin {
    public static void main(String[] args) {
        Object mixin = MixinProxy.newInstance(tuple(new BasicImp(), Basic.class), tuple(new TimeStampedImp(), TimeStamped.class), tuple(new SerialNumberedImp(), SerialNumbered.class));
        Basic b = (Basic) mixin;
        TimeStamped t = (TimeStamped) mixin;
        SerialNumbered s = (SerialNumbered) mixin;
        b.set("Hello");
        System.out.println(b.get());
        System.out.println(t.getStamp());
        System.out.println(s.getSerialNumber());
    }
}
/* Output:
Hello
1494331653339
1
*/

潜在类型机制

Latent Typing

潜在类型机制结构化类型机制,而更古怪的术语称为鸭子类型机制,即“如果它走起来像鸭子,并且叫起来也像鸭子,那么你就可以将它当作鸭子对待。

潜在类型机制是一种代码组织和复用机制。有了它,编写出的代码相对于没有它编写出的代码,能够更容易地复用。代码组织和复用是所有计算机编程的基本手段:编写一次,多次使用,并在一个位置保存代码。因为我并未被要求去命名我的代码要操作于其上的确切接口,所以,有了潜在类型机制,我就可以编写更少的代码,并更容易地将其应用于多个地方。

支持潜在类型机制的语言包括 Python、C++、Ruby、SmallTalk 和 Go。Python 是动态类型语言(几乎所有的类型检查都发生在运行时),而 C++ 和 Go 是静态类型语言(类型检查发生在编译期),因此潜在类型机制不要求静态或动态类型检查。

Java 中的直接潜在类型

会被强制要求使用一个类或接口,并在边界表达式中指定它

public interface Performs {
    void speak();
    void sit();
}

class CommunicateSimply {
    static void perform(Performs performer) {
        performer.speak();
        performer.sit();
    }
}

public class SimpleDogsAndRobots {
    public static void main(String[] args) {
        CommunicateSimply.perform(new PerformingDog());
        CommunicateSimply.perform(new Robot());
    }
}

对缺乏潜在类型机制的补偿

尽管 Java 不直接支持潜在类型机制,但是这并不意味着泛型代码不能在不同的类型层次结构之间应用。

反射

class CommunicateReflectively {
    public static void perform(Object speaker) {
        Class<?> spkr = speaker.getClass();
        try {
            try {
                Method speak = spkr.getMethod("speak");
                speak.invoke(speaker);
            } catch (NoSuchMethodException e) {
                System.out.println(speaker + " cannot speak");
            }
            try {
                Method sit = spkr.getMethod("sit");
                sit.invoke(speaker);
            } catch (NoSuchMethodException e) {
                System.out.println(speaker + " cannot sit");
            }
        } catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            throw new RuntimeException(speaker.toString(), e);
        }
    }
}

public class LatentReflection {
    public static void main(String[] args) {
        CommunicateReflectively.perform(new SmartDog());
        CommunicateReflectively.perform(new Robot());
        CommunicateReflectively.perform(new Mime());
    }
}

Java 8 中的辅助潜在类型

Assisted Latent Typing in Java 8

先前声明关于 Java 缺乏对潜在类型的支持在 Java 8 之前是完全正确的。但是,Java 8 中的非绑定方法引用使我们能够产生一种潜在类型的形式,以满足创建一段可工作在不相干类型上的代码。因为 Java 最初并不是如此设计,所以结果可想而知,比其他语言中要尴尬一些。但是,至少现在成为了可能,只是缺乏令人惊艳之处。

package generics;

import typeinfo.pets.Dog;

import java.util.function.Consumer;

class PerformingDogA extends Dog {
    public void speak() {
        System.out.println("Woof!");
    }

    public void sit() {
        System.out.println("Sitting");
    }

    public void reproduce() {
    }
}

class RobotA {
    public void speak() {
        System.out.println("Click!");
    }

    public void sit() {
        System.out.println("Clank!");
    }

    public void oilChange() {
    }
}

class CommunicateA {
    public static <P> void perform(P performer, Consumer<P> action1, Consumer<P> action2) {
        action1.accept(performer);
        action2.accept(performer);
    }
}

public class DogsAndRobotMethodReferences {
    public static void main(String[] args) {
        CommunicateA.perform(new PerformingDogA(), PerformingDogA::speak, PerformingDogA::sit);
        CommunicateA.perform(new RobotA(), RobotA::speak, RobotA::sit);
        CommunicateA.perform(new Mime(), Mime::walkAgainstTheWind, Mime::pushInvisibleWalls);
    }
}

这里它们不继承通用接口

总结:类型转换真的如此之糟吗?

在 Java 中,泛型是在这种语言首次发布大约 10 年之后才引入的,因此向泛型迁移的问题特别多,并且对泛型的设计产生了明显的影响。其结果就是,程序员将承受这些痛苦,而这一切都是由于 Java 设计者在设计 1.0 版本时所表现出来的短视造成的。

posted @ 2019-09-20 22:43  流星<。)#)))≦  阅读(264)  评论(0编辑  收藏  举报