🍧 Peach

蜜桃学代码

IO - 输入/输出流⑵_实现类

流体系:

分类 字节输入流 字节输出流 字符输入流 字符输出流
抽象类 InputStream OutputStream Reader Writer
转换类 InputStream-
Reader
OutputStream-
Writer
第一类:数据源类
文件 File-
InputStream
File-
OutputStream
FileReader FilerWriter
数组 ByteArray-
InputStream
ByteArray-
OutputStream
CharArray-
Reader
CharArray-
Writer
对象 Object-
InputStream
Object-
OutputStream
字符串 StringBuffer-
InputStream
StringReader StringWriter
第二类:数据流串联类
管道 Pipe-
InputStream
Pipe-
OutputStream
PipeReader PipeWriter
序列 Sequence-
InputStream
第三类:数据流过滤类
过滤 Filter-
InputStream
Filter-
OutputStream
FilterReader FilterWriter
缓冲 Buffered-
InputStream
Buffered-
OutputStream
Buffered-
Reader
Buffered-
Writer
数据 Data-
InputStream
Data-
OutputStream
行号 LineNumber-
InputStream
LineNumber-
Reader
推回 Pushback-
InputStream
Pushback-
Reader
格式化 PrintStream PrintWriter

(一)文件流

graph LR
T(["文件流
(低级文件流)"]):::p T --> t("可操作文件
中的内容
"):::info t --> A(["按照(读/写)
方向分类"]):::p t --> B(["按照(读/写)
字节数大小分类"]):::b A -.-> |"in(读取)"| a("文件输入流
字节型文件输入流
字符型文件输入流
"):::lp A -.-> |"out(写入)"| a2("文件输出流
字节型文件输出流
字符型文件输出流
"):::lp B -.-> |"1字节"| b("字节型文件流
FileInputStream
FileOUtputStream"):::lb B -.-> |"1字符
(2字节)
"| b2("字符型文件流
FileReader
FileWriter"):::lb classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef g fill:#9ac5bb classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#ddebe6 classDef info fill:#f6f6f7,color:#737379,stroke-dasharray: 3 3, stroke-width: 2px

(1)FileInputStram

字节型文件输入流

* 继承关系

继承于 InputStream 类(字节型输入流)

* 构造方法

构造方法 说明
(没有无参构造方法)
FileInputStram(File file) 通过File对象构造
FileInputStram(String name) 通过文件路径字符串构造

* 常用方法

常用方法 说明 返回值类型
finalize() GC对象回收
available() 返回当前流管道中有多少缓存字节,每次读取一个字节对应的 Unicode 码。(读得到返回 0–65536,读不到返回-1) int
🔺 read() 从输入流中读取单个字节,返回读取的字节数据(Unicode码) int
read(byte[]):将输入流内容写入数组里,返回实际读取的字节数。 int
read(byte[] b, int off, int leng):将输入流内容写入数组里,从off位置开始,放入len个字节(不是从数组起点开始),返回实际读取的字节数。 int
skip() 跳过几个字节读取 long n
close() 关闭流管道(⚠️ 必须要做,最好放在 finally{} 里)
- read()

创建一个字节型文件输入流,读取文件中的内容。

try{
File file = new File("D://Test//test.txt");
FileInputStream fis = new FileInputStram(file);
int i = fis.read(); // ← 每次读取一个字节
while(i!=-1){
System.out.ptinln(i); // → 返回字节对应得Unicode码
i = fis.read();
}
}catch(IOException e){
e.printStackTrace;
}

- read(byte[] b)

读取文件内容,装入数组内。返回读取的有效字符个数。

  try{
FileInputStream fis = new FileInputStram("D://Test//test.txt");
byte[] b = new bytep[5];
int count = fis.read(b); // ← 读取文件内容,装进数组里
while(count!=-1){ // → 返回读取到的有效字节个数
// 🔸 String value = new String(b);
String value = new String(b, 0, count); // 使用偏移量构造字节型数组字符串
System.out.ptint(value); //
count = fis.read(b);
}
}catch(IOException e){
e.printStackTrace;
}

/* 文件内容:
* abcdefg
* hijklmn
* opq
*/

/* 🔸 直接使用String(b)读取内容:
* a b c d e
* f g \r \n h
* i j k l m
* n \r \n o p
* q \r \n o p <—— 到q结束,但是之前的没有被替换掉
- skip()
try {
FileInputStream fis = new FileInputStream("D://Test//test.txt");
fis.skip(2);
int code = fis.read(); // c
System.out.println((char)code);
} catch (IOException e) {
e.printStackTrace();
}
- close()
try{
File file = new File("D://Test//test.txt");
FileOutStream fos = new FileOutStram(file);
fos.write(97); // 写入code码
fos.flush(); // 刷新 将管道中的字节推入文件中
}catch(IOException e){
e.printStackTrace;
}

(2)FileOutputStream

字节型文件输出流

* 继承关系

继承于 OutputStream

* 构造方法

构造方法 说明
(没有无参构造方法)
FileOutputStram(File file) 利用File对象构造(默认覆盖原文)
FileOutputStram(File file, Boolean append) 利用File对象构造、是否将内容追加到文件末尾(true:不覆盖)
FileOutputStram(String name) 通过文件路径字符串构造
FileOutputStram(String name, Boolean append) 通过文件路径构造、是否追加到文件末尾

* 常用方法

常用方法 说明
*finalize()* GC对象回收
write() 写入到文件(与read()区别)
write(int code):将给定code写入文件 (’=’字符char也行,会自动转换)
write(byte[]): 将数组内容写入文件流里(String—getByte())
flush() 刷新:将管道内字节推入文件
close() * 注意:在finally中关闭
- write()
try{
File file = new File("D://Test//test.txt");
FileOutStream fos = new FileOutStram(file);
fos.write(97); // 写入code码
fos.flush(); // 刷新 将管道中的字节推入文件中
}
}catch(IOException e){
e.printStackTrace;
}

(3)FileReader

字符型文件输入流

* 继承关系

继承于InputStreamReader(字节型输入流)

* 构造方法

构造方法 说明
(没有无参构造方法)
FileReader(File file) 通过File对象构造(默认覆盖原文)
FIleReader(String fileName) 通过文件路径字符串构造
FileReader(FileDescriptor fd) 通过文件描述符构建
文件描述符的有效范围是 0 到 OPEN_MAX

* 常用方法

常用方法 说明
🔺 read() 从输入流中读取单个字符,返回所读取的字符数据
int read(char[] cbuf) 从输入流中最多读取cbuf.length个字符的数据,并将其存储在字符数组cbuf中,返回实际读取的字符数。
int read(char[] cbuf, int off, int len) 从输入流中最多读取len个字符的数据,并将其存储在字符数组cbuf中,从off位置开始存储(并不是从数组起点开始),返回实际读取的字符数。

(4)FileWriter

字符型文件输出流

* 继承关系

继承于 OutputStreamWriter(字节型输出流)

* 构造方法

构造方法 说明
(没有无参构造方法)
FileWriter(File file) 利用File对象构建(默认覆盖原文)
FileWriter(File file, Boolean boolean) 利用File对象构造、是否将内容追加到文件末尾(true:不覆盖)
FileWriter(String name) 通过文件路径字符串构造
FileWriter(String name, Boolean boolean) 通过文件路径字符串构造、是否追加到文件末尾

* 常用方法

常用方法 说明
write() 读取
write(char)
write(String)
- write()
String str = "abc";
char[] c = str.toCharArray();
FileWriter fw = new FileWriter(file);
fw.write(97);
fw.write(c);

(5)Demo

① 文件的复制

import java.io.*;

public class OperateFile {

public void copyFile(File file, String path){
try {
// 🔻 0 创建对象
FileInputStream fis = new FileInputStream(new File("D://test.txt"));
File newFile = new File(path + "//" + file.gsetName());
FileOutputStream fos = new FileOutputStream(newFile);

// 🔻 1 读取文件
byte[] b = new byte[1024]; // 通常创建的数组:1kb-8kb之间
int count = fis.read(b); // count记录读取的有效字节数,读取到byte数组b里

// 🔻 2 写入文件
while(count!=-1){ // 当能读取到内容时

// 🔸(做点手脚)
fos.write(b, 0, count); // 写入有效字节数
fos.flush();
count = fis.read(b); // 最后再次读取,读取不到内容时,count=-1时退出while循环
}
System.out.println("复制完毕!");

} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭
}
}

public static void main(String[] args) {
OperateFile of = new OperateFile();
of.copyFile(new File("D://test.txt"), "D://test");
}
}

② 文件的加密

// 读取文件 -> 做点手脚 -> 写入文件
byte temp = b[0];
b[0] = b[1];
b[1] = temp;

③ 文件夹的复制

public void superCopyFile(File file, String newPath){

String oldFilePath = file.getAbsolutePath();
String newFilePath = newPath + oldFilePath.split(":")[1];

File[] files = file.listFiles(); // 获取当前传递进来的File对象的所有子元素
File newFile = new File(newFilePath);

// 判断当前传递进来的file是个文件还是个文件夹 (①isFile isDirectory ②listFile)
if(files!=null){ // 🔻 1. 文件夹(才能构成文件数组对象)
newFile.mkdir(); // 通过新的file对象操作 在硬盘上创建一个文件夹
if(files.length!=0){ // 🔻 3. 文件夹内有文件
for(File f:files){
this.superCopyFile(f, newPath); // 👈 使用递归
}
}
System.out.println(newFile.getName() + "文件夹复制完成!");

} else { // 🔻 2. 文件 (没有子文件了)

FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(new File(oldFilePath));
fos = new FileOutputStream(newFile);
byte[] b = new byte[1024];
int count = fis.read(b);
while(count!=-1){
fos.write(b,0,count);
fos.flush();
count = fis.read(b);
}
System.out.println(newFile.getName() + "文件复制完成!");

} catch (IOException e) {
e.printStackTrace();
} finally {
if(fis!=null){
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (fos!=null){
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}


}
}


(二)对象流

(1)对象序列化

%%{
  init: {
    'themeVariables': {
       'fontSize': '12px'
     }
  }
}%%
graph LR
subgraph "应用层1"
A("对象"):::lb
end

subgraph "应用层2"
B("对象"):::lb
end

subgraph "网络协议"
C("二进制流"):::p
end

A --> |"[ 转换 ]
ObjectOutputStream
FileOutputStream
"|C C --> |"[ 还原 ]
ObjectInputStream
FileInputStream
"|B style 网络协议 fill:#f4e4e9, stroke-dasharray: 3 3, stroke-width: 2px classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef g fill:#9ac5bb classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#ddebe6 classDef info fill:#f6f6f7,color:#3f3f3f,stroke-dasharray: 3 3, stroke-width: 1px
  • 序列化:对象的持久存储

    • ObjectOutputStreamObjectInputStream 分别与 FileOutputStreamFileInputStream 一起使用,通过在流中使用文件可以实现对象的持久存储
    • ObjectInputStream:用于恢复那些已经序列化存储的对象。确保从流创建的图形中所有对象的类型与Java虚拟机中显示的类相匹配。
  • 序列化细节:

    1. 读写顺序要一致(对象类型转换异常:java.io.OptionalDataException)
    2. 要求必须实现 SerializableExternalizable 接口
    3. 在序列化类中添加 SerialVersionUID,提高版本兼容性
    4. 序列化对象时默认所有属性都序列化,除 statictransit
    5. 序列化可继承性,若某类实现序列化,其子类也默认实现序列化

(2)方法

  • readObject()

    readObject():负责读取通过对应的 writeObject() 方法写入的数据,为特定类读取和恢复对象的状态。

    恢复状态:从 ObjectInputStream 读取数据并将其分配给对象的适当字段。

  • 读取各种数据类型的函数

    // 与 ObjectOutputStream write()相对应
    boolean readBoolean(); // 读取一个boolean值
    byte readByte(); // 读取一个8位的字节
    char readChar(); // 读取一个16位的char值
    double readDouble(); // 读取一个 64 位的double值
    float readFloat(); // 读取一个 32 位的float值
    int readInt(); // 读取一个 32 位的int值
    long readLong(); // 读取一个 64 位的long值
    short readShort(); // 读取一个 16 位的short值

    // 从序列化流读取类描述符
    protected ObjectStreamClass readClassDescriptor();
    // 按名称从流中读取持久字段并使其可用
    ObjectInputStream.GetField readFields();
    // 允许子类读取并验证它们自己的流头部
    protected void readStreamHeader();
    // 从 ObjectInputStream 读取“非共享”对象
    Object readUnshared();

(3)示例

① 对象序列化

  • Person 类:

    public class Person implements java.io.Serializable { 
    // 🔹 使用Serializable来实现序列化,让目标类实现Serializable标记接口

    private String name;
    private int age;
    // 注意此处没有提供无参数的构造器:🔻反序列化机制无须通过构造器来初始化Java对象
    public Person(String name , int age) {
    System.out.println("有参数的构造器");
    this.name=name;
    this.age=age;
    }
    // 省略name与age的setter和getter方法
    ...
    }
  • 使用 ObjectOutputStream 将一个Person对象写入磁盘文件:

      public class WriteObject{

    public static void main(String[] args) {
    try(
    // 🔹(1)创建一个ObjectOutputStream输出流
    ObjectOutputStream oos = new ObjectOutputStream(
    new FileOutputStream("object.txt"))) // 这个输出流是一个处理流,所以必须建立在其他节点流(文件输出流)的基础之上
    {
    Person per = new Person("孙悟空", 500);
    // 🔹(2)调用ObjectOutputStream对象的writeObject()方法输出可序列化对象
    oos.writeObject(per); // 将per对象写入输出流
    }
    catch (IOException ex){
    ex.printStackTrace();
    }
    }
    }

    // 运行上面程序,将会看到生成了一个object.txt文件,该文件的内容就是Person对象。

② 对象反序列化

  • 从刚刚生成的object.txt文件中读取Person对象:

    public class ReadObject {
    public static void main(String[] args) {
    try(
    // 🔹(1)创建一个ObjectInputStream输入流
    ObjectInputStream ois=new ObjectInputStream(
    new FileInputStream("object.txt"))) // 这个输入流是一个处理流,所以必须建立在其他节点流的基础之上。
    {
    // 🔹(2)从输入流中读取一个Java对象,并将其强制类型转换为Person类
    Person p = (Person)ois.readObject(); // 调用ObjectInputStream对象的readObject()
    System.out.println("名字为:" + p.getName()
    + "\n年龄为:" + p.getAge());
    }
    catch (Exception ex) {
    ex.printStackTrace();
    }
    }
    }


(三)转换流

转换流(InputStreamReaderOutputStreamWriter),Reader的子类,可以将 InputStream 包装成 Reader。用于实现将字节流转换成字符流。—— 可解决文件乱码问题。

  • Demo:获取键盘输入

    /* 该程序将 System.in 包装成 BufferedReader,
    BufferedReader流具有缓冲功能,它可以一次读取一行文本:
    以换行符为标志,若没有读到换行符,则程序阻塞,等到读到换行符为止。*/

    public class KeyinTest {
    public static void main(String[] args) {
    try{
    // 将 Sytem.in 对象转换成 Reader 对象
    InputStreamReader reader = new InputStreamReader(System.in);
    // 将普通的Reader包装成BufferedReader
    BufferedReader br = new BufferedReader(reader))
    String buffer = null;
    // 采用循环方式来逐行地读取
    while ((buffer = br.readLine()) !=null) {
    //如果读取的字符串为"exit",则程序退出
    if (buffer.equals("exit")) {
    System.exit(1);
    }
    //打印读取的内容
    System.out.println("输入内容为:" + buffer);
    }
    } catch (IOException ioe) {
    ioe.printStackTrace();
    }
    }
    }

    InputStreamReader

    Java使用 System.in 代表标准输入(即键盘输入),但这个标准输入流是 InputStream 类的实例,使用不太方便,且键盘输入内容都是文本内容,所以可用 InputStreamReader 将其转换成字符输入流。

    BufferedReader

    由于 BufferedReader 具有一个 readLine() 方法,可以非常方便地一次读入一行内容,所以经常把读取文本内容的输入流包装成 BufferedReader,用来方便地读取输入流的文本内容。

🔸 只有字节转字符:因为字符流比字节流操作更方便



(四)管道流

实现了pipe的概念,主要在线程中使用:一个线程可以随时向管道输出流发送数据,另一个线程随时从管道输入流读取数据,实现两个线程间的通信异步传输数据)。

