从访问器抛出异常的错误设计决策?

发布于 2024-12-03 03:49:31 字数 812 浏览 1 评论 0原文

我已经阅读了一些关于在访问器中抛出异常的优点和缺点的答案,但我想我会用一个例子来提出我的具体问题:

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() is throwing an Exception 当 strings 尚未设置时。这种情况是否有更好的方法来处理?

I have read some answers re the pro's and con's of throwing an exception within an accessor, but I thought I would broach my specific question with an example:

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() is throwing an Exception when strings has not been set. Would this situation be better handled by a method?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

感悟人生的甜 2024-12-10 03:49:31

是的,那很糟糕。我建议在声明中初始化字符串变量,例如:

private List<String> strings = new ArrayList<String>();

并且您可以将 setter 替换为类似的内容

public void addToList(String s) {
     strings.add(s);
}

,因此不可能出现 NPE。

(或者不要在声明中初始化 strings 变量,将初始化添加到 addToList 方法中,并让使用它的代码检查 getStrings() 以查看它是否返回 null。)

至于为什么它不好,很难说使用这样一个人为的示例,但似乎有太多的状态更改,并且您通过让用户处理异常来惩罚此类的用户。还有一个问题是,一旦程序中的方法开始抛出异常,那么它往往会转移到整个代码库。

检查此实例成员是否已填充需要在某个地方完成,但我认为应该在比此类更了解所发生情况的地方完成。

Yes, that's bad. I suggest initializing the strings variable in the declaration, like:

private List<String> strings = new ArrayList<String>();

and you could replace the setter with something like

public void addToList(String s) {
     strings.add(s);
}

So there's no possibility of a NPE.

(Alternatively don't initialize the strings variable in the declaration, add the initialization to the addToList method, and have the code using it check getStrings() to see if it gets null back.)

As to why it's bad, it's hard to say with such a contrived example, but it seems like there's too much state-changing and you're penalizing the user of this class for it by making the user handle the exception. Also there's the issue that once methods in your program start throwing Exception then that tends to metastasize all over your codebase.

Checking that this instance member gets populated needs to be done somewhere, but I think it should be done somewhere that has more knowledge of what is going on than in this class.

深海少女心 2024-12-10 03:49:31

这实际上取决于您对所获得的列表的处理方式。我认为更优雅的解决方案是返回 Collections.EMPTY_LIST或者,正如@Nathan建议的,一个空的ArrayList。然后,使用该列表的代码可以根据需要检查它是否为空,或者像处理任何其他列表一样处理它。

区别在于没有字符串没有字符串列表之间的区别。第一个应由空列表表示,第二个应返回 null 或引发异常。

It really depends on what are are doing with the list you get. I think a more elegant solution would be to return a Colletions.EMPTY_LISTor, as @Nathan suggests, an empty ArrayList. Then the code that uses the list can check if it is empty if it wants to, or just work with it like with any other list.

The difference is the distinction between there are no strings and there is no list of strings. The first should be represented by an empty list, the second by either returning null or throwing an exception.

长发绾君心 2024-12-10 03:49:31

处理这个问题的正确方法,假设要求在调用 getter 之前在其他地方设置列表,如下所示:

public List<String> getStrings() {
    if (strings == null)
        throw new IllegalStateException("strings has not been initialized");

    return strings;
}

作为一个未经检查的异常,它不是要求您在方法上声明它,这样您就不会因为大量的 try/catch 噪音而污染客户端代码。另外,因为这种情况是可以避免的(即客户端代码可以确保列表已初始化),所以这是一个编程错误,因此未经检查的异常是可接受的并且更可取。

The correct way to handle this, assuming it is a requirement that the list be set elsewhere before calling the getter, is as follows:

public List<String> getStrings() {
    if (strings == null)
        throw new IllegalStateException("strings has not been initialized");

    return strings;
}

Being an unchecked exception, it isn't required that you declare it on the method, so you don't pollute the client code with lots of try/catch noise. Also, because this condition is avoidable (ie the client code can ensure the list has been initialized), it is a programming error, and therefore an unchecked exception is acceptable and preferable.

第七度阳光i 2024-12-10 03:49:31

总的来说,这是一种设计的味道。

如果字符串未初始化确实是一个错误,那么要么删除 setter,并在构造函数中强制执行约束,要么按照 Bohemian 所说的操作并抛出带有解释性消息的 IllegalArgumentException。如果你选择前者,你就会出现快速失败的行为。

如果 strings is null 不是错误,则考虑将列表初始化为空列表,并在 setter 中强制其为 not-null,与上面类似。这样做的优点是您不需要在任何客户端代码中检查 null 。

for (String s: t.getStrings()) {
  // no need to check for null
}

这样可以大大简化客户端代码。

编辑:好的,我刚刚看到您正在从 JSON 进行序列化,因此您无法删除构造函数。因此,您需要:

  1. 如果字符串为 null,则从 getter 抛出 IllegalArgumentException
  2. ,返回 Collections.EMPTY_LIST。
  3. 或更好:在反序列化之后添加验证检查,专门检查字符串的值,并抛出一个合理的异常。

In general, this is a design smell.

If it really is an error that strings is not initialized, then either remove the setter, and enforce the constraint in a constructor, or do as Bohemian says and throw an IllegalArgumentException with an explanatory message. If you do the former, you get fail-fast behaviour.

If it is not an error is strings is null, then consider initialising the list to an empty list, and enforce it to be not-null in the setter, similarly to the above. This has the advantage that you don't need to check for null in any client code.

for (String s: t.getStrings()) {
  // no need to check for null
}

This can simplify client code a lot.

EDIT: Ok, I've just seen that the you are serializing from JSON, so you can't remove the constructor. So you'll need to either:

  1. throw an IllegalArgumentException from the getter
  2. if strings is null, return a Collections.EMPTY_LIST.
  3. or better: add a validation check just after the deserialization where you specifically check the value of strings, and throw a sensible exception.
千笙结 2024-12-10 03:49:31

我认为您在最初的问题背后暴露了一个更重要的问题:您是否应该努力防止 getter 和 setter 中出现异常情况?

答案是肯定的,您应该努力避免微不足道的异常。

这里实际上是延迟实例化,在此示例中根本没有动机:

在计算机编程中,延迟初始化是一种延迟策略
对象的创建、值的计算或其他一些
直到第一次需要时,过程都是昂贵的。

此示例中存在两个问题:

  1. 您的示例不是线程安全的。对 null 的检查可以同时在两个线程上成功(即发现对象为 null)。然后,您的实例化将创建两个不同的字符串列表。将会发生不确定的行为。

  2. 在此示例中没有充分的理由推迟实例化。这不是一个昂贵或复杂的操作。 就是我所说的“努力避免微不足道的异常”的意思:值得投入周期来创建一个有用的(但空的)列表,以确保您不会抛出延迟爆炸空指针异常。

请记住,当您对外部代码施加异常时,您基本上希望开发人员知道如何用它做一些明智的事情。您还希望等式中没有第三个开发人员将所有内容包装在异常吞噬者中,只是为了捕获并忽略像您这样的异常:

try {
    // I don't understand why this throws an exception.  Ignore.
    t.getStrings();
} catch (Exception e) {
    // Ignore and hope things are fine.
}

在下面的示例中,我使用 < a href="http://en.wikipedia.org/wiki/Null_object_pattern" rel="nofollow">空对象模式 向未来的代码表明您的示例尚未设置。但是,空对象作为非异常代码运行,因此不会产生开销,也不会对未来开发人员的工作流程产生影响。

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.
    }
}

