java泛型类型

tech2023-02-16  123

java泛型类型

In some situations, particularly when implementing the builder pattern or creating other fluent APIs, methods return this. The method’s return type is likely to be the same as the class in which the method is defined–but sometimes that doesn’t cut it! If we want to inherit methods and their return type should be the inheriting type (instead of the declaring type), then we’re fresh out of luck. We would need the return type to be something like “the type of this”, often called a self type but there is no such thing in Java.

在某些情况下,尤其是在实现构建器模式或创建其他流畅的API时,方法将返回this 。 该方法的返回类型可能与定义该方法的类相同,但有时并不能削减它! 如果我们想继承方法,并且它们的返回类型应该是继承类型(而不是声明类型),那么我们很幸运。 我们需要返回类型类似于“此类型”,通常称为自类型,但Java中没有这种类型 。

Or is there?

还是在那里?

一些例子 (Some Examples)

Before we go any further let’s look at some situations where self types would come in handy.

在继续之前,让我们看一下自我类型会派上用场的某些情况。

Object::clone (Object::clone)

A very good example is hiding in plain sight: Object::clone. With some black magic incantations it creates a copy of the object on which it is called. Barring an exception that we can ignore here, it has the following signature:

一个很好的例子是: Object::clone 。 使用一些黑魔法咒语,它会创建对象的副本,并在该对象上调用它。 除非有一个我们可以在此处忽略的异常 ,否则它具有以下签名:

protected Object clone();

Note the return type: Object. Why so general? Let’s say we have a Person and would like to expose clone:

注意返回类型: Object 。 为什么这么一般? 假设我们有一个Person并且想公开clone :

