JAVA加速列表过滤

时间:2017-09-08 07:50:02

标签: java collections java-stream

我有一个初始列表,每个科目都有主题和短语。

public class Subject {
    private String subject_name;
    private List<Phrase> phrases;
}

public class Phrase {
    private String phrase_name; 
}

我需要过滤初始主题列表(应该得到另一个列表),条件是短语名称应该匹配输入文本中的单词。 所以,如果我有输入List:

subjects :
[
    {
        subject_name : "black",
        phrases : 
        [
            phrase_name : "one",
            phrase_name : "two",
            phrase_name : "three"       
        ]
    },
    {
        subject_name : "white",
        phrases : 
        [
            phrase_name : "qw",
            phrase_name : "as",
            phrase_name : "do",
            phrase_name : "oopopop"
        ]
    },
    {
        subject_name : "green",
        phrases : 
        [
            phrase_name : "rrr",
            phrase_name : "ppo" 
        ]
    }
]

我输入text = "one year today some rrr",最后我需要获取以下列表

subjects :
[
    {
        subject_name : "black",
        phrases : 
        [
            phrase_name : "one"
        ]
    },
    {
        subject_name : "green",
        phrases : 
        [
            phrase_name : "rrr" 
        ]
    }
]

下面的代码工作正常,我得到了理想的结果,但是当我需要过滤例如20000&#34; text&#34;对于那些带我一些〜5分钟的受试者,取决于文字大小。

private List<Subject> filterSubjects(List<Subject> subjects, String text) {
    List<Subject> result = new ArrayList<Subject>();

    for (Subject subject : subjects) {

        List<Phrase> p = new ArrayList<Phrase>();
        for (Phrase phrase : subject.getPhrases()) {
            String regex = "\\b(" + replaceSpecialChars(phrase.getName()).toLowerCase() + ")\\b";
            Pattern pattern = Pattern.compile(regex);
            Matcher matcher = pattern.matcher(text);

            if (matcher.find()) {
                p.add(phrase);
            }
        }

        if (!p.isEmpty()) {
            result.add(new Subject.SubjectBuilder(subject.getSubjectId(), subject.getName())
                    .setWeight(subject.getWeight()).setColor(subject.getColor())
                    .setOntologyId(subject.getOntologyId()).setCreatedBy(subject.getCreatedBy())
                    .setUpdatedBy(subject.getUpdatedBy()).setPhrases(p).build());

        }
    }

    return result;
}

我也尝试使用流,但这对我不起作用,因为我不想过滤初始主题列表,但需要新的一个:

subjects = subjects.stream()
        .filter(s -> s.getPhrases().parallelStream()
                .anyMatch(p -> text.matches(".*\\b" + replaceSpecialChars(p.getName().toLowerCase()) + "\\b.*")))
        .collect(Collectors.toList());

subjects.parallelStream()
        .forEach(s -> s.getPhrases().removeIf(p -> !text.matches(".*\\b"
                + replaceSpecialChars(p.getName().toLowerCase())
                + "\\b.*")));

修改

这是分析的结果

enter image description here

6 个答案:

答案 0 :(得分:3)

正如评论中所建议的那样,您应该进行介绍。正确使用,分析器应该给你更多的细节,而不是“在该方法中消耗的整个时间”。您应该能够看到Pattern.compile()Matcher.find()ArrayList.add()以及所有其他方法花费了多少时间,无论它们是您的还是JDK方法。

你这样做是绝对至关重要的,否则你就会因盲目工作而浪费精力。例如,也许ArrayList.add()正在花费时间。你可以用各种方式解决它。但是,除非你有确凿的证据证明这就是问题所在,否则为什么要花时间呢?

您还可以应用提取方法重构,以便您拥有更多自己的方法供分析器报告。这样做的一个好处是编译器和运行时非常适合优化小方法。

当您找到花费时间的方法时,您需要:

  • 使该方法更有效率
  • 找到一种方法来更少次地调用该方法

如果它在replaceSpecialChars()花了很多时间,你应该看看它,并提高它的表现。

根据其复杂性,编译正则表达式可能需要一些时间。如果replaceSpecialChars()中包含Pattern.compile(),请将其移动到仅调用一次的地方(静态初始值设定项,构造函数等)。如果它使用正则表达式并且没有Pattern.compile(),请考虑引入一个。

