我正在创建一个平均四个数字的程序-一个文件中的三个数字和用户输入中的一个数字。很简单。但要注意的是,教授将有意打破程序。

我想在任何方面都提供建议,但请记住重点是健壮性。

import java.util.*;
import java.io.*;

public class average {
    public static void main (String[] args) throws IOException{
        double[] nums = new double[4];
        nums = inputHandler();
        System.out.println("Average " + findAverage(nums));

        System.out.println("End Program.");
    }

    public static double findAverage(double[] nums){
        double average = 0.0;
        final double N = 4.0;

        for(int i = 0; i < N; i++){
            average += nums[i] / N;
        }

        return average;
    }

    public static double[] inputHandler() throws IOException{
        Scanner input = new Scanner(System.in);
        double[] nums = new double[4];
        double[] fileNums = new double[3];

        // get first three numbers from file
        System.out.print("Enter the name of the file which contains the first three numbers: ");
        fileNums = fileInput(input.nextLine());
        System.out.println();

        // copy numbers from fileNums[] to nums[]
        for(int i = 0; i < 3; i++){
            nums[i] = fileNums[i];
        }

        //get last number from user input
        System.out.print("Enter the fourth number: ");
        nums[3] = userInput(input.nextLine());
        System.out.println();

        input.close();

        return nums;
    }

    public static double[] fileInput(String fileName) throws IOException{
        double[] nums = new double[3];

        System.out.println();

        File file = new File(fileName);

        if(!file.isFile()){
            System.err.println("ERROR: File does not exist.");
            System.exit(-1);
        }

        System.out.println(fileName + ":");

        Scanner input = new Scanner(file);
        String cur = "";

        for(int i = 0; i < 3; i++){

            if(!input.hasNext()){
                System.err.println("ERROR: File does not contain enough numbers.");
                System.exit(-1);
            }

            cur = input.next();

            System.out.println("Number " + (i + 1) + ": " + cur);

            if(cur.matches("-?\d+(\.\d+)?")){ //regex ensures numeric input
                if(Double.parseDouble(cur) < 1.7e308){
                    nums[i] = Double.parseDouble(cur);
                }
                else{
                    System.err.println("ERROR: Number too large");
                    System.exit(-1);
                }
            }
            else{
                System.err.println("ERROR: Non numeric input. Please check your file and try again.");
                System.exit(-1);
            }
        }

        input.close();

        return nums;
    }

    public static double userInput(String userInput){
        double num = 0;

        if(userInput.matches("-?\d+(\.\d+)?")){ //regex ensures input is numeric
            num = Double.parseDouble(userInput);
        }
        else{
            System.err.println("ERROR: Non numeric input. Please check your file and try again.");
            System.exit(-1);
        }

        return num;
    }
}


评论

许多人提到了有关上溢/下溢的问题。但是,可以使用BigDecimal避免此类错误。这样,实际上就可以制作出坚如磐石,牢不可破的程序。

#1 楼

首先,在可能引发异常的零件周围使用try / catch块。例如,当文件抛出IOException时会发生什么情况,也许是因为数据已损坏并且文件存在但无法打开?在我的书中,关闭应用程序失败是失败:

if(!file.isFile()){
    System.err.println("ERROR: File does not exist.");
    System.exit(-1);
}


第三,@ SirPython是正确的-使用内置方法读取您的电话号码。无论您的程序需要什么,都提供了用于读取doubleint的内置函数。第四,这不一定有助于提高鲁棒性,将您的方法逻辑拆分开来。您正在使用fileInput()方法进行大量打印。用专用方法进行打印。用专用方法进行输入。发生异常行为时,请随时引发异常,但请务必处理该异常。将您的逻辑拆分成小块大小的组件并分别实现-这将使重用变得容易。

第五,从错误中恢复。如果需要,请重新开始整个输入过程,但不要关闭程序。由于错误而关闭程序可能会被视为错误,并且在许多程序中是不可接受的。

#2 楼

该程序不能完全坚不可摧。

for(int i = 0; i < N; i++){
    average += nums[i] / N;
}

for(int i = 0; i < N; i++){
    total += nums[i];
}
average = total / N;


有两种方法可以计算数字列表的平均值,这两种方法均可被破坏:


使用的方法:将每个数字除以列表的大小,然后将其添加到运行总计中。这不受溢出错误的影响,但是比方法2更容易遭受舍入错误的影响,特别是在数字非常小的情况下(如果教授输入Double.MIN_VALUE的四个副本,则代码应在返回0时返回Double.MIN_VALUE)。将所有数字相加,然后将总数除以列表的大小。这可以抵抗舍入错误,但很容易发生溢出错误(如果教授输入Double.MAX_VALUE的四个副本,则当应返回Double.MAX_VALUE时,它将溢出)。它也比方法1更快,但是在大多数情况下,速度差异并不重要。

选择正确的方法取决于溢出错误还是舍入错误更重要。

评论


\ $ \ begingroup \ $
如果您确实想要,可以检查所有输入的幅度,并确定如何进行。但是代码看起来要复杂得多。
\ $ \ endgroup \ $
– JS1
2015年9月17日在9:16

\ $ \ begingroup \ $
avr = nums [i] + i /(i + 1。)*(avr-nums [i])怎么办?应该足够容易,并且没有这种精度问题。
\ $ \ endgroup \ $
–TonioElGringo
2015年9月17日在11:47

\ $ \ begingroup \ $
为了获得更多乐趣,请注意Java中的浮点加法不是关联的,因此“平均值”的定义不明确。找出平均0.0、0.1、0.2和0.3时会发生什么,然后对0.3、0.2、0.1和0.0尝试相同的事情。
\ $ \ endgroup \ $
–James_pic
2015年9月17日下午13:33

