Java I/O 流

标签: java  io

摘要

输入输出(I/O)是程序编写时不可避免的重要基础操作。但是各种计算语言对于I/O操作都有自己的优劣。本文主要介绍了Java的I/O流,作为自己的阶段学习理解。

简介

本文主要介绍Java I/O流体系结构,分为如下几个部分。

  • 第一部分 流以及I/O流的相关概念
  • 第二部分 Java的I/O流概述
  • 第三部分 磁盘操作I/O
  • 第四部分 字节流的理解
  • 第五部分 字符流的理解
  • 第六部分 其他I/O类的理解
  • 参考文献与文章

(一) 流概念与I/O流

输入输出与流的概念

流是一种抽象概念,它代表了数据的无结构化传递。按照流的方式进行输入输出,数据被当成无结构的字节或者字符序列。输入和输出操作的流称之为IO流。

输入输出(I/O)指的是计算机同设备之间的数据传递。常见的输入输出设备有文件,键盘,打印机和屏幕等。数据可以按照记录的形式(或称数据块)的方式传递,也可以按照的方式传递。所谓记录,指的是有用内部机构的数据块。记录内部除了需要处理的实际数据以外,还可能包含附加信息,这些附加信息通常包含对本记录的描述。

数据的表示形式——编码

IO操作过程中,任何被传递的数据,在经过I/O类库处理前后是不同的。我们可以将其分为两种:内部表示和外部表示

数据的内部表示便于程序进行数据处理。典型的内部表示二进制,浮点的IEEE表示,字符的ASCII或者Unicode表示。数据的外部表示则是由外部设备决定的。如果外部数据表示是可读的字符序列,则称为文本IO;否则为二进制表示。表示IO主要支持文本IO,而非二进制IO。

虽然IO流是以流的方式进行数据传递,但这并不表明传递的数据不能有任何结构,而是指IO流的概念是以流的方式进行输入输出,所传递数据的内部结构隐藏在对流数据的解释中。
注 : 这一部分的内容,主要来自百度百科,链接为IO流

(二)Java的I/O流体系

Java 为 I/O 提供了强大的而灵活的支持,使其更广泛地应用到文件传输和网络编程中。下图,是自己理解的Java I/O相关的内容:
Java I/O 体系
其中特殊的流:标准输入流(System.in),标准输出流(System.out),标准错误流(System.err) 都可以与其他相应的输入输出流进行对应套接。
同时,提及I/O与之相关的不得不提及的便是对象的序列化与反序列化操作,该部分在另外一篇文章中进行了较为详细的讨论。
## 字节流与字符流
二者的主要区别在于处理的数据类型与处理数据块的大小。

  • 读写单位不同: 字节流以字节(8bit)为单位进行读取;字符流以字符为单位,根据编码表的不同字符包含的字节数量不同,在Java中字节流单位是byte,字符流的读取单位是char.
  • 处理对象不同: 字节流能处理所有的数据类型;字符流仅仅能够处理字符类型的数据。例如,图片,视频等字节流可以处理,但是字符流不能。

(三)磁盘操作I/O

文本与文本文件

java的文本(char)指的是16位无符号整数,是字符的unicode编码(双字节编码)。文件指的是字节序列, 文本文件是文本(char)序列按照某种编码方案(utf-8, utf-16be,gbk)序列化为byte的存储结果。
相关操作,如下代码所示:

