轩辕李的博客 轩辕李的博客
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

轩辕李

勇猛精进,星辰大海
首页
  • Java
  • Spring
  • 其他语言
  • 工具
  • HTML&CSS
  • JavaScript
  • 分布式
  • 代码质量管理
  • 基础
  • 操作系统
  • 计算机网络
  • 编程范式
  • 安全
  • 中间件
  • 心得
关于
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java

    • 核心

      • Java8--Lambda 表达式、Stream 和时间 API
      • Java集合
      • Java IO
        • 初识
        • 基本划分
        • 操作对象划分
          • 1、文件流
          • 2、数组流
          • 3、管道流
          • 4、基本类型流
          • 5、缓冲流
          • 6、打印流
          • 7、序列化流
        • 字节与字符流的转换
        • 工具类库
          • 1、Scanner
          • 2、Apache Common IO
        • 新IO
          • 1、文件拷贝对比
          • 2、Buffer掌握
          • Direct Buffer
          • MappedByteBuffer
        • 总结
      • Java 文件操作
      • Java 网络编程
      • Java运行期动态能力
      • Java可插入注解处理器
      • Java基准测试(JMH)
      • Java性能分析(Profiler)
      • Java调试(JDI与JDWP)
      • Java管理与监控(JMX)
      • Java加密体系(JCA)
      • Java服务发现(SPI)
      • Java随机数生成研究
      • Java数据库连接(JDBC)
      • Java历代版本新特性
      • 写好Java Doc
      • 聊聊classpath及其资源获取
    • 并发

    • 经验

    • JVM

    • 企业应用

  • Spring

  • 其他语言

  • 工具

  • 后端
  • Java
  • 核心
轩辕李
2021-08-25
目录

Java IO

# 初识

所谓IO,就是in和out,也就是输入和输出,指应用程序和外部设备之间的数据传输。
Java中通过流来处理IO,所谓流,就是一连串的数据。
流的几个特性:

  • 先进先出:最先写入输出流的数据最先被输入流读取到
  • 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序读取一串字节,不能随机访问中间的数据。(RandomAccessFile除外)
  • 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,如果既要写入数据,又要读取数据,则要分别提供两个流

# 基本划分

如果按照传输方式来划分IO,可以分为字节流和字符流。
字节是面向计算机的,我们知道计算机是按位(bit)存储和计算的,一个字节有8 bit。
字符是面向人类的,比如“我喜欢看电影”就是一串字符。字符在存储的时候,还是要转换为字节,由于存在不同语言文字,所以需要字符和字节转换的对照表,这诞生了很多字符编码集。最常见的就是UTF-8编码,它对全球的语言文字都做了对照,使用比较广泛。

字节流按照输入输出,在Java中的抽象分别是:

  • InputStream
  • OutputStream

字符流按照输入输出,在Java中的抽象分别是:

  • Reader
  • Writer

以上就是Java IO的四个基本抽象类。
其中InputStream和Reader的核心方法是read(),这很好理解,所谓的输入流,就是供我们读取使用的。
而OutputStream和Writer的核心方法是write(),所谓的输出流,就是要向外部写东西。

# 操作对象划分

Java中,IO会有很多操作对象,最常见的就是文件,其次还有数组、管道、基本类型、缓冲、打印、序列化等。

# 1、文件流

首先是文件读的示例,分别演示了字节流读取和字符流读取:

int b;
FileInputStream fis1 = new FileInputStream("fis.txt");
// 循环读取
while ((b = fis1.read())!=-1) {
    System.out.println((char)b);
}
fis1.close();
int b = 0;
// 注意此处会使用系统默认编码来读取文件
FileReader fileReader = new FileReader("read.txt");
// 循环读取
while ((b = fileReader.read())!=-1) {
    System.out.println((char)b);
}
fileReader.close();

然后是写入指定内容到文件:

FileOutputStream fos = new FileOutputStream("fs.txt");
fos.write("爱看电影".getBytes());
fos.close();
FileWriter fileWriter = new FileWriter("fw.txt");
char[] chars = "爱看电影".toCharArray();
fileWriter.write(chars, 0, chars.length);
fileWriter.close();

# 2、数组流

数组流,也叫内存流:

