Kotlin中优雅的一行行读取文本文件
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? = nullprivate var done = falseoverride 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 = nextValuenextValue = nullreturn 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 | 读完所有的行保存到集合,适合数据量小的情况 |