【问题标题】:Static factory methods with generics带有泛型的静态工厂方法
【发布时间】:2018-08-04 09:55:15
【问题描述】:

我在使用 Java 中的泛型时遇到了一些问题。我有一个通用基类A,它定义了一个静态工厂方法来创建具有初始值的A 实例。我这样做是为了在创建 A 的实例时设置一些初始值,而不是指定设置初始值的构造函数,因为它会涉及在构造函数中调用可覆盖的方法(强烈建议不要这样做)。

public class A<T> {
    private List<T> values;

    public A() {
        this.values = new ArrayList<>();
    }

    public void setValues(List<T> values) {
        this.values.clear();
        for (T value : values) {
            this.values.add(this.modify(value));
        }
    }

    protected T modify(T value) {
        // Make modifications to value before it is stored. My real implementation actually does stuff.
        return value;
    }

    public static <T> A<T> create(List<T> values) {
        A<T> a = new A<>();
        a.setValues(values);
        return a;
    }
}

我还有一个类B,它扩展A 并指定要使用的泛型类型。

public class B extends A<Integer> {
    public B() {
        super();
    }
}

我希望能够使用B 中的create 静态方法来创建带有初始项的B 实例。尝试调用 B.create(...) 时,我收到一条错误消息:

错误:(5, 23) java: 不兼容的类型: 不存在类型变量 T,E 的实例,因此 A 符合 B

所以我决定尝试为create 定义一个特定于B 类的实现:

public static B create(List<String> values) {
    B b = new B();
    b.setValues(values);
    return b;
}

但这次我得到一个不同的错误:

Error:(8, 21) java: name conflict: create(java.util.List) in B 和 create(java.util.List) in A 具有相同的擦除,但都没有隐藏另一个

虽然我理解每个错误的实际含义,但我不知道如何绕过它们。如何在A 上定义一个可与任何子类一起使用的静态工厂方法,或者如何在A 上定义一个静态工厂方法,以及在覆盖/隐藏的A 的每个适用子类上定义一个静态工厂方法A的实现?

