这些天来,我发布了Wordcounter ,这是一个Java库和命令行实用程序,用于对文本文件中的单词进行计数并对单词计数进行分析,从而大量使用了功能编程结构和并行计算方法。 这是我在“令人讨厌的快速问答”大赛第四个条目SAP ,经过给料机 , 托多尔和Hanoier 。
该库使用JDK 8 lambda ,以及新的JDK 7功能,例如Fork / Join和NIO.2 。 它是内置的,只能与支持lambda的JDK 8的早期访问版本一起使用 。
随着JDK 8中lambda及其支持功能的引入,我们用Java构建软件的方式将发生变化。 如果您想了解几年后Java代码的外观,可以看看Wordcounter。 与当前大多数资源不同,这不是一个简单的教程,而是一个实际的项目。
竞赛任务要求使用Fork / Join和lambdas实现算法,该算法分析目录中的所有文件,并找到文件中十个最常用的单词以及它们出现的次数。 我没有简单地坚持使用Fork / Join,而是尝试找到最适合此任务的并行方法,这使我选择了Producer / Consumer作为核心的单词计数逻辑。
您可以在github上探索源代码。 还有一个相当全面的自述文件,它提供了更详细的文档。
最新的二进制,javadoc和源代码包可在GitHub 下载部分中找到 。 如果有足够的兴趣,我将在线发布Javadoc,并在中央Maven存储库中提供该库。
欢迎反馈,评论和贡献!
总览
图书馆特色
- 计算字符串,单个文本文件或包含文本文件的目录树中的所有单词。
- 分析单词计数以找到前N个最常用的单词,后N个最不常用的单词或总单词数。
- 通过外部谓词指定字符是否为文字字符。
- 指定要对单词执行的可选操作,例如通过外部运算符转换为小写字母。
- 在非并行和并行实现之间进行选择,以比较其性能。
- 如果需要,将并行度指定为与内核数不同的值。
编程要点
- 使用生产者/使用者来读取文件并并行计算每个文件中的单词。 实际的机制封装在通用的可重用实现中。
- 使用Fork / Join对字数进行分析。 这里,实际的机制再次封装在通用的可重用实现中。
- 使用NIO.2遍历目录树和读取文件。
- 大量使用函数接口和lambda表达式 ,以便在适当的地方传递函数而不是数据。
- 有两个最重要的类的综合单元测试和性能测试。
- 像往常一样,代码干净,结构合理且易于阅读。 格式,命名和注释是统一且一致的。 适当地使用了面向对象和功能编程技术已引起了很多关注。
命令行界面
要启动命令行程序,请执行以下命令:
java -jar wordcounter-1.0.4.jar <options>
所有选项都有合理的默认值,因此都不是必需的。 对所有选项使用默认值会导致在当前目录及其子目录中找到前10个最常用的单词。 指定非默认值允许指定不同的目录,分析模式,单词字符,单词数和并行度,以及忽略大小写或使用串行而不是并行计算,例如:
在“单词”目录中找到最常用的10个单词-p words
在目录“ wordsx”中查找前5个最不常用的单词,并将数字视为单词字符,忽略大小写,并进行信息记录-p wordsx -m bottom -d 1234567890 -i -n 5 -l info
有关命令行界面选项的更多信息,请参见自述文件中的命令行界面 。
设计
库的设计将问题划分为通用并行处理实用程序,封装用于表示原始字数和排序字数的数据结构的类,最后是使用前两组功能执行计数和分析的类。 实际上,所有这些类都大量使用功能接口的实例,以便允许对其通用行为进行特定的自定义。 这导致代码中大量注入了lambda表达式和方法引用。 欢迎来到Java函数编程的世界!
通用并行处理实用程序
ForkJoinComputer类
ForkJoinComputer<T>
类是通用的Fork / Join计算机。 它将初始大小除以2,直到达到指定的并行度或低于指定的阈值,然后使用指定的Computer<T>
串行计算每个部分,然后使用指定的Merger<T>
将所有计算的结果Merger<T>
。 此处,计算机和合并是定义如下的功能接口:
public interface Computer<T> {T compute(int lo, int hi);
}public interface Merger<T> {T merge(T result1, T result2);
}
可以通过简单地使用适当的lambda实例化该类,然后调用其compute
方法来使用此类。
new ForkJoinComputer<Integer>(n, 1000,(lo, hi) -> { int sum = 0; for (int i = lo + 1; i <= hi; i++) sum += i; return sum; },(a, b) -> a + b).compute();
ProducerConsumerExecutor类
ProducerConsumerExecutor<T1, T2>
类是通用的Producer / Consumer执行程序。 它启动一个Producer<T1>
任务和多个Mediator<T1, T2>
和Consumer<T2>
任务,它们的数量等于指定的并行度。 生产者将T1
实例放入BlockingQueue<T1>
。 中介者从那里获取这些实例,将其转换为T2
,并将其放入另一个类型为BlockingQueue<T2>
阻塞队列中。 最后,使用者从第二个阻塞队列中获取T2
实例并对其进行处理。
这里, Producer, Consumer
和Mediator
是功能接口,定义如下:
public interface Producer<T> {void produce(Block<T> block);
}public interface Consumer<T> {void consume(T t);
}public interface Mediator<T1, T2> {void mediate(T1 t, Block<T2> block);
}
在上面的代码中, Block
是java.util.functions
定义的标准函数。 传递给Producer
和Mediator
方法的块将产生的数据放入相应的阻塞队列中。
与ForkJoinComputer
相似,可以通过在适当的lambda上实例化该类然后调用其execute
方法来使用此类。
数据结构类
这些类封装了用于表示原始和已排序字数的数据结构。
- WordCounts类表示映射到其使用计数的单词列表。
-
TopWordCounts
类表示映射到具有此类计数的所有单词的单词使用情况计数的排序列表。
字数统计和分析类
WordCounter类
WordCounter
类提供了一种方法,用于以串行或并行方式对表示文件或目录树的Path
单词进行计数。 通过使用适当的lambda实例化它,然后调用其count
方法,可以使用它:
// Count all words consisting of only alphabetic chars, ignoring case, using parallel processing
WordCounts wc = new WordCounter(path, (c) -> Character.isAlphabetic(c), (s) -> s.toLowerCase(), true).count();
并行实现使用ProducerConsumerExecutor<Path, String>
。 生产者只需遍历目录树并产生Path
实例。 中介者将文件读入文本片段,而使用者则对每个文本片段中的单词进行计数并将它们收集在单个WordCounts
实例中。 这是通过以下代码完成的:
private WordCounts countPar() {final WordCounts wc = new WordCounts(parLevel);new ProducerConsumerExecutor<Path, String>((block) -> collectPaths(block),(file, block) -> readFileToBlock(file, block),(text) -> wc.add(countWords(text, pred, op)), parLevel).execute();return wc;
}
WordCountAnalyzer类
WordCountAnalyzer
类提供了对WordCounter
产生的字数进行分析的方法,例如查找前N个最常用的字。 也可以通过简单地实例化它,然后调用其方法之一(例如findTop
或total
来使用它:
// Find the top 10 most used words in wc
TopWordCounts twc = new WordCountAnalyzer(wc, true).findTop(10, (x, y) -> (y - x));
Differentnet分析类型实现内部Analysis<T>
接口,该接口定义如下:
interface Analysis<T> {T compute(int lo, int hi);T merge(T r1, T r2);
}
由于以上两种方法的签名模仿了ForkJoinComputer
使用的Computer
和Merger
功能接口,因此我们可以通过以下方式对所有分析类型使用fork / join:
public TopWordCounts findTop(int number, Comparator<Integer> comparator) {return analyse(new FindTopAnalysis(number, comparator));
}private <T> T analyse(Analysis<T> a) {if (par) {return new ForkJoinComputer<T>(wc.getSize(), THRESHOLD, a::compute, a::merge, parLevel).compute();} else {return a.compute(0, wc.getSize());}
}
有关库设计的更多信息,请参见自述文件中的设计。
性能
我发现并行的Producer / Consumer字数统计实现很好地适应了不同数量的内核和I / O速度。 它比串行实现要快得多。 与之不同的是,当使用不切实际的大量唯一单词进行测试时,并行的Fork / Join分析实现仅比串行的快,并且程度适中。 由于唯一字的数量很少,因此实际上比串行字慢。
下表比较了单词计数的性能,并在以下条件下找到了最佳分析:
- CPU AMD Phenom II X4 965 3.4 GHz(4核),4 GB RAM,Windows 7,JDK 8
- 默认选项:由字母字符组成的单词,区分大小写
- 默认并行度,等于内核数
字数统计性能
实作 | 档案 | 话 | 大小(MB) | 时间(毫秒) |
---|---|---|---|---|
序列号 | 1个 | 10000000 | 〜65 | 2200-2400 |
平行 | 1个 | 10000000 | 〜65 | 500-600 |
序列号 | 100 | 10000000 | 〜65 | 1600-1800 |
平行 | 100 | 10000000 | 〜65 | 500-600 |
查找最佳分析性能
实作 | 话 | 最大计数 | 最佳 | 时间(毫秒) |
---|---|---|---|---|
序列号 | 2000000 | 10000000 | 10 | 200-250 |
平行 | 2000000 | 10000000 | 10 | 200-250 |
玩代码
如果您想使用这些代码,我建议您使用最新的NetBeans 7.3 beta,在撰写本文时为NetBeans IDE 7.3 Beta 2 。 请注意,即使在此版本中,也无法在IDE中编译lambda,因此周围到处都有红色标记。 但是,从IDE启动Maven构建并运行测试仍然可以正常工作。 根据此博客文章 ,应该可以对lambda使用IntelliJ IDEA 12 EAP内部版本122.202或更高版本,但是我没有亲自尝试过。 我确实尝试了Eclipse,但由于Eclipse使用了自己的对lambda不了解的JDT编译器,因此发现它是一场失败的比赛。
结论
这是我第一次接触Java函数编程。 尽管Java仍然不是Scala,但是与我以前的Java代码相比,新的函数式编程结构大大改变了我设计和实现Wordcounter的方式。 我发现这种新的编程风格是强大而富有表现力的,并且我相信随着Java 8的发布,它将很快成为主流。
对我来说,这也是最后的“怪异敏捷”任务。 评审团慷慨地授予了我的意见书,甚至在比赛结束之前,我就找到了自己的优胜者。
如果这篇文章引起了您的兴趣,请随时下载并浏览Wordcounter,用它来学习新的Java函数编程构造,并让我知道我是否可以在此过程中为您提供帮助。
参考: Wordcounter,来自JCG合作伙伴 Stoyan Rachev的Lambdas和Fork / Join在Java中计算单词数,网址 为Stoyan Rachev博客。
翻译自: https://www.javacodegeeks.com/2012/12/wordcounter-counting-words-in-java-with-lambdas-and-forkjoin.html