Вступление
Есть много способов чтения и записи файлов на 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()
:
|
|
...
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
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
).