likes
comments
collection
share

从零开始学Java之详解I/O流中的字节流

作者站长头像
站长
· 阅读数 19

前言

在前面的几篇文章中,壹哥给大家讲解了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流的基本使用有如下几个步骤:

  1. 创建一个InputStream对象;
  2. 调用InputStream对象的read()方法来读取数据;
  3. 使用处理数据的逻辑,对读取到的数据进行处理;
  4. 关闭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流的基本使用有如下几个步骤:

  1. 创建一个OutputStream对象,可以是任何子类的实例;
  2. 使用write()方法将数据写入到输出流;
  3. 使用flush()方法将数据刷新到输出流中;
  4. 最后,使用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的视频文件,复制一份,咱们来看看这样的效果该怎么实现吧。

从零开始学Java之详解I/O流中的字节流

实现代码如下:

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流中的字符流,敬请期待哦。

另外如果你独自学习觉得有很多困难,可以加入壹哥的学习互助群,大家一起交流学习。