public class FileTest {
    public static void main(String[] args) throws Exception{
        /**
         * /F:/Source/eclipse/java-base/target/
         * /F:/Source/eclipse/java-base/target/
         * 获取当前项目根目录的两种方式, 注意在静态方法中不能使用this关键字代替类名, 项目编译输出的根目录
         */
//      String str = FileTest.class.getClassLoader().getResource("").getPath();
//      System.out.println(str);
//      String str1 = FileTest.class.getClassLoader().getResource(".").getPath();
//      System.out.println(str1);
        String path = FileTest.class.getClassLoader().getResource(".").getPath() + "res/";
        File file = new File(path);
        if(!file.exists()) file.mkdirs();
        if(file.isDirectory()){
            System.out.println("This is a directory");
        }else if(file.isFile()){
            System.out.println("This is a file");
        }else{
            System.out.println("What ??");
        }
        File file1 = new File(file, "new.txt");
        if(!file1.exists()){
            boolean fileExist = file1.createNewFile();
            System.out.println("Not exist, create = " + fileExist);
        }else{
            System.out.println("Exist, delete = " + file1.delete());
        }

        System.out.println(file1); // file对象,也就是toString() , 绝对路径,
        System.out.println(file1.getCanonicalPath()); // 打印file的绝对路径
        System.out.println(file1.getAbsolutePath()); // 打印file的绝对路径
        //这里需要说明的是Java项目和Java Web项目中以上两个路径的打印结果不同
        System.out.println(File.separator); //打印file路径使用的分割符

        System.out.println("File 测试");
        /***************************************************************
         *  File常用API 以及 自己给出简单的常用封装  测试
         **************************************************************/
        listDirectory(file);
    }
    /***************************************************************
     *  File常用API 以及 自己给出简单的常用封装
     **************************************************************/
    /**
     * 遍历所有的文件和目录
     * @param dir
     * @throws IllegalArgumentException if {@code dir} does not exist 
     * @throws IllegalArgumentException if {@code dir} 
     */
    public static void listDirectory(File dir) throws IllegalArgumentException{
        if(!dir.exists()){
            throw new IllegalArgumentException("目录:" + dir + "不存在");
        }
        if(!dir.isDirectory()){
            throw new IllegalArgumentException(dir + "不是一个目录");
        }
//      for(String s : dir.list()){ // list() // 返回文件路径的字符串数组
//          System.out.println(dir + s);
//          //System.out.println(s);
//      }
        for(File file : dir.listFiles()){ // listFiles() 返回该目录下的所有文件对象(文件和文件夹抽象成的文件对象)
            if(file.isDirectory()){
               listDirectory(file); // 递归调用
            }else{
                System.out.println(file);
            }
        }
    }
}

有关磁盘文件的读写曹祖,在下面的代码中有所体现,这里就不再重复。

(四) 字节流

在字节流的类继承体系中,Inpustream和OutputStream是所有字节I/O流类的父类,它们是字节流的基本抽象类,同时也是最重要的两个类,在这里将着重讨论。InputStream(OutputStream)定义了对于数据的基本读(写)操作,后续继承该类的具有特定功能的读(写)操作都是对与该类中基本操作的组合封装。其中ByteArrayInputStream、StringBufferInputStream、FileInputStream是三种基本的介质流,它们分别从Byte数组、串缓冲区(StringBuffer)、和本地文件中读取数据。PipedInputStream从与其它线程共用的管道中读取数据;ObjectInputStream主要用于数据的序列化与反序列化操作,它和所有FilterInputStream的子类一样,都是装饰流(通过装饰器模式实现),也就是说FileInputStream类可以通过一个String路径名创建一个对象,例如,FileInputStream(String name),而DataInputStream必须装饰一个类才能返回一个对象(也就是传入一个相应的对象),例如,DataInputStream(InputStream in)。与字节输入流相对应的,ByteArrayOutputStream、FileOutputStream是两种基本的介质流,它们分别向Byte数组和本地文件中写入数据。PipedOutputStream 是向与其它线程共用的管道中写入数据,ObjectOutputStream 和所有FilterOutputStream的子类都是装饰流。

下面,我们将通过源码以及代码示例着重分析InpuStream与OutputStream,从而分析字节流的基本操作。

首先, 分析InpuStream的源代码,为了凸显关注的重点,这里忽略了一些代码与注释。

public abstract class InputStream implements Closeable {
    /**
     * Reads the next byte of data from the input stream. The value byte is
     * returned as an <code>int</code> in the range <code>0</code> to
     * <code>255</code>. If no byte is available because the end of the stream
     * has been reached, the value <code>-1</code> is returned. This method
     * blocks until input data is available, the end of the stream is detected,
     * or an exception is thrown.
     *
     * <p> A subclass must provide an implementation of this method.
     */
    public abstract int read() throws IOException;

    /**
     * Reads some number of bytes from the input stream and stores them into
     * the buffer array <code>b</code>. The number of bytes actually read is
     * returned as an integer.  This method blocks until input data is
     * available, end of file is detected, or an exception is thrown.
     *
     */
    public int read(byte b[]) throws IOException {
        return read(b, 0, b.length);
    }

    /**
     * Reads up to <code>len</code> bytes of data from the input stream into
     * an array of bytes.  An attempt is made to read as many as
     * <code>len</code> bytes, but a smaller number may be read.
     * The number of bytes actually read is returned as an integer.
     *
     */
    public int read(byte b[], int off, int len) throws IOException {
        if (b == null) {
            throw new NullPointerException();
        } else if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException();
        } else if (len == 0) {
            return 0;
        }

        int c = read();
        if (c == -1) {
            return -1;
        }
        b[off] = (byte)c;

