为什么 Java 中的断言充当 "missing return statement" 而 throw 语句却没有?

Why do assertions in Java act as a "missing return statement" whereas throw statements do not?

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

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

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

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 + "\"");
}

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

因为两件事:

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

其次,您在最后抛出异常,而在代码之前 运行 断言。从理论上讲,断言可以在没有循环返回的情况下为真,因为 Map 内容理论上可以在断言和循环之间改变,或者 containsValue 可以做一些编译器不期望的事情。编译器只检查语法,不检查逻辑上的不可能性。

有两个原因:

  1. 编译器 'smart' 不足以弄清楚 this.charMap.containsValue(s) 为真意味着 this.charMap.get(c).equals(s) 必须对某些 c 为真。它只做更简单的分析,比如检查 if 语句的两个分支是否都有 return。
  2. 即使它足够聪明,Java 也是一种具有可变对象和线程的语言 - 即使映射包含 assert 时的键,它也可能从在循环开始之前由另一个线程映射。

如果您想要一种具有足够 'smart' 编译器的语言,您可能需要查看像 Idris 这样的依赖类型语言。

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

If a method is declared to have a return type (§8.4.5), then a compile-time error occurs if the body of the method can complete normally (§14.1).

In other words, a method with a return type must return only by using a return statement that provides a value return; the method is not allowed to "drop off the end of its body". See §14.17 for the precise rules about return statements in a method body.

由于您的方法必须 return 一个值(而不是 void),因此只有 returnthrow return 执行流向调用者。

此规则可确保您避免这种情况(这是一个 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 中 - 在这种情况下 returned 的值是 undefined 这可能会导致几个讨厌的问题,尤其是指针。

Java 中的规则确保您始终:

  1. Return一个值,例如:null.
  2. 抛出异常,表示错误。

这并不意味着 returned 值会使事情起作用:returning 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()