Это то, для чего была создана Java.

Любой, кто знает меня достаточно хорошо в моей жизни программирования, знает, что я не пристрастен к Java.

Я в первую очередь разработчик JavaScript. Это было то, что я узнал в первую очередь, это сбивало меня с толку, а затем порадовало меня после того, как я начал разбираться в этом, и это имело для меня гораздо больше смысла, чем Java, с ее компиляцией, с ее необходимостью объявлять каждый отдельный тип переменной ( да, я знаю, что последние версии Java отказались от этого требования для некоторых из более простых выводов) и его огромных библиотек карт, списков, коллекций и т.д .: HashMaps, Maps, HashTables, TreeMaps, ArrayLists, LinkedLists, Arrays , это продолжается и продолжается.

При этом я стараюсь лучше разбираться в Java и других недостатках, которые у меня есть, потому что у меня нет традиционной степени в области информатики. Если вы хотите узнать больше о моем нетипичном пути к тому, чтобы стать инженером-программистом, вы можете прочитать мою первую запись в блоге здесь.

Поэтому, когда я рассказал своему коллеге о проблеме кодирования, с которой я столкнулся пару месяцев назад, и о том, как я решил ее (а затем производительность протестировал свои различные решения) в JavaScript, он оглянулся на меня и сказал: Как бы вы решили эту проблему с помощью Java?

Я смотрел на него, когда в моей голове начали крутиться колеса, и я тоже принял вызов найти оптимальное решение на Java.

Итак, прежде чем я перейду к тому, как я решил свою задачу с помощью Java, позвольте мне на самом деле резюмировать требования.

Вызов, с которым я столкнулся

Как я сказал в своей исходной статье, при использовании Node.js для чтения действительно очень больших файлов это была проблема кодирования, брошенная другому разработчику для страховой технологической компании.

Задача была достаточно простой: загрузите этот большой zip-файл с текстом из Федеральной избирательной комиссии, прочтите эти данные из предоставленного файла .txt и предоставьте следующую информацию:

  • Напишите программу, которая распечатает общее количество строк в файле.
  • Обратите внимание, что восьмой столбец содержит имя человека. Напишите программу, которая загружает эти данные и создает массив со всеми строками имен. Распечатайте 432-е и 43243-е имена.
  • Обратите внимание, что 5-й столбец содержит дату. Подсчитайте, сколько пожертвований было сделано за каждый месяц, и распечатайте результаты.
  • Обратите внимание, что восьмой столбец содержит имя человека. Создайте массив с каждым именем. Определите, какое имя наиболее часто встречается в данных и сколько раз оно встречается.

Ссылка на данные: «https://www.fec.gov/files/bulk-downloads/2018/indiv18.zip Фак*

Примечание. Я добавляю звездочку после ссылки на файл, потому что другие, которые решили взять на себя эту задачу, на самом деле заметили увеличение размера файла с тех пор, как я загрузил его в начале октября 2018 года. По последним подсчетам , кто-то упомянул, что сейчас это было до 3,5 ГБ, поэтому кажется, что эти данные все еще живы и постоянно добавляются. Я считаю, что решения, которые я представляю ниже, по-прежнему будут работать, но ваши подсчеты и цифры будут отличаться от моих.

Мне понравилась задача, и я хотел попрактиковаться в манипулировании файлами, поэтому решил посмотреть, смогу ли я в ней разобраться. Теперь, без лишних слов, давайте поговорим о некоторых различных решениях, которые я придумал для чтения действительно очень больших файлов на Java.

Три решения Java, которые я придумал

Java уже давно является стандартным языком программирования с возможностями обработки файлов, и поэтому появилось множество постоянно улучшающихся способов чтения, записи и управления файлами с его помощью.

Некоторые методы встроены прямо в базовую структуру Java, а некоторые по-прежнему являются независимыми библиотеками, которые необходимо импортировать и объединить вместе для запуска. Но, тем не менее, я придумал три разных метода чтения файлов на Java, а затем я проверил их производительность, чтобы увидеть, какие методы более эффективны.

