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

2023年 9月 27日 53.6k 0

作者:孙玉昌,昵称【一一哥】,另外【壹壹哥】也是我哦

千锋教育高级教研员、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流中的字符流,敬请期待哦。

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

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论