JAVA—IO流详解

1. 流概述

1.1. 什么是IO

IO:Input/Output即输入&输出,也称之为流(河流,水流),指的是数据从一个地点到另一个地点过程;对于计算机来说文件的拷贝过程,文件的编写保存,显示功能都是使用到IO;将数据的传输过程看做一个输入输出。现实角度考虑可以将耳朵作为输入流,将嘴巴当做输出流

  • 输入输出介质:
    • 文件
    • 网络
    • 键盘(输入)
    • 显示器(输出)

1.2. IO分类

Java针对IO操作提供了相应的API,Java中几乎所有的IO操作都需要使用java.io包;java中对于流的分类包含各种方式:

  • 按流向分(输入输出过程通常都是站在程序角度考虑
    • 输入流(Input)
    • 输出流(Output)
  • 按流的处理类型分
    • 字节流(byte)
    • 字符流(char)
  • 按流的功能来分
    • 节点流(直接跟输入输出源交互)
    • 处理流(对其他流包装的流:包装流)

在这里插入图片描述

1.3. 顶级父类

流虽然内容繁多,但是规律性很强,几乎所有的流都从四个基本流继承而来:

输入流 输出流
字节流 java.io.InputStream java.io.OutputStream
字符流 java.io.Reader java.io.Writer

以上四个流是所有java流的顶层父类,都是抽象类;

流的类型识别规律性很强:一般以Stream结尾的都是字节流;一般以Reader/Writer结尾的流都是字符流

1.4. 使用场景

  • 文件拷贝(File)
  • 文件上传下载
  • Excel导入导出
  • 网络程序中数据传输(聊天工具)

2. 字节流

2.1. 字节概述

​ 在计算机系统中,一切都是字节:系统中存储的各种文件(文本文档,图片,视频,音频)的存储在计算机低层都是以字节的形式存储的,因此对于任何的文件操作都是可以使用一个一个字节进行操作的(读,写);java.io中的字节流顶层父类是:InputStream/OutputStream.

2.2. 字节输入流

java中字节输入流都是从java.io.InputStream继承而来。由于该类为抽象类,因此无法实例化的,所以jdk对于字节输入提供了一些能够直接使用子类:

  • FileInputStream
  • ByteArrayInputStream
  • BufferedInputStream
  • ObjectInputStream

InputStream常用方法

  • int available():获取流中可读字节数
  • int read():从流中读取一个字节,返回当前读取的字节数据
  • int read(byte[] b):将读取的字节数据存储到字节缓冲区,并返回实际的读取字节总数
  • skip(int b):跳过指定个字节发生下一次读取

2.2.1. FileInputStream

FileInputStream是一个针对字节输入流的实现流,主要用于对文件进行读取操作,内部的方法主要是针对父类的实现,

  • 常见构造器

    • FileInputStream(File file):根据提供的文件构建的对象
    • FileInputStream(String filePath):根据提供的文件路径构建对象
  • 使用FileInputStream进行文件读取操作

    1. 基本读取(每次读取一个字节)

      //创建File对象
      File file = new File("C:\\Users\\Administrator\\Desktop\\新建文本文档.txt");
      //基于File创建字节输入流
      InputStream in = new FileInputStream(file);
      
      //读取一个字节
      int i = in.read();
      System.out.println((char)i);
      

      Java的IO只能对标准文件发生读写操作,不允许直接对一个目录创建输入或者输出流(会导致java.io.IOException(拒绝访问)

    2. 使用字节缓冲区读取(缓冲区大小为总可读字节数)

      由于以上的读取方式是每次一个字节读取,因此,读取效率很低,所以实际开发中一般会使用一个字节缓冲区,提高读取效率:

      //创建File对象
      File file = new File("C:\\Users\\Administrator\\Desktop\\新建文本文档.txt");
      //基于File创建字节输入流
      InputStream in = new FileInputStream(file);
      
      //创建字节缓冲区(大小为总可读字节数)
      byte[] arr = new byte[i];
      //将流中读取的字节内容存储到数组中
      int total = in.read(arr);
      //将字节数组转换为String字符串
      System.out.println(new String(arr));
      
    3. 使用合适大小字节缓冲区读取

      以上的读取是一次性将文件中的数据读取到缓冲区中,因此,缓冲区中容量可能会很大,如果针对一个大文件文件的读取,使用一个过大的缓冲区,可能会造成空间的消耗,从而影响其他程序的执行,所以,需要一个合适大小的缓冲区进行反复读取

      //创建文件输入流对象(文件:一缸水)
      InputStream in = new FileInputStream("C:\\Users\\Administrator\\Desktop\\Hero.java");
      
      //创建缓冲区(购买一个大小适中的水桶)
      byte[] b = new byte[1024];
      //声明临时变量表示每次读取的实际字节数
      int len = 0;
      while((len = in.read(b)) != -1){
              
              
          String s = new String(b,0,len);
          System.out.println(s);
      }
      

      以上的读取方式就是对于与字节流的读取常规解决方案

2.3. 字节输出流

根据数据的流向除了可以进行读取(输入)操作之外,数据的写(输出)操作也是十分常见,java中的字节输出流都是从java.io.OutputStream继承过来

由于OutputStream是一个抽象类,无法实例化,因此jdk也提供了针对该流的子类:

  • FileOutputStream
  • ByteArrayOutputStream
  • BufferedOutputStream
  • ObjectOutputStream
  • PrintStream

OutputStream类的常见方法:

  • write(int b):将一个字节通过输出流写出到目标输出源
  • write(byte[] b):将一个字节数组通过输出流写出到目标输出源
  • write(byte[] b,int offset,int len)将一个数组的offset开始写出len个字节到目标输出源

2.3.1. FileOutputStream

FileOutputStream是一个针对字节输出流的实现流,主要用于对文件进行写入操作,内部的方法主要是针对父类的实现

  • 常用构造器

    • FileOutputStream(String filePath):基于一个标准文件的路径创建对其操作的输出流
    • FileOutputStream(String filePath,boolean append):基于一个标准文件的路径创建对其操作的输出流,使用追加模式
    • FileOutputStream(File file):基于一个标准文件的路径创建对其操作的输出流
    • FileOutputStream(File file,boolean append)基于一个标准文件对象创建对其操作的输出流,使用追加模式
  • 使用FileOutputStream进行文件写出操作

     //try...with语句:JDK1.7
            //针对所有的第三方资源不再需要手动回收(关闭)
            //因为从jdk1.7开始,很多资源类都实现过Closeable接口,但凡实现过该接口类
            //只要将其在try()中创建,不再手动关闭资源,会由JVM自动回收
            try(OutputStream os = new FileOutputStream("readme.txt",true);){
          
          
                //写出一个字节到文件中
    //            os.write(101);
    
                //荀子·劝学篇
                String msg = "不积小流无以成江海,不积跬步无以至千里";
                os.write(msg.getBytes());
    
            }catch (IOException e){
          
          
                e.printStackTrace();
            }
    

2.4. 综合案例-文件拷贝

/**
     * 将一个源文件拷贝到一个目标目录中
     * @param src 源文件
     * @param targetDir 目标目录
     */
public static void copyFile(File src, File targetDir) throws IOException {
    
    
    InputStream is = null;
    OutputStream os = null;
    try {
    
    
        //获取源文件的输入流
        is = new FileInputStream(src);
        //获取目标文件的输出流(目标文件是由:目录+源文件名称构成):文件输出流可以创建文件(前提:父目录必须存在)
        os = new FileOutputStream(new File(targetDir,src.getName()));
        //声明字节缓冲区(缓冲区越大,拷贝效率越高,但是带来空间损耗也越大)
        byte[] b = new byte[1024*1024];
        //临时变量表示实际读取的字节数
        int len = 0;
        System.out.println("开始拷贝...");
        while((len = is.read(b)) != -1){
    
    
            //写出读取的内容到输出流
            os.write(b,0,len);
        }
        System.out.println("拷贝完成!");
    }  finally{
    
    
        if(os != null){
    
    
            os.close();
        }
        if(is != null){
    
    
            is.close();
        }
    }

}

//测试
public static void main(String[] args) throws IOException {
    
    
    //源文件
    File src = new File("D:\\素材\\视频\\短视频\\test.mp4");
    //目标目录
    File targetDir = new File("C:\\Users\\Administrator\\Desktop");
    //文件拷贝
    copyFile(src,targetDir);
}

在文件拷贝的基础上实现目录拷贝:

public class FileCopy {
     
     

/**
   * 将一个源目录拷贝到另一个目标目录中
   * @param srcDir
   * @param targetDir
   */
  public static void copyDir(File srcDir,File targetDir) throws IOException {
     
     
      //获取新目录对象
      targetDir = new File(targetDir,srcDir.getName());
      //如果新目录不存在,则创建
      if(!targetDir.exists()){
     
     
          targetDir.mkdirs();
      }
      File[] files = srcDir.listFiles();
      if(Objects.nonNull(files)){
     
     
          for (File file : files) {
     
     
              if(file.isDirectory()){
     
     
                  //目录递归拷贝
                  copyDir(file,targetDir);
              }else{
     
     
                  //执行文件拷贝
                  copyFile(file,targetDir);
              }
          }
      }
  }

  /**
   * 将一个源文件拷贝到一个目标目录中
   * @param src 源文件
   * @param targetDir 目标目录
   */
  public static void copyFile(File src, File targetDir) throws IOException {
     
     
      InputStream is = null;
      OutputStream os = null;
      try {
     
     
          //获取源文件的输入流
          is = new FileInputStream(src);
          //获取目标文件的输出流(目标文件是由:目录+源文件名称构成)
          os = new FileOutputStream(new File(targetDir,src.getName()));
          //声明字节缓冲区(缓冲区越大,拷贝效率越高,但是带来空间损耗也越大)
          byte[] b = new byte[1024*1024];
          //临时变量表示实际读取的字节数
          int len = 0;
          System.out.println("开始拷贝...");
          while((len = is.read(b)) != -1){
     
     
              //写出读取的内容到输出流
             os.write(b,0,len);
          }
          System.out.println("拷贝完成!");
      }  finally{
     
     
          if(os != null){
     
     
              os.close();
          }
          if(is != null){
     
     
              is.close();
          }
      }

  }

  public static void main(String[] args) throws IOException {
     
     
      //源目录
      File srcDir = new File("D:\\素材\\视频");
      //目标目录
      File targetDir = new File("C:\\Users\\Administrator\\Desktop\\video");
      //目录拷贝
      copyDir(srcDir,targetDir);
  }
}

3. 字符流

3.1. 字符概述

通常在文本文件中,文件内容的存在形式一般是以一个个字符(一个中文汉字,一个英文字母,一个符号)的形式存在,在GBK编码模式下通常1个字符=2个字节;字符流一般适用于对文本数据的处理,java中所有的字符流几乎都是以Reader/Writer结尾,并且都是从两个基本的抽象类中继承:

  • java.io.Reader:字符输入流
  • java.io.Writer:字符输出流

3.2. 字符输入流

字符输入流一般用于对文本数据进行读取操作,常见的子类:

  • InputStreamReader
  • FileReader
  • BufferedReader
  • CharArrayReader

3.2.1. FileReader

FileReader是一个以字符流的形式进行文件内容读取的流,从InputStreamReader继承而来,内部没有多余的方法(与父类的API一致),提供一下常见的构造方法:

  • 常用构造方法

    • FileReader(File file):根据提供的文件对象获取文件字符输入流
    • FileReader(String path):根据提供的文件路径获取文件字符输入流
  • 具体使用

    //创建文件字符输入流
    FileReader fr = new FileReader("D:\\文档资料\\电子书\\书籍推荐.txt");
    
    //获取当前流使用的默认编码模式(并非目标文件的编码)
    //        String encoding = fr.getEncoding();
    //        System.out.println(encoding);
    
    //读取一个字符(以int类型存在)
    //        int read = fr.read();
    //        System.out.println((char)read);
    
    //        int c = 0;
    //        while((c = fr.read()) != -1)
    //        {
          
          
    //            System.out.print((char)c);
    //        }
    
    //使用字符缓冲区
    char[] ch = new char[100];
    int len = 0;
    while((len = fr.read(ch)) != -1){
          
          
        String s = new String(ch,0,len);
        System.out.print(s);
    }
    

3.3. 字符输出流

字符输出流一般用于对文本数据进行写出操作,常见的子类:

  • Reader`
  • OutputStreamWriter
  • BufferedWriter
  • CharArrayWriter
  • FileWriter
  • PrintWriter

3.3.1 FileWriter

FileWriter是一个以字符流的形式进行文件内容写出的流,从OutputStreamWriter继承而来,内部没有多余的方法(与父类的API一致),提供一下常见的构造方法:

  • 常用构造方法

    • FileWriter(File file):根据提供的文件对象获取文件字符输入流
    • FileWriter(String path):根据提供的文件路径获取文件字符输入流
    • FileWriter(File file,boolean append):根据提供的文件对象获取文件字符输入流(使用追加模式)
    • FileWriter(String path,boolean append):根据提供的文件路径获取文件字符输入流(使用追加模式)
  • 具体使用

    //基于指定的文件创建字符输出流
    FileWriter fw = new FileWriter("readme.txt");
    fw.write("路漫漫其修远兮,吾将上下而求索");
    
    //允许在流未关闭之前,强制将字符缓冲区中的数据写出到目标输出源
    fw.flush();
    
    Thread.sleep(10000);
    fw.write("好好xio习,天天up!!!");
    fw.close();
    

    对于字符输出流,内部使用到了一个字符缓冲区(字符数组),在进行数据写出时,通常是将需要写出的数据缓存存在了字符数组中,然后在关闭流时(或者缓冲区存满时),一次性将缓冲区的数据写出到目标输出源, 如果需要在流未关闭前(或者缓冲区未满时)强制的将字符缓冲区中的数据写出,可以手动调用flush()强制输出。

4. 处理流

对于流的处理类型(功能)来分又分为节点流和处理流:

  • 节点流

    也称之为低级流,直接跟输入输出源沟通的流(例如:FileReader,FileWriter,FileInputStream,FileOutputStream)

  • 处理流

    处理流也称之为高级流或包装流,即可以用于对其他节点流进行包装,以实现流的类型转换或者效率的提升。处理流主要由缓冲流转换流构成

包装通常使用到了一种设计模式(装饰器模式)

4.1. 缓冲流

缓冲流是一种自带缓冲区的流,主要由以下四种构成

  • BufferedInputStream:字节缓冲输入流
  • BufferedOutputStream:字节缓冲输出流
  • BufferedReader:字符缓冲输入流
  • BufferedWriter:字符缓冲输出流
long start = System.currentTimeMillis();
try(
    //获取输入流
    InputStream in = new FileInputStream("D:\\集团资料\\宣讲\\video\\云计算&大数据\\阿里云.mp4");
    //包装节点流
    BufferedInputStream bis = new BufferedInputStream(in);
    //获取输出流
    OutputStream os = new FileOutputStream("C:\\Users\\Administrator\\Desktop\\阿里云.mp4");
    //包装节点流
    BufferedOutputStream bos = new BufferedOutputStream(os);
) {
    
    
    System.out.println("开始拷贝");
    byte[] b = new byte[1024*1024*8];
    int len = 0;
    while((len = bis.read(b)) != -1){
    
    
        bos.write(b,0,len);
    }
    System.out.println("拷贝结束");
}catch (IOException e){
    
    
    e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("耗时:"+(end-start));

缓冲流内部实现原理使用了了一个默认大小为8kb的字节缓冲区,每次将缓冲区的空间存满之后再将数据通过流对象完成读写操作

4.2. 转换流

在一些需求中经常会需要将一个字节流的数据转换为字符流;又或者需要将一个字符流转换为字节流,此时就需要使用转换流来完成功能,转换流一般应用于:文件转码,从网络中读取的数据为字节流时,但是该流包含内容是字符时,可以使用转换流实现转换;java.io中转换流主要由以下两个类构成:

  • InputStreamReader:用于将字节输入流转换为字符输入流(字节->字符)
  • OutputStreamWriter:用于将字符输出流转换为字节输出流(字符->字节)

使用实例:

//获取标准输入流
InputStream is = System.in;

//将字节流转换为字符流
Reader isr = new InputStreamReader(is);
//将字符节点流包装为缓冲流
BufferedReader br = new BufferedReader(isr);
//读取一行
String line = br.readLine();
System.out.println("输入的内容--->"+line);

在这里插入图片描述

转换流只能对流的类型转换,不能对流向转换

4.3. 综合案例:文件转码工具

日常的文件拷贝中由于多个编辑器(系统)的编码模式存在差异,因此极有可能出现文件乱码问题(对于文本文件较为常见),通过转换流可以实现文件转码的功能:

public class FileCharacterConvert {
    
    

    /**
     * 将一个目标文件的编码转换为新的编码
     * @param file  原始文件
     * @param targetDir 目标目录
     * @param oldEncoding  原始编码
     * @param newEncoding   新编码
     */
    public static void convert(File file,File targetDir, String oldEncoding, String newEncoding) throws IOException {
    
    

        //使用特定的编码获取文件的输入流
        FileInputStream fis = new FileInputStream(file);
        Reader reader = new InputStreamReader(fis,oldEncoding);
        BufferedReader br = new BufferedReader(reader);

        //使用特定的编码获取文件的输出流
        FileOutputStream fow = new FileOutputStream(new File(targetDir,file.getName()));
        Writer writer = new OutputStreamWriter(fow,newEncoding);
        BufferedWriter bw = new BufferedWriter(writer);

        //开始读写
        String line = "";
        //循环读取每一行文本以换行符作为一行的结束标记(但是换行符不会被作为内容读取)
        while((line = br.readLine()) != null){
    
    
            //写入读取的一行文本()
            bw.write(line);
            //手动加入一个换行标记到文件,否则所有内容会在同一行显示
            bw.newLine();
            //将缓冲区的数据强制输出到目标输出源
            bw.flush();
        }
        bw.close();
        br.close();
    }

    public static void main(String[] args) throws IOException {
    
    
        //准备需要转换的文件
        File f = new File("C:\\Users\\Administrator\\Desktop\\GamePanel.java");
        convert(f,new File("C:\\Users\\Administrator\\Desktop\\temp"),"gbk","utf-8");
    }
}

5. 打印流

java.io,对于数据的输出提供了两个特殊的流:打印流,打印流只有输出没有输入,并且可以直接针对输出源创建,也可以将其他输出流包装,打印流主要包含以下两个流:

  • PrintStream:字节打印流
  • PrintWriter:字符打印流

5.1. PrintStream

PrintStream是继承自java.io.OutputStream,是一个用于进行字节数据输出的流,内包含了大量的print/prinln重载方法;并且System.out实际上就是一个PrintStream

  • 常见构造器

    • PrintStream(File file):基于指定的文件创建打印流
    • PrintStream(OutputStream os):将其他字节输出流包装
    • PrintStream(OutputStream os,boolean autoFlush):将其他字节输出流包装,可以自动刷新流中数据
  • 常用方法

    • print(...)
    • println(...)
  • 基本使用

    //        PrintStream ps = new PrintStream(new File("readme.txt"),true);
    
    OutputStream os = new FileOutputStream("readme.txt", true);
    PrintStream ps = new PrintStream(os,true);
    ps.println("你好中国");
    

5.2. PrintWriter

PrintWriterPrintStream区别在于,PrintWriter是基于字符的打印流(包含字符缓冲区),其API与PrintStream极其相似;在使用PrintWriter打印数据的时候记得执行flush()方法

FileWriter fw = new FileWriter("readme.txt",true);

PrintWriter pw = new PrintWriter(fw);

pw.println("哪里有彩虹告诉过我!!!");

pw.flush();

pw.close();

6. 资源文件读取(Properties类)

在后期框架的学习过程中,经常会涉及到一些配置文件的编写,其中属性文件是很常见的一种文件类型,java中对于属性文件的读写提供了一个java.util.Properties类:

InputStream in = PropertiesDemo.class.getResourceAsStream("/jdbc.properties");
//创建属性对象
Properties prop = new Properties();
//        Properties prop = System.getProperties();
//加载流到属性对象中
prop.load(in);

String user = prop.getProperty("user");
String url = prop.getProperty("url");
System.out.println(user);
System.out.println(url);

prop.setProperty("maxActive","10");

File f = new File("D:\\带班资料\\2021\\J2106\\课程资料\\code\\part1-javase\\java高级\\lesson_01_IO\\resources\\jdbc.properties");
FileWriter fw = new FileWriter(f);
prop.store(fw,"this is jdbc config file");
fw.close();

7. 流的总结

  • 所有的输入流名称中包含inputread
  • 所有的输出流名称中包含outputwrite
  • 所有的字节流名称以stream结尾
  • 所有的字符流名称以readerwriter结尾
  • 一般二进制类型的文件(图片,视频,音频,压缩文件等无法直接使用文本文档打开的文件)主要使用字节流操作
  • 一般文本类型文件(txt,md,java等可以直接使用文本文档打开的文件)主要使用字符流操作

猜你喜欢

转载自blog.csdn.net/weixin_48006490/article/details/126649763