Java——IO

六、IO

文章目录

IO是指Input/Output,这里的输入和输出是针对内存而言的:

  • Input指从外部读入数据到内存,例如,把文件从磁盘读取到内存,从网络读取数据到内存等等。
  • Output指把数据从内存输出到外部,例如,把数据从内存写入到文件,把数据从内存输出到网络等等。

为什么要把数据读到内存中才能处理这些数据呢?

是因为代码都是在内存中运行的,数据也必须读到内存,最终的表示方式无非是byte数组,字符串等,都必须存放在内存里。

从Java代码来看,输入实际上就是从外部,例如,硬盘上的某个文件,把内容读到内存,并且以Java提供的某种数据类型表示,例如,byte[]String,这样,后续代码才能处理这些数据。又因为内存有“易失性”,所以必须要把处理后的数据输出保存。

I/O 流是一种顺序读写数据的模式,特点是单向流动。

InputStream/OutputStream 是以byte(字节)为最小单位单向流动;

Reader/Writer 是以 char(字符) 为最小单位单向流动。

同步和异步

同步IO是指,读写IO时代码必须等待数据返回后才继续执行后续代码,它的优点是代码编写简单,缺点是CPU执行效率低。

异步IO是指,读写IO时仅发出请求,然后立刻执行后续代码,它的优点是CPU执行效率高,缺点是代码编写复杂。

Java标准库的包java.io提供了同步IO,而java.nio则是异步IO。上面我们讨论的InputStreamOutputStreamReaderWriter都是同步IO的抽象类。

1、File

文件是一种十分重要的存储方式。在Java的标准库java.io中提供了File 对象来操作文件和目录。

构造一个 File 对象,需要传入一个文件路径。

File f = new File("C:\\Windows\\notepad.exe");

ps:

  1. 构造File对象时,既可以传入绝对路径,也可以传入相对路径。

  2. 在Java字符串中需要用\\表示Windows的分隔符\。Linux平台使用/作为路径分隔符

  3. 可以用. 表示当前目录,.. 表示上级目录。例如:

    // 假设当前目录是C:\Docs
    File f1 = new File("sub\\javac"); // 绝对路径是C:\Docs\sub\javac
    File f3 = new File(".\\sub\\javac"); // 绝对路径是C:\Docs\sub\javac
    File f3 = new File("..\\sub\\javac"); // 绝对路径是C:\sub\javac
    
  4. File对象有一个静态变量用于表示当前平台的系统分隔符:

    System.out.println(File.separator); // 根据当前平台打印"\"或"/"
    

1.1 文件和目录

isFile() :判断该File 对象是否是一个已存在的文件;

isDirectory() :判断该 File 对象是否是一个已存在的目录;

File 对象获取到一个文件时,还可以判断文件的权限和大小:

  • boolean canRead():是否可读;
  • boolean canWrite():是否可写;
  • boolean canExecute():是否可执行;
  • long length():文件字节大小。

对于目录而言,不能直接通过 length() 方法获取大小。

1.2 创建和删除文件

当File对象表示一个文件时,可以通过createNewFile()创建一个新文件,用delete()删除该文件:

@Test
public void m0() throws IOException {
  File file = new File("./aaa.txt");
  if (!file.exists()) {
    if (file.createNewFile()) {
      System.out.println("创建文件成功");
      if (file.delete()) {
        System.out.println("文件已删除");
      }
    }
  }

}

有些时候,程序需要读写一些临时文件,File对象提供了createTempFile()来创建一个临时文件,以及deleteOnExit()在JVM退出时自动删除该文件。

public class Main {
    public static void main(String[] args) throws IOException {
        File f = File.createTempFile("tmp-", ".txt"); // 提供临时文件的前缀和后缀
        f.deleteOnExit(); // JVM退出时自动删除
        System.out.println(f.isFile());
        System.out.println(f.getAbsolutePath());
    }
}

1.3 遍历文件和目录

当File对象表示一个目录时,可以使用 list()listFiles() 列出目录下的文件和目录。两者的区别如下:

  • public String[] list() :返回的是一个字符串数组,内容是File 对象表示目录中的文件名和目录名,并不是完整的路径。
  • public File[] listFiles():返回的是一个 File 数组,表示该目录下的文件和目录名,可以通过 gtePath() 获得路径。

list()listFiles() 还提供了一系列的重载方法,可以过滤不想要的文件和目录。

