垃圾友好替代substring()

时间:2013-12-02 19:40:02

标签: java string garbage-collection

我有一个管道分隔文件,我解析它以获取系统选项。环境对堆分配很敏感,我们正在努力避免垃圾回收。

下面是我用来解析管道分隔字符串的代码。该函数调用约35000次。我想知道是否有更好的方法不能创造更多的内存流失。

static int countFields(String s) {
    int n = 1;
    for (int i = 0; i < s.length(); i++)
        if (s.charAt(i) == '|')
            n++;

    return n;
}

static String[] splitFields(String s) {
    String[] l = new String[countFields(s)];

    for (int pos = 0, i = 0; i < l.length; i++) {
        int end = s.indexOf('|', pos);
        if (end == -1)
            end = s.length();
        l[i] = s.substring(pos, end);
        pos = end + 1;
    }

    return l;
}

编辑1,关于java版本:

出于商业原因,我们坚持使用JDK 1.6.0_25。

编辑2关于String和String []用法:

String []用于执行系统设置逻辑。基本上,如果String [0] .equals(“true”)则启用调试。这是使用模式

关于垃圾收集对象的编辑3:

输入String和String []最终是GC'd。输入字符串是系统设置文件中的一行,在处理完整个文件后GC'd,并且在处理完整行后,String []为GC。

编辑 - 解决方案:

这是Peter Lawrey和zapl解决方案的组合。此外,这个类不是线程安全的。

public class DelimitedString {

    private static final Field EMPTY = new Field("");

    private char delimiter = '|';
    private String line = null;
    private Field field = new Field();

    public DelimitedString() { }

    public DelimitedString(char delimiter) {
        this.delimiter = delimiter;
    }

    public void set(String line) {
        this.line = line;
    }

    public int length() {
        int numberOfFields = 0;
        if (line == null)
            return numberOfFields;

        int idx = line.indexOf(delimiter);
        while (idx >= 0) {
            numberOfFields++;
            idx = line.indexOf(delimiter, idx + 1);
        }
        return ++numberOfFields;
    }

    public Field get(int fieldIndex) {
        if (line == null)
            return EMPTY;

        int currentField = 0;
        int startIndex = 0;
        while (currentField < fieldIndex) {
            startIndex = line.indexOf(delimiter, startIndex);

            // not enough fields
            if (startIndex < 0)
                return EMPTY;

            startIndex++;
            currentField++;
        }

        int endIndex = line.indexOf(delimiter, startIndex);
        if (endIndex == -1)
            endIndex = line.length();

        fieldLength = endIndex - startIndex;
        if (fieldLength == 0)
            return EMPTY;

        // Populate field
        for (int i = 0; i < fieldLength; i++) {
            char c = line.charAt(startIndex + i);
            field.bytes[i] = (byte) c;
        }
        field.fieldLength = fieldLength;
        return field;
    }

    @Override
    public String toString() {
        return new String(line + " current field = " + field.toString());
    }

    public static class Field {

        // Max size of a field
        private static final int DEFAULT_SIZE = 1024;

        private byte[] bytes = null;
        private int fieldLength = Integer.MIN_VALUE;

        public Field() {
            bytes = new byte[DEFAULT_SIZE];
            fieldLength = Integer.MIN_VALUE;
        }

        public Field(byte[] bytes) {
            set(bytes);
        }

        public Field(String str) {
            set(str.getBytes());
        }

        public void set(byte[] str) {
            int len = str.length;
            bytes = new byte[len];
            for (int i = 0; i < len; i++) {
                byte b = str[i];
                bytes[i] = b;
            }
            fieldLength = len;
        }

        public char charAt(int i) {
            return (char) bytes[i];
        }

        public byte[] getBytes() {
            return bytes;
        }

        public int length() {
            return fieldLength;
        }

        public short getShort() {
            return (short) readLong();
        }

        public int getInt() {
            return (int) readLong();
        }

        public long getLong() {
            return readLong();
        }

        @Override
        public String toString() {
            return (new String(bytes, 0, fieldLength));
        }

