Bootstrap

【JAVA】IO操作

一、转换流

文件存储的都是二进制

在 Java 的 I/O 体系中,InputStreamReader 和 FileReader 是处理字符输入的重要类,它们的核心作用是将字节流转换为字符流,并解决字符编码问题。以下是详细讲解:

1.InputStreamReader 的作用与原理

⑴. 基本定义

InputStreamReader 是 字节流通向字符流的桥梁,属于转换流(字符流)。

它从字节输入流(如 FileInputStream)中读取字节数据,并按指定的字符集解码为字符。

⑵. 核心功能:解码

解码(Decoding):将字节(原始二进制数据)按照特定字符集(如 UTF-8、GBK)转换为字符。

乱码的产生:如果文件的编码与 InputStreamReader 使用的字符集不一致,会导致解码错误,产生乱码。

示例:文件以 UTF-8 编码保存,但用 new InputStreamReader(stream, "GBK") 读取,导致解码错误。

⑶. 构造方法

InputStreamReader(InputStream in, String charsetName):显式指定字符集(推荐,避免乱码)。

示例:new InputStreamReader(new FileInputStream("file.txt"), "UTF-8")

InputStreamReader(InputStream in):使用平台默认字符集(如 Windows 中文版默认 GBK)。

2.FileReader 的本质:创建FileReader的时候,没有指定字符集,默认采用UTF-8的字符集进行解码。

⑴. 简化版的 InputStreamReader

FileReader 是 InputStreamReader 的子类,专用于读取文件。

默认行为:直接绑定文件路径,使用平台默认字符集解码。 以下代码等价:

存在乱码问题

// 写法1:显式使用 InputStreamReader
Reader reader1 = new InputStreamReader(new FileInputStream("file.txt"));

// 写法2:使用 FileReader(本质相同)
Reader reader2 = new FileReader("file.txt");

⑵.Java 版本差异

Java 11 之前:FileReader 无法指定字符集,只能依赖默认字符集。

Java 11+:新增构造函数 FileReader(String path, Charset charset),允许指定字符集。

示例:

// Java 11+ 支持
Reader reader = new FileReader("file.txt", StandardCharsets.UTF_8);

3.解决乱码的关键

⑴. 明确文件的实际编码

在代码中指定与文件编码一致的字符集。 示例:文件为 GBK 编码,应使用:

// Java 11 前
Reader reader = new InputStreamReader(new FileInputStream("file.txt"), "GBK");

// Java 11+
Reader reader = new FileReader("file.txt", StandardCharsets.GBK);

⑵.避免依赖默认字符集

默认字符集因操作系统或环境而异(如 Windows 中文版默认 GBK,Linux 可能默认 UTF-8)。 强制指定字符集可确保跨环境一致性。

4.转换流(Conversion Stream)

在 Java 的 I/O 体系中,转换流(Conversion Stream) 是字节流和字符流之间的桥梁,用于解决字节与字符之间的编码和解码问题。其核心类是 InputStreamReader 和 OutputStreamWriter,属于字符流的一部分,但底层依赖于字节流。

⑴.转换流的作用

①字节流 → 字符流的转换

字节流(如 FileInputStream、SocketInputStream)直接操作原始字节,适合处理二进制文件(如图片、视频)。

字符流(如 FileReader、BufferedReader)操作的是字符(char),适合处理文本文件(如 .txt、.csv)。

转换流:将字节流转换为字符流,并自动处理字符编码问题。

②解决字符编码问题

编码(Encode):将字符转换为字节(如 "你好" → UTF-8 字节 0xE4BDA0E5A5BD)。

解码(Decode):将字节转换为字符(如 0xE4BDA0E5A5BD → "你好")。

乱码根源:编码和解码使用的字符集不一致(如用 GBK 解码 UTF-8 字节)。

⑵.核心转换流类

①InputStreamReader

作用:将字节输入流(InputStream)转换为字符输入流(Reader)。

构造方法:

// 显式指定字符集(推荐)
InputStreamReader(InputStream in, String charsetName);
// 使用平台默认字符集
InputStreamReader(InputStream in);

示例:

try (Reader reader = new InputStreamReader(
        new FileInputStream("file.txt"), StandardCharsets.UTF_8)) {
    // 按 UTF-8 解码读取字符
}

②OutputStreamWriter

作用:将字符输出流(Writer)转换为字节输出流(OutputStream)。

构造方法:

