公共静态工厂方法

20

首先,如果我的问题太蠢,请原谅我。我只是想全面学习这门语言。我正在阅读《Effective Java》,第一章讲述了静态工厂方法与构造函数之间的优缺点。有几件事情让我感到困惑:

  1. 由静态工厂方法返回的对象的类是非公共的——这具体是什么意思?
  2. 与构造函数不同,调用静态工厂方法时不需要每次创建一个新对象——这是如何发生的?我只是调用工厂方法来获取一个新对象,难道在工厂方法中要检查对象是否已经存在吗?

谢谢。


2
对于深入探究软件设计实践并提出好问题的人,我给予加一分。 - Brian Driscoll
好的程序员习惯是在编程之前问为什么,而不是机械地按照规定流程进行。这种习惯可以让你成为一个更好的编程人员。 - TreyE
2
了解在哪里可以找到一些真实世界的例子,并检查它们的源代码,可能有助于更好地理解设计模式。 - BalusC
5个回答

11

静态工厂方法返回的对象类是非公开的——这究竟意味着什么?

这意味着,由静态工厂方法返回的对象的实际类可以是声明类型的子类,并且该子类不必是公共的。这只是另一个客户端代码不需要关心的实现细节。

与构造函数不同,调用静态工厂方法时不要求每次都创建一个新对象——这是如何实现的?我只是为了获取新对象而调用工厂方法,我们在工厂方法中添加检查以检查对象是否已存在吗?

是的,这是其中一种实现方式。但实际上,任何事情都有可能发生。


嗨,迈克尔,这取决于需求吗?没有硬性规定工厂方法应始终检查是否已存在实例。 - t0mcat
@t3ch:没错,完全正确。关键是如果使用工厂方法有用并且是您想要的,那么您可以这样做...但是使用new就没有这个选项。 - ColinD
哇,谢谢。至少现在我知道它在哪里会有用了。使用这种方法可以更好地理解单例模式。 - t0mcat

5
首先,对于您选择的Java经典读物Bloch的书,我要向您致以赞扬:这是一本非常好的入门读物。
回答您的第二个问题(“与构造函数不同,静态工厂方法在每次调用时不需要创建一个新对象”),重要的是要意识到Bloch在这里说的是,使用静态工厂,您可以选择返回一个新对象或者返回一个预先存在的对象。这完全取决于您想做什么。
例如,假设您有一个非常简单的值类Money。您的静态工厂方法可能应该返回一个新实例——即具有特定Money值的新对象。像这样:
public class Money { 

    private Money(String amount) { ... } /* Note the 'private'-constructor */

    public static Money newInstance(String amount) {
        return new Money(amount);
    }

}

但是假设您有一些管理某些资源的对象,并且您想通过一些ResourceManager类同步访问该资源。在这种情况下,您可能希望您的静态工厂方法向每个人返回相同的自身实例--强制每个人都通过同一实例,以便该1个实例可以控制进程。这遵循单例模式。像这样:

public ResourceManager {

    private final static ResourceManager me = new ResourceManager();

    private ResourceManager() { ... } /* Note the 'private'-constructor */

    public static ResourceManager getSingleton() {
        return ResourceManager.me;
    }
}

上述方法可以强制用户只能使用单个实例,从而让您精确控制谁可以(何时)访问您管理的内容。


回答你的第一个问题,请考虑以下内容(尽管这并不是最好的示例,它相当临时):

public class Money {

    private Money(String amount) { ... }


    public static Money getLocalizedMoney( MoneyType localizedMoneyType, String amount ) { 
        switch( localizedMoneyType ) {
            case MoneyType.US:
                return new Money_US( amount );
            case MoneyType.BR:
                return new Money_BR( amount );
            default:
                return new Money_US( amount );
        }
    }
}

public class Money_US extends Money { ... }

public class Money_BR extends Money { ... }

注意,我现在可以这样做:
Money money = Money.getLocalizedMoney( user_selected_money_type );
saveLocalizedMoney( money );

再次强调,这只是一个非常牵强的例子,但希望它能帮助你更加理解Bloch所说的那一点。

其他答案都很好,但我认为作为初学者,有时看到一些实际的代码会有所帮助。


就像你上面提到的例子一样,为什么我们不需要在第一个例子中使用静态工厂方法来代替公共构造函数呢?我们可以将其设置为公共构造函数然后使用它,我真的不知道为什么要将构造函数设置为私有并创建一个静态工厂方法来完成与公共构造函数相同的事情。 - Shen
1
@Shen,这个问题的答案太长了,无法在这里回答;作者列出了5个原因,并且需要大约4页来解释他的推理。请参考Joshua Bloch的《Effective Java》中的“第1条:考虑使用静态工厂方法代替构造器”。然而,简短的答案是,你不一定“必须这样做”,但使用静态工厂方法会给你带来更多的灵活性。现在,特别是针对你关于“第一个例子”的问题,那个例子有点牵强附会和过于简单化;不一定是最清晰的传达“为什么”的方式。 - Bane
1
@Shen 话虽如此,仍可视为未来改进的基础。如果您只使用构造函数编写代码,则在以后想要引入我在第一个示例之后展示的“本地化货币类型”代码时,可能会被锁定在该模式中。另一方面,如果默认情况下隐藏构造函数,则可以使用简单的工厂模式调用getLocalizedMoneyType(DefaultMoneyTypeBasedOnLocale, amount)--因为没有人已经执行了new Money(amount),所以不会破坏任何现有的客户端代码。 - Bane
@Bane感谢您的回复,根据您的答案,我知道我们有许多情况可以使用静态工厂方法而不是构造函数,在first-example的情况下,我们使用静态工厂方法是因为我们想在未来使用其他类型和改进,对吗? - Shen
1
@Shen 正确。对于最简单的值类,这可能有些过度了。但是对于您预见可能会变得更加复杂的用途(例如,其他人以 API 方式使用此代码;注意,不一定是 REST-API;只是通用 API),那么是的,我所说的就是:我们应该使用静态工厂方法来抽象底层实现中的未来更改,从而为最终用户留下更灵活的设计。 - Bane
显示剩余3条评论

