java.io.File 类的使用

概述

File 类及本章下的各种流,都定义在 java.io 包下。

一个 File 对象代表硬盘或网络中可能存在的一个文件或者文件目录(俗称文件夹),与平台无关。(体会万事万物皆对象)

File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。(File 对象可以作为参数传递给流的构造器)

想要在 Java 程序中表示一个真实存在的文件或目录,那么必须有一个 File 对象,但是 Java 程序中的一个 File 对象,可能没有一个真实存在的文件或目录。

构造器

  • public File(String pathname):以 pathname 为路径创建 File 对象,可以是绝对路径或者相对路径,如果 pathname 是相对路径,则默认相对的路径是系统属性 user.dir
  • public File(String parent, String child):以 parent 为父路径,child 为子路径创建 File 对象。
  • public File(File parent, String child):根据一个父 File 对象和子文件路径创建 File 对象。

关于路径:

  • 绝对路径:从盘符开始的路径,这是一个完整的路径。
  • 相对路径:相对于项目目录的路径。
    • IDEA 中,main 中的文件的相对路径,是相对于”当前工程”
    • IDEA 中,单元测试方法中的文件的相对路径,是相对于”当前 module”

注意:

  1. 无论该路径下是否存在文件或者目录,都不影响 File 对象的创建。
  2. Windows 的路径分隔符使用“\”,而 Java 程序中的“\”表示转义字符,所以在 Windows 中表示路径,需要用“\\”。或者直接使用“/”也可以,Java 程序支持将“/”当成平台无关的路径分隔符。或者直接使用 File.separator 常量值表示。比如:
    • File file2 = new File("d:" + File.separator + "atguigu" + File.separator + "info.txt");
  3. 当构造路径是绝对路径时,那么 getPathgetAbsolutePath 结果一样;当构造路径是相对路径时,那么 getAbsolutePath 的路径 = user.dir 的路径 + 构造路径

常用方法

1、获取文件和目录基本信息

  • public String getName():获取名称
  • public String getPath():获取路径
  • public String getAbsolutePath():获取绝对路径
  • public File getAbsoluteFile():获取绝对路径表示的文件
  • public String getParent():获取上层文件目录路径。若无,返回 null
  • public long length():获取文件长度(字节数)。不能获取目录的长度。
  • public long lastModified():获取最后一次的修改时间,毫秒值

如果 File 对象代表的文件或目录存在,则 File 对象实例初始化时,就会用硬盘中对应文件或目录的属性信息(例如,时间、类型等)为 File 对象的属性赋值,否则除了路径和名称,File 对象的其他属性将会保留默认值。

列出目录的下一级

  • public String[] list():返回一个 String 数组,表示该 File 目录中的所有子文件或目录。
  • public File[] listFiles():返回一个 File 数组,表示该 File 目录中的所有的子文件或目录。

重命名

  • public boolean renameTo(File dest):把文件重命名为指定的文件路径。

判断功能的方法

  • public boolean exists():此 File 表示的文件或目录是否实际存在。
  • public boolean isDirectory():此 File 表示的是否为目录。
  • public boolean isFile():此 File 表示的是否为文件。
  • public boolean canRead():判断是否可读
  • public boolean canWrite():判断是否可写
  • public boolean isHidden():判断是否隐藏

如果文件或目录不存在,那么 exists()isFile()isDirectory() 都是返回 false

创建、删除

  • public boolean createNewFile():创建文件。若文件存在,则不创建,返回 false
  • public boolean mkdir():创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。
  • public boolean mkdirs():创建文件目录。如果上层文件目录不存在,一并创建。
  • public boolean delete():删除文件或者文件夹 删除注意事项:
    • Java 中的删除不走回收站。
    • 要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。

IO 流原理及流的分类

Java IO 原理

Java 程序中,对于数据的输入/输出操作以“流(stream)” 的方式进行,可以看做是一种数据的流动。

I/O 流中的 I/O 是 Input/Output 的缩写, I/O 技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。

  • 输入(input):读取外部数据(磁盘、光盘等存储设备的数据)到程序(内存)中。
  • 输出(output):将程序(内存)数据输出到磁盘、光盘等存储设备中。

流的分类

java.io 包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。

按数据的流向不同分为:输入流和输出流

  • 输入流:把数据从其他设备上读取到内存中的流。
    • InputStreamReader 结尾
  • 输出流:把数据从内存中写出到其他设备上的流。
    • OutputStreamWriter 结尾

按操作数据单位的不同分为:字节流(8 bit)和字符流(16 bit)

  • 字节流:以字节为单位,读写数据的流。
    • InputStreamOutputStream 结尾
  • 字符流:以字符为单位,读写数据的流。
    • ReaderWriter 结尾

根据 IO 流的角色不同分为:节点流和处理流

  • 节点流:直接从数据源或目的地读写数据
  • 处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能

流的 API

Java 的 IO 流共涉及 40 多个类,实际上非常规则,都是从如下 4 个抽象基类派生的:

(抽象基类)输入流输出流
字节流InputStreamOutputStream
字符流ReaderWriter

由这四个类派生出来的子类名称都是以其父类名作为子类名后缀:

常用的节点流:

  • 文件流:FileInputStreamFileOutputStreamFileReaderFileWriter
  • 字节/字符数组流:ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter
    • 对数组进行处理的节点流(对应的不再是文件,而是内存中的一个数组)。

常用处理流:

  • 缓冲流:BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
    • 作用:增加缓冲功能,避免频繁读写硬盘,进而提升读写效率。
  • 转换流:InputStreamReaderOutputStreamReader
    • 作用:实现字节流和字符流之间的转换。
  • 对象流:ObjectInputStreamObjectOutputStream
    • 作用:提供直接读写 Java 对象功能

节点流之一:FileReader/FileWriter

ReaderWriter

Java 提供一些字符流类,以字符为单位读写数据,专门用于处理文本文件。不能操作图片,视频等非文本文件。

常见的文本文件有如下的格式:.txt.java.c.cpp.py

注意:.doc.xls.ppt 这些都不是文本文件。

字符输入流:Reader

java.io.Reader 抽象类是表示用于读取字符流的所有类的父类,可以读取字符信息到内存中。它定义了字符输入流的基本共性功能方法。

  • public int read():从输入流读取一个字符。虽然读取了一个字符,但是会自动提升为 int 类型。返回该字符的 Unicode 编码值。如果已经到达流末尾了,则返回 -1。
  • public int read(char[] cbuf):从输入流中读取一些字符,并将它们存储到字符数组 cbuf 中 。每次最多读取 cbuf.length 个字符。返回实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返回 -1。
  • public int read(char[] cbuf, int off, int len):从输入流中读取一些字符,并将它们存储到字符数组 cbuf 中,从 cbuf[off] 开始的位置存储。每次最多读取 len 个字符。返回实际读取的字符个数。如果已经到达流末尾,没有数据可读,则返回 -1。
  • public void close():关闭此流并释放与此流相关联的任何系统资源。

注意:当完成流的操作时,必须调用 close() 方法,释放系统资源,否则会造成资源占用、内存泄漏。

字符输出流:Writer

java.io.Writer 抽象类是表示用于写出字符流的所有类的超类,将指定的字符信息写出到目的地。它定义了字节输出流的基本共性功能方法。

  • public void write(int c):写出单个字符。
  • public void write(char[] cbuf):写出字符数组。
  • public void write(char[] cbuf, int off, int len):写出字符数组的某一部分。off:数组的开始索引;len:写出的字符个数。
  • public void write(String str):写出字符串。
  • public void write(String str, int off, int len):写出字符串的某一部分。off:字符串的开始索引;len:写出的字符个数。
  • public void flush():刷新该流的缓冲。
  • public void close():关闭此流。

注意:当完成流的操作时,必须调用 close() 方法,释放系统资源,否则会造成资源占用、内存泄漏。

FileReaderFileWriter

FileReader

java.io.FileReader 类用于读取字符文件,构造时使用系统默认的字符编码和默认字节缓冲区。

  • FileReader(File file):创建一个新的 FileReader ,给定要读取的 File 对象。
  • FileReader(String fileName):创建一个新的 FileReader ,给定要读取的文件的路径和名称。

举例:读取 hello.txt 文件中的字符数据,并显示在控制台上

public class FileReaderWriterTest {
    // 实现方式 1
    @Test
    public void test1() throws IOException {
	    try {
	        // 1. 创建 File 类的对象,对应着物理磁盘上的某个文件
	        File file = new File("hello.txt");
	        // 2. 创建 FileReader 流对象,将 File 类的对象作为参数传递到 FileReader 的构造器中
	        FileReader fr = new FileReader(file);
	        // 3. 通过相关流的方法,读取文件中的数据
	        int data;
	        while ((data = fr.read()) != -1) { // 每调用一次读取一个字符
	            System.out.print((char) data);
	        }
		} catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭相关的流资源
            try {
                if (fr != null)
                    fr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
 
    // 实现方式 2:调用 read(char[] cbuf), 每次从文件中读取多个字符
    @Test
    public void test3() {
        FileReader fr = null;
        try {
            // 1. 创建 File 类的对象,对应着物理磁盘上的某个文件
            File file = new File("hello.txt");
            // 2. 创建 FileReader 流对象,将 File 类的对象作为参数传递到 FileReader 的构造器中
            fr = new FileReader(file);
            // 3. 通过相关流的方法,读取文件中的数据
            char[] cbuf = new char[1024];
            /*
             * read(char[] cbuf): 每次将文件中的数据读入到 cbuf 数组中,并返回读入到数组中的
             * 字符的个数。
            */
            int len; // 记录每次读入的字符的个数
            while ((len = fr.read(cbuf)) != -1) {
                // 处理 char[] 数组即可
                // 错误:
                // for (int i = 0; i < cbuf.length; i++) {
                //     System.out.print(cbuf[i]);
                // }
                // 正确:
                // for (int i = 0; i < len; i++) {
                //     System.out.print(cbuf[i]);
                // }
                // 正确:
                String str = new String(cbuf, 0, len);
                System.out.print(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭相关的流资源
            try {
                if (fr != null)
                    fr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

FileWriter

java.io.FileWriter 类用于写出字符到文件,构造时使用系统默认的字符编码和默认字节缓冲区。

  • FileWriter(File file):创建一个新的 FileWriter,给定要写入的 File 对象。
  • FileWriter(String fileName):创建一个新的 FileWriter,给定要读取的文件的路径和名称。
  • FileWriter(File file, boolean append):创建一个新的 FileWriter,指明是否在现有文件末尾追加内容。

举例:

public class FWWrite {
    // 注意:应该使用 try-catch-finally 处理异常。这里出于方便阅读代码,使用了 throws 的方式
    @Test
    public void test01() throws IOException {
        // 使用文件名称创建流对象
        FileWriter fw = new FileWriter(new File("fw.txt"));
        // 写出数据
        fw.write(97); // 写出第 1 个字符
        fw.write('b'); // 写出第 2 个字符
        fw.write('C'); // 写出第 3 个字符
        fw.write(30000); // 写出第 4 个字符,中文编码表中 30000 对应一个汉字
		
        // 关闭资源
        fw.close();
    }
 
    @Test
    public void test02() throws IOException {
        // 使用文件名称创建流对象
        FileWriter fw = new FileWriter(new File("fw.txt"));
        // 字符串转换为字节数组
        char[] chars = "尚硅谷".toCharArray();
 
        // 写出字符数组
        fw.write(chars); // 尚硅谷
        // 写出从索引 1 开始,2 个字符
        fw.write(chars, 1, 2); // 硅谷
 
        // 关闭资源
        fw.close();
    }
 
    @Test
    public void test03() throws IOException {
        // 使用文件名称创建流对象
        FileWriter fw = new FileWriter("fw.txt");
        // 字符串
        String msg = "尚硅谷";
 
        // 写出字符数组
        fw.write(msg); // 尚硅谷
        // 写出从索引 1 开始,2 个字符
        fw.write(msg,1,2); // 硅谷
 
        // 关闭资源
        fw.close();
    }
    
    @Test
    public void test04() {
        FileWriter fw = null;
        try {
            // 1. 创建 File 对象
            File file = new File("personinfo.txt");
            // 2. 创建 FileWriter 对象,将 File 对象作为参数传递到 FileWriter 的构造器中
            // 如果输出的文件已存在,则会对现有的文件进行覆盖
            fw = new FileWriter(file);
            // 相当于:fw = new FileWriter(file, false);
 
            // 如果输出的文件已存在,则会在现有的文件末尾写入数据
            // fw = new FileWriter(file,true);
 
            // 3. 调用相关的方法,实现数据的写出操作
            // write(String str) / write(char[] cbuf)
            fw.write("I love you,");
            fw.write("you love him.");
            fw.write("so sad".toCharArray());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭资源
            try {
                if (fw != null)
                    fw.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

小结

  1. 因为出现流资源的调用,为了总是关闭流资源,需要使用 try-catch-finally 处理异常
  2. 对于输入流来说,File 类的对象必须在物理磁盘上存在,否则执行就会报 FileNotFoundException。如果传入的是一个目录,则会报 IOException 异常。
  3. 对于输出流来说,File 类的对象是可以不存在的。
    • 如果 File 类的对象不存在,则可以在输出的过程中,自动创建 File 类的对象
    • 如果 File 类的对象存在,
      • 如果调用 FileWriter(File file)FileWriter(File file, false),输出时会覆盖已有的内容
      • 如果调用 FileWriter(File file,true) 构造器,则在现有的文件末尾追加写出内容。

关于 flush(刷新)

因为内置缓冲区的原因,如果 FileWriter 不关闭输出流,无法写出字符到文件中。但是关闭的流对象,是无法继续写出数据的。如果我们既想写出数据到文件,又想继续使用流,就需要 flush() 方法了。

  • flush():刷新缓冲区,流对象可以继续使用。
  • close():先刷新缓冲区,然后通知系统释放资源。流对象不可以再被使用了。

注意:即便是 flush() 方法写出了数据,操作的最后还是要调用 close() 方法,释放系统资源。

节点流之二:FileInputStream/FileOutputStream

如果我们读取或写出的数据是非文本文件,则 ReaderWriter 就无能为力了,必须使用字节流。

InputStreamOutputStream

字节输入流:InputStream

java.io.InputStream 抽象类是表示字节输入流的所有类的超类,可以读取字节信息到内存中。它定义了字节输入流的基本共性功能方法。

  • public int read():从输入流读取一个字节。返回读取的字节值。虽然读取了一个字节,但是会自动提升为 int 类型。如果已经到达流末尾,没有数据可读,则返回 -1。
  • public int read(byte[] b):从输入流中读取一些字节数,并将它们存储到字节数组 b 中。每次最多读取 b.length 个字节。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回 -1。
  • public int read(byte[] b, int off, int len):从输入流中读取一些字节数,并将它们存储到字节数组 b 中,从 b[off] 开始存储,每次最多读取 len 个字节。返回实际读取的字节个数。如果已经到达流末尾,没有数据可读,则返回 -1。
  • public void close():关闭此输入流并释放与此流相关联的任何系统资源。

注意:close() 方法,当完成流的操作时,必须调用此方法,释放系统资源。

字节输出流:OutputStream

java.io.OutputStream 抽象类是表示字节输出流的所有类的超类,将指定的字节信息写出到目的地。它定义了字节输出流的基本共性功能方法。

  • public void write(int b):将指定的字节输出流。虽然参数为 int 类型四个字节,但是只会保留一个字节的信息写出。
  • public void write(byte[] b):将 b.length 字节从指定的字节数组写入此输出流。
  • public void write(byte[] b, int off, int len):从指定的字节数组写入 len 字节,从偏移量 off 开始输出到此输出流。
  • public void flush():刷新此输出流并强制任何缓冲的输出字节被写出。
  • public void close():关闭此输出流并释放与此流相关联的任何系统资源。

注意:close() 方法,当完成流的操作时,必须调用此方法,释放系统资源。

FileInputStreamFileOutputStream

FileInputStream

java.io.FileInputStream 类是文件输入流,从文件中读取字节。

  • FileInputStream(File file):通过打开与实际文件的连接来创建一个 FileInputStream,该文件由文件系统中的 File 对象 file 命名。
  • FileInputStream(String name):通过打开与实际文件的连接来创建一个 FileInputStream ,该文件由文件系统中的路径名 name 命名。

举例:

// read.txt 文件中的内容如下:
abcde
public class FISRead {
    // 注意:应该使用 try-catch-finally 处理异常。这里出于方便阅读代码,使用了 throws 的方式
    @Test
    public void test() throws IOException {
        // 使用文件名称创建流对象
        FileInputStream fis = new FileInputStream("read.txt");
        // 读取数据,返回一个字节
        int read = fis.read();
        System.out.println((char) read); // a
        read = fis.read();
        System.out.println((char) read); // b
        read = fis.read();
        System.out.println((char) read); // c
        read = fis.read();
        System.out.println((char) read); // d
        read = fis.read();
        System.out.println((char) read); // e
        // 读取到末尾,返回 -1
        read = fis.read();
        System.out.println(read); // -1
        // 关闭资源
        fis.close();
    }
 
    @Test
    public void test02() throws IOException{
        // 使用文件名称创建流对象
        FileInputStream fis = new FileInputStream("read.txt");
        // 定义变量,保存数据
        int b;
        // 循环读取
        while ((b = fis.read()) != -1) {
            System.out.println((char)b);
        }
        // 关闭资源
        fis.close();
    }
	
    @Test
    public void test03() throws IOException {
        // 使用文件名称创建流对象.
        FileInputStream fis = new FileInputStream("read.txt");
        // 定义变量,作为有效个数
        int len;
        // 定义字节数组,作为装字节数据的容器
        byte[] b = new byte[2];
        // 循环读取
        while ((len = fis.read(b)) != -1) {
            // 每次读取后, 把数组变成字符串打印
            System.out.println(new String(b));
        }
        // 关闭资源
        fis.close();
        /*
        输出结果:
        ab
        cd
        ed
        最后错误数据 d,是由于最后一次读取时,只读取一个字节 e,数组中,
        上次读取的数据没有被完全替换,所以要通过 len,获取有效的字节
        */
    }
 
    @Test
    public void test04() throws IOException{
        // 使用文件名称创建流对象
        FileInputStream fis = new FileInputStream("read.txt");
        // 定义变量,作为有效个数
        int len;
        // 定义字节数组,作为装字节数据的容器
        byte[] b = new byte[2];
        // 循环读取
        while ((len = fis.read(b)) != -1) {
            // 每次读取后, 把数组的有效字节部分,变成字符串打印
            System.out.println(new String(b, 0, len)); // len: 每次读取的有效字节个数
        }
        // 关闭资源
        fis.close();
        /*
        输出结果:
        ab
        cd
        e
        */
    }
}

FileOutputStream

java.io.FileOutputStream 类是文件输出流,用于将数据写出到文件。

  • public FileOutputStream(File file):创建文件输出流,写出由指定的 File 对象表示的文件。
  • public FileOutputStream(String name):创建文件输出流,指定的名称为写出文件。
  • public FileOutputStream(File file, boolean append):创建文件输出流,指明是否在现有文件末尾追加内容。

举例:

package com.atguigu.fileio;
 
import org.junit.Test;
import java.io.FileOutputStream;
import java.io.IOException;
 
public class FOSWrite {
    // 注意:应该使用 try-catch-finally 处理异常。这里出于方便阅读代码,使用了 throws 的方式
    @Test
    public void test01() throws IOException {
        // 使用文件名称创建流对象
        FileOutputStream fos = new FileOutputStream("fos.txt");
        // 写出数据
        fos.write(97); // 写出第 1 个字节
        fos.write(98); // 写出第 2 个字节
        fos.write(99); // 写出第 3 个字节
        // 关闭资源
        fos.close();
        /* 输出结果:abc */
    }
 
    @Test
    public void test02() throws IOException {
        // 使用文件名称创建流对象
        FileOutputStream fos = new FileOutputStream("fos.txt");
        // 字符串转换为字节数组
        byte[] b = "abcde".getBytes();
        // 写出从索引 2 开始,2 个字节
        // 索引 2 是 c,两个字节,也就是 cd
        fos.write(b, 2, 2);
        // 关闭资源
        fos.close();
    }
 
    // 这段程序如果多运行几次,每次都会在原来文件末尾追加 abcde
    @Test
    public void test03()throws IOException {
        // 使用文件名称创建流对象
        FileOutputStream fos = new FileOutputStream("fos.txt", true);
        // 字符串转换为字节数组
        byte[] b = "abcde".getBytes();
        fos.write(b);
        // 关闭资源
        fos.close();
    }
    
    // 使用FileInputStream/FileOutputStream,实现对文件的复制
    @Test
    public void test05() {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            // 1. 造文件-造流
            // 复制图片:成功
            // fis = new FileInputStream(new File("pony.jpg"));
            // fos = new FileOutputStream(new File("pony_copy1.jpg"));
 
            // 复制文本文件:成功
            fis = new FileInputStream(new File("hello.txt"));
            fos = new FileOutputStream(new File("hello1.txt"));
 
            // 2. 复制操作(读、写)
            byte[] buffer = new byte[1024];
            int len; // 每次读入到 buffer 中字节的个数
            while ((len = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, len);
            }
            System.out.println("复制成功");
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            // 3. 关闭资源
            try {
                if (fos != null)
                    fos.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            try {
                if (fis != null)
                    fis.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
 
    }
}

处理流之一:缓冲流

为了提高数据读写的速度,Java API 提供了带缓冲功能的流类:缓冲流。

缓冲流要“套接”在相应的节点流之上,根据数据操作单位可以把缓冲流分为:

  • 字节缓冲流:BufferedInputStreamBufferedOutputStream
  • 字符缓冲流:BufferedReaderBufferedWriter

缓冲流的基本原理:在创建流对象时,内部会创建一个缓冲区数组(缺省使用 8192 个字节(8 KB))的缓冲区),通过缓冲区读写,减少系统 IO 次数,从而提高读写的效率。

构造器

  • public BufferedInputStream(InputStream in):创建一个新的字节型的缓冲输入流。
  • public BufferedOutputStream(OutputStream out):创建一个新的字节型的缓冲输出流。

代码举例:

// 创建字节缓冲输入流
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("abc.jpg"));
// 创建字节缓冲输出流
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("abc_copy.jpg"));
  • public BufferedReader(Reader in):创建一个新的字符型的缓冲输入流。
  • public BufferedWriter(Writer out):创建一个新的字符型的缓冲输出流。

代码举例:

// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("br.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("bw.txt"));

效率测试

查询 API,缓冲流读写方法与基本的流是一致的,我们通过复制大文件(375 MB),测试它的效率。

// 方法 1:使用 FileInputStream/FileOutputStream 实现非文本文件的复制
public void copyFileWithFileStream(String srcPath, String destPath) {
    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
        // 1. 造文件-造流
        fis = new FileInputStream(new File(srcPath));
        fos = new FileOutputStream(new File(destPath));
 
        // 2. 复制操作(读、写)
        byte[] buffer = new byte[100];
        int len; // 每次读入到 buffer 中字节的个数
        while ((len = fis.read(buffer)) != -1) {
            fos.write(buffer, 0, len);
        }
        System.out.println("复制成功");
    } catch (IOException e) {
        throw new RuntimeException(e);
    } finally {
        // 3. 关闭资源
        try {
            if (fos != null)
                fos.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        try {
            if (fis != null)
                fis.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}
 
@Test
public void test1() {
    String srcPath = "C:\\Users\\shkstart\\Desktop\\01-复习.mp4";
    String destPath = "C:\\Users\\shkstart\\Desktop\\01-复习2.mp4";
 
    long start = System.currentTimeMillis();
    copyFileWithFileStream(srcPath, destPath);
    long end = System.currentTimeMillis();
 
    System.out.println("花费的时间为:" + (end - start)); // 7677 毫秒
}
 
// 方法 2:使用 BufferedInputStream/BufferedOuputStream 实现非文本文件的复制
public void copyFileWithBufferedStream(String srcPath, String destPath) {
    FileInputStream fis = null;
    FileOutputStream fos = null;
    BufferedInputStream bis = null;
    BufferedOutputStream bos = null;
    try {
        // 1. 造文件
        File srcFile = new File(srcPath);
        File destFile = new File(destPath);
        // 2. 造流
        fis = new FileInputStream(srcFile);
        fos = new FileOutputStream(destFile);
 
        bis = new BufferedInputStream(fis);
        bos = new BufferedOutputStream(fos);
 
        // 3. 读写操作
        int len;
        byte[] buffer = new byte[100];
        while ((len = bis.read(buffer)) != -1) {
            bos.write(buffer, 0, len);
        }
        System.out.println("复制成功");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        // 4. 关闭资源(如果有多个流,我们需要先关闭外面的流,再关闭内部的流)
        try {
            if (bos != null)
                bos.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        try {
            if (bis != null)
                bis.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
 
    }
}
 
@Test
public void test2(){
    String srcPath = "C:\\Users\\shkstart\\Desktop\\01-复习.mp4";
    String destPath = "C:\\Users\\shkstart\\Desktop\\01-复习2.mp4";
 
    long start = System.currentTimeMillis();
    copyFileWithBufferedStream(srcPath, destPath);
    long end = System.currentTimeMillis();
 
    System.out.println("花费的时间为:" + (end - start)); // 415 毫秒
}

字符缓冲流特有方法

字符缓冲流的基本方法与普通字符流调用方式一致,不再阐述,我们来看它们具备的特有方法。

  • BufferedReaderpublic String readLine(): 读一行文字。
  • BufferedWriterpublic void newLine(): 写一行,行分隔符由系统属性定义符号。
public class BufferedIOLine {
    @Test
    public void testReadLine() throws IOException {
        // 创建流对象
        BufferedReader br = new BufferedReader(new FileReader("in.txt"));
        // 定义字符串,保存读取的一行文字
        String line;
        // 循环读取, 读取到最后返回 null
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
        // 释放资源
        br.close();
    }
 
    @Test
    public void testNewLine() throws IOException{
        // 创建流对象
        BufferedWriter bw = new BufferedWriter(new FileWriter("out.txt"));
        // 写出数据
        bw.write("尚");
        // 写出换行
        bw.newLine();
        bw.write("硅");
        bw.newLine();
        bw.write("谷");
        bw.newLine();
        // 释放资源
        bw.close();
    }
}

说明:

  1. 涉及到嵌套的多个流时,如果都显式关闭的话,需要先关闭外层的流,再关闭内层的流
  2. 其实在开发中,只需要关闭最外层的流即可,因为在关闭外层流时,内层的流也会被关闭。

处理流之二:转换流

问题引入

引入情况 1:

使用 FileReader 读取项目中的文本文件。由于 IDEA 设置中针对项目设置了 UTF-8 编码,当读取 Windows 系统中创建的文本文件时,如果 Windows 系统默认的是 GBK 编码,则读入内存中会出现乱码。

package com.atguigu.transfer;
 
import java.io.FileReader;
import java.io.IOException;
 
public class Problem {
    public static void main(String[] args) throws IOException {
        FileReader fileReader = new FileReader("E:\\File_GBK.txt");
        int data;
        while ((data = fileReader.read()) != -1) {
            System.out.print((char)data);
        }
        fileReader.close();
    }
}
 
// 输出结果:
// ���

那么如何读取 GBK 编码的文件呢?

引入情况 2:

针对文本文件,现在使用一个字节流进行数据的读入,希望将数据显示在控制台上。此时针对包含中文的文本数据,可能会出现乱码。

转换流的理解

作用:转换流是字节与字符间的桥梁!

具体来说:

InputStreamReaderOutputStreamWriter

InputStreamReader

转换流 java.io.InputStreamReader,是 Reader 的子类,是从字节流到字符流的桥梁。它读取字节,并使用指定的字符集将其解码为字符。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造器:

  • InputStreamReader(InputStream in): 创建一个使用默认字符集的字符流。
  • InputStreamReader(InputStream in, String charsetName): 创建一个指定字符集的字符流。

举例:

package com.atguigu.transfer;
 
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
 
public class InputStreamReaderDemo {
    public static void main(String[] args) throws IOException {
        // 定义文件路径,文件为 GBK 编码
        String fileName = "E:\\file_gbk.txt";
        // 方式 1:
        // 创建流对象,默认 UTF8 编码
        InputStreamReader isr1 = new InputStreamReader(new FileInputStream(fileName));
        // 定义变量,保存字符
        int charData;
        // 使用默认编码字符流读取,乱码
        while ((charData = isr1.read()) != -1) {
            System.out.print((char)charData); // ��Һ�
        }
        isr1.close();
		
        // 方式 2:
        // 创建流对象,指定 GBK 编码
        InputStreamReader isr2 = new InputStreamReader(new FileInputStream(fileName), "GBK");
        // 使用指定编码字符流读取,正常解析
        while ((charData = isr2.read()) != -1) {
            System.out.print((char)charData); // 大家好
        }
        isr2.close();
    }
}

OutputStreamWriter

转换流 java.io.OutputStreamWriter,是 Writer 的子类,是从字符流到字节流的桥梁。使用指定的字符集将字符编码为字节。它的字符集可以由名称指定,也可以接受平台的默认字符集。

构造器:

  • OutputStreamWriter(OutputStream in): 创建一个使用默认字符集的字符流。
  • OutputStreamWriter(OutputStream in, String charsetName): 创建一个指定字符集的字符流。

举例:

package com.atguigu.transfer;
 
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
 
public class OutputStreamWriterDemo {
    public static void main(String[] args) throws IOException {
        // 定义文件路径
        String FileName = "E:\\out_utf8.txt";
        // 创建流对象,默认 UTF8 编码
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(FileName));
        // 写出数据
        osw.write("你好"); // 保存为 6 个字节
        osw.close();
 
        // 定义文件路径
        String FileName2 = "E:\\out_gbk.txt";
        // 创建流对象,指定 GBK 编码
        OutputStreamWriter osw2 = new OutputStreamWriter(new FileOutputStream(FileName2), "GBK");
        // 写出数据
        osw2.write("你好"); // 保存为 4 个字节
        osw2.close();
    }
}

字符编码和字符集

编码与解码

计算机中储存的信息都是用二进制数表示的,而我们在屏幕上看到的数字、英文、标点符号、汉字等字符是二进制数转换之后的结果。按照某种规则,将字符存储到计算机中,称为编码。反之,将存储在计算机中的二进制数按照某种规则解析显示出来,称为解码。

  • 编码:字符(文本) 字节(二进制)
  • 解码:字节(二进制) 字符(文本)

字符编码(Character Encoding):就是一套自然语言的字符与二进制数之间的对应规则。

编码表:生活中文字和计算机中二进制的对应规则

乱码的情况:按照 A 规则存储,同样按照 A 规则解析,那么就能显示正确的文本符号。反之,按照 A 规则存储,再按照 B 规则解析,就会导致乱码现象。

字符集

关于字符集

处理流之三/四:数据流、对象流

如果需要将内存中定义的变量(包括基本数据类型或引用数据类型)保存在文件中,那怎么办呢?

Java 提供了数据流和对象流来处理这些类型的数据:

数据流 :DataOutputStreamDataInputStream

  • DataOutputStream:允许应用程序将基本数据类型、String 类型的变量写入输出流中
  • DataInputStream:允许应用程序以与机器无关的方式从底层输入流中读取基本数据类型、String 类型的变量。

数据流 DataInputStream 中的方法:

byte readByte()                short readShort()
int readInt()                  long readLong()
float readFloat()              double readDouble()
char readChar()				   boolean readBoolean()					
String readUTF()               void readFully(byte[] b)

数据流 DataOutputStream 中的方法:将上述的方法的 read 改为相应的 write 即可。

数据流的弊端:只支持 Java 基本数据类型和字符串的读写,而不支持其它 Java 对象的类型。而 ObjectOutputStreamObjectInputStream 既支持 Java 基本数据类型的数据读写,又支持 Java 对象的读写,所以重点介绍对象流 ObjectOutputStreamObjectInputStream

对象流:ObjectOutputStreamObjectInputStream

  • ObjectOutputStream:将 Java 基本数据类型和对象写入字节输出流中。通过在流中使用文件可以实现 Java 各种基本数据类型的数据以及对象的持久存储。
  • ObjectInputStreamObjectInputStream 对以前使用 ObjectOutputStream 写出的基本数据类型的数据和对象进行读入操作,保存在内存中。

对象流的强大之处就是可以把 Java 中的对象写入到数据源中,也能把对象从数据源中还原回来。

ObjectOutputStream

构造器:

  • public ObjectOutputStream(OutputStream out)

方法:

  • public void writeBoolean(boolean val):写出一个 boolean
  • public void writeByte(int val):写出一个 8 位字节
  • public void writeShort(int val):写出一个 16 位的 short
  • public void writeChar(int val):写出一个 16 位的 char
  • public void writeInt(int val):写出一个 32 位的 int
  • public void writeLong(long val):写出一个 64 位的 long
  • public void writeFloat(float val):写出一个 32 位的 float
  • public void writeDouble(double val):写出一个 64 位的 double
  • public void writeUTF(String s):将表示长度信息的两个字节写入输出流,后跟字符串 s 中每个字符的 UTF-8 修改版表示形式。根据字符的值,将字符串 s 中每个字符转换成一个字节、两个字节或三个字节的字节组。
    • 注意,将 String 作为基本数据写入流中与将它作为 Object 写入流中明显不同。如果 snull,则抛出 NullPointerException
  • public void writeObject(Object obj):写出一个对象
  • public void close():关闭此输出流并释放与此流相关联的任何系统资源

ObjectInputStream

构造器:

  • public ObjectInputStream(InputStream in)

方法:

  • public boolean readBoolean():读取一个 boolean
  • public byte readByte():读取一个 8 位的字节
  • public short readShort():读取一个 16 位的 short
  • public char readChar():读取一个 16 位的 char
  • public int readInt():读取一个 32 位的 int
  • public long readLong():读取一个 64 位的 long
  • public float readFloat():读取一个 32 位的 float
  • public double readDouble():读取一个 64 位的 double
  • public String readUTF():读取 UTF-8 修改版格式的 String
  • public void readObject(Object obj):读入一个对象
  • public void close():关闭此输入流并释放与此流相关联的任何系统资源

认识对象序列化机制

1、何为对象序列化机制?

对象序列化机制允许把内存中的 Java 对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象。

  • 序列化过程:用一个字节序列可以表示一个对象,该字节序列包含该对象的类型和对象中存储的属性等信息。字节序列写出到文件之后,相当于文件中持久保存了一个对象的信息。
  • 反序列化过程:该字节序列还可以从文件中读取回来,重构对象,对它进行反序列化。对象的数据、对象的类型和对象中存储的数据信息,都可以用来在内存中创建对象。

2、序列化机制的重要性

序列化是 RMI(Remote Method Invoke、远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是 JavaEE 平台的基础。

序列化的好处,在于可将任何实现了 Serializable 接口的对象转化为字节数据,使其在保存和传输时可被还原。

如何实现序列化机制

如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现 java.io.Serializable 接口。Serializable 是一个标记接口,不实现此接口的类将不会使任何状态序列化或反序列化,会抛出 NotSerializableException

  • 如果对象的某个属性也是引用数据类型,那么如果该属性也要序列化的话,也要实现 Serializable 接口
  • 该类的所有属性必须是可序列化的。如果有一个属性不需要可序列化的,则该属性必须注明是瞬态的,使用 transient 关键字修饰。
  • 静态(static)变量的值不会序列化。因为静态变量的值不属于某个对象。
  • 如果有多个对象需要序列化,可以将对象放到集合中,再序列化集合对象即可。

反序列化失败问题

问题 1:

对于 JVM 可以反序列化对象,它必须是能够找到 class 文件的类。如果找不到该类的 class 文件,则抛出一个 ClassNotFoundException 异常。

问题 2:

当 JVM 反序列化对象时,能找到 class 文件,但是 class 文件在序列化对象之后发生了修改,那么反序列化操作也会失败,抛出一个 InvalidClassException 异常。发生这个异常的原因如下:

  • 该类的序列版本号与从流中读取的类描述符的版本号不匹配
  • 该类包含未知数据类型

解决办法:

Serializable 接口给需要序列化的类,提供了一个序列版本号:serialVersionUID。凡是实现 Serializable 接口的类都应该有一个表示序列化版本标识符的静态变量:

static final long serialVersionUID = 234242343243L; // 它的值由程序员随意指定即可(保证不重复)
  • serialVersionUID 用来表明类的不同版本间的兼容性。简单来说,Java 的序列化机制是通过在运行时判断类的 serialVersionUID 来验证版本一致性的。在进行反序列化时,JVM 会把传来的字节流中的 serialVersionUID 与本地相应实体类的 serialVersionUID 进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常 InvalidCastException
  • 如果类没有显示定义这个静态常量,它的值是 Java 运行时环境根据类的内部细节自动生成的。若类的实例变量做了修改,serialVersionUID 可能发生变化。因此,建议显式声明。
  • 如果声明了 serialVersionUID,即使在序列化完成之后修改了类导致类重新编译,则原来的数据也能正常反序列化,只是新增的字段值是默认值而已。

题目

面试题:谈谈你对 java.io.Serializable 接口的理解,我们知道它用于序列化,是空方法接口,还有其它认识吗?

答:

实现了 Serializable 接口的对象,可将它们转换成一系列字节,并可在以后完全恢复回原来的样子。这一过程亦可通过网络进行。这意味着序列化机制能自动补偿操作系统间的差异。换句话说,可以先在 Windows 机器上创建一个对象,对其序列化,然后通过网络发给一台 Unix 机器,然后在那里准确无误地重新“装配”。不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节。

由于大部分作为参数的类如 StringInteger 等都实现了 java.io.Serializable 的接口,也可以利用多态的性质,作为参数使接口更灵活。

其他流的使用

标准输入、输出流

System.inSystem.out 分别代表了系统标准的输入和输出设备

  • System.in 的类型是 InputStream

  • System.out 的类型是 PrintStream,其是 OutputStream 的子类 FilterOutputStream 的子类

  • 默认输入设备是:键盘;输出设备是:显示器

  • 重定向:通过 System 类的 setInsetOut 方法对默认设备进行改变:

    • public static void setIn(InputStream in)
    • public static void setOut(PrintStream out)

拓展:

System 类中有三个常量对象:System.outSystem.inSystem.err

查看 System 类中这三个常量对象的声明:

public final static InputStream in = null;
public final static PrintStream out = null;
public final static PrintStream err = null;

奇怪的是:

  • 这三个常量对象有 final 声明,但是却初始化为 nullfinal 声明的常量一旦赋值就不能修改,那么 null 不会空指针异常吗?
  • 这三个常量对象为什么要小写?final 声明的常量按照命名规范不是应该大写吗?
  • 这三个常量的对象有 set 方法?final 声明的常量不是不能修改值吗?set 方法是如何修改它们的值的?

答:final 声明的常量,表示在 Java 的语法体系中它们的值是不能修改的,而这三个常量对象的值是由 C/C++ 等系统函数进行初始化和修改值的,所以它们故意没有用大写,也有 set 方法。

public static void setOut(PrintStream out) {
    checkIO();
    setOut0(out);
}
public static void setErr(PrintStream err) {
    checkIO();
    setErr0(err);
}
public static void setIn(InputStream in) {
    checkIO();
    setIn0(in);
}
private static void checkIO() {
    SecurityManager sm = getSecurityManager();
    if (sm != null) {
        sm.checkPermission(new RuntimePermission("setIO"));
    }
}
private static native void setIn0(InputStream in);
private static native void setOut0(PrintStream out);
private static native void setErr0(PrintStream err);

打印流

实现将基本数据类型的数据格式转化为字符串输出。

打印流:PrintStreamPrintWriter

提供了一系列重载的 print()println() 方法,用于多种数据类型的输出:

  • PrintStreamPrintWriter 的输出不会抛出 IOException 异常
  • PrintStreamPrintWriter 有自动 flush 功能
  • PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类。
  • System.out 返回的是 PrintStream 的实例

构造器:

  • PrintStream(File file):创建具有指定文件且不带自动行刷新的新打印流。
  • PrintStream(File file, String csn):创建具有指定文件名称和字符集且不带自动行刷新的新打印流。
  • PrintStream(OutputStream out):创建新的打印流。
  • PrintStream(OutputStream out, boolean autoFlush):创建新的打印流。
    • autoFlush 如果为 true,则每当写入 byte 数组、调用其中一个 println 方法或写入换行符或字节('\n')时都会刷新输出缓冲区。
  • PrintStream(OutputStream out, boolean autoFlush, String encoding)
  • PrintStream(String fileName):创建具有指定文件名称且不带自动行刷新的新打印流。
  • PrintStream(String fileName, String csn):创建具有指定文件名称和字符集且不带自动行刷新的新打印流。

代码举例 1:

package com.atguigu.systemio;
 
import java.io.FileNotFoundException;
import java.io.PrintStream;
 
public class TestPrintStream {
    public static void main(String[] args) throws FileNotFoundException {
        PrintStream ps = new PrintStream("io.txt");
        ps.println("hello");
        ps.println(1);
        ps.println(1.5);
        ps.close();
    }
}

代码举例 2:

PrintStream ps = null;
try {
    FileOutputStream fos = new FileOutputStream(new File("D:\\IO\\text.txt"));
    // 创建打印输出流,设置为自动刷新模式(写入换行符或字节 '\n' 时都会刷新输出缓冲区)
    ps = new PrintStream(fos, true);
    if (ps != null) { // 把标准输出流(控制台输出)改成文件
        System.setOut(ps);
    }
    for (int i = 0; i <= 255; i++) { // 输出 ASCII 字符
        System.out.print((char) i);
        if (i % 50 == 0) { // 每 50 个数据一行
            System.out.println(); // 换行
        }
    }
} catch (FileNotFoundException e) {
    e.printStackTrace();
} finally {
    if (ps != null) {
        ps.close();
    }
}

代码举例 3:自定义一个日志工具

/*
日志工具
 */
public class Logger {
    /*
    记录日志的方法
     */
    public static void log(String msg) {
        try {
            // 指向一个日志文件
            PrintStream out = new PrintStream(new FileOutputStream("log.txt", true));
            // 改变输出方向
            System.setOut(out);
            // 日期当前时间
            Date nowTime = new Date();
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
            String strTime = sdf.format(nowTime);
 
            System.out.println(strTime + ": " + msg);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }
}
public class LogTest {
    public static void main(String[] args) {
        // 测试工具类是否好用
        Logger.log("调用了 System 类的 gc() 方法,建议启动垃圾回收");
        Logger.log("调用了 TeamView 的 addMember() 方法");
        Logger.log("用户尝试进行登录,验证失败");
    }
}

Scanner

构造方法:

  • Scanner(File source):构造一个新的 Scanner,它生成的值是从指定文件扫描的。
  • Scanner(File source, String charsetName)
  • Scanner(InputStream source)
  • Scanner(InputStream source, String charsetName)

常用方法:

  • boolean hasNextXxx():如果通过使用 nextXxx() 方法,此扫描器输入信息中的下一个标记可以解释为默认基数中的一个 Xxx 类型值,则返回 true
  • Xxx nextXxx():将输入信息的下一个标记扫描为一个 Xxx 类型。

apache-common 包的使用

介绍

IO 技术开发中,代码量很大,而且代码的重复率较高,为此 Apache 软件基金会,开发了 IO 技术的工具类 commonsIO,大大简化了 IO 开发。

Apahce 软件基金会属于第三方,(Oracle 公司第一方,我们自己第二方,其他都是第三方)我们要使用第三方开发好的工具,需要添加 jar 包。

导包及举例

在导入 commons-io-2.5.jar 包之后,内部的 API 都可以使用。

IOUtils 类:(都是静态方法)

IOUtils.copy(InputStream in, OutputStream out)
// 传递字节流,实现文件复制
 
IOUtils.closeQuietly(任意流对象)
// 悄悄的释放资源,自动处理 close() 方法抛出的异常

FileUtils 类:(都是静态方法)

void copyDirectoryToDirectory(File src, File dest)
// 整个目录的复制,自动进行递归遍历
 
void copyFile(File srcFile, File destFile)
// 文件复制
 
void writeStringToFile(File file, String content)
// 将内容 content 写入到 file 中
 
String readFileToString(File file)
// 读取文件内容,并返回一个 String