I/O 流

IO(Input Output)用于实现对数据的输入与输出操作,Java把不同的输入/输出源(键盘、文件、网络等)抽象表述为流(Stream)。流是从起源到接收的有序数据,有了它程序就可以采用同一方式访问不同的输入/输出源。

  • 按照数据流向,可以将流分为输入流和输出流,其中输入流只能读取数据、不能写入数据,而输出流只能写入数据、不能读取数据。
  • 按照数据类型,可以将流分为字节流和字符流,其中字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符。
  • 按照处理功能,可以将流分为节点流和处理流,其中节点流可以直接从/向一个特定的IO设备(磁盘、网络等)读/写数据,也称为低级流,而处理流是对节点流的连接或封装,用于简化数据读/写功能或提高效率,也称为高级流。

Java提供了大量的类来支持IO操作,下表给大家整理了其中比较常用的一些类。其中,黑色字体的是抽象基类,其他所有的类都继承自它们。红色字体的是节点流,蓝色字体的是处理流。
image.png
根据命名很容易理解各个流的作用:

  • 以File开头的文件流用于访问文件;
  • 以ByteArray/CharArray开头的流用于访问内存中的数组;
  • 以Piped开头的管道流用于访问管道,实现进程之间的通信;
  • 以String开头的流用于访问内存中的字符串;
  • 以Buffered开头的缓冲流,用于在读写数据时对数据进行缓存,以减少IO次数;
  • InputStreamReader、InputStreamWriter是转换流,用于将字节流转换为字符流;
  • 以Object开头的流是对象流,用于实现对象的序列化;
  • 以Print开头的流是打印流,用于简化打印操作;
  • 以Pushback开头的流是推回输入流,用于将已读入的数据推回到缓冲区,从而实现再次读取;
  • 以Data开头的流是特殊流,用于读写Java基本类型的数据。

怎么用流打开一个大文件?

打开大文件,应避免直接将文件中的数据全部读取到内存中,可以采用分次读取的方式。

  1. 使用缓冲流。缓冲流内部维护了一个缓冲区,通过与缓冲区的交互,减少与设备的交互次数。使用缓冲输入流时,它每次会读取一批数据将缓冲区填满,每次调用读取方法并不是直接从设备取值,而是从缓冲区取值,当缓冲区为空时,它会再一次读取数据,将缓冲区填满。使用缓冲输出流时,每次调用写入方法并不是直接写入到设备,而是写入缓冲区,当缓冲区填满时它会自动刷入设备。
  2. 使用NIO。NIO采用内存映射文件的方式来处理输入/输出,NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样来访问文件了(这种方式模拟了操作系统上的虚拟内存的概念),通过这种方式来进行输入/输出比传统的输入/输出要快得多。

普通 Socket 方法处理的 Server

服务端

  1. public class Server {
  2. public static void main(String[] args) {
  3. try {
  4. // 创建一个端口来监听
  5. ServerSocket server = new ServerSocket(8080);
  6. // 等待请求
  7. Socket socket = server.accept();
  8. // 接收的请求后使用socket进行通信,创建BufferedReader
  9. BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  10. String line = is.readLine();
  11. System.out.println("received from client: " + line);
  12. // 创建PrintWrite,用于发送数据
  13. PrintWriter pw = new PrintWriter(socket.getOutputStream());
  14. pw.println("received data: " + line);
  15. pw.flush();
  16. // 关闭资源
  17. pw.close();
  18. is.close();
  19. socket.close();
  20. server.close();
  21. } catch (IOException e) {
  22. e.printStackTrace();
  23. }
  24. }
  25. }

客户端

  1. public class Client {
  2. public static void main(String[] args) {
  3. String msg = "Clinet data";
  4. try {
  5. // 创建一个socket,跟本机8080相连
  6. Socket socket = new Socket("127.0.0.1",8080);
  7. PrintWriter pw = new PrintWriter(socket.getOutputStream());
  8. BufferedReader is = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  9. // 发送数据
  10. pw.println(msg);
  11. pw.flush();
  12. // 接收数据
  13. String line = is.readLine();
  14. System.out.println("received from server :" + line);
  15. // 关闭资源
  16. pw.close();
  17. is.close();
  18. socket.close();
  19. } catch (IOException e) {
  20. e.printStackTrace();
  21. }
  22. }
  23. }

NIO

程序执行效率更多的是由IO的效率决定的,JVM在IO操作方面效率不足。在操作系统中,可以从硬件上直接读取大块的数据,然而JVM的IO更喜欢小块数据的读取。在JDK4中引入了NIO,可以最大限度的满足java程序IO的需求。

java.nio包中定义了各种Buffer相关的类。
NIO中有三大组件:Channel ,Buffer ,Selector

Channel

程序和数据源之间的传输不是直接的,是经过buffer的,程序的读写数据都是针对buffer,然后buffer通过channel与数据源进行交互。

  • IO流是线程阻塞的,在调用read() / write() 读写数据的时候,线程阻塞,直到数据读取完毕或者数据完全写入,在读写过程中,线程不能做其他任务。
  • NIO不是线程阻塞的,当线程从Channel中读取数据时,如果通道中没有可用的数据,线程不阻塞,可以做其他的任务。

Buffer

buffer本质就是一个数组,把数组的内容与信息包装成一个buffer对象,它提供了一组访问这些信息的方法。

buffer缓冲区的重要属性:

  • capacity容量,创建缓冲区的时候指定大小,创建后不能再修改,如果缓冲区满了,需要清空后才能继续写入。
  • position表示当前位置
  • limit上限 指第一个不能被读出或写入的位置。limit上限后面的单元既不能读也不能写。
  • mark标记,设置一个标记位置,可以调用mark方法。

Selector

Selector选择器是java NIO中能够检测一到多个NIO通道,并能够知晓通道是都为诸如读写事件做好准备的组件。