InputStream is =new BufferedInputStream(new ByteArrayInputStream("爱看电影".getBytes(StandardCharsets.UTF_8)));
byte[] flush =new byte[1024];
int len =0;
while(-1!=(len=is.read(flush))){
    System.out.println(new String(flush,0,len));
}
is.close();
ByteArrayOutputStream bos =new ByteArrayOutputStream();
byte[] info ="爱看电影".getBytes();
bos.write(info, 0, info.length);
byte[] dest =bos.toByteArray();
bos.close();

# 3、管道流

Java中的管道流不同于Linux用来进程间通信,它只能用来线程间通信。

final PipedOutputStream pipedOutputStream = new PipedOutputStream();
final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream);

Thread thread1 = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            pipedOutputStream.write("爱看电影".getBytes(StandardCharsets.UTF_8));
            pipedOutputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
});

Thread thread2 = new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            byte[] flush =new byte[1024];
            int len =0;
            while(-1!=(len=pipedInputStream.read(flush))){
                System.out.println(new String(flush,0,len));
            }

            pipedInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

    }
});
thread1.start();
thread2.start();

# 4、基本类型流

用来写入和读取基本类型:

DataInputStream dis = new DataInputStream(new FileInputStream("fs.txt"));
// 读取接下来的一个字节,相当于 (byte)dis.read()
byte b = dis.readByte();
// 读取接下来的两个字节,转换为short
short s = dis.readShort();
// 读取接下来的四个字节,转换为int
int i = dis.readInt();
// ...
long l = dis.readLong();
float f = dis.readFloat();
double d = dis.readDouble();
boolean bb = dis.readBoolean();
char ch = dis.readChar();
dis.close();
DataOutputStream das = new DataOutputStream(new FileOutputStream("fs.txt"));
das.writeByte(10);
das.writeShort(100);
das.writeInt(1000);
das.writeLong(10000L);
das.writeFloat(12.34F);
das.writeDouble(12.56);
das.writeBoolean(true);
das.writeChar('A');
das.flush();
das.close();

# 5、缓冲流

缓冲流在内存中设置了一个缓冲区,只有缓冲区存储了足够多的带操作的数据后,才会和内存或者硬盘进行交互。
缓冲输入流相对于普通输入流的优势是,它提供了一个缓冲数组(默认是8192,参考BufferedInputStream.DEFAULT_BUFFER_SIZE),每次调用read方法的时候,它首先尝试从缓冲区里读取数据,若读取失败(缓冲区无可读数据),则选择从物理数据源(譬如文件)读取新数据(这里会尝试尽可能读取多的字节)放入到缓冲区中,最后再将缓冲区中的内容部分或全部返回给用户.由于从缓冲区里读取数据远比直接从物理数据源(譬如文件)读取速度快。

BufferedInputStream在模式上是一个包装类:

int bytesRead;
byte[] buffer = new byte[1024];
BufferedInputStream is = new BufferedInputStream(new FileInputStream("fis.txt"));
while ((bytesRead = is.read(buffer)) != -1) {
    System.out.println(new String(buffer, 0, bytesRead));
}
is.close();

# 6、打印流

打印流就是向控制台中打印信息,我们最常用的System.out.println()用到的就是打印流。
打印流本身是输出流,字节流的实现是PrintStream,System.out返回的就是System.out;
字符流的实现是PrintWriter。

StringWriter buffer = new StringWriter();
try (PrintWriter pw = new PrintWriter(buffer)) {
    pw.println("爱看电影");
}
System.out.println(buffer.toString());

# 7、序列化流

要将Java对象保存为流进行传输,需要使用到序列化流。
对象必须先实现Serializable接口,创建序列化流:

Employee e = new Employee();
e.name = "jack";
e.address = "beijing";
e.age = 30; 
try {
    // 创建序列化流对象
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("employee.txt"));
    // 写出对象
    out.writeObject(e);
    // 释放资源
    out.close();
    fileOut.close();
    // Employee对象被保存到了文件中
} catch(IOException i)   {
    i.printStackTrace();
}

读取被序列化的对象:

 // 创建反序列化流
 FileInputStream fileIn = new FileInputStream("employee.txt");
 ObjectInputStream in = new ObjectInputStream(fileIn);
 // 读取一个对象
 e = (Employee) in.readObject();
 // 释放资源
 in.close();
 fileIn.close();

注意:Java本身的序列化性能较差,业界对此有很多优化方案,比如Hessian2、Protobuf等。