        int i = 1;
        try {
            for (; i < len ; i++) {
                c = read();
                if (c == -1) {
                    break;
                }
                b[off + i] = (byte)c;
            }
        } catch (IOException ee) {
        }
        return i;
    }
}

注重关注三个方法抽象方法:

  • int read() : 读取一个字节
  • int read(byte[] b[]) : 读取特定个字节,放入缓存数组中
  • int read(byte[], int off, int len) : 读取特定长度len个字节,从位置off开始放入缓存数组中

第一个空参read方法为抽象方法,这是考虑到不同的子类默认情况下可能具有不同的实现。例如,ObjectInputStream的默认空参read方法是调用其内部私有读方法实现,而FileInputStream则是调用具有naive关键字修饰的本地read方法实现,这中实现方式是出于性能和子类的特点考虑的。与空参read方法相对,其他两个带参read方法则是所有字节输入流共有的且功能和性能一致,因此,字节输入流的顶层父类InputStream负责实现了这两个方法。同样,与之对应的在字节输出流的类继承体系中,顶层父类OutputStream的有一个抽象的write(int b)方法(注意这里实现仅仅写入一个字节),以及两个不同签名的带参写方法write(byte[] b)和write(byte[] b, int off, int len)

  • void write(int b) : 将一个字节写入输出流
  • void write(byte b[]) : 将字节数组中的所有数据写入输出流
  • void write(byte b[], int off, int len) : 将在字节数组中从off位置开始且长度为len的数据写入输出流

以下是相关的代码示例,通过输入流进行数据读写:

public class InputStreamTest {
    /**
     * 读取指定文件内容,按照16进制输出到控制台
     * 并且每输出10个byte换行
     * @param fileName
     * 单字节读取不适合大文件,大文件效率很低
     */
    public static void printHex(String fileName)throws IOException{
        //把文件作为字节流进行读操作
        InputStream in = new FileInputStream(fileName);
        int b ;
        int i = 1;
        while((b = in.read())!=-1){
            if(b <= 0xf){
                //单位数前面补0
                System.out.print("0");
            }
            System.out.print(Integer.toHexString(b)+"  ");
            if(i++%10==0){
                System.out.println();
            }
        }
        in.close();
    }
    /**
     * 批量读取,对大文件而言效率高,也是我们最常用的读文件的方式
     * @param fileName
     * @throws IOException
     */
    public static void printHexByByteArray(String fileName)throws IOException{
        InputStream in = new FileInputStream(fileName);
        byte[] buf = new byte[8 * 1024];
        /*从in中批量读取字节,放入到buf这个字节数组中,
         * 从第0个位置开始放,最多放buf.length个 
         * 返回的是读到的字节的个数
        */
        /*int bytes = in.read(buf,0,buf.length);//一次性读完,说明字节数组足够大
        int j = 1; 
        for(int i = 0; i < bytes;i++){
            System.out.print(Integer.toHexString(buf[i] & 0xff)+"  ");
            if(j++%10==0){
                System.out.println();
            }
        }*/
      int bytes = 0;
      int j = 1;
      while((bytes = in.read(buf,0,buf.length))!=-1){
          for(int i = 0 ; i < bytes;i++){
              System.out.print(Integer.toHexString(buf[i] & 0xff)+"  ");
              if(j++%10==0){
                  System.out.println();
              }
          }
      }
      in.close();
    }
    /**
     * 文件拷贝,字节批量读取
     * @param srcFile
     * @param destFile
     * @throws IOException
     */
    public static void copyFile(File srcFile,File destFile)throws IOException{
        if(!srcFile.exists()){
            throw new IllegalArgumentException("文件:"+srcFile+"不存在");
        }
        if(!srcFile.isFile()){
            throw new IllegalArgumentException(srcFile+"不是文件");
        }
        InputStream in = new FileInputStream(srcFile);
        OutputStream out = new FileOutputStream(destFile);
        byte[] buf = new byte[8*1024];
        int b ;
        while((b = in.read(buf,0,buf.length))!=-1){
            out.write(buf,0,b);
            out.flush();//最好加上
        }
        in.close();
        out.close();

    }
    /**
     * 进行文件的拷贝,利用带缓冲的字节流
     * @param srcFile
     * @param destFile
     * @throws IOException
     */
    public static void copyFileByBuffer(File srcFile,File destFile)throws IOException{
        if(!srcFile.exists()){
            throw new IllegalArgumentException("文件:"+srcFile+"不存在");
        }
        if(!srcFile.isFile()){
            throw new IllegalArgumentException(srcFile+"不是文件");
        }
        InputStream bis = new BufferedInputStream(
                new FileInputStream(srcFile));  // IO流的套接
        OutputStream bos = new BufferedOutputStream(
                new FileOutputStream(destFile));
        int c ;
        while((c = bis.read())!=-1){
            bos.write(c);
            bos.flush();//刷新缓冲区
        }
        bis.close();
        bos.close();
    }
    /**
     * 单字节,不带缓冲进行文件拷贝
     * @param srcFile
     * @param destFile
     * @throws IOException
     */
    public static void copyFileByByte(File srcFile,File destFile)throws IOException{
        if(!srcFile.exists()){
            throw new IllegalArgumentException("文件:"+srcFile+"不存在");
        }
        if(!srcFile.isFile()){
            throw new IllegalArgumentException(srcFile+"不是文件");
        }
        InputStream in = new FileInputStream(srcFile);
        OutputStream out = new FileOutputStream(destFile);
        int c ;
        while((c = in.read())!=-1){
            out.write(c);
            out.flush();
        }
        in.close();
        out.close();
    }


