Java 的 I/O 操做类在包 java.io 下,有将近 80 个类。css
按数据格式分类:前端
按做用位置分类:java
(1)面向字节:操做以8位为单位对二进制数据进行操做,不对数据进行转换。这些类都是InputStream 和 OutputStream的子类。以InputStream/OutputStream为后缀的类都是字节流,能够处理全部类型的数据。数据库
(2)面向字符:操做以字符为单位,读时将二进制数据转换为字符,写时将字符转换为二进制数据Writer 和 Reader的子类,以Writer/Reader为后缀的都是字符流。后端
硬盘上全部的文件都是以字节形式保存,字符只在内存中才会造成。即只在处理纯文本文件时,优先考虑使用字符流,除此以外都用字节流。浏览器
其中:缓存
字符流:服务器
字节流:网络
读写操做实例:数据结构
/** * 使用FileReader进行读取文件,而后FileWriter写入另外一个文件 */ @Test public void testFileReaderAndFileWriter() throws IOException { FileReader fileReader = new FileReader("h:\\haha.txt"); char[] buff = new char[512]; StringBuffer stringBuffer = new StringBuffer(); while (fileReader.read(buff) > 0) { stringBuffer.append(buff); } System.out.println(stringBuffer.toString()); FileWriter fileWriter = new FileWriter("h:\\haha2.txt"); fileWriter.write(stringBuffer.toString().trim()); fileWriter.close(); System.out.println("写入文件成功"); } /** * 使用InputStreamReader进行读取文件,而后用OutputStreamWriter写入文件 */ @Test public void testInputStreamReader() throws IOException { //操做数据的方式是能够组合的,此处FileInputStream读出的字节流用InputStreamReader转化为了字符流对象 InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream("h:\\haha.txt"), "utf-8"); char[] buff = new char[512]; StringBuffer stringBuffer = new StringBuffer(); while (inputStreamReader.read(buff) > 0) { stringBuffer.append(buff); } System.out.println(stringBuffer.toString());
//写文件时,要指定写入的地方(网络或本地)、路径 OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream("h:\\haha2.txt"), "utf-8"); outputStreamWriter.write(stringBuffer.toString().trim()); outputStreamWriter.close(); } @Test public void testIntputStream2() throws IOException { InputStreamReader inputStreamReader = new InputStreamReader(new StringBufferInputStream("hello world")); char[] buff = new char[512]; int n = inputStreamReader.read(buff); System.out.println(n); System.out.println(buff); }
FileReader类继承了InputStreamReader,FileReader读取文件流,经过StreamDecoder解码成char,其解码字符集使用的是默认字符集。在Java中,咱们应该使用File对象来判断某个文件是否存在,若是咱们用FileOutputStream或者FileWriter打开,那么它确定会被覆盖。
(1) 磁盘IO工做机制
前面介绍了基本的 Java I/O 的操做接口,这些接口主要定义了如何操做数据,以及介绍了操做两种数据结构:字节和字符的方式。还有一个关键问题就是数据写到何处,其中一个主要方式就是将数据持久化到物理磁盘,下面将介绍如何将数据持久化到物理磁盘的过程。
数据在磁盘的惟一最小描述是文件,也就是说上层应用程序只能经过文件来操做磁盘上的数据,文件也是操做系统和磁盘驱动器交互的一个最小单元。值得注意的是 Java 中一般的 File 并不表明一个真实存在的文件对象,当你经过指定一个路径描述符时,它就会返回一个表明这个路径相关联的一个虚拟对象,这个多是一个真实存在的文件或者是一个包含多个文件的目录。
什么时候真正会要检查一个文件存不存?就是在真正要读取这个文件时,例如 FileInputStream 类都是操做一个文件的接口,注意到在建立一个 FileInputStream 对象时,会建立一个 FileDescriptor 对象,其实这个对象就是真正表明一个存在的文件对象的描述,当咱们在操做一个文件对象时能够经过 getFD() 方法获取真正操做的与底层操做系统关联的文件描述。例如能够调用 FileDescriptor.sync() 方法将操做系统缓存中的数据强制刷新到物理磁盘中。
从磁盘读取文件过程:
当传入一个文件路径,将会根据这个路径建立一个 File 对象来标识这个文件,而后将会根据这个 File 对象建立真正读取文件的操做对象,这时将会真正建立一个关联真实存在的磁盘文件的文件描述符 FileDescriptor,经过这个对象能够直接控制这个磁盘文件。因为咱们须要读取的是字符格式,因此须要 StreamDecoder 类将 byte 解码为 char 格式,至于如何从磁盘驱动器上读取一段数据,由操做系统帮咱们完成。
(2)网络IO工做机制(Socket)
Socket 描述计算机之间完成相互通讯一种抽象功能。Socket 也同样有多种,大部分状况下咱们使用的都是基于 TCP/IP 的流套接字,它是一种稳定的通讯协议。
下典型的基于 Socket 的通讯的场景:
主机 A 的应用程序要能和主机 B 的应用程序通讯,必须经过 Socket 创建链接,而创建 Socket 链接必须须要底层 TCP/IP 协议来创建 TCP 链接。创建 TCP 链接须要底层 IP 协议来寻址网络中的主机。咱们知道网络层使用的 IP 协议能够帮助咱们根据 IP 地址来找到目标主机,可是一台主机上可能运行着多个应用程序,如何才能与指定的应用程序通讯就要经过 TCP 或 UPD 的地址也就是端口号来指定。这样就能够经过一个 Socket 实例惟一表明一个主机上的一个应用程序的通讯链路了。
(TCP/UDP:找端口号,从而与应用程序通讯。IP:找主机)
当客户端要与服务端通讯,客户端首先要建立一个 Socket 实例,操做系统将为这个 Socket 实例分配一个没有被使用的本地端口号,并建立一个包含本地和远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中直到这个链接关闭。在建立 Socket 实例的构造函数正确返回以前,将要进行 TCP 的三次握手协议,TCP 握手协议完成后,Socket 实例对象将建立完成,不然将抛出 IOException 错误。
与之对应的服务端将建立一个 ServerSocket 实例,ServerSocket 建立比较简单只要指定的端口号没有被占用,通常实例建立都会成功,同时操做系统也会为 ServerSocket 实例建立一个底层数据结构,这个数据结构中包含指定监听的端口号和包含监听地址的通配符,一般状况下都是“*”即监听全部地址。以后当调用 accept() 方法时,将进入阻塞状态,等待客户端的请求。当一个新的请求到来时,将为这个链接建立一个新的套接字数据结构,该套接字数据的信息包含的地址和端口信息正是请求源地址和端口。这个新建立的数据结构将会关联到 ServerSocket 实例的一个未完成的链接数据结构列表中,注意这时服务端与之对应的 Socket 实例并无完成建立,而要等到与客户端的三次握手完成后,这个服务端的 Socket 实例才会返回,并将这个 Socket 实例对应的数据结构从未完成列表中移到已完成列表中。因此 ServerSocket 所关联的列表中每一个数据结构,都表明与一个客户端的创建的 TCP 链接。
传输数据是咱们创建链接的主要目的,如何经过 Socket 传输数据:
当链接已经创建成功,服务端和客户端都会拥有一个 Socket 实例,每一个 Socket 实例都有一个 InputStream 和 OutputStream,正是经过这两个对象来交换数据。同时咱们也知道网络 I/O 都是以字节流传输的。当 Socket 对象建立时,操做系统将会为 InputStream 和 OutputStream 分别分配必定大小的缓冲区,数据的写入和读取都是经过这个缓存区完成的。写入端将数据写到 OutputStream 对应的 SendQ 队列中,当队列填满时,数据将被发送到另外一端 InputStream 的 RecvQ 队列中,若是这时 RecvQ 已经满了,那么 OutputStream 的 write 方法将会阻塞直到 RecvQ 队列有足够的空间容纳 SendQ 发送的数据。值得特别注意的是,这个缓存区的大小以及写入端的速度和读取端的速度很是影响这个链接的数据传输效率,因为可能会发生阻塞,因此网络 I/O 与磁盘 I/O 在数据的写入和读取还要有一个协调的过程,若是两边同时传送数据时可能会产生死锁,在后面 NIO 部分将介绍避免这种状况。
提高磁盘 I/O 性能一般的方法:
网络 I/O 优化一般有一些基本处理原则:
参考连接:https://www.ibm.com/developerworks/cn/java/j-lo-javaio/