对于任何程序设计语言而言,输入输出(Input/Output)系统都是非常核心的功能。程序运行需要数据,数据的获取往往需要跟外部系统进行通信,外部系统可能是文件、数据库、其他程序、网络、IO设备等等。外部系统比较复杂多变,那么我们有必要通过某种手段进行抽象、屏蔽外部的差异,从而实现更加便捷的编程。
I/O 问题是任何编程语言都无法回避的问题,可以说 I/O 问题是整个人机交互的核心问题,因为 I/O 是机器获取和交换信息的主要渠道。在当今这个数据大爆炸时代,I/O 问题尤其突出,很容易成为一个性能瓶颈。正因如此,所以 Java 在 I/O 上也一直在做持续的优化。
IO分为两大类:
在整个Java.io包中最重要的就是5个类和一个接口。5个类指的是File、OutputStream、InputStream、Writer、Reader;一个接口指的是Serializable。其中常用类如下:
类 | 说明 |
---|---|
File | 文件类 |
RandomAccessFile | 随机存取文件类 |
InputStream | 字节输入流 |
OutputStream | 字节输出流 |
Reader | 字符输入流 |
Writer | 字符输出流 |
Java I/O主要包括如下几个层次,包含三个部分:
1.流式部分――IO的主体部分;
2.非流式部分――主要包含一些辅助流式部分的类,如:File类、RandomAccessFile类和FileDescriptor等类;
3.其他类–文件读取部分的与安全相关的类,如:SerializablePermission类,以及与本地操作系统相关的文件系统的类,如:FileSystem类和Win32FileSystem类和WinNTFileSystem类。
File类: 文件和文件目录路径的抽象表示形式,与平台无关
流的本质:数据传输,根据数据传输特性将流抽象为各种类,方便更直观的进行数据操作。
流的作用:为数据源和目的地建立一个输送通道,处理设备之间的数据传输。
在程序中所有的数据都是以流的方式进行传输或保存的,程序需要数据的时候要使用输入流读取数据,而当程序需要将一些数据保存起来的时候,就要使用输出流完成。程序中的输入输出都是以流的形式保存的,流中保存的实际上全都是字节文件。
根据处理数据单位的不同分为:字符流和字节流
根据数据流向不同分为:输入流和输出流
根据流的角色不同分为:节点流和处理流
节点流:直接从数据源或目的地读写数据
处理流:不直接连接到数据源或目的地,而是“连接” 在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。如缓冲流等
流序列中的数据既可以是未经加工的原始二进制数据,也可以是经一定编码处理后符合某种格式规定的特定数据。因此Java中的流分为两种:
字符流和字节流的使用范围:
字节流一般用来处理图像,视频,以及PPT,Word类型的文件
字符流一般用于处理纯文本类型的文件,如TXT文件等
字节流可以用来处理纯文本文件,但是字符流不能用于处理图像视频等非文本类型的文件。
根据数据的输入、输出方向的不同对而将流分为输入流和输出流。
此输入、输出是相对于我们写的代码程序而言。
输入流 | 输出流 | |
---|---|---|
字节流 | InputStream | OutputStream |
字符流 | Reader | Writer |
//使用字节流FileInputStream处理文本文件,可能出现乱码。 @Test public void testFileInputStream() { FileInputStream fis = null; try { //1. File实例化 File file = new File("hello.txt"); //2. 流实例化 fis = new FileInputStream(file); //3.读数据 byte[] buffer = new byte[5]; int len;//记录每次读取的字节的个数 while((len = fis.read(buffer)) != -1){ String str = new String(buffer,0,len); System.out.print(str); } } catch (IOException e) { e.printStackTrace(); } finally { if(fis != null){ //4.关闭资源 try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } }
OutputStream 是所有的输出字节流的父类,它是一个抽象类
注:在网络传输中使用的是字节流传输,所以需要序列化与反序列化
/* 实现对图片的复制操作 */ @Test public void testFileInputOutputStream() { FileInputStream fis = null; FileOutputStream fos = null; try { // File srcFile = new File("爱情与友情.jpg"); File destFile = new File("爱情与友情2.jpg"); // fis = new FileInputStream(srcFile); fos = new FileOutputStream(destFile); //复制的过程 byte[] buffer = new byte[5]; int len; while((len = fis.read(buffer)) != -1){ fos.write(buffer,0,len); } } catch (IOException e) { e.printStackTrace(); } finally { if(fos != null){ // try { fos.close(); } catch (IOException e) { e.printStackTrace(); } } if(fis != null){ try { fis.close(); } catch (IOException e) { e.printStackTrace(); } } } }
Reader 是所有的输入字符流的父类,它是一个抽象类。
/** * 使用字符输入流FileReader读取文件内容 * 1. read()的理解:返回读入的一个字符。如果达到文件末尾,返回-1 * 2. 异常的处理:为了保证流资源一定可以执行关闭操作。需要使用try-catch-finally处理 * 3. 读入的文件一定要存在,否则就会报FileNotFoundException。 */ @Test public void testFileReader() { //1 File类的实例化 File file = new File("hello.txt"); // 参数为相对路径形式 System.out.println(file.getAbsolutePath()); //2 FileReader流的实例化 FileReader reader = null;// 为了保证流资源的关闭,使用try-catch-finally把对流的操作包起来 try { reader = new FileReader(file); // 可能会报文件找不到异常 //3 文件内容的读取 // read()方法返回读取的一个字符。若读到文件末尾,则返回-1 int data = reader.read(); while (data != -1) { System.out.print((char) data); data = reader.read(); } } catch (IOException e) { e.printStackTrace(); } finally { //4 流的关闭 //垃圾回收机制只回收JVM堆内存里的对象空间。 //对其他物理连接,比如数据库连接、输入流输出流、Socket连接JVM无法进行自动回收 try { if (reader != null) // 可能会报空指针异常 reader.close(); } catch (IOException e) { e.printStackTrace(); } } }
Writer是所有的输出字符流的父类,它是一个抽象类。
/** * 使用字符输出流FileWriter从内存中写出数据到文件 * 1. 输出操作,对应的File可以不存在的。并不会报异常 * 2. File对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建此文件。 * File对应的硬盘中的文件如果存在: * 如果流使用的构造器是:FileWriter(file,false) / FileWriter(file):对原有文件的覆盖 * 如果流使用的构造器是:FileWriter(file,true):不会对原有文件覆盖,而是在原有文件基础上追加内容 */ @Test public void testFileWriter() throws IOException { // 使用throws解决异常,但是推荐使用try-catch-finally //1 File类实例化,指明要写出到的文件 File file = new File("mine.txt"); //2 字符输出流的实例化 FileWriter fileWriter = new FileWriter(file); //3 写出操作 fileWriter.write("1zdpzdpzdpzdpnb"); fileWriter.write("2zdpzdpzdpzdpnb"); //4 流的关闭 fileWriter.close(); }
转换流的作用,文本文件在硬盘中以字节流的形式存储时,通过InputStreamReader读取后转化为字符流给程序处理,程序处理的字符流通过OutputStreamWriter转换为字节流保存。
InputStreamReader:字节到字符的桥梁,将字节流以字符流输入。
OutputStreamWriter:字符到字节的桥梁,将字节流以字符流输出。
这两个流对象是字符体系中的成员,它们有转换作用,本身又是字符流,所以在构造时需要传入字节流对象进来。
字节流没有缓冲区,是直接输出的,而字符流是输出到缓冲区的。因此在输出时,字节流不调用close()方法时,信息已经输出了,而字符流只有在调用close()方法关闭缓冲区时,信息才输出。要想字符流在未关闭时输出信息,则需要手动调用flush()方法。
结论:只要是处理纯文本数据,就优先考虑使用字符流。除此之外都使用字节流。
有的时候我们想要把一个Java对象变成字节流的形式传出去,有的时候我们想要从一个字节流中恢复一个Java对象。例如,有的时候我们想要把一个Java对象写入到硬盘或者传输到网路上面的其它计算机,这时我们就需要自己去通过java把相应的对象写成转换成字节流。对于这种通用的操作,可以使用序列化与反序列化来实现。
对象流ObjectInputStream与ObjectOutputStream用于处理存储与读取基本数据类型或对象的处理流。它的强大之处就是可以把对象写入到数据源中,也能把对象从数据源出还原回来。
对象流ObjectInputStream与ObjectOutputStream不能序列化static和transient修饰的成员变量。
序列化可以在传递和保存对象时,保证对象的完整性和可传递性,对象转换为有序字节流可以使数据在网络中的传输或者保存至本地文件中变得更加方便。序列化与反序列化的核心是对象状态的保存与重建。
对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。
所有可能在网络上传输的对象的类都应该是可序列化的,否则程序将会出现异常,比如RMI(Remote Method Invoke,即远程方法调用,是JavaEE的基础)过程中的参数和返回值;所有需要保存到磁盘里的对象的类都必须可序列化,比如Web应用中需要保存到 HttpSession 或 ServletContext 属性的Java对象。
因为序列化是RMI过程的参数和返回值都必须实现的机制,而RMI又是Java EE技术的基础——所有的分布式应用常常需要跨平台、跨网络,所以要求所有传递的参数、返回值必须实现序列化。**因此序列化机制是Java EE平台的基础。**通常建议:程序创建的每个JavaBean类都实现Serializable。
序列化的好处在于可将任何实现了Serialization接口的对象转化为字节数据,使其在保存和传输时可被还原。同时,序列化也是RMI(远程方法调用)的参数和返回值必须要实现的机制,因此序列化机制是RMI的基础。
如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须要实现如下两个接口之一:
如果某个类的属性不是基本数据类型【默认可被序列化】或 String 类型,而是另一个引用类型,那么这个引用类型也必须是可序列化的,否则拥有该类型的Field类也不能被序列化。
Serializable接口是一个标志性接口(Marker Interface),也就是说,该接口并不包含任何具体的方法,是一个空接口,仅仅用来判断该类是否能够序列化。
其中被序列化的java对象是String类型的。因为String类本身实现了Serializable接口,所以可以被序列化:
读取网络传过来的或者是本地的字节流文件,还原为JAVA对象信息。
在一些特殊的场景下,如果一个类里包含的某些Field值是敏感信息,例如银行账户信息等,这时不希望系统将该Field值进行序列化;或者某个Field的类型是不可序列化的,因此不希望对该Field进行递归序列化,以避免引发
java.io.NotSerializableException异常。
此时,我们就需要自定义序列化了。自定义序列化的常用方式有两种:
transient关键字只能用于修饰Field,不可修饰Java程序中的其他成分。使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。
使用transient关键字修饰Field虽然简单、方便,但被transient修饰的Field将被完全隔离在序列化机制之外,这样导致在反序列化恢复Java对象时无法取得该Field值。Java还提供了一种自定义序列化机制,通过这种自定义序列化机制可以让程序控制如何序列化各Field,甚至完全不序列化某些Field(与使用transient关键字的效果相同)。在序列化和反序列化过程中需要特殊处理的类应该提供如下特殊签名的方法,这些特殊的方法用以实现自定义序列化。
private void writeObject(java.io.ObjectOutputStream out) throws IOException private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException; private void readObjectNoData() throws ObjectStreamException;
凡是实现了Serializable接口的类都有一个表示序列化版本标识符的静态变量——serialVersionUID。
serialVersionUID 用来表明累的不同版本之间的兼容性。其目的是以序列化对象进行版本控制,判断有关各版本反序列化时是否兼容。
如果类没有显示地定义这个静态变量,该值将由JAVA运行时环境根据类的内部细节自动生成。若类的实例变量做了修改,serialVersionUID 可能会发生变化,故建议显式声明。
简单来说,序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本的一致性。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类中的serialVersionUID进行比较,如果相同就认为是一致的,就可以进行反序列化操作,否则就会出现序列化版本不一致的异常。
序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级,就无法正确反序列化;不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。
然后在依次执行序列化与反序列化操作,执行成功:
修改自定义类Person,添加属性id后,直接去执行反序列化操作将会报错【因为在反序列化操作时,将找不到要还原的那个类,因此会还原失败】:
在Person中设置了 serialVersionUID 之后,要首先在原有不变的属性基础上进行一次序列化和反序列化操作,将UID与两个序列化操作进行绑定起来。然后再修改自定义类Person,添加属性id,然后直接去执行反序列化操作也将不会报错【因为有了版本一致性校验】:
Java序列化存在四个致命缺点,导致其不适用于网络传输:
在真正的生产环境中,一般会选择其它编解码框架,领先的跨平台结构化数据表示是 JSON 和 Protocol Buffers,也称为 protobuf。JSON 由 Douglas Crockford 设计用于浏览器与服务器通信,Protocol Buffers 由谷歌设计用于在其服务器之间存储和交换结构化数据。JSON 和 protobuf 之间最显著的区别是 JSON 是基于文本的,并且是人类可读的,而 protobuf 是二进制的,但效率更高。
先来举个实例生活中的例子:
如果你想吃一份宫保鸡丁盖饭:
同步阻塞:你到饭馆点餐,然后在那等着,还要一边喊:好了没啊!
同步非阻塞:在饭馆点完餐,就去遛狗了。不过溜一会儿,就回饭馆喊一声:好了没啊!
异步阻塞:遛狗的时候,接到饭馆电话,说饭做好了,让您亲自去拿。
异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心遛狗就可以了。
一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作。
同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO。
阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。
而阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。
所以,IO操作可以分为3类:同步阻塞(即早期的BIO操作)、同步非阻塞(NIO)、异步非阻塞(AIO)。
同步阻塞(BIO):
在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式。
同步非阻塞(NIO):
在此种方式下,用户进程发起一个IO操作以后便可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而导致不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。
异步非阻塞(AIO):
此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序。
同步阻塞IO(JAVA BIO):
同步并阻塞,服务器实现模式为一个连接对应一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
同步非阻塞IO(Java NIO):
同步非阻塞,服务器实现模式为一个请求对应一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。用户进程也需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问。
异步阻塞IO(Java NIO):
此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄(如果从UNP的角度看,select属于同步操作。因为select之后,进程还需要读写数据),从而提高系统的并发性!
异步非阻塞IO(Java AIO(NIO.2)):
在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。