前言
本篇是 IO系列 的第4篇,前三篇文章中,咱们现已对JAVA经典IO规划、JAVA-NIO内容、操作系统IO架构根底概念、Zero-Copy做了较为系统的回顾。
而绝大部分Android应用中都会涉及到网络模块,Retrofit
、Okhttp
几乎是必用的结构, Okio
作为 Okhttp
中的重要模块,原先用于处理网络模块中的IO问题,随着其项目开展,Okio也开端面向其他问题。
这一篇,咱们一同对OKIO做一次系统的收拾,搞理解OKIO为什么OK,做到在面试中自若的吹嘘批、在日常作业中灵敏运用。
编者按:面试吹嘘批需求掌握标准,防止远超岗位预期,导致浪费时刻
因文章篇幅较长,可结合内容导图阅读:
okio的宗旨与架构
在OKIO项目的 wiki 中,对其宗旨有如下介绍:
Okio is a library that complements java.io and java.nio to make it much easier to access, store, and process your data. It started as a component of OkHttp, the capable HTTP client included in Android. It’s well-exercised and ready to solve new problems.
简略直译为中文如下:
Okio是一个类库,对
java.io
和java.nio
进行了弥补,使得拜访、存储和处理数据变得愈加简略。它最初是OkHttp的一个组件,OkHttp是安卓中的一个功用强大的HTTP客户端。它非常健壮,能够解决新问题。
简言之:为了更简略的拜访、存储、处理数据,根据 java.io
和 java.nio
进行了功用弥补
wiki中,简略介绍了规划中的几个重点人物:
- ByteStrings and Buffers
- Sources and Sinks
分层架构中相对扁平、简略:在应用和Java IO 之间增加了一层,即OKIO本身,包括 数据封装
、 输入输出
、 超时机制
体现在类图上仍是比较杂乱的:
在库内部,ByteStrings
的运用不多,对 Buffer
数据包装后为上层应用服务,独自拎出。
信息噪声比较多,去掉功用装修的完成类后较为精简:
与Java的输入输出的比照
Java经典IO中的输入输出界说为Stream,在 系列文章 中进行了介绍。字符流相似,图略
在JDK的IO结构中,运用装修模式建立了规模庞大、功用丰厚的输入输出流。从OKIO的宗旨出发,不难理解其规划者期望类库尽或许简略、易扩展、内建部分功用满足完善。因而,OKIO会恰当的另起炉灶,不会全面的运用JDK中的Stream。
OKIO中运用了自界说的输入、输出,即 Source
和 Sink
,注意淡黄色、淡粉色部分:
Sink 在核算机领域有特定意义:指程序或许线程,能够接纳数据,而且能够处理或许发送这些数据
差异点
在wiki中说到如下内容:
An elegant part of the java.io design is how streams can be layered for transformations like encryption and compression. Okio includes its own stream types called Source and Sink that work like InputStream and OutputStream, but with some key differences:
- Timeouts.
- Easy to implement.
- Easy to use.
- No artificial distinction between byte streams and char streams.
- Easy to test.
简略翻译下, Java IO的规划中有一处非常优雅:能够调整流的分层包装以完成加密、紧缩等转化。OKIO包括自有的流类型 Source
、Sink
,与Java的 InputStream
、 OutputStream
功用相似,但是有几点要害的不同:
- 超时机制
- 更简略完成
- 更简略运用
- 字节省、字符流之间没有人为的差异
- 更简略测试
从输入方面看:
在JDK中,InputStream
运用多种层(以及复合层)处理种类繁复的各类数据
-
DataInputStream
用于处理根底数据类型 -
BufferedInputStream
处理缓冲 -
InputStreamReader
处理文本字符串
而OKIO在这些层之上建立了 BufferedSource
,Source防止了一些无法完成 available()
方法的窘境, 转而由调用者指定它们需求的byte个数
在完成一个Source时,不用操心 read()
方法,它难以有效完成且需从257种值中回来一个 ,注:null & [0x00,0xFF]
从输出方面看:
相似的,在JDK中 OutputStream
运用多种层(以及复合层)处理种类繁复的各类数据,而Sink也非常简略采用分层规划
相同点
-
Source
、Sink
的功用与InputStream
、OutputStream
、Reader
、Writer
相同 - 运用时能够经过装修追加功用
关于功用相同,wiki中说到如下内容:
Sources and sinks interoperate with InputStream and OutputStream. You can view any Source as an InputStream, and you can view any InputStream as a Source. Similarly for Sink and OutputStream.
下文的Source、Sink详解中,解析他们与IOStream 为何 “等价”、如何“互操作”
Source、Sink 详解
Source 系统
抛开功用类(紧缩、哈希、加密、装修等),主要重视:
- Source
- BufferedSource
- Buffer
- RealBufferedSource
Source的界说中规中矩:
interface Source : Closeable {
@Throws(IOException::class)
fun read(sink: Buffer, byteCount: Long): Long
fun timeout(): Timeout
@Throws(IOException::class)
override fun close()
}
其间的 timeout
将在下文超时机制章节中打开。
BufferedSource接口约好运用 Buffer
承受实践数据,而且界说了一系列便利运用的接口,如:
- 读取Int
- 读取Short
- 读取字符串
- 内容选择 等,不做罗列
RealBufferedSource
完成了 BufferedSource
接口,从实质上能够以为是 Buffer
类的署理,增加了鸿沟校验
Buffer
完成了 BufferedSource
接口,包括 读 的详细完成
Sink 系统
抛开功用类(紧缩、哈希、加密、装修等),主要重视:
- Sink
- BufferedSink
- Buffer
- RealBufferedSink
interface Sink : Closeable {
@Throws(IOException::class)
fun write(source: Buffer, byteCount: Long)
@Throws(IOException::class)
fun flush()
fun timeout(): Timeout
@Throws(IOException::class)
override fun close()
}
相同,Sink接口的界说也中规中矩。
相似的,BufferedSink
接口继承了 Sink
接口,约好了运用 Buffer
承受实践数据, RealBufferedSink
是详细完成,从实质上是 Buffer
作为 Sink
时的署理,进行了鸿沟校验。
Buffer
完成了 BufferedSink
接口,包括 写 的详细完成
Source、Sink与I/O-Stream的互操作
作者按:请仔细思考一下 互操作
,其实质是:运用一种实例目标的API去操作另一种目标实例的API, 请留意直接操作和直接操作,国内程序员更习惯运用 “转”、“转化” ,着眼点是从一种实例目标取得另一种实例目标。
从转化角度思考时,简略堕入误区,枚举出没必要地转化状况、疏忽掉必要的直接转化。
首要清晰一点:在根据I/O-Stream进行读写时,InputStreamSource、 OutputStreamSink 是 I/O-Stream的读写署理; I/O-Stream 是 InputStreamSource、 OutputStreamSink 的读写托付。
依靠转化API:
fun InputStream.source(): Source = InputStreamSource(this, Timeout())
fun OutputStream.sink(): Sink = OutputStreamSink(this, Timeout())
排除此 “基本状况1” ,还有一种 基本状况2 ,即咱们期望运用I/O-Stream的API去实质操作OKIO。不难想象,此刻必然存在用OKIO完成的定制事务,即数据的实质处理,OKIO选择了 BufferedSink
和 BufferedSource
而非 Sink
、 Source
,此刻 BufferedSink
和 BufferedSource
是 I/O-Stream 的事务托付。
API如下:
interface BufferedSink : Sink, WritableByteChannel {
//... ignore
/** Returns an output stream that writes to this sink. */
fun outputStream(): OutputStream
}
interface BufferedSource : Source, ReadableByteChannel {
//... ignore
/** Returns an input stream that reads from this source. */
fun inputStream(): InputStream
}
更杂乱的状况 — 根据基本状况加各类事务的组合
排除去这些基本状况,还需求需求互操作时,意味着存在两套模块,一套运用Okio的Source、Sink,一套运用Java的InputStream、OutputStream。假定运用Sink、Source的模块为A,运用I/O-Stream的模块为B。
以写为例,进行分析,有两种或许:
- 操作模块A,数据从A流向B,从而写入 (一般来说,B存在事务定制,不然是基本状况1)
- 操作模块B,数据从B流向A,从而写入(A终究运用了基本状况1,此状况属于基本状况2的杂乱版)
1. 数据从A流向B,从而写入
重视A模块的数据出口,A模块或许的规划
- A1:承受一个Sink实例,或许承受一些参数构建Sink实例
- A2:对外供给一个Source实例,由外界自行消费 — 这种规划思路比较奇葩
- 承受回调函数,供给实践数据 — 不属于实践评论领域,疏忽
重视B模块的数据进口,B模块或许的规划
- B1:承受一个InputStream实例,或许承受一些参数构建InputStream实例,消费其数据 — 这种规划思路比较奇葩
- B2:对外供给一个OutputStream实例,由外界操控实践写
- 暴露运用ByteArray等写入API — 不属于实践评论领域,疏忽
A1+B1 状况的伪代码
A1:承受一个Sink实例,或许承受一些参数构建Sink实例
B1:承受一个InputStream实例,或许承受一些参数构建InputStream实例,消费其数据
class Client {
fun demo() {
//运用Sink、Source的模块A
val moduleA = A()
//运用I/O-Stream的模块B
val moduleB = B()
val buffer = Buffer()
//设置模块A的数据出口
moduleA.setSink(buffer)
//设置模块B的数据进口
moduleB.setInputStream(buffer.inputStream())
//假定终究写入file:
moduleB.setOutputFile(File("/target.txt"))
//调用模块A开端写入
moduleA.write("some thing")
}
}
值得注意的是,需求在Buffer区的数据被消费后,进行收拾,以防止内存占用越来越多,而由于B模块的奇葩规划,往往带入多线程问题,编程难度较大
A1+B2 状况的伪代码
A1:承受一个Sink实例,或许承受一些参数构建Sink实例
B2:对外供给一个OutputStream实例,由外界操控实践写
class Client {
fun demo() {
//运用Sink、Source的模块A
val moduleA = A()
//运用I/O-Stream的模块B
val moduleB = B()
//假定其写入方法为:moduleB.writer().write("xxx"), moduleB.writer()取得OutputStream实例
//假定终究写入file:
moduleB.setOutputFile(File("/target.txt"))
//运用B的进口,套接到模块A的数据出口
moduleA.setSink(moduleB.writer().sink())
//调用模块A开端写入
moduleA.write("some thing")
}
}
A2+B1 状况的伪代码
A2:对外供给一个Source实例,由外界自行消费
B1:承受一个InputStream实例,或许承受一些参数构建InputStream实例,消费其数据
class Client {
fun demo() {
//运用Sink、Source的模块A
val moduleA = A()
//运用I/O-Stream的模块B
val moduleB = B()
//假定终究写入file:
moduleB.setOutputFile(File("/target.txt"))
//此状况需求moduleA.getSource() 供给 BufferedSource实例,
//假如完成了Sink而并未完成BufferedSource, 需求模块供给者自己考虑接口系统的转化
moduleB.setInputStream(moduleA.getSource().inputStream())
//调用模块A开端写入
moduleA.write("some thing")
}
}
A2+B2 状况的伪代码
A2:对外供给一个Source实例,由外界自行消费
B2:对外供给一个OutputStream实例,由外界操控实践写
class Client {
fun demo() {
//运用Sink、Source的模块A
val moduleA = A()
//运用I/O-Stream的模块B
val moduleB = B()
//假定终究写入file:
moduleB.setOutputFile(File("/target.txt"))
val resultFromA = moduleA.getSource()
val buffer = Buffer()
while (resultFromA.read(buffer, count) != -1) {
buffer.writeTo(moduleB.writer())
buffer.clear()
}
//调用模块A开端写入
moduleA.write("some thing")
}
}
2. 数据从B流向A,从而写入
重视A模块的数据进口,A模块或许的规划
- A1:承受一个Source实例,或许承受一些参数构建Source实例 — 这种规划思路比较奇葩,在自动拥抱杂乱
- A2:对外供给一个Sink实例,由外界自行操控写入
- 暴露运用ByteArray等写入API — 不属于实践评论领域,疏忽
重视B模块的数据出口,B模块或许的规划
- B1:承受一个OutputStream实例,或许承受一些参数构建OutputStream实例
- B2:对外供给一个InputStream实例,由外界自行消费数据 — 这种规划思路比较奇葩
- 承受回调函数,供给实践数据 — 不属于实践评论领域,疏忽
简略归纳伪代码如下
//A1 + B1 相同会有内存开释、多线程编程难度问题
val buffer = Buffer()
moduleA.setSource(buffer)
moduleB.setWriter(buffer.outputStream())
//A1 + B2
// moduleB.getResult() 回来InputStream实例
moduleA.setSource(moduleB.getResult().source())
//A2 + B1
moduleB.setWriter(moduleA.getSink().outputStream())
//A2 + B2
val resultFromB:InputStream = moduleB.getResult()
val buffer = ByteArray(1024)
while ((len = in.read(buffer))!=-1) {
moduleA.sink().write(buffer,0,len)
}
很明显,OKIO供给的转化方法,能够满足正常的规划,而剩下的奇葩规划,自然需求规划者自行处理内存、多线程问题。
而读的例子也是相似的,考虑到篇幅现已很长,读者诸君能够自行收拾。
在读和写都能够完成两套系统的互操作时,即可随心随意地构建出愈加杂乱的层叠layer,亦不再打开
Buffer 详解
顾名思义,OKIO 中的 Buffer 是特意规划的缓冲区。它在 数据处理
和 数据读写
之间进行缓冲
class Buffer : BufferedSource, BufferedSink, Cloneable, ByteChannel
它的规划目的能够归纳为三个方面,这三方面并不孤立互斥:
- 内存中的读写缓冲区
- 更便利的API
- 和 Java IO 互操作
更便利的API:系列文章中说到过,数据的实质内容能够编码成ByteArray,Buffer供给了更便利的编解码
和 Java IO 互操作:除了上文中现已提及的内容,还包括
readFrom(input: InputStream)
,fun writeTo(out: OutputStream, byteCount: Long = size)
等API,这些也能够算为更便利的APIBuffer完成了ByteChannel接口,能够习惯NIO的规划系统,当然此刻它又是内存中的读写缓冲区
API方面,读者诸君可自行研读代码,内容比较简略。让咱们将精力放在 缓冲区
上,看一看它的完成原理。
中心完成
移除去搅扰代码后, 能够发现它的重点为 head: Segment
,代码简略扫一眼有个印象即可:
class Buffer : /*BufferedSource, BufferedSink,*/ Cloneable, ByteChannel {
internal var head: Segment? = null
var size: Long = 0L
internal set
@Throws(EOFException::class)
override fun readByte(): Byte = commonReadByte()
//和下面的read相似,仅作为Source的API示例
@Throws(IOException::class)
override fun read(sink: ByteBuffer): Int {
val s = head ?: return -1
val toCopy = minOf(sink.remaining(), s.limit - s.pos)
sink.put(s.data, s.pos, toCopy)
s.pos += toCopy
size -= toCopy.toLong()
if (s.pos == s.limit) {
head = s.pop()
SegmentPool.recycle(s)
}
return toCopy
}
override fun write(source: ByteArray): Buffer = commonWrite(source)
//也用到了writableSegment(1),可类比下面的write代码,仅留作Sink的API示例
@Throws(IOException::class)
override fun write(source: ByteBuffer): Int {
val byteCount = source.remaining()
var remaining = byteCount
while (remaining > 0) {
val tail = writableSegment(1)
val toCopy = minOf(remaining, Segment.SIZE - tail.limit)
source.get(tail.data, tail.limit, toCopy)
remaining -= toCopy
tail.limit += toCopy
}
size += byteCount.toLong()
return byteCount
}
}
而Segment是什么呢?是一个链表数据结构:
internal class Segment {
@JvmField val data: ByteArray
@JvmField var pos: Int = 0
@JvmField var limit: Int = 0
@JvmField var shared: Boolean = false
@JvmField var owner: Boolean = false
@JvmField var next: Segment? = null
@JvmField var prev: Segment? = null
//ignore
}
包括了三种内容:
- 实践数据 data
- 标记 pos+limit和读写有关,shared、owner和数据保护有关
- 上下游节点,链表的实质
而 SegmentPool
是很典型的池化规划,究竟 ByteArray
需求分配内存空间,运用池化能够很好地减少无效内存管理(频频分配收回)
不难得出总结:
-
Segment
是完成了便利运用的API的ByteArray链表 -
Buffer
是运用Segment
包装而成的数据缓冲区,完成了便利运用的API,完成了和JAVA IO间的互操作 -
Buffer
既能够作为上游Source
人物,也能够作为下游Sink
人物 - 在规划
Buffer
和Segment
时,结合了日常运用场景进行了特定优化,例如经过转移、共享数据而非复制实质数据
ByteString 概述
在Okio中,ByteString也是一个重要的、便利运用的规划,但比Buffer简略的多。
它的命名也非常有趣,核算机领域中,String一词对应字符串,它本身具有一个更宽泛的意义便是“一串”。 在不满足严谨的评论场景下,咱们能够以为 String
便是指定了编码的CharArray或ByteArray。而CharSequence又太过于笼统,规划者好像仅期望将一些特定的详细状况进行封装,因而制造了ByteString。
open class ByteString
internal actual constructor(
internal actual val data: ByteArray,
) : Serializable, Comparable<ByteString> {}
_ 作者按:我在开发一些蓝牙应用时,数据传输层和应用协议层有一些特定的数据操作,例如:信息摘要核算、CRC校验、AES加解密、Hex-String转化用作日志输出。在很多年前才接触Android时,运用HttpUrlConnection,对body也有相似的处理。属实单调繁琐_
它是一个内容不可变byte序列,这一点能够经过观测API发现,它并不供给修正内容的相关API。但它封装了一系列好用的API,例如:
- utf8(): String
- string(charset: Charset)
- fun base64()
- fun md5() = digest(“MD5”)
- fun sha1() = digest(“SHA-1”)
- digest(algorithm: String): ByteString 等等
相比于界说与运用Utility类,代码可读性更强。
值得注意 :尽管它在规划目的上是内容不可变的,但注意它的结构函数,它只保留了引用,并没有对内容进行复制,这意味实质内容能够在外部篡改
它完成了 Comparable
接口,值得一提的是,它依照无符号数巨细进行 “字典序” 比对。
- “字典序” 比对,即依照从头到尾的顺序,依次比对,脑补一下英文词典。
- Byte运用8bit表示,0xFF(补码)假如视作符号数为255,排在0x00后边,假如视作有符号数,则为-1,排在0x00前面
超时机制
简略思考一下,你的BOSS是如何依照Deadline来查看你的作业的。
假如你没有提前奉告已完成,
- 最理想的BOSS会在到期时查你
- 宽松一点的BOSS会在Deadline当天或提前一天过问一下,到点再查一下
- 焦虑一点的BOSS会频频一点
- 有缺点的BOSS会一天到晚盯着你
明显,需求先约好一个超时的信息:
class Timeout {
private var hasDeadline = false
private var deadlineNanoTime = 0L
private var timeoutNanos = 0L
}
假定有一项详细作业,当你和BOSS约好好时刻,他会记载这一信息,得到一个timeout
,当然,并非一切事情都会有Deadline
此刻,你去履行这一事项:
timeout.withTimeout {
//详细的事项
}
而你的BOSS,则会根据是否有真实Deadline,决定是否记载到他的查看单上。
很明显,你的BOSS需求跟踪的事项进度比较多,他依照到期时刻先后顺序对查看单内容进行收拾,这样他就省事了,他只需求盯着第一个到期时刻进行追寻即可。
当发现超时时,他会将这一项移除,调整他的查看单,并通报此项现已超时…
不难想象,假如他的查看单上没有追寻项,他不会给自己来一个遥遥无期的度假,不然有后续事项没被跟踪,他就惨了,但一向盯着有没有新事项发生会很累,所以他每两小时就会看一下,是否有事项需求写入查看单。即使写入查看单时此事现已延期,但一个2小时内就会到Deadline的事情,略微拖延了一会去追查,也没啥缺点。
将BOSS的这部分作业写成代码如下:
internal fun awaitTimeout(): AsyncTimeout? {
// Get the next eligible node.
val node = head!!.next
// The queue is empty. Wait until either something is enqueued or the idle timeout elapses.
if (node == null) {
val startNanos = System.nanoTime()
condition.await(IDLE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)
return if (head!!.next == null && System.nanoTime() - startNanos >= IDLE_TIMEOUT_NANOS) {
head // The idle timeout elapsed.
} else {
null // The situation has changed.
}
}
var waitNanos = node.remainingNanos(System.nanoTime())
// The head of the queue hasn't timed out yet. Await that.
if (waitNanos > 0) {
condition.await(waitNanos, TimeUnit.NANOSECONDS)
return null
}
// The head of the queue has timed out. Remove it.
head!!.next = node.next
node.next = null
return node
}
private class Watchdog internal constructor() : Thread("Okio Watchdog") {
init {
isDaemon = true
}
override fun run() {
while (true) {
try {
var timedOut: AsyncTimeout? = null
AsyncTimeout.lock.withLock {
timedOut = awaitTimeout()
// The queue is completely empty. Let this thread exit and let another watchdog thread
// get created on the next call to scheduleTimeout().
if (timedOut === head) {
head = null
return
}
}
// Close the timed out node, if one was found.
timedOut?.timedOut()
} catch (ignored: InterruptedException) {
}
}
}
}
再回到你这边,假如你的作业完成了,你去会找BOSS汇报作业,让他处理查看单:
inline fun <T> withTimeout(block: () -> T): T {
var throwOnTimeout = false
enter()
try {
val result = block()
throwOnTimeout = true
return result
} catch (e: IOException) {
throw if (!exit()) e else `access$newTimeoutException`(e)
} finally {
// 找BOSS汇报作业,让他处理查看单
val timedOut = exit()
if (timedOut && throwOnTimeout) throw `access$newTimeoutException`(null)
}
}
此刻轮到BOSS来处理,假如这个事项并没有真实Deadline,他并不会额外做什么。不然他会从头保护查看单内容,但假如没有在查看单中发现这一项,则说明该项在之前通报过现已超时。
fun exit(): Boolean {
return cancelScheduledTimeout(this)
}
private fun cancelScheduledTimeout(node: AsyncTimeout): Boolean {
AsyncTimeout.lock.withLock {
if (!node.inQueue) return false
node.inQueue = false
// Remove the node from the linked list.
var prev = head
while (prev != null) {
if (prev.next === node) {
prev.next = node.next
node.next = null
return false
}
prev = prev.next
}
// The node wasn't found in the linked list: it must have timed out!
return true
}
}
咱们经过一个故事演示了一种异步的超时检测机制,在Okio中,对应了 AsyncTimeout
。当然,实践场景中还有一些更杂乱的,例如两个事项兼并。
甚至,咱们能够直接借用此机制:
演示代码:
class Job<T>(
val block: () -> T,
private val onTimeout: ((afterResult: Boolean, result: T?) -> Boolean)? = null
) {
@Volatile
private var timeout = false
@Volatile
private var timeoutHandled = false
@Volatile
private var execFinished = false
fun exec(): T = block().also { result ->
execFinished = true
if (timeout && !timeoutHandled) {
onTimeout?.let {
timeoutHandled = it(true, result)
}
}
}
fun timeout() {
if (execFinished) return
timeout = true
onTimeout?.let {
timeoutHandled = it(false, null)
}
}
}
class JobsAsyncTimeout<T>(private val job: Job<T>) : AsyncTimeout() {
override fun timedOut() {
job.timeout()
}
override fun timeout(timeout: Long, unit: TimeUnit): JobsAsyncTimeout<T> {
super.timeout(timeout, unit)
return this
}
fun delegate(): () -> T {
return {
withTimeout {
job.exec()
}
}
}
}
fun <T> (() -> T).timeoutJob(
timeout: Long,
timeUnit: TimeUnit,
onTimeout: ((afterResult: Boolean, result: T?) -> Boolean)? = null
): () -> T {
return JobsAsyncTimeout(Job(block = this, onTimeout = onTimeout))
.timeout(timeout = timeout, unit = timeUnit)
.delegate()
}
以一个功率低下的递归核算斐波那契数列进行演示(假如你的机器性能反常的好,能够恰当调大入参):
Demo 代码 :
class Demo {
@Test
fun testTimeOut() {
val fib30 = {
fibonacci(30)
}.timeoutJob(1, TimeUnit.NANOSECONDS) { afterResult, result ->
if (!afterResult) {
// 假如是能够打断的操作,履行打断;除非你仍然想要结果,这样运用超时机制是很勉强的
println("on timeout, callback before result, you should interrupt the job")
// 回来true则意味着现已消费
false
} else {
//假如 afterResult 为false时,现已回来true,则不会有此轮回调
//除非你真的需求结果
println("on timeout, callback after result, $result")
//回来true则意味着现已消费
false
}
}
//超时是会抛出InterruptedIOException
assertThrows(InterruptedIOException::class.java) {
println("fib100-> ${fib30()}")
}
}
@Throws(Exception::class)
fun fibonacci(n: Int): Long {
when {
n < 0 -> throw Exception("n为非法值!")
else -> return when (n) {
0 -> 0
1 -> 1
else -> fibonacci(n - 1) + fibonacci(n - 2)
}
}
}
}
结语
至此,IO系列告一段落。
依照常规,再絮叨几句。在上一年放缓节奏后,读了一些书、想了一些事、观了一些人。近期于《孟子》中得一句:
或劳心,或劳力;劳心者治人,劳力者治于人;治于人者食人,治人者食于人:全国之通义
下个系列还在构思酌量中,下个月再会。