Ниже приведены скриншоты всего моего кода в формате Polacode вместе с фрагментами различных фрагментов кода. Если вы хотите увидеть весь исходный код, вы можете получить доступ к моему репозиторию Github здесь.

Вы заметите, что я использовал одну и ту же логику кода для извлечения данных из каждого файла, основные различия заключаются между начальным подключением к файлу и анализом текста. Я сделал это, чтобы иметь более точное представление о том, как разные методы сочетаются друг с другом для оценки производительности. Сравнение яблок с яблоками и все такое.

Реализация Java FileInputStream () и Scanner ()

Первое решение, которое я придумал, использует встроенный в Java метод FileInputStream() в сочетании с Scanner().

По сути, FileInputStream просто открывает соединение с файлом для чтения, будь то изображения, символы и т. Д. Его не особо заботит, что это за файл на самом деле, потому что Java считывает входной поток как необработанные байты данных. Другой вариант (по крайней мере, для моего случая) - использовать FileReader(), который предназначен специально для чтения потоков символов, но я выбрал FileInputStream() для этого конкретного сценария. Я использовал FileReader() в другом решении, которое тестировал позже

Как только соединение с файлом установлено, в игру вступает Scanner, который фактически анализирует текстовые байты на строки читаемых данных. Scanner() разбивает свои входные данные на токены, используя шаблон разделителя, который по умолчанию соответствует пробелам (но также может быть переопределен для использования регулярного выражения или других значений). Затем, используя логическое значение Scanner.hasNextLine() и метод Scanner.nextLine(), я могу построчно читать содержимое текстового файла и извлекать нужные мне фрагменты данных.

Scanner.nextLine() фактически продвигает этот сканер за пределы текущей строки и возвращает пропущенный ввод. Таким образом я могу собирать необходимую информацию из каждой строки, пока не закончатся строки для чтения, а Scanner.hasNextLine() вернет false и цикл while завершится.

Вот пример кода с использованиемFileInputStream() и Scanner().

File f = new File("src/main/resources/config/test.txt");

try {
   FileInputStream inputStream = new FileInputStream(f);
   Scanner sc = new Scanner(inputStream, "UTF-8");
   do some things ...
   while (sc.hasNextLine()) {
      String line = sc.nextLine();
      do some more things ...
   } 
   do some final things
}

А вот мой полный код для решения всех задач, поставленных выше.

После того, как данные файла считываются по одной строке за раз, остается просто получить необходимые данные и манипулировать ими в соответствии с моими потребностями.

Задача 1. Получите общее количество строк в файле

Было легко подсчитать количество строк для всего файла. Все, что было задействовано, это новый int lines = 0, объявленный вне цикла while, который я увеличивал каждый раз, когда цикл запускался снова. Запрос №1: выполнено.

Задача 2. Создайте список всех имен и найдите 432-е и 43243-е имена

Второй запрос, который заключался в том, чтобы собрать все имена и распечатать 432-е и 43243-е имена из массива, потребовал от меня создать ArrayList<String> names = new ArrayList<>(); и ArrayList<Integers> indexes = new ArrayList<>();, которым я незамедлительно добавил индексы 432 и 43243 с indexes.add(433) и indexes.add(43244) соответственно.

Мне пришлось добавить 1 к каждому индексу, чтобы получить правильную позицию имени в массиве, потому что я увеличил количество строк (начиная с 0), как только Scanner.hasNextLine() вернул истину. После того, как Scanner.nextLine() вернул содержимое предыдущей строки, я мог вытащить нужные мне имена, что означало, что его истинный индекс (начиная с индекса 0) на самом деле был индексом количества строк минус один. (Поверьте мне, я трижды проверил это, чтобы убедиться, что правильно делаю математику).

Я использовал ArrayList для имен, потому что он поддерживает порядок вставки элементов, что означает, что при отображении элементов ArrayList набор результатов всегда будет иметь тот же порядок, в котором элементы были вставлены в список. Поскольку я просматриваю файл построчно, элементы всегда будут вставляться в список в одном и том же порядке.