2
当你使用new关键字时,作为开发人员,你知道JDK会创建该对象的新实例。作者所说的是,当您使用静态方法时,开发人员不再知道该方法是否正在创建一个新实例或可能执行其他操作。其他操作可能包括重用缓存数据、对象池、创建私有实现并返回类的子类。

2
你并不总是需要一个新的实例。以数据库连接为例:在Java中,你可以使用jdbc.newConnection()来创建一个新的连接。但是Java并不会每次都创建一个新的实例。它首先检查是否已经存在一个连接。如果没有,它将获取一个新的实例。另一个例子可能是你想创建一个单例。(单例也有它们自己的问题)这意味着一个类应该只有一个实例,这是有充分理由的。因此,你应该将构造函数设为私有,并且只允许通过静态类来使用你的API。 - Amir Raminfar
2
@Amir: 使用构造函数比一些令人困惑的方法更清晰,因为开发者不需要寻找它们...统一的命名约定可以解决这个问题。浏览静态方法名称比基于它们的参数尝试确定要使用哪个构造函数要容易得多。使用构造函数时很容易出错(“哦,我想要 new SomeType(int, int, String, int) 而不是 new SomeType(int, int, int, String)...”) - Mark Peters
2
@Amir:这个缺点只是为了让论点看起来更平衡。使用注释和适当的命名可以轻松地为您提供静态支持,以便在Javadocs等文档中区分它们。这并不是不使用它们的好理由。 - Mark Peters
1
@Amir:在Java中,您无法选择构造函数的名称。当然,我并不是为每个类都这样做,但绝对不是因为缺乏清晰度。还有其他有效的原因(例如,如果类型旨在扩展,则基本上不可能)。 - Mark Peters
1
@Amir:我完全同意Mark的观点——静态工厂方法是最好的选择。话虽如此,如果你正在编写一个简单的值类,只打算自己使用,那么(按照我的经验)我不会担心静态工厂类——我只使用构造函数。但是,Bloch的大部分书籍都应该在API DEVELOPER的背景下理解。也就是说,如果你正在开发一个面向他人消费的api,几乎总是应该使用静态工厂方法。 - Bane
显示剩余25条评论

1
静态工厂方法返回的对象类是非公共的。通常,静态工厂方法将返回一个接口类型的对象(最常见),或者有时返回一些基类(不太常见)。在任何情况下,您都不知道返回对象的确切类。
这样做的好处是获得一个行为已知的对象,而不必担心实例化它的混乱细节。
与构造函数不同,静态工厂方法不需要每次调用时创建一个新对象。要理解这一点,请考虑使用单例模式的情况。您可以在某些工厂类上调用.getInstance()以获取某个对象的单例实例。通常,如果该对象不存在,则会创建该对象的实例;如果已经存在,则会给您现有的实例。在任何情况下,您都会收到该对象的副本。但是您不会(也不会)知道此单例是否必须创建,或者以前是否已经构建了一个单例。
这样做的好处是对象的生命周期和创建时间由系统管理。

嗨 Trey,你的单例模式示例真的解决了我的疑惑。谢谢 :) - t0mcat

0

通过查看使用静态工厂方法这两个属性的代码,可以回答你的两个问题。我建议查看Guava的ImmutableList

注意无参数工厂方法of()总是返回相同的实例(它不会每次创建新实例)。如果仔细观察,您还会注意到其copyOf(Iterable)工厂方法实际上返回传递给它的对象,如果该对象本身就是ImmutableList。这两者都利用了ImmutableList永远不会改变的事实。

请注意,它中的各种工厂方法返回不同的子类,例如EmptyImmutableListSingletonImmutableListRegularImmutableList,而不会暴露这些对象的类型。方法签名只显示它们返回ImmutableList,并且所有ImmutableList的子类都具有包私有(默认)可见性,使它们对库用户不可见。这为多个实现类带来了所有优点,而从用户的角度来看,没有增加任何复杂性,因为他们只允许将ImmutableList视为单个类型。

除了ImmutableList之外,Guava中的大多数可实例化类都使用静态工厂方法。Guava还展示了《Effective Java》中提出的许多原则(这并不奇怪,因为它是根据这些原则设计的,并在Josh Bloch的指导下进行了设计),因此在阅读本书时,您可能会发现它更有用。


谢谢你的示例,Colin。我正在仔细阅读它。 - t0mcat

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接