// 显式指定字符集(推荐)
OutputStreamWriter(OutputStream out, String charsetName);
// 使用平台默认字符集
OutputStreamWriter(OutputStream out);

示例

try (Writer writer = new OutputStreamWriter(
        new FileOutputStream("file.txt"), StandardCharsets.UTF_8)) {
    writer.write("你好"); // 按 UTF-8 编码写入字节
}
/**
 * OutputStreamWriter也是一个字符流。也是一个转换流。
 * OutputStreamWriter是一个编码的过程。
 * 如果OutputStreamWriter在编码的过程中使用的字符集和文件的字符集不一致时会出现乱码。
 *
 * FileWriter是OutputStreamWriter的子类。
 * FileWriter的出现简化了java代码。
 * FileWriter是一个包装流,不是节点流。
 */
public class OutputStreamWriterEncodingTest {
    public static void main(String[] args) throws Exception{

        // 创建转换流对象OutputStreamWriter
        // 以下代码采用的是UTF-8的字符集进行编码。(采用平台默认的字符集)
        // 注意:以下代码中输出流以覆盖的方式输出/写。
        //OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("C:\\Users\\86178\\Desktop\\2024\\test3.txt"));

        //OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("C:\\Users\\86178\\Desktop\\2024\\test3.txt"), "GBK");

        //OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream("C:\\Users\\86178\\Desktop\\2024\\test3.txt", true), "GBK");

        /*OutputStreamWriter osw = new OutputStreamWriter(
                new FileOutputStream("C:\\Users\\86178\\Desktop\\2024\\test3.txt", true));*/

        /*OutputStreamWriter osw = new OutputStreamWriter(
                new FileOutputStream("C:\\Users\\86178\\Desktop\\2024\\test3.txt", true), "GBK");*/

        FileWriter osw = new FileWriter("C:\\Users\\86178\\Desktop\\2024\\test3.txt", Charset.forName("UTF-8"), true);

        // 开始写
        osw.write("来学Java");

        osw.flush();;
        osw.close();
    }
}

⑶.转换流的本质

① 底层依赖字节流

InputStreamReader 内部封装了一个 InputStream,通过 CharsetDecoder 将字节解码为字符。

OutputStreamWriter 内部封装了一个 OutputStream,通过 CharsetEncoder 将字符编码为字节。

②字符集(Charset)的核心作用

支持的字符集:UTF-8、GBK、ISO-8859-1 等。

默认字符集:通过 Charset.defaultCharset() 获取(依赖操作系统或 JVM 配置)。

关键原则:必须保证输入/输出字符集一致,否则会乱码。

二、数据流   DataOutputStream/DataInputStream

这两个流都是包装流,读写数据专用的流。

DataOutputStream直接将程序中的数据写入文件,不需要转码,效率高。程序中是什么样子,原封不动的写出去。写完后,文件是打不开的。即使打开也是乱码,文件中直接存储的是二进制。

使用DataOutputStream写的文件,只能使用DataInputStream去读取。并且读取的顺序需要和写入的顺序一致,这样才能保证数据恢复原样。

1.构造方法:开启读写之旅

这两个是配合使用的

  • DataInputStream(InputStream in):这个构造方法需要传入一个字节输入流 InputStream,就像是给 DataInputStream 找了个 “源头”,让它知道从哪里获取数据进行读取。
  • DataOutputStream(OutputStream out):与之对应,这个构造方法接收一个字节输出流 OutputStream,为 DataOutputStream 指明了数据输出的 “目的地”。
DataOutputStreamTest类:
import java.io.DataOutputStream;
import java.io.FileOutputStream;

/**
 * java.io.DataOutputStream:数据流(数据字节输出流)
 * 作用:将java程序中的数据直接写入到文件,写到文件中就是二进制。
 * DataOutputStream写的效率很高,原因是:写的过程不需要转码。
 * DataOutputStream写到文件中的数据,只能由DataInputStream来读取。
 */
