面试必备基础知识 — I/O

文章目录

  • 磁盘操作
  • 字节操作
    • 实现文件复制
    • 装饰者模式
  • 字符操作
    • 编码与解码
    • String 的编码方式
    • Reader 和 Writer
  • 对象操作
    • 序列化
    • Serializable
  • 网络操作
  • IO 模型
    • 五种IO模型
      • 阻塞IO模型
      • 非阻塞IO模型
      • 多路复用IO模型
      • 信号驱动IO模型
      • 异步IO模型

磁盘操作

File 类可用于表示 文件 和 目录 的信息,但不表示文件的内容。

字节操作

InputStream Java标准库中提供的一个最基本的 输入流 ,是一个抽象类,而不是接口。

FileInputStream 是InputStream的一个子类。FileInputStream就是从文件流中读取数据。

outputStream Java标准库中提供的一个最基本的 输出流 ,也是一个抽象类。

FileOutputStream 可以从文件获取输出流。

实现文件复制

public class CopyDemo {
 
	public static void main(String[] args) throws IOException {
        // 创建字节输入流对象
        FileInputStream in = new FileInputStream("a.txt");
        // 创建字节输出流对象
        FileOutputStream out = new FileOutputStream("b.txt");
 
        // 创建byte对象数组
        byte[] by = new byte[1024];
        // read() 最多读取 by.length 个字节
        // 返回的是实际读取的个数
        // 返回 -1 的时候表示读到 eof,即文件尾
        int len = 0;
        while ((len = in.read(by)) != -1) {
            out.write(by, 0, len);
        }
        // 释放资源
        in.close();
        out.close();
    }
}

装饰者模式

Java I/O 使用 装饰者模式 来实现,以 InputStream 为例:

  • InputStream 是抽象组件
  • FileInputStream 是InputStream 的子类,属于具体组件,提供了字节流的输入操作。
  • FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream(字节缓冲流) 为 FileInputStream 提供缓存的功能。
	//传递的是一个对象
   BufferedInputStream bis = new BufferedInputStream(
       new FileInputStream("a.txt"));

字符操作

编码与解码

编码就是将字符转换为字节,而解码就是将字节重新组合成字符。

如果编码和解码过程使用不同的编码方法那么就会出现乱码。

  • GBK 编码中,中文字符占两个字节,英文字符占一个字节
  • UTF-8 编码中,中文字符占三个字节,英文字符占一个字节
  • UTF-16be 编码中,中文和英文字符都占两个字节

UTF-16be 中,be 指的是Big Endian ,大端

Java的内存编码使用 双字节编码 UTF-16be,这不是指Java 只支持这一种类型的编码格式,而是说char 这种类型使用 UTF-16be 进行编码

char 类型占16位,两个字节,Java使用这种双字节编码 是为了让一个中文 或者英文字符都能使用一个 char来存储。

String 的编码方式

/*
 * String 可以看成一个字符序列,
 * 可以指定一个编码方式将它编码为字节序列,
 * 也可以指定一个编码方式将一个字节序列解码为 String。
 */
public class Test {
	
    public static void main(String[] args) {
        //String -- byte[]
        String str1 = "无敌敏";
        byte[] by = str1.getBytes();
		
        //byte[] -- String
        String str2 = new String(by);
        System.out.println(str2);
    }
}

Reader 和 Writer

不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符
但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。

  • InputStreamReader :实现从字节流解码成字符流。
  • OutputStreamWriter :实现从字符流编码成字节流。

将一个文本文件的内容读入到另一个文件中

public static void main(String[] args) throws IOException {
    //封装数据源
    BufferedReader in = new BufferedReader(new FileReader("a.txt"));
    //封装目的地
    BufferedWriter out = new BufferedWriter(new FileWriter("b.txt"));
 
    String line = null;
    //readLine()  一次读取一行数据
    while((line=in.readLine()) != null) {
        out.write(line);
       	// newLine()  写入一个行分隔符
        out.newLine();
        out.flush();
}
    //关闭
    in.close();
    out.close();
}

对象操作

序列化

序列化是指把一个 Java对象 变成二进制内容(字节序列),本质上就是一个byte[] 数组。
方便存储和传输 :序列化后可以把 byte[] 保存到文件中,或者把 byte[] 通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。

  • 序列化 :ObjectOutputStream.writeObject()
  • 反序列化 :ObjectInputStream.readObject()

不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。

Serializable

一个Java对象要能序列化,必须实现一个特殊的 java.io.Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。(我们把这样的空接口称为“标记接口”)

private class A implements Serializable{
 
}

序列化和反序列化实例

public static void main(String[] args) throws IOException, ClassNotFoundException {
 
    A a1 = new A(123, "abc");
 
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("a.txt"));
    objectOutputStream.writeObject(a1);
    objectOutputStream.close();
 
    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("a.txt"));
    A a2 = (A) objectInputStream.readObject();
    objectInputStream.close();
    System.out.println(a2);
}
 
private static class A implements Serializable {
 
