本章概要
- 泛型接口
- 泛型方法
- 变长参数和泛型方法
- 一个泛型的 Supplier
- 简化元组的使用
- 一个 Set 工具
泛型接口
泛型也可以应用于接口。例如 生成器,这是一种专门负责创建对象的类。实际上,这是 工厂方法 设计模式的一种应用。不过,当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数。生成器无需额外的信息就知道如何创建新对象。
一般而言,一个生成器只定义一个方法,用于创建对象。例如 java.util.function
类库中的 Supplier
就是一个生成器,调用其 get()
获取对象。get()
是泛型方法,返回值为类型参数 T
。
为了演示 Supplier
,我们需要定义几个类。下面是个咖啡相关的继承体系:
Coffee.java
public class Coffee {private static long counter = 0;private final long id = counter++;@Overridepublic String toString() {return getClass().getSimpleName() + " " + id;}
}
Latte.java
public class Latte extends Coffee {
}
Mocha.java
public class Mocha extends Coffee {
}
Cappuccino.java
public class Cappuccino extends Coffee {
}
Americano.java
public class Americano extends Coffee {
}
Breve.java
public class Breve extends Coffee {
}
现在,我们可以编写一个类,实现 Supplier<Coffee>
接口,它能够随机生成不同类型的 Coffee
对象:
import java.util.*;
import java.util.function.*;
import java.util.stream.*;public class CoffeeSupplierimplements Supplier<Coffee>, Iterable<Coffee> {private Class<?>[] types = {Latte.class, Mocha.class,Cappuccino.class, Americano.class, Breve.class};private static Random rand = new Random(47);public CoffeeSupplier() {}// For iteration:private int size = 0;public CoffeeSupplier(int sz) {size = sz;}@Overridepublic Coffee get() {try {return (Coffee) types[rand.nextInt(types.length)].newInstance();} catch (InstantiationException | IllegalAccessException e) {throw new RuntimeException(e);}}class CoffeeIterator implements Iterator<Coffee> {int count = size;@Overridepublic boolean hasNext() {return count > 0;}@Overridepublic Coffee next() {count--;return CoffeeSupplier.this.get();}@Overridepublic void remove() {throw new UnsupportedOperationException();}}@Overridepublic Iterator<Coffee> iterator() {return new CoffeeIterator();}public static void main(String[] args) {Stream.generate(new CoffeeSupplier()).limit(5).forEach(System.out::println);for (Coffee c : new CoffeeSupplier(5)) {System.out.println(c);}}
}
输出结果:
参数化的 Supplier
接口确保 get()
返回值是参数的类型。CoffeeSupplier
同时还实现了 Iterable
接口,所以能用于 for-in 语句。不过,它还需要知道何时终止循环,这正是第二个构造函数的作用。
下面是另一个实现 Supplier<T>
接口的例子,它负责生成 Fibonacci 数列:
import java.util.function.*;
import java.util.stream.*;public class Fibonacci implements Supplier<Integer> {private int count = 0;@Overridepublic Integer get() {return fib(count++);}private int fib(int n) {if (n < 2) {return 1;}return fib(n - 2) + fib(n - 1);}public static void main(String[] args) {Stream.generate(new Fibonacci()).limit(18).map(n -> n + " ").forEach(System.out::print);}
}
输出结果:
虽然我们在 Fibonacci
类的里里外外使用的都是 int
类型,但是其参数类型却是 Integer
。这个例子引出了 Java 泛型的一个局限性:基本类型无法作为类型参数。不过 Java 5 具备自动装箱和拆箱的功能,可以很方便地在基本类型和相应的包装类之间进行转换。通过这个例子中 Fibonacci
类对 int
的使用,我们已经看到了这种效果。
如果还想更进一步,编写一个实现了 Iterable
的 Fibnoacci
生成器。我们的一个选择是重写这个类,令其实现 Iterable
接口。不过,你并不是总能拥有源代码的控制权,并且,除非必须这么做,否则,我们也不愿意重写一个类。而且我们还有另一种选择,就是创建一个 适配器 (Adapter) 来实现所需的接口,我们在前面介绍过这个设计模式。
有多种方法可以实现适配器。例如,可以通过继承来创建适配器类:
import java.util.*;public class IterableFibonacciextends Fibonacci implements Iterable<Integer> {private int n;public IterableFibonacci(int count) {n = count;}@Overridepublic Iterator<Integer> iterator() {return new Iterator<Integer>() {@Overridepublic boolean hasNext() {return n > 0;}@Overridepublic Integer next() {n--;return IterableFibonacci.this.get();}@Overridepublic void remove() { // Not implementedthrow new UnsupportedOperationException();}};}public static void main(String[] args) {for (int i : new IterableFibonacci(18)) {System.out.print(i + " ");}}
}
输出结果:
在 for-in 语句中使用 IterableFibonacci
,必须在构造函数中提供一个边界值,这样 hasNext()
才知道何时返回 false,结束循环。
泛型方法
到目前为止,我们已经研究了参数化整个类。其实还可以参数化类中的方法。类本身可能是泛型的,也可能不是,不过这与它的方法是否是泛型的并没有什么关系。
泛型方法独立于类而改变方法。作为准则,请“尽可能”使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂。
如果方法是 static 的,则无法访问该类的泛型类型参数,因此,如果使用了泛型类型参数,则它必须是泛型方法。
要定义泛型方法,请将泛型参数列表放置在返回值之前,如下所示:
public class GenericMethods {public <T> void f(T x) {System.out.println(x.getClass().getName());}public static void main(String[] args) {GenericMethods gm = new GenericMethods();gm.f("");gm.f(1);gm.f(1.0);gm.f(1.0F);gm.f('c');gm.f(gm);}
}
尽管可以同时对类及其方法进行参数化,但这里未将 GenericMethods 类参数化。只有方法 f()
具有类型参数,该参数由方法返回类型之前的参数列表指示。
对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 类型参数推断。因此,对 f()
的调用看起来像普通的方法调用,并且 f()
看起来像被重载了无数次一样。它甚至会接受 GenericMethods 类型的参数。
如果使用基本类型调用 f()
,自动装箱就开始起作用,自动将基本类型包装在它们对应的包装类型中。
变长参数和泛型方法
泛型方法和变长参数列表可以很好地共存:
import java.util.ArrayList;
import java.util.List;public class GenericVarargs {@SafeVarargspublic static <T> List<T> makeList(T... args) {List<T> result = new ArrayList<>();for (T item : args)result.add(item);return result;}public static void main(String[] args) {List<String> ls = makeList("A");System.out.println(ls);ls = makeList("A", "B", "C");System.out.println(ls);ls = makeList("ABCDEFFHIJKLMNOPQRSTUVWXYZ".split(""));System.out.println(ls);}
}
此处显示的 makeList()
方法产生的功能与标准库的 java.util.Arrays.asList()
方法相同。
@SafeVarargs
注解保证我们不会对变长参数列表进行任何修改,这是正确的,因为我们只从中读取。如果没有此注解,编译器将无法知道这些并会发出警告。
一个泛型的 Supplier
这是一个为任意具有无参构造方法的类生成 Supplier 的类。为了减少键入,它还包括一个用于生成 BasicSupplier 的泛型方法:
import java.util.function.Supplier;public class BasicSupplier<T> implements Supplier<T> {private Class<T> type;public BasicSupplier(Class<T> type) {this.type = type;}@Overridepublic T get() {try {// Assumes type is a public class:return type.newInstance();} catch (InstantiationException |IllegalAccessException e) {throw new RuntimeException(e);}}// Produce a default Supplier from a type token:public static <T> Supplier<T> create(Class<T> type) {return new BasicSupplier<>(type);}
}
此类提供了产生以下对象的基本实现:
- 是 public 的。 因为 BasicSupplier 在单独的包中,所以相关的类必须具有 public 权限,而不仅仅是包级访问权限。
- 具有无参构造方法。要创建一个这样的 BasicSupplier 对象,请调用
create()
方法,并将要生成类型的类型令牌传递给它。通用的create()
方法提供了BasicSupplier.create(MyType.class)
这种较简洁的语法来代替较笨拙的new BasicSupplier <MyType>(MyType.class)
。
例如,这是一个具有无参构造方法的简单类:
public class CountedObject {private static long counter = 0;private final long id = counter++;public long id() {return id;}@Overridepublic String toString() {return "CountedObject " + id;}
}
CountedObject 类可以跟踪自身创建了多少个实例,并通过 toString()
报告这些实例的数量。 BasicSupplier 可以轻松地为 CountedObject 创建 Supplier:
import java.util.stream.Stream;public class BasicSupplierDemo {public static void main(String[] args) {Stream.generate(BasicSupplier.create(CountedObject.class)).limit(5).forEach(System.out::println);}
}
泛型方法减少了产生 Supplier 对象所需的代码量。 Java 泛型强制传递 Class 对象,以便在 create()
方法中将其用于类型推断。
简化元组的使用
使用类型参数推断和静态导入,我们将把早期的元组重写为更通用的库。在这里,我们使用重载的静态方法创建元组:
Tuple.java
public class Tuple {public static <A, B> Tuple2<A, B> tuple(A a, B b) {return new Tuple2<>(a, b);}public static <A, B, C> Tuple3<A, B, C>tuple(A a, B b, C c) {return new Tuple3<>(a, b, c);}public static <A, B, C, D> Tuple4<A, B, C, D>tuple(A a, B b, C c, D d) {return new Tuple4<>(a, b, c, d);}public static <A, B, C, D, E>Tuple5<A, B, C, D, E> tuple(A a, B b, C c, D d, E e) {return new Tuple5<>(a, b, c, d, e);}
}
Tuple2.java
public class Tuple2<A, B> {public final A a1;public final B a2;public Tuple2(A a, B b) {a1 = a;a2 = b;}public String rep() {return a1 + ", " + a2;}@Overridepublic String toString() {return "(" + rep() + ")";}
}
Tuple3.java
public class Tuple3<A, B, C> extends Tuple2<A, B> {public final C a3;public Tuple3(A a, B b, C c) {super(a, b);a3 = c;}@Overridepublic String rep() {return super.rep() + ", " + a3;}
}
Tuple4.java
public class Tuple4<A, B, C, D>extends Tuple3<A, B, C> {public final D a4;public Tuple4(A a, B b, C c, D d) {super(a, b, c);a4 = d;}@Overridepublic String rep() {return super.rep() + ", " + a4;}
}
Tuple5.java
public class Tuple5<A, B, C, D, E>extends Tuple4<A, B, C, D> {public final E a5;public Tuple5(A a, B b, C c, D d, E e) {super(a, b, c, d);a5 = e;}@Overridepublic String rep() {return super.rep() + ", " + a5;}
}
我们修改 TupleTest.java 来测试 Tuple.java :
import static com.example.test.Tuple.tuple;public class TupleTest2 {static Tuple2<String, Integer> f() {return tuple("hi", 47);}static Tuple2 f2() {return tuple("hi", 47);}static Tuple3<Amphibian, String, Integer> g() {return tuple(new Amphibian(), "hi", 47);}static Tuple4<Vehicle, Amphibian, String, Integer> h() {return tuple(new Vehicle(), new Amphibian(), "hi", 47);}static Tuple5<Vehicle, Amphibian,String, Integer, Double> k() {return tuple(new Vehicle(), new Amphibian(),"hi", 47, 11.1);}public static void main(String[] args) {Tuple2<String, Integer> ttsi = f();System.out.println(ttsi);System.out.println(f2());System.out.println(g());System.out.println(h());System.out.println(k());}
}
Americano.java
public class Americano extends Coffee {
}
Vehicle.java
public class Vehicle {
}
请注意,f()
返回一个参数化的 Tuple2 对象,而 f2()
返回一个未参数化的 Tuple2 对象。编译器不会在这里警告 f2()
,因为返回值未以参数化方式使用。从某种意义上说,它被“向上转型”为一个未参数化的 Tuple2 。 但是,如果尝试将 f2()
的结果放入到参数化的 Tuple2 中,则编译器将发出警告。
一个 Set 工具
对于泛型方法的另一个示例,请考虑由 Set 表示的数学关系。这些被方便地定义为可用于所有不同类型的泛型方法:
import java.util.HashSet;
import java.util.Set;public class Sets {public static <T> Set<T> union(Set<T> a, Set<T> b) {Set<T> result = new HashSet<>(a);result.addAll(b);return result;}public static <T>Set<T> intersection(Set<T> a, Set<T> b) {Set<T> result = new HashSet<>(a);result.retainAll(b);return result;}// Subtract subset from superset:public static <T> Set<T>difference(Set<T> superset, Set<T> subset) {Set<T> result = new HashSet<>(superset);result.removeAll(subset);return result;}// Reflexive--everything not in the intersection:public static <T> Set<T> complement(Set<T> a, Set<T> b) {return difference(union(a, b), intersection(a, b));}
}
前三个方法通过将第一个参数的引用复制到新的 HashSet 对象中来复制第一个参数,因此不会直接修改参数集合。因此,返回值是一个新的 Set 对象。
这四种方法代表数学集合操作: union()
返回一个包含两个参数并集的 Set , intersection()
返回一个包含两个参数集合交集的 Set , difference()
从 superset 中减去 subset 的元素 ,而 complement()
返回所有不在交集中的元素的 Set。作为显示这些方法效果的简单示例的一部分,下面是一个包含不同水彩名称的 enum :
public enum Watercolors {ZINC, LEMON_YELLOW, MEDIUM_YELLOW, DEEP_YELLOW,ORANGE, BRILLIANT_RED, CRIMSON, MAGENTA,ROSE_MADDER, VIOLET, CERULEAN_BLUE_HUE,PHTHALO_BLUE, ULTRAMARINE, COBALT_BLUE_HUE,PERMANENT_GREEN, VIRIDIAN_HUE, SAP_GREEN,YELLOW_OCHRE, BURNT_SIENNA, RAW_UMBER,BURNT_UMBER, PAYNES_GRAY, IVORY_BLACK
}
为了方便起见(不必全限定所有名称),将其静态导入到以下示例中。本示例使用 EnumSet 轻松从 enum 中创建 Set 。在这里,静态方法 EnumSet.range()
要求提供所要在结果 Set 中创建的元素范围的第一个和最后一个元素:
import java.util.EnumSet;
import java.util.Set;import static com.example.test.Sets.*;
import static com.example.test.Watercolors.*;public class WatercolorSets {public static void main(String[] args) {Set<Watercolors> set1 =EnumSet.range(BRILLIANT_RED, VIRIDIAN_HUE);Set<Watercolors> set2 =EnumSet.range(CERULEAN_BLUE_HUE, BURNT_UMBER);System.out.println("set1: " + set1);System.out.println("set2: " + set2);System.out.println("union(set1, set2): " + union(set1, set2));Set<Watercolors> subset = intersection(set1, set2);System.out.println("intersection(set1, set2): " + subset);System.out.println("difference(set1, subset): " +difference(set1, subset));System.out.println("difference(set2, subset): " +difference(set2, subset));System.out.println("complement(set1, set2): " +complement(set1, set2));}
}
接下来的例子使用 Sets.difference()
方法来展示 java.util 包中各种 Collection 和 Map 类之间的方法差异:
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;public class CollectionMethodDifferences {static Set<String> methodSet(Class<?> type) {return Arrays.stream(type.getMethods()).map(Method::getName).collect(Collectors.toCollection(TreeSet::new));}static void interfaces(Class<?> type) {System.out.print("Interfaces in " +type.getSimpleName() + ": ");System.out.println(Arrays.stream(type.getInterfaces()).map(Class::getSimpleName).collect(Collectors.toList()));}static Set<String> object = methodSet(Object.class);static {object.add("clone");}static voiddifference(Class<?> superset, Class<?> subset) {System.out.print(superset.getSimpleName() +" extends " + subset.getSimpleName() +", adds: ");Set<String> comp = Sets.difference(methodSet(superset), methodSet(subset));comp.removeAll(object); // Ignore 'Object' methodsSystem.out.println(comp);interfaces(superset);}public static void main(String[] args) {System.out.println("Collection: " +methodSet(Collection.class));interfaces(Collection.class);difference(Set.class, Collection.class);difference(HashSet.class, Set.class);difference(LinkedHashSet.class, HashSet.class);difference(TreeSet.class, Set.class);difference(List.class, Collection.class);difference(ArrayList.class, List.class);difference(LinkedList.class, List.class);difference(Queue.class, Collection.class);difference(PriorityQueue.class, Queue.class);System.out.println("Map: " + methodSet(Map.class));difference(HashMap.class, Map.class);difference(LinkedHashMap.class, HashMap.class);difference(SortedMap.class, Map.class);difference(TreeMap.class, Map.class);}
}