【Java SE 基础】IO

File对象

File用于操作文件和目录:

File f = new File("C:\\Windows\\notepad.exe");
//即可传入绝对路径,也可以传入相对路径 
//可以用.表示当前目录,..表示上级目录。
//Windows平台使用\作为路径分隔符,Java字符串中需要用\\表示一个\。Linux平台使用/作为路径分隔符:
System.out.println(f);

用File获取路径:

  1. getPath(),返回构造方法传入的路径

  2. getAbsolutePath(),返回绝对路径

  3. getCanonicalPath,它和绝对路径类似,但是返回的是规范路径。

    规范路径即将.和… 转换后的绝对路径 如\System32\…\notepad.exe --> \notepad.exe

文件和目录:

  1. File对象既可以表示文件,也可以表示目录;
  2. 构造一个File对象,不会导致磁盘操作,只有当调用File对象的某些方法的时候,文件或目录不存在,会出错;
  3. isFile(),判断该File对象是否是一个已存在的文件,调用isDirectory(),判断该File对象是否是一个已存在的目录:
  4. File对象获取到一个文件时,还可以进一步判断文件的权限和大小:
    • boolean canRead():是否可读;
    • boolean canWrite():是否可写;
    • boolean canExecute():是否可执行;
    • long length():文件字节大小。

创建和删除文件:

  1. 当File对象表示一个文件时,可以通过createNewFile()创建一个新文件,用delete()删除该文件:
  2. File对象提供了createTempFile()来创建一个临时文件,以及deleteOnExit()在JVM退出时自动删除该文件。

遍历文件和目录:

  1. 当File对象表示一个目录时,可以使用list()listFiles()列出目录下的文件和子目录名。

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

    File[] fs2 = f.listFiles(new FilenameFilter() { // 仅列出.exe文件
        public boolean accept(File dir, String name) {
            return name.endsWith(".exe"); // 返回true表示接受该文件
        }
    });
    
  3. 和文件操作类似,File对象如果表示一个目录,可以通过以下方法创建和删除目录:

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

Path:

  1. Path对象和File对象类似,如果需要对目录进行复杂的拼接、遍历等操作,使用Path对象更方便。

InputStream

InputStream介绍:

  1. InputStream就是Java标准库提供的最基本的输入流。位于java.io这个包;

  2. InputStream并不是一个接口,而是一个抽象类,它是所有输入流的超类。

  3. 这个抽象类定义的一个最重要的方法就是int read()签名如下:

    public abstract int read() throws IOException;
    //该方法会读取输入流的下一个字节,并返回字节表示的int值(0~255)。如果已读到末尾,返回-1表示不能继续读取了。
    

FileInputStream:

  1. 从文件流中读取数据:

    public void readFile() throws IOException {
        InputStream input = null;
        try {
            input = new FileInputStream("src/readme.txt");
            int n;
            while ((n = input.read()) != -1) { // 利用while同时读取并判断
                System.out.println(n);
            }
        } finally {
            if (input != null) { input.close(); }//必须要关闭流,释放底层资源
        }
    }
    ------------------//java7引入的新的try(resource)的语法
    public void readFile() throws IOException {
        try (InputStream input = new FileInputStream("src/readme.txt")) {
            int n;
            while ((n = input.read()) != -1) {
                System.out.println(n);
            }
        } // 编译器在此自动为我们写入finally并调用close() try中的resource对象需实现java.lang.AutoCloseable接口,InputStream和OutputStream都实现了这个接口
    }
    

缓冲:

  1. InputStream提供了两个重载方法来支持读取多个字节:

    • int read(byte[] b):读取若干字节并填充到byte[]数组,返回读取的字节数
    • int read(byte[] b, int off, int len):指定byte[]数组的偏移量和最大填充数
  2. 利用缓冲读取数据:

    public void readFile() throws IOException {
        try (InputStream input = new FileInputStream("src/readme.txt")) {
            // 定义1000个字节大小的缓冲区:
            byte[] buffer = new byte[1000];
            int n;
            while ((n = input.read(buffer)) != -1) { // 读取到缓冲区
                System.out.println("read " + n + " bytes.");
            }
        }
    }
    

阻塞:

int n;
n = input.read(); // 必须等待read()方法返回才能执行下一行代码
int m = n;

InputStream实现类:

  1. FileInputStream:可以从文件获取输入流 (上面提到)

  2. ByteArrayInputStream:在内存中模拟 一个InputStream

    byte[] data = { 72, 101, 108, 108, 111, 33 };
    //实际上是把一个byte[]数组在内存中变成一个InputStream,多用于测试时构造InputStream
    try (InputStream input = new ByteArrayInputStream(data)) {
        int n;
        while ((n = input.read()) != -1) {
            System.out.println((char)n);
        }
    }
    

OutputStream

OutputStream介绍:

  1. OutputStream是Java标准库提供的最基本的输出流。位于java.io这个包;

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

  3. 这个抽象类定义的一个最重要的方法就是void write(int b),签名如下:

    public abstract void write(int b) throws IOException;
    //这个方法会写入一个字节到输出流,传入的是int参数,只写入int最低8位(为一个字节)
    
  4. OutputStream不仅提供了close()方法关闭输出流,还提供了一个flush()方法,将缓冲区的内容真正输出到目的地。(操作系统会等待缓冲区满再输出,flush()方法能强制输出)

FIleOutputStream:

  1. 将若干个字节写入文件流:

    public void writeFile() throws IOException {
        //没有考虑发生异常
        OutputStream output = new FileOutputStream("out/readme.txt");
        output.write(72); // H
        output.write(101); // e
        output.write(108); // l
        output.write(108); // l
        output.write(111); // o
        output.close();
    }
    
  2. 缓冲–OutputStream提供的重载方法void write(byte[])来实现:

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

OutputStream实现类:

  1. FileOutputStream: 可以从文件获取输出流

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

    byte[] data;
    //实际上是把一个byte[]数组在内存中变成一个OutputStream,多用于测试时构造OutputStream
    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"));
    
  3. 补充:同时操作多个AutoCloseable资源时,在try(resource) { ... }语句中可以同时写出多个资源,用;隔开

    // 读取input.txt,写入output.txt:
    try (InputStream input = new FileInputStream("input.txt");
         OutputStream output = new FileOutputStream("output.txt"))
    {
        input.transferTo(output); // transferTo的作用是直接将输入流写入输出流
    }
    

Filter模式

存在的问题:

  1. Java的IO标准库提供的InputStream根据来源可以包括:
    • FileInputStream:从文件读取数据,是最终数据源;
    • ServletInputStream:从HTTP请求读取数据,是最终数据源;
    • Socket.getInputStream():从TCP连接读取数据,是最终数据源;
  2. 我们要给FileInputStream添加缓冲功能,需要从FileInputStream派生一个类
  3. 如果要给FileInputStream添加计算签名的功能,需要从FileInputStream派生一个类
  4. 如果要给FileInputStream添加加密/解密功能,还是需要从FileInputStream派生一个类
  5. 对其他数据来源的InputStream添加功能,就会出现非常多的子类

解决方式:

  1. 将InputStream分为两大类:

    1. 一类是直接提供数据的基础InputStream,例如:
      • FileInputStream
      • ByteArrayInputStream
      • ServletInputStream
    2. 一类是提供额外附加功能的InputStream,例如:
      • BufferedInputStream
      • DigestInputStream
      • CipherInputStream
  2. 当我们需要添加功能时,先确定提供数据来源的InputStream,例如数据来自文件

    InputStream file = new FileInputStream("test.gz");
    
  3. 开始添加功能,我们希望添加缓冲的功能,因此我们用BufferedInputStream包装这个InputStream,得到的包装类型是BufferedInputStream,但仍用InputStream指向它

    InputStream buffered = new BufferedInputStream(file);
    
  4. 最后,如果该文件被gzip压缩了,我们希望直接读取压缩的内容,就可以进一步包装一个GZIPInputStream

    InputStream gzip = new GZIPInputStream(buffered);
    

但无论包装几次,我们都用InputStream来指向它,这就是FIlter模式,也可以称为装饰器模式

实践:

