26-IO(中)

1. 数据流

用于读取或写出基本类型的变量或字符串

  • 为了方便地操作 Java 语言的基本数据类型和 String 的数据,可以使用数据流
  • 数据流有 2 个类(用于读取和写出基本数据类型、String 类的数据)
    • public class DataInputStream extends FilterInputStream implements DataInput
    • public class DataOutputStream extends FilterOutputStream implements DataOutput
    • 分别套接在 InputStreamOutputStream 子类的流上
  • DataInputStream 中的方法
    • boolean readBoolean()
    • byte readByte()
    • char readChar()
    • float readFloat()
    • double readDouble()
    • short readShort()
    • long readLong()
    • int readInt()
    • String readUTF()
    • void readFully(byte[] b)
    • void flush():清空此数据输出流。这迫使所有缓冲的输出字节被写出到流中。底层调用其基础输出流的 flush 方法
  • DataOutputStream 中的方法:将上述的方法的 read 改为相应的 write 即可

注意!读取不同类型的数据必须要与当初写入时的顺序一致,否则会抛出异常 EOFException

@Test
public void write() {
    DataOutputStream dos = null;
    try {
        // 创建连接到指定文件的数据输出流对象
        dos = new DataOutputStream(new FileOutputStream("destData.dat"));
        dos.writeUTF("源啊"); // 写UTF字符串
        dos.writeBoolean(false); // 写入布尔值
        dos.writeLong(1234567890L); // 写入长整数
        System.out.println("写文件成功!");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            // 关闭流对象, 关闭过滤流时, 会自动关闭它包装的底层节点流
            if (dos != null) dos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

@Test
public void read() {
    DataInputStream dis = null;
    try {
        dis = new DataInputStream(new FileInputStream("destData.dat"));
        String info = dis.readUTF();
        boolean flag = dis.readBoolean();
        long time = dis.readLong();
        System.out.println(info);
        System.out.println(flag);
        System.out.println(time);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (dis != null)
            try {
                dis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
    }
}

2. 对象流

2.1 说明

截取:《Java语言程序设计(基础篇)》

  • 序列化:用 ObjectOutputStream 类保存基本类型数据或对象的机制
  • 反序列化:用 ObjectInputStream 类读取基本类型数据或对象的机制

2.2 对象的序列化

  • [对象序列化机制] 允许把内存中的Java 对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点 // /当其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象。
  • 序列化的好处在于可将任何实现了 Serializable<I> 的对象转化为字节数据,使其在保存和传输时可被还原 // 序列化接口没有方法或字段,仅用于标识可序列化的语义。
  • 序列化是 RMI (Remote Method Invoke – 远程方法调用) 过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平台的基础。
  • 如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现 SerializableExternalizable 两个接口之一。否则,会抛出 NotSerializableException

2.3 serialVersionUID

凡是实现 Serializable<I> 的类都有一个表示序列化版本标识符的静态变量:

private static final long serialVersionUID

serialVersionUID 用来表明类的不同版本间的兼容性。序列化运行时使用这个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。如果接收者加载的该对象的类的 serialVersionUID 与对应的发送者的类的版本号不同,则反序列化将会导致 InvalidClassException。可序列化类可以通过声明名为 "serialVersionUID" 的字段(该字段必须是静态 (static)、最终 (final) 的 long 型字段)显式声明其自己的 serialVersionUID。简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容。

如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 值,如“Java(TM) 对象序列化规范”中所述。不过,强烈建议所有可序列化类都显式声明 serialVersionUID 值,原因是计算默认的 serialVersionUID 对类的详细信息具有较高的敏感性(值是Java运行时环境根据类的内部细节自动生成的)。若类的实例变量做了修改, serialVersionUID 可能发生变化,如此一来,在反序列化过程中就有可能会导致意外的 InvalidClassException。因此,序列化类必须声明一个明确的 serialVersionUID 值。还强烈建议使用 private 修饰符显示声明 serialVersionUID(如果可能),原因是这种声明仅应用于直接声明类 -- serialVersionUID 字段作为继承成员没有用处。数组类不能声明一个明确的 serialVersionUID,因此它们总是具有默认的计算值,但是数组类没有匹配 serialVersionUID 值的要求。

简单来说,Java的序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常:InvalidCastException

2.4 自定义类实现序列化

  • 实现 Serializable<I>
  • 提供一个私有全局常量:serialVersionUID
  • 除了当前类要实现接口外,还必须保证所有 Field 也都必须是可序列化的,否则拥有该类型的 Field 的类也不能序列化。
  • ObjectOutputStreamObjectInputStream 不能序列化 statictransient 修饰的成员变量

3. 随机存取文件流

  • RandomAccessFile 虽声明在 java.io 包下,但直接继承于 java.lang.Object 类。并且它实现了 DataInputDataOutput 这两个接口,也就意味着这个类既可以读也可以写
  • RandomAccessFile 类支持 “随机访问” 的方式,程序可以直接跳到文件的任意地方来读、写文件
    • 支持只访问文件的部分内容
    • 可以向已存在的文件后追加内容
  • RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。类对象可以自由移动记录指针:
    • long getFilePointer():获取文件记录指针的当前位置
    • void seek(long pos):将文件记录指针定位到 pos 位置
  • 构造器
    • public RandomAccessFile(File file, String mode)
    • public RandomAccessFile(String name, String mode)
  • 创建 RandomAccessFile 类实例需要指定一个 mode 参数,该参数指定 RandomAccessFile 的访问模式:
    • r:以只读方式打开
    • rw:打开以便读取和写入
    • rwd:打开以便读取和写入;同步文件内容的更新
    • rws:打开以便读取和写入;同步文件内容和元数据的更新
  • 如果模式为只读 r。则不会创建文件,而是会去读取一个已经存在的文件,如果读取的文件不存在则会出现异常。 如果模式为 rw 读写。如果文件不存在则会去创建文件,如果存在则不会创建,并且,如果存在且其中已有内容,则默认会从头对原有文件内容进行覆盖

实现向指定位置进行数据插入:

public void test3() throws IOException {
    RandomAccessFile raf = new RandomAccessFile("src.txt", "rw");
    raf.seek(3);
    int len;
    byte[] buf = new byte[1024];
    // 1. 插入位置往后的数据先存起来
    StringBuilder builder = new StringBuilder((int) new File("src.txt").length());
    while((len = raf.read(buf)) != -1)
        builder.append(new String(buf, 0, len));
    // 2. 插入
    raf.seek(3);
    raf.write("1101".getBytes());
    // 3. 将原尾部数据再写回去
    raf.write(builder.toString().getBytes());
    raf.close();
}

我们可以用 RandomAccessFile 这个类,来实现一个多线程断点下载的功能,用过下载工具的朋友们都知道,下载前都会建立两个临时文件,一个是与被下载文件大小相同的空文件,另一个是记录文件指针的位置文件,每次暂停的时候,都会保存上一次的指针,然后断点下载的时候,会继续从上一次的地方下载,从而实现断点下载或上传的功能。

4. ByteArrayOutputStream

  • public class ByteArrayOutputStream extends OutputStream
  • 此类实现了一个输出流,其中的数据被写入一个 byte 数组。缓冲区会随着数据的不断写入而自动增长。可使用 toByteArray()toString() 获取数据。相当于一个中间缓冲层,将类写入到文件等其他 OutputStream。它是对字节进行操作,属于内存操作流
  • 成员变量
    protected byte buf[]; // 存储数据的缓冲区
    protected int count; // 缓冲区中的有效字节数
    
  • 成员方法
    • int size() 将指定的字节写入此 byte 数组输出流
    • String toString() 用平台默认的字符集,通过解码字节将缓冲区内容转换为字符串
    • String toString(String charsetName) 使用指定的 charsetName,通过解码字节将缓冲区内容转换为字符串
    • void write(int b) 将指定的字节写入此 byte 数组输出流
    • void write(byte[] b, int off, int len) 将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此 byte 数组输出流
    • void writeTo(OutputStream out) 将此 byte 数组输出流的全部内容写入到指定的输出流参数中,这与使用 out.write(buf, 0, count) 调用该输出流的 write() 效果一样
    • void close() 关闭 ByteArrayOutputStream 无效。此类中的方法在关闭此流后仍可被调用,而不会产生任何 IOException

5. NIO、AIO

  • Java NIO (Non-Blocking IO)是从 Java 1.4 版本开始引入的一套新的 IO API,可以替代标准的 Java IO API。NIO 与原来的 IO 有同样的作用和目的,但是使用的方式完全不同,NIO 支持面向缓冲区的(IO 是面向流的)、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。
  • Java API 中提供了两套 NIO,一套是针对标准输入输出 NIO,另一套就是网络编程 NIO。
  • 随着 JDK 7 的发布,Java 对 NIO 进行了极大的扩展,增强了对文件处理和文件系统特性的支持,以至于我们称他们为 NIO.2(AIO)。因为 NIO 提供的一些功能,NIO 已经成为文件处理中越来越重要的部分。
  • API
    • 早期的 Java 只提供了一个 File 类来访问文件系统,但 File 类的功能比较有限,所提供的方法性能也不高。而且,大多数方法在出错时仅返回失败,并不会提供异常信息
    • NIO. 2 为了弥补这种不足,引入了 Path<I>,代表一个平台无关的平台路径,描述了目录结构中文件的位置。Path 可以看成是 File 类的升级版本,实际引用的资源也可以不存在
    • 在以前 IO 操作都是这样写的:
      import java.io.File;
      File file = new File("index.html");
      
    • 但在 JDK7 中,我们可以这样写:
      import java.nio.file.Path;
      import java.nio.file.Paths;
      Path path = Paths.get("index.html");
      
    • 同时,NIO.2 在 java.nio.file 包下还提供了 FilesPaths 工具类,Files 包含了大量静态的工具方法来操作文件。Paths 则包含了两个返回 Path 的静态工厂方法
    • Paths 类提供的 get() 用来获取 Path 对象:
      • static Path get(String first, String … more) 用于将多个字符串串连成路径
      • static Path get(URI uri):返回指定 uri 对应的 Path 路径

BIO、NIO、AIO 有什么区别?

  • BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
  • NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
  • AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。
原文地址:https://www.cnblogs.com/liujiaqi1101/p/13340662.html