@Test 
public void m1() {
  File file = new File("/Users/zhang/Desktop/2020春上课");
  String[] list = file.list();
  for (String str : list) {
    System.out.println(str);
  }

  System.out.println("========================");
  File[] files = file.listFiles();
  for (File f : files) {
    System.out.println(f.getName());
  }
  
  System.out.println("========================");

  File[] files1 = file.listFiles(new FilenameFilter() { // 仅列出.png文件
    public boolean accept(File dir, String name) {
      return name.endsWith(".png"); // 返回true表示接受该文件
    }
  });
  for (File f : files1) {
    System.out.println(f.getPath());
  }

}

和文件操作类似,File对象如果表示一个目录,可以通过以下方法创建和删除目录:

  • boolean mkdir():创建当前File对象表示的目录;
  • boolean mkdirs():创建当前File对象表示的目录,并在必要时将不存在的父目录也创建出来;
  • boolean delete():删除当前File对象表示的目录,当前目录必须为空才能删除成功。

2、InputStream

InputStream是Java标准库提供的最基本的输入流。它位于java.io这个包里。java.io包提供了所有同步IO的功能。

InputStream只是一个抽象类,它是所有输入流的超类。

2.1 FileInputStream

FileInputStreamInputStream的一个子类。它是从文件流中读取数据。下面是读取 FileInputStream 所有字节的例子:

@Test
public void m2() throws IOException {
    try (InputStream inputStream = new FileInputStream("/Users/zhangsan/Desktop/111.png")) {
        byte[] bytes = new byte[1024];
        int n = 0;
        while ((n = inputStream.read(bytes)) != -1) {
            System.out.println(bytes);
        }
    }
}

上面的 try(resource) 的语法,是和在finally 中调用 close()一样的效果 ,是让编译器自动为我们关闭资源。编译器是看 try(resource = ...) 中的对象是否实现了java.lang.AutoCloseable接口,如果实现了,就自动加上finally语句并调用close()方法。

2.2 ByteArrayInputStream

ByteArrayInputStream 可以在内存中模拟一个InputStream,虽然在实际应该中并不多,但在测试时,可以使用它来构造一个 InputStream ,不用真实的创建一个文件来读取。

@Test
public void m2() throws IOException {
  byte[] data = { 72, 101, 108, 108, 111, 33 };
  try (InputStream input = new ByteArrayInputStream(data)) {
    int n;
    while ((n = input.read()) != -1) {
      System.out.println(n);
    }
  }
}

3、OutputStream

OutputStream是Java标准库提供的最基本的输出流。

OutputStream也是抽象类,它是所有输出流的超类。

需要特别注意的是:OutputStream 还提供了一个 flush() 方法,它的目的是将缓冲区的内容真正输出到目的地。

为什么要有 flush() 方法?

因为向磁盘、网络写入数据的时候,出于效率的考虑,操作系统并不是输出一个字节就立刻写入到文件或者发送到网络,而是把输出的字节先放到内存的一个缓冲区里(本质上就是一个byte[]数组),等到缓冲区写满了,再一次性写入文件或者网络。对于很多IO设备来说,一次写一个字节和一次写1000个字节,花费的时间几乎是完全一样的,所以OutputStream有个flush()方法,能强制把缓冲区内容输出。

OutputStream 会在缓冲区满了之后,自动调用 flush() ,并且,在调用close()关闭OutputStream之前,也会自动调用flush()方法。但是如果我们想要把缓冲区中的内容立刻发送出去,就必须自己调用 flush()

3.1 FileOutputStream

@Test
public void m2() throws IOException {
  try (OutputStream output = new FileOutputStream("./readme.txt")) {
    output.write("Hello".getBytes("UTF-8")); // Hello
  } // 编译器在此自动为我们写入finally并调用close()
}

3.2 ByteArrayOutputStream

ByteArrayOutputStream可以在内存中模拟一个OutputStream

@Test
public void m2() throws IOException {
  byte[] data;
  try (ByteArrayOutputStream output = new ByteArrayOutputStream()) {
    output.write("Hello ".getBytes("UTF-8"));
    output.write("world!".getBytes("UTF-8"));
    data = output.toByteArray();
  }
  System.out.println(new String(data, "UTF-8"));
}

4、Filter模式

在Java中,JDK将InputStream分为两大类:

一类是直接提供数据的基础InputStream,例如:

  • FileInputStream
  • ByteArrayInputStream
  • ServletInputStream

一类是提供额外附加功能的InputStream,例如:

  • BufferedInputStream
  • DigestInputStream
  • CipherInputStream

当我们想给一个“基础”InputStream ,例如FileInpuStream,提供缓冲的功能时,可以用BufferedInputStream包装这个InputStream,得到的包装类型是BufferedInputStream,但它仍然被视为一个InputStream

InputStream buffered = new BufferedInputStream(file);

如果还想要其他附加功能,可以继续包装。无论我们包装多少次,得到的对象始终是 InputStream

