Java: чтение файла в ArrayList

Введение Есть много способов чтения и записи файлов на Java [/ read-and-writing-files-in-java /]. Обычно у нас есть некоторые данные в памяти, с которыми мы выполняем операции, а затем сохраняем их в файле. Однако, если мы хотим изменить эту информацию, нам нужно вернуть содержимое файла в память и выполнить операции. Если, например, наш файл содержит длинный список, который мы хотим отсортировать, нам нужно будет прочитать его в адекватную структуру данных, выполнить операции и т. Д.

Вступление

Есть много способов чтения и записи файлов на Java .

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

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

Этого можно достичь несколькими разными подходами:

  • Files.readAllLines()
  • FileReader
  • Scanner
  • BufferedReader
  • ObjectInputStream
  • Java Streams API

Files.readAllLines ()

Начиная с Java 7, можно очень просто ArrayList

 try { 
 ArrayList<String> lines = new ArrayList<>(Files.readAllLines(Paths.get(fileName))); 
 } 
 catch (IOException e) { 
 // Handle a potential exception 
 } 

Мы также можем указать charset для обработки различных форматов текста, если необходимо:

 try { 
 Charset charset = StandardCharsets.UTF_8; 
 ArrayList<String> lines = new ArrayList<>(Files.readAllLines(Paths.get(fileName), charset)); 
 } 
 catch (IOException e) { 
 // Handle a potential exception 
 } 

Files.readAllLines() автоматически открывает и закрывает необходимые ресурсы.

Сканер

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

Scanner - это простой в использовании инструмент для анализа примитивных типов и строк. Использование Scanner может быть настолько простым или сложным, насколько этого хочет разработчик.

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

Разделитель - это последовательность символов, которую Scanner использует для разделения значений. По умолчанию он использует серию пробелов / табуляций в качестве разделителя (пробелы между значениями), но мы можем объявить наш собственный разделитель и использовать его для анализа данных.

Давайте посмотрим на пример файла:

 some-2123-different-values- in - this -text-with a common-delimiter 

В таком случае легко заметить, что все значения имеют общий разделитель. Мы можем просто объявить, что наш разделитель - "-", окруженный любым количеством пробелов.

 // We'll use "-" as our delimiter 
 ArrayList<String> arrayList = new ArrayList<>(); 
 try (Scanner s = new Scanner(new File(fileName)).useDelimiter("\\s*-\\s*")) { 
 // \\s* in regular expressions means "any number or whitespaces". 
 // We could've said simply useDelimiter("-") and Scanner would have 
 // included the whitespaces as part of the data it extracted. 
 while (s.hasNext()) { 
 arrayList.add(s.next()); 
 } 
 } 
 catch (FileNotFoundException e) { 
 // Handle the potential exception 
 } 

Запуск этого фрагмента кода даст нам список ArrayList со следующими элементами:

 [some, 2, different, values, in, this, text, with a common, delimiter] 

С другой стороны, если бы мы использовали только разделитель по умолчанию (пробел), ArrayList выглядел бы так:

 [some-2-different-values-, in, -, this, -text-with, a, common-delimiter] 

Scanner имеет несколько полезных функций для анализа данных, таких как nextInt() , nextDouble() и т. Д.

Важно : вызов .nextInt() НЕ вернет следующее int которое можно найти в файле! Он вернет int только в том случае, если следующие элементы, которые "сканирует" Scanner int , в противном случае будет выдано исключение. Простой способ убедиться, что исключение не возникает, - это выполнить соответствующую проверку «имеет» - например, .hasNextInt() перед фактическим использованием .nextInt() .

Несмотря на то, что мы не видим, что когда мы вызываем такие функции, как scanner.nextInt() или scanner.hasNextDouble() , Scanner использует регулярные выражения в фоновом режиме.

Очень важно: чрезвычайно распространенная ошибка при использовании Scanner возникает при работе с файлами, состоящими из нескольких строк, и использовании .nextLine() вместе с .nextInt() , nextDouble() и т. Д.

Взглянем на другой файл:

 12 
 some data we want to read as a string in one line 
 10 

Часто новые разработчики, использующие Scanner , пишут такой код:

 try (Scanner scanner = new Scanner(new File("example.txt"))) { 
 int a = scanner.nextInt(); 
 String s = scanner.nextLine(); 
 int b = scanner.nextInt(); 
 
 System.out.println(a + ", " + s + ", " + b); 
 } 
 catch (FileNotFoundException e) { 
 // Handle a potential exception 
 } 
 //catch (InputMismatchException e) { 
 // // This will occur in the code above 
 //} 

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

Если вы начнете отладку и распечатать то, что вы отсканировали, вы увидите, что int a загружен, но этот String s пуст.

Это почему? Первое, что следует отметить, это то, что как только Scanner что-то читает из файла, он продолжает сканирование файла с первого символа после данных, которые он ранее отсканировал.

Например, если у нас есть «12 13 14» в файле и .nextInt() , сканер впоследствии будет делать вид, будто в файле только «13 14». Обратите внимание, что пробел между «12» и «13» все еще присутствует.

Второе важное замечание: первая строка в нашем example.txt содержит не только число 12 , но и то, что она называет «символом новой строки», и на самом деле это 12\n а не просто 12 .

Наш файл на самом деле выглядит так:

 12\n 
 some data we want to read as a string in one line\n 
 10 

Когда мы впервые вызываем .nextInt() , Scanner считывает только число 12 и оставляет первое \n непрочитанным.

.nextLine() считывает все символы, которые сканер еще не прочитал, пока не достигнет первого \n , который он пропускает, а затем возвращает прочитанные символы. В этом и заключается проблема в нашем случае - у нас остался \n после чтения 12 .

