for循环是OutOfMemoryError的原因吗? (蚀)

Is the for loop the reason for OutOfMemoryError? (Eclipse)

您好,我正在编写一个将字符串解析为单个组件的程序,但是当我尝试对其进行测试时,出现内存不足错误。我觉得我的 for/while 循环好像是无限的,但我似乎找不到原因。

    //for loop to loop through char of string
    for(int i=0; i<expressionString.length(); i++) {

        //cast char into ascii int
        int ascii = (int) charAt(i);

        //appending to token if one of  singly operator symbols: *,/,(,),[,]
        if(ascii == 40 || ascii == 41 || ascii == 42 || ascii == 47 || ascii == 91 || ascii == 93){
            token.append((char) ascii);
            tokenList.add(token.toString());

        } //append if +, -
        else if(ascii == 43 || ascii == 45) {
            token.append((char) ascii);

            //check next char if + or /, if so append to token again
            int nextChar = (char) charAt(i+1);
            if(nextChar == 43 || nextChar == 45) {
                token.append((char) nextChar);
            }
            tokenList.add(token.toString());

        } //appending to token if it's a num
        else if ( ascii >= 48 || ascii <=57) {
            token.append((char) ascii);

            //check if next char is a num
            while ((int) charAt(i+1) >= 48 || (int) charAt(i+1) <= 57) {
                //increment i in for loop to check
                i++;
                token.append((int) charAt(i));
            }
            tokenList.add(token.toString());
        }
        //  
    }

请让我知道这是否是我的代码错误,因为我似乎无法确定问题出在哪里。谢谢!

这是您在该循环中所做操作的简化版本。

public class Main {

    public static void main(String[] args) {
        String str = "ABCDE";

        StringBuilder sb = new StringBuilder();
        List<String> list = new ArrayList<>();
        for (char c : str.toCharArray()) {
            sb.append(c);                     
            list.add(sb.toString());  // <-- Problem! This adds the *entire* contents of the StringBuilder as a new String to the list.
        }

        System.out.println(list);
    }
}

这个程序打印

[A, AB, ABC, ABCD, ABCDE]

这是因为每次我们将 char 附加到 StringBuilder 时,我们都会将 StringBuilder 整个 内容添加为ArrayList.

的新 String

现在假设我们将 "ABCDE" 替换为长度 1000000String,例如我们将第一行更改为

String str = Stream.generate(() -> "A").limit(1000000).collect(Collectors.joining()); // String of length 1000000

我们现在正尝试创建 1000000 个 String 个长度从 11000000 的对象,结果可预测。

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
    at java.util.Arrays.copyOfRange(Arrays.java:3664)
    at java.lang.String.<init>(String.java:207)
    at java.lang.StringBuilder.toString(StringBuilder.java:407)
    at my_package.Main.main(Main.java:17)

如何解决?这取决于你想做什么(我们没有所有的上下文),但我怀疑你不需要 StringBuilderList.

正如我在评论中指出的那样,您附加到 StringBuilder 而没有从中删除任何内容的事实是可疑的。

StringBuilder 只是 char[] 的包装器,它会在必要时自动调整大小以容纳您尝试追加的新文本。您可以在堆栈跟踪中看到 OOM 发生在这些自动调整大小之一期间。

此问题的一个解决方案是最初分配足够大的缓冲区,然后在向 StringBuilder 添加更多文本之前不需要调整大小:

StringBuilder token = new StringBuilder(MAXIMUM_EXPECTED_SIZE);

问题在于可能难以确定MAXIMUM_EXPECTED_SIZE;此外,大多数时候您可能会浪费大量内存,因为您几乎没有向缓冲区追加那么多文本。

似乎您实际上并不想在将文本传输到 tokenList 后将其保留在 token 中。您可以使用以下命令从缓冲区中明确删除它:

token.delete(0, token.length());
// or
token.setLength(0);

(实际上,这并没有删除数据,它只是允许后续追加覆盖它)

但这仍然是一种浪费:您根本不需要 StringBuilder

考虑一下你如何处理这些数字:

     if ( ascii >= 48 || ascii <=57) {
        token.append((char) ascii);

        //check if next char is a num
        while ((int) charAt(i+1) >= 48 && (int) charAt(i+1) <= 57) {
                                   //  ^^ NB
            //increment i in for loop to check
            i++;
            token.append((int) charAt(i));
        }
        tokenList.add(token.toString());
    }

你在这里显然想做的是附加第 i 个字符(含)和第 j 个字符(不含)之间的所有内容,其中 j 指向字符串的末尾或非数字字符。所以你可以这样做:

     if ( ascii >= 48 || ascii <=57) {
        int j = i + 1;

        //check if next char is a num
        while (j < expressionString.length() && charAt(j) >= '0' && charAt(j) <= '9') {
            j++;
        }
        tokenList.add(expressionString.subString(i, j));
        i = j;
    }

您可以对其他附加标记执行类似操作。这只是删除了 StringBuilder 的 "middle man",这显然避免了重新分配其内部缓冲区的问题。