В этом руководстве рассмотрим различные способы чтения из файла в Java.
Во-первых, узнаем, как загрузить файл из classpath, URL-адреса или из файла JAR, используя стандартные классы Java.
Во-вторых, увидим, как читать содержимое с помощью BufferedReader, Scanner, StreamTokenizer, DataInputStream, SequenceInputStream и FileChannel. Также обсудим, как читать файл в кодировке UTF-8.
Наконец, рассмотрим новые методы загрузки и чтения файла в Java 7 и Java 8.
Настройка
Входной файл
В большинстве примеров в этой статье будем читать текстовый файл fileTest.txt, который содержит одну строку:
Hello, world!
Для нескольких примеров будем использовать другой файл; в этих случаях будем явно указывать файл и его содержимое.
Вспомогательный метод
Будем использовать набор тестовых примеров только с основными классами Java, а в тестах будем использовать утверждения с matchers Hamcrest.
Тесты будут использовать общий метод readFromInputStream, который преобразует InputStream в String для упрощения утверждения результатов:
private String readFromInputStream(InputStream inputStream)
throws IOException {
StringBuilder resultStringBuilder = new StringBuilder();
try (BufferedReader br
= new BufferedReader(new InputStreamReader(inputStream))) {
String line;
while ((line = br.readLine()) != null) {
resultStringBuilder.append(line).append("\n");
}
}
return resultStringBuilder.toString();
}
Обратите внимание, что есть и другие способы достижения того же результата. Можно обратиться к этой статье для некоторых вариантов.
Чтение файла из Classpath
Использование стандартной Java
В этом разделе объясняется, как читать файл, доступный в Classpath. Прочитаем fileTest.txt, доступный в src/main/resources:
@Test
public void givenFileNameAsAbsolutePath_whenUsingClasspath_thenFileData() {
String expectedData = "Hello, world!";
Class clazz = FileOperationsTest.class;
InputStream inputStream = clazz.getResourceAsStream("/fileTest.txt");
String data = readFromInputStream(inputStream);
Assert.assertThat(data, containsString(expectedData));
}
В приведенном выше фрагменте кода используется текущий класс для загрузки файла с помощью метода getResourceAsStream и передается абсолютный путь к файлу для загрузки.
Тот же метод доступен и для экземпляра ClassLoader:
ClassLoader classLoader = getClass().getClassLoader();
InputStream inputStream = classLoader.getResourceAsStream("fileTest.txt");
String data = readFromInputStream(inputStream);
Получаем classLoader текущего класса, используя getClass().getClassLoader().
Основное отличие состоит в том, что при использовании getResourceAsStream в экземпляре ClassLoader путь рассматривается как абсолютный, начиная с корня classpath.
При использовании для экземпляра класса путь может быть относительным к пакету или абсолютным путем, на что намекает косая черта в начале.
Обратите внимание, что на практике открытые потоки всегда должны быть закрыты, например, InputStream в следующем примере:
InputStream inputStream = null;
try {
File file = new File(classLoader.getResource("fileTest.txt").getFile());
inputStream = new FileInputStream(file);
//...
}
finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Использование библиотеки commons-io
Другой распространенный вариант – использование класса FileUtils пакета commons-io:
@Test
public void givenFileName_whenUsingFileUtils_thenFileData() {
String expectedData = "Hello, world!";
ClassLoader classLoader = getClass().getClassLoader();
File file = new File(classLoader.getResource("fileTest.txt").getFile());
String data = FileUtils.readFileToString(file, "UTF-8");
assertEquals(expectedData, data.trim());
}
Здесь передаем объект File в метод readFileToString() класса FileUtils. Этот служебный класс позволяет загружать содержимое без необходимости написания какого-либо шаблонного кода для создания экземпляра InputStream и чтения данных.
Эта же библиотека также предлагает класс IOUtils:
@Test
public void givenFileName_whenUsingIOUtils_thenFileData() {
String expectedData = "Hello, world!";
FileInputStream fis = new FileInputStream("src/test/resources/fileTest.txt");
String data = IOUtils.toString(fis, "UTF-8");
assertEquals(expectedData, data.trim());
}
Здесь передаем объект FileInputStream в метод toString() класса IOUtils. Этот служебный класс действует так же, как и предыдущий, для создания экземпляра InputStream и чтения данных.
Чтение с помощью BufferedReader
Теперь сосредоточимся на различных способах парсинга содержимого файла.
Начнем с простого способа чтения из файла с помощью BufferedReader:
@Test
public void whenReadWithBufferedReader_thenCorrect()
throws IOException {
String expected_value = "Hello, world!";
String file ="src/test/resources/fileTest.txt";
BufferedReader reader = new BufferedReader(new FileReader(file));
String currentLine = reader.readLine();
reader.close();
assertEquals(expected_value, currentLine);
}
Обратите внимание, что readLine() вернет null, когда будет достигнут конец файла.
Чтение из файла с помощью Java NIO
В JDK7 значительно обновлен пакет NIO.
Рассмотрим пример с использованием класса Files и метода readAllLines. Метод readAllLines принимает Path.
Класс Path можно рассматривать как обновление java.io.File с некоторыми дополнительными операциями.
Чтение небольшого файла
В следующем коде показано, как прочитать небольшой файл с помощью нового класса Files:
@Test
public void whenReadSmallFileJava7_thenCorrect()
throws IOException {
String expected_value = "Hello, world!";
Path path = Paths.get("src/test/resources/fileTest.txt");
String read = Files.readAllLines(path).get(0);
assertEquals(expected_value, read);
}
Обратите внимание, что можно использовать метод readAllBytes(), если нужны двоичные данные.
Чтение большого файла
Если хотим прочитать большой файл с помощью класса Files, можно использовать метод BufferedReader.
Следующий код читает файл, используя новый класс Files и BufferedReader:
@Test
public void whenReadLargeFileJava7_thenCorrect()
throws IOException {
String expected_value = "Hello, world!";
Path path = Paths.get("src/test/resources/fileTest.txt");
BufferedReader reader = Files.newBufferedReader(path);
String line = reader.readLine();
assertEquals(expected_value, line);
}
Чтение файла с помощью Files.lines()
JDK8 предлагает метод lines() внутри класса Files. Он возвращает поток элементов String.
Рассмотрим пример того, как читать данные в байты и декодировать их с помощью кодировки UTF-8.
Следующий код считывает файл с помощью новой функции Files.lines():
@Test
public void givenFilePath_whenUsingFilesLines_thenFileData() {
String expectedData = "Hello, world!";
Path path = Paths.get(getClass().getClassLoader()
.getResource("fileTest.txt").toURI());
Stream<String> lines = Files.lines(path);
String data = lines.collect(Collectors.joining("\n"));
lines.close();
Assert.assertEquals(expectedData, data.trim());
}
Используя Stream с каналами ввода-вывода, такими как файловые операции, нужно явно закрыть поток, используя метод close().
Как видим, Files API предлагает еще один простой способ чтения содержимого файла в строку.
В следующих разделах рассмотрим другие менее распространенные методы чтения файла, которые могут быть уместны в некоторых ситуациях.
Чтение через Scanner
Далее давайте использовать Scanner для чтения из файла. Здесь будем использовать пробел в качестве разделителя:
@Test
public void whenReadWithScanner_thenCorrect()
throws IOException {
String file = "src/test/resources/fileTest.txt";
Scanner scanner = new Scanner(new File(file));
scanner.useDelimiter(" ");
assertTrue(scanner.hasNext());
assertEquals("Hello,", scanner.next());
assertEquals("world!", scanner.next());
scanner.close();
}
Обратите внимание, что разделителем по умолчанию является пробел, но со Scanner можно использовать несколько разделителей.
Класс Scanner полезен при чтении содержимого из консоли или когда содержимое содержит примитивные значения с известным разделителем (например, список целых чисел, разделенных пробелом).
Чтение с помощью StreamTokenizer
Теперь прочитаем текстовый файл в токены с помощью StreamTokenizer.
Tokenizer работает, сначала выясняя, что будет следующим токеном, строкой или числом. Мы делаем это, просматривая поле tokenizer.ttype.
Затем прочитаем фактический токен на основе этого типа:
- tokenizer.nval – если тип был числом;
- tokenizer.sval – если тип был String
В этом примере будем использовать другой входной файл, который содержит:
Hello 1
Следующий код считывает из файла как строку, так и число:
@Test
public void whenReadWithStreamTokenizer_thenCorrectTokens()
throws IOException {
String file = "src/test/resources/fileTestTokenizer.txt";
FileReader reader = new FileReader(file);
StreamTokenizer tokenizer = new StreamTokenizer(reader);
// token 1
tokenizer.nextToken();
assertEquals(StreamTokenizer.TT_WORD, tokenizer.ttype);
assertEquals("Hello", tokenizer.sval);
// token 2
tokenizer.nextToken();
assertEquals(StreamTokenizer.TT_NUMBER, tokenizer.ttype);
assertEquals(1, tokenizer.nval, 0.0000001);
// token 3
tokenizer.nextToken();
assertEquals(StreamTokenizer.TT_EOF, tokenizer.ttype);
reader.close();
}
Обратите внимание, как конец файла токена используется в конце.
Этот подход полезен для парсинга входного потока на токены.
Чтение через DataInputStream
Можно использовать DataInputStream для чтения двоичных или примитивных типов данных из файла.
Следующий тест считывает файл с помощью DataInputStream:
@Test
public void whenReadWithDataInputStream_thenCorrect() throws IOException {
String expectedValue = "Hello, world!";
String file ="src/test/resources/fileTest.txt";
String result = null;
DataInputStream reader = new DataInputStream(new FileInputStream(file));
int nBytesToRead = reader.available();
if(nBytesToRead > 0) {
byte[] bytes = new byte[nBytesToRead];
reader.read(bytes);
result = new String(bytes);
}
assertEquals(expectedValue, result);
}
Чтение через FileChannel
Если читаем большой файл, FileChannel может быть быстрее, чем стандартный ввод-вывод.
Следующий код считывает байты данных из файла, используя FileChannel и RandomAccessFile:
@Test
public void whenReadWithFileChannel_thenCorrect()
throws IOException {
String expected_value = "Hello, world!";
String file = "src/test/resources/fileTest.txt";
RandomAccessFile reader = new RandomAccessFile(file, "r");
FileChannel channel = reader.getChannel();
int bufferSize = 1024;
if (bufferSize > channel.size()) {
bufferSize = (int) channel.size();
}
ByteBuffer buff = ByteBuffer.allocate(bufferSize);
channel.read(buff);
buff.flip();
assertEquals(expected_value, new String(buff.array()));
channel.close();
reader.close();
}
Чтение файла в кодировке UTF-8
Теперь посмотрим, как прочитать файл в кодировке UTF-8 с помощью BufferedReader. В этом примере прочитаем файл, содержащий китайские символы:
@Test
public void whenReadUTFEncodedFile_thenCorrect()
throws IOException {
String expected_value = "青空";
String file = "src/test/resources/fileTestUtf8.txt";
BufferedReader reader = new BufferedReader
(new InputStreamReader(new FileInputStream(file), "UTF-8"));
String currentLine = reader.readLine();
reader.close();
assertEquals(expected_value, currentLine);
}
Чтение контента с URL-адреса
Чтобы прочитать содержимое с URL-адреса, в примере будем использовать URL-адрес «/»:
@Test
public void givenURLName_whenUsingURL_thenFileData() {
String expectedData = "JavaMaster";
URL urlObject = new URL("/");
URLConnection urlConnection = urlObject.openConnection();
InputStream inputStream = urlConnection.getInputStream();
String data = readFromInputStream(inputStream);
Assert.assertThat(data, containsString(expectedData));
}
Существуют также альтернативные способы подключения к URL-адресу. Здесь использовались классы URL и URLConnection, доступные в стандартном SDK.
Чтение файла из JAR
Чтобы прочитать файл, который находится внутри файла JAR, понадобится JAR с файлом внутри него. Для нашего примера прочитаем LICENSE.txt из файла hamcrest-library-1.3.jar:
@Test
public void givenFileName_whenUsingJarFile_thenFileData() {
String expectedData = "BSD License";
Class clazz = Matchers.class;
InputStream inputStream = clazz.getResourceAsStream("/LICENSE.txt");
String data = readFromInputStream(inputStream);
Assert.assertThat(data, containsString(expectedData));
}
Здесь хотим загрузить LICENSE.txt, который находится в библиотеке Hamcrest, поэтому будем использовать класс Matcher, который помогает получить ресурс. Тот же файл можно загрузить и с помощью загрузчика классов.
Заключение
Как видим, есть много возможностей для загрузки файла и чтения из него данных с помощью обычной Java.
Можно загрузить файл из разных мест, таких как classpath, URL или jar-файлы.
Можно использовать BufferedReader для чтения построчно, Scanner для чтения с использованием разных разделителей, StreamTokenizer для чтения файла в токены, DataInputStream для чтения двоичных данных и примитивных типов данных, SequenceInput Stream для связывания нескольких файлов в один поток, FileChannel для более быстрого чтения из больших файлов и т.д.
Исходный код этой статьи можно найти на GitHub.