public class DataOutputStreamTest {
    public static void main(String[] args) throws Exception{
        // 节点流
        //OutputStream os = new FileOutputStream("data");
        // 包装流
        //DataOutputStream dos = new DataOutputStream(os);

        DataOutputStream dos = new DataOutputStream(new FileOutputStream("data"));

        // 准备数据
        byte b = -127;
        short s = 32767;
        int i = 2147483647;
        long l = 1111111111L;
        float f = 3.0F;
        double d = 3.14;
        boolean flag = false;
        char c = '国';
        String str = "快乐星球";

        // 开始写
        dos.writeByte(b);
        dos.writeShort(s);
        dos.writeInt(i);
        dos.writeLong(l);
        dos.writeFloat(f);
        dos.writeDouble(d);
        dos.writeBoolean(flag);
        dos.writeChar(c);
        dos.writeUTF(str);

        dos.flush();
        dos.close();
    }
}
DataInputStreamTest类;
import java.io.DataInputStream;
import java.io.FileInputStream;

/**
 * java.io.DataInputStream:数据流(数据字节输入流)
 * 作用:专门用来读取使用DataOutputStream流写入的文件。
 * 注意:读取的顺序要和写入的顺序一致。(要不然无法恢复原样。)
 */
public class DataInputStreamTest {
    public static void main(String[] args) throws Exception{
        // 创建数据字节输入流对象
        DataInputStream dis = new DataInputStream(new FileInputStream("data"));

        //System.out.println(dis.readBoolean());

        // 开始读
        byte b = dis.readByte();
        short s = dis.readShort();
        int i = dis.readInt();
        long l = dis.readLong();
        float f = dis.readFloat();
        double d = dis.readDouble();
        boolean flag = dis.readBoolean();
        char c = dis.readChar();
        String str = dis.readUTF();

        System.out.println(b);
        System.out.println(s);
        System.out.println(i);
        System.out.println(l);
        System.out.println(f);
        System.out.println(d);
        System.out.println(flag);
        System.out.println(c);
        System.out.println(str);

        // 关闭流
        dis.close();

        /*FileInputStream fis = new FileInputStream("data");

        System.out.println(fis.read());
        System.out.println(fis.read());
        System.out.println(fis.read());
        System.out.println(fis.read());

        fis.close();*/


    }
}

运行结果:

2.丰富方法:

写入方法:

writeByte():能写入一个字节数据,小巧玲珑的数据它能轻松搞定。 writeShort():负责写入短整型数据,为特定数据类型的存储提供支持。 以此类推,writeInt()、writeLong()、writeFloat()、writeDouble()、writeBoolean()、writeChar()、writeUTF(String) 分别对应不同数据类型的写入,从整数到浮点数,从布尔值到字符串,涵盖全面。

读取方法:

readByte():读取字节数据,与写入方法一一对应。 其他 readShort()、readInt() 等读取方法也都各司其职,按照写入的顺序和数据类型,准确地从文件中把数据读出来。

三、对象流:ObjectOutputStream/ObjectInputStream

Java 序列化与反序列化详解

Java 的 ObjectOutputStream 和 ObjectInputStream 是实现对象序列化与反序列化的核心类。以下是其工作原理、关键机制及最佳实践的详细说明。

1.序列化与反序列化概念

⑴.序列化(Serialization)

定义:将 Java 对象转换为字节序列,便于存储到文件或通过网络传输。

工具类:ObjectOutputStream。

⑵.反序列化(Deserialization)

定义:将字节序列恢复为 Java 对象。

工具类:ObjectInputStream。

2.核心要求:实现 Serializable 接口

⑴.标记接口

类必须实现 java.io.Serializable 接口(无方法,仅作标记)。

标志接口的特点:一个方法也没有

示例:

public class User implements Serializable {  
    private String name;  
    private transient String password; // 不参与序列化  
    // 显式声明序列化版本号  
    private static final long serialVersionUID = 1L;  
}  

⑵.未实现的后果

若尝试序列化未实现 Serializable 的对象,抛出 NotSerializableException。


3.序列化版本号 serialVersionUID

⑴.作用

版本一致性校验:JVM 通过对比序列化前后的 serialVersionUID 判断类是否兼容。

不显式声明的风险: 若类结构变化(如新增字段),编译器自动生成的新 serialVersionUID 会导致反序列化失败。

⑵.显式声明方式

使用 private static final long 类型,并通过 @Serial 注解辅助生成或校验。

示例:

@Serial  
private static final long serialVersionUID = 123456789L;  

4.transient 关键字

作用

标记不参与序列化的字段(如敏感信息或临时状态)。

反序列化结果:

数值类型 → 0 或 0.0。

对象类型 → null。

示例:

public class User implements Serializable {  
    private String name;  
    private transient String password; // 序列化时忽略  
}  

5.序列化与反序列化操作

⑴.序列化步骤

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.util.Date;

