作者:孙玉昌,昵称【一一哥】,另外【壹壹哥】也是我哦
千锋教育高级教研员、CSDN博客专家、万粉博主、阿里云专家博主、掘金优质作者
前言
在前面的几篇文章中,壹哥给大家讲解了IO流的概念、作用及核心API,并讲解了File文件类和Path路径类的使用,有了以上这些内容的铺垫,接下来咱们就可以具体使用IO流了。在今天的内容中,壹哥会通过一些案例,来给大家讲解IO流中的字节流,尤其是InputStream和OutputStream,大家这就拿出小本本练起来吧。
------------------------------前戏已做完,精彩即开始----------------------------
全文大约【5800】 字,不说废话,只讲可以让你学到技术、明白原理的纯干货!本文带有丰富的案例及配图视频,让你更好地理解和运用文中的技术概念,并可以给你带来具有足够启迪的思考......
配套开源项目资料
Github: github.com/SunLtd/Lear…
Gitee: gitee.com/sunyiyi/Lea…
我们知道,计算机中的一切文件(文本、图片、视频等)在存储时,都是以二进制的形式保存的,即一个一个的字节,在传输时也同样如此。在IO流中有一种字节流,这种流会以字节(8bit)为单位进行数据的按序传输,一次读取或写入一个字节(8位)的二进制数据,并能处理所有类型的数据(如图片、音频、视频等)。所以,我们对任意类型的数据进行传输操作,都可以通过字节流进行实现。另外我们在操作流的时候要时刻明白,无论使用什么样的流,底层传输的数据其实始终都是二进制格式的数据。
Java中的字节流,可以分为字节输入流(InputStream)和字节输出流(OutputStream),输入流用于读取数据,输出流用于写入数据,接下来就让我们来逐个了解吧。
一. InputStream字节输入流
1. 简介
InputStream是一个用于从源中读取字节数据的抽象类,它提供了一系列方法用于从不同的输入源中读取字节数据。Java中的很多字节输入流都是它的子类,比如FileInputStream、ByteArrayInputStream、ObjectInputStream等,这里的每个子类都实现了不同的读取方式,以便从不同的输入源中读取字节数据。
2. 常用子类
InputStream的子类有很多,但常用的子类有如下几个:
- FileInputStream类:从文件中读取数据;
- ByteArrayInputStream类:将字节数组转换为字节输入流,从中读取字节;
- ObjectInputStream类:将对象反序列化;
- PipedInputStream类:连接到一个PipedOutputStream(管道输出流)对象;
- SequenceInputStream类:将多个字节输入流串联成一个字节输入流。
3. 常用方法
对日常开发来说,InputStream有如下几个常用的方法需要我们掌握。
方法名及返回值类型 | 说明 |
---|---|
int read() | 从输入流中读取一个 8 位的字节,并把它转换为 0~255 的整数。最后返回整数,如返回 -1,表示已经到了输入流的末尾,就不能再读了。注意,该方法使用较少。 |
int read(byte[] b) | 从输入流中读取若干字节,并把它们保存到参数 b 指定的字节数组中。 最后返回读取的字节数,如果返回 -1,则表示已经到了输入流的末尾。该方法使用较多。 |
int read(byte[] b, int off, int len) | 从输入流中读取若干字节,并把它们保存到参数 b 指定的字节数组中。其中,off 表示字节数组中开始保存数据的起始下标;len表示要读取的字节数。最后返回实际读取的字节数,如果返回 -1,则表示已经到了输入流的末尾 |
void close() | 关闭输入流,释放与这个输入流相关的资源。注意,InputStream类本身的close() 方法没有执行任何操作,是由它的许多子类重写了close()方法。 |
int available() | 返回可以从输入流中读取到的字节数。 |
long skip(long n) | 从输入流中跳过参数 n 指定数目的字节,最终返回要跳过的字节数。 |
void mark(int readLimit) | 在输入流的当前位置开始设置标记,readLimit参数指定了最多可以被设置标记的字节数。 |
boolean markSupported() | 判断当前输入流是否允许设置标记,是则返回 true,否则返回 false。 |
void reset() | 将输入流的指针,返回到设置标记的起始处。 |
但我们在使用 mark() 方法和 reset() 方法之前,需要判断该文件系统是否支持这两个方法,以避免对程序造成影响。
4. 实现步骤
一般情况下,InputStream流的基本使用有如下几个步骤:
创建一个InputStream对象; 调用InputStream对象的read()方法来读取数据; 使用处理数据的逻辑,对读取到的数据进行处理; 关闭InputStream对象,释放资源。
我们在开发时,只需要记住以上几个步骤进行代码的填充就可以了。接下来我们就在下面的案例中,看看这些步骤该如何具体实现吧。
5. 代码案例
因为InputStream是一个抽象类,所以我们在实际使用时都是根据自己的实际需要,使用某个具体的子类进行IO流的开发。
5.1 read()读取文件内容
比如我们现在想读取一个文件中的内容,就可以使用InputStream的子类之一FileInputStream。为了方便操作,壹哥首先在F盘中创建一个记事本文件a.txt,里面有若干信息。一个简单的读取操作,代码如下:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Demo01 {
public static void main(String[] args) {
try {
//创建文件字节输入流对象,对接指定的文件路径
FileInputStream fis = new FileInputStream("F:/a.txt");
//一次读取一个字节,返回该字节对应的ASCII值,如果到了流的末尾则返回-1
System.out.println(fis.read());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在这个案例中,壹哥只是用read()方法简单读了一下a.txt的文件内容,该方式只能得到所读内容的ASCII值,但并不符合大多数开发需求。如果想符合开发的实际需求,我们一般是利用read(byte[] b)或read(byte[] b, int off, int len)方法,接下来请继续往下看。
5.2 read(byte[])读取文件内容
上面的案例其实不符合实际的开发需求,因为我们不可能每次只读取出来一个字节就完事了,肯定是想把文件中的所有内容都读取出来,此时就需要使用read(byte[] b)等方法。
//创建文件字节输入流
FileInputStream fis = new FileInputStream("F:/a.txt");
//read(byte[]):从流中一次读取自定义缓冲区大小的字节,并返回读取到的字节长度,
//如果读到流的末尾则返回-1
// 自定义缓冲区
byte[] buf = new byte[1024];
int len = fis.read(buf);
// 将byte数组转换成String字符串
String str = new String(buf, 0, len);
System.out.println(str);
read(byte[])方法会从流中一次读取到自定义缓冲区大小的字节,并返回读取到的字节长度,如果读到流的末尾则返回-1,后面就不会再读了。
但是这个案例也不太好,如果文件的内容比较少,数据在缓冲区的范围内是没问题的。但如果文件中的数据较多,这种方式的读取效率也很低下,所以我们需要继续对其改造。
5.3 通过循环读取文件内容
实际上,我们在利用输入流进行信息的读取时,一般都是通过一个while循环来实现,如下所示:
//创建文件字节输入流
FileInputStream fis = new FileInputStream("F:/a.txt");
// read(byte[]):从流中一次读取自定义缓冲区大小的字节,并返回读取到的字节长度,如果读到流的末尾则返回-1
// 自定义缓冲区
byte[] buf = new byte[1024];
int len;
while ((len = fis.read(buf)) != -1) {
String str = new String(buf, 0, len);
System.out.println(str);
}
这样,我们就通过一个while循环不停地进行文件的读取,最终把文件内容读取完毕了。
5.4 关闭IO流
虽然我们现在已经把文件的内容完整得读取完毕了,但该代码并不是最完善的。对于项目中用到的IO流,在使用完之后应该给与关闭,一般的实现代码如下:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Demo01 {
public static void main(String[] args) {
FileInputStream fis = null;
try {
//1.创建文件字节输入流
fis = new FileInputStream("F:/a.txt");
byte[] buf = new byte[1024];
int len;
//2.read读取数据
while ((len = fis.read(buf)) != -1) {
//3.处理读到的数据
String str = new String(buf, 0, len);
System.out.println(str);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//4.关闭IO流对象
try {
if (fis != null) {
fis.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
以上代码就是一般开发时利用IO流进行文件读取的实现步骤,但有细心的同学会发现,这样的代码写起来真的挺复杂,尤其是最后的finally代码块尤其繁琐。
5.5 自动资源释放
不知道大家还能不能想起来壹哥之前讲解try-catch异常处理时的内容,在JDK 7中有一个可以自动关闭资源文件的新特性--自动资源管理(Automatic Resource Management,简称ARM) 。利用该特性,我们就可以在处理异常时正确地管理IO流等资源,避免因为忘记关闭资源而导致一些问题。所以上面的代码,如果改成try(resource)的写法,代码如下:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class Demo02 {
public static void main(String[] args) throws FileNotFoundException, IOException {
try (InputStream fis = new FileInputStream("F:/a.txt")) {
byte[] buf = new byte[1024];
int len;
while ((len = fis.read(buf)) != -1) {
String str = new String(buf, 0, len);
System.out.println(str);
}
} // 编译器会自动为我们写入finally,并调用close()方法
}
}
这样一来,我们的代码看起来就很清爽了,大家以后就可以这么编写IO流的代码啦。但大家要注意,编译器并不只是特意为InputStream添加自动关闭功能的。实际上,只要try(resource)中的对象实现了java.lang.AutoCloseable接口,编译器就会自动添加finally语句并调用close()方法。因为InputStream和OutputStream都实现了AutoCloseable接口,所以我们都可以使用try(resource)。
这样,壹哥就通过层层递进的方式,给大家讲清楚了该如何正确地实现文件的读取,现在你会了吗?
二. OutputStream字节输出流
1. 简介
OutputStream是Java标准库中的基本输出流,是所有输出流的抽象父类,用于从流中写入一个或一批字节到外部文件中。Java中的很多字节输出流都是它的子类,比如FileOutputStream、ByteArrayOutputStream、ObjectOutputStream等,这里的每个子类都实现了不同的写入方式,以便把内存中的信息写入到不同的外部设备中。
2. 常用子类
OutputStream的子类有很多,常用的子类有如下几个:
- FileOutputStream类:向文件中写入数据;
- ByteArrayOutputStream类:向内存缓冲区的字节数组中写入数据;
- ObjectOutputStream类:将对象序列化;
- PipedOutputStream类:连接到一个PipedlntputStream(管道输入流)对象。
3. 常用方法
对日常开发来说,OutputStream有如下几个常用的方法需要我们掌握。
方法名及返回值类型 | 说明 |
---|---|
void write(int b) | 向输出流写入一个字节。该方法使用较少。 |
void write(byte[] b) | 把字节数组参数中的所有字节写到输出流中。 |
void write(byte[] b,int off,int len) | 把字节数组参数中的若干字节写到输出流中。off表示字节数组的起始下标,len表示元素的个数。 |
void close() | 关闭输出流。写操作完成后,应该关闭输出流,释放该输出流占用的资源。 |
void flush() | 强制将缓冲区中的数据写入到输出流,并清空缓冲区。设置缓冲区主要是为了提高写入效率,在向输出流写入数据时,数据一般会先保存到内存缓冲区中,当缓冲区中的数据达到一定程度时就会被写入到输出流中。 |
4. 实现步骤
一般情况下,OutputStream流的基本使用有如下几个步骤:
创建一个OutputStream对象,可以是任何子类的实例; 使用write()方法将数据写入到输出流; 使用flush()方法将数据刷新到输出流中; 最后,使用close()方法关闭输出流。
接下来我们还是在下面的案例中,看看这些步骤该如何具体实现吧。
5. 代码案例
5.1 write写入信息到文件中
在下面的案例中,壹哥会创建一个 FileOutputStream对象,并将字符串 "Hello, 一一哥!" 写入到输出流中。最后,我们会刷新数据到输出流中,并关闭和释放该输出流。
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class Demo03 {
public static void main(String[] args) throws FileNotFoundException, IOException {
FileOutputStream output=null;
try {
//1.创建一个FileOutputStream对象
output = new FileOutputStream("F:/output.txt");
//如果想要向文件中追加内容,可以把第二个参数设置为true
//new FileOutputStream("F:/output.txt", true);
//2.写入数据到输出流中
output.write("Hello, 一一哥!".getBytes());
//3.刷新数据到输出流中
output.flush();
} catch (IOException e) {
e.printStackTrace();
}finally {
//4.关闭IO流
if(output!=null) {
output.close();
}
}
}
}
大家注意,如果我们想要向文件中追加内容,可以把FileOutputStream构造方法的第二个参数设置为true。
5.2 自动资源释放
其实,上面的代码同样很繁琐,所以我们也可以通过自动资源释放机制来简化代码,我们把上面的代码改造之后,如下所示:
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class Demo04 {
public static void main(String[] args) throws FileNotFoundException, IOException {
//1.创建一个FileOutputStream对象
try(FileOutputStream output = new FileOutputStream("F:/output.txt")) {
//2.写入数据到输出流中
output.write("Hello, 一一哥Java!".getBytes());
//3.刷新数据到输出流中
output.flush();
} catch (IOException e) {
e.printStackTrace();
}//不用在finally中关闭OutputStream流了,会自动关闭
}
}
这样经过简化之后,OutputStream流的代码就显得很清爽了。
6. 小结
通过上面的一些案例,我们已经学会了IO流的基本用法,但在使用IO流时,有一些小细节需要我们注意一下:
- 在使用FileOutputStream文件输出流时,如果文件不存在则会自动创建,但要保证其父目录存在;
- 在使用FileOutputStream文件输出流时,如果想要向文件中追加内容,可以将构造参数的append属性设置为true;
- 在使用IO流读写时,读写操作应当写在try代码块中,关闭资源的代码应写在finally代码块中;
- 利用自动资源释放机制,将IO流的创建写在try()中,这样IO流在使用完成之后就无需关闭了。
7. 配套视频
与本节内容配套的视频链接如下:
player.bilibili.com/player.html…
三. 字节缓冲流
1. 简介
我们在进行文件读写操作时,虽然可以直接使用InputStream和OutputStream进行文件的读写,但这种方式效率并不高,因为是一个字节一个字节进行的读取,及时设置了缓冲区,效率也并不高。所以为了减少访问磁盘的次数,提高IO访问效率,Java中还给我们提供了字节缓冲流,配套字节流进行文件的读写操作。
这种字节缓冲流是Java中一种很高效的IO流,可以实现对二进制数据的快速读取和写入。在字节缓冲流的内部维护了一个缓冲区,通过将数据读入缓冲区进行处理,可以一次读取或写入多个字节。因为是使用缓冲区进行数据的读写,避免了频繁地访问磁盘,从而减少了IO操作的次数,提高了效率。所以这种字节缓冲流就比较适合读写大量的数据,尤其是处理大文件,其性能的提升更为显著。
2. 常用子类
Java中的字节缓冲流可以分为缓冲的字节输入流BufferedInputStream和缓冲的字节输出流BufferedOutputStream。
- BufferedInputStream:继承自FilterInputStream类, 用于读取二进制数据,并将数据存储在内部缓冲区中;
- BufferedOutputStream:继承自FilterOutputStream类,用于写入二进制数据,并将数据存储在内部缓冲区中。
3. BufferedInputStream的用法
BufferedOutputStream是字节缓冲输出流,它可以将数据写入底层输出流,并缓冲数据以提高写入效率。以下是使用BufferedOutputStream写入文件的示例代码:
import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class Demo05 {
public static void main(String[] args) throws FileNotFoundException, IOException {
try {
//1.创建一个文件字节输入流对象
FileInputStream fileInputStream = new FileInputStream("F:/a.txt");
//2.创建一个字节缓冲流对象,将该对象与FileInputStream套接在一起
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
//3.开始进行数据的读取
int data = bufferedInputStream.read();
while (data != -1) {
System.out.print((char) data);
data = bufferedInputStream.read();
}
//4.为了简便,我就直接把close方法在这里操作了,大家可以在finally中或使用try(resource)的写法
fileInputStream.close();
bufferedInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上面的代码中,我们创建了一个BufferedInputStream对象,它使用FileInputStream作为底层输入流。然后我们又创建了一个byte数组作为缓冲区,并使用read()方法从输入流中读取数据,并将读取到的字节数存储在缓冲区中。在while循环中,我们不断地从输入流中读取数据,并在读取完成后进行数据的处理。
4. BufferedOutputStream的用法
BufferedOutputStream是字节缓冲输出流,它可以将数据写入底层输出流,并缓冲数据以提高写入效率。以下是使用BufferedOutputStream写入文件的示例代码,壹哥在这里使用了try(resource)的写法:
import java.io.BufferedOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class Demo06 {
public static void main(String[] args) throws FileNotFoundException, IOException {
//try(resource)的写法
try (BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("F:/b.txt"))){
String message = "Hello, 一一哥!";
byte[] bytes = message.getBytes();
//写入数据
bos.write(bytes);
//刷新缓存
bos.flush();
//不需要close
//bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在上面的代码中,我们首先创建了一个BufferedOutputStream对象,它使用FileOutputStream作为底层输出流。然后,我们又将字符串信息转为了byte数组,并使用write()方法将数据写入到输出流中,最后刷新了一下缓冲区。
3. 综合案例-文件复制
学习了以上这些内容之后,接下来壹哥通过一个综合案例,来把上面的这几个流都使用一下。在接下来的这个案例中,壹哥会把F盘中一个200多M的视频文件,复制一份,咱们来看看这样的效果该怎么实现吧。
实现代码如下:
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class Demo06 {
public static void main(String[] args) throws FileNotFoundException, IOException {
// 记录本次任务的开始时间
long start = System.currentTimeMillis();
FileInputStream fis = null;
BufferedInputStream bis = null;
FileOutputStream fos = null;
BufferedOutputStream bos = null;
try{
//1.创建输入流对象
fis = new FileInputStream("F:/payVideo/支付宝支付实现流程讲解.wmv");
// 第2个参数是缓冲区的大小
bis = new BufferedInputStream(fis, 80 * 1024);
//2.创建输出流对象
fos = new FileOutputStream("F:/payVideo2/支付宝支付实现流程讲解2.wmv");
bos = new BufferedOutputStream(fos, 80 * 1024);
byte[] buf = new byte[1024];
int len;
//3.从源文件中进行读取
while ((len = bis.read(buf)) != -1) {
//4.写入到目标文件
bos.write(buf, 0, len);
// 刷新一次就相当于在磁盘之间进行一次IO操作,这样会降低效率,所以这里可以关闭掉
// bos.flush();
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (fis != null) {
fis.close();
}
if (bis != null) {
bis.close();
}
if (bos != null) {
bos.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
//记录任务的结束时间
long end = System.currentTimeMillis();
// 缓冲流耗时:224毫秒
System.out.println("缓冲流耗时:" + (end - start));
}
}
从上面代码的执行结果可以得知,一个200多M的文件,只需要200多毫秒就可以复制完成,这个速度还是挺快的。当然大家可以测试一下,更大的文件复制速度如何,可以在评论区跟我说一下你的测试结果哦。另外需要注意,要复制到的文件夹路径如果不存在,需要你提前手动创建出来,否则可能会出现FileNotFoundException异常。
因为上面的代码,用到了多个IO流,所以这个try-catch-finally代码块结构看起来就比较繁琐,你可以把上面的代码改造成try(resource)的形式进行简化。
------------------------------正片已结束,来根事后烟----------------------------
四. 结语
这样,壹哥在今天的文章中,给大家讲解了IO流中字节流的常规用法,当然还有另外的几个字节流子类,其用法没有给大家讲到。但这些类的基本使用都大同小异,大家可以自行摸索一下,如果你遇到了一些问题,可以给我私信或评论留言。在下一篇文章中,壹哥会继续带大家学习IO流中的字符流,敬请期待哦。
另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。