        // Code taken from Java class Long method parseLong()
        public long readLong() {
            int radix = 10;
            long result = 0;
            boolean negative = false;
            int i = 0, len = fieldLength;
            long limit = -Long.MAX_VALUE;
            long multmin;
            int digit;

            if (len > 0) {
                char firstChar = (char) bytes[0];
                if (firstChar < '0') { // Possible leading "-"
                    if (firstChar == '-') {
                        negative = true;
                        limit = Long.MIN_VALUE;
                    } else
                        throw new NumberFormatException("Invalid leading character.");

                    if (len == 1) // Cannot have lone "-"
                        throw new NumberFormatException("Negative sign without trailing digits.");
                    i++;
                }
                multmin = limit / radix;
                while (i < len) {
                    // Accumulating negatively avoids surprises near MAX_VALUE
                    digit = Character.digit(bytes[i++], radix);
                    if (digit < 0)
                        throw new NumberFormatException("Single digit is less than zero.");
                    if (result < multmin)
                        throw new NumberFormatException("Result is less than limit.");

                    result *= radix;
                    if (result < limit + digit)
                        throw new NumberFormatException("Result is less than limit plus new digit.");

                    result -= digit;
                }
            } else {
                throw new NumberFormatException("Called readLong with a length <= 0. len=" + len);
            }
            return negative ? result : -result;
        }
    }
}

8 个答案:

答案 0 :(得分:6)

我会做这样的事情。

public static void main(String[] args) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader("inputfile"));
    StringBuilder sb = new StringBuilder();
    do {
        boolean flag = readBoolean(br, sb);
        long val = readLong(br, sb);
        process(flag, val);
    } while (nextLine(br));
    br.close();
}

private static void process(boolean flag, long val) {
    // do something.
}

public static boolean readBoolean(BufferedReader br, StringBuilder sb) throws IOException {
    readWord(br, sb);
    return sb.length() == 4
            && sb.charAt(0) == 't'
            && sb.charAt(1) == 'r'
            && sb.charAt(2) == 'u'
            && sb.charAt(3) == 'e';
}

public static long readLong(BufferedReader br, StringBuilder sb) throws IOException {
    readWord(br, sb);
    long val = 0;
    boolean neg = false;
    for (int i = 0; i < sb.length(); i++) {
        char ch = sb.charAt(i);
        if (ch == '-')
            neg = !neg;
        else if (ch >= '0' && ch <= '9')
            val = val * 10 + ch - '0';
        else
            throw new NumberFormatException();
    }
    return neg ? -val : val;
}

public static boolean nextLine(BufferedReader br) throws IOException {
    while (true) {
        int ch = br.read();
        if (ch < 0) return false;
        if (ch == '\n') return true;
    }
}

public static void readWord(BufferedReader br, StringBuilder sb) throws IOException {
    sb.setLength(0);
    while (true) {
        br.mark(1);
        int ch = br.read();
        switch (ch) {
            case -1:
                throw new EOFException();
            case '\n':
                br.reset();
            case '|':
                return;
            default:
                sb.append((char) ch);
        }
    }
}

这是更复杂的,但创造了很少的垃圾。事实上,StringBuilder可以回收利用。 ;)

注意:这不会创建StringString[]

答案 1 :(得分:2)

  

基本上,if String[0].equals("true")然后启用调试。

您可以通过直接与输入字符串进行比较来摆脱数组和子字符串的创建。像Peter Lawrey的解决方案那样不能避免创建输入字符串,但改变的工作可能会少一些(尽管我对此表示怀疑)。

public static boolean fieldMatches(String line, int fieldIndex, String other) {
    int currentField = 0;
    int startIndex = 0;
    while (currentField < fieldIndex) {
        startIndex = line.indexOf('|', startIndex);

        // not enough fields
        if (startIndex < 0)
            return false;

        startIndex++;
        currentField++;
    }

    int start = startIndex;
    int end = line.indexOf('|', startIndex);
    if (end == -1) {
        end = line.length();
    }
    int fieldLength = end - start;

    // make sure both strings have the same length
    if (fieldLength != other.length())
        return false;

    // regionMatches does not allocate objects
    return line.regionMatches(start, other, 0, fieldLength);
}

public static void main(String[] args) {
    String line = "Config|true"; // from BufferedReader
    System.out.println(fieldMatches(line, 0, "Config"));
    System.out.println(fieldMatches(line, 1, "true"));
    System.out.println(fieldMatches(line, 1, "foobar"));
    System.out.println(fieldMatches(line, 2, "thereisnofield"));
}

输出

true
true
false
false

答案 2 :(得分:1)

只是一个想法。不要拆分任何东西。做相反的事 - 追加 它们(例如在一些StringBuilder中)并将它们保存在一个大字符串中 或者实际上StringBuilder会更好。字符串可以 以|分隔和(当前是什么)字符串数组,如#。