编写一个CountInputStream,它的作用是对输入的字节进行计数:

public class Main {
    public static void main(String[] args) throws IOException {
        byte[] data = "hello, world!".getBytes("UTF-8");
        try (CountInputStream input = new CountInputStream(new ByteArrayInputStream(data))) {//只要持有最外层的InputStream,当最外层的InputStream关闭时,内层的InputStream的close()方法也会被自动调用
            int n;
            while ((n = input.read()) != -1) {
                System.out.println((char)n);
            }
            System.out.println("Total read " + input.getBytesRead() + " bytes");
        }
    }
}

class CountInputStream extends FilterInputStream {
    private int count = 0;

    CountInputStream(InputStream in) {
        super(in);
    }

    public int getBytesRead() {
        return this.count;
    }

    public int read() throws IOException {
        int n = in.read();
        if (n != -1) {
            this.count ++;
        }
        return n;
    }

    public int read(byte[] b, int off, int len) throws IOException {
        int n = in.read(b, off, len);
        this.count += n;
        return n;
    }
}

操作Zip

ZipInputStream:

  1. ZipInputStream是一种FilterInputStream,它可以直接读取zip包的内容。
  2. JarInputStream是从ZipInputStream派生,它增加的主要功能是直接读取jar文件里面的MANIFEST.MF文件。

实践:

  1. 读取zip包

    try (ZipInputStream zip = new ZipInputStream(new FileInputStream(...))) {//传入一个FileInputStream作为数据源
        ZipEntry entry = null;//一个ZipEntry表示一个压缩文件或目录
        while ((entry = zip.getNextEntry()) != null) {//返回null,表示zip流结束
            String name = entry.getName();
            if (!entry.isDirectory()) {
                int n;
                while ((n = zip.read()) != -1) {
                    ...
                }
            }
        }
    }
    
  2. 写入zip包

    try (ZipOutputStream zip = new ZipOutputStream(new FileOutputStream(...))) {
        File[] files = ...
        for (File file : files) {
            zip.putNextEntry(new ZipEntry(file.getName()));
            zip.write(getFileDataAsBytes(file));
            zip.closeEntry();//closeEntry()结束这个文件的打包。
        }
    }
    //要实现目录层次结构,new ZipEntry(name)传入的name要用相对路径。
    

读取classpath资源

存在问题:

  1. Java存放.class的目录或jar包也可以包含任意其他类型的文件,例如:
    • 配置文件,例如.properties
    • 图片文件,例如.jpg
    • 文本文件,例如.txt.csv
  2. 不同环境下文件路径不一致,不能从磁盘的固定目录读取配置文件;

解决方法:

  1. default.properties配置文件文件放到classpath中,不用关心它的实际存放路径。

  2. 在classpath中的资源文件,路径总是以开头,我们先获取当前的Class对象,然后调用getResourceAsStream()就可以直接从classpath读取任意的资源文件:

    try (InputStream input = getClass().getResourceAsStream("/default.properties")) {
        if (input != null) {//资源不存在,返回null,需要判断
            // TODO:
        }
    }
    
  3. 灵活配置:把默认的配置放到jar包中,再从外部文件读取一个可选的配置文件,就可以做到既有默认的配置,又可以让用户自己修改配置

    Properties props = new Properties();
    props.load(inputStreamFromClassPath("/default.properties"));
    props.load(inputStreamFromFile("./conf.properties"));
    

序列化

介绍:

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

序列化:

  1. 一个Java对象要能序列化,必须实现一个特殊的java.io.Serializable接口,它的定义如下:

    public interface Serializable {
    }
    //没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”.
    
  2. 把一个Java对象变为byte[]数组,需要使用ObjectOutputStream。它负责把一个Java对象写入一个字节流:

    public class Main {
        public static void main(String[] args) throws IOException {
            ByteArrayOutputStream buffer = new ByteArrayOutputStream();
            try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
                // 写入int:
                output.writeInt(12345);
                // 写入String:
                output.writeUTF("Hello");
                // 写入Object:
                output.writeObject(Double.valueOf(123.456));
            }
            System.out.println(Arrays.toString(buffer.toByteArray()));
        }
    }
    //ObjectOutputStream既可以写入基本类型,也可以写入String(以UTF-8编码),还可以写入实现了Serializable接口的Object。
    