您的修改显示大部分时间都花费在您向我们展示的代码调用的Pattern.compile()中。

由于您向我们展示的代码中的regex是使用数据中的字符串构建的,因此您不能只调用Pattern.compile()一次。但是,您可能会从重复短语的备忘中受益 - 这取决于数据中的重复次数。

 Map<String, Pattern> patterns = new HashMap<>();

 Pattern pattern(String s) {
     Pattern pattern = patterns.get(s);
     if(pattern == null) {
         pattern = Pattern.compile("\\b" + s + "\\b");
         patterns.put(s,pattern);
     }
     return pattern;
 }

(注意这不是线程安全的 - 有更好的缓存类,例如在Guava中)

您可以通过准备(每次输入一次)使文本内的查找更容易:

  • 将所有边界字符转换为空格
  • 在正面和背面添加空格

现在您只需要preparedText.contains(" " + phrase.getName() + " ")。这避免了完全编译正则表达式。您可以使用正则表达式来准备文本,但这只需要执行一次(如果您有多个文本,则可以重用已编译的Pattern

但如果你这样做,你也可以将文本处理成Set,搜索速度比字符串更快:

Set<String> wordSet = new HashSet<>(Arrays.asList(preparedText.split(" ")));
对于足够大的文本,

wordSet.contains(phrase.getName())应该比preparedText.contains(phrase.getName())更快。

它也可能 - 再次,取决于数据 - 更快地迭代text中的标记,寻找集合中的单词,而不是循环遍历单词。这可能会以不同的顺序返回项目 - 这是否重要取决于您的要求。

 Set<String> lookingFor = collectWordsToFind(subject);
 StringTokenizer tokens = new StringTokenizer(text);
 for(String token : tokens) {
     if(lookingFor.contains(token)) {  // or if(lookingFor.remove(token))
          outputlist.add(token);
     }
 }

这可以避免多次扫描每个text

最后,踩到后面,我会考虑先预处理Subject数据,制作phrase_nameSubject的地图。也许你已经从外部资源中读取数据了 - 如果是这样的话,一定要在你阅读的时候建立这个地图(也许不是你现在拥有的List):

Map<String,Set<Subject>> map = new HashMap<>();
for(Subject subject : subjects) {
    for(String phrase : subject.phrases()) {
        String name = phrase.name();
        Set<Subject> subjectsForName = map.get(name);
        if(subjectsForName == null) {
            subjectsForName = new HashSet<>();
            map.put(name, subjectsForName);
        }
        subjectsForName.add(subject);
    }
}

现在,对于输入text中的每个字词,您可以快速获得包含该phrase_name Set<Subjects> subjectsForThisWord = map.get(word)的一组主题。

Map<T,Collection<U>>是一种相当常见的模式,但是像Guava和Apache Commons这样的第三方集合库提供了MultiMap,它使用更干净的API做同样的事情。

答案 1 :(得分:1)

正如您所提到的那样,您尝试了没有运气的流,这是我尝试将您的功能转换为流(警告:未经测试!):

subjects.parallelStream()
            .map(subject -> {
                List<Phrase> filteredPhrases = subject.getPhrases().parallelStream()
                        .filter(p -> text.matches(".*\\b" + replaceSpecialChars(p.getName().toLowerCase()) + "\\b.*"))
                        .collect(Collectors.toList());
                return new AbstractMap.SimpleEntry<>(subject, filteredPhrases);
            })
            .filter(entry -> !entry.getValue().isEmpty())
            .map(entry -> {
                Subject subj = entry.getKey();
                List<Phrase> filteredPhrases = entry.getValue();
                return new Subject.SubjectBuilder(subj.getId(), subj.getName()).setWeight(subj.getWeight()).setPhrases(filteredPhrases);
            })
            .map(Subject.SubjectBuilder::build)
            .collect(Collectors.toList());

基本上,第一张地图是构建一对原始主题和过滤后的短语,在第二张地图中,这些对映射到单个SubjectBuilder实例,并初始化所有参数(另请注意,而不是原始短语,过滤后的短语,然后第三张地图,只是新主题的建立。

我不确定这段代码是否会比你的更快(我也没有测试过,所以也没有任何保证!),它只是一个想法,你如何解决你的任务与溪流。

答案 2 :(得分:1)

你必须找到的词越多,执行独特的正则表达式匹配就越少。除了每个不同正则表达式的准备成本之外,您还要为每个单词执行新的线性搜索操作。相反,让引擎仅匹配整个单词并对单词执行快速地图查找。

首先,准备一个查找地图

Map<String,Map.Entry<Phrase,Subject>> lookup = subject.stream()
  .flatMap(s->s.getPhrases().stream().map(p->new AbstractMap.SimpleImmutableEntry<>(p,s)))
  .collect(Collectors.toMap(e -> e.getKey().getName(), Function.identity()));

然后,使用正则表达式引擎对整个单词进行流式处理,并通过Subject查找关联的Phrase / Subject组合,组,并将结果组转换为新的{之后{1}}:

Subject

如果List<Subject> result = Pattern.compile("\\W+").splitAsStream(text) .map(lookup::get) .filter(Objects::nonNull) .collect(Collectors.groupingBy(Map.Entry::getValue, Collectors.mapping(Map.Entry::getKey, Collectors.toList()))) .entrySet().stream() .map(e -> { Subject subject=e.getKey(); return new Subject.SubjectBuilder(subject.getSubjectId(), subject.getName()) .setWeight(subject.getWeight()).setColor(subject.getColor()) .setOntologyId(subject.getOntologyId()).setCreatedBy(subject.getCreatedBy()) .setUpdatedBy(subject.getUpdatedBy()).setPhrases(e.getValue()).build(); }) .collect(Collectors.toList()); 支持将现有Subject.SubjectBuilder指定为模板而不必手动复制每个属性,那会简单得多......

答案 3 :(得分:0)

在我看来,你无法摆脱for循环(这是代码复杂性的绝对杀手),因为你需要检查每个主题(即使你在过滤之前对主题进行了排序)。所以,我认为唯一可能的加速可以通过多线程完成(如果你不关心输出列表中的顺序)。为此,您可以使用java的内置ExecutorService。它将产生指定数量的线程,您提交所有过滤任务,ExecutorService会自动在线程中调度它们。

修改:您可能还需要确保SubjectBuilder不会创建p的副本,因为这也会花费大量时间。< / p>

答案 4 :(得分:0)

我会尝试摆脱正则表达式,因为你正在为每个主题中的每个短语编译这些。我不确定它会更有效率,或者达到完全相同的结果,因为我无法针对您的数据集运行它,但您可以尝试这样的更改:

        List<Phrase> p = new ArrayList<Phrase>();
        for (Phrase phrase : subject.getPhrases()) {
            //String regex = "\\b(" + phrase.getName().toLowerCase() + ")\\b";
            //Pattern pattern = Pattern.compile(regex);
            //Matcher matcher = pattern.matcher(text);
            //
            //if (matcher.find()) {
            //    p.add(phrase);
            //}
            if (text.contains(phrase.getName().toLowerCase())) {
                p.add(phrase);
            }
        }

我做了一个基本测试,我认为它应该以类似的方式匹配

答案 5 :(得分:0)

解决方案似乎非常简单,使用&#34;包含&#34;而不是使用消耗最多处理时间的Pattern:

private List<Subject> filterSubjects(List<Subject> subjects, String text) {

    String SPACE_PATTERN = " ";
    List<Subject> result = new ArrayList<Subject>();

    for (Subject subject : subjects) {

        List<Phrase> p = new ArrayList<Phrase>();
        for (Phrase phrase : subject.getPhrases()) {        
            if (text.contains(SPACE_PATTERN + replaceSpecialChars(phrase.getName()).toLowerCase() + SPACE_PATTERN)) {
                p.add(phrase);
            }
        }

        if (!p.isEmpty()) {
            result.add(new Subject.SubjectBuilder(subject.getSubjectId(), subject.getName())
                    .setWeight(subject.getWeight()).setColor(subject.getColor())
                    .setOntologyId(subject.getOntologyId()).setCreatedBy(subject.getCreatedBy())
                    .setUpdatedBy(subject.getUpdatedBy()).setPhrases(p).build());

        }
    }

    return result;
}

这给我的表现从大约5分钟开始,现在大约20秒,20K文字处理。我要优化的另一个步骤是从循环中取出replaceSpecialChars以获取短语名称