Java I/O 流

摘要

输入输出(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的简单理解,但是仍旧不全面,不深刻,同样也会有错误出现,后续会陆续出现补文。

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

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值