大致介绍:
Java I/O (Input/Output) streams
文件流:文件在程序中是以流的形式来操作的

Drawing 2025-07-13 22.46.19.excalidraw

⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠ You can decompress Drawing data with the command palette: ‘Decompress current Excalidraw file’. For more info check in plugin settings under ‘Saving’

Excalidraw Data

Text Elements

文件(磁盘)

java程序
(内存)

输入流

输出流

Link to original

流:数据在数据源(文件)和程序(内存)之间经历的路径
输入流:数据从数据源(文件)到程序(内存)的路径
输出流:数据从程序(内存)到数据源(文件)的路径

流的分类

  • 按操作数据单位不同分为:字节流(8bit)二进制文件,字符流(按字符)文本文件
  • 按数据流的流向不同分为:输入流,输出流
  • 按流的角色的不同分为:节点流,处理流/包装流
(抽象基类)字节流字符流
输入流InputStreamReader
输出流OutputStreamWriter

Java的IO流共涉及40多个类,实际上非常规整,都是从如上4个抽象基类派生的。
由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。

FileInputStream

String 构造方法用法示范

    // 用法示范
byte[] bytes = "hello, world".getBytes(); // 使用平台默认编码生成 byte[]
String str = new String(bytes, 7, 5); // 从第7个字节开始,取5个字节构建字符串
System.out.println(str); // 输出: world

Drawing 2025-07-16 02.43.53.excalidraw

⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠ You can decompress Drawing data with the command palette: ‘Decompress current Excalidraw file’. For more info check in plugin settings under ‘Saving’

Excalidraw Data

Text Elements

h

e

l

l

,

w

o

r

l

d

space

0

1

2

3

4

5

o

6

7

8

9

10

11

Link to original

/**
 * @ Author: ferry
 * @ Date 16/07/2025 12:53 am
 */
public class FileInputStreamTest {
    int readData = 0;
    FileInputStream fileInputStream = null;
    @Test
    public void readFile1() throws FileNotFoundException {
 
        fileInputStream = new FileInputStream("test.txt");
        /*
         * Reads a byte of data from this input stream. This method blocks if no input is yet available.
         * Returns: the next byte of data, or -1 if the end of the file is reached.
         */
        try {
            while ((readData = fileInputStream.read()) != -1){
                // System.out.print(readData); // 104101108108111443211911111410810033
                // System.out.println();
                System.out.print((char)readData); // hello, world!
            }
        } catch (IOException ioException){
            ioException.printStackTrace();
        } finally {
            // 关闭文件流,释放资源.
            try {
                fileInputStream.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
 
    }
 
    @Test
    public void readFileArr() throws FileNotFoundException {
        fileInputStream = new FileInputStream("test.txt");
        byte[] buffer = new byte[8]; // 一次读取 8bit
        /**
         * @return the total number of bytes read into the buffer, or
         * {@code -1} if there is no more data because the end of
         * the file has been reached.
         */
        // hello, world!
 
				// fileInputStream 第一次 读取 8个字符
				// 之后 自动读取 5 个字符
        try {
            while ((readData = fileInputStream.read(buffer)) != -1){
                // buffer: [104, 101, 108, 108, 111, 44, 32, 119]  h e l l o , {space} w
                System.out.print(new String(buffer, 0 , readData)); // hello, world!
                // 用byte[]数组构建一个String
                // offset - The index of the first byte to decode
                // length – The number of bytes to decode
            }
        } catch (IOException ioException){
            ioException.printStackTrace();
        } finally {
            // 关闭文件流,释放资源.
            try {
                fileInputStream.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
 

Drawing 2025-07-16 02.43.53.excalidraw 1

⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠ You can decompress Drawing data with the command palette: ‘Decompress current Excalidraw file’. For more info check in plugin settings under ‘Saving’

Excalidraw Data

Text Elements

h

e

,

w

l

l

o

r

l

d

space

1

2

3

4

5

o

6

7

8

9

10

11

!

12

13

第二次 readData = 5

Link to original

FileOutputStream

//创建 FileOutputStream对象
String filePath = "e:\\a.txt";
FileOutputStream fileOutputStream = null;
try {
	
		//1. new FileOutputStream(filePath) 创建方式: 当写入内容是,会覆盖原来的内容
		//2. new FileOutputStream(filePath, true) 创建方式: 当写入内容是,是追加到文件后面
		fileOutputStream = new FileOutputStream(filePath, true);
		//写入一个字节
		//fileOutputStream.write('H');
		
		//写入字符串
		String str = "xyy,world!";
		//str.getBytes() 可以把 字符串-> 字节数组
		//fileOutputStream.write(str.getBytes());
		/*
		write(byte[] b, int off, int len) 将 len字节从位于偏移量 off的指定字节数组写入此文件输出流
		 */
		fileOutputStream.write(str.getBytes(), 0, 3); // xyy
 
} catch (IOException e) {
		e.printStackTrace();
} finally {
		try {
				fileOutputStream.close();
		} catch (IOException e) {
				e.printStackTrace();
		}
}

具体查看源码

FileReader

// 单个字符读取文件
String filePath = "e:\\story.txt";
FileReader fileReader = null;
int data = 0;
//1. 创建FileReader对象
try {
		fileReader = new FileReader(filePath);
		//循环读取 使用read, 单个字符读取
		while ((data = fileReader.read()) != -1) {
				System.out.print((char) data);
		}
 
} catch (IOException e) {
		e.printStackTrace();
} finally {
		try {
				if (fileReader != null) {
						fileReader.close();
				}
		} catch (IOException e) {
				e.printStackTrace();
		}
}
// 字符数组读取文件
 System.out.println("~~~readFile02 ~~~");
String filePath = "e:\\story.txt";
FileReader fileReader = null;
 
int readLen = 0;
char[] buf = new char[8];
//1. 创建FileReader对象
try {
		fileReader = new FileReader(filePath);
		//循环读取 使用read(buf), 返回的是实际读取到的字符数
		//如果返回-1, 说明到文件结束
		while ((readLen = fileReader.read(buf)) != -1) {
				System.out.print(new String(buf, 0, readLen));
		}
} catch (IOException e) {
		e.printStackTrace();
} finally {
		try {
				if (fileReader != null) {
						fileReader.close();
				}
		} catch (IOException e) {
				e.printStackTrace();
		}
}

FileWriter

String filePath = "e:\\note.txt";
FileWriter fileWriter = null;
char[] chars = {'a', 'b', 'c'};
try {
	fileWriter = new FileWriter(filePath);
	fileWriter.write(chars);
}catch (IOException e){
	e.printStackTrace();
} finally{
	try {
		//fileWriter.flush();
		fileWriter.close(); // 也可用 flush() 方法
	} catch (IOException e) {
			e.printStackTrace();
	}
}

注意使用 FileWriter 一定要关闭
在调用了fileWriter.close();后 底层使用了 FileOutputStream的 write()方法,自己debug追源码

节点流和处理流

节点流可以从一个特定的数据源读写数据,如FileReader、FileWriter

Drawing 2025-07-18 17.45.58.excalidraw

⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠ You can decompress Drawing data with the command palette: ‘Decompress current Excalidraw file’. For more info check in plugin settings under ‘Saving’

Excalidraw Data

Text Elements

program

file

节点流

Link to original

处理流(也叫包装流)是“连接”在已存在的流(节点流或处理流)之上,为程序提供更为强大的读写功能,也更加灵活,如BufferedReader、BufferedWriter

Drawing 2025-07-18 17.45.58.excalidraw 1

⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠ You can decompress Drawing data with the command palette: ‘Decompress current Excalidraw file’. For more info check in plugin settings under ‘Saving’

Excalidraw Data

Text Elements

program

文件/
数组/
数据源…

处理流

通过一个类的包装来同时支持这些类型

BufferedReader、BufferedWriter

Link to original

  • 节点流是底层流/低级流,直接跟数据源相接。
  • 处理流(包装流)包装节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入输出。
// 源码
public class BufferedReader extends Reader{
	private Reader in;
	// ...
}
public class BufferedWriter extends Writer{
	private Writer out;
	// ...
}
  • 处理流(也叫包装流)对节点流进行包装,使用了修饰器设计模式,不会直接与数据源相连
    模拟一下:
public abstract class Reader { //抽象类
    public void readFile() {}
    public void readString() {}
    //在Reader_ 抽象类,使用read方法统一管理.
    //后面在调用时,利于对象动态绑定机制, 绑定到对应的实现子类即可.
    //public abstract void read();
}
 
public class StringReader extends Reader {
    public void readString() {
        System.out.println("读取字符串..");
    }
}
 
public class FileReader extends Reader {
    public void readFile() {
        System.out.println("对文件进行读取...");
    }
}
 
public class BufferedReader extends Reader{
 
    private Reader reader; //属性是 Reader 类型
 
    //接收Reader 子类对象
    public BufferedReader(Reader reader) {
        this.reader = reader;
    }
 
    public void readFile() { //封装一层
        reader.readFile();
    }
 
    //让方法更加灵活, 多次读取文件, 或者加缓冲byte[] ....
    public void readFiles(int num) {
        for(int i = 0; i < num; i++) {
            reader.readFile();
        }
    }
 
    //扩展 readString, 批量处理字符串数据
    public void readStrings(int num) {
        for(int i = 0; i <num; i++) {
            reader.readString();
        }
    }
 
}
 
 public static void main(String[] args) {
        BufferedReader bufferedReader = new BufferedReader(new FileReader());
        bufferedReader.readFiles(10);
        //bufferedReader.readFile();
        //Serializable
        //Externalizable
        //ObjectInputStream
        //ObjectOutputStream
        //这次希望通过 BufferedReader 多次读取字符串
        BufferedReader bufferedReader_2 = new BufferedReader(new StringReader());
        bufferedReader_2.readStrings(5);
    }

节点流

分类字节输入流字节输出流字符输入流字符输出流
抽象基类InputStreamOutputStreamReaderWriter
访问文件FileInputStreamFileOutputStreamFileReaderFileWriter
访问数组ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter
访问管道PipedInputStreamPipedOutputStreamPipedReaderPipedWriter
访问字符串StringReaderStringWriter

处理流

分类字节输入流字节输出流字符输入流字符输出流
缓冲流BufferdInputStreamBufferdOutputStreamBufferdReaderBufferdWriter
转换流InputStreamReaderOutputStreamWriter
对象流ObjectInputStreamObjectOutputStream
抽象基类FilterInputStream
FilterOutputStreamFilterReaderFilterWriter
打印流PrintStreamPrintWriter
推回输入流PushbackInputStreamPushbackReader
特殊流DatalnputStreamDataOutputStream

ObjectInputStream 与 ObjectOutputStream

序列化和反序列化

  1. 序列化就是在保存数据时,保存数据的值和数据类型
  2. 反序列化就是在恢复数据时,恢复数据的值和数据类型
  3. 需要让某个对象支持序列化机制,则必须让其类是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一:

Serializable
Externalizable

对象流
功能:提供了对基本类型或对象类型的序列化和反序列化的方法
ObjectOutputStream 提供 序列化功能
ObjectInputStream 提供 反序列化功能

上代码

class Student implements Serializable { 
// 要求序列化或反序列化的对象,需要是实现 Serializable 接口
    @Getter  
    String name;  
    
    @Getter  
    transient int age;  
    //序列化对象时,默认将里面所有属性都进行序列化,
    //但除了statici或transient修饰的成员
    
    // serialVersionUID序列化的版本号,提高兼容性
    private static final long serialVersionUID = 1L;
    
    public Student(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }  
  
    @Override  
    public String toString() {  
        return "Student{" +  
                "name='" + name + '\'' +  
                ", age=" + age +  
                '}';  
    }  
}

OutputStream 输出对象流到文件“object.dat”

ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream("object.dat")));
oos.writeInt(100);
oos.writeBoolean(true);
oos.writeObject(new Student("rain", 17));
oos.flush();

读取

 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.dat"));
// 读取(反序列化)的顺序需要和你保存数据(序列化)的顺序一致
System.out.println(ois.readInt()); // 100
System.out.println(ois.readBoolean()); // true
Object obj = ois.readObject();
System.out.println("ois.readObject()的运行类型: " + obj.getClass()); // ois.readObject()的运行类型: class com.laohei.io.objectstream.output.Student
System.out.println(obj); // Student{name='rain', age=17}
 
Student student = (Student) obj;
student.getAge(); // 17
 
ois.close();// 关闭外层流即可,底层还会关闭 FileInputStream 流
  • 序列化对象时,要求里面属性的类型也需要实现序列化接口
  • 序列化具备可继承性,也就是如果某类已经实现了序列化,则它的所有子类也已经默认实现了序列化