01-IO流(对象的序列化)
接下来继续介绍IO包中的其他常用对象,常用频率不是特别特别高,但是也会用到。
它们叫ObjectInputStream和ObjectOutputStream,当我们看到后缀名就知道它们是字节流,看到前缀名就知道它的功能是什么了。它们是可以直接操作对象的字节流。
那么它存在的意义是什么呢?
我们知道,对象本身存在堆内存中。 可是当程序用完之后,堆内存就被释放了,这个对象就不存在了。接下来,我们可以通过流的方式,将这个对象,比方说,存在硬盘上。而对象中封装的数据也都随对象一起存在了硬盘中。我们想再将这个对象拿出来用,只要将文件读取一下就OK了。
它的构造方法:
它还有一些特有的方法,我们来使用一下。
单独写了个Person对象:
运行之后,我们发现报了一个异常:
再看文件夹里,object.txt文件产生了,但是里面存了一堆乱乱的东西:
我们再看看控制台上报的异常的内容,发现第9行和第17行有问题:
显然,和writeObject这个方法有关,我们查看一下这个方法,找到了出现NotSerializable异常的原因:
发现是Person没有实现Serializable接口,我们再去Serializable接口中看一看:
对头,类通过实现这个接口来启用其序列化功能。
OK,我们把它实现上:
再回到Serializable接口中看一下,我们发现它里面没有任何方法,这种接口叫做标记接口。实现没有方法的接口,其实就是给这个类盖个戳。
而Person实现它之后,才具备了被序列化的资格。没盖章就没资格喔,盖了章才有被序列化的资格。
Serializable这个接口给每一个实现它的类都加了一个UID,这个标识通常用来被编译器所使用,因为一个类产生对象之后,这个类可以被改变,类被修改后重新编译会产生新的序列号,那么原先的那个对象就和这个新的序列号不匹配。所以编译器是用这个UID序列号来判断这个类和这个对象是不是同一个序号产生的。
OK,实现完这个接口之后,我们再运行一下,看看obj.txt这个文件中的内容有没有变化:
我们看不太懂,应该是在存入的过程中通过查表而存入的这些字符,但是我们也不需要看懂,反正这个对象已经存好啦,以后内存能读懂就好。
存好之后我们接下来来读取它:
报了一个异常,类没有找到:
因为如果这个文件中根本没有存储对象,存储了一些其他数据的话,就返回不了对象,所以它报了一个新的异常:类没有找到异常。
那这个异常我们也抛一下,干脆抛个大的,把父类抛出去了:
运行,取到对象的值啦:
这样,将这个java文件和存储对象的文件obj.txt发给其他人,其他人也可以用程序读到obj中的对象。
接下来,我们给Person类中的name属性私有化一下:
然后把文件夹中之前编译Person.java产生的Person.class删掉,重新编译一下。
运行:
挂掉了,因为两个序列号不一致。
我们再把Person类中刚刚给name属性添加的private去掉,再编译、运行,又正常了。
这说明了序列号就是根据成员获取出来的,加了修饰符之后,这个成员变量锁定的UID的数字签名的值就发生变化了。
那我们有一个想法,即使Person类中的name被私有化了,我们也想之前定义的那个对象可以被使用。有个办法,不要让Java帮我们定义UID了,我们自己来定义。
怎么定义呢?
我们找到Serializable接口,这里面有一句话:
将它复制过来:
我们重新给obj.txt中写入一个lisi0对象。写入之后,读取:
都没有问题。
接下来我们将Person类中的name私有化:
运行:
这次就可以了。
因为UID的值没有再变了。而UID就是给类一个固定的标识,固定标识的目的就是为了序列化方便,新的类还能操作曾经被序列化的对象。
接下来给Person加一个国籍属性:
运行:
让国籍也可以被设置:
传入新的带国籍的对象:
存好之后我们读取一下,发现kr这个国籍没有读出来,还是之前的cn:
为什么会这样呢?
记住,静态是不能被序列化的。
而我们刚刚添加的country属性就是static的:
道理很简单,静态是在方法区中的,而对象是在堆中的。
它可以将堆中的数据序列化,却不能将其他地方的数据序列化。
那我们也不想将age序列化怎么办呢?
对于非静态的成员,如果也不想将它序列化,可以加上一个修饰词:transient。这样就保证了它的值在堆内存中存在而不在文本文件中存在。
OK,再重新运行一下,发现年龄没有了:
另外注意喔,我们一般不会将对象存成txt文件。我们打开它看也没有意义,也看不懂。这节课是为了方便我们看和理解才存成txt的。一般会存成这样的文件:
还有,我们存入多个对象的时候,像这样:
每readObj一次,就返回一个对象,下一次再执行readObj,就返回下一个对象。
02-IO流(管道流)
读取流和写入流之间通常没有关系,那什么时候才能结合着来使用呢?
中间需要一个中转站。
就是读的时候把数据存到一个数组里面去,写的时候再操作数组就可以了。
而到了管道流中,它们可以对接在一起。
那么一根管子,这边读,这边写,到底谁先执行呢?
我们来看一下管导流的介绍,这是管道输入流PipeInputStream:
那怎样将它和管道输出流连接上呢?
通过构造函数:
或者这个对象创建的是空参数的,这时我们可以通过它里面的一个方法connect让它们连接上:
代码示例:
主函数:
运行,读到数据了:
我们分析一下,有两个线程,一个执行Read中的run,一个执行Write中的run,它们两个谁抢到资源不重要。假设Read中的run先抢到资源了,它就建立了一个buf数组,并调用read方法读取这个数组,返回长度,可是这时这个数组中并没有数据,所以它就等在这里不动了。这时另一个线程就执行了,即Write中的run,因为它处于就绪状态,这个线程就写入了"piped lai la"这个数据,这个数据写到哪里去了呢?写到这个管道中来了:
所以我们就看到了最后的运行结果,它读到了这个数据。
我们可以在Write的run写入数据之前停6s:
同时给Read中的run也加上这两句话:
这样的运行结果什么时候执行的哪个就一目了然了:
当然,哪个先执行是不一定的。像这次,就是写入数据先执行:
03-IO流(RandomAccessFile)
接下来说一下IO包中一个非常特殊的对象,就是RandomAccessFile,我们发现它的名称没有后缀名。
由上,RandomAccessFile不算是IO体系中的子类,而是直接继承自Object接口。但是,它是IO包中的成员,因为它具备读和写的功能。它在内部封装了一个数组,而且通过指针对数组的元素进行操作,可以通过getFilePointer获取指针位置,同时可以通过seek改变指针的位置。
其实,它能够完成读写的原理就是内部封装了字节输入流和输出流。
为什么不封装字符流而是封装字节流呢?
上图也提到了是一个大型byte数组,byte数组当然操作字节啦。
通过构造函数可以看出,该类只能操作文件,而且操作文件还有模式:
这个mode是什么呢,点进去一看究竟:
再点:
代码示例:
运行之后,我们发现文档中它查表将97转成a了:
write方法有一个特点,就是只写出int型数据的最低八位,比如说我们想写一个258,那么它的最低八位:
这样就造成了数据的丢失。
这两个例子我们发现两个问题,1,查表之后将数据转换了,2,丢失数据了。
对于丢数据的问题,用这个方法才是最靠谱的:
改:
OK,写完之后就开始读了。读之前我们先玩一下这个权限,我们这里试着设置了只读,又调用了写方法:
所以运行之后权限不够拒绝访问:
下面就开始读啦。
我们想把年龄取出来,用这个方法:
代码:
我们现在不想取李四了,想取王五。
而这个文件中的数据其实是在数组中存着,所以我们可以通过调整指针的位置来实现。调整指针有两种方式,第一种方式就是seek方法。
我们先看一下指针移动的原理:
读四个字节,铛铛铛铛读到这里:
然后来了个readInt,也是读四个字节:
铛铛铛铛读到这里:
下面我们想取王五,就需要把指针挪到8这里:
我们可以通过seek方法来实现:
取到了:
所以,我们可以通过seek方法取到文件中的任意数据,但有一个前提,就是得保证数据是有规律的,没有规律的话取起来就老费劲了。
如果姓名和年龄都是由8个字节组成,我们就可以通过8的倍数来取姓名和年龄。比如我们想取第1个人的:
取第2个人的:
还有一个方法就是skipBytes:
代码:
但是很遗憾的是,skipBytes只能往下跳,不能往回走。
而seek是前后都能指,爱指哪指哪,所以用途比skipBytes要大得多。
除了读还能写,而且还能随机的往里写。这个是它最666的方法。
(我们都知道,流在操作数据的时候都是按顺序写按顺序读)
我们现在想把“周期”存在第四个位置:
我们发现周期前面就空出了一段:
我们直接读周期没有问题,直接将周期写到指定位置也没有问题。
它不只能随机的读写,还能对数据进行修改,比如将第一个位置也换成周期:
如果模式为只读r,不会创建文件,会去读取一个已存在的文件,如果该文件不存在,则会出现异常。
如果模式为读写rw,若该文件不存在,会自动创建,如果存在则不会覆盖。
比如:
ran1.txt并不存在,而且这里的模式为只读:
则会出现异常:
而将它的模式变成读写rw,这样运行就没有问题了:
04-IO流(操作基本数据类型的流对象DataStream)
DataInputStream与DataOutputStream:可以用于操作基本数据类型的数据的流对象。
话不多说,直接用代码来表示它的用法:
运行后,我们看看data.txt文档的属性,13个字节,靠谱:
文档中的内容:
因为都是查表之后做了转换,所以我们看不懂,看不懂没有关系,只要能读出来就好啦。
接下来读:
我们发现,它还有一个writeUTF方法(使用UTF-8修改版编码):
我们用这种方式写入字符串的话,只能用它对应的方式读出来。用转换流读不出来。
代码示例:
我们用两种方式UTF-8和UTF-8修改版分别写了两个文件utf.txt和utfdate.txt:
utf.txt内容:
大小为6个字节:
utfdata.txt大小为8个字节:
如果想用utf-8来读utfdata.txt中的数据,读不出来,但可以读出来utf.txt中的数据。所以用writeUTF方式写的话,只能用它对应的方式读出来。
我们再用gbk编码集写一个gbk.txt:
运行之后,内容还是一样的,但是大小变成了4个字节:
现在读utfdate.txt,我们只能这么去做:
而读utf.txt就会报错:
它报的是这个异常:
因为readUTF要读8个字节,可是现在就读了6个,还没有读完呢,就到结尾了,没读完就到结尾了,数据能正确吗~肯定不能呀。所以就抛出了异常。
UTF-8修改版和UTF-8区别不是特别大,但是它的编码方式发生了变化,它们的不同有:
这节课的总结,记住,凡是操作基本数据类型,就用DataInputStream。
还有,用writeUTF写的数据,只有用readUTF才能读出来。
05-IO流(ByteArrayStream)
说完了能操作对象的和能操作基本数据类型的,接下来我们说一下能操作字节数组的:ByteArrayInputStream和ByteArrayOutputStream。
但是字节流内部不是本身封装的就是字节数组吗?那么这两个类的出现有什么意义呢?
我们看一下ByteArrayInputStream:
也就是说,ByteArrayInputStream它负责的是源,它会直接将源数据所对应的字节存储进内部缓冲区。
也就是说,这个对象一建立,它就有一个数据源存在的:
这个流对象,它有调用过底层资源吗?没有,所以它有写这句话:
关不关都一样。
再看一下ByteArrayOutStream:
而这个对象在构造的时候就不需要封装目的了:
因为目的已经在这个对象的内部了:一个可变长度的数组。
总结一下,ByteArrayInputStream:在构造的时候,需要接收数据源,而且数据源是一个字节数组。
ByteArrayOutputStream:在构造的时候,不用定义数据目的,因为该对象中已经内部封装了可变长度的字节数组,这就是数据目的地。
因为这两个流对象都操作的是数组,并没有使用系统资源,所以不用进行close关闭。
代码示例:
在流操作规律讲解时:
源设备:
键盘 System.in,硬盘 FileStream,内存 ArrayStream。
目的设备:
控制台 System.out,硬盘 FileStream,内存 ArrayStream。
而上面这个例子中的源设备和目的设备都是内存。
还有一个问题。
有人说,不就是把这个字符串变成数组:
然后new一个数组,再把刚刚那个数组的内容倒到新数组中来嘛。
可是我们自己手动建立数组也可以呀,这是可以的,但是:1,它封装好了我们可以直接拿来用;2,把数组进行封装,不光是提高封装性、代码的复用性、提供更简单的功能,我们对数组的操作无非是两种情况,设置和获取,反映到IO中就是读和写,这叫做用流的读写思想来操作数组。
接下来简单介绍一下writeTo方法:
大概像这样:
注意这个方法抛出了异常:
而这个对象中应该就这一个方法抛出异常了。
除了操作字节数组的对象:ByteArrayInputStream和ByteArrayOutputStream,还有操作字符数组的对象:CharArrayReader和CharArrayWrite,操作字符串的对象:StringReader和StringWriter。
我们会用操作字节数组的对象,后面两个就不用再特别讲了,因为方法和原理都是一样一样的。
06-IO流(转换流的字符编码)
字符流的出现是为了方便操作字符,而之所以会方便操作字符的原因是内部加入了编码表。
字节和字符之间的转换需要通过两个对象来完成:InputStreamReader和OutputStreamWriter,这是两个加入了编码表的对象,它俩非常特殊,要记住。
还有两个加入了编码表的对象:PrintStream和PrintWriter,但是它俩只能去打印而不能去读取。所以说,玩编码表的话,还得以转换流为主。
接下来问题来了,什么是编码表呢?
编码表的由来:计算机只能识别二进制数据,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字,就将各个国家的文字用1和0的数字来表示,并一一对应,形成一张表。这就是编码表。
常见的编码表有:
UTF-8是全世界通用的。这里面产生了一个问题,UTF-8和GBK的码表都识别中文,可是同一个中文文字在这两张码表中对应的数字却不是同一个。这时就涉及到了编码转换问题。
代码示例:
用UTF-8编码存储到utf.txt文件中:
文件内容:
文件大小为6字节:
存储原理,先将每个文字在编码表中找到对应的数字,然后将数字存入文件中:
那么问题来了,为什么我们打开文件看到的是文字呢?
我们另存为这个文件看一下,它的编码就是UTF-8:
它在打开的时候,文本文档会在UTF-8的编码表中对照着数字进行查找,找到相应的文字,最后我们看到的就是文字了。
用GBK编码存储到gbk.txt中,这里我们没有指定编码表,因为默认的就是GBK编码表:
gbk.txt文档的内容:
它的大小为4字节:
存储原理也是查表:
用GBK编码读取gbk.txt文件的数据:
读取结果:
读取原理,查表:
如果不小心把编码这里写成UTF-8了:
结果会变成这样:
为什么会是两个?呢?
原因是:
因为都没有找到,所以是未知字符,返回了??。
正常读UTF-8略。
用GBK编码读UTF-8:
结果为:
出现这个结果的原因:
That's all.