# 字节与字符流的转换

直接上代码:

InputStreamReader isr = new InputStreamReader(new FileInputStream("demo.txt"));
char []cha = new char[1024];
int len = isr.read(cha);
System.out.println(new String(cha,0,len));
isr.close();
try (ObjectInputStream input = new ObjectInputStream(new FileInputStream(
        new File("Person.txt")))) {
    String s = input.readUTF();
}

前面说到使用FileReader直接读取存在编码问题,要指定编码读取文件,可以这么写:

final InputStreamReader reader = new InputStreamReader(input, Charsets.toCharset(inputCharset));
while ((b = reader.read())!=-1) {
    System.out.println((char)b);
}
fileReader.close();

# 工具类库

# 1、Scanner

从输入流获取扫描值,示例:

Scanner sc = new Scanner(System.in);
int i = sc.nextInt();
System.out.println(i);
Scanner sc = new Scanner(new File("num.txt"));
while (sc.hasNextLong()) {
    long aLong = sc.nextLong();
    System.out.println(aLong);
}

# 2、Apache Common IO

maven引入:

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.11.0</version>
</dependency>

IO相关的工具主要是IOUtils,有很多有用的方法。

copy和copyLarge用于拷贝流,比如把网络文件保存到本地:

InputStream is = new URL("http://t.com/1.jpg").openConnection().getInputStream();
IOUtils.copy(is, new FileOutputStream("1.jpg"));

按照指定编码读取文件:

IOUtils.readLines(is, StandardCharsets.UTF_8);

InputStream转换为byte[]或char[]:

IOUtils.toByteArray(is);

IOUtils.toCharArray(is, StandardCharsets.UTF_8);

IOUtils.toString(is, StandardCharsets.UTF_8);

写入文件:

IOUtils.write("爱看电影", new FileOutputStream("1.txt"), StandardCharsets.UTF_8);

IOUtils.writeLines(list, new FileOutputStream("1.txt"), StandardCharsets.UTF_8);

关闭资源:

IOUtils.closeQuietly(is);

# 新IO

在Java1.4中引入了nio,Java1.7又引入了aio。aio和Java网络编程关系很大,后面会有专题文章。
这里说一下nio,主要引入了两个接口:

  • Channel: 流只能在一个方向上移动,而通道可以双向,可以用于读、写或者同时用于读写。主要实现类:
    • FileChannel: 从文件中读写数据
    • DatagramChannel: 通过 UDP 读写网络中数据
    • SocketChannel: 通过 TCP 读写网络中数据
    • ServerSocketChannel: 可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel
  • Buffer: 发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。主要实现类:
    • ByteBuffer
    • CharBuffer
    • ShortBuffer
    • IntBuffer
    • LongBuffer
    • FloatBuffer
    • DoubleBuffer

# 1、文件拷贝对比

可以看到nio相对于传统io而言,更强调用快的方式来处理数据,这在性能上会有一定优势。
缺点是不够简单和优雅,其实对于文件读写来说,传统io使用缓冲区不一定比nio要慢。nio的新特性更多用在网络编程方面,后续的专题文章会讲到。

以文件拷贝为例,从理论上来说,传统io在操作系统层面会多次在用户态和内核态之间进行上下文切换,而nio的transferTo方法会用到零拷贝技术,也就是在内核态完成拷贝,省去了上下文切换和内存拷贝。
看例子比较一下:


public static void copyFileByStream(File source, File dest) throws
        IOException {
    try (InputStream is = new FileInputStream(source);
         OutputStream os = new FileOutputStream(dest);){
        byte[] buffer = new byte[1024];
        int length;
        while ((length = is.read(buffer)) > 0) {
            os.write(buffer, 0, length);
        }
    }
 }

public static void copyFileByChannel(File source, File dest) throws
        IOException {
    try (FileChannel sourceChannel = new FileInputStream(source)
            .getChannel();
         FileChannel targetChannel = new FileOutputStream(dest).getChannel
                 ();){
        for (long count = sourceChannel.size() ;count>0 ;) {
            long transferred = sourceChannel.transferTo(sourceChannel.position(), count, targetChannel);            
            sourceChannel.position(sourceChannel.position() + transferred);
            count -= transferred;
        }
    }
 }

# 2、Buffer掌握

