在私有实用程序类构造函数中,首选使用哪种Throwable?

34

Effective Java (第二版),第4条,讨论使用私有构造函数来实现无法实例化。以下是书中的代码示例:

public final class UtilityClass {
    private UtilityClass() {
        throw new AssertionError();
    }
}

然而,AssertionError 似乎并不是正确的抛出对象。没有被"asserted",这正是API定义使用 AssertionError 的方式。

在这种情况下,是否有不同的 Throwable 可以使用?通常是否只需抛出带有消息的一般性 Exception?还是常常编写自定义的 Exception

这相当琐碎,但最重要的是,我想从风格和规范的角度了解它。


2
我已经在以下问题上设置了一个(相当大的)赏金,该问题本身询问是否应该在此处抛出AssertionError,还是应该使用assert false语句。 - Maarten Bodewes
8个回答

42

这里有一个断言:"我断言这个构造函数永远不会被调用"。因此,AssertionError 在这里是正确的。


1
强烈不同意这里,详见下文。你需要能够找到异常开始的地方;你还应该让异常给你或代码的初学者一个很好的提示发生了什么。与断言无关的AssertionError都没有做到这一点。 - Charlie Martin
2
错误的,参见javadoc:http://java.sun.com/j2se/1.4.2/docs/api/java/lang/AssertionError.html - Charlie Martin
2
@Charlie的javadoc说:“抛出以指示断言失败。”您正在将断言视为“使用assert关键字”,而不是“程序员将其视为错误”。 - Craig P. Motlin
2
你说得对,这就是为什么它被称为“assert”语句。就像我不会使用NullPointerException来表示值为0而不是范围内的数字的名称,即使数字被用作索引也是如此。 - Charlie Martin
1
这里 CharlieMartin 提供的链接似乎无法访问。您可以尝试访问此链接 https://docs.oracle.com/javase/10/docs/api/java/lang/AssertionError.html。 - Dut A.
显示剩余3条评论

11

我喜欢引用Bloch的评论:

// Suppress default constructor for noninstantiability

或者更好的方法是将它放在错误信息中:

private UtilityClass()
{
    throw new AssertionError("Suppress default constructor for noninstantiability");
}

他在询问为什么AssertionError的类型不是其他东西? - Pacerier

5

UnsupportedOperationException听起来似乎是最合适的,但是一个检查异常会更好,因为它可以在编译时警告错误地实例化类的人。


3
呵呵,声明它为“可抛出的异常”,然后无论如何都抛出一个AssertionError。 :-) - C. K. Young
1
@ChrisJester-Young,不,如果你声明它“throws Throwable”,那么更好的实现方式是抛出另一个异常,该异常具有AssertionError的“cause”。 - Pacerier

2

当代码需要包含JUnit作为依赖项,例如在Maven测试范围内<scope>test</scope>中时,直接使用Assertion.fail()方法,可以显著提高代码的清晰度。

public final class UtilityClass {
    private UtilityClass() {
        fail("The UtilityClass methods should be accessed statically");
    }
}

当超出测试范围时,您可以使用以下内容,这需要静态导入才能像上面那样使用。import static pkg.Error.fail;

注:此处的“测试范围”可能指的是程序测试的某个特定阶段或环节。
public class Error {
    private static final Logger LOG = LoggerFactory.getLogger(Error.class);
    public static void fail(final String message) {
        LOG.error(message);
        throw new AssertionError(message);
        // or use your preferred exception 
        // e.g InstantiationException
    }
}

以下是使用方法。
public class UtilityClassTwo {
    private UtilityClassTwo() {
        Error.fail("The UtilityClass methods should be accessed statically");
    }
}

在最常用的形式中,它们都归结为以下内容:
public class UtilityClassThree {
    private UtilityClassThree() {
        assert false : "The UtilityClass methods should be accessed statically";
    }
}

UnsupportedOperationException是内置的异常之一,可以被抛出来表示“不支持所请求的操作”。

 private Constructor() {
    throw new UnsupportedOperationException(
            "Do not instantiate this class, use statically.");
}

2
“UnsupportedOperationException” 对我来说似乎是最具描述性的一个。 - Vic

2

我不确定我是否喜欢那个,因为它是IncompatibleClassChangeError的子类。但听起来不错。 :) - Rob Hruska
通常,编译器会捕获此错误;只有在类的定义不兼容更改时,此错误才可能在运行时发生。 - Maarten Bodewes

2
不不不,尊重Josh Bloch的所有权利,除非是来自断言,否则永远不要抛出AssertionError。如果你想在这里抛出AssertionError,请使用assert(false)进行抛出。然后阅读代码的人可以稍后找到它。
更好的方法是定义自己的异常,比如CantInstantiateUtilityClass。然后你会有这样的代码。
try {
    // some stuff
} catch (CantInstantiateUtilityClass e) {
    // react
}

所以读者能够知道捕手发生了什么。


让我注意一下,标准仍将AssertionError定义为断言失败的结果,并不是像一些初学者认为的应该抛出一个明确定义的信息性异常。可悲的是,在Java编程中,良好的异常处理技巧可能是最不受鼓励的技能。


7
强烈不同意。(顺便提一下,是Bloch而不是Block。)“assert false”的问题在于,如果你关闭了断言,那么该断言将无法被抛出。人们应该通过查找AssertionError和assert来查找断言。 - C. K. Young
6
某些(可能是随机的)catch (Exception e)块可以捕获异常,但无法捕获错误。为只能通过修改源代码或使用自定义ClassLoader才能发生的情况创建自定义异常是不好的建议。 - cletus
2
我不同意,如果构造函数被调用,程序应该放弃并死掉,因为这显然是一个错误。assert(false)也会导致程序放弃并死掉,并且还会出现AssertionError,所以我不确定为什么你更喜欢assert关键字。 - Craig P. Motlin
2
我相信当你的飞机控制系统放弃并死亡而不是尝试恢复时,你一定会感到非常激动。异常终止不是可靠性策略。 - Charlie Martin
3
关于你的更新:实际文本是“表示断言失败的抛出”。它没有明确说明我们在谈论谁的断言 - 至少我对我的代码有断言,你可能没有。但整个事情都是纠缠琐事 - 没有人会错误地解释这样一个带有良好原因(“无法实例化类X的实例”)和指向实际构造函数的堆栈跟踪的异常。 - Voo
显示剩余14条评论

0

断言失败意味着您已经违反了代码的合同规范。所以这里是正确的做法。

然而,我假设您将私下实例化一个实例,它也会调用构造函数并导致错误 - 除非您有另一个构造函数?


我的理解是,这个实用类只提供了一些静态方法,因此构造函数不会被调用。 - Matthew Murdoch
Matthew是正确的。从技术上讲,甚至不需要抛出任何东西,因为构造函数是私有的。抛出异常可以确保类本身不调用构造函数。 - Rob Hruska

0

您可以创建一个继承 Throwable 的自定义类,例如:

class NoninstantiabilityError extends Throwable

这样做有以下优点:

  • 名称指示了问题
  • 因为它直接扩展了Throwable,所以不太可能被意外捕获
  • 因为它直接扩展了Throwable,所以它是受检异常,意外调用相应的构造函数需要捕获异常

使用示例:

public final class UtilityClass {
    private UtilityClass() throws NoninstantiabilityError {
        throw new NoninstantiabilityError();
    }

    ...
}

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