public class Person { // ... @Override public Person clone() { return (Person) super.clone(); } }

We have to override the method to make it publicly visible. But we also have to cast the result of super.clone(), which is an Object, to Person. Mentally freeing ourselves from Java’s type system for a moment, we can see why that is weird. What else could clone return but an instance of the same class on which the method is called?

我们必须重写该方法以使其公开可见。 但是我们还必须将作为Object的super.clone()的结果super.clone()为Person 。 暂时将自己从Java的类型系统中解放出来,我们可以看到为什么这很奇怪。 clone返回的其他内容除了调用该方法的同一个类的实例之外,还有什么?

建造者 (Builder)

Another example arises when employing the builder pattern. Let’s say our Person needs a PersonBuilder:

使用构建器模式时会出现另一个示例。 假设我们的Person需要一个PersonBuilder :

public class PersonBuilder { private String name; public PersonBuilder withName(String name) { this.name = name; return this; } public Person build() { return new Person(name); } }

We can now create a person as follows:

现在,我们可以创建一个人,如下所示:

Person doe = new PersonBuilder() .withName("John Doe") .build();

So far, so good.

到目前为止,一切都很好。

Now let’s say we do not only have persons in our system–we also have employees and contractors. Of course they are also persons (right?) so Employee extends Person and Contractor extends Person. And because it went so well with Person we decide to create builders for them as well.

现在让我们说,我们不仅在系统中有人员,而且还有员工和承包商。 当然他们也是个人(对吗?),所以Employee extends Person而Contractor extends Person 。 并且由于与Person进行得如此顺利,我们决定也为他们创建构建器。

And here our journey begins. How do we set the name on our EmployeeBuilder?

从这里开始我们的旅程。 我们如何在EmployeeBuilder上设置名称?

We could just implement a method with the same name and code as in PersonBuilder, thus duplicating it except that it returns EmployeeBuilder instead of PersonBuilder. And then we do the same for ContractorBuilder? And then whenever we add a field to Person we add three fields and three methods to our builders? That doesn’t sound right.

我们可以使用与PersonBuilder相同的名称和代码来实现一个方法,从而将其复制,只是它返回EmployeeBuilder而不是PersonBuilder 。 然后我们对ContractorBuilder做同样的事情吗? 然后,每当我们向Person添加一个字段时,我们都会向构建器添加三个字段和三个方法? 听起来不对。

Let’s try a different approach. What’s our favorite tool for code reuse? Right, inheritance. (Ok, that was a bad joke. But in this case I’d say that inheritance is ok.) So we have EmployeeBuilder extend PersonBuilder and we get withName for free.

让我们尝试另一种方法。 我们最喜欢的代码重用工具是什么? 对, 继承 。 (好吧,这是个不EmployeeBuilder extend PersonBuilder笑话。但是在这种情况下,我会说继承是可以的。)因此,我们有EmployeeBuilder extend PersonBuilder并且免费获得了withName 。

But while the inherited withName indeed returns an EmployeeBuilder, the compiler does not know that–the inherited method’s return type is declared as PersonBuilder. That’s no good either! Assuming our EmployeeBuilder does some employee-specific stuff (like withHiringDate) we can’t access those methods if we call in the wrong order:

但是,尽管继承的withName确实返回了EmployeeBuilder ,但编译器并不知道-继承的方法的返回类型被声明为PersonBuilder 。 那也不好! 假设我们的EmployeeBuilder做一些特定于员工的工作(例如withHiringDate ),那么如果我们以错误的顺序调用它们,我们将无法访问这些方法:

Employee doe = new EmployeeBuilder() // now we have an EmployeeBuilder .withName("John Doe") // now we have a PersonBuilder .withHiringDate(LocalDateTime.now()) // compile error :( .build();

We could override withName in EmployeeBuilder:

我们可以在EmployeeBuilder覆盖withName :

public class EmployeeBuilder { public EmployeeBuilder withName(String name) { return (EmployeeBuilder) super.withName(name); } }

But that requires almost as many lines as the original implementation. Repeating such snippets in every subtype of PersonBuilder for each inherited method, is clearly not ideal.

但这几乎需要与原始实现相同的行。 对于每个继承的方法,在PersonBuilder的每个子类型中重复这样的片段显然是不理想的。

Taking a step back, let’s see how we ended up here. The problem is that the return type of withName is explicitly fixed to the class that declares the method: PersonBuilder.

退后一步,让我们看看如何结束这里。 问题是withName的返回类型明确地固定到声明方法的类: PersonBuilder 。

public class PersonBuilder { public PersonBuilder withName(String name) { this.name = name; return this; } }

So if the method is inherited, e.g. by EmployeeBuilder, the return type remains the same. But it shouldn’t! It should be the type on which the method was called instead of the one that declares it.

因此,如果方法被EmployeeBuilder继承,则返回类型保持不变。 但这不应该! 它应该是调用该方法的类型,而不是声明该方法的类型。

This problem quickly rears its head in fluent APIs, like the builder pattern, where the return type is of critical importance to make the whole API work.

这个问题很快在流畅的API中引起了关注,例如构建器模式,其中返回类型对于使整个API正常工作至关重要。

递归容器 (Recursive Containers)

Finally let’s say we want to build a graph:

最后,假设我们要构建一个图形:

public class Node { private final List<Node> children; public Stream<? extends Node> children() { return children.stream(); } }

Down the road we realize that we need different kinds of nodes and that trees can only contain nodes of one kind. As soon as we use inheritance to model the different kinds of nodes we end up in a very similar situation:

后来,我们意识到我们需要不同类型的节点,并且树只能包含一种类型的节点。 一旦使用继承为不同类型的节点建模,我们最终会遇到非常相似的情况:

public class SpecialNode extends Node { @Override public Stream<? extends SpecialNode> children() { return super.stream(); // compile error :( } }

A Stream<? extends Node> is no Stream<? extends SpecialNode> so this doesn’t even compile. Again we have the problem that we would like to say “we return a stream of nodes of the type on which this method was called”.

Stream<? extends Node> Stream<? extends Node>是没有Stream<? extends SpecialNode> Stream<? extends SpecialNode>因此甚至无法编译。 同样,我们有一个问题要说:“我们返回调用此方法的类型的节点流”。

救援的自我类型 (Self Types to the Rescue)

Some languages have the concept of self types:

一些语言具有自我类型的概念:

A self type refers to the type on which a method is called (more formally called the receiver).

自我类型是指在其上调用方法的类型(更正式地称为接收器 )。

If a self type is used in an inherited method, it represents a different type in each class that declares or inherits that method–namely that specific class, no matter whether it declared or inherited the method. Casually speaking it is the compile-time equivalent of this.getClass() or “the type of this”.

如果在继承的方法中使用了自类型,则在声明或继承该方法的每个类中,它都表示一个不同的类型,即该特定类 ,无论它是声明还是继承了该方法。 随意地说,它是this.getClass()或“此类型”的编译时等效项。

In the remainder of this post I will notate it as [this] ([self] would be good, too, but with [this] we get some syntax highlighting).

在本文的其余部分中,我将其标记为[this] ( [self]也很好,但是使用[this]我们会突出显示一些语法)。

自我类型的例子 (The Examples with Self Types)

To be perfectly clear: Java has no self types. But what if it had? How would our examples look then?

完全清楚: Java没有self类型 。 但是,如果有呢? 那么我们的示例看起来如何?

Object::clone (Object::clone)

Object::clone was supposed to return a copy of the instance it was called on. That instance should of course be of the same type, so the signature could look as follows:

Object::clone应该返回它被调用的实例的副本。 该实例当然应该是相同的类型,因此签名可能如下所示:

protected [this] clone();

Subclasses can then directly get an instance of their own type:

然后,子类可以直接获取自己类型的实例:

public class Person { // ... @Override public [this] clone() { // no cast required // because in this class [this] means Person Person clone = super.clone(); return clone; } }

建造者 (Builder)

For the builders we discussed above, the solution is similarly obvious. We simply declare all with... methods to return the type [this]:

对于我们上面讨论的建造者,解决方案同样显而易见。 我们只with...方法声明所有方法以返回类型[this] :

public class PersonBuilder { public [this] withName(String name) { this.name = name; return this; } }

As before EmployeeBuilder::withName returns an instance of EmployeeBuilder but this time the compiler knows that and what we wanted before works now:

如前EmployeeBuilder::withName返回的实例EmployeeBuilder但此时的编译器知道什么,我们之前想现在工作:

Employee doe = new EmployeeBuilder() // now we have an EmployeeBuilder .withName("John Doe") // still an EmployeeBuilder thanks to [this] .withHiringDate(LocalDateTime.now()) // works! :) .build();

递归容器 (Recursive Containers)

Last but not least, let’s see how our graph works out:

最后但并非最不重要的一点,让我们看看我们的图形是如何工作的:

public class Node { private final List<[this]> children; public Stream<? extends [this]> children() { return children.stream(); } }

Now SpecialNode::children returns a Stream<? extends SpecialNode>, which is exactly what we wanted.

现在SpecialNode::children返回Stream<? extends SpecialNode> Stream<? extends SpecialNode> ,这正是我们想要的。

用泛型模拟自我类型 (Emulating Self Types with Generics)

While Java doesn’t have self types, there is a way to emulate them with generics. This is limited and a little convoluted, though.

虽然Java没有自我类型,但是有一种方法可以用泛型来模拟它们。 但是,这是有限的,并且有点令人费解。

If we had a generic type parameter referring to this class, say THIS, we could simply use it wherever we used [this] above. But how do we get THIS? Simple (almost), we just add THIS as a type parameter and have inheriting classes specify their own type as THIS.

如果我们有一个泛型类型参数引用这个类,例如THIS ,那么我们可以在上面使用[this]任何地方简单地使用它。 但是我们如何得到THIS呢? 简单(几乎),我们只是将THIS添加为类型参数,并让继承的类将其自身类型指定为THIS 。

Wait, what? I think an example clears this up.

等一下 我认为有一个例子可以解决这个问题。

public class Object<THIS> { protected THIS clone(); } public class Person extends Object<Person> { @Override public Person clone() { // no cast required because // in this class THIS was specified as Person Person clone = super.clone(); return clone; } }

If this looks fishy (what does Object<Person> even mean?), you already stumbled upon one of the weaknesses of this approach but we’ll cover that in a minute. First, let’s explore it a little more and check our other examples.

如果这看起来很可疑( Object<Person>甚至意味着什么?),您已经偶然发现了这种方法的弱点之一,但我们将在稍后讨论。 首先,让我们进一步研究它,并查看其他示例。

public class PersonBuilder<THIS> { private String name; public THIS withName(String name) { this.name = name; return (THIS) this; // if we do this more often, '(THIS) this' // should become its own method } } public class EmployeeBuilder extends PersonBuilder<EmployeeBuilder> { }

Now EmployeeBuilder::withName returns an EmployeeBuilder without us having to do anything.

现在, EmployeeBuilder::withName无需执行任何操作EmployeeBuilder::withName返回EmployeeBuilder 。

Similarly our problems with Node goes away:

同样,我们关于Node的问题也消失了:

public class Node<THIS> { private final List<THIS> children; public Stream<? extends THIS> children() { return children.stream(); } } public class SpecialNode extends Node<SpecialNode> { }

Same as with [this], we need no additional code in SpecialNode.

与[this]相同,我们在SpecialNode不需要其他代码。

局限性和弱点 (Limitations and Weaknesses)

That doesn’t look too bad, right? But there are some limitations and weaknesses that we have to iron out.

看起来还不错,对吧? 但是,我们必须克服一些限制和弱点。

令人困惑的抽象 (Confusing Abstraction)

As I said above, what does Object<Person> even mean? Is it an object that holds, creates, processes or otherwise deals with a person? Because that is how we usually understand a generic type argument. But it is not–it’s just an “Object of Person”, which is rather strange.

就像我上面说的, Object<Person>甚至意味着什么? 它是持有,创建,处理或以其他方式与人打交道的对象吗? 因为这就是我们通常理解泛型类型参数的方式。 但事实并非如此,它只是一个“人的对象”,这很奇怪。

卷积类型 (Convoluted Types)

It is also unclear how to declare the supertypes now. Is it Node, Node<Node>, or even more Nodes? Consider the following:

现在还不清楚如何声明超类型。 是Node , Node<Node>还是更多Node ? 考虑以下:

Node<Node> node = new Node<>(); Stream<Node> children = node.children(); Stream grandchildren = children .flatMap(child -> child.children());

Calling children twice, we “used up” the generic types we declared für node and now we get a raw stream. Note that thanks to the recursive declaration of SpecialNode extends Node<SpecialNode> we don’t have that problem there:

两次调用children ,我们“用完”了声明为node的通用类型,现在我们得到了原始流。 请注意,由于SpecialNode extends Node<SpecialNode>的递归声明SpecialNode extends Node<SpecialNode> ,所以这里没有这个问题:

SpecialNode node = new SpecialNode(); Stream<SpecialNode> children = node.children(); Stream<SpecialNode> grandchildren = children .flatMap(child -> child.children());

All of this will confuse and ultimately alienate users of such types.

所有这些都会使这种类型的用户感到困惑,并最终使其疏远。

单级继承 (Single Level Inheritance)

The trick to get SpecialNode to behave as expected on repeated calls to children meant that it had no self-referencing type parameter of its own. Now, a type extending it can not specify itself anywhere, so its methods return special nodes instead:

使SpecialNode在重复调用children表现出预期效果的技巧意味着它没有自己的自引用类型参数。 现在,扩展它的类型无法在任何地方指定其自身,因此其方法将返回特殊节点:

public class VerySpecialNode extends SpecialNode { } VerySpecialNode node = new VerySpecialNode(); // we a want Stream<VerySpecialNode> Stream<SpecialNode> children = node.children(); // damn :(

THIS太普通了 (THIS Is Too Generic)

As it stands, THIS can be any type, which means we must treat it as an Object. But what if we wanted to do something with our instances of it that was specific to our current class?

就目前而言, THIS可以是任何类型,这意味着我们必须将其视为Object 。 但是,如果我们想对我们当前班级的实例进行某些处理呢?

public class Node<THIS> { // as before, especially `children` public Stream<THIS> grandchildren() { // doesn't compile because `child` is no `Node` // and hence has no `children` method return children.flatMap(child -> child.children()); } }

Well, that’s just stupid.

好吧,那只是愚蠢的。

完善方法 (Refining the Approach)

Let’s see if we can’t do a little better than before and tackle some of those weaknesses. And there is indeed something we can do that addresses all the problems we just discussed.

让我们看看我们是否能做得比以前更好,并解决其中的一些弱点。 实际上,我们可以做些事情来解决我们刚刚讨论的所有问题。

递归泛型 (Recursive Generics)

Last things first, let’s look at THIS being too generic. This can easily be fixed with recursive generics:

首先要说的最后一点,让我们看一下THIS太笼统了。 这可以通过递归泛型轻松解决:

public class Node<THIS extends Node<THIS>> { // as before, especially `children` public Stream<THIS> grandChildren() { // hah, now `child` is a `Node` return children.flatMap(child -> child.children()); } }

(I said “easily” not “simply”.)

(我说的是“轻松”而不是“简单”。)

But this exacerbates the problem of convoluted types considerably. Now it’s not even possible to declare a Node because the compiler always expects another type parameter:

但这极大地加剧了卷积类型的问题。 现在甚至不可能声明Node因为编译器始终需要另一个类型参数:

// doesn't compile // the fourth `Node` is not // within its type bounds of `Node<Node>` Node<Node<Node<Node>>> node = new Node<>();

私人阶层 (Private Hierarchy)

We can fix this, though, and the remaining problems with another detour.

不过,我们可以解决此问题,并解决其他绕道问题。

We can create a hierarchy of abstract classes that contain all the code we talked about so far. The concrete implementations our clients will use are then offshoots of that hierarchy. Because nobody will ever directly use the abstract classes we can use THIS on every level of inheritance–the implementations will then specify it.

我们可以创建一个抽象类的层次结构,其中包含到目前为止我们所讨论的所有代码。 我们的客户将使用的具体实现就是该层次结构的分支。 因为没有人会直接使用抽象类,所以我们可以在继承的每个级别上使用THIS然后实现将对其进行指定。

Ideally the abstract classes are package visible so their hideousness is hidden from the outside world.

理想情况下,抽象类是包可见的,因此它们的丑陋对外界是隐藏的。

abstract class NodeScaffold<THIS extends NodeScaffold<THIS>> { private final List<THIS> children; public Stream<THIS> children() { return children.stream(); } public Stream<THIS> grandChildren() { return children.stream() .flatMap(child -> child.children()); } } abstract class SpecialNodeScaffold<THIS extends SpecialNodeScaffold<THIS>> extends NodeScaffold<THIS> { // special methods } abstract class VerySpecialNodeScaffold<THIS extends VerySpecialNodeScaffold<THIS>> extends SpecialNodeScaffold<THIS> { // more special methods } public class Node extends NodeScaffold<Node> { } public class SpecialNode extends SpecialNodeScaffold<SpecialNode> { } public class VerySpecialNode extends VerySpecialNodeScaffold<VerySpecialNode> { }

A small detail: The publicly visible classes do not inherit from one another so a SpecialNode is no Node. If we did the same with the builders that might be ok but it is awkward with nodes. To fix this, we need yet another layer of abstraction, namely some interfaces that extend each other and are implemented by the public classes.

一个小细节:公开可见的类不会互相继承,因此SpecialNode不是Node 。 如果我们对构建器做同样的事情,那可能没问题,但是节点很尴尬。 为了解决这个问题,我们需要再一层抽象,即一些互相扩展并由公共类实现的接口。

Now the users of the public classes see clear abstractions and no convoluted types. The approach works across arbitrary many levels of inheritance and THIS always refers to the most specific type.

现在,公共类的用户可以看到清晰的抽象,而没有复杂的类型。 该方法适用于任意多个继承级别,并且THIS始终引用最具体的类型。

THIS作为参数类型 (THIS As Argument Type)

You might have noticed that all the examples use [this] and THIS as a return type. Can’t we also use it as a type for arguments? Unfortunately not because while return types are covariant, argument types are contravariant–and [this]/THIS is inherently covariant. (Check out this StackOverflow question for a brief explanation of these terms.)

您可能已经注意到,所有示例都使用[this]和THIS作为返回类型。 我们不能也将它用作参数的类型吗? 不幸的是,不是因为返回类型是协变的,而是参数类型是协变的,并且[this] / THIS本质上是协变的。 (查看此StackOverflow问题 ,以获取对这些术语的简要说明。)

If you try to do it (e.g. by adding void addChild(THIS node)) following the approach above, it will seem to work out until you try to create the interfaces that bring Node, SpecialNode, and VerySpecialNode into an inheritance relationship. Then the type of node can not become more specific as you go down the inheritance tree.

如果您按照上述方法尝试执行此操作(例如,通过添加void addChild(THIS node) ),则在您尝试创建将Node , SpecialNode和VerySpecialNode引入继承关系的接口之前,它似乎可以解决。 然后,沿着继承树向下移动时, node的类型就无法变得更加具体。

This post is already long enough so I will leave the details as an exercise to the curious reader.

这篇文章已经足够长了,因此我会将详细信息作为练习留给好奇的读者。

摘要 (Summary)

We have seen why we would sometimes need to reference “the type of this” and how a language feature could look like that does that. But Java doesn’t have that feature, so we had to come up with some tricks to do it ourselves:

我们已经了解了为什么有时需要引用“这种类型”,以及某种语言功能看起来如何做到这一点。 但是Java没有该功能,因此我们必须自己想出一些技巧:

we defined the methods we want to inherit in an abstract classes (A)

我们在抽象类( A )中定义了要继承的方法

we gave them a recursive generic type parameter (A<THIS extends A<THIS>>)

我们为他们提供了一个递归的泛型类型参数( A<THIS extends A<THIS>> )

we created publicly visible concrete implementations that specify themselves as that type (C extends A<C>)

我们创建了公开可见的具体实现,将其指定为该类型( C extends A<C> )

if required we create an interface inheritance tree that our concrete classes implement

如果需要,我们创建一个接口继承树,由我们的具体类实现

Whether this was worth all the effort and trickery is up to you to decide and depends on the use case you have. Generally speaking, the more methods you can inherit this way the more you’ll feel the benefits (look at AssertJ’s API implementation for an example of how many this can be). Conversely the friction from understanding this pattern decreases when the classes you create this way are fundamental for your code base–in line with “if it hurts, do it more often”. If it remains a fringe solution, developers will not know it’s there and stumble into it inadvertently and unexpectedly, being more easily confused.

这是否值得所有的努力和技巧取决于您的决定,并取决于您的用例。 一般而言,您可以通过这种方式继承的方法越多,您会感受到的好处就越多(有关示例,请参见AssertJ的API实现 )。 相反,当您以这种方式创建的类对于代码库而言是基本的时,理解这种模式所产生的摩擦就会减少,这与“如果有损,请经常执行”相一致。 如果它仍然是一个边缘解决方案,那么开发人员将不会知道它在那里,而会不经意间意外地陷入其中,更容易感到困惑。

What do you think? Do you see a use case in your code base? I’m interested to hear about it, so leave a comment.

你怎么看? 您在代码库中看到用例了吗? 我很想听听它,所以发表评论。

翻译自: https://www.sitepoint.com/self-types-with-javas-generics/

java泛型类型

最新回复(0)