    private int x;
    private String y;
 
    A(int x, String y) {
        this.x = x;
        this.y = y;
    }
 
    @Override
    public String toString() {
        return "x = " + x + "  " + "y = " + y;
    }
}

transient

transient 关键字可以使一些 序性不被序列化

ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。

private transient Object[] elementData;

网络操作

Java 中的网络 支持:

  • InetAddress :用于表示网络上的硬件资源,即 IP地址
    • 没有公共的构造函数,只能通过静态方法来创建实例
    • InetAddress.getByName(String host);
    • InetAddress.getByAddress(byte[] address);
  • URL :统一资源定位符
  • Sockets (套接字):使用 TCP 网络协议实现网络通信
  • Datagram (数据报文):使用 UDP 网络协议实现网络通信

IO 模型

同步 和 异步

同步

发出一个功能调用时,在没有得到结果之前,该调用就不返回。也就是必须一件一件事做,等前一件做完了才能做下一件事。

例如,普通B/S模式(同步):提交请求 -> 等待服务器才处理 -> 处理完毕返回
这期间客户端浏览器不能干任何事情。

异步

当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。

例如,Ajax请求(异步):请求通过事件触发 -> 服务器处理(此时浏览器仍然可以做其他事情)-> 处理完毕。

同步和异步的重点在于多个任务或事件发生时,一个事件或任务的执行是否会导致整个流程的暂时等待

一般来说,可以通过 多线程的方式 来实现异步,但二者并不能画等号,异步只是宏观上的一个模式,而采用多线程来实现异步只是一种手段,通过多进程的方式也能实现异步。

阻塞 和 非阻塞

阻塞

当某个任务 或 事件在执行过程中,它发出一个请求操作,但是由于该请求操作需要的条件不满足,那么就会一直等待,直到条件满足。
阻塞调用是指,调用结果返回前,当前线程会被挂起(线程进入非可执行状态,在此状态下,CPU不会给线程分配时间片,即线程暂停运行)。函数只有在得到结果后才会返回。

非阻塞

当某个任务 或 事件在执行过程中,它发出一个请求操作,如果该请求操作需要的条件不满足,会立即返回一个标志信息告知条件不满足,不会一直在那等待。

阻塞与非阻塞的关键在于,当发出一个请求操作时,如果条件不满足,是会一直等待,还是返回一个标志信息

阻塞IO 和 非阻塞IO

当用户线程发起一个IO请求操作,内核会去查看要读取的数据是否就绪
对于阻塞IO来说,如果数据没有就绪,则会一直在此等待,直到数据就绪;
对于非阻塞IO来说,如果数据没有就绪,则会返回一个标志信息,告知用户线程当前数据没有就绪。
当数据就绪之后,便将数据拷贝到用户线程,这样就完成了一个完成的IO读请求操作。
一个完整的 IO读请求操作包括两个阶段:

  • 查看数据是否就绪
  • 进行数据拷贝(内核将数据拷贝到用户线程)

阻塞IO 与 非阻塞IO 的区别在于第一阶段如果数据没有就绪,在查看数据是否就绪的过程中,是一直等待,还是直接返回一个标志信息

同步IO 和 异步IO

同步IO

如果一个线程请求进行IO操作,在IO操作完成之前,该线程会被阻塞。

异步IO

如果一个线程请求进行IO操作,IO操作不会导致请求线程被阻塞。

同步IO和异步IO模型是针对 用户线程内核 的交互来说的,同步IO 和 异步IO的关键区别在于:数据拷贝阶段,是由用户线程完成还是内核完成。(异步IO的两个阶段都是由内核自动完成,所以说,异步IO需要操作系统的底层支持)

五种IO模型

阻塞IO模型

在读写中会发生的一种阻塞现象。
当用户线程发出IO请求之后,内核会去查看数据是否就绪 ,如果没有就绪,就会等待数据就绪,此时用户线程会处于等待状态,用户线程交出CPU。当数据就绪之后,内核将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除阻塞状态。

非阻塞IO模型

当用户线程发起一个read 操作之后,并不需要等待,而是马上就能得到一个结果。如果结果是 error,就代表数据还没有准备好,于是,它可以再次发送read操作。一旦内核中的操作准备好了,并且又再次收到了用户线程的请求,则会马上将数据拷贝到用户线程中,然后返回。

在非阻塞IO模型中,用户线程需要不断地去询问内核数据是否就绪,也就是说,非阻塞IO不会交出CPU,而是一直占用。

多路复用IO模型

在多路复用IO模型中,会有一个线程不断去轮询多个socket的状态,只有当socket真正有读写事件时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。

信号驱动IO模型

在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。

异步IO模型

异步IO模型才是最理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。

在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。

前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。

面试必备基础知识 — I/O_第1张图片
图片来源

BIO里用户最关心“我要读”,NIO里用户最关心”我可以读了”,在AIO模型里用户更需要关注的是“读完了”。

Java NIO:浅析I/O模型
CYC

你可能感兴趣的:(Java)