全网好评万字全面整理Java字节流体系,小白也可直接飞升
[啤酒]满怀忧思,不如先干再说!做干净纯粹的技术分享!欢迎评论区或私信交流!
[微风]代码已上传【gitee】,因平台限制,可在个人首页简介中免费获取链接!
上一篇文章介绍了IO模型的理论知识,本章通过代码介绍 字节流读写文件 的操作,主要内容属于BIO,通过本章你可以了解到: IO流的分类和IO流体系结构,即常见的流类都有哪些代码实现普通的字节流读写文件代码实现缓冲流的使用代码实现数据流的使用代码实现对象流的使用代码实现顺序流的使用代码实现打印流的使用
其中涵盖了节点流【低级流】和处理流【高级流】,文章内容超万字,建议收藏阅读,别忘了点赞,关注哦流分类
IO流主要的分类方式有以下3种: 按数据流的方向:输入流、输出流 按处理数据单位:字节流、字符流 按功能:节点流、处理流
输入流与输出流
输入与输出是相对于应用程序而言的,比如文件读写,读取文件是将数据从文件中读取到系统中,属于输入流,写文件是将系统内数据写到文件内,属于输出流
字节流和字符流
字节流和字符流的用法几乎完成全一样,区别在于字节流和字符流所操作的数据单元不同
字节流 : 它处理单元为1个字节(byte),操作字节和字节数组,存储的是二进制文件,如果是音频文件、图片、歌曲,就用字节流好点(1byte = 8位);
字符流 : 它处理的单元为2个字节的Unicode字符,分别操作字符、字符数组或字符串,字符流是由Java虚拟机将字节转化为2个字节的Unicode字符为单位的字符而成的,如果是关系到中文(文本)的,用字符流好点(1Unicode = 2字节 = 16位);
注意:所有文件的存储都是以字节形式存储的,即使是纯文本文件,在磁盘上保留的并不是文件的字符而是先把 字符编码成字节 ,再储存这些字节到磁盘。在读取文件(特别是文本文件)时,也是一个字节一个字节地读取以形成字节序列。可以认为字符流是对字节流通过转换 包了一层外壳,方便处理纯文本的数据
节点流和处理流
节点流: 直接操作数据读写的流类,文件和应用程序直接点到点传输,比如FileInputStream,FileOutputStream
处理流: 对一个已存在的流的增强或包装,为程序提供功能强大、灵活的读写功能,例如BufferedInputStream(缓冲字节流),这里一般使用到装饰者模式
在诸多处理流中,有一个非常重要,那就是缓冲流。
因为程序与磁盘的交互相对于内存运算很慢,容易成为程序性能瓶颈。减少程序与磁盘的交互频率,是提升程序效率一种有效手段。缓冲流,就应用这种思路:普通流每次读写一个字节,而缓冲流在内存中设置一个缓存区,缓冲区先存储足够的待操作数据后,再与内存或磁盘进行交互。这样,在总数据量不变的情况下,通过提高每次交互的数据量,减少了交互次数。
字节流
缓冲字节流
有的地方称:节点流是低级流,处理流是高级流。这种叫法知道就行 BIO
BIO是一种阻塞式IO,起源于JDK1.0,存在于java.io包下,本章主要介绍BIO中的字节流,下方为Java BIO的主要应用类,通过思维导图分类整理好
Java的IO特点: java.io 包中几乎包含了所有操作输入、输出需要的类,所有这些流类代表了输入源和输出目标。 java.io 包中的流支持很多种格式,比如:基本类型、对象、本地化字符集等等,一个流可以理解为一个数据的序列。 输入流表示从一个源读取数据,输出流表示向一个目标写数据。 Java 为 I/O 提供了强大的而灵活的支持,使其更广泛地应用到 文件传输 和网络编程 中。字节流
字节流是指传输过程中,传输数据的最基本单位是字节的流,一个不包含边界数据的连续流;字节流是由字节组成的,主要用在处理二进制数据。 字节输入流【InputStream】
InputStream 是字节输入流的顶级抽象类,是所有字节输入流的最终父类,其结构如下
InputStream抽象类 实现 Closeable接口,Closeable接口继承 AutoCloseable接口,AutoCloseable接口中定义了 close() 方法抛出Exception,表示输入流使用后需要关闭 。
Closeable接口继承close方法没有做具体实现,方法抛出IOException。
InputStream常用方法
方法名
作用
read()
从输入流中读取下一个字节的数据。 值字节以0到255范围内的int形式返回。 如果由于已到达流末尾而没有可用字节,则返回值-1 。 此方法会阻塞,直到输入数据可用、检测到流结束或抛出异常为止
read(byte b[])
从输入流中读取一定数量的字节并将它们存储到缓冲区数组b 。 实际读取的字节数作为整数返回。 此方法会阻塞,直到输入数据可用、检测到文件结尾或抛出异常。 如果b的长度为零,则不读取字节并返回0 ; 否则,将尝试读取至少一个字节。 如果由于流位于文件末尾而没有可用字节,则返回值-1 ; 否则,至少读取一个字节并将其存储到b 。 读取的第一个字节存储到元素b[0] ,下一个存储到b[1] ,依此类推。 读取的字节数最多等于b的长度。 令k为实际读取的字节数; 这些字节将存储在元素b[0]到b[ k -1] ,而元素b[ k ]到b[b.length-1]不受影响。
read(byte b[], int off, int len)
从输入流中读取最多len个字节的数据到一个字节数组中。 尝试读取多达len个字节,但可能会读取较小的数字。 实际读取的字节数作为整数返回。 读取的第一个字节存储到元素b[off] ,下一个存储到b[off+1] ,依此类推
skip(long n)
跳过并丢弃此输入流中的n字节数据,在跳过n个字节之前到达文件末尾, 返回实际跳过的字节数并抛出异常。 如果n为负,则InputStream类的skip方法始终返回 0
available()
返回当前可以跳过的最大值
close()
关闭流
markSupported()
如果此流实例支持标记和重置方法,则为true ; 否则为false
mark(int readlimit)
标记此输入流中的当前位置。 对reset方法的后续调用将此流重新定位在最后标记的位置,以便后续读取重新读取相同的字节。
reset()
将此流重新定位到上次在此输入流上调用mark方法时的位置 字节输出流【OutputStream】
OutputStream是字节输出流的顶级抽象类,是所有字节输出流的最终父类,除了实现Closeable接口之外,另外实现了Flushable接口,结构如下
实现Closable接口作用和InputStream一样,使用完之后关闭流,另外实现的Flushable接口是为了将读取到用户内存中的数据刷新到内核中再由操作系统写入到磁盘内
OutputStream常用方法
方法名
作用
write(int b)
将指定的字节写入此输出流
write(byte b[])
将指定字节数组中的b.length个字节写入此输出流
write(byte b[], int off, int len)
将指定字节数组中的len个字节从偏移量off开始写入此输出流。 write(b, off, len)的一般约定是将数组b中的某些字节按顺序写入输出流; 元素b[off]是写入的第一个字节, b[off+len-1]是此操作写入的最后一个字节
flush()
刷新此输出流并强制写出任何缓冲的输出字节
close()
关闭此输出流并释放与此流关联的任何系统资源 FileInputStream和FileOutputStream
FileInputStream:最普通的字节输入流类,通过原始的字节方式读取文件,如图像数据,视频,ppt文档等,如果读取字符文件,如:txt,yml等纯文本文档则考虑使用,如果要读取的文件不存在则抛出异常,初始化主要通过File对象实现。
FileOutputStream:与FileInputStream相辅相成,通过原始的字节方式输出数据到目标文件中,如果目标文件不存在则自动创建文件,初始化通过File和append参数。file:目标文件 append:是否将数据追加到文件后,默认为false,意为覆盖文件,为true则将数据追加到文件末尾
拷贝图片案例public class FileInputOutputStreamTest { public static void main(String[] args) { // 1、设置源文件路径 File srcFile = new File("D:PicturesJavaBIO结构.png"); // 2、设置拷贝文件路径 File distFile = new File("D:PicturesJavaBIO结构-copy.png"); FileInputStream fileInputStream = null; FileOutputStream fileOutputStream = null; try { // 3、创建文件输入流 fileInputStream = new FileInputStream(srcFile); // 4、创建输出流 fileOutputStream = new FileOutputStream(distFile); // 循环读取数据,每次读到的数据存进read中,不等于-1就表示文件还没读完 int read; while ((read = fileInputStream.read()) != -1) { // 写数据,将每次独到的数据写入到输出流中 fileOutputStream.write(read); } } catch (FileNotFoundException e) { System.out.println("文件不存在"); throw new RuntimeException(e); } catch (IOException e) { System.out.println("数据读取失败"); throw new RuntimeException(e); }finally { try { if(fileOutputStream!=null) { // 刷新流 fileOutputStream.flush(); // 关闭流 fileOutputStream.close(); } if(fileInputStream!=null){ // 关闭流 fileInputStream.close(); } } catch (IOException e) { throw new RuntimeException(e); } } } }读写大文件
此处通过最原始的read和write读写一个2G文件测试需要多久,之后再通过指定字节数组读取,看看性能是否有所提升,首先通过 最原始的方式读写2G文件 public class FileInputOutputStreamTest2 { public static void main(String[] args) { // 获取开始时间 long startTime = System.currentTimeMillis(); // 1、设置源文件路径 File srcFile = new File("D:iotest 1-2Gsrc.zip"); System.out.println("源文件大小===》" + srcFile.length()); // 2、设置拷贝文件路径 File distFile = new File("D:iotest 1-2G-copy.zip"); System.out.println("目标文件大小===》" + distFile.length()); FileInputStream fileInputStream = null; FileOutputStream fileOutputStream = null; try { // 3、创建文件输入流 fileInputStream = new FileInputStream(srcFile); // 4、创建输出流 fileOutputStream = new FileOutputStream(distFile); // 循环读取数据,每次读到的数据存进read中,不等于-1就表示文件还没读完 int read; while ((read = fileInputStream.read()) != -1) { // 写数据,将每次独到的数据写入到输出流中 fileOutputStream.write(read); } } catch (FileNotFoundException e) { System.out.println("文件不存在"); throw new RuntimeException(e); } catch (IOException e) { System.out.println("数据读取失败"); throw new RuntimeException(e); }finally { try { if(fileOutputStream!=null) { // 刷新流 fileOutputStream.flush(); // 关闭流 fileOutputStream.close(); } if(fileInputStream!=null){ // 关闭流 fileInputStream.close(); } } catch (IOException e) { throw new RuntimeException(e); } } // 获取结束时间 long endTime = System.currentTimeMillis(); // 计算时间差 long useTime = (endTime-startTime) / 1000; System.out.println("耗时===》" + useTime + "秒"); } }
通过GC工具可以查看程序运行期间的GC信息, 耗时1小时7分钟 ,读写中我一度想终止,这种程序已不想运行第二次
可以通过输出read方法每次读取的字节数,发现最多不会超过255个字节【还记得上边的read方法介绍吧, 前后呼应学习才能更高效 】
甚至有时还会读取0字节数据
读取一次再通过输出流的write方法写入一次数据,那好吧一个2G的文件一共是 2147483648 字节,按照每次读取最多255 字节数据,都需要循环8421505 次,性能极低无比
那么想办法解决一下
上边的方法相当于挑水的时候,一瓢一瓢的倒水,可以弄一个水桶,灌满一桶水再倒进水缸中,这样速度能快点,可以弄一个字节数组,读取数据先放到数组中,满了之后再将数组数据写出去,类似一批一批的处理,read方法也提供了字节数组的方式暂存数据,具体实现如下:public class FileInputOutputStreamTest3 { public static void main(String[] args) { // 获取开始时间 long startTime = System.currentTimeMillis(); // 1、设置源文件路径 File srcFile = new File("D:iotest 1-2Gsrc.zip"); // 2、设置拷贝文件路径 File distFile = new File("D:iotest 1-2G-copy.zip"); FileInputStream fileInputStream = null; FileOutputStream fileOutputStream = null; try { // 3、创建文件输入流 fileInputStream = new FileInputStream(srcFile); // 4、创建输出流,此时因为运行了Test2目标文件已存在,append参数默认为flase就是覆盖文件 // 如果设置为true,就是追加数据 fileOutputStream = new FileOutputStream(distFile); // 定义字节数组,大小为1024 * 1024字节,也就是1M byte[] buff = new byte[1024 * 1024]; // 记录本次循环总共读取的字节数 int len; // 循环读取,将读到的字节数存储进len中,因为如果是最后一次读取,不一定能把数组存满 while ((len = fileInputStream.read(buff)) != -1) { // 写数据,从buff数组中的第0位写到第len位,因为最后一次数组不一定存满, // 如果从头写到尾,当最后一次读取数据不能填满数组时,就会多写进去部分数据 fileOutputStream.write(buff,0,len); } } catch (FileNotFoundException e) { System.out.println("文件不存在"); throw new RuntimeException(e); } catch (IOException e) { System.out.println("数据读取失败"); throw new RuntimeException(e); }finally { try { if(fileOutputStream!=null) { // 刷新流 fileOutputStream.flush(); // 关闭流 fileOutputStream.close(); } if(fileInputStream!=null){ // 关闭流 fileInputStream.close(); } } catch (IOException e) { throw new RuntimeException(e); } } // 获取结束时间 long endTime = System.currentTimeMillis(); // 计算时间差 long useTime = (endTime-startTime) / 1000; System.out.println("耗时===》" + useTime + "秒"); } }
此时拷贝一个2G文件只需耗时2秒,右侧的GC次数和内存占用都比第一种方案优秀
因为是本地IO所以速度很快,跟计算机的内存配置和磁盘配置都有关系,不过整体来说比第一种方案优秀很多
注意:这里写数据时不要调用write(byte[] b)方法,该方法每次都将数组内的所有数据写入,再次强调,当读取到最后一次时,数组不一定被填满,也就是数据没那么多了,此时数组后边都填充了默认值byte数组的默认值都是0,也就是文件会多若干个0,文件就会变大,如果你发现你的文件拷贝后变大了,那么可能就是这个问题。
如下图:只需要将有数据的部分写进去就行,而len变量记录的就是本次读取到了多少数据,读取到len位置即可
BufferedInputStream和BufferedOutputStream
BufferedInputStream 和 BufferedOutputStream是字节缓冲流,是一种处理流【高级流】,但是不能单独使用,需要与节点流【低级流】配合使用 BufferedInputStream
BufferedInputStream继承FilterInputStream,自带缓冲区,默认大小为8192
不能单独使用,根据构造方法发现需要传入InputStream与节点流配合使用,一般都传入FileInputStream //创建一个 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。创建一个内部缓冲区数组并将其存储在 buf 中,该buf的大小默认为8192。 public BufferedInputStream(InputStream in); //创建具有指定缓冲区大小的 BufferedInputStream 并保存其参数,即输入流 in,以便将来使用。创建一个长度为 size 的内部缓冲区数组并将其存储在 buf 中。 public BufferedInputStream(InputStream in,int size);BufferedOutputStream
BufferedOutputStream继承FilterOutputStream,构造方法默认字节大小8192
同样不能单独使用,根据构造方法发现需要传入OutputStream与节点流配合使用,一般都传入FileOutputStream //创建一个新的缓冲输出流,以将数据写入指定的底层输出流。 public BufferedOutputStream(OutputStream out); //创建一个新的缓冲输出流,以将具有指定缓冲区大小的数据写入指定的底层输出流。 public BufferedOutputStream(OutputStream out,int size);实现文件复制public class BufferedInputOutputStreamTest { public static void main(String[] args) throws IOException { long start = System.currentTimeMillis(); // 创建节点流 FileInputStream fis = new FileInputStream("D:" + File.separator + "iotest" + File.separator + "01-2Gsrc.zip"); FileOutputStream fos = new FileOutputStream("D:" + File.separator + "iotest" + File.separator + "01-2Gsrc-copy.zip"); // 创建缓冲流 BufferedInputStream bis = new BufferedInputStream(fis); BufferedOutputStream bos = new BufferedOutputStream(fos); int read; byte[] buff = new byte[1024 * 1024]; // 读数据 while ((read = bis.read(buff)) != -1) { // 写数据 bos.write(buff,0,read); } // 刷新流 bos.flush(); // 关闭流 bis.close(); bos.close(); long end = System.currentTimeMillis(); System.out.println("耗时:"+(end-start) / 1000 +"秒"); } }
耗时和上边通过字节数组的方式基本没多大差异,很多人就迷惑了,那不都一样吗,是的表面看着都一样,不过 内部原理不同 ,以 BufferedInputStream 为例:
BufferedInputStream 通过预先读入一整段原始输入流数据至 缓冲区 中,而外界对BufferedInputStream的读取操作实际上是在缓冲区上进行 ,如果读取的数据超过了缓冲区的范围,那么BufferedInputStream负责重新从原始输入流中载入下一截数据填充缓冲区,然后外界继续通过缓冲区进行数据读取。
这样的设计的好处是:避免了大量的磁盘IO ,因为原始的InputStream类实现的read是即时读取的,即每一次读取都会是一次磁盘IO操作【哪怕只读取了1个字节的数据】,可想而知,如果数据量巨大,这样的磁盘消耗非常可怕。而通过缓冲区的实现,读取可以读取缓冲区中的内容,当读取超过缓冲区的内容后再进行一次磁盘IO,载入一段数据填充缓冲,那么下一次读取一般情况下就直接可以从缓冲区读取,减少了磁盘IO。
此处使用 装饰模式,即在原始的节点流之上套了一层缓冲效果,来改善性能。
FileInputStream的read()方法调用了read0,read0方法你瞧是一个native本地方法,这种方法都是直接调用操作系统了,而read(byte b[])也是同样的道理
而 BufferedInputStream则是从字节数组中获取数据返回
因为目前只运行了一个程序,对磁盘的影响可以忽略不计,如果你的程序很胖,或者服务器上运行了多个产品,对性能的影响就会毫不掩盖的暴露出来。 ByteArrayInputStream和ByteArrayOutputStream
ByteArrayInputStream 和 ByteArrayOutputStream通过直接操作字节数组的形式读写数据,并不适合应用于文件的读写操作,适用于网络编程中的数据传递,在【图片压缩】一文中曾使用过他们两个进行数据转换ByteArrayInputStream
包含一个内部缓冲区 buf[],该缓冲区存储从流中读取的字节。该流类中定义了一下三个变量
pos变量: 记录 要从输入流缓冲区中读取的下一个数据的索引,并且不大于count变量。从输入流缓冲区读取的下一个字节将是buf[pos]。
count变量: 记录当前buf数组的长度,值不大于buf.length
mark变量: 标记的索引,可通过mark()方法和reset()方法进行设置,不设置的时候,调用第一个构造方法时值为0,调用第二个构造方法时mark值被设置成offset
包含两个构造方法: // 接收字节数组buf. public ByteArrayInputStream(byte buf[]) {} // 接收字节数组buf,偏移量offset【数组中读取数据的索引起始位置】,length是从offset索引开始读取字节的长度.即读取的是buf数组中从offset到offset+length之间数据 public ByteArrayInputStream(byte buf[], int offset, int length) {}ByteArrayOutputStream
包含字节数组 buf[],存储数据缓冲区,count变量记录缓冲区中的有效字节数
构造方法也有两个: // 默认缓冲区大小为32字节 public ByteArrayOutputStream() { this(32); } // 可以指定缓冲区大小,如果小于0则抛出异常 public ByteArrayOutputStream(int size) { if (size < 0) { throw new IllegalArgumentException("Negative initial size: " + size); } buf = new byte[size]; }
通过这两个类有没有发现,都是基于byte数组操作数据,也就是说数据存储在内存中, 并没有使用操作系统的磁盘等硬件 ,一般使用这两个类用来做网络通信,并不会使用他们做文件读写操作 public class ByteArrayInputOutputStreamTest { public static void main(String[] args) throws IOException { // 通过FileInputStream读取文件 File file = new File("D:PicturesJavaBIO结构.png"); FileInputStream fileInputStream = new FileInputStream(file); ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); int read; byte[] buff = new byte[1024]; while ((read = fileInputStream.read(buff)) != -1) { // 将数据暂存到 ByteArrayOutputStream 中 byteArrayOutputStream.write(buff,0,read); } FileOutputStream outputStream = new FileOutputStream("D:PicturesJavaBIO结构-copy.png"); // 将字节数组输出流数据输出到FileOutputStream中,完成拷贝 byteArrayOutputStream.writeTo(outputStream); fileInputStream.close(); outputStream.close(); } }
由此发现,ByteArrayOutputStream 和 ByteArrayInputStream数据操作都是基于内存的,并不能直接对文件进行读写,一般都会应用在网络IO方面,来读取数据包数据,通过字节数组形式传递数据
注意:FileOutputStream 和 FileInputStream在调用了close方法之后仍然可以使用,不会关闭,也不会跑出IOException异常 DataInputStream 和 DataOutputStream
DataInputStream 和 DataOutputStream是基于 InputStream流 和 OutputStream流衍生出的子类,当然他们还有父类DataInput 和 DataOutput。它们是 针对Java基本数据类型读写操作。这两个类读写的文件都是针对机器,DataOutputStream写出去的数据不是给人看的是给机器看的【即写出去的数据为字节格式】。而DataInputStream只能读取DataOutputStream写出去的文件
DataInputStream
构造方法如下:传入基础输入流,读取数据实际上从基础输入流读取 public DataInputStream(InputStream in) { super(in);}
内部方法: // 从数据输入流读取数据存储到字节数组b中 public final int read(byte b[]) throws IOException {} // 从数据输入流中读取数据存储到数组b里面,位置从off开始,长度为len个字节 public final int read(byte b[], int off, int len) throws IOException {} // 从数据输入流中循环读取b.length个字节到数组b中 public final void readFully(byte b[]) throws IOException {} // 从数据输入流中循环读取len个字节到字节数组b中.从b的off位置开始 public final void readFully(byte b[], int off, int len) throws IOException {} // 跳过n个字节 public final int skipBytes(int n) throws IOException {} // 从数据输入流读取布尔类型的值 public final boolean readBoolean() throws IOException {} // 从数据输入流中读取一个字节 public final byte readByte() throws IOException {} // 从数据输入流中读取一个无符号的字节,返回值转换成int类型 public final int readUnsignedByte() throws IOException {} // 从数据输入流读取一个short类型数据 public final short readShort() throws IOException {} // 从数据输入流读取一个无符号的short类型数据 public final int readUnsignedShort() throws IOException {} // 从数据输入流中读取一个字符数据 public final char readChar() throws IOException {} // 从数据输入流中读取一个int类型数据 public final int readInt() throws IOException {} // 从数据输入流中读取一个long类型的数据 public final long readLong() throws IOException {} // 从数据输入流中读取一个float类型的数据 public final float readFloat() throws IOException {} // 从数据输入流中读取一个double类型的数据 public final double readDouble() throws IOException {} // 从数据输入流中读取用UTF-8格式编码的UniCode字符格式的字符串 public final String readUTF() throws IOException {}DataOutputStream
与DataInputStream基本对应,构造方法如下:参数传入的基础输出流,将数据实际写到基础输出流中 public DataOutputStream(OutputStream out) { super(out); }
内部方法: // 数据输出流增加的字节数 private void incCount(int value) {} // 将int类型的b写到数据输出流中 public synchronized void write(int b) throws IOException {} // 将字节数组b中off位置开始,len个长度写到数据输出流中 public synchronized void write(byte b[], int off, int len) throws IOException {} // 刷新数据输出流 public void flush() throws IOException {} // 将布尔类型的数据写到数据输出流中,底层是转化成一个字节写到基础输出流中 public final void writeBoolean(boolean v) throws IOException {} // 将一个字节写到数据输出流中(实际是基础输出流) public final void writeByte(int v) throws IOException {} // 将一个short类型的数据写到数据输出流中,底层将v转换2个字节写到基础输出流中 public final void writeShort(int v) throws IOException {} // 将一个charl类型的数据写到数据输出流中,底层是将v转换成2个字节写到基础输出流中 public final void writeChar(int v) throws IOException {} // 将一个int类型的数据写到数据输出流中,底层将4个字节写到基础输出流中 public final void writeInt(int v) throws IOException {} // 将一个long类型的数据写到数据输出流中,底层将8个字节写到基础输出流中 public final void writeLong(long v) throws IOException {} // 将一个float类型的数据写到数据输出流中,底层会将float转换成int类型,写到基础输出流中 public final void writeFloat(float v) throws IOException {} // 将一个double类型的数据写到数据输出流中,底层会将double转换成long类型,写到基础输出流中 public final void writeDouble(double v) throws IOException {} // 将字符串按照字节顺序写到基础输出流中 public final void writeBytes(String s) throws IOException {} // 将字符串按照字符顺序写到基础输出流中 public final void writeChars(String s) throws IOException {} // 以机器无关的方式使用utf-8编码方式将字符串写到基础输出流中 public final void writeUTF(String str) throws IOException {} // 写到数据输出流中的字节数 public final int size() {}读写案例public class DataInputOutputStreamTest { public static void main(String[] args) throws IOException { // 通过输出流向文件内写入数据 FileOutputStream fos = new FileOutputStream("D:iotestdatainput.txt"); DataOutputStream dos = new DataOutputStream(fos); // 写数据 dos.writeUTF("离离原上草"); dos.writeInt(123456); dos.writeBoolean(true); dos.writeShort(123); dos.writeLong(456L); dos.writeDouble(13.14); // 读取数据 DataInputStream dis = new DataInputStream(new FileInputStream("D:iotestdatainput.txt")); System.out.println(dis.readUTF()); System.out.println(dis.readInt()); System.out.println(dis.readBoolean()); System.out.println(dis.readShort()); System.out.println(dis.readLong()); System.out.println(dis.readDouble()); // 关闭流 dos.close(); dis.close(); } }
文件中的数据是通过16进制的形式存储,也就是字节形式存储
读取数据时也是读取文件中的字节,通过拼接得到Java基本数据类型输出
以读写Short为例的原理
写数据的方法源码: public final void writeShort(int v) throws IOException { out.write((v >>> 8) & 0xFF); out.write((v >>> 0) & 0xFF); incCount(2); }
读数据的方法源码: public final short readShort() throws IOException { int ch1 = in.read(); int ch2 = in.read(); if ((ch1 | ch2) < 0) throw new EOFException(); return (short)((ch1 << 8) + (ch2 << 0)); }
当数据输出流使用write写出一个short类型的数据,由于short类型 占2字节 ,即16位,分别移动8,0位,然后和0xFF【二进制是1111 1111】进行逻辑"&"运算分别得到2个8位。进行&0xFF操作,不会改变原先的数值【例如1100 0011&0xFF还是得到原先的11000011】,比如(v>>>8)&0xFF是为了取得v的高8位,而(v>>>0)&0xFF是为了取得低8位。然后两个字节分别存储
如果你的文件中存储的是字节写入的正常数据【即写入的是a,b,c或者汉字这样的人类语言字符】,使用DataInputStream读取时就会报错
出现 EOFException异常,表示流异常,DataInputStream只能读取由DataOutputStream写入的数据,或者说只能读取字节数据
ObjectInputStream 和 ObjectOutputStream
上边的DataInputStream只能读取基本数据类型和字符串类型数据,如果要读写一个对象类型【自定义类型】的数据,则需要使用 对象流,也就是此处要介绍的ObjectInputStream和ObjectOutputStream
ObjectInputStream和ObjectOutputStream是以 【对象】为数据源,但是必须将传输的对象进行序列化与反序列化操作。序列化以后的对象可以保存到磁盘上,也可以在网络上传输, 使得不同的计算机可以共享对象。【序列化的字节序列可以跨平台】
ObjectOutputStream:代表对象输出流,它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
ObjectInputStream:代表对象输入流,它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。
以下是 ObjectInputStream 继承关系
注意:只有实现了Serializable接口的类的对象才能被序列化。Serializable接口是一个空接口,只起到标记作用。如果对象的属性是对象,属性对应类也必须实现 Serializable 接口。 案例
通过对象输入流读取Person对象,再将Person对象存入文件中,首先创建Person类,重写了toString方法 public class Person implements Serializable { private String name; private int age; public Person(String name, int age) { this.name = name; this.age = age; } public Person() {} public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Person [name=" + name + ", age=" + age + "]"; } }
读写类 public class ObjectInputOutputStreamTest { public static void main(String[] args) throws IOException { write(); read(); } /** 使用对象输入流将数据读入程序 */ public static void read() { //1、创建对象流对象 ObjectInputStream ois = null; try { ois = new ObjectInputStream(new FileInputStream( "D:person.txt")); //2、读取对象 Person p = (Person) ois.readObject(); System.out.println(p); } catch (ClassNotFoundException | IOException e) { e.printStackTrace(); }finally { //3、关闭流 try { if(ois != null) ois.close(); } catch (IOException e) { e.printStackTrace(); } } } /** 使用对象输出流将数据写入文件 */ public static void write() { //1、创建对象流对象 ObjectOutputStream oos = null; try { oos = new ObjectOutputStream(new FileOutputStream( "D:person.txt")); //2、写对象 oos.writeObject(new Person("Tom", 23)); } catch (IOException e) { e.printStackTrace(); }finally { //3、关闭流 try { if(oos != null) oos.close(); } catch (IOException e) { e.printStackTrace(); } } } }
读写对象结果
注意: 对象流不仅可以读写对象,还可以读写基本数据类型。 使用对象流读写对象时,该对象必须序列化与反序列化。 Java内部提供的类【如Date、String等】已经实现了序列化接口,而自定义类【如本案例中的Person】必须手动实现序列化接口。 SequenceInputStream
SequenceInputStream 类可以将几个 输入流串联 在一起,合并为一个输入流。当通过这个类来读取数据时,它会依次从所有被串联的输入流中读取数据。对于程序来说,就好像是对同一个流操作。
继承结构如下:
SequenceInputStream类的构造方法为: // 在枚举类型的参数e中包含了若干需要被串联的输入流 SequenceInputStream(Enumeration e) // 参数s1和s2代表两个需要被串联的输入流。顺序输入流先读取s1中的数据,再读取s2中的数据。 SequenceInputStream(InputStream s1, InputStream s2)案例
如需要按顺序读取 1.txt,2.txt,3.txt,之后将数据存储进4.txt中public class SequenceInputStreamTest { public static void main(String[] args) throws IOException { List list = new ArrayList<>(); list.add(new FileInputStream("D:iotest1.txt")); list.add(new FileInputStream("D:iotest2.txt")); list.add(new FileInputStream("D:iotest3.txt")); // 创建顺序流 SequenceInputStream sequenceInputStream = new SequenceInputStream(Collections.enumeration(list)); byte[] buff=new byte[1024]; FileOutputStream out=new FileOutputStream("D:iotest4.txt"); int len; // 读写数据 while((len=sequenceInputStream.read(buff))!=-1) { out.write(buff,0,len); } // 关闭流 out.close(); sequenceInputStream.close(); } }PrintStream
PrintStream是一个处理流,可以为其他输出流增加打印功能,使其他流可以很方便的打印各种数据。PrintStream只负责输出数据,不能读取数据,不会抛出IOException,拥有特有的print和println方法
构造方法有: // 输出的目的地为字节输出流 public PrintStream(OutputStream out) // 输出到指定地址的文件 public PrintStream(String fileName) // 输出到指定文件 public PrintStream(File file)
案例:使用打印流输出数据到文件中 public class PrintStreamTest { public static void main(String[] args) throws FileNotFoundException { PrintStream ps = new PrintStream("D:iotestprintstraem.txt"); ps.write(97); ps.println(97); ps.close(); } }
发现,使用write方法会查询码表,找到对应字符记录下来,而使用println方法,则是原样输出, print与println可以打印任何数据类型的数据
我们经常使用的 System.out.println()其实就是根据System.out获取了一个PrintStream对象,进而输出数据到控制台上
修改输出位置 // 获取控制台输出的流对象 PrintStream printStream = new PrintStream("D:iotestchangePrint.txt"); // 修改默认输出位置 System.setOut(printStream); System.out.println("题序等你回");
发现控制台并没有输出,而在目标文件中有对应内容
总结Java提供了不同的流满足对文件和网络数据传输的读写操作Java满足对磁盘和内存的读写操作,对磁盘有FileInputStream、FileOutputStream等,对内存有BufferedInputStream、BufferedOutputStream和ByteArrayInputStream、ByteArrayOutputStream操作对象读写或者说序列化和反序列化在网络中传输,可以通过ObjectOutputStream和ObjectInputStream如果希望操作基本数据类型和String类的读写可以使用DataInputStream和DataOutputStream如果一次性需要兑取多个文件,则可以使用SequenceInputStream对多个输入流整合排序后一次性直接读取如果你仅仅需要打印或者输出数据则可以使用PrintStreamJava提供了多种丰富的流实现对不同数据的读写操作,恰恰也是这么多的流让开发者云里雾里,在此整理出来,希望可以帮助到读者,在开发时选择合适的流对象使用
本文主要整理字节流对不同类型数据的读写操作,下一篇介绍字符流的读写操作,看看与字节流都有什么不同