【问题标题】:How do I avoid a useless return in a Java method?如何避免 Java 方法中的无用返回?
【发布时间】:2017-06-30 06:33:45
【问题描述】:

我有一种情况,理论上,嵌套在两个 for 循环中的 return 语句总是会到达。

编译器不同意并要求在for 循环之外使用return 语句。我想知道一种优化此方法的优雅方法,这超出了我目前的理解范围,而且我尝试的 break 实现似乎都不起作用。

Attached 是一种来自赋值的方法,它生成随机整数并返回循环的迭代,直到找到第二个随机整数,在作为 int 参数传递给方法的范围内生成。

private static int oneRun(int range) {
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    for (int count = 1; count <= range; count++) { // Run until return.
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.
        for (int i = 0; i < count; i++) { // Check for past occurence and return if found.
            if (rInt[i] == rInt[count]) {
                return count;
            }
        }
    }
    return 0; // Never reached
}

【问题讨论】:

  • 如果最后一项没有到达,那么您可以使用while(true) 而不是索引循环。这告诉编译器循环永远不会返回。
  • 调用范围为 0(或任何其他小于 1 的数字)的函数 (oneRun(0)),您会看到您很快就到达了无法访问的 return
  • 当提供的范围为负数时达到返回值。您还对输入范围进行了 0 验证,因此您目前没有以任何其他方式捕获它。
  • @HopefullyHelpful 当然,这才是真正的答案。根本不是无用的回报!
  • @HopefullyHelpful 不,因为nextInt 会为range &lt; 0 抛出异常。唯一达到返回的情况是range == 0

标签: java


【解决方案1】:

编译器的启发式永远不会让您省略最后一个return。如果您确定永远无法联系到它,我会将其替换为 throw 以说明情况。

private static int oneRun(int range) {
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    for (int count = 1; count <= range; count++) {
        ...
    }

    throw new AssertionError("unreachable code reached");
}

【讨论】:

  • 不仅可读性强,而且可以确保您发现您的代码是否有问题。生成器可能有错误是完全合理的。
  • 如果您绝对确定您的代码永远无法到达那个“无法访问的代码”,那么为所有可能出现的边缘情况创建一些单元测试(范围为 0,范围为 -1,范围为最小/最大整数)。它现在可能是一种私有方法,但下一个开发人员可能不会保持这种方式。如何处理意外值(抛出异常、返回错误值、什么都不做)取决于您将如何使用该方法。根据我的经验,您通常只想记录错误并返回。
  • 也许应该是throw new AssertionError("\"unreachable\" code reached");
  • 我会写出我在生产程序中的答案。
  • 对于给定的示例,有一种更好的处理方法,而不是简单地添加一个永远无法到达的throw。人们常常只想快速应用“一种解决方案来统治所有人”,而没有过多考虑潜在的问题。但编程不仅仅是编码。尤其是在算法方面。如果您(仔细)检查给定的问题,您会意识到您可以将外部 for 循环的最后一次迭代分解出来,从而将“无用”返回替换为有用的返回(参见 this answer)。
【解决方案2】:

使用临时变量,例如 "result" ,并删除内部返回。 将 for 循环更改为具有适当条件的 while 循环。 对我来说,只有一个 return 作为函数的最后一个语句总是更优雅。

【讨论】:

  • 好吧,看起来很内部的 if 可能是外部的 while 条件。请记住,总是有一段时间等效于 for (并且更优雅,因为您不打算总是经历所有迭代)。这两个嵌套的 for 循环肯定可以简化。所有这些代码都闻起来“太复杂了”。
  • @Solomonoff'sSecret 你的说法很荒谬。这意味着我们应该“总体上”针对两个或多个返回语句。有趣。
  • @Solomonoff'sSecret 这是首选的编程风格的问题,不管这周有什么酷的,上周是蹩脚的,只有一个退出点有明确的优点,并且在结束时方法(想想一个循环,其中散布着许多return result,然后想现在想在返回之前对找到的result 进行一次检查,这只是一个例子)。也可能有缺点,但像“一般来说,没有充分的理由只有一次回报”这样笼统的说法是站不住脚的。
  • @David 让我换个说法:没有一般的理由只返回一次。在特定情况下,这可能是原因,但通常情况下它是最糟糕的货物崇拜。
  • 问题在于是什么让代码更具可读性。如果在你的脑海中你说“如果是这个,返回我们找到的东西;否则继续;如果我们没有找到东西,返回这个其他值”。那么你的代码应该有两个返回值。始终编​​写使下一个开发人员更容易理解的代码。编译器从 Java 方法中只有一个返回点,即使在我们看来它看起来像两个。
