1. 传统Java方式
一行一行的读取文本文件的需求是很常见的,Java中的原始方式如下:
BufferedReader + try-with-resources
File file = new File("D:\\demo.txt"); try (BufferedReader reader = new BufferedReader(new FileReader(file))) { String line; while ((line = reader.readLine()) != null) { System.out.println(line); } } catch (Exception e) { e.printStackTrace(); }
Files.lines + try-with-resources
File file = new File("D:\\demo.txt"); try (Stream<String> lines = Files.lines(file.toPath())) { lines.forEach(line -> { System.out.println(line); }); } catch (Exception e) { e.printStackTrace(); }
这里的
Stream<String> lines = Files.lines(file.toPath())
结果是一个Stream
,这是Java8的Stream API
,它是惰性的,也就是说此时一行代码都还没有读取,当有求值操作的时候才会真正去读取,方便我们在读取之前添加各种操作,比如经典的过滤操作。而且它在读取的时候也是一行一行的读取的,并不是一下读完所有的行,所以不要以为Stream<String> lines
就是已经把所有的行都读到了,其实不是的,比如可以给流设置一个查找条件,则找到所需要的行就可以提前结束,无需要读取完所有的行,示例如下:File file = new File("D:\\demo.txt"); try (Stream<String> lines = Files.lines(file.toPath())) { Optional<String> result = lines .filter(line -> { System.out.println(line); return line.contains("World"); }) .findFirst(); result.ifPresent(s -> System.out.println("找到需要的行了:" + s)); } catch (Exception e) { e.printStackTrace(); }
运行结果如下:
Hello World! 找到需要的行了:World!
从结果可以看到,只读取了两行,在第二行就找到需要的行了,然后后面的行就不会读取了,直接结束了。这里需要懂
Java 8
的Lambda
表达式和Stream API
,可能很多人都还没学这块知识,那读起来是会有点懵的,但是这种方式确实比第一种方式好。
2. Kotlin 方式
与Java中
BufferedReader + try-with-resources
对等的方式val file = File("D:\\demo.txt") file.bufferedReader().use { reader -> var line = "" while (reader.readLine()?.also { line = it } != null) { println(line) } }
没用习惯kotlin的人对于while中的写法可能会感觉到怪怪的。了解其原理的感觉其实还好。
优雅的一行行读取
val file = File("D:\\demo.txt") file.forEachLine { println(it) }
这实在是太精简了,你甚至可以合并成一行:
File("D:\\demo.txt").forEachLine { println(it) }
与Java中
Files.lines + try-with-resources
原理相同的方式:val file = File("D:\\demo.txt") file.useLines { lines -> lines.forEach { println(it) } }
这里返回的
lines
类型为Sequence
,它和Java 8中的Stream
是一样的,也是惰性的,比如找到包含有World
的一行,然后就不再读取:val file = File("D:\\demo.txt") file.useLines { lines -> lines.find { line -> println(line) line.contains("World") } }
3. 原理
不论是Java中的Files.lines()
还是Kotlin中的file.useLines
,它们返回的Stream
或 Sequence
都是把文件封装为BufferedReader
,然后再封装一个迭代器来实现一行行读取的,比如查看kotlin的useLines
源码,它返回的是LinesSequence
对象的实现,源码如下:
private class LinesSequence(private val reader: BufferedReader) : Sequence<String> {
override public fun iterator(): Iterator<String> {
return object : Iterator<String> {
private var nextValue: String? = null
private var done = false
override public fun hasNext(): Boolean {
if (nextValue == null && !done) {
nextValue = reader.readLine()
if (nextValue == null) done = true
}
return nextValue != null
}
override public fun next(): String {
if (!hasNext()) {
throw NoSuchElementException()
}
val answer = nextValue
nextValue = null
return answer!!
}
}
}
}
可以看到,它底层也是使用BufferedReader
的readLine()
进行一行一行读取的。所以,从这里我们就能了解到,它底层也是操作文件流,那我使用了一次 Sequence
之后,就不能再使用第二次了,就像使用BufferedReader
读取一次数据后,想要再读取一次这是不可能的,因为流是不能回头的。示例如下:
val file = File("D:\\demo.txt")
file.useLines { lines ->
lines.forEach { println(it) }
lines.forEach { println(it) }
}
运行结果如下:
Hello
World!
Good
morning
Nice.
Exception in thread "main" java.lang.IllegalStateException: This sequence can be consumed only once.
at kotlin.sequences.ConstrainedOnceSequence.iterator(SequencesJVM.kt:23)
at KotlinMainKt.main(KotlinMain.kt:14)
at KotlinMainKt.main(KotlinMain.kt)
可以看到,读取一次之后,再读取就会报异常:This sequence can be consumed only once.
,提示Sequence
只能被消费一次,什么叫消费呢?就是进行了求值操作或遍历。
所以,如果真的要读两遍,则要生成两个流再分别生成Sequence
对象,如下:
val file = File("D:\\demo.txt")
file.useLines { lines ->
lines.forEach { println(it) }
}
file.useLines { lines ->
lines.forEach { println(it) }
}
或者,直接把所有行读出来,然后就可以重复读取,如下:
val file = File("D:\\demo.txt")
val lines: List<String> = file.readLines()
lines.forEach { println(lines) }
lines.forEach { println(lines) }
这种方式的缺点是,如果读取的是大文件,容易内存溢出。
总结:
Kotlin中的三种读取行的函数:
val file = File("D:\\demo.txt")
file.useLines { lines: Sequence<String> -> }
file.forEachLine { line: String -> }
val lines: List<String> = file.readLines()
通过查看源代码发现,useLines
是基本,forEachLine
底层调用的是useLines
,readLines
底层调用的是forEachLine
。
forEachLine
实现:
public fun Reader.forEachLine(action: (String) -> Unit): Unit = useLines { it.forEach(action) }
从这里可以知道一个重要的知识点,forEachLine
会遍历所有的行,且是无法停止的,示例如下:
file.forEachLine { line: String ->
println(line)
return@forEachLine
}
运行结果如下:
Hello
World!
Good
morning
Nice.
代码是希望打印一行就结束,但是实际还是会把每一行都打印。
所以,如果希望在指定条件下可以提前结束读取,则使用useLines
。
方法名 | 选择 |
---|---|
useLines | 一行行读,惰性序列,适合添加过滤条件,且可提前结束 |
forEachLine | 一行行读,不能添加过滤条件,且不能提前结束 |
readLines | 读完所有的行保存到集合,适合数据量小的情况 |