Автор оригинала: Olivera Popović.
Вступление
Существует множество способов чтения и записи файлов на Java .
Обычно у нас есть некоторые данные в памяти, с которыми мы выполняем операции, а затем сохраняем их в файле. Однако, если мы хотим изменить эту информацию, нам нужно поместить содержимое файла обратно в память и выполнить операции.
Если, например, наш файл содержит длинный список, который мы хотим отсортировать, нам придется прочитать его в соответствующей структуре данных, выполнить операции, а затем сохранить его еще раз – в данном случае ArrayList .
Этого можно достичь с помощью нескольких различных подходов:
Файлы.Строки для чтения()Устройство для чтения файловСканерБуферизаторПоток ввода объекта- API потоков Java
Файлы.Строки для чтения()
Начиная с Java 7, можно очень простым способом загрузить все строки файла в ArrayList :
try {
ArrayList lines = new ArrayList<>(Files.readAllLines(Paths.get(fileName)));
}
catch (IOException e) {
// Handle a potential exception
}
Мы также можем указать кодировку для обработки различных форматов текста, если это необходимо:
try {
Charset charset = StandardCharsets.UTF_8;
ArrayList lines = 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"));
ArrayList 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);
}
}
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 ).