    public static void main(String[] args) throws Exception{
        String filePath = "res/ost.dat";
        String distPath = "res/dist.dat";
        printHex(filePath);
        printHexByByteArray(filePath);
        copyFile(new File(filePath), new File(distPath));
        copyFileByBuffer(new File(filePath), new File(distPath));
        copyFileByByte(new File(filePath), new File(distPath));
    }
}

这其中体现了Java I/O的套接思想,也就是采用了装饰模式。

(五) 字符流的理解

与字节流类似,字符流类继承体系中同样有两个顶层父类: Reader和Writer。与字节流以字节为单位读取(写入),字符流是以文本为单位的,也就是字符为单位(Unicode编码)。抽象类Reader(Writer)定义了输入流(输出流)类继承体系中的基本读取(写入)方法。
Reader :

  • int read(): 读取单个字符
  • int read(char[] cBuf) : 读取特定长度的字符串放入缓存数组
  • int read(char[] cBuf, int off, int len) : 读取特定长度len的字符串,从位置off开始放入缓存数组

Writer :

  • void write(char[] cbuf) 将字符数组写入输出流
  • void write(int c) 将单个字符写入输出流
  • void write(String str) 将字符串写入输出流
  • void write(String str, int off, int len) 从位置off开始,将str长度为len的字串写入输出流

其中,第一个方法为抽象方法,其原因同InputStream的抽象方法一样。同样,后两者的也进行了相应的实现。
如下代码,展示了字符流的相关操作:

public class IsrAndOswDemo {
    public static void main(String[] args)throws IOException {
        FileInputStream in = new FileInputStream("res/osd.dat");
        InputStreamReader isr = new InputStreamReader(in,"utf-8");//默认项目的编码,操作的时候,要写文件本身的编码格式
        FileOutputStream out = new FileOutputStream("res/dist.dat");
        OutputStreamWriter osw = new OutputStreamWriter(out,"utf-8");
        /*int c ;
        while((c = isr.read())!=-1){
            System.out.print((char)c);
        }*/
        char[] buffer = new char[8*1024];
        int c;
        /*批量读取,放入buffer这个字符数组,从第0个位置开始放置,最多放buffer.length个
          返回的是读到的字符的个数
        */
        while(( c = isr.read(buffer,0,buffer.length))!=-1){
            String s = new String(buffer,0,c);
            System.out.print(s);
            osw.write(buffer,0,c);
            osw.flush();
        }
        isr.close();
        osw.close();
    }
}

(六) 其他I/O流的理解

Java的I/O操作类在包java.io下,大概拥有近80个类。理解部分仅仅是字节流和字符流的部分。其中管道流仅仅是提及并没有深入探讨,因为把自己这部分并没有深入了解。同时,对于套接字(Socket),网络编程接口I/O,也没有提及,这两个部分将会单独成文,深度理解。

总结

本文仅仅学习了几年Java后,才敢写出自己对于Java I/O的简单理解,但是仍旧不全面,不深刻,同样也会有错误出现,后续会陆续出现补文。

更加详细的有关性能调优的资料可以看官网,以及下列文章。

参考资料

版权声明:本文为u010455714原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u010455714/article/details/77541194

智能推荐

【Network Security!】信息的扫描与嗅探

文章目录 确定扫描目标 确定目标主机IP地址 网站架构探测 确定扫描目标 确定目标主机IP地址 1.获取本机IP地址 2.获取网站地址信息 3.获取指定网站的IP地址 4.确定可能开放的端口和服务 Zenmap是一款非常流行的端口扫描软件(nmap的GUI界面化)。它是用Python语言编写而成的开源的图形界面,能够运行在不同操作系统平台上(Windows/Linux/Unix/Mac OS)) ...