I think you've exposed a more important question behind the original one: should you work to prevent exceptional cases in getters and setters?

The answer is yes, you should work to avoid trivial exceptions.

What you have here is effectively lazy instantiation that is not at all motivated in this example:

In computer programming, lazy initialization is the tactic of delaying
the creation of an object, the calculation of a value, or some other
expensive process until the first time it is needed.

You have two problems in this example:

  1. Your example is not thread safe. That check for null can succeed (i.e., find that the object is null) on two threads at the same time. Your instantiation then creates two different lists of strings. Non-deterministic behavior will ensue.

  2. There is no good reason in this example to defer the instantiation. It's not an expensive or complicated operation. This is what I mean by "work to avoid trivial exceptions": it is worth investing the cycles to create a useful (but empty) list to ensure that you aren't throwing delayed detonation null pointer exceptions around.

Remember, when you inflict an exception on outside code, you're basically hoping that developers know how to do something sensible with it. You're also hoping that there isn't a third developer in the equation whose wrapped everything in an exception eater, just to catch and ignore exceptions like yours:

try {
    // I don't understand why this throws an exception.  Ignore.
    t.getStrings();
} catch (Exception e) {
    // Ignore and hope things are fine.
}

In the example below, I am using the Null Object pattern to indicate to future code that your example hasn't been set. However, the Null Object runs as non-exceptional code and, therefore, doesn't have the overhead and impact on the workflow of future developers.

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.
    }
}
小草泠泠 2024-12-10 03:49:31

我会就此咨询 Java Beans 规范。可能最好不要抛出 java.lang.Exception ,而是使用 java.beans.PropertyVetoException 并使您的 bean 将自身注册为 java.beans.VetoableChangeListener 并触发适当的事件。这有点矫枉过正,但可以正确识别您想要做什么。

I would consult the Java Beans specification on this. Probably would be better not to throw a java.lang.Exception but instead use the java.beans.PropertyVetoException and have your bean register itself as a java.beans.VetoableChangeListener and fire the appropriate event. It is a tad overkill but would correctly identify what you are trying to do.

献世佛 2024-12-10 03:49:31

这实际上取决于您想要做什么。如果您尝试强制执行在调用 getter 之前已调用设置的条件,则可能会抛出 IllegalStateException。

It really depends on what you are trying to do. If you have attempting to enforce the condition that the setting has been called prior to the getter being called, there might be a case for throwing an IllegalStateException.

念﹏祤嫣 2024-12-10 03:49:31

我建议从 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? strings 直接由构造函数调用?这样也许你甚至不需要 setter 方法。

I'd suggest to throw the exception from the setter method. Is there any special reason why wouldn't it better to do so?

public List<String> getStrings() throws Exception {
    return strings;
}

public void setStrings(List<String> strings) {
    if (strings == null)
        throw new Exception();

    this.strings = strings;
}

Also, depending on the particular context, wouldn't it be possible to simply pass the List<String> strings directly by the constructor? That way maybe you wouldn't even need a setter method.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文