В эти дни я выпустил Wordcounter , библиотеку Java и утилиту командной строки для подсчета слов в текстовых файлах и выполнения анализа количества слов, в котором интенсивно используются конструкции функционального программирования и подходы параллельных вычислений. Это моя четвертая запись для конкурса «Geeky Quickies» в SAP , после Feeder , Todor и Hanoier .
Библиотека использует лямбды JDK 8, а также новые функции JDK 7, такие как Fork / Join и NIO.2 . Он построен и может использоваться только с ранней версией JDK 8 с поддержкой лямбды .
С появлением лямбд и их вспомогательных функций в JDK 8 способ, которым мы создаем программное обеспечение на Java, изменится. Если вы хотите получить представление о том, как может выглядеть ваш Java-код через несколько лет, вы можете взглянуть на Wordcounter. В отличие от большинства доступных на данный момент ресурсов, это не простой учебник, а реально работающий проект.
Конкурсное задание попросило реализовать алгоритм с использованием Fork / Join и lambdas, который анализирует все файлы в каталоге и находит десять наиболее часто используемых слов в файлах вместе с тем, сколько раз они встречаются. Вместо того, чтобы просто придерживаться Fork / Join, я попытался найти наиболее подходящий параллельный подход для этой конкретной задачи, что привело меня к выбору Producer / Consumer для основной логики подсчета слов.
Вы можете изучить исходный код на GitHub . Существует также довольно полный README, который предоставляет более подробную документацию.
Последние бинарные, javadoc и исходные пакеты можно найти в разделе загрузок GitHub. Если будет достаточно интереса, я опубликую Javadoc онлайн и сделаю библиотеку доступной также в центральных репозиториях Maven.
Отзывы, комментарии и комментарии приветствуются!
обзор
Особенности библиотеки
- Подсчитайте все слова в строке, отдельном текстовом файле или дереве каталогов, содержащем текстовые файлы.
- Проанализируйте количество слов, чтобы найти первые N наиболее употребляемых слов, нижние N наименее употребляемых слов или общее количество слов.
- Укажите, является ли символ символом слова с помощью внешнего предиката.
- Укажите необязательную операцию, выполняемую над словами, например, преобразование в нижний регистр через внешнего оператора.
- Выберите между непараллельными и параллельными реализациями, чтобы сравнить их производительность.
- Если нужно, укажите уровень параллелизма, который будет отличаться от количества ядер.
Основные моменты программирования
- Использует Producer / Consumer для чтения файлов и параллельного подсчета слов в каждом файле. Фактический механизм инкапсулирован в общей, многократно используемой реализации.
- Использует Fork / Join для выполнения анализа количества слов. Здесь снова фактический механизм заключен в общую, многократно используемую реализацию.
- Использует NIO.2 для обхода каталогов и чтения файлов.
- Интенсивно использует функциональные интерфейсы и лямбда-выражения для передачи функций, а не данных, где это уместно.
- Для двух наиболее важных классов существуют комплексные тесты модулей и производительности.
- Как обычно, код чистый, хорошо структурированный и легко читаемый. Форматирование, наименование и комментарии единообразны и последовательны. Большое внимание было уделено надлежащему использованию как методов объектно-ориентированного, так и функционального программирования.
Интерфейс командной строки
Чтобы запустить программу командной строки, выполните следующую команду:
1
|
java -jar wordcounter- 1.0 . 4 .jar <options> |
Все параметры имеют разумные значения по умолчанию, поэтому ни один из них не является обязательным. Использование значений по умолчанию для всех параметров приводит к нахождению 10 самых популярных слов в текущем каталоге и его подкаталогах. Задание значений не по умолчанию позволяет указывать разные каталоги, режимы анализа, символы слов, количество слов и уровень параллелизма, а также игнорировать регистр или использовать последовательные, а не параллельные вычисления, например:
Найдите 10 самых популярных слов в каталоге «words»: -p words
Найдите нижние 5 наименее употребляемых слов в каталоге «wordsx», считая числа как символы слова, игнорируя регистр, с регистрацией информации: -p wordsx -m bottom -d 1234567890 -i -n 5 -l info
Для получения дополнительной информации об опциях интерфейса командной строки см. Интерфейс командной строки в README.
дизайн
Проект библиотеки разделяет проблему на универсальные утилиты параллельной обработки, классы, которые инкапсулируют структуры данных, используемые для представления необработанного и отсортированного количества слов, и, наконец, классы, которые выполняют подсчет и анализ с использованием возможностей предыдущих двух групп. Практически все эти классы довольно часто используют экземпляры функциональных интерфейсов, чтобы позволить конкретным настройкам их общего поведения. В результате получается код, который сильно насыщен лямбда-выражениями и ссылками на методы. Добро пожаловать в мир функционального программирования на Java!
Универсальные утилиты параллельной обработки
Класс ForkJoinComputer
Класс ForkJoinComputer<T>
является универсальным компьютером Fork / Join. Он делит исходный размер на 2, пока либо не достигнет указанного уровня параллелизма, либо не опустится ниже указанного порогового значения, последовательно вычислит каждую часть с использованием указанного Computer<T>
, а затем объединит результаты всех вычислений с использованием указанного Merger<T>
. Здесь Computer и Merger являются функциональными интерфейсами, которые определены следующим образом:
1
2
3
4
5
6
7
|
public interface Computer<T> { T compute( int lo, int hi); } public interface Merger<T> { T merge(T result1, T result2); } |
Этот класс можно использовать, просто создав его экземпляр с помощью соответствующих лямбд, а затем вызвав его метод compute
.
1
2
3
|
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
являются функциональными интерфейсами, которые определены следующим образом:
01
02
03
04
05
06
07
08
09
10
11
|
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
, этот класс можно использовать, просто создав его экземпляр с помощью соответствующих лямбд, а затем вызвав его метод execute
.
Классы структуры данных
Эти классы инкапсулируют структуры данных, используемые для представления необработанного и отсортированного количества слов.
- Класс WordCounts представляет собой список слов, сопоставленных с их количеством использования.
- Класс
TopWordCounts
представляет отсортированный список счетчиков использования слов, сопоставленных со всеми словами, имеющими такие счетчики.
Классы подсчета и анализа слов
Класс WordCounter
Класс WordCounter
предоставляет метод для подсчета слов в Path
представляющих файл или дерево каталогов, последовательно или параллельно. Его можно использовать, просто создав его экземпляр с помощью соответствующих лямбд, а затем вызвав метод count
:
1
2
|
// 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
. Это делается с помощью следующего фрагмента кода:
1
2
3
4
5
6
7
8
|
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
:
1
2
|
// Find the top 10 most used words in wc TopWordCounts twc = new WordCountAnalyzer(wc, true ).findTop( 10 , (x, y) -> (y - x)); |
Типы анализа difnet реализуют внутренний интерфейс Analysis<T>
, который определяется следующим образом:
1
2
3
4
|
interface Analysis<T> { T compute( int lo, int hi); T merge(T r1, T r2); } |
Поскольку сигнатуры вышеупомянутых двух методов имитируют функциональные интерфейсы Computer
и Merger
используемые ForkJoinComputer
, мы можем использовать fork / join для всех типов анализа следующим образом:
01
02
03
04
05
06
07
08
09
10
11
|
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()); } } |
Для получения дополнительной информации о дизайне библиотеки см. Дизайн в README.
Производительность
Я обнаружил, что параллельная реализация подсчета слов «производитель / потребитель» прекрасно адаптируется к разному количеству ядер и скоростям ввода / вывода. Это значительно быстрее, чем последовательная реализация. В отличие от этого, параллельная реализация анализа Fork / Join только быстрее, чем последовательная, при тестировании с нереально большим количеством уникальных слов и только в незначительной степени. С небольшим количеством уникальных слов, это на самом деле медленнее, чем серийное.
В приведенных ниже таблицах сравнивается эффективность подсчета слов и проводится лучший анализ при следующих условиях:
- Процессор AMD Phenom II X4 965 3,4 ГГц (4 ядра), 4 ГБ ОЗУ, Windows 7, JDK 8
- Параметры по умолчанию: слова, состоящие из буквенных символов, с учетом регистра
- Уровень параллелизма по умолчанию, равный количеству ядер
Производительность подсчета слов
Реализация | файлы | слова | Размер (МБ) | Время (мс) |
---|---|---|---|---|
последовательный | 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, которая на момент написания статьи была IDE NetBeans 7.3 Beta 2 . Обратите внимание, что даже в этой версии невозможно скомпилировать лямбды в IDE, поэтому у вас есть красные метки вокруг. Однако запуск сборки Maven и запуск тестов из IDE по-прежнему работает нормально. Согласно этому сообщению в блоге , должно быть возможно использовать IntelliJ IDEA 12 EAP build 122.202 или более поздней версии с лямбдами, однако я сам не пробовал этого. Я попробовал Eclipse и обнаружил, что это проигранная игра, так как Eclipse использует собственный JDT-компилятор, который не знает лямбда-выражения.
Вывод
Это была моя первая значительная встреча с функциональным программированием на Java. Хотя Java все еще не Scala, новые функциональные программные конструкции кардинально изменили способ, которым я проектировал и реализовал Wordcounter, по сравнению с моим предыдущим кодом Java. Я считаю, что этот новый стиль программирования является мощным и выразительным, и я верю, что с выпуском Java 8 он быстро станет доминирующим.
Это было также последним заданием «Geeky Quickies» для меня. Жюри присудило мои материалы достаточно щедро, и я нашел себя победителем еще до окончания конкурса.
Если этот пост вызвал у вас интерес, не стесняйтесь скачать и изучить Wordcounter, использовать его для изучения новых конструкций функционального программирования на Java и сообщить мне, могу ли я помочь вам в этом путешествии.
Ссылка: Wordcounter, Подсчет слов в Java с помощью Lambdas и Fork / Join от нашего партнера JCG Стояна Рачева в блоге Стояна Рачева .