【问题标题】:Is there a faster way to parse a string for valid integers in Java?有没有更快的方法来解析 Java 中有效整数的字符串?
【发布时间】:2019-01-08 16:54:26
【问题描述】:

我的应用程序期望 json 请求包含一个(可能是多维的)未排序数组,其中只有整数和可能的空值。类似[6, 2, [4, 3],[[[5], nil], 1]]

由于我无法解析无效的 json,我不得不求助于使用正则表达式来完成脏活,而且速度非常慢。

例如上面的测试用例大约需要 1.xx seconds 来完成,而带有 10000 元素的平面数组需要少于 1 second

目前我将请求正文作为字符串获取,然后应用正则表达式。

static ArrayList<Integer> getIntegers(String requestData) {
    // Apply a regex to the request body
    final String regularExpression = "([^\\d])+";
    // to get all the nested arrays
    Pattern pattern = Pattern.compile(regularExpression);
    String[] results = pattern.split(requestData);
    ArrayList<Integer> numbers = new ArrayList<>();
    // loop over the results and add to numbers array
    for (String result : results) {
        try {
            numbers.add(Integer.valueOf(result));
        } catch (NumberFormatException e) {
            // Catch and skip any non integers
        }

    }
    return numbers;
}

}

无论如何我可以加快速度,或者是否有性能更好的替代方法?如果我需要处理一个包含 20000 个元素的多维数组,那就太慢了。

【问题讨论】:

  • 好问题!!如果这是 javascript,我会要求您尝试 .flatmap(Infinite) 然后检查。也许这会给出一些想法。
  • @AdityaGupta 感谢您的提示!一定会研究您对一些想法的建议
  • 您为什么要尝试解析无效的 JSON 而不是简单地拒绝此类请求?
  • 小注,正则表达式[^\d] 等于\D,两者都将匹配除数字之外的所有内容
  • @krankit 如果只是 nil,则使用字符串替换将它们替换为 null。您最终会得到可以使用任何 JSON 解析器解析的有效 JSON。

标签: json regex string performance java-8


【解决方案1】:

This answer 确实已经指向正确的方向。第一个重要的步骤是将昂贵的Pattern.compile 操作移出方法,因为Pattern 实例可以重复使用。

此外,遍历匹配的数量可以让您免于创建split 的数组。现在,你也可以跳过 sub-String 创建:

static final Pattern NUMBER = Pattern.compile("\\d+");
static ArrayList<Integer> getIntegers(String requestData) {
    ArrayList<Integer> numbers = new ArrayList<>();
    Matcher m = NUMBER.matcher(requestData);
    while(m.find()) numbers.add(Integer.parseInt(requestData, m.start(), m.end(), 10));
    return numbers;
}

Integer.parseInt(CharSequence s, int beginIndex, int endIndex, int radix) 已添加到 Java 9 中。如果您使用的是旧版本,您可以创建自己的变体。为简化起见,现在只支持 10 的基数:

static final Pattern NUMBER = Pattern.compile("-?\\d+");
static ArrayList<Integer> getIntegers(String requestData) {
    ArrayList<Integer> numbers = new ArrayList<>();
    Matcher m = NUMBER.matcher(requestData);
    while(m.find()) numbers.add(parseInt(requestData, m.start(), m.end()));
    return numbers;
}

static int parseInt(CharSequence cs, int start, int end) {
    int pos = start;
    if(pos >= end) throw format(cs, start, end);
    boolean negative = cs.charAt(pos) == '-';
    if((negative || cs.charAt(pos) == '+') && ++pos==end)
        throw format(cs, start, end);
    int value = 0;
    for(; pos < end; pos++) {
        int next = cs.charAt(pos) - '0';
        if(next < 0 || next > 9) throw format(cs, start, end);
        if(value < Integer.MIN_VALUE/10) throw size(cs, start, pos, end);
        value = value * 10 - next;
    }
    if(value > 0 || !negative && value == Integer.MIN_VALUE)
        throw size(cs, start, pos, end);
    return negative? value: -value;
}
private static RuntimeException format(CharSequence cs, int start, int end) {
    return start > end? new IndexOutOfBoundsException(end+" < "+start):
        new NumberFormatException(start == end?
            "empty string": cs.subSequence(start, end).toString());
}
private static RuntimeException size(CharSequence cs, int start, int pos, int end) {
    for(; pos < end; pos++) 
        if(cs.charAt(pos) < '0' || cs.charAt(pos) > '9') return format(cs, start, end);
    return new NumberFormatException(cs.subSequence(start, end)+" outside the int range");
}

