【Java SE】十九、I/O 流

java.io 包几乎包含了所有操作输入、输出需要的类。所有这些流类代表了输入源和输出目标。一个流可以理解为一个数据的序列。输入流表示从一个源读取数据,输出流表示向一个目标写数据。

File 类

File 类的一个对象,代表一个文件或一个文件目录,其中涉及到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法

实例化

下面的示例展示了 File 的三种实例化方式:

import java.io.File;
public void test() {
    // 构造方式一
	File file1 = new File("D:\\fuck\\shit\\hello.txt"); // 完整绝对路径(也支持相对路径)
	System.out.println(file1); // 只是输出路径
    
    // 构造方式二
    File file2 = new File("D:\\fuck", "shit"); // 分开传入父目录及文件目录
    
    // 构造方式三
    File file3 = new File(file2, "hello.txt"); // 分开传入file对象及文件
}

注:创建实例时,不管文件或目录是否存在,仅在内存方面先创建一个对象,并不会报错

常用方法

File 类的方法主要分为五部分:获取、重命名、判断、创建、删除。

import java.io.File;
public void test() {
    File file1 = new File("hello.txt");
    File file2 = new File("D:\\fuck\\shit");
    File file3 = new File(file2, "hello.txt");
    File file4 = new File("shit.txt");
        
    // 获取方法
    System.out.println(file1.getAbsolutePath()); // 获取绝对路径
    System.out.println(file1.getPath()); // 获取路径(传给构造器的路径)
    System.out.println(file1.getName()); // 获取文件名
    System.out.println(file1.getParent()); // 获取父目录(根据传给构造器的路径查找)
    System.out.println(file1.length()); // 获取文件长度(字节数)不能获取文件目录长度
    System.out.println(file1.lastModified()); // 获取最后一次修改时间(时间戳)
    System.out.println(file2.list()); // 获取指定目录下的所有文件或者文件目录的名称数组(字符串数组)
    System.out.println(file2.listFiles()); // 获取指定目录下的所有文件或者文件目录的File类型数组
        
    // 重命名方法(要想保证返回true,需要file1是存在的,且file3是不存在的)
    System.out.println(file1.renameTo(file3)); // 把文件重命名为指定的文件路径(可修改的不仅是名称,还有路径)
        
    // 判断方法(若文件不存在,以下皆为false)
    System.out.println(file1.isDirectory()); // 判断是否是文件目录
    System.out.println(file1.isFile()); // 判断是否是文件
    System.out.println(file1.exists()); // 判断是否存在
    System.out.println(file1.canRead()); // 判断是否可读
    System.out.println(file1.canWrite()); // 判断是否可写
    System.out.println(file1.isHidden()); // 判断是否隐藏
        
    // 创建和删除方法
    file4.createNewFile(); // 创建该文件(若文件存在,则返回false)
    file4.delete(); // 彻底删除该文件(若文件不存在,则返回false)
    file2.mkdir(); // 创建文件目录(若文件目录存在或文件目录的上级目录不存在,则返回false)
    file2.mkdirs(); // 递归创建文件目录(若文件目录存在,则返回false;若文件目录的上级目录不存在,则一并创建)
}

Java 提供的 I/O 流光类就有 40 多个,但它们都继承于四个抽象基类:InputStreamOutputStreamReaderWriter ,按照不同的方向可以把他们分类成不同类别:

  • 操作数据单位:字节流、字符流
  • 数据的流向:输入流、输出流
  • 流的角色:节点流、处理流

I/O 流常用体系如下:

分类 字节输入流 字节输出流 字符输入流 字符输出流
抽象基类 InputStream OutputStream Reader Writer
访问文件 FileInputStream FileOutputStream FileReader FileWriter
访问数组 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter
访问管道 PipedlnputStream PipedOutputStream PipedReader PipedWriter
访问字符串 * * StringReader StringWiter
缓冲流 BufferedInputStream BufferedOuputStream BufferedReader BufferedWriter
转换流 * * InputStreamReader OutputStreamWriter
对象流 ObjectInputStream ObjectOutputStream * *
过滤流 FilterlnputStream FilterOutputStream FiterReader FilterWriter
打印流 * PrintStream * PrintWriter
推回流 PushbackInputStream * PushbackReader *
数据流 DatalnputStream DataOutputStream * *

字符流

根据操作数据单位可分为字节流和字符流。因为流的操作都是相似的,所以这里以最基本的文件流为例。

FileReader:顾名思义是输入流,用来读取字符数据,常从文本文件中读取数据,实例如下:

public void test() {
    FileReader fr = null;
    try { // 文件操作会抛出IOException异常,要注意捕获
        File file = new File("hello.txt"); // 实例化File类的对象,指明要操作的文件
        fr = new FileReader(file); // 提供具体的流,若文件不存在,则报错
        
        // 方式一:read() - 返回读入的一一个字符。如果达到文件末尾,返回-1 
        int data; 
        while((data = fr.read()) > 0) // 数据的读入
            System.out.println((char)data);
        
        // 方式二:read(char[] cbuf) - 返回每次读入cbuf数组中的字符的个数。如果达到文件末尾,返回-1
        int len;
        char[] cbuf = new char[5];
        while((len = fr.read(cbuf)) > 0) {
            // 常规写法
            for (int i = 0; i < len; i++)
            	System.out.print(cbuf[i]);
            // 简易写法
            String str = new String(cbuf, 0, 1en);
			System.out.print(str);
        }
        
        // 方式三:read(char cbuf[], int off, int len) - 返回每次读入cbuf数组中限定位置的字符的个数。
    	// 也是达到文件末尾,返回-1;不过这个不常用,不展示
    } catch (IOException e) {
        // 异常处理
    } finally { // 确保流能正常关闭
        try {
            if (fr != null) fr.close(); // 流的关闭操作,它也会抛出IOException异常
        } catch (IOException e) {
        // 异常处理
        }
    }
}

FileWriter:这个是用来向文本文件写入字符数据,示例如下:

public vo1d test() {
    File file = new File("hello1.txt"); // 若文件不存在,则直接新建
    FileWriter writer = null;
    try {
        writer = new FileWriter(file, false); // 后一个参数代表是否添加数据,不然直接覆盖,默认为false
        writer.write("你好");
    } catch (IOException e) {
        System.out.println("一段捕获: " + e);
    } finally { // 依旧做好异常处理
        if (writer != null) {
            try {
                writer.close();
            } catch (IOException e1) {
                System.out.println("二段捕获: " + e1);
            }
        }
    }
}

注:字符流不能用来读取二进制文件,即使这样做并不会报错!

字节流

当需要操纵二进制数据时,常用 FileInputStream & FileOutputStream 来读取图片,视频等文件,实例如下:

public vo1d test() {
    File file1 = new File("he1lo.jpg"), file2 = new File("world.jpg");
    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
        fis = new FileInputStream(file1);
        fos = new FileOutputStream(file2);
        
     /* 方式一,不建议用来读取中文文本,但很常用
        byte[] buffer = new byte[5];
        while((int len = fis.read(buffer)) != -1) {
            fos.write(buffer, 0, len);
        } */
        
     // 方式二,更安全,可以读中文
        ByteArrayOutputStream baos = new ByteArrayOutputStream(); // 创建字节数组输出流
        byte[] buffer = new byte[5];
        while((int len = fis.read(buffer)) != -1){
        	baos.write(buffer, 0, len);
        }
    } catch (IOException e) {
        System.out.println("一段捕获: " + e);
    } finally { // 依旧做好异常处理
        try {
            if (fis != null) {
                fis.close();
            }
            if (fos != null) {
                fos.close();
            }
        } catch (IOException e1) {
            System.out.println("二段捕获: " + e1);
        }
    }
}

注:字节流其实也可以读取文本数据,但方式一读取中文时可能会出现乱码,,所以不建议这样用

缓冲流

缓冲流是处理流的一种,是对原有节点流进行包装后的流,用法相同,但比上面说的节点流效率高,开发常用,用法如下:

// 缓冲流的使用简单的要死,只需要这样
BufferedReader br = new BufferedReader(new FileReader(file));
BufferedWriter bw = new BufferedWriter(new FileWriter(file));
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file));

缓冲流除了原有的节点流的方法,还有一些新增的方法,这里以 readLine() , newLine() 为例:

String data = br,readLine(); // 一行行读取文本(不包含换行符),该行为空则返回null
bw.write(data);
bw.newLine(); // 换行

转换流

转换流是处理流的一种,提供字节流与字符流之间的转换,用法如下:

// 将一个字符的输出流转换为字节的输出泪
FileInputStream fis = new FileInputStream("dbcp.txt");
FileOutputStream fos = new FileOutputStream("dbcp1.txt");
// 将字节输入流转为字符输入流
InputStreamReader isr = new InputStreamReader(fis, "UTF-8"); // 参数二为所使用字符集,默认为UTF-8
OutputStreamWriter osw = new OutputStreamWriter(fos);
//读写过程
char[] cbuf = new char[20];
while((int len = isr.read(cbuf)) != -1){
	osw.write(cbuf, 0, 1en);
}
isr.close();
osw.close();

打印流

说打印流前,我们先得说明一下标准输入,输出流,它是 System 类里的两个类型为字节流的属性。

  • System.in:标准的输入流,默认从键盘输入
  • System.out:标准的输出流,默认从控制台输出
  • System 类里的 setIn(InputStream is) & setOut(PrintStream ps) 方法可重新指定输入和输出的设备

打印流则是用来实现将基本数据类型的数据格式转化为字符串输出。,实例如下:

// 该实例实现了将数据打印到指定文件里
PrintStream ps = null;
try {
    FileOutputStream fos = new FileOutputStream(new File("D:\\IO\\text.txt"));
    // 创建打印输出流,设置为自动刷新模式(写入换行符或字节'\n'时都会刷新输出缓冲区)
    ps = new PrintStream(fos, true) 
    if (ps != nu1l) {
    	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();
    }
}

数据流

为了方便地操作 Java 的基本数据类型和字符串, 我们可以使用数据流,实例如下:

// 该实例实现了将内存中的字符串、基本数据类型的变量写出到文件中。
DataOutputStream dos = new DataOutputStream(new FileOutputStream(new File("D:\\data"));
dos.writeUTF("刘建辰"); // 写入字符串
dos.flush(); // 刷新操作,将内存中的数据写入文件
dos.writeInt(23); // 写入数字
dos.flush();
dos.writeBoolean(true); // 写入布尔值
dos.flush();
dos.close();

值得注意的是,数据流写入的数据文件要用其输入流去按顺序读取,而不应该直接打开查看,如下:

DataInputStream dis = new DataInputStream(new FileInputStream(new File("D:\\data"));
System.out.print(dis.readUTF() + dis.readInt() + dis.readBoolean()); // 打印结果
dis.close();

注:如果你觉得数据存在内存中不靠谱,可以用数据流存在文件里,自带加密

对象流

用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把对象写入到数据源中,也能把对象从数据源中还原回来。

而这中转换靠的是序列化机制:

  • 对象序列化机制允许把内存中的 Java 对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象。
  • 序列化的好处在于可将任何实现了 Serializable 接口的对象转化为字节数据,使其在保存和传输时可被还原。
  • 如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下 Serializable 或 Externalizable 两个接口之一。否则,会抛出 NotSerializableException 异常。
// 序列化,即写入(这里忽略了异常处理)
ObjectOutputStream ooS = new objectOutputStream(new FileOutputStream(new File("string.data"));
oos.writeobiect(new String("我爱北京天安门"));
oos.flush();//剧新操作
oos.close();
                                                
// 反序列化,即读取(同样忽略异常处理)
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("string.data"));
String str = (String) ois.readobject(); // 读取类,如果有多个类,则要按写入顺序读取
ois.close();

我们还可以自定义序列化的类,如下:

// Serializable是一个标识接口,所以只需要实现而不用重写任何方法
public class Person implements Serializable {
    // 还需提供一个全局常量,作为反序列化时的依据
    private static final long serialVersionUID = 151462268431L; // 值随便写,但不能不写!
    ......
}

注意事项:

  • 自定义类要实现标识接口:Serializable
  • 自定义类要提供一个全局常量:serialVersionUID
  • 除自定义类是可序列化的外, 还必须保证其内部所有属性也是可序列化的(默认情况下, 基本数据类型是可序列化的)
  • static 和 transient 修饰的值不能在序列化中被保存下来,正常读取后为默认值

随机存取文件流

RandomAccessFile 声明在 java.io 包下,但直接继承于 java.lang.Object 类。并且它实现了 Datalnput、DataOutput 这两个接口,也就意味着这个类既可以也可以

  • RandomAccessFile类支持随机访问的方式,程序可以直接跳到文件的任意地方来读、写文件
    • 支持只访问文件的部分内容
    • 可以向已存在的文件后追加内容
    • RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置
  • RandomAccessFile类对象可以自由移动记录指针
    • long getFilePointer():获取文件记录指针的当前位置
    • void seek(long pos):将文件记录指针定位到 pos 位置
File file = new File("爱情与友情2.txt");
RandomAccessFile raf1 = new RandomAccessFile("爱情与友情.jpg", "r");
RandomAccessFile raf2 = new RandomAccessFile("爱情与友情1.jpg", "rw");
RandomAccessFile raf3 = new RandomAccessFile(file, "rw");

byte[] buffer = new byte[1024]; // 正常的文件复制用法
while((int len = raf1.read(buffer)) != -1){
	raf2.write(buffer, 0,len);
}
System.out.print(raf1.getFilePointer()); // 获取文件指针的当前位置,此时应该在末尾

raf3.seek(file.length); // 设定文件指针位置,这里是移动到末尾,默认为0
raf3.write("xyz".getBytes()); // 只能以字节形式写入

raf1.close();
raf2.close();
raf3.close();

NIO

Java NIO (New IO,Non-Blocking I0) 是从 Java 1.4 版本开始引入的一套新的IO API,可以替代标准的Java IO API。NIO 与原来的 IO 有同样的作用和目的,但是使用的方式完全不同,NIO 支持面向缓冲区的( IO 是面向流的)、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。Java API 中提供了两套 NIO, 一套是针对标准输入输出 NIO, 另一套就是网络编程 NIO。

java.nio.channels.Channel

  • FileChannel:处理本地文件
  • SocketChannel:TCP网絡編程的客户端的 Channel
  • ServerSocketChannel:TCP网络编程的服务器端的 Channel
  • DatagramChannel:UDP网络编程中发送端和接收端的 Channel

我们现在常用的是 JDK 7 发布的 NIO.2 ,相比上代进行极大的拓展,已经成为文件处理中越来越重要部分。

但因为白嫖的网课内容里没讲,这里就只做了解一下里面的 Path ,Paths 和一些方法,便于之后查询

NIO.2 中使用 Path 代替了 原有的 File 类,而创建一个 Path 对象,则需要用 Paths

Paths 类提供的静态 get() 方法用来获取 Path 对象:

static Path get(String first, String ...more)) // 用于将多个字符串串连成路径
static Path get(URI uni) // 返回指定uri对应的Path路径

Path 有很多常用方法,如下表:

方法 说明
String toString() 返回调用 Path 对象的字符串表示形式
boolean startsWith(String path) 判断是否以 Path 路径开始
boolean endsWith(String path) 判断是否以 Path 路径结束
boolean isAbsolute() 判断是否是绝对路径
Path getParent() 返回 Path 对象包含整个路径,不包含 Path 对象指定的文件路径
Path getRoot() 返回调用 Path 对象的根路径
Path getFileName() 返回与调用 Path 对象关联的文件名
int getNameCount() 返回 Path 根目录后面元素的数量
Path getName(int idx) 返回指定索引位置 idx 的路径名称
Path toAbsolutePath() 作为绝对路径返回调用 Path 对象
Path resolve(Path p) 合并两个路径,返回合并后的路径对应的 Path 对象
File toFile() 将 Path 转化为 File 类的对象

NIO.2 还提供了 Files 类用于操作文件或目录的工具类,常用方法如下表:

方法 说明
Path copy(Path src, Path dest, CopyOption ...how) 文件的复制
Path createDirectory(Path path, FileAttribute<?> ...attr) 创建一个目录
Path createFile(Path path, FileAttribute<?> ...arr) 创建一个文件
void delete(Path path) 删除一个文件/目录,若不存在,执行报错
void deletelfExists(Path path) Path 对应的文件/目录,若存在,执行删除
Path move(Path src, Path dest, CopyOption ...how) 将 src 移动到 dest 位置
long size(Path path) 返回 Path 指定文件的大小
boolean exists(Path path, LinkOption ...opts) 判断文件是否存在
boolean isDirectory(Path path, LinkOption ...opts) 判断是否是目录
boolean isRegularFile(Path path, LinkOption ...opts) 判断是否是文件
boolean isHidden(Path path) 判断是否是隐藏文件
boolean isReadable(Path path) 判断文件是否可读
boolean isWritable(Path path) 判断文件是否可写
boolean notExists(Path path, LinkOption ...opts) 判断文件是否不存在
SeekableByteChannel newByteChannel(Path path, OpenOption ...how) 获取与指定文件的连接,how 指打开方式
DirectoryStream<Path> newDirectoryStream(Path path) 打开 Path 指定的目录
InputStream newInputStream(Path path, OpenOption ...how) 获取 InputStream 对象
OutputStream newOutputStream(Path path, OpenOption ...how) 获取 OutputStream 对象