一、转换流
文件存储的都是二进制
在 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.版本兼容性问题
-
场景
-
修改类结构(如新增字段)后,若未更新
serialVersionUID
,反序列化可能失败。
-
-
错误示例
// 原类
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!");
}
}
运行结果: