Java基础day21笔记:对象的序列化|管道流|RandomAccessFile|操作基本数据类型的流对象DataStream|ByteArrayStream|转换流的字符编码

    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.

你可能感兴趣的:(Java基础day21笔记:对象的序列化|管道流|RandomAccessFile|操作基本数据类型的流对象DataStream|ByteArrayStream|转换流的字符编码)