/**
 * java.io.ObjectOutputStream:对象流(对象字节输出流)
 * 1. 它的作用是完成对象的序列化过程。
 * 2. 它可以将JVM当中的Java对象序列化到文件中/网络中。
 * 3. 序列化:将Java对象转换为字节序列的过程。(字节序列可以在网络中传输。)
 * 4. 序列化:serial
 */
public class ObjectOutputStreamTest {
    public static void main(String[] args) throws Exception{
        // 创建“对象字节输出流”对象
        // 包装流
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object"));

        // 准备一个Java对象
        Date nowTime = new Date();

        // 序列化 serial
        oos.writeObject(nowTime);

        // 刷新
        oos.flush();

        // 关闭
        oos.close();
    }
}

运行结果:

⑵.反序列化步骤

import java.io.FileInputStream;
import java.io.ObjectInputStream;

/**
 * java.io.ObjectInputStream:对象流(对象字节输入流)
 * 1. 专门完成反序列化的。(将字节序列转换成JVM当中的java对象。)
 */
public class ObjectInputStreamTest {
    public static void main(String[] args) throws Exception{
        // 包装流
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object"));

        // 读
        Object o = ois.readObject();

        System.out.println(o);

        // 关闭
        ois.close();
    }
}

运行结果:

6.序列化对象如果是多个对象的话,一般会序列化一个集合

⑴.序列化步骤

import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

/**
 * 序列化对象如果是多个对象的话,一般会序列化一个集合。
 */
public class ObjectOutputStreamTest02 {
    public static void main(String[] args) throws Exception {

        Date date1 = new Date();
        Date date2 = new Date();
        Date date3 = new Date();
        Date date4 = new Date();
        Date date5 = new Date();
        Date date6 = new Date();

        List<Date> list = new ArrayList<>();

        list.add(date1);
        list.add(date2);
        list.add(date3);
        list.add(date4);
        list.add(date5);
        list.add(date6);

        // 序列化
        ObjectOutputStream dos = new ObjectOutputStream(new FileOutputStream("dates"));
        dos.writeObject(list);
        dos.flush();
        dos.close();
    }
}

运行结果:

⑵.反序列化步骤

/**
 * 反序列化
 */
public class ObjectInputStreamTest02 {
    public static void main(String[] args) throws Exception{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("dates"));

        // 反序列化
        List<Date> dates = (List<Date>)ois.readObject();

        for(Date date : dates){
            System.out.println(date);
        }

        // 关闭
        ois.close();
    }
}

运行结果:

7.序列化和反序列化自定义类型

⑴.如果没有实现Serializable接口会报错

Student类:

public class Student {
    private String name;
    private int age;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public Student() {
    }

    public Student(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;
    }
}
ObjectOutputStreamTest03类:
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

/**
 * 序列化Student对象
 */
public class ObjectOutputStreamTest03 {
    public static void main(String[] args) throws Exception{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student"));

        Student stu = new Student("zhangsan", 20);

        oos.writeObject(stu);

        oos.flush();

        oos.close();
    }
}

运行结果:报错

⑵.实现Serializable接口

 Student类:

public class Student implements Serializable{
    private String name;
    private int age;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public Student() {
    }

    public Student(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;
    }
}
序列化步骤
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;

/**
 * 序列化Student对象
 */
public class ObjectOutputStreamTest03 {
    public static void main(String[] args) throws Exception{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student"));

        Student stu = new Student("zhangsan", 20);

        oos.writeObject(stu);

        oos.flush();

        oos.close();
    }
}

运行结果:

反序列化步骤

import java.io.FileInputStream;
import java.io.ObjectInputStream;

/**
 * 反序列化过程:将文件中的Student字节序列恢复到内存中,变成Student对象。
 */
public class ObjectInputStreamTest03 {

    public static void main(String[] args) throws Exception{

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student"));

        System.out.println(ois.readObject());

        ois.close();
    }
}

运行结果:

8.深入剖析序列化版本号

实现了Serializable接口的类,编译器会自动给该类添加序列化版本号的属性:serialVersionUID

在java中,是通过“类名 + 序列化版本号”来进行类的区分的。

serialVersionUID实际上是一种安全机制。

如果改代码了就不是之前的类,加了序列化号就可以认出是同一个

在反序列化的时候,JVM会去检查存储Java对象的文件中的class的序列化版本号是否和当前Java程序中的class的序列化版本号是否一致。如果一致则可以反序列化。如果不一致则报错。

