关键词:对象流、管道流、随机访问流、基本数据流、字节数组流、字符编码、
对象流
可以直接操作对象的流,ObjectInputStream和ObjectOutputStream。这2个类必须成对使用,对对象文件的写入和读取是固定的。
通过ObjectOutputStream将堆内存中的对象的基本数据类型和图形存到硬盘上;通过ObjectInputStream读取(重构)对象。将对象内的数据存到硬盘上,日后想使用的话,直接读取硬盘即可,即为“对象的持久化”,或叫“对象的序列化”。
将对象和流相结合,构造时需要有目的,即字节流;然后再将字节流和文件关联。
write(int val)将int型变成byte型写入;writeInt(int val),直接输出int型;writeObject(Object obj)将对象写入。
类必须实现Serializable接口才能实现序列化功能。该接口属于IO体系,需要导包;但是没有方法需要覆盖,这种接口称为“标记接口”。原理是给类加了UID标识,供编译器使用。类可能会改变,经编译后,流无法识别这个类的对象。
想要再次操作对象,可以重新写入读取对象;也可以自己编写UID,例如,static final long serialVersionUID = 42L;相当于加上固定的门牌号,新类还可以操作之前被序列化的对象,返回的是改变后的类的对象。
但是静态数据无法被序列化,一旦确定,无法被重新赋值。因为只能序列化堆中的内容。使用transisent修饰非静态成员变量,可以使其无法被序列化,保证在堆中但不会出现在文件中。
对象序列化后是无法看懂的,因为对象的字节被编码表翻译后都是乱码。可以读取或写入对象中的基本数据类型数据。
通过ObjectInputStream,可以将文件中的对象读取并使用,此文件也可以传递给其他人使用,这就是对象的持久化存储的好处,本质是一种封装,将对象变成字节后封装。文件名一般是类名+Object,例如,person.object。
多个对象就是写入多次、读取多次即可,内部会自动逐个操作。
管道流
PipedInputStream和PipedOutputStream,一般的输入流和输出流之间需要一个中转站(例如,数组)才能完成数据的传输,而管道流可以直接连接使用。涉及到输入和输出谁先执行的问题,需要使用多线程来解决这个问题,不建议使用单线程。这是涉及到多线程技术的IO流对象。
连接方法:构造函数中添加,或connect()方法。
read()方法是阻塞式方法,没有内容时,必须等待。所以2个线程谁先执行没有影响,读取先执行的话会自动等待,直到有数据后再执行。
RandomAccessFile随机访问流
随机访问文件,自身具备读写方法。
不是IO体系中的子类,直接继承Object,自成一个体系。但是IO包的成员,因为其具备读和写的功能,
对象内部封装有大型的byte数组和指针,而且通过指针对元素进行操作。获取元素就是读或者写。
完成读写的原理:内部封装了字节输入流和字节输出流。许多方法和流的操作一致。
通过构造函数,可以看出该类只能操作硬盘上的文件,其他如内存、键盘录入都不可以操作。构造函数也是将流作为参数。
①操作文件的模式:只接受4种值:只读“r”、读写“rw”、“rws”、“rwd”。注意模式的设置。
②可以直接写入基本数据类型。例如,字节流里的write()方法只能写出最后8位,即将int转成byte,只能写出1个字节。再就是自动通过GBK表进行编码。可以通过writeInt()进行修正,可以写出4个字节。同样,通过readInt()获得4个字节的数据。
内部封装了字节数组和指针,通过设置指针获得对应的字节,从而获得任意(Random)的数据。但是数据需要有一定规律或次序,否则很难读写。而且数据需要分段,事先要考虑好单个数据的大小,例如,姓名需要留出16个字节,即8个汉字的位置。用空来补位。
通过getFilePointer()获取指针位置。③最重要的是:通过seek()设置指针位置,进而读取或写入;向前向后都可以。通过skipBytes(int x)跳过指定的字节数,获得字节。但是只能向前跳,不能向后跳。
可以随机读取,也可以随机写入到指定位置。重新写入的话,不会覆盖文件,只会覆盖部分数据。不同于输出流,输出流会直接覆盖整个文件。
如果模式为只读r,不会创建文件,只会读取已有的文件;如果该文件不存在,则会出现异常。
如果模式为读写rw,该对象的构造函数要操作的文件如果不存在,会自动创建;如果存在,则不会覆盖,只会不断的写入。
可以实现数据的分段写入,用一个线程来负责一段数据的写入,互相直接没有干扰。例如,多线程下载。多个线程同时写入数据,写完后拼成一个完整的文件。普通流是必须从头到尾一次性写成。IO中只有这个类可以完成多线程写入。
基本数据流
DataInputStream和DataOutputStream
可以用于操作基本数据类型数据的流对象,也就是将基本数据类型和流关联起来,功能是操作基本数据类型,所以构造函数就需要传入流,而且是字节流。
存入文本文件中,记事本会将存入的字节按照GBK编码表翻译成字符,基本无法识别。因为数据最重要的读取使用。
读取数据时需要按照顺序,使用不同的数据类型读取。写入的顺序:基本数据——》字节——》字符(文本文件)。读取的顺序:字符(文本文件)——》字节——》基本数据。
writeUTF(String str),使用UTF码写入。只能用readUTF()读取。
字节数组流
ByteArrayInputStream和ByteOutputStream
ByteArrayInputStream,连接的是源,将源数据对应的字节存入自己的内部缓冲区,也就是说,对象一建立就必须有数据源存在,数据源是字节数组(内存中的数据),字节数组作为参数传入构造函数。读取流都有这种特点,创建对象时必须有数据源导入。
ByteArrayOutputStream,在构造时不要定义数据目的,因为自动将数据写入一个字节数组,且数组长度可变。不涉及底层资源操作,关闭无效。
因为操作的是字节数组,属于内存中的内容,没有操作底层资源,close()关闭无效,仍然可以使用。也不会产生任何IOException,因为直接将数据存入字节数组中。
设备及其流
源设备:键盘System.in、硬盘FileStream、内存ArrayStream
目的设备:控制台System.out、硬盘FileStream、内存ArrayStream
针对源、目的是内存的情况,使用字节数组流最方便。将字节数组封装在其中,提高了复用性。对于数组的操作,只有设置和获取,反应到IO中就是读和写,字节数组流的本质就是用流的思想操作数组。
writeTo(),将字节输出流中的内容写入到某个输出流中。会报异常。
字符数组流
CharArrayReader和CharArrayWriter,类似于字节数组流。
字符串流
StringReader和StringWriter,也类似于字节数组流。
字符编码
字符流的出现是为了方便操作字符。内部加入了编码表,所以能够将数字转换成字符。字节和字符之间的转换通过转换流完成,InputStreamReader和OutputStreamWriter。另外两个能使用转换的是PrintStream和PrintWriter,但只能写入不能读取。构造时都可以加入编码表。
编码表,就是将数字和各国的文字对应起来,组成一张表。二进制——》数字——》文字。类似于键值对。
ASCII:美国标准信息交换码,一个字节7位。IOS8859-1,拉丁码表,欧洲码表,一个字节8位。GB2312,中国码表,GBK,升级版中国码表,两个字节16位。各国码表之间有冲突,所以产生了Unicode国际码表,两个字节16位,java中的字符使用此码表。UTF-8国际码表的升级版,三个字节,加了标识头方便区分码表,目前的通用码表。除了美国表,其他都是以1开头,所以数字都是负数。
转换流的特点
本质上存在硬盘的都是二进制,然后转成数字。记事本内部有编码表,可以按照不同的码表自动将数字翻译成字符。使用a码表翻译b码表的文件,可以运行,但无法输出正确内容。
编码:字符串——》字节数组,getBytes();可以使用默认编码表(GBK)或指定编码表获得字节。
解码:字节数组——》字符串,new String(byte[] buf);可以按照默认编码表(GBK)或者指定编码表编码。
中国区常用的只有GBK和UTF-8,服务器常用IOS8859-1。编写时,使用其他码表会编码出错误内容,也就不用解码了;编写正确,使用ISO8859-1解码,会产生乱码,此时需要使用ISO8859-1编码,再使用原码表解码,即可获得原有字符。但此方法不适用于UTF-8,因为UTF-8也识别中文,会产生不必要的字节。
如何确定使用哪种编码?UTF-8加上了标识头信息,通过不同的1和0 的排列,以便确定每次读取几个字节。“联通”这两个字的GBK编码的字节的二进制,符合UTF-8的特征,使得记事本会按照UTF-8解码,导致了错误。这是极少的GBK和UTF-8重复的地方,可以通过加上汉字来修正。