面试题 Java IO流

什么是IO流?

它是一种数据的流从源头流到目的地。比如文件拷贝,输入流和输出流都包括了。输入流从文件中读取数据存储到进程(process)中,输出流从进程中读取数据然后写入到目标文件。

Java中有几种类型的流?

按照流的方向:

  • 输入流(inputStream)
  • 输出流(outputStream)。

按照实现功能分:

  • 节点流(可以从或向一个特定的地方(节点)读写数据。如FileReader)
  • 处理流(是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写。如BufferedReader。处理流的构造方法总是要带一个其他的流对象做参数。一个流对象经过其他流的多次包装,称为流的链接。)

按照处理数据的单位:

  • 字节流
  • 字符流

字节流继承于InputStream和OutputStream
字符流继承于InputStreamReader和OutputStreamWriter

字节流和字符流的区别?

字节流在JDK1.0中就被引进了,用于操作包含ASCII字符的文件。JAVA也支持其他的字符如Unicode,为了读取包含Unicode字符的文件,JAVA语言设计者在JDK1.1中引入了字符流。ASCII作为Unicode的子集,对于英语字符的文件,可以可以使用字节流也可以使用字符流。

字节流有了为什么还要有字符流?

字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。

FileInputStream和FileOutputStream是什么?

这是在拷贝文件操作的时候,经常用到的两个类。在处理小文件的时候,它们性能表现还不错,在大文件的时候,最好使用BufferedInputStream (或 BufferedReader) 和 BufferedOutputStream (或 BufferedWriter)

Files的常用方法都有哪些?

  • Files. size():查看文件个数。
  • Files. read():读取文件。
  • Files. write():写入文件。
  • Files. exists():检测文件路径是否存在。
  • Files. createFile():创建文件。
  • Files. createDirectory():创建文件夹。
  • Files. delete():删除一个文件或目录。
  • Files. copy():复制文件。
  • Files. move():移动文件。

    什么是 java 序列化,如何实现 java 序列化?

    序列化

    是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决在对对象流进行读写操作时所引发的问题。

    序列化的实现

    将需要被序列化的类实现Serializable接口 ,该接口没有需要 实现的方法,implements Serializable只是为了标注该对象是可被序列化的,然后使用一个输出流(如:FileOutputStream)来构造
    一个 ObjectOutputStream(对象流)对象,接着,使用 ObjectOutputStream 对象的 writeObject(Object obj)方法就可以将参数为 obj的对象写出(即保存其状态),要恢复的话则用输入流。

    如何将一个 java 对象序列化到文件里?

    在 java 中能够被序列化的类必须先实现Serializable接口,该接口没有任何抽象方法只是起到一个标记作用。

    如何实现对象克隆?

    两种方式:

  • 实现 Cloneable 接口并重写 Object 类中的 clone()方法;

  • 实现 Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。

    注意:基于序列化和反序列化实现的克隆不仅仅是深度克隆,更重要的是通过泛型限定,可以检查出要克隆的对象是否支持序列化,这项检查是编译器完成的,不是在运行时抛出异常,这种是方案明显优于使用 Object 类的 clone 方法克隆对象。让问题在编译的时候暴露出来总是好过把问题留到运行时。

什么是BIO?

同步阻塞式IO,服务端创建一个ServerSocket,然后客户端用一个Socket去连接那个ServerSocket,然后ServerSocket接收到一个Socket的连接请求就创建一个Socket和一个线程去跟那个Socket进行通信。

  1. public class BioServer {
  2. public static void main(String[] args) {
  3. // 服务端开启一个端口进行监听
  4. int port = 8080;
  5. ServerSocket serverSocket = null; //服务端
  6. Socket socket; //客户端
  7. InputStream in = null;
  8. OutputStream out = null;
  9. try {
  10. serverSocket = new ServerSocket(port); //通过构造函数创建ServerSocket,指定监听端口,如果端口合法且空闲,服务器就会监听成功
  11. // 通过无限循环监听客户端连接,如果没有客户端接入,则会阻塞在accept操作
  12. while (true) {
  13. System.out.println("Waiting for a new Socket to establish" + " ," + new Date().toString());
  14. socket = serverSocket.accept();//阻塞 三次握手
  15. in = socket.getInputStream();
  16. byte[] buffer = new byte[1024];
  17. int length = 0;
  18. while ((length = in.read(buffer)) > 0) {//阻塞
  19. System.out.println("input is:" + new String(buffer, 0, length) + " ," + new Date().toString());
  20. out = socket.getOutputStream();
  21. out.write("success".getBytes());
  22. System.out.println("Server end" + " ," + new Date().toString());
  23. }
  24. }
  25. } catch (Exception e) {
  26. e.printStackTrace();
  27. } finally {
  28. // 必要的清理活动
  29. if (serverSocket != null) {
  30. try {
  31. serverSocket.close();
  32. } catch (IOException e) {
  33. e.printStackTrace();
  34. }
  35. }
  36. if (in != null) {
  37. try {
  38. in.close();
  39. } catch (IOException e) {
  40. e.printStackTrace();
  41. }
  42. }
  43. if (out != null) {
  44. try {
  45. out.close();
  46. } catch (IOException e) {
  47. e.printStackTrace();
  48. }
  49. }
  50. }
  51. }
  52. }

