重新审视Java的泛型功能
大纲
Java引入了泛型,添加了类型参数,参数边界,协变,逆变,不变,通配符等等各种语法和功能。如果搜索一下,就可以找到很多解释泛型语法和这些新功能(虽然已经不新了)的页面,解释得非常易懂。但是,如果只是使用带有泛型的类库,还好说,但是如果要在自己的类定义中使用泛型,就会遇到“那么,该怎么办?”的问题。我之前一直随便使用泛型,但是最近下了一番决心认真思考,总结了一些实用的技巧。
因此,本文省略了对泛型语法和含义的解释。而且,我不能保证说的都是正确的,只是个人理解,如果有错误之处,请指正。
从一开始
使用泛型的目的是为了类自身,并考虑类所包含的变量。典型的例子是集合框架。因此,我们还需要考虑使用边界和通配符等其他情况(虽然最终归结为同样的事情)。
这里将通过以下例子进行具体解释。为了方便您复制粘贴进行确认,我将全部写出来。
public class Human {
public void hello() {
System.out.println("Hello Human");
}
}
class Man extends Human {
@Override
public void hello() {
System.out.println("Hello Man");
}
}
class Woman extends Human {
@Override
public void hello() {
System.out.println("Hello Woman");
}
}
public class House {
public void hello() {
System.out.println("Hello House");
}
}
在这里有四个类,我们考虑一个包含这些类的容器类。
public class Container {
private Human human;
public Container(Human human) {
this.human = human;
}
public Human get() {
human.hello();
return human;
}
public static void main(String ...args) {
Human human = new Human();
Man man = new Man();
Woman woman = new Woman();
Container humanContainer = new Container(human);
Container manContainer = new Container(man);
Container womanContainer = new Container(woman);
humanContainer.get();
manContainer.get();
womanContainer.get();
}
}
不需要解释,执行此操作将获得以下结果。
Hello Human
Hello Man
Hello Woman
由于”Human”、”Man”和”Woman”之间存在继承关系,因此可以执行human.hello()。
在这里,我们试着引入泛型。
public class GenContainer<T extends Human> {
private T t;
public GenContainer(T t) {
this.t = t;
}
public T get() {
t.hello();
return t;
}
public static void main(String ...args) {
Human human = new Human();
Man man = new Man();
Woman woman = new Woman();
GenContainer<Human> humanContainer = new GenContainer<>(human);
GenContainer<Man> manContainer = new GenContainer<>(man);
GenContainer<Woman> womanContainer = new GenContainer<>(woman);
humanContainer.get();
manContainer.get();
womanContainer.get();
}
}
执行这个,
Hello Human
Hello Man
Hello Woman
得到相同的结果。在这个例子中,通过将类型参数T的上限界限设为Human,使得可以执行t.hello()。但是,如果目的是至少将T视为Human来处理,那么我认为不需要使用泛型。
接下来,我们将引入以下的课程
public class User<T extends Human> {
protected T t;
public void usebycon(Container container) { ---(1)
//t = container.get(); // compile error
}
public void strictUsebygen(GenContainer<T> gencontainer) { ---(2)
t = gencontainer.get();
t.hello();
}
public void relaxUsebygen(GenContainer<? extends T> genContainer) { ---(3)
t = genContainer.get();
t.hello();
}
public static void main(String ...args) {
Human human = new Human();
Man man = new Man();
Woman woman = new Woman();
House house = new House();
GenContainer<Human> humanContainer = new GenContainer<>(human);
GenContainer<Man> manContainer = new GenContainer<>(man);
GenContainer<Woman> womanContainer = new GenContainer<>(woman);
//GenContainer<House> houseContainer = new GenContainer<>(house); // compile error --- (*)
User<Human> user = new User<>(); --- (4)
user.strictUsebygen(humanContainer);
//user.strictUsebygen(manContainer); // compile error
user.relaxUsebygen(manContainer); --- (5)
//user.strictUsebygen(houseContainer); // compile error ---(**)
//user.relaxUsebygen(houseContainer); // compile error
}
}
由于执行结果没有意义,因此被省略。
首先,container.get()返回的是Human,而T extends Human,但t = container.get()会导致编译错误。为什么,我不知道(笑)。但至少编译不通过(虽然理论上应该是正确的)。然而,在情况2中,编译是通过的。这表明泛型在类之间交互传递类内包含的对象时很有用。
另外,在(4)中,由于将Human作为User的类型参数,所以我们可以调用humanContainer(类型参数为Human),但是如果给出manContainer,则由于泛型的不变性导致编译错误。因此,我们可以使用类型边界来允许manContainer,如(3)所示。这里我们使用的是上限边界(extends),这取决于在类之间传递的对象的类型关系(PUT/GET等等,也称为PECS)。但是,个人而言我对此理解起来困难,感到很困扰…
下面我们来讨论(*)的编译错误。导致出现错误的原因是,
class GenContainer<T extends Human>
由于根据定义,T的上限是Human,而House并不是Human的派生类。因此,
class GenContainer<T>
如果改变定义,编译就能通过。(在GenContainer的t.hello()会出现错误,但这里省略了。)
然而,在(**)仍然会产生编译错误(给出编译错误)。这是因为
class User<T extends Human>
因为T被设定为Human为上限的原因。
从目前的对话中,我认为可以得出以下结论。
-
- コンテナとしてクラスを使いたい場合はジェネリクスを導入(コレクションがあるので,自作する機会は少ないかも)
-
- 既存クラスで型引数を導入しているクラスに,オブジェクトを内包するクラスのインスタンスを渡す場合(e.g., GenContainerのような)
-
- 既存クラスで型引数を導入しており,オブジェクトを内包するインスタンスを受け取る場合
- オブジェクトを渡すときには,型境界を導入して制限を緩める(上限/下限とワイルドカードを使用)
而且,通过这些所做的事情得到的效果是类型安全的(当然,是的)。在这个例子中,(**)部分绝对不能通过编译。所以,不仅仅是限制参数为GenContainer类型的对象,还限制了GenContainer所包含的对象的类型。这是理所当然的事情…
而且,还有一点。感觉有可能做一些诡计,但实际上并非如此(在《Effective Java》中也有些介绍)。因为它只是为了保证类型安全而存在的机制,所以使用场景是有限的,可以考虑为此而做好限制(也可能只是我不了解,请谅解)。
以下内容的中文同义词:模式
在某些情况下,引入泛型是一个不错的选择,接下来会举例说明(纯属个人意见)。当编写正式的程序时,通常会首先创建接口,然后是(如果需要的话)抽象类和具体类。泛型有多少典型应用场景,我不太清楚,但先从我能想到的开始。我会不断更新(希望能够)。
在具有继承关系的一组类中进行方法链调用
如果想要对具有继承关系的类执行以下代码:
class A {
A do1() {..return this;}
A do2() {..return this;}
A do3() {..return this;}
public static void main(...) {
new A().do1().do2().do3();
}
}
继承这个类并添加方法。
class B extends A {
B do4() {..return this;}
public static void main(...) {
new B().do1().do2().do3().do4(); --> X
}
}
這是一個編譯錯誤。原因是因為do3()返回了A,而A中沒有定義do4()。在這種情況下,可以使用所謂的模擬自類型(simulated self-type)的習慣用法(參考Effective Java)。在Effective Java中,以Builder模式為例,介紹了抽象對象、抽象Builder以及繼承它們的具體對象和具體Builder。在這裡,我們將使用接口來示範。
public interface IBase<T extends IBase<T>> {
public T firstName();
public T lastName();
public T setName(String first, String last);
}
在接口定义中,IBase> 是 self-type 的部分。这个定义限制了 T 的类型。
具体来说,T 必须继承(或实现)IBase,并且其类型参数也必须为 T。
虽然有点令人困惑,但具体定义如下:
public class Something implements IBase<Something> {
......
}
当我们将Something应用于>的T时,确实可以得到Something的类定义。然而,这个类没有任何意义。因为T已经被确定为Something,所以无法实现原本目的中的继承关系的方法链。因此,在具体类之前,我们引入了抽象类。
@SuppressWarnings("unchecked")
public abstract class Base<T extends IBase<T>> implements IBase<T> {
protected String firstName;
protected String lastName;
public T setName(String first, String last) {
this.firstName = first;
this.lastName = last;
return (T)this;//絶対に成功する.TはIBaseを継承または実装する.this(Base)はIBaseを実装する.よって絶対成功する.
// ただし,コンパイラーの型検査ではチェックできない
}
public T firstName() {
System.out.println(firstName);
return (T)this;
}
public T lastName() {
System.out.println(lastName);
return (T)this;
}
}
因为Base实现了IBase,所以如果直接进行类定义的话,
class Base<T> implements IBase<T>
即使这样写,使用类型参数T会导致编译错误。因为IBase的类型参数T被定义为T extends IBase,所以Base的类型参数T也需要相同的定义。
实际上,即使写成Base的形式,”T是Base的上限,并将T应用于IBase”以满足IBase的T extends IBase的约束(T是Base的定义(implements IBase)),编译也可以通过。但是,这会引起扩展性的问题(如下所述)。
下一步,代码的重点是在于return (T)this;这部分,在注释中指出,这个强制转型是绝对成功的。因此,我们使用@SuppressWarnings(“unchecked”)来忽略编译器的警告。通过这种实现方式,我们可以在运行时将类型安全地向下转换为指定的类。
最终,我们实现具象类。
public class Concrete extends Base<Concrete> {
public Concrete company() {
System.out.println("Apple");
return this;
}
public static void main(String ...args) {
Concrete cobj = new Concrete();
cobj.setName("Jobs", "Steve");
Object o = cobj.firstName().lastName().company();
System.out.println(o.getClass().getName());
}
}
如果执行这个,你会得到以下的结果。
Steve
Jobs
Apple
Concrete
尽管我们在这里向子类添加了一个新的company()方法,但方法链成功执行。
从o.getClass().getName()的结果来看,它返回的是Concrete,也就是我们传递给类型参数的类,也就是子类被转换并返回了。
这样应该可以,但是稍微有些不自然的感觉。
class Concrete extends Base,但是Base的类型参数定义是Base>。如果将Concrete直接应用到T上,应该是Concrete extends IBase,那么class Concrete extends Base就对了吗?Base实现了IBase,并且编译也通过了,所以肯定不是错误的。但是为了让它更清晰一些,可以将Base的定义改为
abstract class Base<T extends Base<T>
如果这样做,就没有任何问题了。这样一来,如果接口->抽象类->具体类之间具有类似的关系,会感觉更好。
好的,现在我们在这里进一步引入继承IBase的接口。
public interface IConcrete extends IBase<IConcrete> {
public String greeting();
}
IConcrete也满足IBase所要求的泛型参数T extends IBase。
然后,我们继承Base并定义Concrate2来实现这个IConcrete。
public class Concrete2 extends Base<Concrete2> implements IConcrete { // コンパイルエラー
@Override
public String greeting() {
return "SayHello";
}
}
Concrete2を実装しようとするとIBaseとIBaseという二つの異なる引数を持つ同じインターフェースIBaseを実装することはできないというエラーメッセージが表示されます。これは、Baseということは、
abstract class Base<Concrete2 extends IBase<Concrete2>> implements IBase<Concrete2>
我所指的是
interface IConcrete extends IBase<IConcrete>
似乎会引发冲突的意思。
所以,答案是
public class Concrete2 extends Base<IConcrete> implements IConcrete {
@Override
public String greeting() {
return "SayHello";
}
}
只需将Base这样设定就可以了。稍微有些困惑呢……
来点额外的
在引入了接口的前提下,通过修改基类定义为(T extends Base),可以实现以下操作:
public abstract class Base<T extends Base<T>> implements IBase<T> {
這次被說”Concrete不是一個有效的替代參數,限定為T extends Base”。因為IConcrete是”IConcrete extends IBase”啊。
因此,如果具象类不实现其他接口,那么使用`Base`可能更清晰易懂,如果需要实现的话,可能最好像本次的正确示例一样进行。
使用上限和下限及其使用方法和理念
泛型中的上限是什么?
T extends Number
通过使用extends来确定类型参数的上限(即类层次结构的上界),可以实现像…一样。
下限的意思是指
T super Integer
使用super关键字来确定类型参数的下限(即类层次结构的边界)。
直觉地思考,对于上限有些明白(因为我习惯了类的继承),但下限的存在意义不太容易理解。
首先,我们从容易理解的上限界限开始思考。
上限境界(拓展)
在这里,我将重新列出早先已经提到的一组类。
public class Human {
public void hello() {
System.out.println("Hello Human");
}
}
class Man extends Human {
@Override
public void hello() {
System.out.println("Hello Man");
}
}
class Woman extends Human {
@Override
public void hello() {
System.out.println("Hello Woman");
}
}
下一个是包含这些的类。
public class GenContainer<T extends Human> {
private T t;
public GenContainer(T t) {
this.t = t;
}
public void add(T t) {
this.t = t;
}
public T get() {
t.hello(); ---(1)
return t;
}
}
如果不设定上限,T类型将是不确定的,因此无法对t调用方法(然而,由于任何类型都继承自Object,所以可以调用Object中定义的方法,但没有太多实际意义)。 在 这里,我们关注(1),调用了’hello’。之所以可以这样做,是因为限定了上限T extends Human,确保了T是”至少是Human”的子类。然而,如果只是进行这种用法,普通的继承就足够了,不需要类型参数等(根据前面的讨论)。
请注意,由于计算机翻译的局限性,所提供的翻译可能并非完全准确或符合某些文化/语言习惯。
这里,我们将引入用户。
public class User<T extends Human> {
protected T t;
public void strictUsebygen(GenContainer<T> gencontainer) { ----- (2)
t = gencontainer.get();
t.hello(); ----- (1)
}
public void relaxUsebygen(GenContainer<? extends T> genContainer) { -- (3)
t = genContainer.get();
t.hello(); ----- (1)
}
public static void main(String ...args) {
Human human = new Human();
Man man = new Man();
Woman woman = new Woman();
House house = new House();
GenContainer<Human> humanContainer = new GenContainer<>(human);
GenContainer<Man> manContainer = new GenContainer<>(man);
GenContainer<Woman> womanContainer = new GenContainer<>(woman);
User<Human> user = new User<>();
user.strictUsebygen(humanContainer); --- (4)
//user.strictUsebygen(manContainer); // compile error -- (5)
user.relaxUsebygen(manContainer); --- (6)
}
}
用户,由于我们将上限定义为Human,所以可以像(1)那样调用hello。然后,我们可以像(4)那样在User和GenContainer之间传递Human进行处理。为了进行这种处理,不仅仅需要继承,还需要类型参数。而在(4)中,我们将包含Human的GenContainer作为参数调用方法,处理成功。通过确定上限,我们可以对类型参数的对象执行某些操作(例如方法调用),这就是上限的优点。另一方面,(5)尝试使用包含继承自Human的Man的GenContainer作为参数调用方法时会导致编译错误。这是因为泛型是不变的。出现在这里的是通配符(?)。从使用者的角度来看,“Man继承了Human,所以调用hello一定会成功”。因此,我们将GenContainer所包含的对象,即T类型的约束放宽到(3)的? extends T。这意味着,“如果参数是GenContainer所包含的对象是T类型或者继承T的类型”,我们允许这样的参数。结果,(6)的调用成功了。
当了解到这个上限和通配符后,为了增加灵活性,我想尝试写很多不同的东西,但这正是造成混乱的原因。
我举个可能的错误例子。
class Sample<? extends T> {...}
编译无法通过,无论将T类型定义为String或其他具体类都一样。
GenContainer<? extends Human> container = new GenCongainer<>(human);
这是毫无意义的。想要以这种方式书写的原因是
GenContainer<? extends Human> container = new GenCongainer<>(human);
container.add(man) // コンパイルエラー
我认为是因为我们希望调用一个以继承了Human类的类为参数的方法,但这实际上产生了相反的效果,导致了编译错误。如果仅看这段代码,没有发生错误,但之所以要将其编译错误是由于其原因较为复杂,将在后文中说明。
通配符的目的不是考虑T类型的继承关系本身,而是使得能够互相赋值的包含了T类型(具有继承关系)的对象。因此,在前面的例子中,我们并不希望将Animal的实例和Person的实例赋给list变量,而是希望将它们作为参数传递给add方法。
GenContainer<Human> container = new GenCongainer<>(woman);//コンパイルOK
container.add(man) // コンパイルOK
这是可以的,我想要实现通配符。
GenContainer<Man> manContainer = new GenContainer<>(man)
GenContainer<Human> humanContainer1 = manContainer // (1)コンパイルエラー
GenContainer<? extends Human> humanContainer2 = manContainer; // (2)コンテナの代入
在这个(2)中,考虑到所包含对象的继承关系,允许对容器进行赋值操作。如果是(1),就会出现编译错误。然而,如果是(2),则无法调用add函数,我感到非常恐慌。
我会给出代码来说明即使你理解了这个,也不可以调用add的原因。
GenContainer<Man> manContainer = new GenContainer<>(man)
GenContainer<? extends Human> humanContainer = manContainer; ---(1)
humanContainer.add(woman)---(2)
(1)假设允许对 manContainer 进行赋值,在(2)允许 woman 进行赋值。此时,由于 humanContainer 的实体是 manContainer,可能在内部执行特定于 man 的处理。如果允许 woman 的赋值,可能会导致运行时错误。我认为出于这样的原因才会将其编译为错误。
我认为从这个讨论中可以得出以下关于上限的教训。
-
- 型パラメータを指定してクラスを生成するときには,型を厳密に決めて生成
-
- 型パラメータ付きのコンテナを受け取るメソッドでは,ワイルドカードを使用して型成約を緩めて受け取る
- メソッドの中では,コンテナに型パラメータ型の何かを引数に取るようなメソッド(container.add(t))を呼び出すのではなく,コンテナから値を取り出し,上限の型として扱って処理をする.
用户的relaxUsebygen(GenContainer <? extends T> genContainer)正好符合这种形式。
下限境界(超级)
下限境界和上限境界是相反的,可以像T超级数字那样书写,但直接理解的话,就是将Number作为下限的类型T,那是什么呢?是Object吗?如果是这样的话,只是一个普通的T就可以了,有什么值得高兴的呢?而且,(可能是因为我理解不够深入),与上限相比使用的范围有限。首先,
class Something<T super Number> {...}
这样的定义是不可能的。而且,即使假设这是可能的,
class Something<T super Number> {
T t;
void m(T t) {
// t....何をかけというの?
}
}
虽然可以理解这样的情况,它是Number的超类,但它不是Number,也不知道它的超类是什么,而且只能调用Object的方法……所以我并不认为它有存在的意义。但总归还是有的。
public class Human {
protected String name;
public Human(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class Man extends Human {
public Man(String name) {
super(name);
}
public String getName() {
return "Mr. " + super.getName();
}
}
class Woman extends Human {
public Woman(String name) {
super(name);
}
public String getName() {
return "Ms. " + super.getName();
}
}
public class House {
public int getHight() {
return 10;
}
}
接下来,我们将引入泛型接口和具体类的定义。
public interface Builder<T> {
public void build(T t);
}
public class HumanBuilder implements Builder<Human>{
public void build(Human human) {
System.out.println("HouseBuilder.build is invoked");
System.out.println("Name: " + human.getName());
}
}
public class ManBuilder implements Builder<Man> {
public void build(Man man) {
System.out.println("ManBuilder.build is invoked");
System.out.println("Name: " + man.getName());
}
}
public class HouseBuilder implements Builder<House>{
public void build(House house) {
System.out.println("HouseBuilder.build is invoked");
System.out.println("height = " + house.getHight());
}
}
在这里,我们考虑使用Builder模式,它是一个具有类型参数的类。
public class GeneralBuilderUser<T> {
private T t;
public GeneralBuilderUser(T t) {
this.t = t;
}
public void strictuse(Builder<T> builder) {
builder.build(t);
}
public void relaxuse(Builder<? super T> builder) { --- (1)
builder.build(t);
}
public static void main(String ...args) {
GeneralBuilderUser<House> houseBuildUser = new GeneralBuilderUser<>(new House());
GeneralBuilderUser<Human> humanBuildUser = new GeneralBuilderUser<>(new Human("abc")); // Man is also OK
GeneralBuilderUser<Man> manBuildUser = new GeneralBuilderUser<>(new Man("xyz")); // Woman and Human are not applicable
HouseBuilder houseBuilder = new HouseBuilder();
HumanBuilder humanBuilder = new HumanBuilder();
houseBuildUser.strictuse(houseBuilder);
humanBuildUser.strictuse(humanBuilder);
//manBuildUser.strictuse(humanBuilder); // compile error
manBuildUser.relaxuse(humanBuilder); // compile OK
//houseBuildUser.strictuse(humanBuilder); // compile error
//houseBuildUser.relaxuse(humanBuilder); // compile error
}
}
首先,在main函数中创建3个GeneralBuilderUser(分别指定类型参数为House、Main和Human),然后创建HouseBuilder和HumanBuilder。然后,通过将两个Builder应用于每个BuilderUser的方法的示例,我们考虑下限界限的作用。
houseBuildUser.strictuse(houseBuilder);
由于Builder和BuilderUser的类型参数都是House,因此编译可以通过。
humanBuildUser.strictuse(humanBuilder);
因为Builder和BuilderUser的类型参数都是Human,所以可以成功编译通过。
manBuildUser.strictuse(humanBuilder); // compile error
BuilderUser的类型参数是Man,Builder的类型参数是Human,由于泛型是不变(协变)的,因此会导致编译错误。
manBuildUser.relaxuse(humanBuilder); // compile OK
BuilderUser的类型参数是Man,Builder的类型参数是Human,尽管泛型是不变的,但是由于指定了下限(1),所以编译通过。稍后会仔细考虑这个问题。
houseBuildUser.strictuse(humanBuilder); // compile error
houseBuildUser.relaxuse(humanBuilder); // compile error
在BuilderUser和Builder中,由于将完全不相关的类House和Human作为类型参数,因此会导致编译错误。
第四种情况是使用下限的情况。在这里,存在着Human > Man的关系(Human是父类),Man作为子类作为BuilderUser的类型参数。也就是说,ManBuilderUser的T是Man。另一方面,Builder的类型参数是给定的父类Human作为类型参数。也就是说,build(T t)的T是Human。
现在我们来看一下方法。
public void relaxuse(Builder<? super T> builder) {
builder.build(t);
}
从这个例子中可以得出以下结论:
-
- 下限境界を適用するケースは限られる.例えばクラス定義には使えない.
- 下限境界を適用するクラスのメソッド(e.g., build)と,そこに与える引数(t)の型の関係を考え,下限境界を適用するクラスが引数の親クラスになっている場合に適用する.
所謂的”PECS”规则正是指在作为Consumer时要使用super,但是当提到Consumer时,我并没有完全理解。相比之下,我觉得从方法的调用关系来考虑会更容易理解。
补充一点,这个方法的T是由BuilderUser中的Man给定的,而不是指builder本身。
public class HumanBuilder implements Builder<Human>
在类的定义中给定了一个叫做Human的对象。当出现T或t的时候,会非常令人困惑。
最终的目标是,在两个或更多类型参数的类之间进行对象交互时,使用下限边界用于放宽限制。
此外,还有常用于描述下限边界的Comparable接口,但关于它的详细解释可能会在以后的时间写出来。