Вот полная логика, которую я использовал, чтобы получить все имена, а затем распечатать имена, если индексы, которые у меня были в моем indexes ArrayList, совпадали с индексом lines count.

int lines = 0;
ArrayList<String> names = new ArrayList<>();

// get the 432nd and 43243 names
ArrayList<Integer> indexes = new ArrayList<>();

indexes.add(433);
indexes.add(43244);

System.out.println("Reading file using File Input Stream");

while (sc.hasNextLine()) {
   String line = sc.nextLine();
   lines++;
   // get all the names
   String array1[] = line.split("\\s*\\|\\s*");
   String name = array1[7];
   names.add(name);
   if (indexes.contains(lines)) {
      System.out.println("Name: " + names.get(lines - 1) + " at
      index: " + (lines - 1));
   } 
   ...
}

Запрос №2: выполнено.

Задача 3. Подсчитайте, сколько пожертвований было сделано за каждый месяц

Когда я подошел к запросу на подсчет пожертвований, я хотел больше, чем подсчитывать пожертвования по месяцам, я хотел подсчитывать их по месяцам и годам, так как у меня были пожертвования как с 2017, так и с 2018 года.

Самое первое, что я сделал, - это настроил начальный ArrayList для хранения всех моих дат: ArrayList<String> dates = new ArrayList<>();

Затем я взял 5-й элемент в каждой строке, необработанную дату, и использовал метод substring(), чтобы выделить только месяц и год для каждого пожертвования.

Я переформатировал каждую дату в более удобные для чтения даты и добавил их в новый dates ArrayList.

String rawDate = array1[4];
String month = rawDate.substring(4, 6);
String year = rawDate.substring(0, 4);
String formattedDate = month + "-" + year;
dates.add(formattedDate);

После того, как я собрал все даты, я создал HashMap для хранения моих дат: HashMap<String, Integer> dateMap = new HashMap<>();, а затем перебрал список dates, чтобы либо добавить даты в качестве ключей к HashMap, если они еще не существуют, либо увеличить их количество значений, если бы они действительно существовали.

После создания HashMap я прогнал эту новую карту через еще один цикл for, чтобы получить ключ и значение каждого объекта для вывода на консоль. Вуаля.

Результаты дат не были отсортированы в каком-либо конкретном порядке, но они могли быть преобразованы обратно в HashMap в ArrayList или LinkedList, если это необходимо. Я решил не делать этого, потому что это не было обязательным требованием.

HashMap<String, Integer> dateMap = new HashMap<>();
for (String date : dates) {
   Integer count = dateMap.get(date);
   if (count == null) {
      dateMap.put(date, 1);
   } else {
      dateMap.put(date, count + 1);
   }
}
for (Map.Entry<String, Integer> entry : dateMap.entrySet()) {
   String key = entry.getKey();
   Integer value = entry.getValue();
   System.out.println("Donations per month and year: " +
   entry.getKey() + " and donation count: " + entry.getValue());

}

Запрос №3: выполнено.

Задача 4. Определите наиболее распространенное имя и его частоту

Четвертый запрос, получить только все имена и найти количество наиболее часто встречающихся имен, был самым сложным.

Мне потребовалось сначала проверить, содержит ли массив names запятую (были некоторые названия компаний без запятых), затем split() имя на запятой и trim() любые лишние пробелы в нем.

Как только это было очищено, мне нужно было проверить, есть ли в первой половине имени какие-либо пробелы (что означает, что у человека есть имя и отчество или, возможно, прозвище, например, «Мисс»), и если да, split() это снова , и trim() вверх первый элемент вновь созданного массива (который, как я предполагал, почти всегда будет первым именем).

Если в первой половине имени не было пробела, оно добавлялось в firstNames ArrayList как есть. Вот так я собрал все имена из файла. См. Фрагмент кода ниже.

// count the occurrences of first name
ArrayList<String> firstNames = new ArrayList<>();

System.out.println("Reading file using File Input Stream");

    while (sc.hasNextLine()) {
        String line = sc.nextLine();

        // get all the names
        String array1[] = line.split("\\s*\\|\\s*");
        String name = array1[7];
        names.add(name);

        if (name.contains(", ")) {
            String array2[] = (name.split(", "));
            String firstHalfOfName = array2[1].trim();
  
            if (firstHalfOfName != undefined ||
                !firstHalfOfName.isEmpty()) {
                     if (firstHalfOfName.contains(" ")) {
                       String array3[] = firstHalfOfName.split(" ");
                       String firstName = array3[0].trim();
                       firstNames.add(firstName);
                     } else {
                        firstNames.add(firstHalfOfName);
                     }
                  }
               }

После того, как я собрал все возможные имена и цикл while чтения файла закончился, пора отсортировать имена и найти наиболее распространенное.

Для этого я создал еще одну новую HashMap: HashMap<String, Integer> map = new HashMap<>();, затем перебрал все имена, и если имя еще не существовало на карте, оно было создано как ключ карты, а значение было установлено как 1. Если имя уже существует в HashMap значение было увеличено на 1.

HashMap<String, Integer> map = new HashMap<>();
for (String name : firstNames) {
   Integer count = map.get(name);
   if (count == null) {
      map.put(name, 1);
   } else {
      map.put(name, count + 1);
   }
}

Но подождите - это еще не все! Как только у нас есть HashMap, который по своей природе неупорядочен, его необходимо отсортировать от наибольшего к наименьшему значению, чтобы получить наиболее часто встречающееся имя, поэтому я преобразовываю каждую запись в HashMap в LinkedList, который можно упорядочить и повторить через .

LinkedList<Entry<String, Integer>> list = new LinkedList<>(map.entrySet());

И, наконец, список сортируется с использованием метода Collections.sort() и вызова Comparator interface для сортировки объектов имен в соответствии с их количеством значений в порядке убывания (наивысшее значение - первое). Проверь это.

Collections.sort(list, new Comparator<Map.Entry<String, Integer>>()
  {
     public int compare(Map.Entry<String, Integer> o1,
     Map.Entry<String, Integer> o2) {
         return (o2.getValue()).compareTo(o1.getValue());
       }
  });

Как только все это будет сделано, первая пара значений ключа LinkedList может быть наконец извлечена и показана пользователю. Вот весь набор, который складывается после того, как все имена были прочитаны из файла.

HashMap<String, Integer> map = new HashMap<>();
for (String name : firstNames) {
   Integer count = map.get(name);
   if (count == null) {
      map.put(name, 1);
   } else {
      map.put(name, count + 1);
   }
}

LinkedList<Entry<String, Integer>> list = new LinkedList<>(map.entrySet());

Collections.sort(list, new Comparator<Map.Entry<String, Integer>>()
  {
     public int compare(Map.Entry<String, Integer> o1,
     Map.Entry<String, Integer> o2) {
         return (o2.getValue()).compareTo(o1.getValue());
       }
  });
System.out.println("The most common first name is: " + list.get(0).getKey() + " and it occurs: " + list.get(0).getValue() + " times.");

Запрос №4 (и, пожалуй, самая сложная из всех задач): выполнено.

Отлично, теперь, когда я дал вам монолог логики моего мозга на Java, я могу дать вам гораздо более быстрый обзор двух других методов, которые я пробовал для чтения текстовых данных из файлов. (Потому что логическая часть кода точно такая же.)

Реализация Java BufferedReader () и FileReader ()

Мое второе решение включало еще два основных метода Java: BufferedReader() и FileReader().

BufferedReader считывает текст из потока ввода символов, буферизует символы, чтобы обеспечить эффективное чтение символов, массивов и строк, и оборачивается вокруг метода FileReader, который является фактическим методом чтения указанного текстового файла. BufferedReader делает FileReader более эффективным в работе, вот и все.

Метод BufferedReader readLine() - это то, что фактически считывает каждую строку текста по мере его чтения из потока, что позволяет нам извлекать необходимые данные.

Настройка аналогична FileInputStream и Scanner; вы можете увидеть, как реализовать BufferedReader и FileReader ниже.

File f = new File("src/main/resources/config/test.txt");

try (BufferedReader b = new BufferedReader(new FileReader(f))) { 
   String readLine = "";
   do some things ...
   while ((readLine = b.readLine()) != null) { 
     do some more things...
   } 
do some final things
}

А вот мой полный код с использованием BufferedReader() и FileReader().

Однако, за исключением реализации BufferedReader и FileReader, вся внутренняя логика одинакова, поэтому я перейду к моей последней реализации чтения файлов Java: FileUtils.LineIterator.

Реализация Apache Commons IO FileUtils.LineIterator ()

Последнее решение, которое я придумал, включает библиотеку, созданную Apache, под названием FileUtils.LineIterator(). Включить зависимость достаточно просто. Я использовал Gradle для своего проекта Java, поэтому все, что мне нужно было сделать, это включить библиотеку commons-io в мой файл build.gradle.

LineIterator делает именно то, что предполагает его название: он содержит ссылку на открытый Reader (например, FileReader в моем последнем решении) и выполняет итерацию по каждой строке в файле. Во-первых, настроить LineIterator очень просто.

LineIterator имеет встроенный метод nextLine(), который фактически возвращает следующую строку в обернутом модуле чтения (в отличие от метода nextLine() Scanner или метода readLine() BufferedReader).

Вот код для настройки FileUtils.LineIterator() после включения библиотеки зависимостей.

File f = new File("src/main/resources/config/test.txt");

try {
   LineIterator it = FileUtils.lineIterator(f, "UTF-8"); 
   do some things ...
   while (it.hasNext()) {
      String line = it.nextLine(); 
      do some other things ...
   }
   do some final things
}

А вот мой полный код с использованием FileUtils.LineIterator().

Примечание. Есть одна вещь, о которой вам нужно знать, если вы запускаете простое приложение Java без помощи Spring Boot. Если вы хотите использовать эту дополнительную библиотеку зависимостей Apache, вам необходимо вручную связать ее с файлом JAR вашего приложения в так называемый толстый JAR.

Толстая банка (также известная как uber jar) - это самодостаточный архив, содержащий классы и зависимости, необходимые для запуска приложения.

Spring Boot «автоматически» объединяет все наши зависимости вместе для нас, но он также имеет много дополнительных накладных расходов и функций, которые совершенно не нужны для этого проекта, поэтому я решил не использовать его. Это делает проект излишне тяжелым.

Сейчас доступны плагины, но мне просто нужен был быстрый и простой способ связать мою одну зависимость с моим JAR. Поэтому я изменил задачу jar из подключаемого модуля Java Gradle. По умолчанию эта задача создает jar-файлы без каких-либо зависимостей.

Я могу изменить это поведение, добавив несколько строк кода. Для работы мне нужны две вещи:

  • атрибут Main-Class в файле манифеста (проверьте, у меня было три файла основных классов в моем демонстрационном репозитории для целей тестирования)
  • и любые jar-файлы с зависимостями

Спасибо Baeldung за помощь в приготовлении этой толстой банки.

После того, как основной файл класса был определен (и в моем демонстрационном репозитории я сделал три основных файла классов, которые до бесконечности запутали мою IDE) и зависимости были включены, вы можете запустить команду ./gradlew assemble из терминала, а затем:

java -cp ./build/libs/readFileJava-0.0.1-SNAPSHOT.jar com.example.readFile.readFileJava.ReadFileJavaApplicationLineIterator

И ваша программа должна работать с включенной библиотекой LineIterator.

Если вы используете IntelliJ в качестве IDE, вы также можете просто использовать его конфигурации запуска локально с каждым из основных файлов, указанным в качестве правильного основного класса, и он также должен запускать три программы. См. Мой README.md для получения дополнительной информации об этом.

Отлично, теперь у меня есть три разных способа чтения и обработки больших текстовых файлов на Java, моя следующая задача: выяснить, какой из них более производительный.

Как я оценивал их работу и результаты

Для тестирования производительности моих различных приложений Java и функций внутри них я нашел две удобные, готовые функции в Java 8: Instant.now() и Duration.between().

Я хотел посмотреть, есть ли какие-нибудь измеримые различия между разными способами чтения одного и того же файла. Поэтому, помимо различных параметров чтения файлов: FileInputStream, BufferedReader и LineIterator, я старался сохранить код (и временные метки, отмечающие начало и остановку каждой функции) как можно более похожими. И я думаю, что это неплохо сработало.

Instant.now()

Instant.now () делает именно то, что предполагает его название: он удерживает единственную мгновенную точку на временной шкале, сохраненную как long, представляющую эпоху-секунды, и int, представляющую наносекунду-секунды. Сам по себе это не так уж и полезно, но в сочетании с Duration.between () становится очень полезным.

Duration.between()

Duration.between () берет начальный и конечный интервалы и находит продолжительность между этими двумя временами. Вот и все. И это время может быть преобразовано во всевозможные читаемые форматы: миллисекунды, секунды, минуты, часы и т. Д.

Вот пример реализации Instant.now () и Duration.between () в моих файлах. Это время, необходимое для подсчета количества строк в общем файле.

try {
         LineIterator it = FileUtils.lineIterator(f, "UTF-8");

         // get total line count
         Instant lineCountStart = Instant.now();
         int lines = 0;
         
         System.out.println("Reading file using Line Iterator");

         while (it.hasNext()) {
            String line = it.nextLine();
            lines++;

         }

         System.out.println("Total file line count: " + lines);
         Instant lineCountEnd = Instant.now();
         
         long timeElapsedLineCount =
         Duration.between(lineCountStart, lineCountEnd).toMillis();
         
         System.out.println("Line count time: " +
         timeElapsedLineCount + "ms");

      } 
   }

Полученные результаты

Вот результаты после применения Instant.now() и Duration.between() ко всем моим различным методам чтения файлов в Java.

Я запустил все три своих решения для файла размером 2,55 ГБ, который в общей сложности содержал чуть более 13 миллионов строк.

Как видно из таблицы, у BufferedReader() и LineIterator() дела идут хорошо, но они настолько близки по срокам, что кажется разработчиком, который они предпочли бы использовать.

BufferedReader() хорош, потому что не требует дополнительных зависимостей, но его немного сложнее настроить в начале, с FileReader(), который нужно обернуть внутри. В то время как LineIterator() - это внешняя библиотека, но она упрощает итерацию по файлу после того, как он включен в качестве зависимости.

Процентные улучшения также указаны в конце приведенной выше таблицы для справки.

FileInputStream, что интересно, вылетело из воды двумя другими. За счет буферизации потока данных или использования библиотеки, созданной специально для итерации текстовых файлов, производительность улучшилась примерно на 73% по всем задачам.

Ниже приведены необработанные снимки экрана моего терминала для каждого из моих решений.

Решение №1: FileInputStream()

Решение № 2: BufferedReader()

Решение № 3: FileUtils.lineIterator()

Заключение

В конце концов, буферизованные потоки и пользовательские библиотеки чтения файлов являются наиболее эффективными способами обработки больших наборов данных в Java. По крайней мере, для больших текстовых файлов мне было поручено читать.

Спасибо, что прочитали мой пост об использовании Java для чтения действительно очень больших файлов. Если вы хотите увидеть оригинальные сообщения на Node.js, которые вдохновили вас на создание этого, вы можете увидеть часть 1 здесь и часть 2 здесь.

Вернитесь через несколько недель, я буду писать о Swagger с Express.js или о чем-то еще, связанном с веб-разработкой и JavaScript, поэтому, пожалуйста, подписывайтесь на меня, чтобы не пропустить.

Спасибо за чтение. Надеюсь, это дает вам представление о том, как эффективно обрабатывать большие объемы данных с помощью Java и тестировать производительность ваших решений. Мы очень ценим аплодисменты и акции!

Если вам понравилось это читать, возможно, вам понравятся и другие мои блоги:

Ссылки и дополнительные ресурсы: