Java的I/O流学习

35v0lnriiqz

向大佬学习

Java-IO流 | Drunkbaby’s Blog (drun1baby.top)

java(17)io流 - 弯弓射雕的男人 - 博客园 (cnblogs.com)

前言

IO是指 Input/Output,即输入和输出。以内存为中心:

为什么要把数据读到内存才能处理这些数据?因为代码是在内存中运行的,数据也必须读到内存,最终的表示方式无非是 byte数 组,字符串等,都必须存放在内存里。

从 Java 代码来看,输入实际上就是从外部,例如,硬盘上的某个文件,把内容读到内存,并且以 Java 提供的某种数据类型表示,例如,byte[]String,这样,后续代码才能处理这些数据。

因为内存有“易失性”的特点,所以必须把处理后的数据以某种方式输出,例如,写入到文件。Output 实际上就是把 Java 表示的数据格式,例如,byte[]String等输出到某个地方。

IO 流是一种顺序读写数据的模式,它的特点是单向流动。数据类似自来水一样在水管中流动,所以我们把它称为 IO 流。

IO又分为流IO(java.io)和块IO(java.nio)

Java.io是大多数面向数据流的输入/输出类的主要软件包。此外,Java也对块传输提供支持,在核心库 java.nio中采用的便是块IO。

创建文件的三种方式

1
src/IOStream/CreateForFile/new1.txt

准备将文件都放到新建的 CreateForFile 文件夹中

建议路径跑通一个之后,其余复制粘贴路径(可以写绝对路径,相对路径IDEA是从项目目录开始)。

根据路径创建一个 File 对象

  • 方法 new File(String pathname)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package IOStream;

import java.io.File;
import java.io.IOException;

public class newFile {
public static void main(String[] args) {
createFile();
}
public static void createFile(){
File file = new File("src/IOStream/CreateForFile/new1.txt");
try{
file.createNewFile();
System.out.println("Create Successfully");
} catch (IOException e){
e.printStackTrace();
}
}
}

根据父目录 File 对象,在子路径创建一个文件

  • 方法 new File(File parent, String child)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package IOStream.CreateForFile;

import java.io.File;
import java.io.IOException;

// 根据父目录File对象,在子路径创建一个文件
public class newFile02 {
public static void main(String[] args) {
createFile();
}
public static void createFile(){
File parentFile = new File("src/IOStream/CreateForFile");
File file = new File(parentFile, "new2.txt");
try{
file.createNewFile();
System.out.println("Create Successfully");
} catch (IOException e){
e.printStackTrace();
}
}
}

根据父目录路径,在子路径下生成文件

  • 方法 new File(String parent, String child)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package IOStream;

import java.io.File;
import java.io.IOException;

// 根据父目录路径,在子路径下生成文件
public class newFile03 {
public static void main(String[] args) {
createFile();
}
public static void createFile(){
String parentPath = "src/IOStream/CreateForFile";
String fileName = "new3.txt";
File file = new File(parentPath, fileName);
try{
file.createNewFile();
System.out.println("Create Successfully");
} catch (IOException e){
e.printStackTrace();
}
}
}

可以看到生成了三个新文档。

获取文件信息

我们先在 new1.txt 当中编辑一些消息

image-20240204132105650

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package IOStream;

import java.io.File;

public class GetFileInfo {
public static void main(String[] args) {
getFileContents();
}

public static void getFileContents(){
File file = new File("src/IOStream/CreateForFile/new1.txt");
System.out.println("文件名称为:" + file.getName());
System.out.println("文件的绝对路径为:" + file.getAbsolutePath());
System.out.println("文件的父级目录为:" + file.getParent());
System.out.println("文件的大小(字节)为:" + file.length());
System.out.println("这是不是一个文件:" + file.isFile());
System.out.println("这是不是一个目录:" + file.isDirectory());
}
}

image-20240204132135297

目录与文件操作

文件删除

  • 使用 file.delete(文件)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package IOStream;

import java.io.File;
import java.lang.reflect.Field;

// 文件删除
public class FileDelete {
public static void main(String[] args) {
deleteFile();
}
public static void deleteFile(){
File file = new File("src/IOStream/CreateForFile/new1.txt");
System.out.println(file.delete() ? "Delete Successfully":"Delete failed");
}
}

