如何加速将文件解析为HashMap

时间:2018-04-05 16:46:56

标签: java performance parsing

我编写了以下函数来解析一个大文本文件(大约2 GB) 逐行进入Map,有效地计算每个单词的出现次数。我只对单词感兴趣(小写以避免条目重复),没有标点符号或空格。然而,在大文件上执行以下代码大约需要3分钟。我想知道为什么以及是否有办法加快速度。

import java.util.*;

public class Stream {

Map<String, Integer> map = new HashMap();

public void getLines() {

    try (BufferedReader fileReader = new BufferedReader(new FileReader("resources/hugeFile"))) {
        String line ;
        while ((line = fileReader.readLine()) != null) {
            String[] words = line.toLowerCase().replaceAll("[^a-z ]", "").split("\\s+");
            for (int i = 0; i < words.length; i++) {
                if (map.get(words[i]) == null) {
                    map.put(words[i], 1);
                }
                else {
                    int newValue = Integer.valueOf(String.valueOf(map.get(words[i])));
                    newValue++;
                    map.put(words[i], newValue);
                }
            }
        }

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

1 个答案:

答案 0 :(得分:2)

首先,如果您认真对待优化,则必须衡量效果。因为很多&#34;改进&#34;这似乎是&#34;改进&#34;可能证明不会带来任何甚至恶化表现。在许多情况下,编译器比人类更好地优化代码。所以你必须做基准测试,请看下面的问题:

  

How do I write a correct micro-benchmark in Java?

我在下面发布了两个代码草图。这些只是草图,给出一个粗略的想法。我既没有测试它们也没有基准测试。

一个提示是您访问地图太多了。您可以使用map.get进行检查,然后使用map.put有条件地设置值。您可以改用putIfAbsentcomputeIfAbsent。此外,您可以改进增加现有值的方式。在这种情况下,我使用可变AtomicInteger代替不可变Integer。所以我建议如下:

    Map<String, AtomicInteger> map = new HashMap<>();

    Consumer<String> countWords = word -> map.computeIfAbsent(word, (w) -> new AtomicInteger(0)).incrementAndGet();

    try (BufferedReader fileReader = new BufferedReader(new FileReader("resources/hugeFile"))) {
        String line;
        while ((line = fileReader.readLine()) != null) {
            splitAndConsumeWords(line, countWords);
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }

接下来,您使用line.toLowerCase().replaceAll("[^a-z ]", "").split("\\s+")将字符串转换为小写,仅保留字母和空格并将字符串拆分为单词。没有基准测试我肯定不知道它,但我怀疑这可能是代码中最耗时的操作。没有正则表达式重写它并不是什么大不了的事。您所需要的只是遍历字符串的字符,将它们转换为小写,附加到当前单词或扔掉。以下是我的表现。

我创建一个数组,将每个字符映射到替换字符。 a-z或空格的相同字符,A-Z的小写字母。所有其他字符都将映射到0,这意味着它们应该被丢弃:

private static char[] ONLY_LETTERS_TO_LOWERCASE = new char[65535];

static {
    ONLY_LETTERS_TO_LOWERCASE[' '] = ' ';
    for (char c = 'a'; c <= 'z'; c++) {
        ONLY_LETTERS_TO_LOWERCASE[c] = c;
    }
    for (char c = 'A'; c <= 'Z'; c++) {
        ONLY_LETTERS_TO_LOWERCASE[c] = Character.toLowerCase(c);
    }
}

然后,您只需查找每个角色的替换词并构建单词:

public static void splitAndConsumeWords(String line, Consumer<String> wordsConsumer) {

    char[] characters = line.toCharArray();
    StringBuilder sb = new StringBuilder(16);
    for (int index = 0; index < characters.length; index++) {
        char ch = characters[index];
        char replacementCh = ONLY_LETTERS_TO_LOWERCASE[ch];
        // If we encounter a space
        if (replacementCh == ' ') {
            // And there is a word in string builder
            if (sb.length() > 0) {
                // Send this word to the consumer
                wordsConsumer.accept(sb.toString());
                // Reset the string builder
                sb.setLength(0);
            }
        } else if (replacementCh != 0) {
            sb.append(replacementCh);
        }
    }
    // Send the last word to the consumer
    if (sb.length() > 0) {
        wordsConsumer.accept(sb.toString());
    }
}

ONLY_LETTERS_TO_LOWERCASE映射表的替代方法是if语句,如:

        if (ch >= 'a' && ch <= 'z' || ch == ' ') {
            replacementCh = ch;
        } else if (ch >= 'A' && ch <= 'Z') {
            replacementCh = Character.toLowerCase(ch);
        }
        else {
            replacementCh = 0; 
        }

我不确定什么会更好用,我认为在数组中查找必须更快,但我不确定。这就是您最终需要进行基准测试的原因。