\ $ \ begingroup \ $
这不是完美的方法,但是用方法1进行的Kahan求和将得出非常接近正确的结果。 Double.MIN_VALUE问题并不可怕,因为结果与正确值仅1 ulp。
\ $ \ endgroup \ $
–恢复莫妮卡
2015年9月17日下午16:44

\ $ \ begingroup \ $
您可以避免这两个问题:首先,对数字求和并计数,直到列表用尽或溢出为止。如果发生溢出,请使用先前的总和计算部分平均值,然后继续。最后计算出完整的平均值。
\ $ \ endgroup \ $
–l0b0
2015年9月17日19:25在

#3 楼

public static double userInput(String userInput){
    // ...
    if(userInput.matches("-?\d+(\.\d+)?")){
        // ...
    } else {
        System.err.println("ERROR: Non numeric input.Please check your file and try again.");
        System.exit(-1);
    }
    // ...
}


提示我输入一个数字!我可能输入错误,但是为什么它要求我检查文件?

这是复制粘贴代码的问题...如果您有一种方法可以接受来自一个double实例和一个自定义错误消息,您将可以回避此问题。例如:

/**
 * Gets the number of double values from the scanner.
 *
 * @param scanner      the {@link Scanner} instance to take from
 * @param times        the number of values required
 * @param errorMessage the error message to use if not enough values
 * @return a double array of the required size
 * @throws {@link IllegalArgumentException} if not enough values
 */
private static double[] getInputs(Scanner scanner, int times, String errorMessage) {
    double[] results = new double[times];
    int i = 0;
    for (; i < times && scanner.hasNextDouble(); i++) {
        results[i] = scanner.nextDouble();
    }
    if (i != times) {
        throw new IllegalArgumentException(errorMessage);
    }
    return results;
}


另外,要呼应@ Hosch250的回答,程序的健壮性是其从错误中恢复的能力...您在这里所做的只是为了使用错误消息处理它们,但是您没有尝试恢复。这也许也值得研究,例如您是否希望继续提示输入第四个数字,直到接受有效的Scanner值?

#4 楼

没用的代码
在代码的第一行,我已经看到一个问题:

    double[] nums = new double[4];
    nums = inputHandler();


为什么要为nums分配一个数组,然后立即将其重新分配给还有什么吗第一行完全没有用。应该是:
    double[] nums = inputHandler();

您在inputHandler()中使用fileNums犯了同样的错误。 />
public static double findAverage(double[] nums){
    double average = 0.0;
    final double N = 4.0;

    for(int i = 0; i < N; i++){
        average += nums[i] / N;
    }

    return average;
}



doubleN用作循环限制。
每个循环除一次,而不是一次。 br />这就是我写的方式。请注意,我的函数假定数组将包含至少一个元素。如果这不一定是正确的,也可以检查是否为空或空数组。
public static double findAverage(double[] nums){
    double sum = 0.0;
    final int numElements = nums.length;

    for(int i = 0; i < numElements; i++) {
        sum += nums[i];
    }

    return sum / numElements;
}


评论


\ $ \ begingroup \ $
不过,OP的平均值+ = nums [i] / N的方式确实可以防止溢出。
\ $ \ endgroup \ $
– h.j.k.
2015年9月17日下午5:27

\ $ \ begingroup \ $
@ h.j.k。那么下溢呢?如果nums [0]是最小的可表示双精度数怎么办?无论哪种方式都会有问题。
\ $ \ endgroup \ $
– JS1
2015年9月17日上午9:08

\ $ \ begingroup \ $
没错,@ Mark的答案都涵盖了这两点... :)
\ $ \ endgroup \ $
– h.j.k.
15年9月17日在9:12

#5 楼

您正在做太多工作来验证输入。 Scanner已经有一种读取双精度数据的方法:java.util.Scanner.nextDouble

与其做所有疯狂的正则表达式工作,不如直接在java.util.Scanner.hasNextDouble旁边使用它来接收输入。这样可以确保收到的所有输入都是有效的,并且可以有效地接收到。因此,您的教授很难破坏该应用程序。无需实际进行任何处理。实际上,如果您使用的是IDE,则您的IDE很可能会发脾气。处理它。

只需在方法中插入一个IOException并让该方法处理异常,而不是使用该方法的任何对象。


如果这是一个常量值,则更好的做法是将其移至类的属性并像这样声明它:

final double N = 4.0;



您的代码IOException出现在代码的很多地方。坐在那里看起来有点尴尬。它应该是一个常量,类似于上面。

评论


\ $ \ begingroup \ $
只需在方法中插入try / catch,然后在catch中抛出IOException,这会有什么好处?您会发现常量。我的个人指导原则是,除了0、1和''(空字符串)以及较小范围内的2之外的所有文字都是可疑的,几乎应始终移至常量...然后我也有此个人指导原则常量本身是可疑的,应该几乎总是将其移动到具有合理默认值的可配置设置... :)
\ $ \ endgroup \ $
– Stijn de Witt
2015年9月17日在7:25

\ $ \ begingroup \ $
@StijndeWitt我已经根据你的第一句话对答案进行了编辑。
\ $ \ endgroup \ $
– SirPython
15年9月17日在21:30

#6 楼

如果用户将不包含换行符的字符流传送到stdin,则扫描程序可能只会继续阅读。 (我测试了一个9 mb的文件,它读取了整个内容。)当您尝试从数据文件中读取下一行时,Scanner.next()会执行相同的操作。

读取文件系统的技巧时,也有可能使扫描仪挂起。例如,如果在调用File.isFile()和尝试读取文件之间替换了该文件,则可以用刚刚挂起的管道替换该文件。