【讨论】:

    【解决方案2】:

    我稍微修改了一下并创建了以下类:

    class JsonNumberParser {
        private final String json;
        private final int length;
        private final List<Integer> result;
        private final char[] buffer = new char[64];
        private int bufferIndex = 0;
    
        public JsonNumberParser(String json) {
            this.json = json;
            length = json.length();
            result = new ArrayList<>(length);
        }
    
        public List<Integer> parse() {
            char c;
            for (int i = 0; i < length; i++) {
                c = json.charAt(i);
                // if we encounter a comma and the buffer contains data
                if (c == ',' && bufferIndex > 0) {
                    // then we add the new number
                    addBuffer();
                    // and reset the buffer
                    while (bufferIndex > 0) {
                        buffer[--bufferIndex] = '\0';
                    }
                } else if (c == '-' || (c >= '0' && c <= '9')) {
                    buffer[bufferIndex++] = c;
                }
            }
            // add the last possible number, if there was any
            if (bufferIndex > 0) {
                addBuffer();
            }
    
            // return the result
            return result;
        }
    
        private void addBuffer() {
            result.add(Integer.valueOf(new String(buffer, 0, bufferIndex)));
        }
    }
    

    当然,您可以将所有这些都放在一个方法中,但是在添加 Integers 时会出现一些代码重复。

    这个解析器的工作方式是,它使用一个缓冲区来缓冲数字,直到我们遇到一个逗号。这样我们就可以在 json 中有大数字(在这个实现中最多 64 位)。

    你可以像下面的例子那样使用它:

    List<Integer> integers = new JsonNumberParser(jsonRequest).parse();
    

    关于性能,我希望这比使用Regex 快得多。但遗憾的是我手头没有基准设置


    请记住,这不是验证器,因此 json 字符串:[[,,,]}] 只会产生一个空的 List


    (也许)改进:我已经考虑并搜索了更多。以下是一些可以提高性能的改进:

    1. 可以通过为buffer 分配new int[64] 来重置buffer,这会产生更多垃圾,但最终可能会更快。

    2. 使用建议的答案here 可以改进数字的解析。它只使用普通的旧数学,不创建字符串和解析整数。

    【讨论】:

    • 为什么要重置阵列?您只使用了用实际值覆盖的部分,因此悬空char 值(与对象引用不同)是无关紧要的。只需将bufferIndex 设置为零。但更好的是,与其将char 值复制到您自己的缓冲区数组,您只需记住第一个匹配字符的位置,并在遇到第一个不匹配字符或第一个不匹配字符时在原始字符串上使用substring字符串的结尾。或者使用我回答的parseInt(CharSequence cs, int start, int end)方法。
    【解决方案3】:

    如果性能是您的问题,我认为流 API 不是一个好的解决方案。

    static ArrayList<Integer> getIntegers(String requestData) {
                char[] charArray = requestData.toCharArray();
                 ArrayList<Integer> numbers = new ArrayList<>();
                for(char c : charArray) {
    
                    if(Character.isDigit(c)) {
                        numbers.add(Integer.valueOf(c) - 48);
                    }
                }
                return numbers;
            }
    

    【讨论】:

    • 谢谢!似乎该解决方案不适用于双(或三)位数字,因为它被格式化为单个char,正如您在我的测试用例SEVERE: java.lang.AssertionError: Not equals : [0,2,4,5,5,9,9] != [0,2,4,55,99]中看到的那样
    • 另外Integer.valueOf(c) 的包装似乎是不必要的,因为您已经在检查字符是否是带有Character.isDigit(c) 的数字。稍后会玩这个谢谢!
    • 此外,如this comment 中所述,将Character.isDigit(…) 与仅处理'0' .. '9' 范围内的字符的代码混合不是一个好主意。也没有理由写 48 而不是 '0' 字符常量。此外,通过 toCharArray() 创建字符串内容的完整副本只是为了编写一个 for-each 循环,不会提高性能。
    【解决方案4】:

    使用堆栈怎么样?

    我们可以升级balanced braces问题。

    在迭代字符串时,如果字符是notBracket(),那么它应该是一个数字。不用说,您忽略所有逗号。同时也会验证数组结构。

    这具有O(n) 的摊销复杂度。

    【讨论】:

      【解决方案5】:

      您可以通过解析积极模式(例如\d+)而不是消极模式([^\d]+)来获得更好的性能。

      private static final Pattern NUMBER = Pattern.compile("\\d+");
      
      List<Integer> extractNumbersRegex(String str) throws IOException {
          Matcher m = NUMBER.matcher(str);
          ArrayList<Integer> numbers = new ArrayList<>();
          while (m.find()) {
              numbers.add(Integer.parseInt(m.group()));
          }
          return numbers;
      }
      

      这对于从字符串中提取是可以的,但是对于大数据,可以切换到不依赖于正则表达式而是直接匹配字符的更高效:

      List<Integer> extractNumbersHandcoded(String str) throws IOException {
          ArrayList<Integer> numbers = new ArrayList<>();
          int start = 0;
          while (start < str.length()) {
              if (Character.isDigit(str.charAt(start))) {
                  break;
              } 
              start++;
          }
          int bufferedInt = 0;
          for (int i = start; i < str.length(); i++) {
              char c = str.charAt(i);
              if (Character.isDigit(c)) {
                  bufferedInt = bufferedInt * 10 + (c - '0');
              } else {
                  numbers.add(bufferedInt);
                  bufferedInt = 0;
              }
          }
          return numbers;
      }
      

      如果您的数据与流一样大,您可以考虑使用Streamtokenizer 的解决方案:

      List<Integer> extractNumbersStreamTokenizer(String str) throws IOException {
          StreamTokenizer s = new StreamTokenizer(new StringReader(str));
          ArrayList<Integer> numbers = new ArrayList<>();
          int token;
          while ((token = s.nextToken()) != StreamTokenizer.TT_EOF) {
              if (token == StreamTokenizer.TT_NUMBER) {
                  numbers.add((int) s.nval);
              }
          }
          return numbers;
      }
      

      所有解决方案都假定数据仅包含整数文字(而非浮点文字)。

      【讨论】:

      • 请记住,Character.isDigit(…) 可能会为'0' .. '9' 范围之外的字符返回true。使用Character.isDigit(…)进行测试时,也应该使用Character.digit(…)进行提取。否则,当您只想处理该范围的值时,对'0' .. '9' 范围使用显式测试。此外,当数字的大小太大时,您的代码可能会静默溢出。
      猜你喜欢
      • 2011-08-22
      • 2020-11-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-05-08
      相关资源
      最近更新 更多