我已经阅读了一些关于在访问者中抛出异常的专家和骗子的答案,但我想我会用一个例子来讨论我的具体问题:
public class App {
static class Test {
private List<String> strings;
public Test() {
}
public List<String> getStrings() throws Exception {
if (this.strings == null)
throw new Exception();
return strings;
}
public void setStrings(List<String> strings) {
this.strings = strings;
}
}
public static void main(String[] args) {
Test t = new Test();
List<String> s = null;
try {
s = t.getStrings();
} catch (Exception e) {
// TODO: do something more specific
}
}
}
getStrings()
在Exception
未设置时抛出strings
。这种情况会通过方法更好地处理吗?
答案 0 :(得分:6)
是的,那很糟糕。我建议在声明中初始化strings变量,如:
private List<String> strings = new ArrayList<String>();
你可以用
之类的东西替换setterpublic void addToList(String s) {
strings.add(s);
}
因此不可能有NPE。
(或者不要在声明中初始化strings变量,将初始化添加到addToList方法,并使用它来检查getStrings()以查看它是否返回null。)
至于为什么它很糟糕,用这样一个人为的例子很难说,但似乎有太多的状态改变,你通过让用户处理异常来惩罚这个类的用户。还有一个问题是,一旦程序中的方法开始抛出异常,那么它往往会转移到整个代码库中。
检查此实例成员是否已填充需要在某个地方完成,但我认为应该在某个地方完成,这个知识比正在这个类中有更多的知识。
答案 1 :(得分:6)
这实际上取决于您获得的列表正在做什么。我认为更优雅的解决方案是返回Colletions.EMPTY_LIST
,或者@Nathan建议,返回空ArrayList
。然后,使用该列表的代码可以检查它是否为空,如果它想要,或者只是像使用任何其他列表一样使用它。
区别在于没有字符串和之间没有字符串列表。第一个应该由空列表表示,第二个应该返回null
或抛出异常。
答案 2 :(得分:5)
正确处理此问题的方法,假设要求在调用getter之前将其设置在其他位置,如下所示:
public List<String> getStrings() {
if (strings == null)
throw new IllegalStateException("strings has not been initialized");
return strings;
}
作为未经检查的异常,不需要在方法上声明它,因此您不会使用大量的try / catch噪声污染客户端代码。此外,因为这个条件是可以避免的(即客户端代码可以确保列表已经初始化),所以它是编程错误,因此未经检查的异常是可接受的,也是可取的。
答案 3 :(得分:1)
一般来说,这是一种设计气味。
如果字符串未初始化确实是一个错误,那么要么删除setter,要么在构造函数中强制执行约束,要么像Bohemian所说的那样做,并抛出带有解释性消息的IllegalArgumentException。如果你做前者,你会得到快速失败的行为。
如果不是错误,则字符串为空,则考虑将列表初始化为空列表,并在setter中将其强制为非null,与上述类似。这样做的好处是您不需要在任何客户端代码中检查null。
for (String s: t.getStrings()) {
// no need to check for null
}
这可以大大简化客户端代码。
编辑:好的,我刚看到你正在从JSON序列化,所以你无法删除构造函数。所以你需要:答案 4 :(得分:1)
我认为你已经揭露了原始问题背后的一个更重要的问题:你应该努力防止吸气剂和制定者的特殊情况吗?
答案是肯定的,你应该努力避免琐碎的例外。
你在这里有效的lazy instantiation在这个例子中根本没有动机:
在计算机编程中,延迟初始化是延迟的策略 创建对象,计算值或其他值 昂贵的过程,直到第一次需要它。
此示例中有两个问题:
您的示例不是线程安全的。检查null可以同时在两个线程上成功(即,发现对象为空)。然后,您的实例化会创建两个不同的字符串列表。随后将出现非确定性行为。
此示例中没有充分理由推迟实例化。这不是一项昂贵或复杂的操作。这就是我所说的“努力避免琐碎的异常”:值得投资周期来创建一个有用的(但是空的)列表,以确保你不会抛出延迟爆炸空指针异常。
请记住,当您在外部代码上造成异常时,您基本上希望开发人员知道如何使用它做一些合理的事情。您还希望希望在等式中没有第三个开发人员将其包装在异常食物中,只是为了捕获并忽略像您这样的异常:
try {
// I don't understand why this throws an exception. Ignore.
t.getStrings();
} catch (Exception e) {
// Ignore and hope things are fine.
}
在下面的示例中,我使用Null Object pattern向未来的代码表明您的示例尚未设置。但是,Null对象作为非特殊代码运行,因此不会对未来开发人员的工作流程产生开销和影响。
public class App {
static class Test {
// Empty list
public static final List<String> NULL_OBJECT = new ArrayList<String>();
private List<String> strings = NULL_OBJECT;
public synchronized List<String> getStrings() {
return strings;
}
public synchronized void setStrings(List<String> strings) {
this.strings = strings;
}
}
public static void main(String[] args) {
Test t = new Test();
List<String> s = t.getStrings();
if (s == Test.NULL_OBJECT) {
// Do whatever is appropriate without worrying about exception
// handling.
// A possible example below:
s = new ArrayList<String>();
t.setStrings(s);
}
// At this point, s is a useful reference and
// t is in a consistent state.
}
}
答案 5 :(得分:0)
我会就此咨询Java Beans规范。可能最好不要抛出java.lang.Exception,而是使用java.beans.PropertyVetoException并让你的bean注册为java.beans.VetoableChangeListener并触发相应的事件。这有点过分,但会正确识别你想要做的事情。
答案 6 :(得分:0)
这实际上取决于你想要做什么。如果您试图强制执行在调用getter之前调用该设置的条件,则可能会出现抛出IllegalStateException的情况。
答案 7 :(得分:-1)
我建议从setter方法中抛出异常。有什么特别的理由为什么不这样做会更好?
public List<String> getStrings() throws Exception {
return strings;
}
public void setStrings(List<String> strings) {
if (strings == null)
throw new Exception();
this.strings = strings;
}
此外,根据特定的上下文,是不是可以直接通过构造函数传递List<String> strings
?这样你甚至可能不需要setter方法。