输入流和输出流必须结合使用:若向连接管道输出流提供数据字节的线程(发送端)不存在,则认为该管道已损坏。

📌 注意:数据的传输方向。

(1)PipedInputStream

管道输入流(接收端)应该连接到管道输出流(发送端)。

graph LR
A[["线程1"]]
B[["线程2"]]
pi(["PipedOutputStream"]):::lp
po(["PipedInputStream
提供要写入管道输出流的"]):::g A ==> |"发送"|pi pi -.-> |"|缓冲区"|po po ==> |"读取"|B subgraph "管道输出流|管道输入流" pi po end classDef p fill:#ddaebd classDef b fill:#aab7d2 classDef g fill:#9ac5bb classDef lp fill:#f4e4e9 classDef lb fill:#d9dfeb classDef lg fill:#ddebe6 classDef info fill:#f6f6f7,color:#3f3f3f,stroke-dasharray: 3 3, stroke-width: 1px

* 缓存区:管道输入流包含一个缓存区,可在缓存区限定的范围内将读操作和写操作分离开。
🔺 不建议对这两个对象尝试使用单个线程,因为这样可能死锁线程。

* 输入输出流的连接方法(1):构造方法

// 构造方法:
// ① 以管道输出流的实例作为输入 (使其连接到src指定的管道字节输出流。)
PipedInputStream(PipedOutputStream src);
// ② 创建一个尚未连接的PipedInputStream
PipedInputStream();

* 输入输出流的连接方法(2):connect()

// 调用PipedInputStream类的connect()建立与输出流的连接:使此管道输入流连接到管道输出流src
void connect(PipedOutputStream src);

* 常用方法:

public int avaliable();                       // 返回可读取的字节书
public void close(); // 关闭管道输入流并释放系统资源
public int read(); // 从管道输入流中读取下一个字节数据
public int read(byte[] b, int off, int len); // 从管道输入流读取len字节数据到数组
protected void receive(int b); // 从管道中接受1字节数据
public void connect(PipedOutputStream src); // 连接到指定输出流,管道输入流将从该输出流接受数据

* 示例:

/**
* 简单地建立流链接(📌 注意:这样实际上没有意义,通常应用于线程间通信)
*/
public class TestPipedInputStream {
public static void main(String[] args) throws IOException {
try {
// 🔹 创建管道输出流
PipedOutputStream os = new PipedOutputStream();
// 🔹 创建输入流
PipedInputStream is = new PipedInputStream(os);

// 🔸 从输出流写入数据
byte[] b = new byte[3];
b[0] = 100;
b[1] = 101;
b[2] = 102;
os.write(b);

// 🔸 从输入流读取数据
while (is.available() > 0) {
int c = is.read();
System.out.print((char)c);
}

// 🔹 关闭输入流
is.close();

//关闭输出流
os.close();
} catch (IOException e) {
}
}
}

(2)PipedOutputStream

PipedOutputStream 可以将管道输出流连接到管道输入流来创建通信管道。管道输出流是指一个通信管道的发送端,它必须与一个作为接收端的PipedInputStream对象相连。

graph LR
A[["线程1"]]
B[["线程2"]]
pi(["PipedOutputStream"]):::p
po(["PipedInputStream"]):::lg
A ==> |"发送"|pi
pi -.-> |"|缓冲区"|po
po ==> |"读取"|B

subgraph "管道输出流|管道输入流"
pi
po
end

classDef p fill:#ddaebd
classDef b fill:#aab7d2
classDef g fill:#9ac5bb
classDef lp fill:#f4e4e9
classDef lb fill:#d9dfeb
classDef lg fill:#ddebe6
classDef info fill:#f6f6f7,color:#3f3f3f,stroke-dasharray: 3 3, stroke-width: 1px

* 示例:

- 发送者线程Sender:创建了一个输出流 PipedOutputStream 的对象 out,在该线程的主函数 run() 中调用 out 的 write() 写入数据。

- 接收者线程Receiver:创建了一个输入流 PipedInputStream 的对象 in,在该线程的主函数 run() 中调用 in 的 read() 读取数据。

/**
* 管道输出流与管道输入流在多线程中的应用:
* - Sender:管道输出流(PipedOutputStream)的独立线程类
* - Receiver:管道输入流(PipedInputStream)的独立线程类
*/
public class TestPipedOutputStream {

public static void main(String[] args) {
try {
// 🔹 创建发送者线程
Sender t1 = new Sender();
PipedOutputStream out = t1.getOutputStream();
// 🔹 创建接收者线程
Receiver t2 = new Receiver();
PipedInputStream in = t2.getInputStream();
// 🔸 连接管道流
out.connect(in);

// 🔹 启动发送者线程和接收者线程
new Thread(t1).start();
new Thread(t2).start();
// 在这两个线程都启动后就会运行各自的run()主函数,
// 从out中写入的数据就会传递到in中,实现数据的异步传输。

} catch (Exception e) {
}
}

}

//(1)发送者线程类
class Sender extends Thread {
private PipedOutputStream out = new PipedOutputStream();

public PipedOutputStream getOutputStream() {
return out;
}

public void run() {
// 🔹 发送字符串
String s = new String("hello,receiver,how are you");
try {
out.write(s.getBytes());
out.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}

//(2)接收者线程
class Receiver extends Thread {
private PipedInputStream in = new PipedInputStream();

public PipedInputStream getInputStream() {
return in;
}

public void run() {
// 🔹 接收字符串
String s = null;
byte[] buf = new byte[1024];
try {
int len = in.read(buf);
s = new String(buf, 0, len);
System.out.println("from:\n" + s);
in.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
}

// 运行该程序,即可实现发送者线程往接收者线程发送数据的功能,
// 它们分别使用了管道输入流和输出流。


(五)序列流

SequenceInputStream 类允许连接多个 InputStream 流,表示其他输入流的逻辑串联。(串行输入流)

%%{
  init: {
    'themeVariables': {
       'fontSize': '12px'
     }
  }
}%%
graph
A([InputStream s1]):::lp
B([InputStream s2]):::lp
C([InputStream s3]):::lp
D([InputStream s1]):::lb
E([InputStream s2]):::lb

A --> B --> C
subgraph "(2) SequenceInputStream(e)"
subgraph "Enumeration ⟨? extends InputStream⟩ e"
A
B
C
end
end

subgraph "(1) SequenceInputStream(s1, s2)"
D --> E
end

classDef p fill:#ddaebd
classDef b fill:#aab7d2
classDef g fill:#9ac5bb
classDef lp fill:#f4e4e9
classDef lb fill:#d9dfeb
classDef lg fill:#ddebe6
classDef info fill:#f6f6f7,color:#3f3f3f,stroke-dasharray: 3 3, stroke-width: 1px

串联输入源:它从输入流的有序集合开始,从第一个输入流开始读取,读到文件末尾,接着从第二个输入流读取,依次类推,直到到达包含的最后一个输入流的文件末尾为止。

* 构造函数:

//(1)串联数据源
SequenceInputStream(InputStream s1,InputStream s2); // 将按顺序读取这两个参数,先读取s1,然后读取s2
//(2)串联 Enumeration 类型的数据源列表
SequenceInputStream(Enumeration<?extends InputStream>e);

* Demo

/** 将两个文件数据源串联起来,创建一个SequenceInputStream类型的数据源并输出文件内容到控制台中。 */
public class TestSequenceInputStream {
public static void main(String[] args) {
try {
// 🔹 创建输入源数组
// * 串联的数据源列表中,可以包含各种InputStream实现类的数据流对象。
FileInputStream fis1 = new FileInputStream("D:/demo/test.txt");
FileInputStream fis2 = new FileInputStream("D:/demo/test2.txt");
Vector<FileInputStream> vector = new Vector<FileInputStream>();
vector.add(fis1);
vector.add(fis2);
Enumeration<FileInputStream> e = vector.elements();

// 🔸 创建序列输入源
SequenceInputStream is = new SequenceInputStream(e);

// 🔹 从输入流读取数据
while (is.available() > 0) {
int c = is.read();
System.out.print((char)c);
}

// 🔹 关闭输入流
is.close();
} catch (IOException e) {
}
}
}



(六)过滤流

过滤流用于扩展其他流,增强其功能。它包含其他一些输入/输出流,将这些流用做其基本数据源,它可以直接传输数据或提供一些额外的功能。

但是,例如 FilterInputStream 类本身只是简单地重写那些将所有请求传递给所包含输入流的 InputStream 的所有方法。因此,这个类本身对于数据流并没有什么作用。所以通常并不单独使用它们,而是使用它们的子类。



(七)缓冲流

缓冲流主要为输入/输出流缓存数据,因此需要使用 flush() 方法强制将缓存中的数据真正写入到输出流中。(输入流对象的关闭顺序必须与其创建的顺序相反,后面的输出流也是如此。)

(1)BufferedInpuStream

BufferedInputStream为另一个输入流添加一些功能,即缓存输入的能力。

在创建BufferedInputStream时,会创建一个内部缓存区数组。在读取或跳过流中的字节时,可根据需要从包含的输入流再次填充该内部缓存区,一次填充多个字节。

* Demo:

public class TestBufferedInputStream {
public static void main(String[] args) {
try {
// 🔹创建输入流
FileInputStream is = new FileInputStream("D:/demo/test.txt");
BufferedInputStream bis = new BufferedInputStream(is);
// 🔹从输入流读取数据
while (bis.available() > 0) {
int c = bis.read();
System.out.print((char)c);
}

// 🔹关闭输入流
bis.close();
is.close();
} catch (IOException e) {
}
}
}


(2)BufferedOutputStream

BufferedOutputStream 继承自 FilterOutputStream,它与 BufferedInputStream 相对应,主要为输出流做缓存。

* demo

/** 将文件输入流对象os转换为缓存输出流 */
public class TestBufferedOutputStream {
public static void main(String[] args) {
try {
// 🔹创建输出流
FileOutputStream os = new FileOutputStream("D:/demo/test.txt");
BufferedOutputStream bos = new BufferedOutputStream(os);

// 🔹写入数组数据
byte[] buf = new byte[3];
buf[0] = 100;
buf[1] = 101;
buf[2] = 102;
bos.write(buf);

// 🔹关闭输出流
bos.close();
os.close();
} catch (IOException e) {
}
}
}

// 运行该程序,将会往文件中写入3个字符“def”。

(3)BufferedReader

BufferedReader 从字符输入流中读取文本,缓存各个字符,以实现字符/数组/行的高效读取。

* 构造方法:

📌 Reader所做的每个读取请求都会导致对底层字符或字节流进行相应的读取请求
因此,建议用 BufferedReader 包装所有其 read() 操作可能开销很高的Reader(如 FileReaderInputStreamReader

BufferedReader(Reader in);        // 创建一个使用默认大小输入缓存区的缓存字符输入流
BufferedReader(Reader in,int sz); // 创建一个使用指定大小输入缓存区的缓存字符输入流

* Demo

/**
* 组合使用 FileReader 和 BufferedReader:获得一个具有缓存的文件输入流
*/
public class TestBufferedReader {
public static void main(String[] args) {
try {
// 🔹创建输入流
FileReader is = new FileReader("D:/demo/test.txt");
BufferedReader bis = new BufferedReader(is);

// 🔹从输入流读取数据
while (bis.ready()) {
int c = bis.read();
System.out.print((char)c);
}

// 🔹关闭输入流
bis.close();
is.close();
} catch (IOException e) {
}
}
}

// 运行该程序,将会输出文件中的内容到控制台中。

(4)BufferedWriter

BufferedWriter 将文本写入字符输出流,缓存各个字符,从而提供单个字符、数组和字符串的高效写入。

* 使用

📌 通常 Writer 将其输出立即发送到底层字符或字节流。除非要求提示输出,否则建议用 BufferedWriter 包装所有其 write() 操作可能开销很高的 Writer(如FileWriters和OutputStreamWriters)。例如以下案例将缓冲 PrintWriter 对文件的输出。

 PrintWriter out = new PrintWriter(
new BufferedWriter(new FileWriter("foo.out")));

// 若没有缓冲,则每次调用 print() 方法会导致将字符转换为字节,
// 然后立即写入到文件,而这是极其低效的。

* 输出换行

该类提供了 newLine() 方法,它使用平台自己的行分隔符概念,此概念由系统属性 line.separator 定义。

并非所有平台都使用新行符(\n)来终止各行。因此调用此方法来终止每个输出行要优于直接写入新行符。

* Demo

/** 将文件输入流对象os转换为缓存输出流。 */
public class TestBufferedWriter {
public static void main(String[] args) {
try {
// 🔹 创建输出流
FileWriter os = new FileWriter("D:/demo/test.txt");
BufferedWriter bos = new BufferedWriter(os);

// 🔹 写入数组数据
char[] buf = new char[3];
buf[0] = 'a';
buf[1] = 'b';
buf[2] = '中';
bos.write(buf);

// 🔹 关闭输出流
bos.close();
os.close();
} catch (IOException e) {
}
}
}

// 运行该程序,将会在文件中写入“ab中”。


(八)数据流

(1)DataInputStream

数据输入流允许应用程序以与机器无关方式从底层输入流读取基本Java数据类型

* 方法

boolean readBoolean();   // 读取一个boolean值
byte readByte(); // 读取一个8位的字节
char readChar(); // 读取一个16位的char值
double readDouble(); // 读取一个 64 位的double值
float readFloat(); // 读取一个 32 位的float值
int readInt(); // 读取一个 32 位的int值
long readLong(); // 读取一个 64 位的long值
short readShort(); // 读取一个 16 位的short值

* 类似 ObjectInputStream

ObjectInputStream:读取基本数据类型 + 对象数据类型
DataInputStrream: 读取基本数据类型

* Demo

/**
* 读取DataOutputStream类所写文件的过程
* (data.tmp可以通过下一节中 DataOutputStream 类的演示程序运行生成)
*/
public class TestDataInputStream {

public static void main(String[] args) {
try {
// 🔹 创建文件输入流
FileInputStream fis = new FileInputStream("D:/demo/data.tmp");
// 🔹 创建数据输入流
DataInputStream dis = new DataInputStream(fis);
// 🔸 读取对象数据
int i = dis.readInt();
boolean b = dis.readBoolean();
float f = dis.readFloat();
System.out.println("i=" + i);
System.out.println("b=" + b);
System.out.println("f=" + f);

// 🔹 关闭输入流
dis.close();
fis.close();
} catch (IOException e) {
}
}

}

// i=12345
// b=true
// f=1000.0

*注意:

由于文件 D:/demo/data.tmp 是写入类调用 DataOutputStream 写入的对象,每一个对象的写入都是有顺序的,因此这里的读取也必须采用同样的顺序。如果顺序不一致,将会产生对象类型转换异常 java.io.OptionalDataException,它是IOException的子类。

(2)DataOutputStream

DataOutputStream 与 DataInputStream 相对应,允许应用程序将基本Java数据类型写入输出流中。然后,应用程序可以使用数据输入流 DataInputStream 将数据读入。

* 方法

// 继承OutputStream的同时,实现了DataOutput接口,因此它具有DataOutput所规定的方法:

void writeBoolean(boolean v); // 将一个boolean值以1-byte值形式写入输出流
void writeByte(int v); // 将一个byte值以1-byte值形式写出到输出流中
void writeBytes(Strings); // 将字符串按字节顺序写出到输出流中
void writeChar(int v); // 将一个char值以2-byte值形式写入输出流中,先写入高字节
void writeChars(Strings); // 将字符串按字符顺序写入输出流
void writeDouble(double v); // 使用Double类中的doubleToLongBits方法将double参数转换为一个long值,然后将该long值以8-byte值形式写入输出流中,先写入高字节
void writeFloat(float v); // 使用Float类中的floatToIntBits方法将float参数转换为一个int值,然后将该int值以4-byte值形式写入输出流中,先写入高字节
void writeInt(int v); // 将一个int值以4-byte值形式写入输出流中,先写入高字节
void writeLong(long v); // 将一个long值以8-byte值形式写入输出流中,先写入高字节
void writeShort(int v); // 将一个short值以2-byte值形式写入输出流中,先写入高字节
void writeUTF(Stringstr); // 以与机器无关方式使用UTF-8编码将一个字符串写入输出流

* Demo

/**
* DataOutputStream类写入文件data.tmp的过程。
*/
public class TestDataOutputStream {
public static void main(String[] args) {
try {
// 🔹 创建文件输出流
FileOutputStream fos = new FileOutputStream("D:/demo/data.tmp");
// 🔹 创建对象输出流
DataOutputStream oos = new DataOutputStream(fos);

// 🔸 写入对象数据
oos.writeInt(12345);
oos.writeBoolean(true);
oos.writeFloat(1000.0f);

// 🔹 关闭输出流
oos.close();
fos.close();
} catch (IOException e) {
}
}
}

// 运行该程序,将会将对象数据写入文件中。


(九)行号输入流

(1)LineNumberInputStream

LineNumberInputStream 可跟踪行号、设置行号、对行做标记以便恢复等功能。

名词 说明
以回车符(\r)、换行符(\n)、回车符后紧跟的换行符(\r\n
(三种情况)结尾的字节序列。
行号 以0开头,并在read返回换行符时递增1。

* 方法(Deprecated)

getLineNumber();   // 取得行号
setLineNumber(); // 设置行号

* demo

/** 
* 按照文件的行读取,输出每一行的行号和内容。
*/

public class TestLineNumberInputStream {
public static void main(String[] args) {
try {
// 🔹 创建文件输入流
FileInputStream fis = new FileInputStream("D:/demo/test.txt");
// 🔹 创建行号输入流
LineNumberInputStream lis = new LineNumberInputStream(fis);
// 🔹 创建数据输入流:通过DataInputStream 进行输入流过滤,获得readLine()的功能。
DataInputStream dis = new DataInputStream(lis);

// 🔸 读取数据
String line;
while ((line = dis.readLine()) != null) {
System.out.println(lis.getLineNumber() + ":" + line);
}

// 关闭输入流
dis.close();
lis.close();
fis.close();
} catch (IOException e) {
}
}
}

// 1:hello,World!
// 2:hello,China!
// 3:Hello,Beijing!

(2)LineNumberReader

* 构造方法

由于该类继承自BufferedReader,因此它拥有与BufferedReader相同的两个构造方法:

LineNumberReader(Reader in);           // 使用默认输入缓存区的大小创建新的行编号reader
LineNumberReader(Reader in,int sz); // 创建新的行编号reader,将字符读入给定大小的缓存区

* 方法

getLineNumber();     // 取得行号  
setLineNumber(int); // 设置行号

// 注意:setLineNumber(int)不会实际更改流中的当前位置;它只更改由getLineNumber()返回的值

* demo

/**
* 根据文件输入流fis创建行号输入流的方法,
* 通过lis的readLine()方法可以读取一行数据,使用getLineNumber()取得行号,并输出行号和该行内容。
*/
public class TestLineNumberReader {
public static void main(String[] args) {
try {
// 🔹 创建文件输入流
FileReader fis = new FileReader("D:/demo/test.txt");
// 🔹 创建行号输入流
LineNumberReader lis = new LineNumberReader(fis);

// 🔸 读取数据
String line;
while ((line = lis.readLine()) != null) {
System.out.println(lis.getLineNumber() + ":" + line);
}

// 关闭输入流
lis.close();
fis.close();
} catch (IOException e) {
}
}
}

// 1:hello,World!
// 2:hello,China!
// 3:Hello,Beijing!


(十)推回输入流

PushbackInputStreamPushbackReader 为另一个输入流添加性能,允许将字节(字符)推回到流中:即“推回(pushback)”或“取消读取(unread)”一个字节(字符)的能力。

  • PushbackInputStream 和 PushbackReader 的3个方法:

    方法 说明
    void unread(int b) 将一个字节/字符推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。
    void unread(byte[]/char[] buf) 将一个字节/字符数组内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。
    void unread(byte[]/char[] b, int off, int len) 将一个字节/字符数组里从off开始,长度为len字节/字符的内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。

    在读取终止字节(字符)后,代码片段可以“取消读取”该字节(字符),这样,输入流上的下一个读取操作将会重新读取被推回的字节(字符)。

  • 处理示意图

    pushBack
  • 需指定缓冲区大小

    当创建一个 PushbackInputStreamPushbackReader 时需要指定推回缓冲区的大小,默认的推回缓冲区的长度为1。

    若程序中推回到推回缓冲区的内容超出了推回缓冲区的大小,将会引发 Pushback buffer overflowIOException 异常。

  • Demo

    public class TestPushbackInputStream {
    public static void main(String[] args) {
    try {
    // 创建文件输入流
    FileInputStream fis = new FileInputStream("D:/demo/test.txt");
    // 创建推回输入流
    PushbackInputStream pis = new PushbackInputStream(fis);

    // 读取对象数据
    int c = pis.read();
    System.out.print((char)c);
    pis.unread(c); // 🔻 推回一个字节
    c = pis.read(); // 再次读取
    System.out.print((char)c);

    // 关闭输入流
    pis.close();
    fis.close();
    } catch (IOException e) {
    }
    }
    }

    // 运行该程序将会重复输出第一个字符。


(十一)打印流(格式化)

(1)PrintStream

%%{
  init: {
    'themeVariables': {
         'fontSize': '13px'
       }
    }
}%%
graph LR
A(["PrintStream(与OutputStream相比)"]):::p
A -.-> a("添加了一组 print() / println() 方法,
可以打印各种数据类型,比较方便。"):::lp A -.-> b("不会抛出IOException"):::lp A -.-> c("自动刷新机制
写入内容后自动调用flush()方法"):::lp classDef p fill:#ddaebd classDef lp fill:#f4e4e9
  • (字节型)PrintStream 是一种 FilterOutputStream,在 OutputStream 的接口上,额外提供了一些写入各种数据类型的方法。以及对应的一组 println() 方法(自动加上换行符)。

    方法 说明
    void print(boolean) 将boolean类型数据对应字符串写到PrintStream流中
    void print(char) 将char类型数据对应字符串写到PrintStream流中
    void print(int) 将int类型数据对应字符串写到PrintStream流中
    void print(long) 将long类型数据对应字符串写到PrintStream流中
    void print(float) 将float类型数据对应字符串写到PrintStream流中
    void print(double) 将double类型数据对应字符串写到PrintStream流中
    void print(char[]) 将字符串数组写到PrintStream流中
    void print(String) 将字符串写到PrintStream流中
    void print(Obj) 将对象对应字符串写到PrintStream中

    🔺 两方法写入数据时都会将数据转换成字符串,实际调用的方法:write(String s)。

    ptrint(0x61) = write(String.valueOf(0x61))

(2)PrintWriter

  • (字符型)PrintWriter扩展了 Writer 接口,它的 print() / println() 方法最终输出的是 char 数据。使用方法同 PrintStream

(3)重定向标准输入/输出

  • 重定向标准输入/输出

    Java的标准输入/输出分别通过 System.in(从键盘输入)和 System.out(输出到屏幕)来代表。而 System 类里提供了如下3个重定向标准输入/输出的方法。

    方法 说明
    static void setErr(PrintStream err) 重定向 “标准”错误输出流。
    static void setIn(InputStream in) 重定向“标准”输入流。
    static void setOut(PrintStream out) 重定向 “标准”输出流。
  • demo:重定向标准输出

    /**
    * 通过重定向标准输出流,将System.out的输出重定向到文件输出,而不是在屏幕上输出。
    */
    public class RedirectOut {
    public static void main(String[] args) {
    try(
    // 一次性创建PrintStream输出流
    PrintStream ps=new PrintStream(new FileOutputStream("out.txt")))
    {
    // 将系统的标准输出重定向到ps输出流
    System.setOut(ps);
    // 向标准输出输出一个字符串
    System.out.println("普通字符串");
    // 向标准输出输出一个对象
    System.out.println(new RedirectOut());
    }
    catch (IOException ex)
    {
    ex.printStackTrace();
    }
    }
    }

    该代码创建了一个 PrintStream 输出流,并将系统的标准输出重定向到该 Print Stream 输出流。运行该程序无任何输出:因为标准输出不再输出到屏幕,而是输出到 out.txt 文件。

  • demo:重定向标准输入

    /**
    * 下面程序重定向标准输入,从而可以将System.in重定向到指定文件,而不是键盘输入。
    */
    public class RedirectIn {
    public static void main(String[] args) {
    try(
    FileInputStream fis=new FileInputStream("RedirectIn.java"))
    {
    // 将标准输入重定向到fis输入流
    System.setIn(fis);
    // 使用System.in创建Scanner对象,用于获取标准输入
    Scanner sc=new Scanner(System.in);
    // 增加下面一行只把回车作为分隔符
    sc.useDelimiter("\n");
    // 判断是否还有下一个输入项
    while(sc.hasNext())
    {
    // 输出输入项
    System.out.println("键盘输入的内容是:" + sc.next());
    }
    }
    catch (IOException ex)
    {
    ex.printStackTrace();
    }
    }
    }

    该代码创建了一个 FileInputStream 输入流,并使用 SystemsetIn() 方法将系统标准输入重定向到该文件输入流。

    运行上面程序,程序不会等待用户输入,而是直接输出了 RedirectIn.java 文件的内容,这表明程序不再使用键盘作为标准输入,而是使用指定文件作为标准输入源。




- end -


🔖 笔记来自:

📎 补充: