目录
- IO流
- IO流的四大分类:
- IO流的体系:
- FileinputStream(文件字节输入流)
- FileOutputStream(文件字节输出流)
- 文件复制
- 资源释放
- FileReader(文件字符输入流)
- FileWriter(文件字符输出流)
- 缓冲流
- 字节缓冲流
- 字符缓冲流
- 原始流、缓冲流的性能分析
- 测试代码:
- 测试一:
- 测试二:
- 测试三:
- 测试四:
- 转换流
- InputStreamReader (字符输入转换流)
- OutputStreamWriter(字符输出转换流)
- 打印流(PrintStream/PrintWriter)
- 字节打印流
- 字符打印流
- PrintStream和Printwriter的区别
- 重定向输出语句
- 数据流
- DataOutputStream(数据输出流)
- DatalnputStream(数据输入流)
- 序列化流
- ObjectOutputStream(对象字节输出流)
- ObjectinputStream(对象字节输入流)
- 如果要一次序列化多个对象,怎么做?
- IO框架
IO流
用于读写数据的数据流 (可以读写磁盘文件,或网络中的数据…)
IO流的四大分类:
- 字节输入流:以内存为基准,来自磁盘文件/网络中的数据以字节的形式读入到內存中去的流
- 字节输出流:以内存为基准,把内存中的数据以字节写出到磁盘文件或者网络中去的流。
- 字符输入流:以内存为基准,来自磁盘文件/网络中的数据以字符的形式读入到内存中去的流。
- 字符输出流:以内存为基准,把内存中的数据以字符写出到磁盘文件或者网络介质中去的流。
IO流的体系:
FileinputStream(文件字节输入流)
作用:以内存为基准,可以把磁盘文件中的数据以字节的形式读入到内存中去。
FileOutputStream(文件字节输出流)
作用:以内存为基准,把内存中的数据以字节的形式写出到文件中去。
文件写入换行符:os.write("/r/n”.getBytes());
文件复制
代码实现:
package cn.kt.FileAndIO;
import java.io.*;
public class FileCopy {
public static void main(String[] args) {
InputStream is = null;
OutputStream os = null;
try {
// 需求:复制照片。
//1、创建一个字节输入流管道与源文件接通
is = new FileInputStream("/Users/mac/Downloads/hzw.jpg");
//2、创建一个字节输出流管道与目标文件接通。
os = new FileOutputStream("/Users/mac/tao/hzw.jpg");
//3、创建一个字节数组,负责转移字节数据。
byte[] buffer = new byte[1024];// 1KB.
//4、从字节输入流中读取宁节数据,写出去到宁节输出流中。读多少写出去多少。
int len; // 记住每次读取了多少个字节。
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
System.out.println("复制完成!!");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (os != null) os.close();
} catch (IOException e) {
e.printStackTrace();
}
try {
if (is != null) is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
注意:字节流非常话合做一切文件的复制操作
任何文件的底层都是字节,字节流做复制,是一字不漏的转移完全部字节,只要复制后的文件格式一致就没问题!
资源释放
JDK 7开始提供了更简单的资源释放方案:try-with-resource
代码示例:
package cn.kt.FileAndIO;
import java.io.*;
/**
* 描述: 复制照片。
*/
public class FileCopy {
public static void main(String[] args) {
try (
//1、创建一个字节输入流管道与源文件接通
InputStream is = new FileInputStream("/Users/mac/Downloads/hzw.jpg");
//2、创建一个字节输出流管道与目标文件接通。
OutputStream os = new FileOutputStream("/Users/mac/tao/hzw.jpg");
// 注意:这里只能放置资源对象。(流对象)
// 什么是资源呢?资源都是会实现AutoCloseable接口
) {
//3、创建一个字节数组,负责转移字节数据。
byte[] buffer = new byte[1024];// 1KB.
//4、从字节输入流中读取宁节数据,写出去到宁节输出流中。读多少写出去多少。
int len; // 记住每次读取了多少个字节。
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
System.out.println("复制完成!!");
} catch (IOException e) {
e.printStackTrace();
}
}
}
代码相对简洁很多
FileReader(文件字符输入流)
作用:以内存为基准,可以把文件中的数据以字符的形式读入到内存中去。
FileWriter(文件字符输出流)
作用:以内存为基准,把内存中的数据以字符的形式写出到文件中去。
字符输出流使用时的注意事项
字符输出流写出数据后,必须刷新流,或者关闭流,写出去的数据才能生效
字节流、字符流的使用场景小结:
字节流适合做一切文件数据的拷贝(音视频,文本);字节流不适合读取中文内容输出。
字符流适合做文本文件的操作(读,写)。
缓冲流
字节缓冲流
提高字节流读写数据的性能
原理:字节缓冲输入流自带了8KB缓冲池;字节缓冲输出流也自带了8KB缓冲池。
字符缓冲流
- BufferedReader(字符缓冲输入流)
作用:自带8K(8192)的字符缓冲池,可以提高字符输入流读取字符数据的性能。
- BufferedWriter(字符缓冲输出流)
作用:自带8K的字符缓冲池,可以提高字符输出流写字符数据的性能。
原始流、缓冲流的性能分析
测试代码:
package cn.kt.FileAndIO;
import java.io.*;
/**
* Created by tao.
* Date: 2024/2/22 16:44
* 描述: 观察原始流和缓冲流的性能。
*/
public class BufferStreamTimeTest {
// 复制的视频路径
private static final String SRC_FILE = "D:\\resource\\线程池.avi";
// 复制到哪个目的地
private static final String DEST_FILE = "D:\\";
public static void main(String[] args) {
// copy01(); // 低级字节流一个一个字节的赋值,慢的简直让人无法忍受,直接淘汰!
copy02();// 低级的字节流流按照一个一个字节数组的形式复制,速度较慢!
// copy03(); // 缓冲流按照一个一个字节的形式复制,速度较慢,直接淘汰!
copy04(); // 缓冲流按照一个一个字节数组的形式复制,速度极快,推荐使用!
}
private static void copy01() {
long startTime = System.currentTimeMillis();
try (
InputStream is = new FileInputStream(SRC_FILE);
OutputStream os = new FileOutputStream(DEST_FILE + "1.avi");
) {
int b;
while ((b = is.read()) != -1) {
os.write(b);
}
} catch (Exception e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("低级字节流一个一个字节复制耗时:" + (endTime - startTime) / 1000.0 + "s");
}
private static void copy02() {
long startTime = System.currentTimeMillis();
try (
InputStream is = new FileInputStream(SRC_FILE);
OutputStream os = new FileOutputStream(DEST_FILE + "2.avi");
) {
byte[] buffer = new byte[1024 * 64];
int len;
while ((len = is.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("低级字节流使用字节数组复制耗时:" + (endTime - startTime) / 1000.0 + "s");
}
private static void copy03() {
long startTime = System.currentTimeMillis();
try (
InputStream is = new FileInputStream(SRC_FILE);
BufferedInputStream bis = new BufferedInputStream(is);
OutputStream os = new FileOutputStream(DEST_FILE + "3.avi");
BufferedOutputStream bos = new BufferedOutputStream(os);
) {
int b;
while ((b = bis.read()) != -1) {
bos.write(b);
}
} catch (Exception e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("缓冲流一个一个字节复制耗时:" + (endTime - startTime) / 1000.0 + "s");
}
private static void copy04() {
long startTime = System.currentTimeMillis();
try (
InputStream is = new FileInputStream(SRC_FILE);
BufferedInputStream bis = new BufferedInputStream(is, 64 * 1024);
OutputStream os = new FileOutputStream(DEST_FILE + "4.avi");
BufferedOutputStream bos = new BufferedOutputStream(os, 64 * 1024);
) {
byte[] buffer = new byte[1024 * 64]; // 32KB
int len;
while ((len = bis.read(buffer)) != -1) {
bos.write(buffer, 0, len);
}
} catch (Exception e) {
e.printStackTrace();
}
long endTime = System.currentTimeMillis();
System.out.println("缓冲流使用字节数组复制耗时:" + (endTime - startTime) / 1000.0 + "s");
}
}
测试一:
分别使用原始的字节流,以及字节缓冲流复制一个很大视频(889MB)。
测试步骤:
- 使用低级的字节流按照一个一个字节的形式复制文件。
- 使用低级的字节流按照字节数组的形式复制文件。
- 使用高级的缓冲字节流按照一个一个字节的形式复制文件。
- 使用高级的缓冲字节流按照字节数组的形式复制文件。
默认情况测试结果:
- 低级流一个字节复制: 慢得简直让人无法忍受
- 低级流按照字节数组复制(数组长度1024): 12.117s
- 缓冲流一个字节复制: 11.058s
- 缓冲流按照字节数组复制(数组长度1024): 2.163s
经过上面的测试,我们可以得出一个结论:默认情况下,采用一次复制1024个字节,缓冲流完胜。
测试二:
但是,缓冲流就一定性能高吗?我们采用一次复制8192个字节试
1. 低级流按照字节数组复制(数组长度8192): 2.535s
2. 缓冲流按照字节数组复制(数组长度8192): 2.088s
经过上面的测试,我们可以得出一个结论:一次读取8192个字节时,低级流和缓冲流性能相当。相差的那几毫秒可以忽略不计。
测试三:
继续把数组变大,看一看缓冲流就一定性能高吗?现在采用一次读取1024*32个字节数据试试(缓冲流底层的字节数组也设成32k)
1. 低级流按照字节数组复制(数组长度32k): 1.128s
2. 缓冲流按照字节数组复制(数组长度32k): 1.133s
测试四:
继续把数组变大,看一看缓冲流就一定性能高吗?现在采用一次读取1024*64个字节数据试试
1. 低级流按照字节数组复制(数组长度64k): 1.039s
2. 缓冲流按照字节数组复制(数组长度64k): 1.151s
此时你会发现,当数组大到一定程度,性能已经提高不了多少了,甚至缓冲流的性能还没有低级流高。
在实际开发中,想提升读写性能就扩大数组大小,大小取决于经验,并且缓冲流的性能不一定就比低级流好。
转换流
解决的问题:不同编码读取出现乱码的问题
- 如果代码编码和被读取的文本文件的编码是一致的,使用字符流读取文本文件时不会出现乱码!
- 如果代码编码和被读取的文本文件的编码是不一致的,使用字符流读取文本文件时就会出现乱码!
InputStreamReader (字符输入转换流)
解决不同编码时,字符流读取文本内容乱码的问题。
解决思路:先获取文件的原始字节流,再将其按真实的字符集编码转成字符输入流,这样字符输入流中的字符就不乱码了。
InputStreamReader也是不能单独使用的,它内部需要封装一个InputStream的子类对象,再指定一个编码表,如果不指定编码表,默认会按照UTF-8形式进行转换。我们可以先准备一个GBK格式的文件,然后使用下面的代码进行读取是不会有乱码的。
代码示例:
public class InputStreamReaderTest {
public static void main(String[] args) {
try (
// 1、得到文件的原始字节流(GBK的字节流形式)
// 2、把原始的字节输入流按照指定的字符集编码转换成字符输入流
Reader isr = new InputStreamReader(new FileInputStream("io-app2/src/06.txt"), "GBK");//多态写法
// 3、把字符输入流包装成缓冲字符输入流
BufferedReader br = new BufferedReader(isr);
){
String line;
while ((line = br.readLine()) != null){
System.out.println(line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
OutputStreamWriter(字符输出转换流)
作用:可以控制写出去的字符使用什么字符集编码。
解决思路:获取字节输出流,再按照指定的字符集编码将其转换成字符输出流,以后写出去的字符就会用该字符集编码了。
OutputStreamReader也是不能单独使用的,它内部需要封装一个OutputStream的子类对象,再指定一个编码表,如果不指定编码表,默认会按照UTF-8形式进行转换。我们可以先准备一个GBK格式的文件,使用下面代码往文件中写字符数据。
代码示例:
public class OutputStreamWriterTest2 {
public static void main(String[] args) {
// 指定写出去的字符编码。
try (
// 1、创建一个文件字节输出流
OutputStream os = new FileOutputStream("io-app2/src/out.txt");
// 2、把原始的字节输出流,按照指定的字符集编码转换成字符输出转换流。
Writer osw = new OutputStreamWriter(os, "GBK");
// 3、把字符输出流包装成缓冲字符输出流
BufferedWriter bw = new BufferedWriter(osw);
){
bw.write("我是中国人abc");
bw.write("我爱你中国123");
} catch (Exception e) {
e.printStackTrace();
}
}
}
打印流(PrintStream/PrintWriter)
作用:打印流可以实现更方便、更高效的打印数据出去,能实现打印啥出去就是啥出去。
字节打印流
字符打印流
代码示例:
public class PrintTest1 {
public static void main(String[] args) {
try (
// 1、创建一个打印流管道
// PrintStream ps =
// new PrintStream("io-app2/src/itheima08.txt", Charset.forName("GBK"));
// PrintStream ps =
// new PrintStream("io-app2/src/itheima08.txt");
PrintWriter ps =
new PrintWriter(new FileOutputStream("io-app2/src/itheima08.txt", true));
//注意 高级流不支持追加参数 所以想要追加的方式来写 就得先包装一个低级流并声明是追加型写入
){
ps.print(97); //文件中显示的就是:97而不是a
ps.print('a'); //文件中显示的就是:a
ps.println("我爱你中国abc"); //文件中显示的就是:我爱你中国abc
ps.println(true);//文件中显示的就是:true
ps.println(99.5);//文件中显示的就是99.5
ps.write(97); //文件中显示a,发现和前面println方法的区别了吗?println自带换行
} catch (Exception e) {
e.printStackTrace();
}
}
}
其实打印流我们一直在使用,只是没有感受到而已。打印流可以实现更加方便,更加高效的写数据的方式。
这里所说的打印其实就是写数据的意思,它和普通的write方法写数据还不太一样,一般会使用打印流特有的方法叫print(数据)或者println(数据),它的特点是打印啥就输出啥,并且内部底层自己封装了缓冲流,所以性能也不差。
PrintStream和Printwriter的区别
- 打印数据的功能上是一模一样的:都是使用方便,性能高效(核心优势)
- PrintStream继承自字节输出流OutputStream,因此支持写字节数据的方法。
- PrintWriter继承自字符输出流Writer,因此支持写字符数据出去。
重定向输出语句
System.out.println()这句话表示打印输出,但是至于为什么能够输出,其实我们一直不清楚。
其实是因为System里面有一个静态变量叫out,out的数据类型就是PrintStream,它就是一个打印流,而且这个打印流的默认输出目的地是控制台,所以我们调用System.out.pirnln()就可以往控制台打印输出任意类型的数据,而且打印啥就输出啥。
而且System还提供了一个方法,可以修改底层的打印流,这样我们就可以重定向打印语句的输出目的地了。示例代码如下:
public class PrintTest2 {
public static void main(String[] args) {
System.out.println("老骥伏枥");
System.out.println("志在千里");
try ( PrintStream ps = new PrintStream("io-app2/src/09.txt"); ){
// 把系统默认的打印流对象改成自己设置的打印流
System.setOut(ps);
System.out.println("烈士暮年");
System.out.println("壮心不已");
} catch (Exception e) {
e.printStackTrace();
}
}
}
此时打印语句,将往文件中打印数据,而不在控制台。
数据流
我们想把数据和数据的类型一并写到文件中去,读取的时候也将数据和数据类型一并读出来。这就可以用到数据流,有两个DataInputStream和DataOutputStream.
DataOutputStream(数据输出流)
DataOutputStream类,它也是一种包装流,创建DataOutputStream对象时,底层需要依赖于一个原始的OutputStream流对象。然后调用它的wirteXxx方法,写的是特定类型的数据。
示例代码如下:往文件中写整数、小数、布尔类型数据、字符串数据
public class DataOutputStreamTest1 {
public static void main(String[] args) {
try (
// 1、创建一个数据输出流包装低级的字节输出流
DataOutputStream dos =
new DataOutputStream(new FileOutputStream("io-app2/src/10out.txt"));
){
dos.writeInt(97);
dos.writeDouble(99.5);
dos.writeBoolean(true);
dos.writeUTF("666");
} catch (Exception e) {
e.printStackTrace();
}
}
}
//运行完之后666前面显示的是乱码,但其实不是发生了编码错误,而是一种特定的数据存储方式
//因为本来就不是存给人看的,而是方便下次读取的
DatalnputStream(数据输入流)
DataIntputStream类,它也是一种包装流,用于读取数据输出流输出的数据。创建DataInputStream对象时,底层需要依赖于一个原始的InputStream流对象。然后调用它的readXxx()方法就可以读取特定类型的数据。
示例代码如下:读取文件中特定类型的数据(整数、小数、字符串等)
public class DataInputStreamTest2 {
public static void main(String[] args) {
try (
DataInputStream dis =
new DataInputStream(new FileInputStream("io-app2/src/10out.txt"));//文件是上面数据输出流输出的文件
){
int i = dis.readInt();
System.out.println(i);//97
double d = dis.readDouble();
System.out.println(d);//99.5
boolean b = dis.readBoolean();
System.out.println(b);//true
String rs = dis.readUTF();
System.out.println(rs);//"666"
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意输出的时候读取文件的数据类型要和写入文件的数据类型相同,否则会出问题。
序列化流
序列化流是干什么用的呢? 我们知道字节流是以字节为单位来读写数据、字符流是按照字符为单位来读写数据、而对象流是以对象为单位来读写数据。也就是把对象当做一个整体,可以写一个对象到文件,也可以从文件中把对象读取出来。这里有一个新词 序列化
序列化:意思就是把对象写到文件或者网络中去。(简单记:写对象)
反序列化:意思就是把对象从文件或者网络中读取出来。(简单记:读对象)
ObjectOutputStream(对象字节输出流)
ObjectOutputStream流也是一个包装流,不能单独使用,需要结合原始的字节输出流使用,可以把Java对象进行序列化:把Java对象存入到文件中去。
代码如下:将一个User对象写到文件中去
第一步:先准备一个User类,必须让其实现Serializable接口。
// 注意:对象如果需要序列化,必须实现序列化接口。
public class User implements Serializable {
private String loginName;
private String userName;
private int age;
// transient 这个成员变量将不参与序列化,即某个对象被写到文件后再读取,这个字段就是null了。
private transient String passWord;
public User() {
}
public User(String loginName, String userName, int age, String passWord) {
this.loginName = loginName;
this.userName = userName;
this.age = age;
this.passWord = passWord;
}
@Override
public String toString() {
return "User{" +
"loginName='" + loginName + '\'' +
", userName='" + userName + '\'' +
", age=" + age +
", passWord='" + passWord + '\'' +
'}';
}
}
注意:transient 这个成员变量将不参与序列化,即某个对象被写到文件后再读取,这个字段就是null了。
第二步:再创建ObjectOutputStream流对象,调用writeObject方法对象到文件。
public class Test1ObjectOutputStream {
public static void main(String[] args) {
try (
// 2、创建一个对象字节输出流包装原始的字节 输出流。
ObjectOutputStream oos =
new ObjectOutputStream(new FileOutputStream("io-app2/src/11out.txt"));
){
// 1、创建一个Java对象。
User u = new User("admin", "张三", 32, "666888xyz");
// 3、序列化对象到文件中去
oos.writeObject(u);
System.out.println("序列化对象成功!!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
注意:写到文件中的对象,是不能用记事本打开看的。因为对象本身就不是文本数据,打开是乱码,这里必须用反序列化,自己写代码读。
ObjectinputStream(对象字节输入流)
ObjectInputStream流,它也是一个包装流,不能单独使用,需要结合原始的字节输入流使用,可以读任意的java对象。
接着前面的案例,文件中已经有一个User对象,现在要使用ObjectInputStream读取出来。称之为反序列化。
public class Test2ObjectInputStream {
public static void main(String[] args) {
try (
// 1、创建一个对象字节输入流管道,包装 低级的字节输入流与源文件接通
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("io-app2/src/itheima11out.txt"));
){
User u = (User) ois.readObject();
System.out.println(u);
} catch (Exception e) {
e.printStackTrace();
}
}
}
如果要一次序列化多个对象,怎么做?
用一个ArrayList集合存储多个对象,然后直接对集合进行序列化即可
注意:ArrayList集合已经实现了序列化接口!
IO框架
为了简化对IO操作,很多技术大牛或者组织提供了一些有关IO流小框架,可以提高IO流的开发效率。
如:
- 常用的hutool:https://hutool.cn/docs/#/core/IO/%E6%A6%82%E8%BF%B0
- apache开源基金组织提供了一组有关IO流小框架:commons-io