【解决方案3】:

既然你问过要打破两个for 循环,你可以使用标签来做到这一点(见下面的例子):

private static int oneRun(int range) {
    int returnValue=-1;

    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    OUTER: for (int count = 1; count <= range; count++) { // Run until return.
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.   
        for (int i = 0; i < count; i++) { // Check for past occurence and return if found.
            if (rInt[i] == rInt[count]) {
                returnValue = count;
                break OUTER;
            }
        }
    }
    return returnValue;
}

【讨论】:

  • 这不是因为returnValue可以在未初始化的情况下使用而无法编译吗?
  • 很有可能。这篇文章的目的是展示如何打破这两个循环,正如最初的发帖人所问的那样。 OP 确信执行永远不会到达方法的末尾,因此可以通过在声明时将 returnValue 初始化为任何值来修复您提出的问题。
  • 标签不是那种你最好避免的事情吗?
  • 我相信你在考虑 goto。
  • 高级(-ish)编程语言的标签。 绝对野蛮……
【解决方案4】:

作为@BoristheSpider pointed out,您可以确保第二个return 语句在语义上不可达:

private static int oneRun(int range) {
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    int count = 0;

    while (true) {
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.
        for (int i = 0; i < count; i++) { // Check for past occurence and return if found.
            if (rInt[i] == rInt[count]) {
                return count;
            }
        }
        count++;
    }
}

编译并运行良好。如果您收到ArrayIndexOutOfBoundsException,您就会知道该实现在语义上是错误的,而无需显式抛出任何东西。

【讨论】:

  • 确切地说:该条件实际上并未用于控制循环,因此它不应该存在。但是,我会把它写成for(int count = 0; true; count++)
  • @cmaster: 你可以省略true: for(int count = 0; ; count++) …
  • @Holger 啊。我对此不确定,因为这是关于 java 的,而且由于我更喜欢​​ C/C++,所以多年来我没有接触过这种语言。所以,当我看到while(true)的使用时,我想也许java在这方面要严格一些。很高兴知道没有必要担心...实际上,我更喜欢省略的true 版本:它在视觉上清楚地表明绝对没有条件可以查看:-)
  • 我不想切换到“for”。 “while-true”是一个明显的标志,表明该块应该无条件地循环,除非并且直到内部有东西破坏它。带有省略条件的“for”不能清楚地传达;它可能更容易被忽视。
  • @l0b0 考虑到警告与代码质量有关,整个问题可能更适合代码审查。
【解决方案5】:

由于您的返回值基于外部循环的变量,您可以简单地将外部循环的条件更改为count &lt; range,然后在函数末尾返回最后一个值(您刚刚省略):

private static int oneRun(int range) {
    ...

    for (int count = 1; count < range; count++) {
        ...
    }
    return range;
}

这样你就不需要引入永远无法到达的代码。

【讨论】:

  • 但这不是需要在找到匹配项时打破两层循环吗?我没有看到减少到一个 for 语句的方法,需要生成一个新的随机数并将其与之前的随机数进行比较,然后再生成另一个。
  • 你仍然有两个嵌套的 for 循环(我只是省略了第二个 ...),但是外部循环已经被它的最后一次迭代减少了。与最后一次迭代相对应的情况由最后一个 return 语句单独处理。尽管对于您的示例来说这是一个优雅的解决方案,但在某些情况下,这种方法会变得更难阅读;例如,如果返回值取决于内部循环,那么 - 而不是最后的单个 return 语句 - 你将留下一个额外的循环(代表外部循环最后一次迭代的内部循环)。
