带有递归类型参数的泛型类型和抽象 self 方法如何允许方法链正常工作?

问题描述 投票:0回答:2

我正在阅读Effective Java Edition 3。在第 2 章第 14 页中,作者讨论了构建器模式并呈现了以下代码:

public abstract class Pizza {
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
    final Set<Topping> toppings;
    abstract static class Builder<T extends Builder<T>> {
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }
        abstract Pizza build();
        // Subclasses must override this method to return "this"
        protected abstract T self();
    }
    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); // See Item 50
    }
}

上述抽象类的实现:

public class NyPizza extends Pizza {
    public enum Size { SMALL, MEDIUM, LARGE }
    private final Size size;
    public static class Builder extends Pizza.Builder<Builder> {
        private final Size size;
        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }
        @Override public NyPizza build() {
            return new NyPizza(this);
        }
        @Override protected Builder self() { return this; }
    }
    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}

我们可以使用这样的代码:

NyPizza pizza = new NyPizza.Builder(SMALL)
.addTopping(SAUSAGE).addTopping(ONION).build();

书中引用:

注意

Pizza.Builder
泛型递归类型 参数。这与抽象 self 方法一起允许方法 链接可以在子类中正常工作,而不需要强制转换。

现在我的问题是

<T extends Builder<T>>
添加到
Pizza
类中的力量/价值是什么以及它与
<T extends Builder>
有何不同?如果你要用简单的英语向一个五岁的孩子解释
<T extends Builder<T>>
,你会如何解释?

我无法弄清楚超类中抽象 self 方法的用途?


我因为评论区而添加这部分

想象一下我已经像这样更改了上面的代码(这不会是仅用于说明目的的最佳示例):

我将

NyPizza.Builder
更改为通用:

public class NyPizza extends Pizza {
    public enum Size { SMALL, MEDIUM, LARGE }
    private final Size size;
    public static class Builder<T> extends Pizza.Builder<Builder<T>> {
        private final Size size;
        public Builder(Size size) {
            this.size = Objects.requireNonNull(size);
        }
        @Override public NyPizza build() {
            return new NyPizza(this);
        }
        @Override protected Builder self() { return this; }
    }
    private NyPizza(Builder builder) {
        super(builder);
        size = builder.size;
    }
}

和像这样的

Pizza
类:

public abstract class Pizza {
    public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE }
    final Set<Topping> toppings;
    abstract static class Builder<T extends Builder> {
        T obj;
        EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
        public T addTopping(Topping topping) {
            toppings.add(Objects.requireNonNull(topping));
            return self();
        }

        public T builder(){
            return obj;
        }

        abstract Pizza build();
        // Subclasses must override this method to return "this"
        protected abstract T self();
    }
    Pizza(Builder<?> builder) {
        toppings = builder.toppings.clone(); // See Item 50
    }
}

并像这样使用上面的类:

NyPizza.Builder<String> test = new NyPizza.Builder<String>(SMALL).builder();

现在因为我没有以这种形式定义

class Builder<T extends Builder>
class Builder<T extends Builder<T>>
类中的
builder
方法不应该能够检测到T的类型是
Pizza
,但它可以。这怎么可能?如何将其更改为
需要铸造

java generics design-patterns builder
2个回答
5
投票

NyPizza.Builder<String>

这意味着 
class Foo<T extends Foo<T>> { T foo() { ... } }

类中的

foo()
方法将返回一个
Foo<T>
的实例,因为
Foo<T>
表示的所有可能类型都是
T
的子类。
所以它

可以

返回自身 - 这就是为什么它对于与构建器的方法链接很有用;但实际上没有什么可以阻止它返回范围内的其他类。 如果您将其声明为:

Foo<T>

然后这使得
class Foo<T extends Foo> { T foo() { ... } }

成为

原始类型
T
Foo
返回
foo()
,而不是
Foo
。因为原始类型会删除所有泛型,这意味着在像
Foo<T>
这样的调用链中,您将在第一次调用后丢失类型信息。
在许多情况下,这可能并不“感觉”超级重要。但

在几乎所有情况下都应该避免使用原始类型

。这里原始类型的后果是: 如果您没有返回 self 类型(上面提到“实际上没有什么可以阻止它返回其他类”),那么您将在第一个方法调用后丢失该“其他类”。

如果您在
    foo().foo()
  1. 中有其他通用方法,例如
  2. Foo
  3. ,原始类型的
    List<String> myList() { ... }
    将返回原始
    Foo
    ,而不是
    List
    (原始类型会擦除
    所有
    泛型,而不仅仅是与省略的类型变量相关的泛型)。
    
    这些显然并不适用于所有情况;但由于引入不必要的原始类型是一种不好的做法,因此请确保不要引入它们。
        

问题:“现在因为我没有以这种形式定义 Builder 类:class Builder

Pizza 类中的 builder 方法应该无法检测到 T 的类型是 NyPizza.Builder 但它可以。这怎么可能?如何将其更改为需要铸造?”

0
投票
答案:编译器可以推断类型,因为在您重写的类 NyPizza 中,构建器是通过类型参数 [NyPizza.Builder] 来实现的,这与书中没有类型参数的示例不同。这个附加类型参数帮助编译器正确推断类型。

© www.soinside.com 2019 - 2024. All rights reserved.