文章目录
- 第二十章 泛型
- 1. 简单泛型
- 1.1 简单泛型
- 1.2 一个元组类库
- 2. 泛型接口
- 3. 泛型方法
- 3.1 泛型方法
- 3.2 变长参数和泛型方法
- 4. 构建复杂模型
第二十章 泛型
普通的类和方法只能使用特定的类型:基本数据类型或类类型。如果编写的代码需要应用于多种类型,这种严苛的限制对代码的束缚就会很大。
多态是一种面向对象思想的泛化机制。你可以将方法的参数类型设为基类,这样的方法就可以接受任何派生类作为参数,包括暂时还不存在的类。
拘泥于单一的继承体系太过局限,如果方法以接口而不是类作为参数,限制就宽松多了,只要实现了接口就可以。
即便是接口也还是有诸多限制。一旦指定了接口,它就要求你的代码必须使用特定的接口。而我们希望编写更通用的代码,能够适用“非特定的类型”,而不是一个具体的接口或类。
这就是泛型的概念,是 Java 5 的重大变化之一。在很多情况下,它可以使代码更直接更优雅。
1. 简单泛型
1.1 简单泛型
促成泛型出现的最主要的动机之一是为了创建集合类。
先看一个只能持有单个对象的类。这个类可以明确指定其持有的对象的类型:
package generics;
class Automobile {}
public class Holder1 {
private Automobile a;
public Holder1(Automobile a) {
this.a = a;
}
Automobile get() {
return a;
}
}
这个类的可复用性不高,它无法持有其他类型的对象。我们可不希望为碰到的每个类型都编写一个新的类。
在 Java 5 之前,我们可以让这个类直接持有 Object 类型的对象:
package generics;
public class ObjectHolder {
private Object a;
public ObjectHolder(Object a) {
this.a = a;
}
public Object get() {
return a;
}
public void set(Object a) {
this.a = a;
}
public static void main(String[] args) {
ObjectHolder h2 = new ObjectHolder(new Automobile());
Automobile a = (Automobile) h2.get();
h2.set("Not an Automobile");
String s = (String) h2.get();
h2.set(1);
Integer x = (Integer) h2.get();
}
}
与其使用 Object ,我们更希望先指定一个类型占位符,稍后再决定具体使用什么类型。要达到这个目的,需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类时,再用实际的类型替换此类型参数。在下面的例子中, T 就是类型参数:
package generics;
public class GenericHolder<T> {
private T t;
public GenericHolder() {
}
public T get() {
return t;
}
public void set(T t) {
this.t = t;
}
public static void main(String[] args) {
GenericHolder<Automobile> h3 = new GenericHolder<>();
h3.set(new Automobile()); // 此处有类型校验
Automobile a = h3.get(); // 无需类型转换
// h3.set("Not an Automobile"); // 报错
// h3.set(1); // 报错
}
}
这就是 Java 泛型的核心概念:你只需告诉编译器要使用什么类型,剩下的细节交给它来处理。
1.2 一个元组类库
有时一个方法需要能返回多个对象。而 return 语句只能返回单个对象,解决方法就是创建一个对象,用它打包想要返回的多个对象。当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。
但是有了泛型,我们就可以一劳永逸。同时,还获得了编译时的类型安全。
这个概念称为元组,它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 数据传输对象 或 信使 )。
下面是一个可以存储两个对象的元组:
package onjava;
public class Tuple2<A, B> {
public final A a1;
public final B a2;
// 构造函数传入要存储的对象。这个元组隐式地保持了其中元素的次序。
public Tuple2(A a1, B a2) {
this.a1 = a1;
this.a2 = a2;
}
public String rep() {
return a1 + ", " + a2;
}
@Override
public String toString() {
return "(" + rep() + ")";
}
}
以利用继承机制实现长度更长的元组。添加更多的类型参数就行了:
package onjava;
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;
}
@Override
public String rep() {
return super.rep() + ", " + a3;
}
}
package onjava;
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;
}
@Override
public String rep() {
return super.rep() + ", " + a4;
}
}
package onjava;
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;
}
@Override
public String rep() {
return super.rep() + ", " + a5;
}
}
演示需要,再定义两个类:
package generics;
public class Amphibian {
}
package generics;
public class Vehicle {
}
使用元组时,你只需要定义一个长度适合的元组,将其作为返回值即可。注意下面例子中方法的返回类型:
package generics;
import onjava.Tuple2;
import onjava.Tuple3;
import onjava.Tuple4;
import onjava.Tuple5;
public class TupleTest {
static Tuple2<String, Integer> f() {
return new Tuple2<>("hi", 47);
}
static Tuple3<Amphibian, String, Integer> g() {
return new Tuple3<>(new Amphibian(), "hi", 47);
}
static Tuple4<Vehicle, Amphibian, String, Integer> h() {
return new Tuple4<>(new Vehicle(), new Amphibian(), "hi", 47);
}
static Tuple5<Vehicle, Amphibian, String, Integer, Double> k() {
return new Tuple5<>(new Vehicle(), new Amphibian(), "hi", 47, 11.1);
}
public static void main(String[] args) {
Tuple2<String, Integer> ttsi = f();
System.out.println(ttsi);
// ttsi.a1="there"; // 编译错误,因为 final 不能重新赋值
System.out.println(g());
System.out.println(h());
System.out.println(k());
}
}
输出:
(hi, 47)
(generics.Amphibian@7699a589, hi, 47)
(generics.Vehicle@58372a00, generics.Amphibian@4dd8dc3, hi, 47)
(generics.Vehicle@6d03e736, generics.Amphibian@568db2f2, hi, 47, 11.1)
2. 泛型接口
泛型也可以应用于接口。例如 生成器,这是一种专门负责创建对象的类。
一般而言,一个生成器只定义一个方法,用于创建对象。例如 java.util.function 类库中的 Supplier 就是一个生成器,调用其 get() 获取对象。 get() 是泛型方法,返回值为类型参数 T 。
package generics.coffee;
public class Coffee {
private static long counter = 0;
private final long id = counter++;
@Override
public String toString() {
return getClass().getSimpleName() + " " + id;
}
}
package generics.coffee;
public class Americano extends Coffee{
}
package generics.coffee;
public class Breve extends Coffee{
}
package generics.coffee;
public class Cappuccino extends Coffee{
}
package generics.coffee;
public class Latte extends Coffee{
}
package generics.coffee;
public class Mocha extends Coffee{
}
编写一个类,实现 Supplier 接口,它能够随机生成不同类型的 Coffee 对象:
package generics.coffee;
import java.util.Iterator;
import java.util.Random;
import java.util.function.Supplier;
import java.util.stream.Stream;
public class CoffeeSupplier
implements 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() {
System.out.println("1");
}
// For iteration:
private int size = 0;
public CoffeeSupplier(int sz) {
System.out.println("2");
size = sz;
}
@Override
public Coffee get() {
System.out.println("3");
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;
@Override
public boolean hasNext() {
System.out.println("4");
return count > 0;
}
@Override
public Coffee next() {
System.out.println("5");
count--;
return CoffeeSupplier.this.get();
}
@Override
public void remove() {
System.out.println("6");
throw new UnsupportedOperationException();
}
}
@Override
public Iterator<Coffee> iterator() {
System.out.println("7");
return new CoffeeIterator();
}
public static void main(String[] args) {
Stream.generate(new CoffeeSupplier())
.limit(5)
.forEach(System.out::println);
System.out.println("------------------");
for (Coffee c : new CoffeeSupplier(5)) {
System.out.println(c);
}
}
}
输出:
1
3
Americano 0
3
Latte 1
3
Americano 2
3
Mocha 3
3
Mocha 4
------------------
2
7
4
5
3
Breve 5
4
5
3
Americano 6
4
5
3
Latte 7
4
5
3
Cappuccino 8
4
5
3
Cappuccino 9
4
—PS:为了了解代码执行顺序,加了几个输出语句。迷糊的话,可以看下这个大佬的文章:字节面试官问:Iterator和erable有什么区别?
3. 泛型方法
3.1 泛型方法
泛型方法独立于类而改变方法。
如果方法是 static 的,则无法访问该类的泛型类型参数,因此,如果使用了泛型类型参数,则它必须是泛型方法。
要定义泛型方法,请将泛型参数列表放置在返回值之前,如下所示:
package generics;
public class GenericMethods {
// PS:<T> 泛型参数列表
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);
}
}
输出:
java.lang.String
java.lang.Integer
java.lang.Double
java.lang.Float
java.lang.Character
generics.GenericMethods
对于泛型类,必须在实例化该类时指定类型参数。使用泛型方法时,通常不需要指定参数类型,因为编译器会找出这些类型。 这称为 类型参数推断。
3.2 变长参数和泛型方法
泛型方法和变长参数列表可以很好地共存:
package generics;
import java.util.ArrayList;
import java.util.List;
public class GenericVarargs {
@SafeVarargs
public 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);
}
}
输出:
[A]
[A, B, C]
[A, B, C, D, E, F, F, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z]
4. 构建复杂模型
泛型的一个重要好处是能够简单安全地创建复杂模型。例如,我们可以轻松地创建一个元组列表:
package generics;
import onjava.Tuple4;
import java.util.ArrayList;
public class TupleList<A, B, C, D> extends ArrayList<Tuple4<A, B, C, D>> {
public static void main(String[] args) {
TupleList<Vehicle, Amphibian, String, Integer> tl = new TupleList<>();
tl.add(TupleTest2.h());
tl.add(TupleTest2.h());
tl.forEach(System.out::println);
}
}
class TupleTest2 {
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 Tuple4<Vehicle, Amphibian, String, Integer> h() {
return tuple(new Vehicle(), new Amphibian(), "hi", 47);
}
}
输出:
(generics.Vehicle@306a30c7, generics.Amphibian@b81eda8, hi, 47)
(generics.Vehicle@68de145, generics.Amphibian@27fa135a, hi, 47)
这将产生一个功能强大的数据结构,而无需太多代码。
(图网,侵删)