什么是NIO?

同步非阻塞
包括Selector,这是多路复用器,selector会不断轮询注册的channel,如果某个channel上发生了读写事件,selector就会将这些channel获取出来,通过SelectionKey获取有读写事件的channel,就可以进行IO操作。一个Selector就通过一个线程,就可以轮询成千上万的channel,这就意味着服务端可以接入成千上万的客户端。

  1. public class NioDemo implements Runnable {
  2. public int id = 100001;
  3. public int bufferSize = 2048;
  4. @Override
  5. public void run() {
  6. init();
  7. }
  8. public void init() {
  9. try {
  10. // 创建通道和选择器
  11. ServerSocketChannel socketChannel = ServerSocketChannel.open();
  12. Selector selector = Selector.open();
  13. InetSocketAddress inetSocketAddress = new InetSocketAddress(
  14. InetAddress.getLocalHost(), 4700);
  15. socketChannel.socket().bind(inetSocketAddress);
  16. // 设置通道非阻塞 绑定选择器
  17. socketChannel.configureBlocking(false);
  18. socketChannel.register(selector, SelectionKey.OP_ACCEPT).attach(
  19. id++);
  20. System.out.println("Server started .... port:4700");
  21. listener(selector);
  22. } catch (Exception e) {
  23. }
  24. }
  25. public void listener(Selector in_selector) {
  26. try {
  27. while (true) {
  28. Thread.sleep(1 * 1000);
  29. in_selector.select(); // 阻塞 直到有就绪事件为止
  30. Set<SelectionKey> readySelectionKey = in_selector
  31. .selectedKeys();
  32. Iterator<SelectionKey> it = readySelectionKey.iterator();
  33. while (it.hasNext()) {
  34. SelectionKey selectionKey = it.next();
  35. // 判断是哪个事件
  36. if (selectionKey.isAcceptable()) {// 客户请求连接
  37. System.out.println(selectionKey.attachment()
  38. + " - 接受请求事件");
  39. // 获取通道 接受连接,
  40. // 设置非阻塞模式(必须),同时需要注册 读写数据的事件,这样有消息触发时才能捕获
  41. ServerSocketChannel serverSocketChannel = (ServerSocketChannel) selectionKey
  42. .channel();
  43. serverSocketChannel
  44. .accept()
  45. .configureBlocking(false)
  46. .register(
  47. in_selector,
  48. SelectionKey.OP_READ
  49. | SelectionKey.OP_WRITE).attach(id++);
  50. System.out
  51. .println(selectionKey.attachment() + " - 已连接");
  52. // 下面这种写法是有问题的 不应该在serverSocketChannel上面注册
  53. /*
  54. * serverSocketChannel.configureBlocking(false);
  55. * serverSocketChannel.register(in_selector,
  56. * SelectionKey.OP_READ);
  57. * serverSocketChannel.register(in_selector,
  58. * SelectionKey.OP_WRITE);
  59. */
  60. }
  61. if (selectionKey.isReadable()) {// 读数据
  62. System.out.println(selectionKey.attachment()
  63. + " - 读数据事件");
  64. SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
  65. ByteBuffer receiveBuf = ByteBuffer.allocate(bufferSize);
  66. clientChannel.read(receiveBuf);
  67. System.out.println(selectionKey.attachment()
  68. + " - 读取数据:" + getString(receiveBuf));
  69. }
  70. if (selectionKey.isWritable()) {// 写数据
  71. System.out.println(selectionKey.attachment()
  72. + " - 写数据事件");
  73. SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
  74. ByteBuffer sendBuf = ByteBuffer.allocate(bufferSize);
  75. String sendText = "hello\n";
  76. sendBuf.put(sendText.getBytes());
  77. sendBuf.flip(); //写完数据后调用此方法
  78. clientChannel.write(sendBuf);
  79. }
  80. if (selectionKey.isConnectable()) {
  81. System.out.println(selectionKey.attachment()
  82. + " - 连接事件");
  83. }
  84. // 必须removed 否则会继续存在,下一次循环还会进来,
  85. // 注意removed 的位置,针对一个.next() remove一次
  86. it.remove();
  87. }
  88. }
  89. } catch (Exception e) {
  90. System.out.println("Error - " + e.getMessage());
  91. e.printStackTrace();
  92. }
  93. }
  94. /**
  95. * ByteBuffer 转换 String
  96. *
  97. * @param buffer
  98. * @return
  99. */
  100. public static String getString(ByteBuffer buffer) {
  101. String string = "";
  102. try {
  103. for (int i = 0; i < buffer.position(); i++) {
  104. string += (char) buffer.get(i);
  105. }
  106. return string;
  107. } catch (Exception ex) {
  108. ex.printStackTrace();
  109. return "";
  110. }
  111. }
  112. }

什么是AIO?