Поэтому, когда мы вызываем .nextLine() мы получаем в результате пустую строку, поскольку Scanner не добавляет \n к возвращаемой строке.

Теперь Scanner находится в начале второй строки в нашем файле, и когда мы пытаемся вызвать .nextInt() , Scanner обнаруживает что-то, что не может быть проанализировано до int и выдает вышеупомянутое InputMismatchException .

Решения

  • Поскольку мы знаем, что именно не так в этом коде, мы можем жестко закодировать обходной путь. Мы просто «потребляем» символ новой строки между .nextInt() и .nextLine() :
1
<!-- -->
 ... 
 int a = scanner.nextInt(); 
 scanner.nextLine(); // Simply consumes the bothersome \n 
 String s = scanner.nextLine(); 
 ... 
  • Учитывая, что мы знаем, как example.txt мы можем прочитать весь файл построчно и проанализировать необходимые строки с помощью Integer.parseInt() :
1
<!-- -->
 ... 
 int a = Integer.parseInt(scanner.nextLine()); 
 String s = scanner.nextLine(); 
 int b = Integer.parseInt(scanner.nextLine()); 
 ... 

BufferedReader

BufferedReader читает текст из потока ввода символов, но делает это путем буферизации символов для обеспечения эффективных .read() . Поскольку доступ к жесткому диску - это очень трудоемкая операция, BufferedReader собирает больше данных, чем мы запрашиваем, и сохраняет их в буфере.

Идея состоит в том, что когда мы вызываем .read() (или аналогичную операцию), мы, скорее всего, вскоре снова будем читать из того же блока данных, из которого мы только что прочитали, и поэтому «окружающие» данные сохраняются в буфере. Если бы мы захотели его прочитать, мы бы прочитали его прямо из буфера, а не с диска, что намного эффективнее.

Это подводит нас к тому, чем BufferedReader - чтению больших файлов. BufferedReader имеет значительно большую буферную память, чем Scanner (8192 символа по умолчанию против 1024 символа по умолчанию соответственно).

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

Мы используем try-with-resources, поэтому нам не нужно закрывать программу чтения вручную:

 ArrayList<String> arrayList = new ArrayList<>(); 
 
 try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) { 
 while (reader.ready()) { 
 arrayList.add(reader.readLine()); 
 } 
 } 
 catch (IOException e) { 
 // Handle a potential exception 
 } 

Рекомендуется заключить FileReader в BufferedReader именно из-за повышения производительности.

ObjectInputStream

ObjectInputStream следует использовать только вместе с ObjectOutputStream . Эти два класса помогают нам сохранять объект (или массив объектов) в файл, а затем легко читать из этого файла.

Это можно сделать только с классами, реализующими интерфейс Serializable Интерфейс Serializable не имеет методов или полей и служит только для определения семантики сериализации:

 public static class MyClass implements Serializable { 
 int someInt; 
 String someString; 
 
 public MyClass(int someInt, String someString) { 
 this.someInt = someInt; 
 this.someString = someString; 
 } 
 } 
 
 public static void main(String[] args) throws IOException, ClassNotFoundException { 
 // The file extension doesn't matter in this case, since they're only there to tell 
 // the OS with what program to associate a particular file 
 ObjectOutputStream objectOutputStream = 
 new ObjectOutputStream(new FileOutputStream("data.olivera")); 
 
 MyClass first = new MyClass(1, "abc"); 
 MyClass second = new MyClass(2, "abc"); 
 
 objectOutputStream.writeObject(first); 
 objectOutputStream.writeObject(second); 
 objectOutputStream.close(); 
 
 ObjectInputStream objectInputStream = 
 new ObjectInputStream(new FileInputStream("data.olivera")); 
 
 ArrayList<MyClass> arrayList = new ArrayList<>(); 
 
 try (objectInputStream) { 
 while (true) { 
 Object read = objectInputStream.readObject(); 
 if (read == null) 
 break; 
 
 // We should always cast explicitly 
 MyClass myClassRead = (MyClass) read; 
 arrayList.add(myClassRead); 
 } 
 } 
 catch (EOFException e) { 
 // This exception is expected 
 } 
 
 for (MyClass m : arrayList) { 
 System.out.println(m.someInt + " " + m.someString); 
 } 
 } 

Java Streams API

Начиная с Java 8, еще одним быстрым и простым способом загрузки содержимого файла в ArrayList было бы использование Java Streams API :

 // Using try-with-resources so the stream closes automatically 
 try (Stream<String> stream = Files.lines(Paths.get(fileName))) { 
 ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new)); 
 } 
 catch (IOException e) { 
 // Handle a potential exception 
 } 

Однако имейте в виду, что этот подход, как и Files.readAllLines() будет работать только в том случае, если данные хранятся в строках.

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

Мы можем легко отсортировать / отфильтровать / сопоставить данные перед сохранением их в ArrayList :

 try (Stream<String> stream = Files.lines(Paths.get(fileName))) { 
 ArrayList<String> arrayList = stream.map(String::toLowerCase) 
 .filter(line -> !line.startsWith("a")) 
 .sorted(Comparator.comparing(String::length)) 
 .collect(Collectors.toCollection(ArrayList::new)); 
 } 
 catch (IOException e) { 
 // Handle a potential exception 
 } 

Заключение

Есть несколько различных способов чтения данных из файла в ArrayList . Когда вам нужно только прочитать строки как элементы, используйте Files.readAllLines ; когда у вас есть данные, которые можно легко проанализировать, используйте Scanner ; при работе с большими файлами используйте FileReader с BufferedReader ; при работе с массивом объектов используйте ObjectInputStream (но убедитесь, что данные были записаны с использованием ObjectOutputStream ).

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus