Автор оригинала: Olivera Popović.
Вступление
Существует множество способов чтения и записи файлов на Java .
Обычно у нас есть некоторые данные в памяти, с которыми мы выполняем операции, а затем сохраняем их в файле. Однако, если мы хотим изменить эту информацию, нам нужно поместить содержимое файла обратно в память и выполнить операции.
Если, например, наш файл содержит длинный список, который мы хотим отсортировать, нам придется прочитать его в соответствующей структуре данных, выполнить операции, а затем сохранить его еще раз – в данном случае ArrayList
.
Этого можно достичь с помощью нескольких различных подходов:
Файлы.Строки для чтения()
Устройство для чтения файлов
Сканер
Буферизатор
Поток ввода объекта
- API потоков Java
Файлы.Строки для чтения()
Начиная с Java 7, можно очень простым способом загрузить все строки файла в ArrayList
:
try { ArrayListlines = new ArrayList<>(Files.readAllLines(Paths.get(fileName))); } catch (IOException e) { // Handle a potential exception }
Мы также можем указать кодировку
для обработки различных форматов текста, если это необходимо:
try { Charset charset = StandardCharsets.UTF_8; ArrayListlines = new ArrayList<>(Files.readAllLines(Paths.get(fileName), charset)); } catch (IOException e) { // Handle a potential exception }
Files.ReadAllLines()
автоматически открывает и закрывает необходимые ресурсы.
Сканер
Каким бы приятным и простым ни был предыдущий метод, он полезен только для чтения файла строка за строкой. Что произойдет, если все данные будут храниться в одной строке?
Сканер
– это простой в использовании инструмент для анализа примитивных типов и строк. Использование Сканера
может быть настолько простым или сложным, насколько этого хочет разработчик.
Простым примером того, когда мы предпочли бы использовать Сканер
, было бы, если бы в нашем файле была только одна строка, и данные должны быть проанализированы во что-то полезное.
Разделитель – это последовательность символов, которую Сканер
использует для разделения значений. По умолчанию он использует ряд пробелов/вкладок в качестве разделителя (пробелы между значениями), но мы можем объявить наш собственный разделитель и использовать его для анализа данных.
Давайте взглянем на пример файла:
some-2123-different-values- in - this -text-with a common-delimiter
В таком случае легко заметить, что все значения имеют общий разделитель. Мы можем просто объявить, что” -“, окруженное любым количеством пробелов, является нашим разделителем.
// We'll use "-" as our delimiter ArrayListarrayList = 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]
Сканер
имеет некоторые полезные функции для анализа данных, такие как nextInt()
, nextDouble ()
и т.д.
Важно : Вызов .nextInt()
будет НЕ возвращать следующее int
значение, которое можно найти в файле! Он вернет значение int
только в том случае, если следующие элементы Сканер
“сканирует” являются допустимым значением int
, в противном случае будет выдано исключение. Простой способ убедиться, что исключение не возникает, – выполнить соответствующую проверку “имеет”, как .hasNextInt()
перед фактическим использованием .nextInt()
.
Даже если мы не видим этого, когда мы вызываем такие функции, как scanner.nextInt()
или scanner.hasNextDouble ()
, Сканер
использует регулярные выражения в фоновом режиме.
Очень важно: | Чрезвычайно Распространенная ошибка при использовании Сканера возникает при работе с файлами, содержащими несколько строк , и использовании
. nextLine () в сочетании с
. nextInt () ,
nextDouble () и т. Д.
Давайте взглянем на другой файл:
12 some data we want to read as a string in one line 10
Часто новые разработчики, использующие Сканер
, пишут код, подобный:
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 //}
Git Essentials
Ознакомьтесь с этим практическим руководством по изучению Git, содержащим лучшие практики и принятые в отрасли стандарты. Прекратите гуглить команды Git и на самом деле изучите это!
Этот код кажется логически звучащим – мы считываем целое число из файла, затем следующую строку, затем второе целое число. Если вы попытаетесь запустить этот код, исключение InputMismatchException
будет выдано без очевидной причины.
Если вы начнете отладку и распечатаете отсканированное, вы увидите, что в
хорошо загружено, но Строки
пусты.
Почему это так? Первое, что важно отметить, это то, что как только Сканер
считывает что-либо из файла, он продолжает сканирование файла с первого символа после ранее отсканированных данных.
Например, если бы в файле было “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()
, Сканер
считывает только число 12 и оставляет первое \n
непрочитанным.
..следующая строка()
затем считывает все символы, которые сканер еще не прочитал, пока не достигнет первого символа \n
, который он пропускает, а затем возвращает прочитанные символы. Именно в этом и заключается проблема в нашем случае – у нас остался символ \n
после прочтения 12
.
Поэтому, когда мы вызываем .nextLine ()
, в результате мы получаем пустую строку, так как Сканер
не добавляет символ \n
в возвращаемую строку.
Теперь Сканер
находится в начале второй строки вашего файла, и когда мы пытаемся вызвать .nextInt()
, Сканер
обнаруживает что-то, что не может быть проанализировано в int
и выдает вышеупомянутое Исключение InputMismatchException
.
Решения
- Поскольку мы знаем, что именно не так в этом коде, мы можем жестко прописать обходной путь. Мы просто “потребим” символ новой строки между
.nextInt()
и.nextLine()
:
... int a = scanner.nextInt(); scanner.nextLine(); // Simply consumes the bothersome \n String s = scanner.nextLine(); ...
- Учитывая, что мы знаем, как
example.txt
отформатирован, мы можем прочитать весь файл строка за строкой и проанализировать необходимые строки с помощьюInteger.parseInt()
:
... int a = Integer.parseInt(scanner.nextLine()); String s = scanner.nextLine(); int b = Integer.parseInt(scanner.nextLine()); ...
Буферизатор
BufferedReader
считывает текст из потока ввода символов, но делает это путем буферизации символов для обеспечения эффективных операций .read ()
. Поскольку доступ к жесткому диску занимает очень много времени, BufferedReader
собирает больше данных, чем мы просим, и сохраняет их в буфере.
Идея заключается в том, что когда мы вызываем .read()
(или аналогичную операцию), мы, скорее всего, скоро снова прочитаем из того же блока данных, из которого мы только что прочитали, и поэтому “окружающие” данные хранятся в буфере. В случае, если бы мы хотели прочитать его, мы бы прочитали его непосредственно из буфера, а не с диска, что гораздо эффективнее.
Это подводит нас к тому, что BufferedReader
хорошо подходит для чтения больших файлов. BufferedReader
имеет значительно большую буферную память, чем Сканер
(8192 символа по умолчанию против 1024 символов по умолчанию соответственно).
BufferedReader
используется в качестве оболочки для других Читателей , и поэтому конструкторы для BufferedReader
принимают Читатель объект в качестве параметра, например Читатель файлов
.
Мы используем пробные ресурсы, поэтому нам не нужно закрывать считыватель вручную:
ArrayListarrayList = new ArrayList<>(); try (BufferedReader reader = new BufferedReader(new FileReader(fileName))) { while (reader.ready()) { arrayList.add(reader.readLine()); } } catch (IOException e) { // Handle a potential exception }
Рекомендуется обернуть файловый редактор
с Буферным читателем
, именно из-за преимуществ производительности.
Поток ввода объекта
ObjectInputStream
следует использовать только вместе с ObjectOutputStream
. Что эти два класса помогают нам сделать, так это сохранить объект (или массив объектов) в файл, а затем легко прочитать из этого файла.
Это можно сделать только с классами, реализующими Сериализуемый
интерфейс. Интерфейс Сериализуемый
не имеет методов или полей и служит только для определения семантики сериализуемости:
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")); ArrayListarrayList = 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); } }
API потоков Java
Начиная с Java 8, еще одним быстрым и простым способом загрузки содержимого файла в Список массивов
было бы использование Java Streams API :
// Using try-with-resources so the stream closes automatically try (Streamstream = Files.lines(Paths.get(fileName))) { ArrayList arrayList = stream.collect(Collectors.toCollection(ArrayList::new)); } catch (IOException e) { // Handle a potential exception }
Однако имейте в виду, что этот подход, как и Files.ReadAllLines ()
, будет работать только в том случае, если данные хранятся в строках.
Приведенный выше код не делает ничего особенного, и мы редко использовали бы потоки таким образом. Однако, поскольку мы загружаем эти данные в ArrayList
, чтобы мы могли обработать их в первую очередь, потоки предоставляют отличный способ сделать это.
Мы можем легко сортировать/фильтровать/сопоставлять данные перед их сохранением в списке массивов
:
try (Streamstream = Files.lines(Paths.get(fileName))) { ArrayList 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
; когда у вас есть данные, которые можно легко проанализировать, используйте Сканер
; при работе с большими файлами используйте FileReader
, завернутый в BufferedReader
; при работе с массивом объектов используйте ObjectInputStream
(но убедитесь, что данные были записаны с использованием ObjectOutputStream
).