目录删除

  • 方法 file.delete(目录),只有空的目录才可以删除,不然会显示删除失败。
  • CreateForFile 同级目录下新建了一个文件夹 New 用以测试。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package IOStream;

import java.io.File;

//删除目录
public class DirectoryDelete {
public static void main(String[] args) {
deleteDirectory();
}
public static void deleteDirectory(){
File file = new File("src/IOStream/New");
System.out.println(file.delete()? "Delete Successfully":"Delete failed");
}
}

创建单级目录

  • 方法 file.mkdir()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package IOStream;

import java.io.File;

// 创建单级目录
public class CreateSingleDirectory {
public static void main(String[] args) {
createSingleDir();
}
public static void createSingleDir(){
File file = new File("src/IOStream/CreateForDirectory");
System.out.println(file.mkdir() ? "Create Successfully":"Create failed");
}
}

创建多级目录

  • 方法 file.mkdirs(),注意多了个 s 别搞错了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package IOStream;

import java.io.File;

// 创建多级目录
public class CreateMultiDirectory {
public static void main(String[] args) {
createMultiDir();
}

public static void createMultiDir(){
File file = new File("src/IOStream/CreateMultiDirectory/test");
System.out.println(file.mkdirs() ? "Create Successfully":"Create failed");

}
}

流的基本概念

在电脑上的数据有三种存储方式,一种是外存,一种是内存,一种是缓存。比如电脑上的硬盘,磁盘,U盘等都是外存,在电脑上有内存条,缓存是在CPU里面的。外存的存储量最大,其次是内存,最后是缓存,但是外存的数据的读取最慢,其次是内存,缓存最快。这里总结从外存读取数据到内存以及将数据从内存写到外存中。对于内存和外存的理解,我们可以简单的理解为容器,即外存是一个容器,内存又是另外一个容器。那又怎样把放在外存这个容器内的数据读取到内存这个容器以及怎么把内存这个容器里的数据存到外存中呢?

在Java类库中,IO部分的内容是很庞大的,因为它涉及的领域很广泛:

标准输入输出,文件的操作,网络上的数据流,字符串流,对象流,zip文件流等等,java中将输入输出抽象称为流,就好像水管,将两个容器连接起来。将数据从外存中读取到内存中的称为输入流,将数据从内存写入外存中的称为输出流。

流的分类

抽象基类 字节流 字符流
输入流 InputStream Reader
输出流 OutputStream Writer

Java IO流中的四大基流。这四大基流都是抽象类,其他流都是继承于这四大基流的。

根据流向分为输入流和输出流

输出:把程序(内存)中的内容输出到磁盘、光盘等存储设备中

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

传输数据单位分为字节流和字符流

字节流:数据流中最小的数据单元是字节

字符流:数据流中最小的数据单元是字符, Java中的字符是Unicode编码,一个字符占用两个字节(无论中文还是英文都是两个字节)。

根据功能分为节点流和包装流

节点流:可以从或向一个特定的地方(节点)读写数据,直接连接数据源。如最常见的是文件的FileReader,还可以是数组、管道、字符串,关键字分别为ByteArray/CharArray,Piped,String。.

处理流(包装流):并不直接连接数据源,是对一个已存在的流的连接和封装,是一种典型的装饰器设计模式,使用处理流主要是为了更方便的执行输入输出工作,如PrintStream,输出功能很强大,又如BufferedReader提供缓存机制,推荐输出时都使用处理流包装。

一个流对象经过其他流的多次包装,称为流的链接。

注意:一个IO流可以即是输入流又是字节流又或是以其他方式分类的流类型,是不冲突的。比如FileInputStream,它既是输入流又是字节流还是文件节点流。

一些特别的的流类型

转换流:转换流只有字节流转换为字符流,因为字符流使用起来更方便,我们只会向更方便使用的方向转化。如:InputStreamReader与OutputStreamWriter。

缓冲流:有关键字Buffered,也是一种处理流,为其包装的流增加了缓存功能,提高了输入输出的效率,增加缓冲功能后需要使用flush()才能将缓冲区中内容写入到实际的物理节点。但是,在现在版本的Java中,只需记得关闭输出流(调用close()方法),就会自动执行输出流的flush()方法,可以保证将缓冲区中内容写入。

对象流:有关键字Object,主要用于将目标对象保存到磁盘中或允许在网络中直接传输对象时使用(对象序列化)。

操作 IO 流的模板

①、创建源或目标对象

输入:把文件中的数据流向到程序中,此时文件是源,程序是目标

输出:把程序中的数据流向到文件中,此时文件是目标,程序是源

②、创建 IO 流对象

输入:创建输入流对象

输出:创建输出流对象

③、具体的 IO 操作

④、关闭资源

输入:输入流的 close() 方法

输出:输出流的 close() 方法

注意:1、程序中打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该资源。如果不关闭该资源,那么磁盘的文件将一直被程序引用着,不能删除也不能更改。所以应该手动调用 close() 方法关闭流资源

标准I/O

命令行参数

1
2
3
4
5
6
7
public class TestArgs {  
public static void main(String[] args) {
for (int i = 0; i < args.length; i++) {
System.out.println("args[" + i + "] is <" + args[i] + ">");
}
}
}

image-20240204181750105

标准输入,输出数据流

java系统自带的标准数据流:java.lang.System:

1
2
3
4
5
6
java.lang.System   
public final class System extends Object{
static PrintStream err;//标准错误流(输出)
static InputStream in;//标准输入(键盘输入流)
static PrintStream out;//标准输出流(显示器输出流)
}

注意:
(1)System类不能创建对象,只能直接使用它的三个静态成员。
(2)每当main方法被执行时,就自动生成上述三个对象。

标准输出流 System.out

System.out向标准输出设备输出数据,其数据类型为PrintStream。方法:Void print(参数),Void println(参数)

write(byte[] b) 方法

1
2
3
4
5
6
7
8
9
10
write(byte[] b)
public void write(byte[] b)
throws IOException
将 b.length 个字节从指定 byte 数组写入此文件输出流中。
覆盖:
类 OutputStream 中的 write
参数:
b - 数据。
抛出:
IOException - 如果发生 I/O 错误。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package IOStream;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

