【问题标题】:Why do assertions in Java act as a "missing return statement" whereas throw statements do not?为什么 Java 中的断言充当“缺少的返回语句”而 throw 语句却没有?
【发布时间】:2021-08-16 04:34:44
【问题描述】:

我有一个具有以下字段和方法的类:

private final Map<Character, String> charMap = new LinkedHashMap<>();

public Character charOf(String s) {
    assert this.charMap.containsValue(s);
    for (Character c : this.charMap.keySet()) {
        if (this.charMap.get(c).equals(s)) return c;
    }
}

编译器不喜欢这样,给我一个“缺少返回语句”错误,而这编译得很好:

private final Map<Character, String> charMap = new LinkedHashMap<>();

public Character charOf(String s) {
    for (Character c : this.charMap.keySet()) {
        if (this.charMap.get(c).equals(s)) return c;
    }
    throw new IllegalArgumentException("There is no mapping for \"" + s + "\"");
}

据我所知,这两种方法的功能应该完全相同,并且做的事情完全相同,但前者的可读性略高(但错误消息的详细程度较低)。它总是要么返回一个值,要么抛出一个异常。为什么编译器没有意识到这一点?

【问题讨论】:

  • 无条件抛出将结束该方法的流程。这种方法再也无法继续下去了。然而,断言是一个可选功能,可以通过 JVM 标志启用和禁用。所以当然可以继续流程,因此会遇到方法结束而不返回的情况,这是不允许的。
  • 另外,编译器不够聪明,无法将断言背后的实际逻辑与代码逻辑联系起来。这是静态代码分析器可以做的事情,但编译器不能。
  • 附带说明,...使用 Map.Entry 而不是 keySet() + get()。

标签: java return assertion


【解决方案1】:

有两个原因:

  1. 编译器不够“聪明”,无法确定this.charMap.containsValue(s) 为真意味着this.charMap.get(c).equals(s) 对于某些c 必须为真。它只进行更简单的分析,例如检查 if 语句的两个分支是否都有返回值。
  2. 即使它足够聪明,Java 也是一种具有可变对象和线程的语言——即使映射包含 assert 时的键,它也可能在循环开始之前被另一个线程从映射中删除.

如果您想要一种具有足够“智能”编译器的语言,您可能需要查看 Idris 等依赖类型语言。

【讨论】:

    【解决方案2】:

    因为两件事:

    首先,断言只有在运行时使用 jvm 参数 -ea 启用时才会引发异常。这意味着有可能被跳过。

    其次,当您在代码之前运行断言时,您会在最后抛出异常。从理论上讲,如果没有循环返回,断言可能是真的,因为理论上Map 的内容可能会在断言和循环之间发生变化,或者containsValue 可能会做一些编译器没有预料到的事情。编译器只检查语法,而不是逻辑上的不可能性。

    【讨论】:

      【解决方案3】:

      这与 JLS 的关系更大:https://docs.oracle.com/javase/specs/jls/se10/html/jls-8.html#jls-8.4.7 而不是assert

      如果方法被声明为具有返回类型(第 8.4.5 节),则 如果方法的主体可以完成,则会发生编译时错误 通常(第 14.1 节)。

      换句话说,具有返回类型的方法必须仅通过使用 提供返回值的 return 语句;方法不是 允许“从身体的末端掉下来”。详见§14.17 关于方法体中的返回语句的规则。

      由于您的方法必须返回一个值(不是void),因此只有returnthrow 将执行流程返回给调用者。

      此规则确保您避免这种情况(这是一个 C 示例):

      #define N = 10;
      const char* CHAR_MAP[N] = ...;
      const char  CHAR_VALUE[N] = ...;
      
      char charOf(const char* s) {
        for (int i = 0; i < N; ++i) {
          if (strcmp(s, CHAR_MAP[i]) == 0) {
            return CHAR_VALUE[i];
          }
        }
        // not return value
      }
      

      我的 C 有点生疏,这可能无法编译,但重点是:在 C 中 - 至少在 C99 中 - 在这种情况下返回的值是 undefined 这可能会导致一些令人讨厌的问题,尤其是指针。

      Java 中的规则确保您始终:

      1. 返回一个值,例如:null
      2. 抛出异常,意味着错误。

      这并不意味着返回的值会起作用:在这里返回null 可能会产生NullPointerException

      另外,附注:

      public Character charOf(String s) {
          for (Map.Entry<Character,String> entry : this.charMap.entrySet()) {
              if (s.equals(entry.getValue()) {
                return entry.getKey();
              }
          }
          throw new IllegalArgumentException("There is no mapping for \"" + s + "\"");
      }
      

      当你可以使用entrySet()时,你应该避免混淆keySet()get()

      【讨论】:

      • 谢谢!使用entrySet() 代替keySet()get() 有什么好处?
      • 因为keySet() 和get() 之间可能存在差异(例如:key 可能会消失,...)。此外,如您所见,这使代码更清晰。仅当您知道不会同时使用两者时,才应使用 keySet() 或 values()。
      猜你喜欢
      • 2017-01-26
      • 1970-01-01
      • 2014-03-01
      • 2022-01-07
      • 2023-02-21
      • 1970-01-01
      • 2016-06-10
      • 2014-09-22
      • 1970-01-01
      相关资源
      最近更新 更多