异步非阻塞
每个连接发送过来的请求,都会绑定一个buffer,然后通知操作系统去异步完成读,此时程序是会去干别的事儿的,等操作系统完成数据读取之后,就会回调接口,给操作系统异步读完的数据。

  1. public class AIOServer {
  2. public final static int PORT = 9888;
  3. private AsynchronousServerSocketChannel server;
  4. public AIOServer() throws IOException {
  5. server = AsynchronousServerSocketChannel.open().bind(
  6. new InetSocketAddress(PORT));
  7. }
  8. public void startWithFuture() throws InterruptedException,
  9. ExecutionException, TimeoutException {
  10. while (true) {// 循环接收客户端请求
  11. Future<AsynchronousSocketChannel> future = server.accept();
  12. AsynchronousSocketChannel socket = future.get();// get() 是为了确保 accept 到一个连接
  13. handleWithFuture(socket);
  14. }
  15. }
  16. public void handleWithFuture(AsynchronousSocketChannel channel) throws InterruptedException, ExecutionException, TimeoutException {
  17. ByteBuffer readBuf = ByteBuffer.allocate(2);
  18. readBuf.clear();
  19. while (true) {// 一次可能读不完
  20. //get 是为了确保 read 完成,超时时间可以有效避免DOS攻击,如果客户端一直不发送数据,则进行超时处理
  21. Integer integer = channel.read(readBuf).get(10, TimeUnit.SECONDS);
  22. System.out.println("read: " + integer);
  23. if (integer == -1) {
  24. break;
  25. }
  26. readBuf.flip();
  27. System.out.println("received: " + Charset.forName("UTF-8").decode(readBuf));
  28. readBuf.clear();
  29. }
  30. }
  31. public void startWithCompletionHandler() throws InterruptedException,
  32. ExecutionException, TimeoutException {
  33. server.accept(null,
  34. new CompletionHandler<AsynchronousSocketChannel, Object>() {
  35. public void completed(AsynchronousSocketChannel result, Object attachment) {
  36. server.accept(null, this);// 再此接收客户端连接
  37. handleWithCompletionHandler(result);
  38. }
  39. @Override
  40. public void failed(Throwable exc, Object attachment) {
  41. exc.printStackTrace();
  42. }
  43. });
  44. }
  45. public void handleWithCompletionHandler(final AsynchronousSocketChannel channel) {
  46. try {
  47. final ByteBuffer buffer = ByteBuffer.allocate(4);
  48. final long timeout = 10L;
  49. channel.read(buffer, timeout, TimeUnit.SECONDS, null, new CompletionHandler<Integer, Object>() {
  50. @Override
  51. public void completed(Integer result, Object attachment) {
  52. System.out.println("read:" + result);
  53. if (result == -1) {
  54. try {
  55. channel.close();
  56. } catch (IOException e) {
  57. e.printStackTrace();
  58. }
  59. return;
  60. }
  61. buffer.flip();
  62. System.out.println("received message:" + Charset.forName("UTF-8").decode(buffer));
  63. buffer.clear();
  64. channel.read(buffer, timeout, TimeUnit.SECONDS, null, this);
  65. }
  66. @Override
  67. public void failed(Throwable exc, Object attachment) {
  68. exc.printStackTrace();
  69. }
  70. });
  71. } catch (Exception e) {
  72. e.printStackTrace();
  73. }
  74. }
  75. public static void main(String args[]) throws Exception {
  76. // new AIOServer().startWithFuture();
  77. new AIOServer().startWithCompletionHandler();
  78. Thread.sleep(100000);
  79. }
  80. }

什么是epoll?

把一个磁盘文件映射到内存里来,然后把映射到内存里来的数据通过socket发送出去 。
有一种mmap技术,也就是内存映射,直接将磁盘文件数据映射到内核缓冲区,这个映射的过程是基于DMA引擎拷贝的,同时用户缓冲区是跟内核缓冲区共享一块映射数据的,建立共享映射之后,就不需要从内核缓冲区拷贝到用户缓冲区了。
光是这一点,就可以避免一次拷贝,但是这个过程中还是会用户态切换到内核态去进行映射拷贝,接着再次从内核态切换到用户态, 建立用户缓冲区和内核缓冲区的映射 ,
接着把数据通过Socket发送出去,还是要再次切换到内核态 ,
接着直接把内核缓冲区里的数据拷贝到Socket缓冲区里去,然后再拷贝到网络协议引擎里,发送出去就可以了,最后切换回用户态 。
减少一次拷贝,但是并不减少切换次数,一共是4次切换,3次拷贝

什么是零拷贝技术?

linux提供了sendfile,也就是零拷贝技术
这个零拷贝技术,就是先从用户态切换到内核态,在内核态的状态下,把磁盘上的数据拷贝到内核缓冲区,同时从内核缓冲区拷贝一些 offset和length到Socket缓冲区;接着从内核态切换到用户态,从内核缓冲区直接把数据拷贝到网络协议引擎里去
同时从Socket缓冲区里拷贝一些offset和length到网络协议引擎里去,但是这个offset和length的量很少,几乎可以忽略
只要2次切换,2次拷贝

说一下select,poll,epoll的区别?

select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。epoll也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。这就是回调机制带来的性能提升。
select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内部定义的等待队列)。这也能节省不少的开销。