太赞了!Linux 架构师总结的学习笔记,提供下载

  本文字数:1247,阅读时长大约:1分钟 导读:你想学习 Linux 吗? 最近很多小伙伴找我要一些 Linux 基础资料,于是我翻箱倒柜,把这份技术大牛总结的 Linux 归纳笔记找出来,免费共享给大家! 据说有小伙伴靠这份笔记顺利进入 BAT 哦,所以一定要好好学习这份资料! 资料介绍 这份资料非常全面且详细,从 Linux 常用命令到 Linux 常用操作,再到shell编程、...

【底层原理】高级开发必须懂的"字节对齐"

认识字节对齐之前,假定int(4Byte),char(1Byte),short(2Byte) 认识字节对齐 先看段代码:   sizeof(Data1)和sizeof(Data2)分别表示Data1和Data2内存占用字节数,输出结果不一样是因为编译时对Data1和Data2做了不同的字节对齐。Data1的对齐为4Byte,Data2的对齐是2Byte。   假定存储起始地址为...

爬lol全英雄皮肤

初学爬虫简单的爬取一下lol全英雄皮肤,自己写的,和网上CV的好不一样,觉得文章说得过去的记得留下足迹。 一 分析页面 1.英雄列表 首先在英雄页面找到hero_list.js;至于为什么是这个文件,看图: 观察该文件响应头,获取访问的url,打开新的窗口,访问该链接,能获取对应数据。(这边显示在一行很男查看,推荐一款好用的chrome插件JSONView,可以帮帮我们格式化json数据,可以在g...

解决VUE项目重复点击菜单报错:Avoided redundant navigation to current location: “/xxxxx“. 问题

描述: 报错见下图: 解决方法: 在router文件夹下添加下面一段代码...

猜你喜欢

Nginx 入门指南(十)

负载均衡模块 负载均衡模块用于从upstream指令定义的后端主机列表中选取一台主机。Nginx 先使用负载均衡模块找到一台主机,再使用 upstream 模块实现与这台主机的交互。为了方便介绍负载均衡模块,做到言之有物,以下选取 Nginx 内置的 ip hash 模块作为实际例子进行分析。 配置 要了解负载均衡模块的开发方法,首先需要了解负载均衡模块的使用方法。因为负载均衡模块与之前书中提到的...

文本和输入:复制和粘贴

Android提供了一个功能强大的基于剪贴板的复制和粘贴框架。 它支持简单和复杂的数据类型,包括文本字符串,复杂数据结构,文本和二进制流数据,甚至应用程序资产。 简单的文本数据直接存储在剪贴板中,而复杂数据则作为粘贴应用程序与内容提供者解析的参考进行存储。 复制和粘贴在应用程序中以及在实现框架的应用程序之间工作。 由于框架的一部分使用内容提供者,因此本主题假定您熟悉Android内容提供程序API...

[unity]代码批量修改图片、文本文件的AssetBundle的Name

  当项目工程内有大量文件,需要打包成AssetBundle的时候,一个一个打包是一件非常麻烦的事情。 批量修改AssetBundle的Name并对AssetBundle包 进行批量 打包。   批量修改项目工程文件的图片、文本文件的AssetBundle   1.批量建立AssetBundle 1.1修改文件的.meta文件的文本内容 来自参考资料1   ...

[Python] 用K-means算法进行客户分群

目录 一、背景 1.项目描述 2.数据描述 二、相关模块 三、数据可视化 1.数据读取 2.数据可视化 2.1 平行坐标图 2.2 年龄/年收入/消费分数的分布 2.3 年龄/年收入/消费分数的柱状图 2.4 不同性别用户占比 2.5 两两特征之间的关系 2.6 两两特征之间的分布 四、K-means聚类分析 0.手肘法简介 1.基于年龄和消费分数的聚类 2.基于年收入和消费分数的聚类 3.基于年...

CUDA9.0+win10+Visual Studio2017版本配置安装教程

CUDA9.0+win10+Visual Studio2017版本配置安装教程 不幸掉入深度学习的深渊,从此以后无法自拔,哈哈,虽然电脑属于平民配置,但因为有师姐顶配DELL工作站的操练,可以肆无忌惮的配置安装自己想要的东西,一路踩坑,一路爬,一把辛酸泪。安装的时候特别要注意VS和CUDA版本的兼容问题,否则要么卸载重装,要么一系列的路径操作会让你绝望。 如果电脑配置够新,不妨试试安装最新版本的C...