然后只返回方法中的索引 splitFields - 起始索引和结束索引 (当前你的String [])。

在这里抛出一些想法。不确定 您确切的用例场景,取决于 你使用返回值做什么。

当然,您需要管理那个庞大的StringBuilder 您自己并在不需要该数据时从中删除数据 任何更多,否则它最终会变得太大。

即使这个想法不能直接适用,我也希望你 明白我的观点 - 我认为你需要一些游泳池或记忆区 或类似你自己管理的东西。

答案 3 :(得分:1)

由于此处最大的“违规者”是解析输入后创建的字符串,后面跟着每次调用创建一次的字符串数组,并且因为它从您的一条注释中显示您不需要同时显示所有子字符串,你可以创建一个对象,一次一个地为你提供字符串,重用相同的StringBuilder对象。

这是一个骨架类,展示了如何做到这一点:

class Splitter {
    private String s="";
    private int pos = 0;
    public void setString(String newS) {
        s = newS;
        pos = 0;
    }
    boolean tryGetNext(StringBuilder result) {
        result.delete(0, result.length());
        // Check if we have anything to return
        if (pos == s.length()) {
            return false;
        }
        // Go through the string starting at pos, adding characters to result
        // until you hit the pipe '|'
        // At that point stop and return
        while (...) {
            ...
        }
        return true;
    }
}

现在您可以按如下方式使用此类:

StringBuilder sb = new StringBuilder(MAX_LENGTH);
Splitter splitter = new Splitter();
for (String s: sourceOfStringsToBeSplit) {
    sb.setString(s);
    while (splitter.tryGetNext(sb)) {
        ... // Use the string from sb
    }
}

如果sb的内部缓冲区的大小调整正确,则此代码将在整个运行期间创建三个对象 - SplitterStringBuilder和内部字符数组StringBuilder。但是,当您使用StringBuilder不创建其他对象时需要小心 - 具体而言,您需要避免在其上调用toString()。例如,不要将StringBuilder的相等性与这样的固定字符串进行比较

if (sb.toString().equals(targetString)) {
    ...
}

你应该写

if (targetString.length() == sb.length() && sb.indexOf(targetString) == 0) {
    ...
}

答案 4 :(得分:0)

喜欢这个

static String[] splitFields(String s) {    
    List<String> list = new ArrayList<String>();
    StringBuilder sb  = new StringBuilder();
    for (int i = 0; i < s.length(); i++) {
         char charAt = s.charAt(i);
         if (charAt == '|'){
            list.add(sb.toString());
            sb = new StringBuilder();
        }else{
           sb.append(charAt);
        }
    }
    list.add(sb.toString());//last chunk
    return list.toArray(new String[list.size()]);; 
}

答案 5 :(得分:0)

为什么重新发明轮子?这要简单得多,不需要先计算子串。

http://docs.oracle.com/javase/7/docs/api/java/lang/String.html#split%28java.lang.String%29

答案 6 :(得分:0)

为什么不能这样:http://javacsv.sourceforge.net/com/csvreader/CsvReader.html

这些数据是纯粹的管道分隔文本,还是像HL7或ER7更具特色的东西,它保证使用特定的解析器?

答案 7 :(得分:0)

上面的答案似乎没有回答你的问题。

他们都建议你使用String.split() - 我承认,这也是我推荐的。

但那不是你想知道的。你想要一些内存流失较少的东西。答案是否定的,没有。事实上,你所做的比split更有效率。 (特殊情况下是单字符正则表达式,但使用List然后将列表导出为String []。) - 如果您的目标确实是减少内存占用,则不希望使用split

然而,我没有看到的是,你是否已将GC问题确定为实际问题。现代JVM非常擅长清理短期对象。因此,如果您正在尝试提前计划,请不要 - 只使用split。如果您已经将GC识别为问题并且正在尝试找到解决方案,那么此解决方案是您将获得的最佳解决方案(除非您实现自己的String对象,该对象保留与Java相同的后备字符数组。)< / p>

您可以传入char[]并返回实际放入该子数组的数组中的字符数。这样你只有一个缓冲区。这假设您正在处理从此函数中一次返回的令牌。您还可以传入自己创建的MutableString对象。摆脱String []将大大减轻GC的负担。

当然,现在你必须考虑像缓冲区溢出这样的事情,以及其他类似的废话。系统为您解决的问题。