反序列化:

  1. ObjectInputStream负责从一个字节流读取Java对象:

    try (ObjectInputStream input = new ObjectInputStream(...)) {
        int n = input.readInt();
        String s = input.readUTF();
        Double d = (Double) input.readObject();
    }
    //ObjectInputStream除了能读取基本类型和String类型外,调用readObject()可以直接返回一个Object对象。要把它变成一个特定类型,必须强制转型。
    
  2. readObject()可能抛出的异常有:

    • ClassNotFoundException:没有找到对应的Class;-- 传输后另一台电脑上没有定义该类,无法反序列化。
    • InvalidClassException:Class不匹配。-- 定义的字段在反序列化时被修改了类型,导致class不兼容。
  3. 为避免不兼容,Java的序列化允许class定义一个特殊的serialVersionUID静态变量,用于标识序列化的“版本",通常由IDE自动生成,在增加或修改字段时,可以改变serialVersionUID的值,能阻止不匹配的class版本

    public class Person implements Serializable {
        private static final long serialVersionUID = 2709425275741743919L;
    }
    

安全性:

  1. Java的序列化机制可以导致一个实例能直接从byte[]数组创建,不经过构造方法,存在一定安全隐患。
  2. Java的序列化还存在兼容性问题,通过JSON这样的通用数据结构来实现序列化是更好的方法。

Reader

和InputStream区别:

  1. InputStream是一个字节流,即以byte为单位读取,而Reader是一个字符流,即以char为单位读取:

    InputStreamReader
    字节流,以byte为单位字符流,以char为单位
    读取字节(-1,0~255):int read()读取字符(-1,0~65535):int read()
    读到字节数组:int read(byte[] b)读到字符数组:int read(char[] c)
  2. java.io.Reader是所有字符输入流的超类,它最主要的方法是:

    public int read() throws IOException;
    //读取字符流的下一个字符,并返回字符表示的int,范围是0~65535。如果已读到末尾,返回-1。
    

FileReader:

  1. FileReaderReader的一个子类,它可以打开文件并获取Reader:

    public void readFile() throws IOException {
        // 创建一个FileReader对象:
        Reader reader = new FileReader("src/readme.txt"); // 可以设置字符编码,但此处没有设置
        //Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8);  避免乱码,应该设置编码
        for (;;) {
            int n = reader.read(); // 反复调用read()方法,直到返回-1
            if (n == -1) {
                break;
            }
            System.out.println((char)n); // 打印char
        }
        reader.close(); // 关闭流
    }
    
  2. Reader也是一种资源,需要保证出错的时候能正确关闭,我们可以用try (resource)来保证Reader能够正确地关闭:

    try (Reader reader = new FileReader("src/readme.txt", StandardCharsets.UTF_8) {
        // TODO
    }
    
  3. Reader还提供了一次性读取若干字符并填充到char[]数组的方法:

    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) {//回实际读入的字符个数,最大不超过char[]数组的长度。返回-1表示流结束。
                System.out.println("read " + n + " chars.");
            }
        }
    }
    

Reader实现类:

  1. FileReader: 可以打开文件并获取Reader

  2. CharArrayReader: 可以在内存中模拟一个Reader

    try (Reader reader = new CharArrayReader("Hello".toCharArray())) {
    }
    //它的作用实际上是把一个char[]数组变成一个Reader
    
  3. StringReader: 可以直接把String作为数据源

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

InputStreamReader:

  1. Reader是基于InputStream构造的:可以通过InputStreamReader在指定编码的同时将任何InputStream转换为Reader

    // 持有InputStream:
    InputStream input = new FileInputStream("src/readme.txt");
    // 变换为Reader:
    Reader reader = new InputStreamReader(input, "UTF-8");
    
  2. 可以指定编码,并通过try (resource)更简洁地改写如下:

    try (Reader reader = new InputStreamReader(new FileInputStream("src/readme.txt"), "UTF-8")) {
        // TODO:
    }
    //使用try (resource)结构时,只需要关闭最外层的Reader对象即可。
    

Writer

和OutputStream区别:

  1. Writer就是带编码转换器的OutputStream,它把char转换为byte并输出。

    OutputStreamWriter
    字节流,以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)
  2. Writer是所有字符输出流的超类,它提供的方法主要有:

    • 写入一个字符(0~65535):void write(int c)
    • 写入字符数组的所有字符:void write(char[] c)
    • 写入String表示的所有字符:void write(String s)

Writer实现类:

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

    try (Writer writer = new FileWriter("readme.txt", StandardCharsets.UTF_8)) {
        writer.write('H'); // 写入单个字符
        writer.write("Hello".toCharArray()); // 写入char[]
        writer.write("Hello"); // 写入String
    }
    
  2. CharArrayWriter:可以在内存中创建一个Writer

    try (CharArrayWriter writer = new CharArrayWriter()) {
        writer.write(65);
        writer.write(66);
        writer.write(67);
        char[] data = writer.toCharArray(); // { 'A', 'B', 'C' }
    }
    //它的作用实际上是构造一个缓冲区,可以写入char,最后得到写入的char[]数组
    
  3. StringWriter也是一个基于内存的Writer,它和CharArrayWriter类似。实际上,StringWriter在内部维护了一个StringBuffer,并对外提供了Writer接口。

OutputStreamWriter:

  1. Writer是基于OutputStream构造的,可以通过OutputStreamWriterOutputStream转换为Writer,转换时需要指定编码。

    try (Writer writer = new OutputStreamWriter(new FileOutputStream("readme.txt"), "UTF-8")) {
        // TODO:
    }
    

PrintStream和PrintWriter

PrintStream:

  1. PrintStream是一种FilterOutputStream,它在OutputStream的接口上,额外提供了一些写入各种数据类型的方法:

    • 写入intprint(int)
    • 写入booleanprint(boolean)
    • 写入Stringprint(String)
    • 写入Objectprint(Object),实际上相当于print(object.toString())
  2. 我们经常使用的System.out.println()实际上就是使用PrintStream打印各种数据。其中,System.out是系统默认提供的PrintStream,表示标准输出:

    System.out.print(12345); // 输出12345
    System.out.print(new Object()); // 输出类似java.lang.Object@3c7a835a
    System.out.println("Hello"); // 输出Hello并换行
    

PrintWriter:

  1. PrintStream最终输出的总是byte数据,而PrintWriter则是扩展了Writer接口,它的print()/println()方法最终输出的是char数据:

    public class Main {
        public static void main(String[] args)     {
            StringWriter buffer = new StringWriter();
            try (PrintWriter pw = new PrintWriter(buffer)) {
                pw.println("Hello");
                pw.println(12345);
                pw.println(true);
            }
            System.out.println(buffer.toString());
        }
    }
    

使用Files

FIles:

  1. Files封装了很多读写文件的简单方法,例如把一个文件的全部内容读取为一个byte[]

    byte[] data = Files.readAllBytes(Paths.get("/path/to/file.txt"));
    
  2. 把一个文件的全部内容读取为String

    // 默认使用UTF-8编码读取:
    String content1 = Files.readString(Paths.get("/path/to/file.txt"));
    // 可指定编码:
    String content2 = Files.readString(Paths.get("/path/to/file.txt"), StandardCharsets.ISO_8859_1);
    // 按行读取并返回每行内容:
    List<String> lines = Files.readAllLines(Paths.get("/path/to/file.txt"));
    
  3. 写入文件:

    // 写入二进制文件:
    byte[] data = ...
    Files.write(Paths.get("/path/to/file.txt"), data);
    // 写入文本并指定编码:
    Files.writeString(Paths.get("/path/to/file.txt"), "文本内容...", StandardCharsets.ISO_8859_1);
    // 按行写入文本:
    List<String> lines = ...
    Files.write(Paths.get("/path/to/file.txt"), lines);
    
  4. Files工具类还有copy()delete()exists()move()等快捷方法操作文件和目录。

  5. Files提供的读写方法,受内存限制,只能读写小文件,例如配置文件等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值