Java - 如何只创建具有有效属性的对象?
Posted
技术标签:
【中文标题】Java - 如何只创建具有有效属性的对象?【英文标题】:Java - How to only create an object with valid attributes? 【发布时间】:2015-08-28 11:57:03 【问题描述】:我正在上一门基本的 Java 课程,但遇到了一个问题:只有在将有效参数传递给构造函数的情况下,如何创建对象?
我应该创建一个替代类并在实现验证后从那里调用构造函数吗?
或者我应该/可以在类中使用静态方法进行验证吗?
在这种情况下,最佳做法是什么?
【问题讨论】:
【参考方案1】:标准做法是验证构造函数中的参数。例如:
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。
【讨论】:
你和其他人说的非法参数会破坏程序,对吗? @TiagoSirious “休息”是什么意思?它将立即退出构造函数并向调用者抛出异常。你想要这样是因为你收到了你不知道该怎么做的参数......所以这将帮助你识别调用这个构造函数的代码中的错误。 好的,但是假设用户传递了一个无效的社会安全号码。我希望它检查,但我不希望应用程序停止工作。我想警告用户此 SSN 无效,我希望它重新插入有效的 SSN。在这种情况下,如果我抛出异常,是只是构造函数退出还是整个应用程序退出? 为了避免空值我会使用@NotNull 注释 +1 更新。我教给年轻程序员的规则之一是“异常处理是针对异常事件”。正确处理 SSN 并要求更正,然后让构造函数 ALSO 检查并处理任何异常情况,这并不可耻。我唯一要做的其他事情是在性能关键代码中,其中检查可能会占用总运行时预算的很大一部分。在这些情况下,Danikov 对“不要使用异常进行流量控制”的回答成为正确的解决方案,我会使用流量控制来处理所有事情,并且没有异常。【参考方案2】:我只是在构造函数本身中抛出一个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;
【讨论】:
【参考方案3】:编程中一个众所周知的真理是“不要使用异常来控制流”。在调用构造函数而不是处理错误之前,您的代码应该了解这些限制并加以防范。预期情况存在例外情况,尤其是那些无法预测或无法防范的情况(例如,IO 流可能在写入期间变为无效,尽管在之前的检查中是正常的)。
虽然您可以在构造函数中抛出异常,但这并不总是理想的。如果您正在编写您希望被其他人使用/重用的公共对象,异常是公共构造函数的唯一真正选择,但是此类限制及其结果(例如将抛出什么异常)应在 javadoc 中明确记录类。
对于内部类,断言更合适。正如 Oracle 所说:“断言......应该用于检查不应该发生的情况,检查有关数据结构的假设,或对私有方法的参数实施约束。”—Using Assertions in Java Technology。您可能仍应记录您对该类的期望,但您的应用程序应事先在内部进行任何检查,而不是依赖于抛出的任何异常。
静态工厂方法可以提供一点帮助,它们的好处正在通过另一个问题进行详细说明:How to use “Static factory methods” instead of constructors。但是,它们不会提供强大的验证选项,而在事情无效时再次依赖异常(即返回 null,信息量较少)。
您的理想解决方案是Builder pattern。它不仅在管理参数方面提供了更大的灵活性,而且您可以单独验证每个参数,或者拥有一个可以一次评估所有字段的验证方法。构建器可以而且应该用于隐藏对象的实际构造器,享有对其的唯一访问权并防止任何不需要的值被提交,而断言可以防止“构建器永远不应该提交这些值”。
【讨论】:
我发现“不要对流控制使用异常”这句话毫无意义:由于 throw 语句是控制流语句(它将控制转移到其他代码块),这意味着用户代码绝不能抛出异常。 @meriton 我相信那里有一个隐含的“正常”,也就是说,“不要对正常流控制使用异常”。这意味着,如果 if 语句可以完成相同的工作,请改用它,正如我的回答中所限定的那样。 每个异常都可以用 if 语句替换(毕竟 C 程序员没有异常)。【参考方案4】:构造函数可以抛出异常(参见Can constructors throw exceptions in Java?),因此如果传递了无效值,您可以让构造函数抛出异常。您还可以将构造函数设为私有并使用静态方法创建执行检查的对象。这可能更干净。
【讨论】:
【参考方案5】:确保将有效参数传递给构造函数的一种方法是使用仅接受所需参数的构造函数创建父类,然后创建最终用户使用的子类。如果你强迫你的用户调用 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;
除了将一堆逻辑和异常硬编码到构造函数中之外,这是一种相对干净的方法来强制创建对象所需的参数。
【讨论】:
【参考方案6】:如果您不想从构造函数中抛出异常,您可以将构造函数设为私有并创建一个返回对象新实例的静态方法,如果参数无效,则返回 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.
【讨论】:
谁能检查这是否是个好主意?它对我来说看起来非常简单和酷。【参考方案7】:从构造函数中抛出异常是不好的做法。您最终会得到一个部分初始化的对象,这可能会破坏各种合同。
如果构造函数并非对所有输入组合都有效,则创建一个进行验证的工厂方法并将构造函数设为私有会更简洁。如果确实存在失败的可能性(也就是说,失败不是由于编程错误),那么返回Optional
可能是合适的。
【讨论】:
如果没有构造函数泄漏(当你分配一个静态变量或将 this 实例传递给另一个线程 - 这无论如何都是不好的做法),那么对象将被垃圾收集没有后果。 如果构造函数抛出异常,新对象不会返回给调用者,并且不会造成任何伤害(当然,除非构造函数在构造函数之前将正在构造的对象的引用泄漏给其他代码)完全初始化,无论如何这是一个不好的做法)。以上是关于Java - 如何只创建具有有效属性的对象?的主要内容,如果未能解决你的问题,请参考以下文章
为啥我使用有效的 withLatestFrom 操作从选择器获取具有动作观察者属性的对象?