Рубрики
Без рубрики

Java: Считывание файла в список массивов

Автор оригинала: 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
ArrayList 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]

Сканер имеет некоторые полезные функции для анализа данных, такие как 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 принимают Читатель объект в качестве параметра, например Читатель файлов .

Мы используем пробные ресурсы, поэтому нам не нужно закрывать считыватель вручную:

ArrayList 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
}

Рекомендуется обернуть файловый редактор с Буферным читателем , именно из-за преимуществ производительности.

Поток ввода объекта

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 (Stream stream = Files.lines(Paths.get(fileName))) {
    ArrayList arrayList = stream.collect(Collectors.toCollection(ArrayList::new));
}
catch (IOException e) {
    // Handle a potential exception
}

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

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

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

try (Stream stream = 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 ).