【解决方案6】:
private static int oneRun(int range) {
    int result = -1; // use this to store your result
    int[] rInt = new int[range+1]; // Stores the past sequence of ints.
    rInt[0] = generator.nextInt(range); // Inital random number.

    for (int count = 1; count <= range && result == -1; count++) { // Run until result found.
        rInt[count] = generator.nextInt(range); // Add randint to current iteration.   
        for (int i = 0; i < count && result == -1; i++) { // Check for past occurence and leave after result found.
            if (rInt[i] == rInt[count]) {
                result = count;
            }
        }
    }
    return result; // return your result
}

【讨论】:

  • 代码也效率低下,因为一个人会做很多result == -1检查,可以用循环内的return省略...
【解决方案7】:

也许这表明您应该重写代码。例如:

  1. 创建一个整数数组 0 .. range-1。将所有值设置为 0。
  2. 执行循环。在循环中,生成一个随机数。在您的列表中查看该索引处的值是否为 1 如果是,则跳出循环。否则,将该索引处的值设置为 1
  3. 计算列表中 1 的数量,并返回该值。

【讨论】:

    【解决方案8】:

    虽然断言是一个很好的快速解决方案。一般来说,这种问题意味着你的代码太复杂了。当我查看您的代码时,很明显您并不真正想要一个数组来保存以前的数字。你想要一个Set

    Set<Integer> previous = new HashSet<Integer>();
    
    int randomInt = generator.nextInt(range);
    previous.add(randomInt);
    
    for (int count = 1; count <= range; count++) {
        randomInt = generator.nextInt(range);
        if (previous.contains(randomInt)) {
           break;
        }
    
        previous.add(randomInt);
    }
    
    return previous.size();
    

    现在请注意,我们返回的实际上是集合的大小。代码复杂度从二次方降低到线性度,可读性立即提高。

    现在我们可以意识到我们甚至不需要那个 count 索引:

    Set<Integer> previous = new HashSet<Integer>();
    
    int randomInt = generator.nextInt(range);
    
    while (!previous.contains(randomInt)) {          
        previous.add(randomInt);      
        randomInt = generator.nextInt(range);
    }
    
    return previous.size();
    

    【讨论】:

      【解决方案9】:

      具有 return 语句并在其中有一个循环的方法总是需要在循环之外有一个 return 语句。即使永远不会到达循环外的这个语句。在这种情况下,为了避免不必要的返回语句,您可以在方法的开头定义相应类型的变量,在您的情况下为整数,即在相应循环之前和之外。当达到循环内所需的结果时,您可以将相应的值赋予此预定义变量,并将其用于循环外的 return 语句。

      由于您希望您的方法在 rInt[i] 等于 rInt[count] 时返回第一个结果,因此仅实现上述变量是不够的,因为该方法将在 rInt[i] 等于 rInt[ 时返回最后一个结果数数]。一种选择是实现两个“中断语句”,当我们获得所需结果时调用它们。因此,该方法将如下所示:

      private static int oneRun(int range) {
      
              int finalResult = 0; // the above-mentioned variable
              int[] rInt = new int[range + 1];
              rInt[0] = generator.nextInt(range);
      
              for (int count = 1; count <= range; count++) {
                  rInt[count] = generator.nextInt(range);
                  for (int i = 0; i < count; i++) {
                      if (rInt[i] == rInt[count]) {
                          finalResult = count;
                          break; // this breaks the inside loop
                      }
                  }
                  if (finalResult == count) {
                      break; // this breaks the outside loop
                  }
              }
              return finalResult;
          }
      

      【讨论】:

        【解决方案10】:

        我同意应该在出现无法访问的语句时抛出异常。只是想展示相同的方法如何以更易读的方式执行此操作(需要 java 8 流)。

        private static int oneRun(int range) {
            int[] rInt = new int[range + 1];
            return IntStream
                .rangeClosed(0, range)
                .peek(i -> rInt[i] = generator.nextInt(range))
                .filter(i -> IntStream.range(0, i).anyMatch(j -> rInt[i] == rInt[j]))
                .findFirst()
                .orElseThrow(() -> new RuntimeException("Shouldn't be reached!"));
        }
        

        【讨论】:

          猜你喜欢
          • 2021-03-10
          • 1970-01-01
          • 1970-01-01
          • 2021-02-03
          • 1970-01-01
          • 2017-03-26
          • 2019-01-20
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多