上述这种通过一个“基础”组件再叠加各种“附加”功能组件的模式,称之为Filter模式(或者装饰器模式:Decorator)

5、操作Zip

ZipInputStream是一种FilterInputStream,它可以直接读取zip包的内容:

┌───────────────────┐
│    InputStream    │	->java.io
└───────────────────┘
          ▲
          │
┌───────────────────┐
│ FilterInputStream │	->java.io
└───────────────────┘
          ▲
          │
┌───────────────────┐
│InflaterInputStream│	->java.util.zip
└───────────────────┘
          ▲
          │
┌───────────────────┐
│  ZipInputStream   │	->java.util.zip
└───────────────────┘
          ▲
          │
┌───────────────────┐
│  JarInputStream   │	->java.util.jar
└───────────────────┘

压缩和解压

6、Reader

Reader是Java的IO库提供的另一个输入流接口。和InputStream的区别是,InputStream是一个字节流,即以byte为单位读取,而Reader是一个字符流,即以char为单位读取:

InputStream Reader
字节流,以byte为单位 字符流,以char为单位
读取字节(-1,0~255):int read() 读取字符(-1,0~65535):int read()
读到字节数组:int read(byte[] b) 读到字符数组:int read(char[] c)

java.io.Reader 是所有字符输入流的超类。

6.1 FileReader

FileReaderReader的一个子类,它可以打开文件并获取Reader。下面是如何读取一个 FileReader 的所有字符:

/*缓冲区的方法*/
public void readFile() throws IOException {
   try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8)) {
       char[] buffer = new char[1000];	//设置缓冲区
       int n;
       while ((n = reader.read(buffer)) != -1) {
           System.out.println("read " + n + " chars.");
       }
   }
}

如果文件中包含中文,可能会乱码,因为FileReader默认的编码与系统相关,例如,Windows系统的默认编码可能是GBK,打开一个UTF-8编码的文本文件就会出现乱码。要避免这样的问题,需要在创建 FileReader 时指定编码。

Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);

6.2 CharArrayReader

ByteArrayInputStream类似,CharArrayReader可以在内存中模拟一个Reader,它的作用实际上是把一个char[]数组变成一个Reader

try (Reader reader = new CharArrayReader("Hello".toCharArray())) {
}

6.3 StringReader

StringReaderCharArrayReader 几乎一样,只是把 String 作为数据源:

try (Reader reader = new StringReader("Hello")) {
}

6.4 Reader 和 InputStream关系

除了特殊的CharArrayReaderStringReader,普通的Reader实际上是基于InputStream构造的,因为Reader需要从InputStream中读入字节流(byte),然后,根据编码设置,再转换为char就可以实现字符流。

6.5 InputStreamReader

根据上面的解释可以知道,把一个InputStream转换为Reader是完全可行的。InputStreamReader就是这样一个转换器,它可以把任何InputStream转换为Reader

// 持有InputStream:
InputStream input = new FileInputStream("src/readme.txt");
// 变换为Reader:
Reader reader = new InputStreamReader(input, "UTF-8");

简洁的写法:

try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
    // TODO:
}

7、Writer

Reader是带编码转换器的InputStream,它把byte转换为char,类似的Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。

WriterOutputStream的区别如下:

OutputStream Writer
字节流,以byte为单位 字符流,以char为单位
写入字节(0~255):void write(int b) 写入字符(0~65535):void write(int c)
写入字节数组:void write(byte[] b) 写入字符数组:void write(char[] c)
无对应方法 写入String:void write(String s)

7.1 FileWriter

FileWriter就是向文件中写入字符流的Writer

try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
    writer.write('H'); // 写入单个字符
    writer.write("Hello".toCharArray()); // 写入char[]
    writer.write("Hello"); // 写入String
}

7.2 CharArrayWriter

CharArrayWriter可以在内存中创建一个Writer,它的作用实际上是构造一个缓冲区,可以写入char,最后得到写入的char[]数组,这和ByteArrayOutputStream非常类似:

try (CharArrayWriter writer = new CharArrayWriter()) {
    writer.write(65);
    writer.write(66);
    writer.write(67);
    char[] data = writer.toCharArray(); // { 'A', 'B', 'C' }
}

7.3 StringWriter

StringWriter也是一个基于内存的Writer,它和CharArrayWriter类似。实际上,StringWriter在内部维护了一个StringBuffer,并对外提供了Writer接口。

7.4 OutputStreamWriter

InputStreamReader 类似,可以通过OutputStreamWriterOutputStream转换为Writer,转换时需要指定编码:

try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
    // TODO:
}
上一篇:java读配置文件


下一篇:JavaSE基础加强之网络编程(七)