【发布时间】:2015-08-28 11:57:03
【问题描述】:
我正在上一门基本的 Java 课程,但遇到了一个问题:只有在将有效参数传递给构造函数的情况下,如何创建对象?
我应该创建一个替代类并在实现验证后从那里调用构造函数吗?
或者我应该/可以在类中使用静态方法进行验证吗?
在这种情况下,最佳做法是什么?
【问题讨论】:
标签: java validation object constructor
我正在上一门基本的 Java 课程,但遇到了一个问题:只有在将有效参数传递给构造函数的情况下,如何创建对象?
我应该创建一个替代类并在实现验证后从那里调用构造函数吗?
或者我应该/可以在类中使用静态方法进行验证吗?
在这种情况下,最佳做法是什么?
【问题讨论】:
标签: java validation object constructor
标准做法是验证构造函数中的参数。例如:
class Range {
private final int low, high;
Range(int low, int high) {
if (low > high) throw new IllegalArgumentException("low can't be greater than high");
this.low = low;
this.high = high;
}
}
旁注:要验证参数不为空,这很常见,您可以使用:
import static java.util.Objects.requireNonNull;
Constructor(Object o) {
this.o = requireNonNull(o); //throws a NullPointerException if 'o' is null
}
更新
回复您关于社保号的具体评论。一种方法是在类中添加一个方法:
//constructor
public YourClass(String ssn) {
if (!isValidSSN(ssn)) throw new IllegalArgumentException("not a valid SSN: " + ssn);
this.ssn = ssn;
}
public static boolean isValidSSN(String ssn) {
//do some validation logic
}
调用代码可能如下所示:
String ssn = getSsnFromUser();
while(!YourClass.isValidSSN(ssn)) {
showErrorMessage("Not a valid ssn: " + ssn);
ssn = getSsnFromUser();
}
//at this point, the SSN is valid:
YourClass yc = new YourClass(ssn);
通过这种设计,您实现了两件事:
YourClass 被滥用,则会引发异常,它将帮助您检测错误您可以通过创建一个包含 SSN 并封装验证逻辑的 SSN 类来更进一步。然后,YourClass 将接受 SSN 对象作为参数,该参数始终是有效的 SSN。
【讨论】:
我只是在构造函数本身中抛出一个IllegalArgumentException:
public class MyClass {
private int i;
public MyClass (int i) {
// example validation:
if (i < 0) {
throw new IllegalArgumentException ("i mustn't be negatve!");
}
this.i = i;
}
【讨论】:
编程中一个众所周知的真理是“不要使用异常来控制流”。在调用构造函数而不是处理错误之前,您的代码应该了解这些限制并加以防范。预期情况存在例外情况,尤其是那些无法预测或无法防范的情况(例如,IO 流可能在写入期间变为无效,尽管在之前的检查中是正常的)。
虽然您可以在构造函数中抛出异常,但这并不总是理想的。如果您正在编写您希望被其他人使用/重用的公共对象,异常是公共构造函数的唯一真正选择,但是此类限制及其结果(例如将抛出什么异常)应在 javadoc 中明确记录类。
对于内部类,断言更合适。正如 Oracle 所说:“断言......应该用于检查不应该发生的情况,检查有关数据结构的假设,或对私有方法的参数实施约束。”—Using Assertions in Java Technology。您可能仍应记录您对该类的期望,但您的应用程序应事先在内部进行任何检查,而不是依赖于抛出的任何异常。
静态工厂方法可以提供一点帮助,它们的好处正在通过另一个问题进行详细说明:How to use “Static factory methods” instead of constructors。但是,它们不会提供强大的验证选项,而在事情无效时再次依赖异常(即返回 null,信息量较少)。
您的理想解决方案是Builder pattern。它不仅在管理参数方面提供了更大的灵活性,而且您可以单独验证每个参数,或者拥有一个可以一次评估所有字段的验证方法。构建器可以而且应该用于隐藏对象的实际构造器,享有对其的唯一访问权并防止任何不需要的值被提交,而断言可以防止“构建器永远不应该提交这些值”。
【讨论】:
构造函数可以抛出异常(参见Can constructors throw exceptions in Java?),因此如果传递了无效值,您可以让构造函数抛出异常。您还可以将构造函数设为私有并使用静态方法创建执行检查的对象。这可能更干净。
【讨论】:
确保将有效参数传递给构造函数的一种方法是使用仅接受所需参数的构造函数创建父类,然后创建最终用户使用的子类。如果你强迫你的用户调用 super() 并传入你需要的参数,那么他们至少必须传入正确的数据对象。至于这些参数的有效值,这取决于您是否要在父类构造函数中包含验证并抛出运行时异常或诸如此类。
这是一个超类/子类的例子。让我们称超类SomeShape 和子类Triangle。对于任何SomeShape 对象,您将强制“用户”提供边数和边长。就是这样……
public class SomeShape {
private int numSides;
private int sideLength;
public SomeShape(int mNumSides, int mSideLength) {
numSides = mNumSides;
sideLength = mSideLength;
}
}
public class Triangle extends SomeShape {
private int height;
public Triangle(int mNumSides, int mSideLength, int mHeight) {
super(mNumSides, mSideLength);
height = mHeight;
}
}
除了将一堆逻辑和异常硬编码到构造函数中之外,这是一种相对干净的方法来强制创建对象所需的参数。
【讨论】:
如果您不想从构造函数中抛出异常,您可以将构造函数设为私有并创建一个返回对象新实例的静态方法,如果参数无效,则返回 null。但是,此方法的调用者必须检查结果是否为空。
示例:
public class Foo {
private Foo(int arg1, Bar arg2) {
// guaranteed to be valid.
}
public static Foo construct(int arg1, Bar arg2) {
// perform validation
if (arg1 < 0 || arg2 == null) {
return null;
} else {
return new Foo(arg1, arg2);
}
}
}
用法
Foo object = Foo.construct(1, new Bar());
if (object == null) {
// handle error here.
}
【讨论】:
从构造函数中抛出异常是不好的做法。您最终会得到一个部分初始化的对象,这可能会破坏各种合同。
如果构造函数并非对所有输入组合都有效,则创建一个进行验证的工厂方法并将构造函数设为私有会更简洁。如果确实存在失败的可能性(也就是说,失败不是由于编程错误),那么返回Optional 可能是合适的。
【讨论】: