目录
IO?指应用程序和外部设备之间的数据传递,常见的外部设备包括文件、管道、网络连接
Java 中是通过流处理 IO 的,那么什么是流呢?
流(Stream),是一个抽象的概念,指一连串的数据,以先进先出的方式发送信息的通道
关于流的特性有下面几点:
传输方式有两种,字节和字符
具体还要看字符编码,比如说在 UTF-8 编码下,一个英文字母(不分大小写)为一个字节,一个中文汉字为三个字节;在 Unicode 编码中,一个英文字母为一个字节,一个中文汉字为两个字节
字节流和字符流的区别:
- 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本文件
- 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大
文件流也就是直接操作文件的流,可以细分为字节流(FileInputStream 和 FileOuputStream)和字符流(FileReader 和 FileWriter)
FileInputStream 的例子:
// 声明一个 int 类型的变量 b,用于存储读取到的字节
int b;
// 创建一个 FileInputStream 对象,用于读取文件 fis.txt 中的数据
FileInputStream fis1 = new FileInputStream("fis.txt");
// 循环读取文件中的数据
while ((b = fis1.read()) != -1) {
// 将读取到的字节转换为对应的 ASCII 字符,并输出到控制台
System.out.println((char)b);
}
// 关闭 FileInputStream 对象,释放资源
fis1.close();
FileOutputStream 的例子:
// 创建一个 FileOutputStream 对象,用于写入数据到文件 fos.txt 中
FileOutputStream fos = new FileOutputStream("fos.txt");
// 向文件中写入数据,这里写入的是字符串 "沉默王二" 对应的字节数组
fos.write("沉默王二".getBytes());
// 关闭 FileOutputStream 对象,释放资源
fos.close();
FileReader 的例子:
// 声明一个 int 类型的变量 b,用于存储读取到的字符
int b = 0;
// 创建一个 FileReader 对象,用于读取文件 read.txt 中的数据
FileReader fileReader = new FileReader("read.txt");
// 循环读取文件中的数据
while ((b = fileReader.read()) != -1) {
// 将读取到的字符强制转换为 char 类型,并输出到控制台
System.out.println((char)b);
}
// 关闭 FileReader 对象,释放资源
fileReader.close();
FileWriter 的例子:
// 创建一个 FileWriter 对象,用于写入数据到文件 fw.txt 中
FileWriter fileWriter = new FileWriter("fw.txt");
// 将字符串 "沉默王二" 转换为字符数组
char[] chars = "沉默王二".toCharArray();
// 向文件中写入数据,这里写入的是 chars 数组中的所有字符
fileWriter.write(chars, 0, chars.length);
// 关闭 FileWriter 对象,释放资源
fileWriter.close();
文件流还可以用于创建、删除、重命名文件等操作。FileOutputStream 和 FileWriter 构造函数的第二个参数可以指定是否追加数据到文件末尾
// 创建文件
File file = new File("test.txt");
if (file.createNewFile()) {
System.out.println("文件创建成功");
} else {
System.out.println("文件已存在");
}
// 删除文件
if (file.delete()) {
System.out.println("文件删除成功");
} else {
System.out.println("文件删除失败");
}
// 重命名文件
File oldFile = new File("old.txt");
File newFile = new File("new.txt");
if (oldFile.renameTo(newFile)) {
System.out.println("文件重命名成功");
} else {
System.out.println("文件重命名失败");
}
针对文件的读写操作,使用文件流配合缓冲流就够用了,但为了提升效率,频繁地读写文件并不是太好,那么就出现了数组流,有时候也称为内存流
ByteArrayInputStream 的例子:
// 创建一个 ByteArrayInputStream 对象,用于从字节数组中读取数据
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 的例子:
// 创建一个 ByteArrayOutputStream 对象,用于写入数据到内存缓冲区中
ByteArrayOutputStream bos = new ByteArrayOutputStream();
// 定义一个字节数组用于存储要写入内存缓冲区中的数据
byte[] info = "输入输出".getBytes();
// 向内存缓冲区中写入数据,这里写入的是 info 数组中的所有字节
bos.write(info, 0, info.length);
// 将内存缓冲区中的数据转换为字节数组
byte[] dest = bos.toByteArray();
// 关闭 ByteArrayOutputStream 对象,释放资源
bos.close();
数组流可以用于在内存中读写数据,比如将数据存储在字节数组中进行压缩、加密、序列化等
优点:不需要创建临时文件,可以提高程序的效率
缺点:只能存储有限的数据量,如果存储的数据量过大,会导致内存溢出
在 Unix/Linux 中,不同的进程之间可以通过管道来通信。java 中则是通信的双方必须在同一个进程中,管道为线程之间的通信提供了通信能力
一个线程通过 PipedOutputStream 写入的数据可以被另外一个线程通过相关联的 PipedInputStream 读取出
// 创建一个 PipedOutputStream 对象和一个 PipedInputStream 对象
final PipedOutputStream pipedOutputStream = new PipedOutputStream();
final PipedInputStream pipedInputStream = new PipedInputStream(pipedOutputStream);
// 创建一个线程,向 PipedOutputStream 中写入数据
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 将字符串 "输入输出" 转换为字节数组,并写入到 PipedOutputStream 中
pipedOutputStream.write("输入输出".getBytes(StandardCharsets.UTF_8));
// 关闭 PipedOutputStream,释放资源
pipedOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
// 创建一个线程,从 PipedInputStream 中读取数据并输出到控制台
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,释放资源
pipedInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
// 启动线程1和线程2
thread1.start();
thread2.start();
使用管道流可以实现不同线程之间的数据传输,可以用于线程间的通信、数据的传递等
管道的局限性:只能在同一个 JVM 中的线程之间使用,不能跨越不同的 JVM 进程
基本数据类型输入输出流是一个字节流,该流不仅可以读写字节和字符,还可以读写基本数据类型
DataInputStream 例子:
// 创建一个 DataInputStream 对象,用于从文件中读取数据
DataInputStream dis = new DataInputStream(new FileInputStream("das.txt"));
// 读取一个字节,将其转换为 byte 类型
byte b = dis.readByte();
// 读取两个字节,将其转换为 short 类型
short s = dis.readShort();
// 读取四个字节,将其转换为 int 类型
int i = dis.readInt();
// 读取八个字节,将其转换为 long 类型
long l = dis.readLong();
// 读取四个字节,将其转换为 float 类型
float f = dis.readFloat();
// 读取八个字节,将其转换为 double 类型
double d = dis.readDouble();
// 读取一个字节,将其转换为 boolean 类型
boolean bb = dis.readBoolean();
// 读取两个字节,将其转换为 char 类型
char ch = dis.readChar();
// 关闭 DataInputStream,释放资源
dis.close();
DataOutputStream 例子:
// 创建一个 DataOutputStream 对象,用于将数据写入到文件中
DataOutputStream das = new DataOutputStream(new FileOutputStream("das.txt"));
// 将一个 byte 类型的数据写入到文件中
das.writeByte(10);
// 将一个 short 类型的数据写入到文件中
das.writeShort(100);
// 将一个 int 类型的数据写入到文件中
das.writeInt(1000);
// 将一个 long 类型的数据写入到文件中
das.writeLong(10000L);
// 将一个 float 类型的数据写入到文件中
das.writeFloat(12.34F);
// 将一个 double 类型的数据写入到文件中
das.writeDouble(12.56);
// 将一个 boolean 类型的数据写入到文件中
das.writeBoolean(true);
// 将一个 char 类型的数据写入到文件中
das.writeChar('A');
// 关闭 DataOutputStream,释放资源
das.close();
ObjectInputStream 和 ObjectOutputStream(用于读写对象)
创建了一个 Person 对象,将其写入文件中,然后从文件中读取该对象,并打印在控制台上
public static void main(String[] args) {
try (ObjectOutputStream oos = new ObjectOutputStream(new
FileOutputStream("person.dat"))) {
Person p = new Person("张三", 20);
oos.writeObject(p);
} catch (IOException e) {
e.printStackTrace();
}
try (ObjectInputStream ois = new
ObjectInputStream(new FileInputStream("person.dat"))) {
Person p = (Person) ois.readObject();
System.out.println(p);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
为了减少程序和硬盘的交互,提升程序的效率,就引入了缓冲流,也就是类名前缀带有 Buffer 的
例如:BufferedInputStream、BufferedOutputStream、BufferedReader、BufferedWriter
BufferedInputStream 读取文件的示例:
// 创建一个 BufferedInputStream 对象,用于从文件中读取数据
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("data.txt"));
// 创建一个字节数组,作为缓存区
byte[] buffer = new byte[1024];
// 读取文件中的数据,并将其存储到缓存区中
int bytesRead;
while ((bytesRead = bis.read(buffer)) != -1) {
// 对缓存区中的数据进行处理
// 这里只是简单地将读取到的字节数组转换为字符串并打印出来
System.out.println(new String(buffer, 0, bytesRead));
}
// 关闭 BufferedInputStream,释放资源
bis.close();
使用 BufferedOutputStream 写入文件的示例:
注:写入数据时,由于使用了 BufferedOutputStream,数据会先被写入到缓存区中,只有在缓存区被填满或者调用了 flush() 方法时才会将缓存区中的数据写入到文件中
// 创建一个 BufferedOutputStream 对象,用于将数据写入到文件中
BufferedOutputStream bos =
new BufferedOutputStream(new FileOutputStream("data.txt"));
// 创建一个字节数组,作为缓存区
byte[] buffer = new byte[1024];
// 将数据写入到文件中
String data = "沉默王二是个大傻子!";
buffer = data.getBytes();
bos.write(buffer);
// 刷新缓存区,将缓存区中的数据写入到文件中
bos.flush();
// 关闭 BufferedOutputStream,释放资源
bos.close();
使用 BufferedReader 读取文件的示例:
// 创建一个 BufferedReader 对象,用于从文件中读取数据
BufferedReader br = new BufferedReader(new FileReader("data.txt"));
// 读取文件中的数据,并将其存储到字符串中
String line;
while ((line = br.readLine()) != null) {
// 对读取到的数据进行处理
// 这里只是简单地将读取到的每一行字符串打印出来
System.out.println(line);
}
// 关闭 BufferedReader,释放资源
br.close();
使用 BufferedWriter 写入文件的示例:
// 创建一个 BufferedWriter 对象,用于将数据写入到文件中
BufferedWriter bw = new BufferedWriter(new FileWriter("data.txt"));
// 将数据写入到文件中
String data = "输入输出";
bw.write(data);
// 刷新缓存区,将缓存区中的数据写入到文件中
bw.flush();
// 关闭 BufferedWriter,释放资源
bw.close();
缓冲
优点:可以提高读写效率,减少了频繁的读写磁盘或网络的次数,从而提高了程序的性能
缺点:需要注意缓冲区的大小和清空缓冲区的时机,以避免数据丢失或不完整的问题
打印输出数据的类 包括 PrintStream 和 PrintWriter 两个类
PrintStream 最终输出的是字节数据,而 PrintWriter 则是扩展了 Writer 接口,所以它的?print()/println()
?方法最终输出的是字符数据。使用上几乎和 PrintStream 一模一样
StringWriter buffer = new StringWriter();
try (PrintWriter pw = new PrintWriter(buffer)) {
pw.println("输入输出");
}
System.out.println(buffer.toString());
序列化本质上是将一个 Java 对象转成字节数组,然后可以将其保存到文件中,或者通过网络传输到远程
// 创建一个 ByteArrayOutputStream 对象 buffer,用于存储数据
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
// 使用 try-with-resources 语句创建一个 ObjectOutputStream 对象 output
// 并将其与 buffer 关联
try (ObjectOutputStream output = new ObjectOutputStream(buffer)) {
// 使用 writeUTF() 方法将字符串 "输入输出" 写入到缓冲区中
output.writeUTF("输入输出");
}
// 使用 toByteArray() 方法将缓冲区中的数据转换成字节数组,并输出到控制台
System.out.println(Arrays.toString(buffer.toByteArray()));
反序列化,也就是再将字节数组转成 Java 对象的过程
try (ObjectInputStream input = new ObjectInputStream(new FileInputStream(
new File("Person.txt")))) {
String s = input.readUTF();
}
InputStreamReader 是从字节流到字符流的桥连接,它使用指定的字符集读取字节并将它们解码为字符
// 创建一个 InputStreamReader 对象 isr
// 使用 FileInputStream 对象读取文件 demo.txt 的内容并将其转换为字符流
InputStreamReader isr = new InputStreamReader(new FileInputStream("demo.txt"));
// 创建一个字符数组 cha,用于存储读取的字符数据,其中 1024 表示数组的长度
char[] cha = new char[1024];
// 使用 read() 方法读取 isr 中的数据,并将读取的字符数据存储到 cha 数组中
// 返回值 len 表示读取的字符数
int len = isr.read(cha);
// 将 cha 数组中从下标 0 开始、长度为 len 的部分转换成字符串,并输出到控制台
System.out.println(new String(cha, 0, len));
// 关闭 InputStreamReader 对象 isr,释放资源
isr.close();
OutputStreamWriter 将字符流的输出对象变为字节流的输出对象,是字符流通向字节流的桥梁
// 创建一个 File 对象 f,表示文件 test.txt
File f = new File("test.txt");
// 创建一个 OutputStreamWriter 对象 out,
// 使用 FileOutputStream 对象将数据写入到文件 f 中,并将字节流转换成字符流
Writer out = new OutputStreamWriter(new FileOutputStream(f));
// 使用 write() 方法将字符串 "输入输出!!" 写入到文件 f 中
out.write("输入输出!!");
// 关闭 Writer 对象 out,释放资源
out.close();
在进行文本文件读写时,通常使用字符流进行操作,而在进行网络传输或与设备进行通信时,通常使用字节流进行操作
?注:在使用转换流时需要注意字符编码的问题。如果不指定字符编码,则使用默认的字符编码,可能会出现乱码问题
File 跟流无关,File 类不能对文件进行读和写
// 文件路径名
String path = "/Users/username/123.txt";
File file1 = new File(path); -------相当于/Users/username/123.txt
// 文件路径名
String path2 = "/Users/username/1/2.txt";
File file2 = new File(path2); -------相当于/Users/username/1/2.txt
// 通过父路径和子路径字符串
String parent = "/Users/username/aaa";
String child = "bbb.txt";
File file3 = new File(parent, child); ------相当于/Users/username/aaa/bbb.txt
// 通过父级File对象和子路径字符串
File parentDir = new File("/Users/username/aaa");
String child = "bbb.txt";
File file4 = new File(parentDir, child); ----相当于/Users/username/aaa/bbb.txt
注:
- macOS 与 Linux 路径使用正斜杠 / 作为路径分隔符,而 Windows 路径使用反斜杠作 \ 为路径分隔符,使用 File.separator ,这个属性会根据操作系统自动返回正确的路径分隔符
- File 类的构造方法不会检验这个文件或目录是否真实存在,因此无论该路径下是否存在文件或者目录,都不影响 File 对象的创建
File f = new File("/Users/username/aaa/bbb.java");
System.out.println("文件绝对路径:"+f.getAbsolutePath());
System.out.println("文件构造路径:"+f.getPath());
System.out.println("文件名称:"+f.getName());
System.out.println("文件长度:"+f.length()+"字节");
File f2 = new File("/Users/username/aaa");
System.out.println("目录绝对路径:"+f2.getAbsolutePath());
System.out.println("目录构造路径:"+f2.getPath());
System.out.println("目录名称:"+f2.getName());
System.out.println("目录长度:"+f2.length());
注:length() 表示文件的长度,File 对象表示目录的时候毫无意义
绝对路径是从文件系统的根目录开始的完整路径
相对路径是相对于当前工作目录的路径
注:Windows 文件系统默认是不区分大小写,Unix 系统中默认是区分大小写的
File file = new File("/Users/username/example");
// 判断文件或目录是否存在
if (file.exists()) {
System.out.println("文件或目录存在");
} else {
System.out.println("文件或目录不存在");
}
// 判断是否是目录
if (file.isDirectory()) {
System.out.println("是目录");
} else {
System.out.println("不是目录");
}
// 判断是否是文件
if (file.isFile()) {
System.out.println("是文件");
} else {
System.out.println("不是文件");
}
// 创建文件
File file = new File("/Users/username/example/test.txt");
if (file.createNewFile()) {
System.out.println("创建文件成功:" + file.getAbsolutePath());
} else {
System.out.println("创建文件失败:" + file.getAbsolutePath());
}
// 删除文件
if (file.delete()) {
System.out.println("删除文件成功:" + file.getAbsolutePath());
} else {
System.out.println("删除文件失败:" + file.getAbsolutePath());
}
// 创建多级目录
File directory = new File("/Users/username/example/subdir1/subdir2");
if (directory.mkdirs()) {
System.out.println("创建目录成功:" + directory.getAbsolutePath());
} else {
System.out.println("创建目录失败:" + directory.getAbsolutePath());
}
File directory = new File("/Users/Documents/Github");
// 列出目录下的文件名
String[] files = directory.list();
System.out.println("目录下的文件名:");
for (String file : files) {
System.out.println(file);
}
// 列出目录下的文件和子目录
File[] filesAndDirs = directory.listFiles();
System.out.println("目录下的文件和子目录:");
for (File fileOrDir : filesAndDirs) {
if (fileOrDir.isFile()) {
System.out.println("文件:" + fileOrDir.getName());
} else if (fileOrDir.isDirectory()) {
System.out.println("目录:" + fileOrDir.getName());
}
}
注:listFiles?必须满足下面两个条件
- 指定的目录必须存在
- 指定必须是目录,否则引发 NullPointException 异常
public static void main(String[] args) {
File directory = new File("/Users/Documents/Github");
// 递归遍历目录下的文件和子目录
traverseDirectory(directory);
}
public static void traverseDirectory(File directory) {
// 列出目录下的所有文件和子目录
File[] filesAndDirs = directory.listFiles();
// 遍历每个文件和子目录
for (File fileOrDir : filesAndDirs) {
if (fileOrDir.isFile()) {
// 如果是文件,输出文件名
System.out.println("文件:" + fileOrDir.getName());
} else if (fileOrDir.isDirectory()) {
// 如果是目录,递归遍历子目录
System.out.println("目录:" + fileOrDir.getName());
traverseDirectory(fileOrDir);
}
}
}
RandomAccessFile?既可以用来读取文件,也可以用来写入文件
不同之处:允许跳转到文件的任何位置,从那里开始读取或写入,特别适用于需要在文件中随机访问数据的场景,如数据库系统
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileDemo {
public static void main(String[] args) {
String filePath = "logs/test.txt";
try {
// 使用 RandomAccessFile 写入文件
writeToFile(filePath, "Hello, world!");
// 使用 RandomAccessFile 读取文件
String content = readFromFile(filePath);
System.out.println("文件内容: " + content);
} catch (IOException e) {
e.printStackTrace();
}
}
private static void writeToFile(String filePath, String content) throws IOException {
try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "rw")) {
// 将文件指针移动到文件末尾(在此处追加内容)
randomAccessFile.seek(randomAccessFile.length());
// 写入内容
randomAccessFile.writeUTF(content);
}
}
private static String readFromFile(String filePath) throws IOException {
StringBuilder content = new StringBuilder();
try (RandomAccessFile randomAccessFile = new RandomAccessFile(filePath, "r")) {
// 将文件指针移动到文件开始处(从头开始读取)
randomAccessFile.seek(0);
content.append(randomAccessFile.readUTF());
}
return content.toString();
}
}
为了避免中文乱码问题,我们使用 RandomAccessFile 的 writeUTF 和 readUTF 方法,它们将使用 UTF-8 编码处理字符串
访问模式 mode 的值可以是:
FileUtils 类是 Apache Commons IO 库中的一个类,提供了一些更为方便的方法来操作文件或目录
File srcFile = new File("path/src/file");
File destFile = new File("path/dest/file");
// 复制文件
FileUtils.copyFile(srcFile, destFile);
// 复制目录
FileUtils.copyDirectory(srcFile, destFile);
File file = new File("path/file");
// 删除文件或目录
FileUtils.delete(file);
注:如果要删除一个非空目录,需要先删除目录中的所有文件和子目录
File srcFile = new File("path/to/src/file");
File destFile = new File("path/to/dest/file");
// 移动文件或目录
FileUtils.moveFile(srcFile, destFile);
File file = new File("path/file");
// 获取文件或目录的修改时间
Date modifyTime = FileUtils.lastModified(file);
// 获取文件或目录的大小
long size = FileUtils.sizeOf(file);
// 获取文件或目录的扩展名
String extension = FileUtils.getExtension(file.getName());
将指定的源文件复制到指定的目标文件中
File dest = FileUtil.file("FileUtilDemo2.java");
FileUtil.copyFile(file, dest);
FileUtil.move(file, dest, true);
FileUtil.del(file);
FileUtil.rename(file, "FileUtilDemo3.java", true);
FileUtil.readLines(file, "UTF-8").forEach(System.out::println);
一切文件(文本、视频、图片)的数据都是以二进制的形式存储的,传输时也是。所以,字节流可以传输任意类型的文件数据
OutputStream 是字节输出流的超类(父类),它定义的一些共性方法:
FileOutputStream类,用于将数据写入到文件
1. 使用文件名创建:
String fileName = "example.txt";
FileOutputStream fos = new FileOutputStream(fileName);
使用文件名 "example.txt" 创建一个 FileOutputStream 对象,将数据写入到该文件中。如果文件不存在,则创建一个新文件;如果文件已经存在,则覆盖原有文件
2. 使用文件对象创建
File file = new File("example.txt");
FileOutputStream fos = new FileOutputStream(file);
使用示例:
FileOutputStream fos = null;
try {
fos = new FileOutputStream("example.txt");
fos.write("你好呀".getBytes());
} catch (IOException e) {
e.printStackTrace();
} finally {
if (fos != null) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
创建一个 FileOutputStream 对象,将字符串 "你好呀" 写入到 example.txt 文件中,最后关闭了输出流
1. 写入字节
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");
// 写出数据
fos.write(97); // 第1个字节 等价于 fos.write('a');
fos.write(98); // 第2个字节 等价于 fos.write('b');
fos.write(99); // 第3个字节 等价于 fos.write('c');
// 关闭资源
fos.close();
注:一个字节只有8位,因此参数 b 的取值范围应该在 0 到 255 之间,超出这个范围的值将会被截断;当将一个整型值传递给 write(int b) 方法时,方法会将该值转换为 byte 类型,发生截断只会将参数 b 的低8位写入,而忽略高24位
2. 写入字节数组
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");
// 字符串转换为字节数组
byte[] b = "在深度学习里迷路".getBytes();
// 写入字节数组数据
fos.write(b);
// 关闭资源
fos.close();
3. 写入指定长度字节数组
// 使用文件名称创建流对象
FileOutputStream fos = new FileOutputStream("fos.txt");
// 字符串转换为字节数组
byte[] b = "abcde".getBytes();
// 从索引2开始,2个字节。索引2是c,
fos.write(b,2,2); //写进去的就是 cd
// 关闭资源
fos.close();
1.?使用文件名和追加标志创建 FileOutputStream 对象
String fileName = "example.txt";
boolean append = true;
FileOutputStream fos = new FileOutputStream(fileName, append);
如果文件不存在,则创建一个新文件;如果文件已经存在,则在文件末尾追加数据。
true
?表示追加数据,false
?表示不追加也就是清空原有数据
2.?使用文件对象和追加标志创建 FileOutputStream 对象
File file = new File("example.txt");
boolean append = true;
FileOutputStream fos = new FileOutputStream(file, append);
如果文件不存在,则创建一个新文件;如果文件已经存在,则在文件末尾追加数据
3.?Windows 中,换行符号 \r\n
String filename = "example.txt";
FileOutputStream fos = new FileOutputStream(filename, true); // 追加模式
String content = "你好呀\r\n"; // 使用回车符和换行符的组合
fos.write(content.getBytes());
fos.close();
4. macOS 中,换行符 \n
String filename = "example.txt";
FileOutputStream fos = new FileOutputStream(filename, true); // 追加模式
String content = "macOS使用的换行符\n"; // 只使用换行符
fos.write(content.getBytes());
fos.close();
InputStream?是字节输入流的超类(父类),它的一些共性方法:
FileInputStream 就是一个子类,是文件输入流,用于将数据从文件中读取数据
1. FileInputStream(String name)
创建一个 FileInputStream 对象,并打开指定名称的文件进行读取。如果文件不存在,将会抛出 FileNotFoundException 异常
2. FileInputStream(File file)
创建一个 FileInputStream 对象,并打开指定的 File 对象表示的文件进行读取
1. 读取字节
// 创建一个 FileInputStream 对象
FileInputStream fis = new FileInputStream("test.txt");
// 读取文件内容
int data;
while ((data = fis.read()) != -1) {
System.out.print((char) data);
}
// 关闭输入流
fis.close();
读取一个字节并返回其整数表示。如果已经到达文件的末尾,则返回 -1。如果在读取时发生错误,则会抛出 IOException 异常
2. 字节数组读取
从输入流中最多读取 b.length 个字节,并将它们存储到缓冲区数组 b 中
// 创建一个 FileInputStream 对象
FileInputStream fis = new FileInputStream("test.txt");
// 读取文件内容到缓冲区
byte[] buffer = new byte[1024];
int count;
while ((count = fis.read(buffer)) != -1) {
System.out.println(new String(buffer, 0, count));
}
// 关闭输入流
fis.close();
原理:将文件读入到字节输入流,然后通过字节输出流写入文件
// 创建一个 FileInputStream 对象以读取原始图片文件
FileInputStream fis = new FileInputStream("original.jpg");
// 创建一个 FileOutputStream 对象以写入复制后的图片文件
FileOutputStream fos = new FileOutputStream("copy.jpg");
// 创建一个缓冲区数组以存储读取的数据
byte[] buffer = new byte[1024];
int count;
// 读取原始图片文件并将数据写入复制后的图片文件
while ((count = fis.read(buffer)) != -1) {
fos.write(buffer, 0, count);
}
// 关闭输入流和输出流
fis.close();
fos.close();
字符流是一种用于读取和写入字符数据的输入输出流。与字节流不同,字符流以字符为单位读取和写入数据,常用来处理文本信息
注:?字符流 = 字节流 + 编码表(要注意处理乱码问题)
java.io.Reader?是字符输入流的超类(父类),定义了字符输入流的一些共性方法:
FileReader 是 Reader 的子类,用于从文件中读取字符数据
// 使用File对象创建流对象
File file = new File("a.txt");
FileReader fr = new FileReader(file);
// 使用文件名称创建流对象
FileReader fr = new FileReader("b.txt");
1. 读取字符
每次可以读取一个字符,返回读取的字符(转为 int 类型),当读取到文件末尾时,返回?-1
// 使用文件名称创建流对象
FileReader fr = new FileReader("abc.txt");
// 定义变量,保存数据
int b;
// 循环读取
while ((b = fr.read())!=-1) {
System.out.println((char)b);
}
// 关闭资源
fr.close();
2. 读取指定长度的字符
每次读取 len 个字符,然后使用 String 构造方法将其转换为字符串并输出
File textFile = new File("docs.md");
// 给一个 FileReader 的示例
// try-with-resources FileReader
try(FileReader reader = new FileReader(textFile);) {
// read(char[] cbuf)
char[] buffer = new char[1024];
int len;
while ((len = reader.read(buffer, 0, buffer.length)) != -1) {
System.out.print(new String(buffer, 0, len));
}
}
java.io.Writer?是字符输出流类的超类(父类),它定义的一些共性方法:
FileWrite 是 Writer 子类,用来将字符写入到文件
// 第一种:使用File对象创建流对象
File file = new File("a.txt");
FileWriter fw = new FileWriter(file);
// 第二种:使用文件名称创建流对象
FileWriter fw = new FileWriter("b.txt");
1. 写入字符,每次可以写出一个字符
FileWriter fw = null;
try {
fw = new FileWriter("output.txt");
fw.write(72); // 写入字符'H'的ASCII码
fw.write(101); // 写入字符'e'的ASCII码
fw.write(108); // 写入字符'l'的ASCII码
fw.write(108); // 写入字符'l'的ASCII码
fw.write(111); // 写入字符'o'的ASCII码
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fw != null) {
fw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
注:
1. writer(int b) 写入的是一个字节,而不是一个字符
2. writer(char cbuf[ ])? writer(String str)?写入字符
?2. 写入字符数组
FileWriter fw = null;
try {
fw = new FileWriter("output.txt");
char[] chars = {'H', 'e', 'l', 'l', 'o'};
fw.write(chars); // 将字符数组写入文件
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (fw != null) {
fw.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
3. 写入指定字符数组
fw = new FileWriter("output.txt");
char[] chars = {'H', 'e', 'l', 'l', 'o', ',', ' ', 'W', 'o', 'r', 'l', 'd', '!'};
fw.write(chars, 0, 5); // 将字符数组的前 5 个字符写入文件
4. 写入字符串
fw = new FileWriter("output.txt");
String str = "你好呀,哈哈哈";
fw.write(str); // 将字符串写入文件
5. 写入指定字符串
String str = "这个时间变得好快呀!";
try (FileWriter fw = new FileWriter("output.txt")) {
fw.write(str, 0, 5); // 将字符串的前 5 个字符写入文件
} catch (IOException e) {
e.printStackTrace();
}
因为 FileWriter 内置了缓冲区 ByteBuffer,所以如果不关闭输出流,就无法把字符写入到文件中
关闭了流对象,就无法继续写数据了。如果既想写入数据,又想继续使用流,就需要 flush()、
注:即便是flush方法写出了数据,操作的最后还是要调用close方法,释放系统资源。当然你也可以用 try-with-resources 的方式
// 使用文件名称创建流对象,可以续写数据
FileWriter fw = new FileWriter("fw.txt",true);
// 写出字符串
fw.write("你好");
// 写出换行
fw.write("\r\n");
// 写出字符串
fw.write("世界");
// 关闭资源
fw.close();
public class CopyFile {
public static void main(String[] args) throws IOException {
//创建输入流对象
FileReader fr=new FileReader("aa.txt");//文件不存在会抛出java.io.FileNotFoundException
//创建输出流对象
FileWriter fw=new FileWriter("copyaa.txt");
/*创建输出流做的工作:
* 1、调用系统资源创建了一个文件
* 2、创建输出流对象
* 3、把输出流对象指向文件
* */
//文本文件复制,一次读一个字符
copyMethod1(fr, fw);
//文本文件复制,一次读一个字符数组
copyMethod2(fr, fw);
fr.close();
fw.close();
}
public static void copyMethod1(FileReader fr, FileWriter fw) throws IOException {
int ch;
while((ch=fr.read())!=-1) {//读数据
fw.write(ch);//写数据
}
fw.flush();
}
public static void copyMethod2(FileReader fr, FileWriter fw) throws IOException {
char chs[]=new char[1024];
int len=0;
while((len=fr.read(chs))!=-1) {//读数据
fw.write(chs,0,len);//写数据
}
fw.flush();
}
}
工作原理:将数据先写入缓冲区中,当缓冲区满时再一次性写入文件或输出流,或者当缓冲区为空时一次性从文件或输入流中读取一定量的数据。
目的:减少系统的 I/O 操作次数,提高系统的 I/O 效率,从而提高程序的运行效率
BufferedInputStream 和 BufferedOutputStream 属于字节缓冲流
// 创建字节缓冲输入流,先声明字节流
FileInputStream fps = new FileInputStream(b.txt);
BufferedInputStream bis = new BufferedInputStream(fps)
// 创建字节缓冲输入流(一步到位)
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("b.txt"));
// 创建字节缓冲输出流(一步到位)
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("b.txt"));
思想:一次多读点多写点,减少读写的频率,用空间换时间
BufferedInputStream 的 read 方法:
public synchronized int read() throws IOException {
if (pos >= count) { // 如果当前位置已经到达缓冲区末尾
fill(); // 填充缓冲区
if (pos >= count) // 如果填充后仍然到达缓冲区末尾,说明已经读取完毕
return -1; // 返回 -1 表示已经读取完毕
}
return getBufIfOpen()[pos++] & 0xff; // 返回当前位置的字节,并将位置加 1
}
FileInputStream 的 read 方法:
调用的是系统底层的方法 read0() ,不同系统上它们的功能都是相同的,都是用于读取一个字节
BufferedOutputStream 的 write 方法:
public synchronized void write(byte b[], int off, int len) throws IOException {
if (len >= buf.length) { // 如果写入的字节数大于等于缓冲区长度
/* 如果请求的长度超过了输出缓冲区的大小,
先刷新缓冲区,然后直接将数据写入。
这样可以避免缓冲流级联时的问题。*/
flushBuffer(); // 先刷新缓冲区
out.write(b, off, len); // 直接将数据写入输出流
return;
}
if (len > buf.length - count) { // 如果写入的字节数大于空余空间
flushBuffer(); // 先刷新缓冲区
}
System.arraycopy(b, off, buf, count, len); // 将数据拷贝到缓冲区中
count += len; // 更新计数器
}
思想:
步骤 1?的目的:避免缓冲流级联时的问题 ->?缓冲区的大小不足以容纳写入的数据时,可能会引发级联刷新,导致效率降低
级联问题(Cascade Problem)是指在一组缓冲流(Buffered Stream)中,由于缓冲区的大小不足以容纳要写入的数据,导致数据被分割成多个部分,并分别写入到不同的缓冲区中,最终需要逐个刷新缓冲区,从而导致性能下降的问题。
?也就是说,只有当 buf 写满了,才会 flush,将数据刷到磁盘,默认一次刷 8192 个字节
public BufferedOutputStream(OutputStream out) {
this(out, 8192);
}
如果 buf 没有写满,会继续写 buf
byte 类型是有符号的,即其取值范围为 -128 到 127。如果我们希望得到的是一个无符号的 byte 值,就需要使用?byte & 0xFF?来进行转换
BufferedReader 类继承自 Reader 类,readLine()?方法可以一次读取一行数据,而不是一个字符一个字符地读取
BufferedWriter 类继承自 Writer 类,newLine() 方法可以写入一个系统特定的行分隔符
// 创建字符缓冲输入流
BufferedReader br = new BufferedReader(new FileReader("b.txt"));
// 创建字符缓冲输出流
BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));
// 创建流对象
BufferedReader br = new BufferedReader(new FileReader("a.txt"));
// 定义字符串,保存读取的一行文字
String line = null;
// 循环读取,读取到最后返回null
while ((line = br.readLine())!=null) {
System.out.print(line);
System.out.println("------");
}
// 释放资源
br.close();
// 创建流对象
BfferedWriter bw = new BufferedWriter(new FileWriter("b.txt"));
// 写出数据
bw.write("你");
// 写出换行
bw.newLine();
bw.write("好");
bw.newLine();
bw.write("呀");
bw.newLine();
// 释放资源
bw.close();
转换流主要有两种类型:InputStreamReader 和 OutputStreamWriter
在计算机中,数据通常以二进制形式存储和传输
String str = "你好";
String charsetName = "UTF-8";
// 编码
byte[] bytes = str.getBytes(Charset.forName(charsetName));
System.out.println("编码: " + bytes);
// 解码
String decodedStr = new String(bytes, Charset.forName(charsetName));
System.out.println("解码: " + decodedStr);
Charset:字符集,是一组字符的集合,每个字符都有一个唯一的编码值,也称为码点
作用是将字节流(InputStream)转换为字符流(Reader),同时支持指定的字符集编码方式,从而实现字符流与字节流之间的转换
// 创建一个使用默认字符集的字符流
InputStreamReader isr = new InputStreamReader(new FileInputStream("in.txt"));
// 创建一个指定字符集的字符流
InputStreamReader isr2 = new InputStreamReader(new FileInputStream("in.txt") , "GBK");
String s = "你好呀!";
try {
// 将字符串按GBK编码方式保存到文件中
OutputStreamWriter outUtf8 = new OutputStreamWriter(
new FileOutputStream("logs/test_utf8.txt"), "GBK");
outUtf8.write(s);
outUtf8.close();
// 将字节流转换为字符流,使用GBK编码方式
InputStreamReader isr = new InputStreamReader(new
FileInputStream("logs/test_utf8.txt"), "GBK");
// 读取字符流
int c;
while ((c = isr.read()) != -1) {
System.out.print((char) c);
}
isr.close();
} catch (IOException e) {
e.printStackTrace();
}
将字符流转换为字节流,是字符流到字节流的桥梁
// 创建一个使用默认字符集的字符流
OutputStreamWriter isr = new OutputStreamWriter(new FileOutputStream("a.txt"));
// 创建一个指定字符集的字符流
OutputStreamWriter isr2 = new OutputStreamWriter(new FileOutputStream("b.txt") , "GBK");
通常为了提高读写效率,我们会在转换流上再加一层 缓冲流
try {
// 从文件读取字节流,使用UTF-8编码方式
FileInputStream fis = new FileInputStream("test.txt");
// 将字节流转换为字符流,使用UTF-8编码方式
InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
// 使用缓冲流包装字符流,提高读取效率
BufferedReader br = new BufferedReader(isr);
// 创建输出流,使用UTF-8编码方式
FileOutputStream fos = new FileOutputStream("output.txt");
// 将输出流包装为转换流,使用UTF-8编码方式
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF-8");
// 使用缓冲流包装转换流,提高写入效率
BufferedWriter bw = new BufferedWriter(osw);
// 读取输入文件的每一行,写入到输出文件中
String line;
while ((line = br.readLine()) != null) {
bw.write(line);
bw.newLine(); // 每行结束后写入一个换行符
}
// 关闭流
br.close();
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
一种可以将 Java 对象序列化和反序列化的流
在 Java 中,序列化通过实现 java.io.Serializable 接口来实现,只有实现了 Serializable 接口的对象才能被序列化
反序列化是指将一个字节序列转换为一个对象,以便在程序中使用
java.io.ObjectOutputStream 继承自OutputStream 类,因此可以将序列化后的字节序列写入到文件、网络等输出流中
构造方法接收一个 OutputStream 对象作为参数,将序列化后的字节序列输出到指定的输出流中
FileOutputStream fos = new FileOutputStream("file.txt");
ObjectOutputStream oos = new ObjectOutputStream(fos);
一个对象要想序列化,必须满足两个条件:
public class Employee implements Serializable {
public String name;
public String address;
public transient int age; // transient瞬态修饰成员,不会被序列化
}
writeObject 方法,将对象序列化成字节序列并输出到输出流中的方法,可以处理对象之间的引用关系、继承关系、静态字段和 transient 字段
public class ObjectOutputStreamDemo {
public static void main(String[] args) {
Person person = new Person("小林", 20);
try {
FileOutputStream fos = new FileOutputStream("logs/person.dat");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(person);
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
}
ObjectInputStream 可以读取 ObjectOutputStream 写入的字节流,并将其反序列化为相应的对象
String filename = "logs/person.dat"; // 待反序列化的文件名
try (FileInputStream fileIn = new FileInputStream(filename);
ObjectInputStream in = new ObjectInputStream(fileIn)) {
// 从指定的文件输入流中读取对象并反序列化
Object obj = in.readObject();
// 将反序列化后的对象强制转换为指定类型
Person p = (Person) obj;
// 打印反序列化后的对象信息
System.out.println("Deserialized Object: " + p);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
实际开发中,很少使用 JDK 自带的序列化和反序列化
Kryo 是一个优秀的 Java 序列化和反序列化库,具有高性能、高效率和易于使用和扩展等特点
?使用:
1.?在 pom.xml 中引入依赖
<!-- 引入 Kryo 序列化工具 -->
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>5.4.0</version>
</dependency>
2. 调用
public class KryoDemo {
public static void main(String[] args) throws FileNotFoundException {
Kryo kryo = new Kryo();
// 将对象进行注册
kryo.register(KryoParam.class);
KryoParam object = new KryoParam("小林", 123);
Output output = new Output(new FileOutputStream("logs/kryo.bin"));
kryo.writeObject(output, object);
output.close();
Input input = new Input(new FileInputStream("logs/kryo.bin"));
KryoParam object2 = kryo.readObject(input, KryoParam.class);
System.out.println(object2);
input.close();
}
}
class KryoParam {
private String name;
private int age;
public KryoParam() {
}
public KryoParam(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "KryoParam{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
PrintStream 是 OutputStream 的子类(字节流),PrintWriter 是 Writer 的子类(字符流)
特点:
常用转换说明符及对应的输出格式:
%s
:输出一个字符串。%d
?或?%i
:输出一个十进制整数。%x
?或?%X
:输出一个十六进制整数,%x
?输出小写字母,%X
?输出大写字母。%f
?或?%F
:输出一个浮点数。%e
?或?%E
:输出一个科学计数法表示的浮点数,%e
?输出小写字母 e,%E
?输出大写字母 E。%g
?或?%G
:输出一个浮点数,自动选择?%f
?或?%e/%E
?格式输出。%c
:输出一个字符。%b
:输出一个布尔值。%h
:输出一个哈希码(16进制)。%n
:换行符。还支持一些修饰符,用于指定输出的宽度、精度、对齐方式等
int num = 123;
System.out.printf("%5d\n", num); // 输出 " 123"
System.out.printf("%-5d\n", num); // 输出 "123 "
System.out.printf("%05d\n", num); // 输出 "00123"
double pi = Math.PI;
System.out.printf("%10.2f\n", pi); // 输出 " 3.14"
System.out.printf("%-10.4f\n", pi); // 输出 "3.1416 "
String name = "是打印流";
System.out.printf("%10s\n", name); // 输出 " 是打印流"
System.out.printf("%-10s\n", name); // 输出 "是打印流 "