泛型
前言
泛型出现的原因:一般的类和方法,只能使用具体的类型:要么是基本类型,要么是自定义的类。如果要编写可以应用于多中类型的代码,这种刻板的限制对代码的束缚性很大。
- oop中,多态算是一种泛化机制。只是有一些性能损耗
- 之前一直是单继承结构,使得程序受限太多。如果方法的参数是一个接口,这种限制就放松了很多。即使这样,很多时候也不够
所以,泛型出现了:泛型实现了参数化列表的概念,使代码可以应用于多种类型。泛型出现的目的:希望类或方法能够具备最广泛的表达能力。如何做到这一点就是通过解耦 类或方法与所使用的类型之间的约束。
泛型的优点:Java泛型使代码更直接更优雅。
泛型的局限:Java泛型的作用很有限,不像C++那么
一、简单泛型
泛型出现最主要的原因就是为了创造容器类。容器类是你在开发中最具重用性的类库(之前是这样,但是现在有了流式编程)
泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。
在泛型类中它的特点就是将泛型跟在类名称的后面。这和泛型方法定义不同
public class Holder<T> {
private T a;
public Holder(T a) {
this.a = a;
}
public T getA() {
return a;
}
public void setA(T a) {
this.a = a;
}
}
当你创建Holder对象时,必须指明想持有什么类型的对象,将其置于尖括号内。它以及它的子类都可以存入(多态),而取的时候都会是正确的类型。
这就是Java泛型的核心概念:告诉编译器想使用什么类型,然后编译器帮你处理一切细节。
其实Java泛型和一般类型没有太大差别,只不过泛型有类型参数而已。
1.1 元组类库
想要仅一次方法调用就能返回多个对象,可是return语句只允许返回单个对象,因此,之前解决方案就是创建一个对象,用它来持有想要返回的多个对象。所以当有这样的需求时,每次都需要创建一个类。
但是有了泛型,我们就能够一次性的解决该问题,以后再也不在这个问题上浪费时间了。同时可以保证编译安全。
元组:它是将一组对象直接打包存储于它中的一个单一对象。这个容器允许读取数据,但是不能存放新的对象。也叫数据传输对象或者信使。
下面是一个简单的二维元组,它能够持有两个对象:
public class TwoTuple<A, B> {
public final A first;
public final B second;
public TwoTuple(A a, B b){
first = a;
second = b;
}
public String toString(){
return "(" + first + "," + second +")";
}
}
构造器捕获了要存储的对象,并且将字段的初始值赋给它们。toString是一个便利函数,用来显示列表中的值。注意:元组隐含地保持了其中元素的次序。
第一次阅读上面代码时,会发现上面的代码违反了Java编程的安全性原则。first和second应该声明为private,然后提供getter和setter方法。
但是这两句的真正的含义是:客户端程序可以读取first和second对象,然后可以随心所欲的使用这两个对象。但是它们却无法将其他值赋给它们。因为final声明为你买了相同的安全保险,而且这种方式更简洁明了。还有另一种设计考虑,即你确实想要让客户端程序员改变类中字段所引用的对象。然而采用上述做法无疑是更安全的做法。因为如果想要改变引用就会强制他们另外创建一个新的TwoTuple对象。
此外,我们可以利用继承机制实现长度更长的元组。
public class ThreeTuple<A, B, C> extends TwoTuple<A, B>{
public final C third;
public ThreeTuple(A a, B b, C c) {
super(a, b);
this.third = c;
}
@Override
public String toString() {
return "{" +
"third=" + third +
", first=" + first +
", second=" + second +
'}';
}
}
使用它们的时候,你只需要定义一个长度合适的元组,将其作为方法的返回值,然后在return语句中创建该元组,并返回即可。
这里的ABC与T有异曲同工之妙。
下面是一个实例:
class Amphibian{}
class Vehicle{}
public class TupleTest {
static TwoTuple<String, Integer> f(){
return new TwoTuple<>("hi", 47);
}
static ThreeTuple<Amphibian, String, Integer> g() {
return new ThreeTuple<>(new Amphibian(), "hi", 47);
}
static FourTuple<Vehicle, Amphibian, String, Integer> h() {
return new FourTuple<>(new Vehicle(), new Amphibian(), "hi", 47);
}
public static void main(String[] args) {
TwoTuple<String, Integer> ttsi = f();
System.out.println(ttsi);
ThreeTuple<Amphibian, String,Integer> ttasi = g();
System.out.println(ttasi);
FourTuple<Vehicle, Amphibian, String, Integer> ftvasi = h();
System.out.println(ftvasi);
}
}
/*
输出:
(hi,47)
{third=47, first=com.gui.demo.thingInJava.collectionAndMap.Amphibian@41629346, second=hi}
FourTuple{fourth=47, third=hi, first=com.gui.demo.thingInJava.collectionAndMap.Vehicle@404b9385, second=com.gui.demo.thingInJava.collectionAndMap.Amphibian@6d311334}
*/
由于有了泛型,你可以很容易的创建元组,令其返回一组任意类型的对象。而你所要做的,就是编写表达式而已。
上面的代码中,new表达式有点啰嗦,之后我们会用泛型来简化它。
1.2 RandomList
使用泛型,构建一个可以应用于各种类型的对象的工具类:假设我们需要一个持有特定类型对象的列表,每次调用其中的select(),它就可以随机地选择一个元素返回。
public class RandomList<T> {
private ArrayList<T> storage = new ArrayList<>();
private Random rand = new Random(47);
public void add(T item) {
storage.add(item);
}
public T select(){
return storage.get(rand.nextInt(storage.size()));
}
public static void main(String[] args) {
RandomList<String> rs = new RandomList<>();
for (String s : ("The quick brown fox jumped over the lazy brown dog").split(" ")) {
rs.add(s);
}
for (int i = 0; i < 11; i++) {
System.out.print(rs.select() + " ");
}
}
}
/*
输出:
brown over fox quick quick dog brown The brown lazy brown
*/
二、泛型接口:工厂方法设计模式
泛型也可以用于接口。例如一个生成器:专门负责创建对象的类。这是工厂方法设计模式的一种应用。
不过,当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数。
public interface Generator<T>{
T next();
}
这个工厂方法接口和之前的工厂设计模式的接口并没有太大的差别。
现在编写一个类来实现这个接口,它能够随机生成不同类型的Coffee对象:
public class CoffeeGenerator implements Generator<Coffee>, Iterable<Coffee> {
private Class[] types = {Latte.class, Mocha.class, Cappuccino.class};
private static Random rand = new Random(47);
public CoffeeGenerator() {
}
//对于Iterator
private int size = 0;
public CoffeeGenerator(int sz) {
this.size = sz;
}
@Override
public Coffee next() {
try {
return (Coffee) types[rand.nextInt(types.length)].newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
class CoffeeIterator implements Iterator<Coffee>{
int count = size;
public boolean hasNext(){
return count > 0;
}
public Coffee next(){
count--;
return CoffeeGenerator.this.next();
}
//不做实现
public void remove(){
throw new UnsupportedOperationException();
}
}
/**
* 当对这个类调用遍历时,就会调用Iterator方法。但是执行这个方法创建
对内部类的引用后,遍历时就会执行内部类CoffeeIterator的方法
* @author gt
* @date 2021/5/10 14:26
* @return Coffee
*/
@Override
public Iterator<Coffee> iterator() {
return new CoffeeIterator();
}
public static void main(String[] args) {
CoffeeGenerator gen = new CoffeeGenerator();
for (int i = 0; i < 5; i++) {
System.out.println(gen.next());
}
for (Coffee c : new CoffeeGenerator(5)) {
System.out.println(c);
}
}
}
/*
输出:
Cappuccino 0
Cappuccino 1
Mocha 2
Cappuccino 3
Mocha 4
Cappuccino 5
Mocha 6
Cappuccino 7
Latte 8
Mocha 9
*/
参数化的Generator接口确保next()的返回值是参数的类型。CoffeeGenerator同时还实现了Iterator接口,所以它可以在foreach循环语句中使用。当对其遍历时Iterator就会调用默认的hasnext()和next()。不过,它还需要一个“末端哨兵”来判断何时停止。这是第二个有参构造器的功能。
三、泛型方法
泛型方法使得你能够独立于类而发生变化。
这里有一个指导原则:无论何时,只要可以都使用泛型方法。也就是说,如果你能用泛型方法将整个泛型类取代,那就应该用泛型方法,因为它可以使得事情更加清楚明白。此外,如果方法是static的,则无法访问该泛型类的类型参数。所以如果static方法需要使用泛型能力,就必须使它称为泛型方法。
要定义泛型方法,只需要将泛型参数列表置于返回值之前(泛型方法是先声明在使用):
public class GenericMethods {
public <T> void f(T x){
System.out.println(x.getClass().getSimpleName());
}
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);
}
}
/*
输出:
String
Integer
Double
Float
Character
GenericMethods
*/
方法f()拥有参数类型,我们可以像调用普通方法一样调用它,而且它就像被无限次重载过,甚至可以接受GenericMethods作为其类型参数。
public class New {
public static <K, V> Map<K, V> map() {
return new HashMap<>();
}
public static <T> List<T> list() {
return new ArrayList<>();
}
public static <T> LinkedList<T> linkedList() {
return new LinkedList<>();
}
public static <T> Set<T> set() {
return new HashSet<>();
}
public static <T> Queue<T> queue() {
return new LinkedList<>();
}
/*
public static void main(String[] args) {
Map<String, List<String>> sls = New.map();
List<String> ls = New.list();
LinkedList<String> lls = New.linkedList();
Set<String> ss = New.set();
Queue<String> qs = New.queue();
}*/
}
这是一个工具类:利用泛型方法简化类的生成,专门用来创建各种常用的容器。
3.1 可变参数与泛型方法
public class GenericVarargs {
@SafeVarargs
public static <T> List<T> makeList(T... args) {
List<T> results = New.list();
for (T item : args) {
results.add(item);
}
return results;
}
public static void main(String[] args) {
List<String> ls = makeList("A");
System.out.println(ls);
ls = makeList("A", "B", "C");
System.out.println(ls);
}
}
/*
输出:
[A]
[A, B, C]
*/
每次调用New.list()方法都返回一个新的ArrayList,所以会失去之前list的引用。
3.2 一个通用的生成器Generator
下面的程序可以为任何类创建一个Generator,这个类要具备的条件是:
(1)必须声明为public
(2)必须具备默认的构造器
public interface Generator<T> {
T next();
}
/*************************************************/
public class BasicGenerator<T> implements Generator<T> {
private Class<T> type;
public BasicGenerator(Class<T> type) {
this.type = type;
}
@Override
public T next() {
try {
//type的类型必须是public的
return type.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public static <T> Generator<T> create(Class<T> type) {
return new BasicGenerator<>(type);
}
}
要创建想要的对象,只需调用create()方法,并传入想要生成的类型。
这里有一个简单类:
public class CountedObject {
private static long counter = 0;
private final long id = counter++;
public long id(){
return id;
}
public String toString(){
return "CountedObject " + id;
}
}
它可以记录下创建了多少个此类的实例,并通过tostring()方法告诉我们编号。
使用BasicGenerator,你可以很容易的为CountedObject创建一个生成器。
public class BasicGeneratorDemo {
private static Generator<CountedObject> gco = BasicGenerator.create(CountedObject.class);
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
System.out.println(gco.next());
}
}
}
/*
输出:
CountedObject 0
CountedObject 1
CountedObject 2
CountedObject 3
CountedObject 4
*/
由其是创建多个对象时,使用泛型方法可以大大减少我们要编写的代码。Java泛型要求传入class对象,以便它进行类型推断。
而不使用create方法也可以显式的构建generator:就是使用new
3.3 泛型的Supplier
这是一个为任意具有无参构造方法的类去生成 Supplier 的类。为了减少键入,它还包含一个用于生成 BasicSupplier 的泛型方法:
public class BasicSupplier<T> implements Supplier<T> {
private Class<T> type;
public BasicSupplier(Class<T> type) {
this.type = type;
}
@Override
public T get() {
try {
return type.newInstance();
} catch (InstantiationException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
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.class>(MyType.class)
。
在实现一下上面的类:
public class BasicSupplierDemo {
public static void main(String[] args) {
//BasicSupplier 实现
Stream.generate(BasicSupplier.create(CountedObject.class))
.limit(5)
.forEach(System.out::println);
//这个是用supplier实现
Stream.generate(CountedObject::new)
.limit(5)
.forEach(System.out::println);
}
}
/*
output:
CountedObject{id=0}
CountedObject{id=1}
CountedObject{id=2}
CountedObject{id=3}
CountedObject{id=4}
CountedObject{id=5}
CountedObject{id=6}
CountedObject{id=7}
CountedObject{id=8}
CountedObject{id=9}
*/
CountedObject 类可以跟踪自身创建了多少个实例,并通过toString() 可以知道这些实例的数量。BasicSupplier 可以轻松地为 CountObject 创建Supplier。
泛型方法减少了产生Supplier 对象所需的代码量(我现在看不出来,2021年5月26日17:43:16)。Java 泛型强制传递Class对象,以便在 create() 方法中将其用于类型推断。
3.4 简化元组
通过泛化方法和使用static配合,我们可以使得元组变成更加通用的工具类库。
通过重载static方法创建元组:
import com.gui.demo.thingInJava.collectionAndMap.FourTuple;
import com.gui.demo.thingInJava.collectionAndMap.ThreeTuple;
import com.gui.demo.thingInJava.collectionAndMap.TwoTuple;
public class GeneratorTuple{
public static <A, B> TwoTuple<A, B> tuple(A a, B b) {
return new TwoTuple<>(a, b);
}
public static <A, B, C> ThreeTuple<A, B, C> tuple(A a, B b, C c) {
return new ThreeTuple<>(a, b, c);
}
public static <A, B, C, D> FourTuple<A, B, C, D> tuple(A a, B b, C c, D d) {
return new FourTuple<>(a, b, c, d);
}
}
四、匿名内部类
泛型可以应用于内部类和匿名内部类。
下面的示例展示了使用匿名内部类实现了Generator接口:
class Customer{
private static long count = 0;
private final long id = count++;
private Customer() {
}
@Override
public String toString() {
return "Customer{" + "id=" + id + '}';
}
//生成器方法
public static Generator<Customer> generator(){
return Customer::new;
/*
上面的匿名内部类的Lambda表达式等价于下面的
return new Generator<Customer>() {
@Override
public Customer next() {
return new Customer();
}
};
*/
}
}
class Teller{
private static long count = 0;
private final long id = count++;
private Teller(){}
@Override
public String toString() {
return "Teller{" + "id=" + id + '}';
}
public static Generator<Teller> generator = Teller::new;
}
public class BankTeller {
public static void serve(Teller t, Customer c) {
System.out.println(t + " serves " + c);
}
public static void main(String[] args) {
Random rand = new Random(47);
Queue<Customer> queue = new LinkedList<>();
Generators.fill(queue, Customer.generator(),5);
List<Teller> list = new ArrayList<>();
Generators.fill(list, Teller.generator, 5);
for (Customer c : queue) {
serve(list.get(rand.nextInt(list.size())),c);
}
}
}
/*
shuchu :
Teller{id=3} serves Customer{id=0}
Teller{id=0} serves Customer{id=1}
Teller{id=3} serves Customer{id=2}
Teller{id=1} serves Customer{id=3}
Teller{id=1} serves Customer{id=4}
*/
Customer和Teller都只有private的构造器,这样可以强制你使用Generator对象。
Customer有一个Generator()方法,每次执行它都会生成一个新的Generator<Customer>
对象。而Teller就只创建了一个public的Generator对象。
这里之所以不将Customer和Teller定义为接口是因为他们中的Generator方法或对象都是static的。
这里的匿名内部类是用lambda表达式写的,所以看起来很简洁
五、 构建复杂的模型
泛型的一个重要好处就是:能够简单而安全的创建复杂的模型。
public class TupleList<A, B, C, D> extends ArrayList<FourTuple<A, B, C, D>> {
public static void main(String[] args) {
TupleList<Vehicle, Amphibian, String, Integer> tuples = new TupleList<>();
tuples.add(TupleTest.h());
tuples.add(TupleTest.h());
for (FourTuple<Vehicle, Amphibian, String, Integer> f : tuples) {
System.out.println(f);
}
}
}
六、Java的擦除
public class ErasedTypeEquivalence {
public static void main(String[] args) {
Class c1 = new ArrayList<String>().getClass();
Class c2 = new ArrayList<Integer>().getClass();
System.out.println(c1 == c2);
}
}
这段代码的输出是true。
在泛型代码内部,无法获得任何有关泛型参数类型的信息。
利用泛型你可以知道它的类型参数和类型边界这一类的信息,但是你却无法知道用来创建某个特定实例的实际参数类型。
这是因为Java泛型是用擦除来实现的。这意味着当你使用泛型时,任何具体的类型信息都被擦除了,你唯一知道的就是你在使用一个对象。上面代码中的List都被擦除信息而回到了它的原始类型,即List。
class HasF{
public void f(){
System.out.println("HasF.f()");
}
}
class Manipulator<T>{
private T obj;
public Manipulator(T t) {
obj = t;
}
//这句报错
public void manipulate() {
obj.f();
}
}
public class Manipulation {
public static void main(String[] args) {
Manipulator<HasF> manipulator = new Manipulator<>(new HasF());
manipulator.manipulate();
}
}
因为擦除,Java 编译器无法将调用 obj 的f() 方法这一需求隐射到 HasF 具有 f() 这一事实上。为了调用 f() ,我们必须协助泛型类,给泛型一个边界,以此告诉编译器只能接受遵循这个边界的类型。这里重用了 extends 关键字:class Manipulator<T extends HasF>
;边界声明T 必须是 HasF 类型或其子类。
我们说泛型类型参数会擦除到它的第一个边界(可能有多个边界),上边的示例,T 擦除到了HasF,就像在类的声明中用 HasF 替换了 T 一样。
这里有个很重要的一点:泛型只有在类型参数比某个具体类型(以及其子类)更加“泛化”(代码能跨多个类工作)时才有效。
6.1 擦除的问题
擦除的正当理由是从非泛化代码到泛化代码的转变过程,以及在不破坏现有类库的情况下将泛型融入到语言中。
擦除的代价是明显的:泛型不能用于显式的引用运行时类型的操作中,例如转型、instanceof 操作和 new 表达式。因为所有关于参数的具体类型信息都丢失了。
6.2 边界处的动作
如果让你实现一个创建泛型数组的方法,你会怎么做呢?
T[] create(Class<T> c, int size) {
return (T[]) Array.newInstance(c, size);
}
对于在泛型中创建数组,使用Array.newInstance()
是推荐的方式。
尽管我们知道从擦除中 被移除了——在运行时,类内部没有任何 ,如果在创建 List 的同时向其中放入一些对象呢,像这样:
public class FilledList1<T> extends ArrayList<T> {
FilledList1(Supplier<T> gen, int size) {
Suppliers.fill(this, gen, size);
}
public FilledList1(T t, int size) {
for (int i = 0; i < size; i++) {
this.add(t);
}
}
public static void main(String[] args) {
List<String> list = new FilledList1<>("Hello", 4);
System.out.println(list);
//Supplier 版本
List<Integer> integerList = new FilledList1<>(() -> 47, 4);
System.out.println(integerList);
}
}
/*
output:
[Hello, Hello, Hello, Hello]
[47, 47, 47, 47]
*/
即使编译器无法得知add() 中的 T 的任何信息,但它仍可以在编译期确保你放入 FilledList1 中的对象是T类型。因此,即使擦除移除了方法或类中的实际类型的信息,编译器仍可以确保方法或类中使用的类型的内部一致性。
因为擦除移除了方法体中的类型信息,所以在运行时的问题就是边界:即对象进入和离开方法的地点。这些正是编译器在编译期执行类型检查并插入转型代码的地点。
所以只需要记住一点:“边界就是动作发生的地方”。
6.3 补偿擦除
因为擦除,我们将失去执行代码中某些操作的能力。 因为无法在运行时知道确切类型信息:
public class Erased<T> {
private final int SIZE = 100;
public void f(Object arg) {
//编译错误
if (arg instanceof T) {}
//编译错误
T var = new T();
//编译错误
T[] array = new T[SIZE];
//Unchecked cast: 'java.lang.Object[]' to 'T[]'
T[] array2 = (T[]) new Object[SIZE];
}
}
有时,我们可以绕过这些问题编程,但是有时必须通过引入类型标签来补偿擦除。这意味着为所需的类型显式传递一个 Class 对象,以便在类型表达式中使用它。
如:在上一个程序中尝试使用 instanceof 会失败,但是可以使用类型标签:isInstance()
:可以动态的比较。
第二个问题是试图在Erased.java 中 new T() 是行不通的,部分原因是因为擦除,部分原因是编译器无法验证T是否具有默认(无参)构造器。
Java中的解决方案是传入一个工厂对象,并使用该对象创建新实例。方便的工厂对象只是class对象,因此 ,如果使用类型标记,则可以使用 newInstance() 创建该类型的对象:
class ClassAsFactory<T> implements Supplier<T> {
Class<T> kind;
ClassAsFactory(Class<T> kind) {
this.kind = kind;
}
@Override
public T get() {
try {
return kind.newInstance();
} catch (InstantiationException | IllegalAccessException exception) {
throw new RuntimeException(exception);
}
}
}
class Employee{
@Override
public String toString() {
return "Employee{}";
}
}
public class InstantiateGenericType{
public static void main(String[] args) {
ClassAsFactory<Employee> caf = new ClassAsFactory<>(Employee.class);
System.out.println(caf.get());
ClassAsFactory<Integer> af = new ClassAsFactory<>(Integer.class);
try {
System.out.println(af.get());
} catch (Exception e) {
System.out.println(e.getMessage());
}
}
}
/*
output:
Employee{}
java.lang.InstantiationException: java.lang.Integer
*/
这样写虽然可以编译,但是可以看到 ClassAsFactory 运行时已经报错,这是因为Integer 没有无参构造器。因为错误不是在编译时捕获的,所以这种写法不推荐,建议使用显式工厂(Supplier)并约束类型,以便只有实现该工厂的类可以这样创建对象。
class IntegerFactory implements Supplier<Integer>{
private int i = 0;
@Override
public Integer get() {
return ++i;
}
}
class Widget{
private int id;
Widget(int n) {
id = n;
}
@Override
public String toString() {
return "Widget{" + id +
'}';
}
public static class Factory implements Supplier<Widget> {
private int i = 0;
@Override
public Widget get() {
return new Widget(++i);
}
}
}
class Fudge{
private static int count = 1;
private int n = count++;
@Override
public String toString() {
return "Fudge{" + n +
'}';
}
}
class Foo<T>{
private List<T> x = new ArrayList<>();
Foo(Supplier<T> factory) {
Suppliers.fill(x, factory, 5);
}
@Override
public String toString() {
return "Foo{" +
"x=" + x +
'}';
}
}
public class FactoryConstraint {
public static void main(String[] args) {
System.out.println(new Foo<>(new IntegerFactory()));
System.out.println(new Foo<>(new Widget.Factory()));
System.out.println(new Foo<>(Fudge::new));
}
}
/*
output:
Foo{x=[1, 2, 3, 4, 5]}
Foo{x=[Widget{1}, Widget{2}, Widget{3}, Widget{4}, Widget{5}]}
Foo{x=[Fudge{1}, Fudge{2}, Fudge{3}, Fudge{4}, Fudge{5}]}
*/
IntegerFactory 本身就是通过实现 Supplier 的工厂。 Widget 包含一个内部类,它是一个工厂。还要注意,Fudge 并没有做任何类似于工厂的操作,并且传递 Fudge::new 仍然会产生工厂行为,因为编译器将对函数方法 ::new 的调用转换为对 get() 的调用。
模板方法设计模式
另一种方法就是模板方法设计模式。在以下示例中,create() 是模板方法,在子类中被重写以生成该类型的对象:
abstract class GenericWithCreate<T> {
final T element;
GenericWithCreate() {
element = create();
}
abstract T create();
}
class X{}
class XCreator extends GenericWithCreate<X> {
@Override
X create() {
return new X();
}
void f() {
System.out.println(
element.getClass().getSimpleName());
}
}
public class CreatorGeneric {
public static void main(String[] args) {
XCreator xc = new XCreator();
xc.f();
}
}
/*
output:
X
*/
GenericWithCreate 包含 element 字段,并通过无参构造函数强制其初始化,该构造函数又调用抽象的 create() 方法。这种创建方式可以在子类中定义,同时建立T 的类型。
6.4 泛型边界
边界(bounds)在本章的前面进行了简要介绍。边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则,但潜在的更重要的效果是我们可以在绑定的类型中调用方法。
因为擦除的原因,Java 泛型使用了extends 关键字。重要的是要记住以下几点::
- 当用于限定泛型类型时,extends 的含义与通常的意义截然不同。
- 如果使用了泛型限定,绑定之后则你可以调用边界的方法。
- 使用限定泛型时,都是先继承类之后才能使接口,顺序不能错。
- 此外,只能继承一个类而继承多个接口
- 泛型边界可以定义在类上,方法上,而不能定义在变量中。
interface HasColor{
Color getColor();
}
//此处是继承了一个接口,这是泛型限定边界的不同之处。在边界这里的extends和我们所理解的extends的含义完全不同
class WithColor<T extends HasColor> {
T item;
WithColor(T item){
this.item = item;
}
T getItem() {
return item;
}
//绑定之后,则可以调用继承类中的方法
Color color() {
return item.getColor();
}
}
class Coord {
public int x, y, z;
}
//这里注意绑定必须先继承类再继承接口
//多重绑定
class WithColorCoord<T extends Coord & HasColor> {
T item;
WithColorCoord(T item) {
this.item = item;
}
T getItem() {
return item;
}
Color color() {
return item.getColor();
}
int getX() {
return item.x;
}
int getY() {
return item.y;
}
int getZ() {
return item.z;
}
}
interface Weight {
int weight();
}
class Solid<T extends Coord & Weight & HasColor> {
T item;
Solid(T item) {
this.item = item;
}
T getItem() {
return item;
}
Color color() {
return item.getColor();
}
int getX() {
return item.x;
}
int getY() {
return item.y;
}
int getZ() {
return item.z;
}
int weight() {
return item.weight();
}
}
class Bounded extends Coord implements HasColor, Weight {
@Override
public Color getColor() {
return null;
}
@Override
public int weight() {
return 0;
}
}
public class BasicBounds {
public static void main(String[] args) {
Solid<Bounded> solid = new Solid<>(new Bounded());
solid.color();
solid.getItem();
solid.getX();
solid.getY();
solid.weight();
}
}
注意:这里的边界更直白的意思是:你往此容器中放置的类型必须继承了容器的边界且实现了其中的接口。
七、通配符
通配符我们已经学过了,在这里将进行更深入的探讨:
起始示例是要展示数组的一种特殊行为:你可以将派生类(子类)的数组赋值给基类的引用:
class Fruit{}
class Apple extends Fruit{}
class Jonathan extends Apple{}
class Orange extends Fruit{}
public class CovariantArrays {
public static void main(String[] args) {
Fruit[] fruits = new Apple[10];
fruits[0] = new Apple();
fruits[1] = new Jonathan();
//运行时的类型是Apple[],而不是Fruit[]或者Orange[]
try {
//编译的时候是允许你这么写的,但是运行时会报数组存储异常(ArrayStoreException)
fruits[0] = new Fruit();
} catch (Exception e) {
System.out.println(e);
}
try {
//编译的时候也是允许你去添加,但是运行时同样会报一样的错误
fruits[0] = new Orange();
} catch (Exception e) {
System.out.println(e);
}
}
}
/*
output:
java.lang.ArrayStoreException: com.gui.demo.thingInJava.collectionAndMap.erase.arrays.Fruit
java.lang.ArrayStoreException: com.gui.demo.thingInJava.collectionAndMap.erase.arrays.Orange
*/
main 中的第一行创建了 Apple 数组,并赋值给一个Fruit 数组引用。这是有意义的,因为Apple 也是一种 Fruit,因此 Apple 数组应该也是一个 Fruit 数组。下面的就要注意了:
但是,如果实际的数组类型是 Apple[],你可以在其中放置Apple 或 Apple的子类型,这在编译和运行期都可以。但是你也可以在数组中放置 Fruit 对象,编译期是通的过的,因为它有一个 Fruit[] 引用——它有什么理由不允许将 Fruit 对象或任何从 Fruit 继承出来的对象(比如 Orange),放置到这个数组中呢?因此在编译期,这是允许的。然而,运行时的数组机制知道它处理的是 Apple[],因此会在向数组中放置异构类型时抛出异常。
向上转型在这里不适用,你真正在做的是将一个数组赋给另一个数组。数组的行为是持有其他对象,这里能够赋值只是因为我们能够向上转型而已,所以很明显,数组对象可以保留有关它们包含的对象类型的规则。
数组赋值的报错会出现在运行时,而使用泛型可以将这个错误转移到编译期,更加方便我们发现错误:
//编译会报错
List<HasColor> list = new ArrayList<Bounded>();
可能在阅读这样的代码时我们会理所当然的认为:不能将Bounded 集合赋值给一个 HasColor 集合,即使前者实现了后者的接口。但是要记住:泛型不仅仅是关于集合,它真正要表达的是“不能把涉及 Bounded 的泛型赋值给一个涉及 HasColor 的泛型”。如果像在数组中的情况一样,编译器对代码的了解足够多,可以确定所涉及到的集合,那么它可能会允许你这样做,但是偏偏它不知道任何有关这方面的信息,所以它拒绝向上转型。最主要的是后者的集合在类型上不等价于前者的集合,即使Bounded 是 HasColor 类型!这就是我们在讨论的问题:我们在讨论集合类型,而不是集合持有对象的类型。与数组不同不同,泛型没有内建的协变机制。这是因为数组是完全在语言中定义的,因此可以具有编译期和运行时的内建检查,但是在使用泛型时,编译器和运行时系统不知道你想用类型做什么,以及应该采用什么规则。
// List<HasColor> list = new ArrayList<Bounded>();
List<? extends HasColor> list2 = new ArrayList<Bounded>();
List<Bounded> boundeds = Arrays.asList(new Bounded());
List<? extends HasColor> list1 = boundeds;
//下面三个都报错
//list2.add(new Bounded());
//list2.add(new NextSom());
//list.add(new Object());
你会发现,使用这种通配符会让你连最基本的object类型都加不进去了。
7.1 逆变
还有另一种方法:使用超类型通配符。
这里可以声明通配符是由某个特定类的任何基类来界定的,方法是指定<? super MyClass>
,或者甚至使用类型参数:? super T
。但是,千万别写成<T super MyClass>
,千万不要记混了。
这种方式可以使你可以安全地传递一个类型对象到泛型参数中。因此,有了超类型通配符,就可以向Collection 中写入了:
public class SuperTypeWildcards {
static void writeTo(List<? super Apple> apples) {
apples.add(new Apple());
apples.add(new Jonathan());
// apples.add(new Orange());//报错
}
}
参数apples 是 Apple 的某种基类型的 List,这样你就知道向其中添加 Apple 或 Apple 的子类是安全的。
下面示例复习一下逆变和通配符的使用:
public class GenericReading {
static List<Apple> apples = Arrays.asList(new Apple());
static List<Fruit> fruits = Arrays.asList(new Fruit());
static <T> T readExact(List<T> list) {
return list.get(0);
}
static void f1() {
Apple a = readExact(apples);
Fruit f = readExact(fruits);
//集合和对象的类型是有区别的
f = readExact(apples);
}
static class Reader<T> {
T readExact(List<T> list) {
return list.get(0);
}
}
static void f2() {
Reader<Fruit> fruitReader = new Reader<>();
Fruit f = fruitReader.readExact(fruits);
//指定了T的类型就必须一致
// Fruit a = fruitReader.readExact(apples);
}
static class CovariantReader <T>{
T readCovariant(List<? extends T> list) {
return list.get(0);
}
}
static void f3() {
CovariantReader<Fruit> fruitReader = new CovariantReader<>();
Fruit f = fruitReader.readCovariant(fruits);
Fruit a = fruitReader.readCovariant(apples);
}
public static void main(String[] args) {
f1();
f2();
f3();
}
}
readExact() 方法使用了精确的类型。如果使用这个没有任何通配符的精确类型,就只可以向其中读取和写入这个精确类型。
7.2 无界通配符
无界通配符<?>
看起来表示“任何事物”,因此使用无界通配符好像等价于使用原生类型。
public class UnboundedWildcards1 {
static List list1;
static List<?> list2;
static List<? extends Object> list3;
static void assign1(List list) {
list1 = list;
list2 = list;
//会提示一个未检查的转换:Unchecked assignment: 'java.util.List' to 'java.util.List<? extends java.lang.Object>'
list3 = list;
}
static void assign2(List<?> list) {
list1 = list;
list2 = list;
list3 = list;
}
static void assign3(List<? extends Object> list) {
list1 = list;
list2 = list;
list3 = list;
}
public static void main(String[] args) {
assign1(new ArrayList());
assign2(new ArrayList());
//再次报未检查的类型错误
assign3(new ArrayList());
assign1(new ArrayList<>());
assign2(new ArrayList<>());
assign3(new ArrayList<>());
List<?> wildList = new ArrayList();
wildList = new ArrayList<>();
assign1(wildList);
assign2(wildList);
assign3(wildList);
}
}
在很多情况下,编译器很少关心你使用的是原生类型还是<?>。在这些情况下,<?>可以被认为是一种装饰,但是它仍然是有价值的,它表示一个泛型参数且这个参数可以持有任何类型。
下面示例展示一个无界通配符的重要应用:当你在处理多个泛型时,有时允许一个参数是任何类型,且同时其他参数确定某种特定类型。
public class UnboundedWildcards2 {
static Map map1;
static Map<?, ?> map2;
static Map<String, ?> map3;
static void assign1(Map map) {
map1 = map;
}
static void assign2(Map<?, ?> map) {
map2 = map;
}
static void assign3(Map<String, ?> map) {
map3 = map;
}
public static void main(String[] args) {
assign1(new HashMap());
assign2(new HashMap());
//报类型错误
assign3(new HashMap());
assign1(new HashMap<>());
assign2(new HashMap<>());
assign3(new HashMap<>());
}
}
如果泛型参数全是无界通配符,就像Map<?, ?>
,编译器看起来无法区分它与原生类型,这让人很疑惑,它们看起来就像是相同的。事实上是因为:泛型擦除会擦除到它的第一个边界,因此List<?>
实际上等价于List<Object>
,而List 实际上也是List<Object>
。
尽管如此,编译器何时才会关注原生类型和和涉及无界通配符之间的差异呢?
public class WildCards {
static void rawArgs(Holder holder, Object obj) {
holder.setA(obj);
Object object = holder.getA();
}
static void unboundedArg(Holder<?> holder, Object arg) {
//编译报错
// holder.setA(arg);
}
static <T> T exact1(Holder<T> holder) {
return holder.getA();
}
static <T> T exact2(Holder<T> holder, T arg) {
holder.setA(arg);
return holder.getA();
}
static <T> T wildSubtype(Holder<? extends T> holder, T arg) {
//编译报错,必须是T的子类型
// holder.setA(arg);
return holder.getA();
}
static <T> void wildSuperType(Holder<? super T> holder, T arg) {
holder.setA(arg);
}
public static void main(String[] args) {
Holder raw = new Holder();
Holder<Long> qualified = new Holder<>();
Holder<?> unbounded = new Holder<>();
Holder<? extends Long> bounded = new Holder<>();
Long l = 1L;
rawArgs(raw, l);
rawArgs(qualified, l);
rawArgs(unbounded, l);
rawArgs(bounded, l);
unboundedArg(raw, l);
unboundedArg(qualified, l);
unboundedArg(unbounded, l);
unboundedArg(bounded, l);
//报警告:
// Object o = exact1(raw);
Long r2 = exact1(qualified);
//必须返回object
Object r3 = exact1(unbounded);
Long r4 = exact1(bounded);
//报警告
// Long r5 = exact2(raw, l);
Long r6 = exact2(qualified, l);
//报错:: no instance(s) of type variable(s) exist so that Long conforms to capture of ? inference variable T has incompatible bounds: equality constraints: capture of ? lower bounds: Long
// Long r7 = exact2(unbounded, l);
//报错:equality constraints: capture of ? extends Long lower bounds: Long
// Long r8 = exact2(bounded, l);
//警告Unchecked assignment: 'Holder' to 'Holder<? extends java.lang.Long>'
// Long r9 = wildSubtype(raw, l);
Long r10 = wildSubtype(qualified, l);
Object r11 = wildSubtype(unbounded, l);
Long r12 = wildSubtype(bounded, l);
//警告: [unchecked] unchecked method invocation:required: Holder<? super T>,T;found: Holder,Long
// wildSuperType(raw,l);
wildSuperType(qualified,l);
//编译报错:no instance(s) of type variable(s) exist so that Long conforms to capture of ?
// wildSuperType(unbounded,l);
//报错
// wildSuperType(bounded,l);
}
}
在rawArgs() 中,编译器知道Holder是一个泛型参数
八、问题
8.1 任何基本类型都不能作为参数类型
Java泛型的限制之一是不能将基本类型用作参数类型。
8.2 实现参数化接口
一个类不能实现一个泛型接口的两种变体,因为由于擦除的原因,这两个变体会成为相同的接口。
8.3 转型与警告
当你在进行对泛型类型的集合操作时,大部分时候读取是可以的,但是在写入值时,就需要手动进行强制转型,并且会在这时候进行一个转型警告,这时候如果你确定转型是没有错误的,就可以用@SuppressWarnings(“unchcked”),来进行警告压制。
8.4 重载
这个和第二点类型,当你在对泛型方法进行重载时:
public class UserList<W,T>{
void f(List<T> v){}
void f(List<W> v){}
}
这种是不能编译的,因为擦除的存在,这种重载产生了相同的方法。
九、动态类型安全
当你需要在运行时确保你添加类型的安全,可以使用Java 5 中的 java.util.Collections 的便利工具,可以解决在这种情况下的检查问题。这些工具是:静态方法:checkedCollection(),checkedList(),checkedMap(),checkedSet(),checkedSortedMap,checkedSortedSet()
。这些方法每一个都会将你希望动态检查的集合当做第一个参数接受,并将你希望强制要求的类型作为第二个参数接受。
public class CheckedList {
@SuppressWarnings("unchecked")
static void oldStyleMethod(List probablyDogs) {
probablyDogs.add(new Cat());
}
public static void main(String[] args) {
List<Dog> dogs = new ArrayList<>();
//这里这个集合接受了一个Cat型
oldStyleMethod(dogs);
//这里使用了Collections中的便利工具:checkedList();它的第一个参数为你需要动态检查的集合,第二个参数为你希望强制要求的类型
List<Dog> dogs2 = Collections.checkedList(new ArrayList<>(), Dog.class);
try {
oldStyleMethod(dogs2);
} catch (Exception e) {
System.out.println("Expected: " + e);
}
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 Cat element into
collection with element type class Dog
*/
如上面的 dogs2 一样,受检查的集合在你试图插入类型不正确的对象会抛出ClassCastException。这与 dogs 的集合形成了鲜明的对比,对于后者来说,当你把对象从集合中取出时,才会通知你出现了问题。
十、泛型异常
由于擦除的原因,catch 语句不能捕获泛型类型的异常,因为在编译器和运行期都必须知道异常的确切类型。泛型类也不能直接或间接继承自Throwable。但是,类型参数可能会在一个方法的throw子句中用到,这使得你可以编写随检查型异常变化的泛型代码
//定义一个泛型接口,这个接口里传入你要操作的集合和接受这个集合抛出的异常
interface Processor<T, E extends Exception> {
void process(List<T> resultCollector) throws E;
}
/**
* 这个类继承了带了限制的ArrayList,且这个类自己也实现了泛型限定,在其中实现了processAll() 方法,这个方法将调用Processor接口中的方法,
* 但是因为没有实现Processor接口,所以并不需要去重写它的方法。
* 可以这样理解:这整个程序先定义了一个接口,然后有其他实现接口的类,然后这些类因为有共性,所以可以用一个通用的模型来操作它们:ProcessRunner
*/
class ProcessRunner<T, E extends Exception> extends ArrayList<Processor<T, E>> {
List<T> processAll() throws E {
List<T> resultCollector = new ArrayList<>();
for (Processor<T, E> processor : this) {
processor.process(resultCollector);
}
return resultCollector;
}
}
class Failure1 extends Exception{}
class Process1 implements Processor<String, Failure1> {
static int count = 3;
@Override
public void process(List<String> resultCollector) throws Failure1 {
//**这里我忘记了,当在做这一步的判断的时候,count的值就已经被变化了!!!!,所以循环三次这个正好没有报错
if (count-- > 3) {
resultCollector.add("Hep!");
} else resultCollector.add("Ho!");
if (count < 0) throw new Failure1();
}
}
class Failure2 extends Exception{}
class Process2 implements Processor<Integer, Failure2> {
static int count = 2;
@Override
public void process(List<Integer> resultCollector) throws Failure2 {
//而这个正好就在循环第三次的时候报错了!!!
if (count-- == 0) {
resultCollector.add(47);
} else{
resultCollector.add(11);//这里的添加操作因为有动态判断,赋值正确是不会走异常抛出的
}
if (count < 0) {
throw new Failure2();
}
}
}
public class ThrowGenericException {
public static void main(String[] args) {
ProcessRunner<String, Failure1> runner = new ProcessRunner<>();
for (int i = 0; i < 3; i++) {
//因为ProcessRunner继承了ArrayList,所以可以调用这些方法
runner.add(new Process1());
}
try {
System.out.println(runner.processAll());
} catch (Failure1 failure1) {
System.out.println(failure1);
}
ProcessRunner<Integer, Failure2> runner2 = new ProcessRunner<>();
for (int i = 0; i < 3; i++) {
runner2.add(new Process2());
}
try {
System.out.println(runner2.processAll());
} catch (Failure2 failure2) {
System.out.println(failure2);
}
}
}
/*
output:
[Ho!, Ho!, Ho!]
Failure2
*/
Processor 执行 process() 方法,并且可能会抛出具有类型 E 的异常。process() 的结果存在List<T> resultCollector
中(这叫做收集参数)。ProcessorRunner 有一个 processAll() 方法,它会在所持有的每个 Process 对象执行,并返回 resultCollector。
正是由于可以参数化所抛出的异常,所以上述代码可以执行。
十一、混型
混型的最基本概念就是混合多个类,以产生一个可以表示混型中所有类型的类。它将使组装多个类变得简单易行。
混型的价值之一是它们可以将特性和行为一致的应用于多个类之上。如果想在混型中修改某东西,这些修改将会应用与混型所应用的所有类型之上。正因如此,混型有一点面向切面编程AOP的味道。
在Java 中:泛型类不能直接继承自一个泛型参数。
一种更常见的解决方案是使用接口来产生混型效果:
interface TimeStamped{
long getStamp();
}
class TimeStampedImp implements TimeStamped {
private final long timeStamp;
TimeStampedImp() {
//new Date():date类会自己调用自己的有参构造方法将当前的时间(long型)赋给date字段,getTime()会获取到当前时间
this.timeStamp = new Date().getTime();
}
@Override
public long getStamp() {
return timeStamp;
}
}
interface SerialNumbered {long getSerialNumber();}
class SerialNumberedImpl 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 SerialNumberedImpl();
@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");
mixin2.set("test String");
System.out.println(mixin1.get() + " " + mixin1.getStamp() + " " + mixin1.getSerialNumber());
System.out.println(mixin2.get() + " " + mixin2.getStamp() + " " + mixin2.getSerialNumber());
}
}
/*
output:
test string 1622861295429 1
test String 1622861295430 2
*/
Mixin 类基本上是在使用委托,因此每个混入类型都要求在 Mixin 中有一个相应的域。而你必须在Mixin 中编写所有必需的方法,将方法调用传给恰当的对象。
这个示例使用了非常简单的类,但是当使用更复杂的类时,代码数量会急速增加。
11.1 使用装饰器模式
混型的使用方式和概念和装饰器设计模式关系很类似。
装饰器经常用于满足各种可能的组合,而直接子类化会产生过多的类,因此是不实际的。装饰器模式使用分层对象来动态透明地向单个对象添加责任。
装饰器指定:包装在最初的对象周围的所有对象都具有相同的基本接口。某些事物是可装饰的,可以通过将其他类包装在这个可装饰对象的四周围,来将功能分层。
这使得对装饰器的使用是透明的————无论对象是否被装饰,你都拥有一个可以向对象发送的公共消息集。
装饰类也可以添加新方法,但是这是受限的。
装饰器是通过使用组合和形式化结构(可装饰物/装饰器层次结构)来实现的;而混型是基于继承的。因此可以将基于参数化类型的混型当做是一种泛型装饰器机制,这种机制不需要装饰器设计模式的继承结构。
前面的示例可以被改写为使用装饰器模式:
class Basic{
private String value;
public void set(String value) {
this.value = value;
}
public String get(){
return value;
}
}
//装饰器:继承了基类,虽然重写了父类方法,但是实现还是父类来实现。
class Decorator extends Basic {
protected Basic basic;
Decorator(Basic basic) {
this.basic = basic;
}
@Override
public void set(String value) {
basic.set(value);
}
@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 getTimeStamp() {
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()),
t2 = new TimeStamped(new SerialNumbered(new Basic()));
// t2.getSerialNumber();//不可行
SerialNumbered s = new SerialNumbered(new Basic()),
s2 = new SerialNumbered(new TimeStamped(new Basic()));
// s2.getTimeStamped();//不可行
}
}
产生自泛型的类包含所有感兴趣的方法(?这一段好几个因此都不是很明白),但是由使用装饰器所产生的对象类型是最后被装饰的类型。
也就是说:尽管可以添加多个层,但是最后一层才是实际的类型,因此只有最后一层的方法是可视的,而混型的类型是所有被混合到一起的类型。
因此对于装饰器来说,其明显的缺陷是:它只能有效的作用在装饰中的一层(最后一层),而混型方法显然会更自然一些。因此,装饰器只是对由混型提出的问题的一种局限(勉强)的解决方案。
11.2 与动态代理混合
可以使用动态代理来创建一种比装饰器更贴近混型模型的机制(随意AOP与动态代理结合)。通过使用动态代理,所产生的类的动态类型j将会是已经混入的组合类型。由于动态代理的限制,每个被混入的类必须是某个接口的实现:
class MixinProxy implements InvocationHandler {
Map<String, Object> delegatesByMethod;
@SuppressWarnings("unchecked")
MixinProxy(TwoTuple<Object, Class<?>>... pairs) {
delegatesByMethod = new HashMap<>();
for (TwoTuple<Object, Class<?>> pair :
pairs) {
for (Method me : pair.second.getMethods()) {
System.out.println(pair.first);
System.out.println(pair.second);
System.out.println(pair.first.getClass().getMethods());
System.out.println(pair.second.getMethods());
String methodName = me.getName();
//
if (!delegatesByMethod.containsKey(methodName)) {
delegatesByMethod.put(methodName, pair.first);
}
}
}
}
@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);
}
//这个是一个实现,根据传入的对象数组,取出其中的接口字段再重新组合到接口数组中,作为InvocationHandler的第二个参数
@SuppressWarnings("unchecked")
public static Object newInstance(TwoTuple... pairs) {
//接口数组
Class[] interfaces = new Class[pairs.length];
for (int i = 0; i < pairs.length; i++) {
//将数组中的第i个元素的第二个接口类获取到
interfaces[i] = (Class) pairs[i].second;
}
//类加载器是tuple中的实现类来获取类加载器
ClassLoader cl = pairs[0].first.getClass().getClassLoader();
return Proxy.newProxyInstance(cl, interfaces, new MixinProxy(pairs));
}
}
public class DynamicProxyMixin {
private static class BasicImpl implements Basic {
private String val;
@Override
public void set(String val) {
this.val = val;
}
@Override
public String get() {
return val;
}
}
public static void main(String[] args) {
//先将这三对接口和实现类传入代理类
Object mixin = MixinProxy.newInstance(tuple(new BasicImpl(), Basic.class),
tuple(new TimeStampedImp(), TimeStamped.class),
tuple(new SerialNumberedImpl(), 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
1623035786112
1
*/
因为只有动态类型而不是静态类型才包含所有的混合类型,因此Java 的动态代理方式仍不如 C++ 的方式好,但是由于在这些对象调用方法之前你被强制将这些对象进行向下转型到恰当类型,所以,这种方式明显更接近于真正的混型。
十二、潜在类型机制及对缺乏潜在类型机制的补偿(反射)
Java 没有任何机会可以去实现任何类型的潜在类型机制(某些编程语言提供的一种解决方案称为潜在类型机制或结构化类型机制,而更古怪的术语称为鸭子类型机制,即“如果它走起来像鸭子,并且叫起来也像鸭子,那么你就可以将它当作鸭子对待。”鸭子类型机制变成了一种相当流行的术语,可能是因为它不像其他的术语那样承载着历史的包袱。
泛型代码典型地只能在泛型类型上调用少量方法,而具有潜在类型机制的语言只要求实现某个方法子集,而不是某个特定类或接口,从而放松了这种限制(并且可以产生更加泛化的代码))。
尽管Java 不直接支持潜在类型机制,但是这并不意味着泛型代码不能在不同的类型层次结构之间应用。也就是说,我们仍然可以创建真正的泛型代码,但是需要其他 的努力。
12.1 反射
可以使用的一种方式是反射:
//没有实现 行为方式
class Mime{
public void walkAgainstTheWind(){}
public void sit(){
System.out.println("Pretending to sit");
}
public void pushInvisibleWalls(){}
@Override
public String toString() {
return "Mime";
}
}
//没有实现 行为
class SmartDog{
public void speak(){
System.out.println("Woof!");
}
public void sit(){
System.out.println("Sitting");
}
public void reproduce(){}
}
//这个实现了行为Performs
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 + "can not speak");
}
try {
Method sit = spkr.getMethod("sit");
sit.invoke(speaker);
} catch (NoSuchMethodException e) {
System.out.println(speaker + "cannot sit");
}
} catch (SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException exception) {
throw new RuntimeException(speaker.toString(), exception);
}
}
}
public class LatentReflection {
public static void main(String[] args) throws AWTException {
CommunicateReflectively.perform(new SmartDog());
CommunicateReflectively.perform(new Mime());
}
}
/*
Woof!
Sitting
Mimecan not speak
Pretending to sit
*/
这些类彼此之间是完全分离的,没有任何公共基类(除了Object)或接口。通过反射,CommunicateReflectively.perform()
能够动态的确定所需要的方法是否可用并调用它们。它甚至能处理Mime 只具有一个必须的方法这个事实,并能够部分实现其目标。
12.2 将一个方法适应于各种序列
反射提供了一些有用的可能性,且它将所有的类型检查都转移到了运行时,因此在许多情况下可能不是符合我们的需要。如果能实现编译期类型检查,这更符合通常情况。
如果想要创建一个apply()
方法:它能够将任何方法应用于某个序列中的所有对象。
这种情况下使用接口不合适,因为你想要将任何方法应用于对象集合,而接口不可能提供所有接口。如何用Java来实现这个需求呢?
我们现在就可以用反射来实现这个问题,且相当优雅:
public class Apply {
public static <T, S extends Iterable<T>> void apply(S seq, Method f, Object... args) {
try {
for (T t : seq) {
f.invoke(t, args);
}
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
//如果报错就是程序员的错误
throw new RuntimeException(e);
}
}
}
在上述代码中,异常被转换为 RuntimeException,因为没有多少方法可从这种异常中恢复。
注意:invoke() 和 apply() 方法的优点是它们可以接受任意数量的参数。在某些情况下,灵活性至关重要。
下面的类用来测试Apply:
public class Shape {
private static long counter = 0;
//如果这一句加上static ,那么即使是final引用,值也不会改变!!
private final long id = counter++;
@Override
public String toString() {
return getClass().getSimpleName() + " " + id;
}
//旋转,循环
public void rotate() {
System.out.println(this + " rotate ");
}
//调整
public void resize(int newSize) {
System.out.println(this + " resize " + newSize);
}
}
//===================================================================
public class Square extends Shape {}
//===================================================================
public class ApplyTest {
public static void main(String[] args) throws NoSuchMethodException {
List<Shape> shapes = Suppliers.create(ArrayList::new, Shape::new, 3);
Apply.apply(shapes, Shape.class.getMethod("rotate"));
Apply.apply(shapes, Shape.class.getMethod("resize", int.class), 7);
List<Square> squares = Suppliers.create(ArrayList::new, Square::new, 3);
Apply.apply(squares, Shape.class.getMethod("rotate"));
Apply.apply(squares, Shape.class.getMethod("resize", int.class), 7);
Apply.apply(new FilledList1<>(Shape::new, 3), Shape.class.getMethod("rotate"));
Apply.apply(new FilledList1<>(Square::new, 3), Square.class.getMethod("rotate"));
}
}
/*
output:
Shape 0 rotate
Shape 1 rotate
Shape 2 rotate
Shape 0 resize 7
Shape 1 resize 7
Shape 2 resize 7
Square 3 rotate
Square 4 rotate
Square 5 rotate
Square 3 resize 7
Square 4 resize 7
Square 5 resize 7
Shape 6 rotate
Shape 7 rotate
Shape 8 rotate
Square 9 rotate
Square 10 rotate
Square 11 rotate
*/
在Apply 中,碰巧在 Java 中内建了一个由 Java 集合类库使用的 Iterator 接口,所以apply()
可以接受任何实现了 Iterator 接口的事物,包括诸如 List 这样的 Collection 类。除此之外它还可以接受其他事物,只要这个事物实现了 Iterator 接口就可以。
反射很优雅,但是有了Java 8 ,我们现在有了更好的选择,通过 Java 8 的函数式方法,我们可以更简洁,下面的类对ApplyTest.java进行了重写:
public class ApplyFunctional {
public static void main(String[] args) {
Stream.of(
Stream.generate(Shape::new).limit(2),
Stream.generate(Square::new).limit(2))
//把它们压平到一个流中
.flatMap(c -> c)
.peek(Shape::rotate)
.forEach(s -> s.resize(7));
new FilledList1<>(Shape::new, 2)
.forEach(Shape::rotate);
new FilledList1<>(Square::new, 2)
.forEach(Shape::rotate);
}
}
/*
output:
Shape 0 rotate
Shape 0 resize 7
Shape 1 rotate
Shape 1 resize 7
Square 2 rotate
Square 2 resize 7
Square 3 rotate
Square 3 resize 7
Shape 4 rotate
Shape 5 rotate
Square 6 rotate
Square 7 rotate
*/
由于使用 Java 8,因此不需要Apply.apply().
我们首先生成两个 Stream :并将它们展平为单个流。尽管Java 缺少功能语言中经常出现的 flatten();但是我们可以使用flatMap(c->c)
产生相同结果,后者使用身份映射将操作简化为“flatten”。
我们使用peek() 当做对 rotate() 的调用,因为 peek() 执行一个操作,并在未更改的情况下传递对象。
注意:使用Java 8 的语法使得代码更加整洁,在代码简单性和可读性方面,结果比以前的方法好得多,并且也不会从main方法引发异常。
12.3 Java 8 中的辅助潜在类型
先前声明的关于Java 缺乏对潜在类型的支持在 Java 8 之前是完全正确的。但是,Java 8 中的非绑定方法引用使我们能够产生一种潜在类型的形式,以满足我们创建一段可以工作在不相干类型上的代码。
下面这个类来演示该技术。这个类重写了DogsAndRobots.java
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("Sitting");
}
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);
}
}
/*
output:
Woof!
Sitting
Click
Sitting
*/
PerformingDogA 和 RobotA 与DogsAndRobots.java 中的相同,不同之处在于它们不继承通用接口Performs,因此它们没有通用性。
CommunicateA.perform() 在没有约束的P 上生成。只要使用 Consumer<P>
,它在这里可以是任何东西,这些 Consumer<P>
代表不带参数的P 方法的未绑定方法引用。当你调用Consumer 的 accept()
方法时,它将方法引用绑定到执行者对象并调用该方法。由于函数式编程的魔术,我们可以将任何签名的未绑定方法引用传递给Communicate.perform()
。
12.3.1 调用Suppliers 类的通用方法
通过辅助潜在类型,我们可以定义本章其他部分中使用的 Suppliers 类。此类包含使用生成器填充 Collection 的工具方法。泛化这些操作很有意义:
public class Suppliers {
//创建一个集合并填充它
public static <T, C extends Collection<T>> C create(Supplier<C> factory, Supplier<T> gen, int n) {
return Stream.generate(gen)
.limit(n)
.collect(factory, C::add, C::addAll);
}
//填充一个现有的集合
public static <T, C extends Collection<T>> C fill(C coll, Supplier<T> gen, int n) {
Stream.generate(gen)
.limit(n)
.forEach(coll::add);
return coll;
}
//使用一个未绑定的方法引用去产生一个更通用的方法
public static <H, A> H fill(H holder, BiConsumer<H, A> adder, Supplier<A> gen, int n) {
Stream.generate(gen)
.limit(n)
.forEach(a -> adder.accept(holder, a));
return holder;
}
}
这个方类在前面的好几处地方已经调用过了。但是它真正的归属是在这里,create方法为你创建一个新的Collection子类,而fill() 的第一个版本元素放入Collection 的现有子类型中。请注意:还会返回传入的容器的确切类型,因此不会丢失类型信息。
前两种方法一般都受约束,只能与Collection 子类型一起使用。 fill() 的第二个版本适用于任何类型的 holder。它需要一个附加参数:未绑定的方法引用adder.fill()
,使用辅助潜在类型来使其与holder 类型(任何具有添加元素的方法)一起使用。因为此为绑定方法 adder 必须带有一个参数(要添加到 adder的元素),所以 adder 必须是Biconsumer <H, A>
,其中 H 是要绑定到的holder对象的类型,而 A 是要被添加的绑定元素类型。对 accept() 的调用将使用参数 a 调用对象 holder 上的未绑定方法 holder。
总结
这一章记了很多很多,有很多东西可能也用不到几次,但是记在这里可以随时查阅,因为这些代码真的都太好太好了,全文将近4w字,纯手敲,敲完后很多知识点还是不连贯,但是我现在比之前清晰多了。