缓冲区有几个基本属性:

  • capacity: 最大容量
  • position: 当前已经读写的字节数
  • limit: 还可以读写的字节数

Buffer的操作大概是:

  • 创建Buffer之后,capacity是缓冲区大小,position=0,limit=capacity-1
  • 写入字节,position增大
  • 调用flip(),limit=position,position=0
  • 取出字节到缓冲区,position=limit。想要重复读,调用rewind(),position归0
  • 调用clear,position=limit=0

一个实际的文件复制例子大概是这样:

public static void fastCopy(String src, String dist) throws IOException {
    /* 获得源文件的输入字节流 */
    FileInputStream fin = new FileInputStream(src);
    /* 获取输入字节流的文件通道 */
    FileChannel fcin = fin.getChannel();
    /* 获取目标文件的输出字节流 */
    FileOutputStream fout = new FileOutputStream(dist);
    /* 获取输出字节流的通道 */
    FileChannel fcout = fout.getChannel();
    /* 为缓冲区分配 1024 个字节 */
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    while (true) {
        /* 从输入通道中读取数据到缓冲区中 */
        int r = fcin.read(buffer);
        /* read() 返回 -1 表示 EOF */
        if (r == -1) {
            break;
        }
        /* 切换读写 */
        buffer.flip();
        /* 把缓冲区的内容写入输出文件中 */
        fcout.write(buffer);
        /* 清空缓冲区 */
        buffer.clear();
    }
}

# Direct Buffer

当你使用普通的ByteBuffer的话,会被JVM管理起来,会进行自动垃圾回收

void change(){
    ByteBuffer buf = ByteBuffer.allocate(1024);
    ...
}

当change()方法跳出的时候,buf会被自动回收。这里有一个问题:ByteBuffer里都是大量的字节,这些字节在JVM GC整理内存时就显得很笨重,把它们在内存中拷来拷去显然不是一个好主意。
在某些场景下,比如网络编程中,大量的字节在交互,可能导致GC很忙碌。
那么如果有一块内存可以脱离GC管理(此处指堆内GC),是不是更好呢?

ByteBuffer buf = ByteBuffer.allocateDirect(1024);

ByteBuffer.allocateDirect分配了一个Java堆外内存。
它的特点是:

  • 生命周期内内存地址都不会再发生更改,进而内核可以安全地对其进行访问,很多 IO 操作会很高效
  • 减少了堆内对象存储的可能额外维护工作,所以访问效率可能有所提高
  • 创建和销毁的过程会增加开销,建议用于长期使用、数据较大的场景
  • 堆外内存不受-Xmx参数的管理,而要使用-XX:MaxDirectMemorySize=512M来设置
  • ByteBuffer管理的堆外内存受到GC的控制,但只有在Full GC时回收。ByteBuffer的回收机制是:内部有一个低优先级线程Cleaner会执行clean(),内部还是调用System.gc(),所以一定不要-XX:+DisableExplicitGC

# MappedByteBuffer

它将文件按照指定大小直接映射为内存区域,当程序访问这个内存区域时将直接操作这块儿文件数据,省去了将数据从内核空间向用户空间传输的损耗。
它本质上也是一种Direct Buffer,通过FileChannel.map创建。
在处理大文件时使用MappedByteBuffer可能性能会提高,但也存在内存占用、文件关闭不确定(被其打开的文件只有在垃圾回收的才会被关闭,而且这个时间点是不确定的)等问题。

# 总结

Java IO涉及到的类众多,如果限于细节,会导致整体感官上有些混乱。希望你学习本文之后,能站在新的高度认识Java IO,从而建立起自己的知识体系。
IO是Java基础知识点之一,会关联到文件操作、网络编程等,如果要追求一劳永逸的搞明白所有概念,无疑是困难的。
建议采用循序渐进的方式进行学习,后续我也会有文件操作和网络编程的相关文章,敬请期待!

祝你变得更强!

编辑 (opens new window)
#Java IO
上次更新: 2023/06/14
Java集合
Java 文件操作

← Java集合 Java 文件操作→

最近更新
01
Spring Boot版本新特性
09-15
02
Spring框架版本新特性
09-01
03
Spring Boot开发初体验
08-15
更多文章>
Theme by Vdoing | Copyright © 2018-2025 京ICP备2021021832号-2 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式