目录
基本概念
IO流是什么及其作用:IO流是读取和存储数据的解决方法。数据可以简单的理解为本地文件,但其实不仅这些,包括文本数据:(txt文件)、二进制数据(图像、音频、视频)、压缩数据(压缩包)、网络数据、对象数据(序列化)、内存数据等。
IO流的分类:IO流可以分为字节流和字符流。
- 字节流:字节流以字节为单位处理数据,不关心数据的字符编码,可以处理各种类型的数据。
- 字符流:字符流以字符为单位处理数据,关注字符编码,适用于处理文本问文件。
字节流和字符流的区别:他们处理数据的方式或者说读取和存储数据的方式不同,一个是以字节为单位,一个是以字符为单位。
扩展字节和字符概念:
在计算机存储规则中,二进制存储时,一个 1或者 0占 1bit。
字节基本知识:
- 字节概念:字节是计算机内存中最小的可寻址数据单元,通常由8位二进制数字组成。它是计算机存储和处理数据的基本单位,可以表示数字、字符、图像、音频等各种信息。
- 字节比特的单位换算:1字节 == 8比特(bit),即 1个字节通常包含 8位二进制,每位可以是0或1,故1个字节有2^8==256个值。
- 其他字节单位换算:
- 1 EB = 1024 PB
- 1 PB = 1024 TB
- 1 TB = 1024 GB
- 1 GB = 1024 MB
- 1 MB = 1024 KB
- 1 KB = 1024 BYTE
- 1 字节(Byte : /baɪt/)= 8 位(Bit : /bɪt/)
字符基本知识:
- 字符是一种抽象的概念,通常表示人类语言中的文本字符,如字母、数字、标点符号等。字符可以由一个或多个字节来表示,具体取决于字符编码方案。
扩展字符编码概念
常见的字符集有ASCII、Unicode、GBK 、BIG5等字符集。这里重点介绍写代码常用到的Unicode、GBK。
- ASCII 是最早的字符编码标准之一,在计算机和通信中广泛使用。
- BIG5 是中国台湾省繁体中文标准字符集
- GBK
- 为什么叫GBK:汉族首字母拼接,中文名:国家标准扩展字符集。GBK字符集收录21003个汉字,包含了GB和BIG5字符集。GB字符集是早先的国家标准字符集。
- GBK字符集中,一个英文占1个字节、一个汉字占2个字节。
- 上述说过一个字节有256个值,根本存不下所有汉字,所以一个汉字要占2个字节(2^16 = 65,536)。为什么不占更多的字节:主要原因是2个字节足够存储所有的汉字并且为了减少空间的浪费。
- Unicode
- Unicode 是一个全球字符集,旨在包括世界上所有语言的字符。
- Unicode字符集的编码方式
- UTF-8:一个英文占1个字节、一个汉字占3个字节
- UTF-16:一个汉字占2个字节
- UTF-32:一个汉字占4个字节
拓展乱码产生的原因和解决方法:
Java的字节流是以字节为单位进行读取和写入的,它不关心具体的字节码。当你使用字节流读取包包含中文字符的文本时,由于中文字符经常使用多个字节表示,可能会导致乱码的问题。为了正确读取和处理包含中文字符的文本,你应该使用字符流(Reader/Writer)而不是字节流(InputStream/OutputStream),并指定正确的字符编写码。
基于上述字节、字符、字符编码方式概念的扩展,不难得出以下结论:
字符流 == 字节流 + 字符集 (该结论下文中会有进一步解释)
主要目的是为了进一步理解和区分字节流与字符流。 在用到IO流时需要考虑到这些。
简单使用
通过字节流案例入门
1、 读取文本内容:
思路:告诉程序需要读哪一个文件,并且以什么样的方式读取,最后将读取到的内容输出出来
文本内容为(该文本的字符编码方式为UTF-8:记事本打开.txt文件,在右下角可以看到编码方式):
123456
随着风 花儿摇曳着
/**
* 要求:读写纯文本文件
* 思路:告诉程序需要读哪一个文件,并且以什么样的方式读取,最后将读取到的内容输出出来
*/
@Test
public void test1() throws IOException {
// 创建File对象关联本地需要读取的文本文件:文件名称.txt
File file = new File("F:\\demoTemp\\ckdemo\\IOTest\\文件名称.txt");
// 字节输入流绑定要读取的文件
FileInputStream fileInputStream = new FileInputStream(file);
// 定义一个变量临时存放读取的字节
int temp;
// 循环判断输入流读取到的每一个字节,如果读到最后循环结束
while ((temp = fileInputStream.read()) > -1){
// 将每一字节转换成char字符类型并输出
System.out.print((char)temp);
}
}
输出结果为:
123456
éçé£ è±å¿ææ³ç
- 第一次应该有的疑问
- 为什么会有乱码?
- 为什么要转换成 char 类型输出,不转换会输出什么
- 为什么temp变量要定义成 int 类型
- 为什么循环结束的判断条件是 > -1
- 解答
- 为什么会有乱码
- 上述案例中是通过字节流读取的文件内容,而字节流是以字节为单位读取的。我们知道 .txt 文件是以UTF-8编码方式存储的,一个汉字占3字节。当字节流在读取时,他一次只会读取到一个字节,这一个字节不能映射成正确的汉字,故此形成乱码。解决方法:通常用字符流读取文本文件。
- 为什么循环结束的判断条件时 > -1 和 为什么temp要定义成 int 类型
- 请下载附件(Java API)并搜索FileInputStream类查看方法自行得出答案
- 关注方法的返回类型 和 方法的输入参数
- fileInputStream.read()读取的是一个字节,值在0-255之间,返回时将其转换成int类型是为了能够表示文件读取结束,该值是-1。
- 请下载附件(Java API)并搜索FileInputStream类查看方法自行得出答案
- 为什么要转换成char类型输出
- 因为字节流读取的是一个字节,是8位二进制数,为了转换成我们看的懂的内容,所以这里强转成 char 类型。
- 为什么会有乱码
2、写入数据到文本中:
将temp变量中内容写到输出到新建的输出文件.txt文件中。思路:内容是什么,写入到哪一个文件中?怎样将内容转换成字节的方式给字节输出流?
/**
* 要求:写入数据到文本
*/
@Test
public void test2() throws IOException {
// 字节输出流
FileOutputStream fileOutputStream = new FileOutputStream("F:\\输出文件.txt");
String temp = "想像你失落的唇印 想象你失约的旅行";
// 转换成字节数组
byte[] b = temp.getBytes();
fileOutputStream.write(b);
}
这里使用的是字节流的方式将文字写入文本中。通常情况下用字符流完成该功能。
3、文件拷贝:
复制一份文件。思路:通过字节流将内容读取出来暂存到自己定义的变量中,再通过字节输出流将内容输出到新文件中。
/**
* 文件拷贝
*/
@Test
public void test3() throws Exception{
File file1 = new File("F:\\原文件.txt");
File file2 = new File("F:\\原文件-副本.txt");
FileInputStream fileInputStream = new FileInputStream(file1);
FileOutputStream fileOutputStream = new FileOutputStream(file2);
byte[] b = new byte[1024];
int length;
while ((length = fileInputStream.read(b)) > -1){
// 应为b数组元素有默认值是0,这里保证输出的字节都是读取到的,故通过长度条件。
fileOutputStream.write(b,0,length);
}
}
用字节流将原文件内容拷贝一份为源文件-副本。
4、文件加密:
对原文件中的每一个字节数据进行更改,然后将更改以后的数据存储到新的文件中。
/**
* 加密文件后拷贝
*/
@Test
public void test4() throws Exception {
File file1 = new File("F:\\demoTemp\\ckdemo\\IOTest\\输出文件.txt");
File file2 = new File("F:\\demoTemp\\ckdemo\\IOTest\\输出文件-加密.txt");
FileInputStream fileInputStream = new FileInputStream(file1);
FileOutputStream fileOutputStream = new FileOutputStream(file2);
int b;
while ((b = fileInputStream.read()) > -1){
b = b +1;
fileOutputStream.write(b);
}
}
补充:fileInputStream.read()读取的是一个字节,值在0-255之间,返回时将其转换成int类型是为了能够表示文件读取结束,该值是-1。
5、文件解密:
读取加密之后的文件,按照加密规则反向操作,变成原始文件。
/**
* 解密加密后的文件
*/
@Test
public void test5() throws Exception {
File file1 = new File("F:\\demoTemp\\ckdemo\\IOTest\\输出文件-加密.txt");
File file2 = new File("F:\\demoTemp\\ckdemo\\IOTest\\输出文件-解密.txt");
FileInputStream fileInputStream = new FileInputStream(file1);
FileOutputStream fileOutputStream = new FileOutputStream(file2);
int b;
while ((b = fileInputStream.read()) > -1){
b = b - 1;
fileOutputStream.write(b);
}
}
其他流介绍
字符流
字符流的使用方法和字节流基本上是一致的。
字符流的底层是使用了字节流,read()方法默认也一个字节一个字节的读取,但如果遇到中文就会一次读取多个字节,GBK一次会读取2个字节,UTF-8一次会读取3个字节。
/**
* 字符流的使用
*/
@Test
public void test6() throws Exception {
FileReader fileReader = new FileReader("F:\\文件名称.txt");
FileWriter fileWriter = new FileWriter("F:\\文件名称-字符流.txt", true);
char[] b = new char[1024];
int bytesRead;
while ((bytesRead = fileReader.read(b)) > -1) {
System.out.println(new String(b, 0, bytesRead));
fileWriter.write(b, 0, bytesRead);
}
fileWriter.close();
fileReader.close();
}
补充:
fileReader.read():默认也是一个字节一个字节读取,遇到中文读取多个字节,读取之后方法底层还会进行解码并转成十进制。最终把这个十进制作为返回值。
fileReader.read(b)方法:包含了读取数据+解码+强转的步骤。
字符流底层原理:
创建字符输入流对象时,底层会关联文件并且创建缓冲区(长度为8192的字节数组)
字符流读取数据时,底层会先判断缓冲区中是否有数据可读。如果缓冲区没有数据就会从文件中获取并装到缓冲区中,每次尽可能装满。如果文件也没有数据了就返回-1。如果缓冲区有数据,就从换从区中读取。
字节流是没有缓冲区的。
缓冲流(Buffered Streams)
缓冲流用于提高I/O操作的性能。它们通过在内存中分配空间来减少磁盘或网络I/O的次数,从而加速数据的读取读取和写入。缓冲流通常与基础的字节流或字符流一起使用,以提供更高的数据传输。
缓冲流同样分为字节缓存流和字符缓冲流。
字节缓冲输入流:BufferedInputStream
字节缓冲输出流:BufferedOutputStream
字符缓冲输入流:BufferedReader
字符缓冲输出流:BufferedWriter
缓冲流的使用
/**
* 缓冲流的使用
*/
@Test
public void test7() throws Exception{
BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(new File("F:\\文件名称.txt")));
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(new File("F:\\文件名称-缓冲输出.txt")));
byte[] b = new byte[1024];
int length;
while ((length = bufferedInputStream.read(b)) > -1){
bufferedOutputStream.write(b,0,length);
}
bufferedInputStream.close();
bufferedOutputStream.close();
}
上述代码可以看出,字节缓冲流的使用与字符流的使用基本上一致。同理字符流也一样。
转换流
Java的转换流(Conversion Streams)是用于在字节流和字符流之间进行转换的I/O流。它们通常用于处理字符数据和字节数据之间的转换,例如从文件读取字节数据并将其转换为字符数据,或者将字符数据写入文件时进行字节到字符的转换。转换流通常用于处理字符编码和字符集转换的需求。
InputStreamReader:用于将字节输入流转换为字符输入流的转换流
OutputStreamWriter:用于将字符输出流转换为字节输出流的转换流
/**
* 转换流的使用:用UTF-8字符集读取并输出成IOS_8859_1字符集的文件
*/
@Test
public void test8() throws IOException {
InputStreamReader inputStreamReader = new InputStreamReader(new FileInputStream("F:\\文件名称.txt"),StandardCharsets.UTF_8);
OutputStreamWriter outputStreamWriter = new OutputStreamWriter(new FileOutputStream("F:\\文件名称-GBK.txt"), StandardCharsets.ISO_8859_1);
char[] b = new char[1024];
int length;
while((length = inputStreamReader.read(b)) > -1){
outputStreamWriter.write(b,0,length);
}
inputStreamReader.close();
outputStreamWriter.close();
}
序列化流
序列化流(Serialization Streams)是用于将对象转换为字节序列或从字节序列还原对象的I/O流。这是用于对象持久化和数据传输的一种机制,允许将对象转换为字节流,然后将其保存到文件、传输到网络或存储在数据库中,以后可以重新加载并还原为原始对象。序列化主要用于对象的持久化和跨网络传输。
ObjectInputStream:用于从字节流还原对象的流。它可以从输入流中读取字节数据,并将其还原为原始对象。
ObjectOutputStream:用于将对象序列化为字节流的流。它可以将对象写入输出流,从而将对象转换为字节数据。
/**
* 序列化流简单使用
*/
@Test
public void test9() throws IOException, ClassNotFoundException {
IOTestObject object = new IOTestObject("周杰伦", "44");
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("F:\\序列化文件.txt"));
objectOutputStream.writeObject(object);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("F:\\序列化文件.txt"));
IOTestObject o = (IOTestObject)objectInputStream.readObject();
System.out.println(o.toString());
objectInputStream.close();
}
序列化时注意序列号和transient的使用。
打印流
打印流(Print Stream)是Java I/O中的一种特殊类型的输出流,用于简化文本数据的输出操作。它提供了一种便捷的方式来打印文本数据到控制台或文件,而不必手动构建和格式化文本字符串。
PrintStream:字节打印流
PrintWrite:字符打印流
/**
* 打印流
*/
@Test
public void test10() throws FileNotFoundException {
PrintStream printStream = new PrintStream(new FileOutputStream("F:\\字节打印流文件.txt"));
printStream.println("眼神中飘移 总是在关键时刻清楚洞悉");// 写出+自动刷新+换行
printStream.close();
PrintWriter printWriter = new PrintWriter(new FileOutputStream("F:\\字符打印流文件.txt"));
printWriter.println("你的不坚定配合我颠沛流离 死去中清醒明白你背着我聪明");
printWriter.close();
}
压缩流和解压缩流
压缩流:将文件形成压缩包。在压缩流中,每一个文件或者文件夹都是一个zipEntry对象。压缩流也可以理解成将每一个zipEntry对象放入压缩包中。
/**
* 压缩流,压缩单个文件
* 思路:定义要压缩的文件和压缩后的文件
* 新建zipEntry对象并放入压缩包中
* 将要压缩的文件数据给zipEntry关联
*/
@Test
public void test11() throws IOException {
File file = new File("F:\\文件名称.txt");
File zip = new File("F:\\压缩包名称.zip");
ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(zip));
ZipEntry zipEntry = new ZipEntry("文件名称.txt");
// 将zipEntry对象放入压缩包中
zipOutputStream.putNextEntry(zipEntry);
FileInputStream fileInputStream = new FileInputStream(file);
int b;
while ((b = fileInputStream.read()) > -1){
// 把资源数据写入zipEntry对象
zipOutputStream.write(b);
}
zipOutputStream.closeEntry();
zipOutputStream.close();
}
/**
* 压缩流,压缩文件夹
* 区别与压缩单个文件的关键点在于文件夹内的文件夹怎么压缩处理:关注文件中文件夹的路径处理
*/
@Test
public void test12() throws IOException {
// 要压缩的文件夹
File file = new File("F:\\压缩流");
// 压缩包
File zip = new File("F:\\压缩流.zip");
// 压缩流
ZipOutputStream zipOutputStream = new ZipOutputStream(new FileOutputStream(zip));
toZip(file,"",zipOutputStream);
zipOutputStream.close();
}
/**
* 将文件夹压缩成压缩包
* @param src 资源文件夹
* @param zipPath 压缩包路径
* @param zipOutputStream 关联压缩包的压缩流
* @throws IOException
*/
public void toZip(File src,String zipPath,ZipOutputStream zipOutputStream) throws IOException {
File[] files = src.listFiles();
for (File fileTemp:files) {
if(fileTemp.isFile()){
// 资源文件在压缩包内的路径
ZipEntry zipEntry = new ZipEntry("".equals(zipPath)?fileTemp.getName():zipPath+File.separator+fileTemp.getName());
zipOutputStream.putNextEntry(zipEntry);
FileInputStream fileInputStream = new FileInputStream(fileTemp);
int b;
while ((b = fileInputStream.read()) > -1){
zipOutputStream.write(b);
}
fileInputStream.close();
zipOutputStream.closeEntry();
}else {
// 递归
toZip(fileTemp,"".equals(zipPath)?fileTemp.getName():zipPath+File.separator+fileTemp.getName(),zipOutputStream);
}
}
}
解压缩流:将压缩包解压。将压缩包中的每一个文件看成一个zipEntry,然后将每一个zipEntry复制一份,放到目标路径下。
@Test
public void test14() throws Exception {
String zipFilePath = "F:\\压缩流解压.zip";
String extractPath = "F:\\压缩流解压";
extractZipFile(zipFilePath, extractPath);
}
public void extractZipFile(String zipFilePath, String extractPath) throws Exception {
ZipInputStream zipInputStream = new ZipInputStream(new FileInputStream(zipFilePath));
File extractDir = new File(extractPath);
extractDir.mkdirs();
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
String entryName = zipEntry.getName();
File entryFile = new File(extractDir, entryName);
if (zipEntry.isDirectory()) {
entryFile.mkdirs();
} else {
File parentDir = entryFile.getParentFile();
if (parentDir != null) {
parentDir.mkdirs();
}
try (FileOutputStream fileOutputStream = new FileOutputStream(entryFile)) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = zipInputStream.read(buffer)) > 0) {
fileOutputStream.write(buffer, 0, bytesRead);
}
}
}
zipInputStream.closeEntry();
}
}