如果一个类实现了Serializable接口,还是建议将序列化版本号固定死,建议显示的定义出来,原因是:类有可能在开发中升级(改动),升级后会重新编译,如果没有固定死,编译器会重新分配一个新的序列化版本号,导致之前序列化的对象无法反序列化。显示定义序列化版本号的语法:private static final long serialVersionUID = XXL;

为了保证显示定义的序列化版本号不会写错,建议使用 @java.io.Serial 注解进行标注。并且使用它还可以帮助我们随机生成序列化版本号。

加了序列化版本号的Student类:

import java.io.Serializable;
import java.io.Serial;

/**
 * 1. 重点:凡是参与序列化和反序列化的对象必须实现 java.io.Serializable 可序列化的接口。
 * 2. 这个接口是一个标志接口,没有任何方法。只是起到一个标记的作用。
 * 3. 它到底是标记什么呢??????
 * 4. 当java程序中类实现了Serializable接口,编译器会自动给该类添加一个“序列化版本号”。
 *      序列化版本号:serialVersionUID
 * 5. 序列化版本号有什么用?
 *      在Java语言中是如何区分class版本的?
 *      首先通过类的名字,然后再通过序列化版本号进行区分的。
 *      在java语言中,不能仅仅通过一个类的名字来进行类的区分,这样太危险了。
 * 6. 为了保证序列化的安全,只有同一个class才能进行序列化和反序列化。在java中是如何保证同一个class的?
 *      类名 + 序列化版本号:serialVersionUID
 *
 * java.io.InvalidClassException: com.powernode.javase.io.Student;
 * local class incompatible:
 *      stream classdesc serialVersionUID = -4936871645261081394,  (三年前的学生对象,是三年前的Student.class创建的学生对象。)
 *      local class serialVersionUID = 5009257763737485728  (三年后,Student.class升级了。导致了版本发生了变化。)
 */
public class Student implements Serializable {

    // 建议:不是必须的。
    // 如果你确定这个类确实还是以前的那个类。类本身是合法的。没有问题。
    // 建议你将序列化版本号写死!
    @Serial
    private static final long serialVersionUID = -7005027670916214239L;

    private String name;
    private transient int age; // transient关键字修饰的属性不会参与序列化。

    private String addr;

    public String getAddr() {
        return addr;
    }

    public void setAddr(String addr) {
        this.addr = addr;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public Student() {
    }

    public Student(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;
    }
}

9.版本兼容性问题

  1. 场景

    • 修改类结构(如新增字段)后,若未更新 serialVersionUID,反序列化可能失败。

  2. 错误示例

// 原类  
public class User implements Serializable {  
    private String name;  
    private static final long serialVersionUID = 1L;  
}  

// 修改后(新增字段)  
public class User implements Serializable {  
    private String name;  
    private int age;           // 新增字段  
    private static final long serialVersionUID = 1L; // 保持原值  
}  

// 反序列化旧数据 → 成功(serialVersionUID 一致)  

10.不参与序列化的属性需要使用瞬时关键字修饰:transient

transient关键字修饰的属性不会参与序列化

Student类:

import java.io.Serial;
import java.io.Serializable;

/**
 * 1. 重点:凡是参与序列化和反序列化的对象必须实现 java.io.Serializable 可序列化的接口。
 * 2. 这个接口是一个标志接口,没有任何方法。只是起到一个标记的作用。
 * 3. 它到底是标记什么呢??????
 * 4. 当java程序中类实现了Serializable接口,编译器会自动给该类添加一个“序列化版本号”。
 *      序列化版本号:serialVersionUID
 * 5. 序列化版本号有什么用?
 *      在Java语言中是如何区分class版本的?
 *      首先通过类的名字,然后再通过序列化版本号进行区分的。
 *      在java语言中,不能仅仅通过一个类的名字来进行类的区分,这样太危险了。
 * 6. 为了保证序列化的安全,只有同一个class才能进行序列化和反序列化。在java中是如何保证同一个class的?
 *      类名 + 序列化版本号:serialVersionUID
 *
 * java.io.InvalidClassException: com.powernode.javase.io.Student;
 * local class incompatible:
 *      stream classdesc serialVersionUID = -4936871645261081394,  (三年前的学生对象,是三年前的Student.class创建的学生对象。)
 *      local class serialVersionUID = 5009257763737485728  (三年后,Student.class升级了。导致了版本发生了变化。)
 */
public class Student implements Serializable {

    // 建议:不是必须的。
    // 如果你确定这个类确实还是以前的那个类。类本身是合法的。没有问题。
    // 建议你将序列化版本号写死!
    @Serial
    private static final long serialVersionUID = -7005027670916214239L;

    private String name;
    private transient int age; // transient关键字修饰的属性不会参与序列化。

    private String addr;

    public String getAddr() {
        return addr;
    }

    public void setAddr(String addr) {
        this.addr = addr;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    public Student() {
    }

    public Student(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;
    }
}

序列化:

/**
 * 序列化
 */
public class ObjectOutputStreamTest04 {
    public static void main(String[] args) throws Exception{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student2"));

        Student student = new Student("张三", 20);
        // 序列化
        oos.writeObject(student);

        // 其实ObjectOutputStream中也有这些方法,和DataOutputStream中的方法一样。
        /*oos.writeInt(100);
        oos.writeBoolean(false);
        oos.writeUTF("张三");*/

        oos.flush();
        oos.close();
    }
}

反序列化:


import java.io.FileInputStream;
import java.io.ObjectInputStream;

/**
 * 反序列化
 */
public class ObjectInputStreamTest04 {
    public static void main(String[] args) throws Exception{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("student2"));
        Object o = ois.readObject();
        System.out.println(o);
        ois.close();
    }
}

四、打印流

Java 打印流详解:PrintStream 与 PrintWriter

Java 提供了 PrintStream 和 PrintWriter 两个类用于便捷的格式化输出。尽管功能相似,但它们在底层实现(字节流 vs 字符流)、编码处理和灵活性上有显著区别。以下是两者的详细对比与使用指南。

1.PrintStream(字节打印流)

⑴. 核心特点

继承关系:OutputStream 的子类,操作字节数据。

编码处理:使用平台默认字符集(如 UTF-8、GBK)将字符转换为字节。

自动刷新:可通过构造函数启用自动刷新(如调用 println 时触发)。

异常处理:方法不抛出 IOException,通过 checkError() 检查错误状态。

⑵. 构造方法

// 默认不自动刷新
PrintStream ps1 = new PrintStream("output.txt");

// 启用自动刷新(autoFlush=true)
PrintStream ps2 = new PrintStream(new FileOutputStream("output.txt"), true);

// 指定字符编码(如 UTF-8)
PrintStream ps3 = new PrintStream("output.txt", "UTF-8");

⑶.常用方法

方法说明
print(boolean/int/...)输出基本类型、字符串或对象(调用 toString())。
println(boolean/int/...)输出内容后自动换行,若启用自动刷新则触发缓冲区刷新。
printf(String format, ...)格式化输出(同 String.format())。
checkError()检查流是否发生错误(如写入失败)。

便捷在哪里?

直接输出各种数据类型

自动刷新和自动换行(println方法)

支持字符串转义

自动编码(自动根据环境选择合适的编码方式)

格式化输出?调用printf方法。

%s 表示字符串

%d 表示整数

%f 表示小数(%.2f 这个格式就代表保留两位小数的数字。)

%c 表示字符

import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.PrintStream;

/**
 * 1. java.io.PrintStream:打印流(专业的负责打印的流,字节形式。)
 * 2. PrintStream不需要手动刷新,自动刷新。
 */
public class PrintStreamTest {
    public static void main(String[] args) throws Exception{
        // 创建一个打印流对象
        // 构造方法:PrintStream(OutputStream out)
        // 构造方法:PrintStream(String fileName)
        PrintStream ps = new PrintStream("log1");

        // 没有这样的构造方法。
        //PrintStream ps2 = new PrintStream(new FileWriter(""));

        //PrintStream ps2 = new PrintStream(new FileOutputStream("log1"));

        // 打印流可以打印各种数据类型数据。
        ps.print(100);
        ps.println(false);
        ps.println("abc");
        ps.println('T');
        ps.println(3.14);
        ps.println("hell world");
        ps.println(200);

        ps.println("\"hello world!\"");

        String name = "张三";
        double score = 95.5;

        ps.printf("姓名:%s,考试成绩:%.2f", name, score);

        // 关闭
        ps.close();
    }
}

输出结果:

2.PrintWriter(字符打印流)

⑴. 核心特点

继承关系:Writer 的子类,操作字符数据。

编码处理:需显式指定字符集(如 UTF-8),否则使用平台默认。

自动刷新:需通过构造函数启用自动刷新(如 new PrintWriter(file, true))。

灵活性:可接受 Writer 或 OutputStream 作为输出目标。

异常处理:方法可能抛出 IOException,需显式捕获。

⑵. 构造方法

// 包装文件(默认不自动刷新)
PrintWriter pw1 = new PrintWriter("output.txt");

// 包装 OutputStream 并启用自动刷新
PrintWriter pw2 = new PrintWriter(new FileOutputStream("output.txt"), true);

// 包装 Writer 并指定字符集
PrintWriter pw3 = new PrintWriter(
    new OutputStreamWriter(new FileOutputStream("output.txt"), "UTF-8")
);

⑶.常用方法

方法说明
print(boolean/int/...)同 PrintStream,输出内容。
println(boolean/int/...)输出内容并换行,若启用自动刷新则刷新缓冲区。
printf(String format, ...)格式化输出(语法与 PrintStream 一致)。
flush()手动刷新缓冲区(若未启用自动刷新)。

import java.io.FileWriter;
import java.io.PrintWriter;

/**
 * java.io.PrintWriter:专门负责打印的流。(字符形式)
 * 需要手动刷新flush。
 * PrintWriter比PrintStream多一个构造方法:
 *      PrintStream构造方法:
 *          PrintStream(OutputStream)
 *      PrintWriter构造方法:
 *          PrintWriter(OutputStream)
 *          PrintWriter(Writer)
 */
public class PrintWriterTest {
    public static void main(String[] args) throws Exception{
        // 创建字符打印流
        //PrintWriter pw = new PrintWriter(new FileOutputStream("log2"));

        PrintWriter pw = new PrintWriter(new FileWriter("log2"), true);

        // 打印
        pw.println("world hello!!!");
        pw.println("zhangsan hello!!!");

        // 刷新
        //pw.flush();

        // 关闭
        pw.close();
    }
}

运行结果:

五、标准输入流&标准输出流

Java 标准输入输出流详解

Java 的标准输入输出流是程序与外界交互的基础通道,默认指向控制台,但可通过代码重定向到其他设备(如文件)。以下是对 System.in 和 System.out 的详细说明及使用指南。

1.标准输入流 System.in

⑴. 核心特性

类型:InputStream(字节输入流)。

默认来源:控制台(键盘输入)。

管理:由 JVM 自动管理,无需手动关闭。

用途:读取用户输入或重定向后的输入源(如文件)。

⑵. 直接读取控制台输入

// 直接读取字节(返回 ASCII 码)
int data = System.in.read(); 
// 需循环读取并转换字节为字符(不推荐)

⑶.使用 BufferedReader 包装(字符流)

try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
    System.out.print("请输入内容:");
    String input = br.readLine(); // 读取一行文本
    System.out.println("输入内容:" + input);
} catch (IOException e) {
    e.printStackTrace();
}

优点:支持按行读取,避免处理字节细节。

缺点:需处理 IOException,且需手动转换数据类型。

⑷. 使用 Scanner 简化输入

Scanner scanner = new Scanner(System.in);
System.out.print("请输入整数:");
int num = scanner.nextInt();    // 直接读取整数
System.out.print("请输入字符串:");
String str = scanner.nextLine(); // 读取字符串
scanner.close();

优点: 自动解析输入为指定类型(如 nextInt()、nextDouble())。

            无需处理字节或字符转换。

缺点:需注意输入格式匹配(如输入非数字时抛出 InputMismatchException)。

2.标准输出流 System.out

⑴. 核心特性

类型:PrintStream(字节打印流)。

默认目标:控制台(命令行窗口)。

管理:由 JVM 自动管理,无需手动关闭或刷新。

特性:

支持自动刷新(如 println 方法触发)。

提供 print()、println()、printf() 等便捷方法。

⑵. 基本输出示例

System.out.print("Hello, ");    // 不换行
System.out.println("World!");   // 换行输出
System.out.printf("PI: %.2f", 3.1415); // 格式化输出:PI: 3.14

3.标准错误流 System.err

类型:PrintStream,与 System.out 类似。

默认目标:控制台(通常为红色高亮显示)。

用途:输出错误信息(与正常输出分离)。

4.代码示例:

⑴.标准输入流

System.in获取到的InputStream就是一个标准输入流。

标准输入流是用来接收用户在控制台上的输入的。(普通的输入流,是获得文件或网络中的数据)

标准输入流不需要关闭。(它是一个系统级的全局的流,JVM负责最后的关闭。)

/**
 * 标准输入流:System.in
 *      1. 标准输入流怎么获取?
 *          System.in
 *      2. 标准输入流是从哪个数据源读取数据的?
 *          控制台。
 *      3. 普通输入流是从哪个数据源读取数据的?
 *          文件或者网络或者其他.....
 *      4. 标准输入流是一个全局的输入流,不需要手动关闭。JVM退出的时候,JVM会负责关闭这个流。
 */
public class SystemInTest {
    public static void main(String[] args) throws Exception{

        // 获取标准输入流对象。(直接通过系统类System中的in属性来获取标准输入流对象。)
        InputStream in = System.in;

        // 开始读
        byte[] bytes = new byte[1024];
        int readCount = in.read(bytes);

        for (int i = 0; i < readCount; i++) {
            System.out.println(bytes[i]);
        }
    }
}

10为换行符

对于标准输入流来说,也是可以改变数据源的。不让其从控制台读数据。也可以让其从文件中或网络中读取数据。

当然,你也可以修改输入流的方向(System.setIn())。让其指向文件。

import java.io.FileInputStream;
import java.io.InputStream;

/**
 * 对于标准输入流来说,也是可以改变数据源的。不让其从控制台读数据。也可以让其从文件中或网络中读取数据。
 */
public class SystemInTest02 {
    public static void main(String[] args) throws Exception{
        // 修改标准输入流的数据源。
        System.setIn(new FileInputStream("log2"));

        // 获取标准输入流
        InputStream in = System.in;

        byte[] bytes = new byte[1024];
        int readCount = 0;
        while((readCount = in.read(bytes)) != -1){
            System.out.print(new String(bytes, 0, readCount));
        }

    }
}

运行结果:

也可以使用BufferedReader对标准输入流进行包装。这样可以方便的接收用户在控制台上的输入。(这种方式太麻烦了,因此JDK中提供了更好用的Scanner。)

BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

String s = br.readLine();

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

/**
 * 使用BufferedReader去包装一下这个标准输入流,来完成从键盘接收用户的输入。
 */
public class SystemInTest03 {
    public static void main(String[] args) throws Exception{

        // 创建BufferedReader对象
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));//这样可以读取一行

        /*InputStream in = System.in;
        Reader reader = new InputStreamReader(in);
        BufferedReader br = new BufferedReader(reader);*/

        String s = null;
        while((s = br.readLine()) != null){
            if("exit".equals(s)){
                break;
            }
            System.out.println("您输入了:" + s);
        }

        /*Scanner scanner = new Scanner(System.in);

        String name = scanner.next();
        System.out.println("您的姓名是:" + name);*/

    }
}

运行结果:

⑵.标准输出流

System.out获取到的PrintStream就是一个标准输出流。

标准输出流是用来向控制台上输出的。(普通的输出流,是向文件和网络等输出的。)

标准输出流不需要关闭(它是一个系统级的全局的流,JVM负责最后的关闭。)也不需要手动刷新。

当然,你也可以修改输出流的方向(System.setOut())。让其指向文件。

import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * 标准输出流:System.out
 *      1. 标准输出流怎么获取?
 *          System.out
 *      2. 标准输出流是向哪里输出呢?
 *          控制台。
 *      3. 普通输出流是向哪里输出呢?
 *          文件或者网络或者其他.....
 *      4. 标准输出流是一个全局的输出流,不需要手动关闭。JVM退出的时候,JVM会负责关闭这个流。
 */
public class SystemOutTest {
    public static void main(String[] args) throws Exception {

        // 获取标准输出流,标准输出流默认会向控制台输出。
        PrintStream out = System.out;

        // 输出
        out.println("hello world");
        out.println("hello world");
        out.println("hello world");
        out.println("hello world");
        out.println("hello world");

        // 标准输出流也是可以改变输出方向的。
        System.setOut(new PrintStream("log2"));//打印到文件里

        System.out.println("zhangsan");
        System.out.println("lisi");
        System.out.println("wangwu");
        System.out.println("zhaoliu");

        // 获取系统当前时间
        Date now = new Date();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
        String str = sdf.format(now);
        System.out.println(str + ": SystemOutTest's main method invoked!");
    }
}

运行结果:

;