// write(byte[] b) 方法
public class FileOutputWrite01 {
public static void main(String[] args) {
writeFile();
}

public static void writeFile() {
String filePath = "src/IOStream/CreateForFile/new3.txt";
FileOutputStream fileOutputStream = null;
try { // 注意fileOutputStream的作用域,因为fileOutputStream需要在finally分支中被异常捕获
// 所以这里的 try 先不闭合
fileOutputStream = new FileOutputStream(filePath);
String content = "kujiji";
try {
//write(byte[] b) 将 b.length 个字节从指定 byte 数组写入此文件输出流中
//String类型的字符串可以使用getBytes()方法将字符串转换为byte数组
fileOutputStream.write(content.getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}catch (FileNotFoundException e){
e.printStackTrace();
}
finally {
try {
fileOutputStream.close();
} catch (IOException e){
e.printStackTrace();
}
}
}
}

image-20240204185800890

write(byte[] b, int off, int len) 方法

  • 将指定 byte 数组中从偏移量 off 开始的 len 个字节写入此文件输出流。

这里的长度一定要与输入的字符相等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package IOStream;

import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;

// write(byte[] b) 方法
public class FileOutputWrite02 {
public static void main(String[] args) {
writeFile();
}

public static void writeFile() {
String filePath = "src/IOStream/CreateForFile/new3.txt";
FileOutputStream fileOutputStream = null;
try { // 注意fileOutputStream的作用域,因为fileOutputStream需要在finally分支中被异常捕获
// 所以这里的 try 先不闭合
fileOutputStream = new FileOutputStream(filePath);
String content = "666";
try {
//write(byte[] b) 将 b.length 个字节从指定 byte 数组写入此文件输出流中
//String类型的字符串可以使用getBytes()方法将字符串转换为byte数组
fileOutputStream.write(content.getBytes(StandardCharsets.UTF_8), 0, 3);
} catch (IOException e) {
e.printStackTrace();
}
}catch (FileNotFoundException e){
e.printStackTrace();
}
finally {
try {
fileOutputStream.close();
} catch (IOException e){
e.printStackTrace();
}
}
}
}

image-20240204190235020

追加写入

如果想要写入的数据不被覆盖,可以设置 FileOutputStream 的构造方法 append 参数设置为 true

1
2
3
fileOutputStream = new FileOutputStream(filePath);
// 设置追加写入
fileOutputStream = new FileOutputStream(filePath), true;

标准输入流 System.in

System.in读取标准输入设备数据(从标准输入获取数据,一般是键盘),其数 据类型为InputStream。方法:

int read() //返回ASCII码。若,返回值=-1,说明没有读取到任何字节读取工作结束。

int read(byte[] b)//读入多个字节到缓冲区b中返回值是读入的字节数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package IOStream;

import java.io.*;
public class StandardInputOutput {
public static void main(String args[]) {
int b;
try {
System.out.println("please Input:");
while ((b = System.in.read()) != -1) {
System.out.print((char) b);
}
} catch (IOException e) {
System.out.println(e.toString());
}
}
}

read() 方法

1
2
3
4
5
6
7
8
9
10
11
read() 
public int read() throws IOException
从此输入流中读取一个数据字节。

如果没有输入可用,则此方法将阻塞。

指定者: 类 InputStream 中的 read

返回: 下一个数据字节;如果已到达文件末尾,则返回 -1

抛出: IOException - 如果发生 I/O 错误。

之前我们用 file 的一系列操作读取过文件的信息,现在我们用 FileInputStream.read() 来读取文件内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package IOStream;
import java.io.FileInputStream;
import java.io.IOException;

// 使用 FileInputStream.read 读取文件
public class FileInputRead {
public static void main(String[] args) {
readFile();
}
public static void readFile(){
String filePath = "src/IOStream/CreateForFile/new2.txt";
FileInputStream fileInputStream = null;
int readData = 0;
try{
fileInputStream = new FileInputStream(filePath);
while((readData = fileInputStream.read())!=-1){
System.out.print((char)readData);
}
} catch (IOException e){
e.printStackTrace();
} finally {
try{
fileInputStream.close();
} catch (IOException e){
e.printStackTrace();
}
}
}
}

若我们 sout 的时候进行了换行,则每一个字符都会经过换行。而如果不设置换行,才是正常的输出。

read(byte[] d) 方法

允许在方法中添加一个字节数组。
这种方式很有意思,当我们设置缓冲区的值为 8 时,若文件中的字符长度超过了 8,则会换行输出。这和上面的换行实际上是异曲同工。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package IOStream;

import java.io.FileInputStream;
import java.io.IOException;

// read(byte[] d) 方法,允许在方法中添加一个字节数组
public class FileInputRead02 {
public static void main(String[] args) {
readFile();
}
public static void readFile(){
String filePath = "src/IOStream/CreateForFile/new2.txt";
FileInputStream fileInputStream = null;
byte[] cache = new byte[8]; // 设置缓冲区,缓冲区大小为 8 字节
int readLen = 0;
try {
fileInputStream = new FileInputStream(filePath);
while((readLen = fileInputStream.read(cache)) != -1){
System.out.println(new String(cache, 0, readLen));
}
} catch (IOException e){
e.printStackTrace();
} finally {
try {
fileInputStream.close();
} catch (IOException e){
e.printStackTrace();
}
}
}
}

image-20240204185034915

这里的 while 会运行两次,会读取文件当中所有的字符。

标准错误流System.err

System.err输出标准错误,其数据类型为PrintStream。可查阅API获得详细说明。

标准输出通过System.out调用println方法输出参数并换行,而print方法输出参数但不换行。println或print方法都通 过重载实现了输出基本数据类型的多个方法,包括输出参数类型为boolean、char、int、long、float和double。同时,也重载实现 了输出参数类型为char[]、String和Object的方法。其中,print(Object)和println(Object)方法在运行时将调 用参数Object的toString方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package IOStream;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class StandardInputOutput {
public static void main(String args[]) {
String s;
// 创建缓冲区阅读器从键盘逐行读入数据
InputStreamReader ir = new InputStreamReader(System.in);
BufferedReader in = new BufferedReader(ir);
System.out.println("Unix系统: ctrl-d 或 ctrl-c 退出"
+ "\nWindows系统: ctrl-z 退出");
try {
// 读一行数据,并标准输出至显示器
s = in.readLine();
// readLine()方法运行时若发生I/O错误,将抛出IOException异常
while (s != null) {
System.out.println("Read: " + s);
s = in.readLine();
}
// 关闭缓冲阅读器
in.close();
} catch (IOException e) { // Catch any IO exceptions.
e.printStackTrace();
}
}
}

字节流

利用前文的 fileInputStreamfileOutputStream 进行文件拷贝。

原理上来说,先将文件的内容(注意,其实图片当中也是内容,这个内容不光是文字!) 读取出来,再写入新的文件当中。

字节输入流:InputStream

1
2
3
public abstract class InputStream
  extends Object
  implements Closeable

这个抽象类是表示输入字节流的所有类的超类。

字节输出流:OutputStream

1
2
3
public abstract class OutputStream 
extends Object
       implements Closeable, Flushable

这个抽象类是表示字节输出流的所有类的超类。继承自InputStream 的流都是向程序中输入数据的,且数据单位为字节(8bit)。 输出流接收输出字节并将其发送到某个接收器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package IOStream;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

// 文件拷贝操作
public class FileCopy {
public static void main(String[] args) {
copyFile();
}
public static void copyFile(){
String srcFilename = "src/IOStream/CreateForFile/new3.txt";
String desFilename = "src/IOStream/CreateForFile/new4.txt";
FileInputStream fileInputStream = null;
FileOutputStream fileOutputStream = null;
try {
fileInputStream = new FileInputStream(srcFilename);
fileOutputStream = new FileOutputStream(desFilename);
byte[] cache = new byte[1024];
int readLen = 0;
while((readLen = fileInputStream.read(cache)) != -1){
fileOutputStream.write(cache, 0, readLen);
}
} catch (IOException e){
e.printStackTrace();
} finally {
try {
fileInputStream.close();
fileOutputStream.close();
} catch (IOException e){
e.printStackTrace();
}
}
}
}

image-20240204190820325

字符流

①、为什么要使用字符流?

因为使用字节流操作汉字或特殊符号语言的时候容易乱码,因为汉字不止一个字节,为了解决这个问题,建议使用字符流。

②、什么情况下使用字符流?

一般可以用记事本打开的文件,我们可以看到内容不乱码的。就是文本文件,可以使用字符流。而操作二进制文件(比如图片、音频、视频)必须使用字节流

字符输出流:FileWriter

1
2
3
public abstract class Writer
  extends Object
  implements Appendable, Closeable, Flushable

用于写入字符流的抽象类

字符输入流:Reader

1
2
3
public abstract class Reader
  extends Object
  implements Readable, Closeable

用于读取字符流的抽象类。

下方测试代码将会将 src/IOStream/CreateForFile/new3.txt 中的 new3.tx 文件打印输出至控制台:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package IOStream;

import java.io.FileReader;
import java.io.IOException;

// 读取文件的字符流
public class FileReaderPrint {
public static void main(String[] args) {
readFile();
}
public static void readFile(){
String filePath = "src/IOStream/CreateForFile/new3.txt";
FileReader fileReader = null;
try {
fileReader = new FileReader(filePath);
int readLen = 0;
char[] cache = new char[8];
while ((readLen = fileReader.read(cache))!=-1){
System.out.println(new String(cache, 0, readLen));
}
} catch (IOException e){
e.printStackTrace();
} finally {
try {
fileReader.close();
} catch (IOException e){
e.printStackTrace();
}
}
}
}

image-20240204191342921

  • FileReader 将会一个一个字符读取,因此可以不乱码输出中文

对象流(序列化与反序列化)

ObjectOutputStream:通过 writeObject()方法做序列化操作

ObjectInputStream:通过 readObject() 方法做反序列化操作

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

Runtime 命令执行操作的 Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package IOStream;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;

// 使用 Runtime 类进行命令执行
public class RuntimeExec {
public static void main(String[] args) throws Exception {
InputStream inputStream = Runtime.getRuntime().exec("calc.exe").getInputStream();
byte[] cache = new byte[1024];
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int readLen = 0;
while ((readLen = inputStream.read(cache))!=-1){
byteArrayOutputStream.write(cache, 0, readLen);
}
System.out.println(byteArrayOutputStream);
}
}

image-20240204183529170

再回到之前我们讲的 read(byte[] d)方法 ,在那里,我们设置的 Cache 缓冲区的值为 8。