【问题讨论】:

    标签: java generics


    【解决方案1】:

    这里可能没有完美的答案。有几个选项可能有意义,但细节可能取决于应用案例。我将列出一些关于静态工厂方法应该在哪里,它们的名称应该是什么?

    的选项
    1. 将它们放入类中

    这就是你现在所做的,但可能有一些警告:A 中的方法创建了一个A 的实例,你不能仅仅通过调用它“通过”来神奇地将其更改为B B。不管你做什么,B.create() 仍然会创建一个A

    如您所见,在B 中创建create() 方法会导致名称冲突。


    1. 将它们放入与它们操作的类相对应的类中

    Tomasz Linkowski 在他的回答中提到的方法并不少见:对于名为 ClassName 的类,有一个名为 ClassNames 的类包含创建实例的方法。

    当类名没有合理的复数时,这让我很烦恼,但是......

    举个例子:

    class Customer { /* With package-private constructor, usually ... */ }
    public final class Customers {
        public static Customer create() { ... }
    }
    
    class PremiumCustomer { /* With package-private constructor, usually ... */ }
    public final class PremiumCustomers {
        public static PremiumCustomer create() { ... }
    }
    

    这样做的好处是您可以轻松添加扩展 Customer 的类,而不会影响现有代码库。

    这里的一个小缺点是您不能使用方法的静态导入,因为它们都具有相同的名称,但这在实际代码中应该很少成为问题。


    1. 将它们归为一个聚合不同类型的类

    您的类被称为AB,它们并没有传达实际的结构。但是根据这种结构,让一个类具有不同的工厂方法(具有不同的名称)来指示专业化可能更有意义:

    public final class Customers {
        public static Customer createStandard() { ... }
        public static PremiumCustomer createPremium() { ... }
    }
    

    很难说后一种选择哪个“更好”。这也取决于包的结构:当类在不同的包中时,不能将它们的构造函数设为包私有,这可能是不可取的。


    可能很重要的注释:您专门询问了 static 工厂方法。但是您描述的场景引发了另一个问题:谁应该调用这些方法,以及如何调用?

    每个人都必须知道他想要创建的实例的确切类型...

    一般来说,当涉及到多态性和继承性时,static 方法总是会失败,并且会让人头疼。您应该考虑改用(“简单”,非静态)工厂。

    当你有这个时:

    void example() {
        Customer customer = Customer.create();
        doSomeComplexStuffWith(customer);
    }
    

    那么实现者必须显式调用Customer.create()(或Customers.create()Customers.createDefault() - 这在这里无关紧要)。

    无法更改在那里创建的元素的类型

    此时,您可以考虑将静态工厂方法的使用更改为Supplier

    void example(Supplier<? extends Customer> supplier) {
        Customer customer = supplier.get();
        doSomeComplexStuffWith(customer);
    }
    

    这样,您可以将静态方法用作实际的工厂实例,可能用作方法引用。 example 方法的实现不受此处类型更改的影响:

    example(Customers::createStandard); // Run example with standard customers
    example(Customers::createPremium); // Run example with premium customers
    

    但同样,这首先取决于您打算如何创建和使用实例。


    一个小评论:

    强烈建议不要在构造函数中调用可覆盖的方法...

    这是真的。但是当你有一个静态工厂方法时,可以将其视为一种极端情况,并制作privateprotected 类的构造函数。然后,您可以建立一个合约,对创建过程有更多的控制权,并确保没有被覆盖的方法引起的不良影响。

    但一般来说,工厂确实可能不需要在构造函数中调用可覆盖的方法。

    【讨论】:

    • 非常感谢您提供非常丰富的答案。正是我正在寻找的东西,很多细节不仅解释了如何,而且解释了为什么。再次感谢!
    • @Jonathon 我刚刚意识到我描述的示例通常不会将List&lt;T&gt; 作为他们的论点。因此,davidxxx 的答案可能更接近您的具体情况。 (当然可以在我使用的示例中添加此参数,但在上一节中,您必须使用 Function&lt;List&lt;T&gt;, Customer&gt;,而不是 Supplier&lt;Customer&gt;...)
    • 没问题。我主要是在寻找一个在概念层面解释如何克服我的特定问题的答案。在论点类型之间进行翻译应该不会有太多麻烦。我的实际问题也不只是使用List&lt;T&gt;。我刚刚创建了一个简单的 MCVE,将问题简化为任何其他细节。再次感谢!
    【解决方案2】:

    如何在 A 上定义一个可以使用的静态工厂方法 任何子类

    在这种情况下,为什么要在 A 中定义方法?

    或者我如何在 A 上定义静态工厂方法以及静态 覆盖/隐藏的 A 的每个适用子类上的工厂方法 A的实现?

    static 方法和覆盖不是很好地结合在一起的东西。
    你必须改变你的方法。

    我会使用以下两种方式之一:

    • 在每个类中定义一个静态工厂方法。
    • 定义一个工厂类,该类提供工厂方法来通过依赖泛型、推理和Supplier 创建任何实例。

    1) 定义静态工厂方法并不复杂,但您应该注意两件事(类的泛型或不存在)以及具有特定名称和相同擦除的静态方法这一事实类和子类在子类中产生阴影行为。这不是很优雅,可能容易出错。

    它可能看起来像:

    public class A<T> {
        private List<T> values;
    
        public A() {
            this.values = new ArrayList<>();
        }
    
        public static <T> A<T> createA(List<T> values) {
            A<T> a = new A<>();
            a.setValues(values);
            return a;
        }
    
    }
    
    public class B extends A<Integer> {
        public B() {
            super();
        }
    
        public static  B createB(List<Integer> values) {
            B b = new B();
            b.setValues(values);
            return b;
        }           
    }
    

    并以这种方式使用它们:

    A<String> a = A.createA(Arrays.asList("hello" ,"you"));
    B b = B.createB(Arrays.asList(1 ,2));
    

    2) 定义一个提供静态工厂方法的工厂类。

    public final class MyFactory{
    
        public static <T, U extends A<T>> U create(Supplier<U> supplierA, List<T> values) {     
            U a = supplierA.get();
            a.setValues(values);
            return a;
        }
    }
    
    
    public class A<T> {
        public void setValues(List<T> values) {
            this.values.clear();
            for (T value : values) {
                this.values.add(this.modify(value));
            }
        }
     }
    
    public class B extends A<Integer> {
    }
    

    现在你有一个独特的方法来创建 A 的任何实例:

    A<String> list = MyFactory.create(A::new, Arrays.asList("hello", "you"));
    B b = MyFactory.create(B::new, Arrays.asList(1, 2));
    

    【讨论】:

    • 感谢您的回答。非常有用且有用的示例。
    • 不客气 :) 我刚刚注意到我混合了一些东西。接口在这里没用。 MyFactory 作为工厂类就够了。
    • 感谢您的澄清!
    【解决方案3】:

    好吧,你正在走 Guava went 的路,现在他们走的是 somewhat regret it。也就是说,您将静态工厂方法放在非最终类中。

    我想提出一种不同的方法:创建一个“ 伴侣”实用程序类,具有复数名称(例如SomeType -> SomeTypes),并将您的静态工厂放在那里。然后对您的其他类型执行相同的操作。

    由于这种方法,您无法(甚至意外地)获得与预期不同的类型(现在,调用B.create 会产生A 的实例)。

    【讨论】:

    • 感谢您的回答。我会尽快查看这些链接。我认为你建议的方法是我会去的。再次感谢
    猜你喜欢
    • 1970-01-01
    • 2011-01-19
    • 2014-01-12
    • 2011-04-16
    • 2021-07-26
    • 2023-03-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多