JAVA IO基本知识

本部分总结一下JAVA IO的相关知识。

全部章节传送门:

  • JAVA IO学习笔记: IO基本知识
  • JAVA IO学习笔记: NIO介绍
  • JAVA IO学习笔记: AIO介绍
  • JAVA IO学习笔记:压缩和序列化

JAVA IO概要

JAVA I/O主要包括以下几个部分:

  1. 流式部分--IO的主体部分;
  2. 非流式部分--主要包含一些辅助流式部分的类,如File类;
  3. 其它类--文件读取部分与安全/操作系统等相关的类。

层次如下图:

JAVA IO基本知识_第1张图片
JAVA IO框架.png

其中最核心的是5个类和1个接口。5个类指File、OutputStream、InputStream、Writer、Reader;1个接口指Serializable。

常用类的介绍见下表:

说明 描述
File 文件类 用于文件或者目录的描述信息,例如生成新目录,修改文件名,判断文件路径
RandomAccessFile 随机存取文件类 一个独立的类,直接继承Object,可以从文件的任意位置进行存取操作
InputStream 字节输入流 抽象类,基于字节的输入操作,是所有输入流的父类
OutputStream 字节输出流 抽象类,基于字节的输入操作,是所有输出流的父类
Reader 字符输入流 抽象类,基于字符的输入操作
Writer 字符输出流 抽象类,基于字符的输出操作

流的概念

《Thinking in Java》中介绍,流代表任何有能力产出数据的数据源对象或者是有能力接受数据的接收端对象。

Java中的将输入输出抽象成为流,将两个容器连接起来。流是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称或抽象,即数据在两设备间的传输成为流。

Java的IO模型使用装饰者(Decorator)模式,按功能划分Stream,可以动态装配这些Stream,以便获得需要的功能。例如:当你需要一个具有缓冲的文件输入流,可以组合使用FileInputStream和BufferedInputStream。

流的分类

根据处理数据类型不同分为:字符流和字节流。
根据数据流向不同分为:输入流和输出流。
根据数据来源分类:

  1. File: FileInputStream, FileOutputStream, FileReader, FileWriter
  2. byte[]: ByteArrayInputStream, ByteArrayOutputStream
  3. Char[] CharArrayReader, CharArrayWriter
  4. String: StringBufferInputStream, StringReader, StringWriter
  5. 网络数据流: InputStream, OutputStream, Reader, Writer

字符流和字节流

字符流的由来: 因为数据编码的不同,而有了对字符进行高效操作的流对象。本质其实就是基于字节流读取时,去查了指定的码表。Java中字符采用Unicode标准,一个字符2个字节。

字节流和字符流的区别:

  1. 读写单位不同:字节流以字节(8bit)为单位,字符流以字符为单位,根据码表映射字符,一次可能读多个字节。

  2. 处理对象不同:字节流能处理所有类型的数据(如图片、avi等),而字符流只能处理字符类型的数据。

  3. 字节流在操作的时候本身是不会用到缓冲区的,是文件本身的直接操作的;而字符流在操作的时候下后是会用到缓冲区的,是通过缓冲区来操作文件,我们将在下面验证这一点。

结论:优先选用字节流。首先因为硬盘上的所有文件都是以字节的形式进行传输或者保存的,包括图片等内容。但是字符只是在内存中才会形成的,所以在开发中,字节流使用广泛。

输入流和输出流

对输入流只能进行读操作,对输出流只能进行写操作,程序中需要根据待传输数据的不同特性而使用不同的流。

字节流

InputStream和OutputStream为各种输入输出字节流的基类,所有字节流都继承这两个基类。

常见的字节流(以输入流为例,输出一样)包括:

  • ByteArrayInputStream 它包含一个内部缓冲区,该缓冲区包含从流中读取的字节数组。
  • StringBufferInputStream 将String转换为InputStream,已经废弃,不再使用。
  • FileInputStream 文件字节输入流,对文件数据以字节的形式进行读取操作如读取图片视频等。
  • PipedInputStream 管道输出流,让多线程可以通过管道进行线程间的通讯。在使用管道通信时,必须将PipedOutputStream和PipedInputStream配套使用。
  • SequenceInputStream 将2个或者多个InputStream对象转换成单一InputStream。
  • FilterInputStream 抽象类,作为装饰器接口为其它的InputStream提供有用功能。
    • DataIputStream 用来装饰其它输入流,它允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型。
    • BufferedInputStream 代表使用缓冲区。
    • LineNumberInputStream 提供了额外的功能来保留当前行号的记录,已经废弃.
    • PushbackInputStream 可以把读取进来的数据回退到输入流的缓冲区之中。
    • PrintStream 用来产生格式化输出,有2个重要的方法print()和println()。

FileInputStream和FileOutputStream

这两个类对文件流进行操作,是最常见的IO操作流。

/**
 * 复制文件内容
 * @param inFile 源文件
 * @param outFile 目标文件
 */
public static void readFileByBytes(String inFile, String outFile) {
    InputStream in = null;
    OutputStream out = null;
    try {
        byte[] tempBytes = new byte[1024];
        int byteRead = 0;
        in = new FileInputStream(inFile);
        out = new FileOutputStream(outFile);
        while((byteRead = in.read(tempBytes)) != -1) {
            out.write(tempBytes, 0, byteRead);
        }
    } catch (Exception e1) {
        e1.printStackTrace();
    } finally {
        if(in != null) {
            try {
                in.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            try {
                out.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }
}

ByteArrayInputStream和ByteArrayOutputStream

ByteArrayOutputStream类是在创建它的实例时,程序内部创建一个byte型别数组的缓冲区,然后利用ByteArrayOutputStream和ByteArrayInputStream的实例向数组中写入或读出byte型数据。在网络传输中我们往往要传输很多变量,我们可以利用ByteArrayOutputStream把所有的变量收集到一起,然后一次性把数据发送出去。

package medium.io;

import java.io.ByteArrayInputStream;

public class ByteArrayStreamTest {
    
    private static final int LEN = 5;
    
    // 对应abcddefghijklmnopqrsttuvwxyz的ASCII码 
    private static final byte[] arrayLetters = {
        0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F,
        0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7A    
    };

    public static void main(String[] args) {
        testByteArrayInputStream();
    }
    
    private static void testByteArrayInputStream() {
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(arrayLetters);
        
        //读取5个字节
        for(int i = 0; i < LEN; i++) {
            //如果还可以继续读取
            if(byteArrayInputStream.available() >= 0) {
                //读取下一个字节并打印
                int temp = byteArrayInputStream.read();
                System.out.printf("%d : 0x%s\n", i, Integer.toHexString(temp));
            }
        }
        //如果不支持mark功能
        if(!byteArrayInputStream.markSupported()) {
            System.out.println("Make not supported!");
        }
        
        //标记当前位置,一旦调用reset()方法,会重置到标记位置
        //mark方法的参数0其实没有实际意义
        byteArrayInputStream.mark(0);
        //跳过5个字节
        byteArrayInputStream.skip(5);
        //读取5个字节
        byte[] buf = new byte[LEN];
        byteArrayInputStream.read(buf, 0, LEN);
        String str1 = new String(buf);
        System.out.printf("str1=%s\n", str1);
        
        //重置回标记位置
        byteArrayInputStream.reset();
        byteArrayInputStream.read(buf, 0, LEN);
        String str2 = new String(buf);
        System.out.printf("str2=%s\n", str2);
    }

}

PipedInputStream和PipedOutputStream

PipedOutputStream和PipedInputStream分别是管道输出流和管道输入流。
它们的作用是让多线程可以通过管道进行线程间的通讯。在使用管道通信时,必须将PipedOutputStream和PipedInputStream配套使用。
使用管道通信时,大致的流程是:我们在线程A中向PipedOutputStream中写入数据,这些数据会自动的发送到与PipedOutputStream对应的PipedInputStream中,进而存储在PipedInputStream的缓冲中;此时,线程B通过读取PipedInputStream中的数据。就可以实现,线程A和线程B的通信。

package medium.io;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class PipeStreamTest {

    public static void main(String[] args) throws IOException {
        final PipedOutputStream output = new PipedOutputStream();
        final PipedInputStream input = new PipedInputStream(output);
        
        //创建2个线程
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    output.write("Hello Pipe".getBytes());
                    output.close();
                } catch(IOException e) {
                    e.printStackTrace();
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    int data = input.read();
                    while(data != -1) {
                        System.out.print((char)data);
                        data = input.read();
                    }
                    input.close();
                    System.out.println();
                } catch(IOException e) {
                    e.printStackTrace();
                }
            }
        });
        thread1.start();
        thread2.start();
        
    }
}

SequenceInputStream

SequenceInputStream 表示其他输入流的逻辑串联。它从输入流的有序集合开始,并从第一个输入流开始读取,直到到达文件末尾,接着从第二个输入流读取,依次类推,直到到达包含的最后一个输入流的文件末尾为止。

package medium.io;

import java.io.BufferedInputStream;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.SequenceInputStream;

public class SequenceStreamTest {

    public static void main(String[] args) throws IOException{
        FileInputStream f1 = new FileInputStream("123.txt");
        FileInputStream f2 = new FileInputStream("234.txt");
        SequenceInputStream s1 = new SequenceInputStream(f1, f2);
        BufferedInputStream b1 = new BufferedInputStream(s1);
        byte[] b = new byte[1024];
        int n = 0;
        while((n = b1.read(b)) != -1) {
            String s = new String(b, 0, n);
            System.out.println(s);
        }
    }

}

FilterInputStream和FilterOutputStream

FilterInputStream 的作用是用来“封装其它的输入流,并为它们提供额外的功能”。

DataInputStream和DataOutputStream

DataInputStream 是数据输入流,它继承于FilterInputStream。DataOutputStream 是数据输出流,它继承于FilterOutputStream。二者配合使用,“允许应用程序以与机器无关方式从底层输入流中读写基本 Java 数据类型”。

private static void testDataOutputStream() {
    DataOutputStream out = null;
    try {
        File file = new File("file.txt");
        out = new DataOutputStream(new FileOutputStream(file));
        
        out.writeBoolean(true);
        out.writeByte((byte)0x41);
        out.writeChar((char)0x4243);
        out.writeShort((short)0x4445);
        out.writeInt(0x12345678);
        out.writeLong(0x0FEDCBA987654321L);
        out.writeUTF("abcdefg");
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (SecurityException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            out.close();
        } catch (IOException e) {
            
        }
    }
}

private static void testDataInputStream() {
    DataInputStream in = null;
    try {
        File file = new File("file.txt");
        in = new DataInputStream(new FileInputStream(file));
        
        System.out.printf("readBoolean(): %s\n", in.readBoolean());
        System.out.printf("readByte(): %s\n", in.readByte());
        System.out.printf("readChar(): %s\n", in.readChar());
        System.out.printf("readShort(): %s\n", in.readShort());
        System.out.printf("readInt(): %s\n", in.readInt());
        System.out.printf("readLong(): %s\n", in.readLong());
        System.out.printf("readUTF(): %s\n", in.readUTF());
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (SecurityException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            in.close();
        } catch(IOException e) {
        }
    }

}

BufferedInputStream和BufferedOutputStream

BufferedInputStream是带缓冲区的输入流,它继承于FilterInputStream。默认缓冲区大小是8M,能够减少访问磁盘的次数,提高文件读取性能。

BufferedOutputStream是带缓冲区的输出流,它继承于FilterOutputStream,能够提高文件的写入效率。

它们提供的“缓冲功能”本质上是通过一个内部缓冲区数组实现的。例如,在新建某输入流对应的BufferedInputStream后,当我们通过read()读取输入流的数据时,BufferedInputStream会将该输入流的数据分批的填入到缓冲区中。每当缓冲区中的数据被读完之后,输入流会再次填充数据缓冲区;如此反复,直到我们读完输入流数据。

public static void readAndWrite(String src, String des) {
    BufferedInputStream bis = null;
    BufferedOutputStream bos = null;
    try {
        bis = new BufferedInputStream(new FileInputStream(src));
        bos = new BufferedOutputStream(new FileOutputStream(des));
        byte[] b = new byte[1024];
        int len = 0;
        while((len = bis.read(b, 0, b.length)) != -1) {
            bos.write(b, 0, len);
        }       
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(bos != null) {
            try {
                bos.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            
        }
        if(bis != null) {
            try {
                bis.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            
        }
    }
}

字符流

既然字节流提供了能够处理任何类型的输入/输出操作的功能,那为什么还要存在字符流呢?字节流不能直接操作Unicode字符,因为一个字符有两个字节,字节流一次只能操作一个字节。 在平时使用更多的还是字节流。

大部分字节流都可以找到对应的字符流。

public static void main(String[] args) throws IOException {
    FileOutputStream fos = new FileOutputStream("qqq.txt");
    //字节流转字符流
    OutputStreamWriter osw = new OutputStreamWriter(fos);
    BufferedWriter bw = new BufferedWriter(osw);
    String s1 = "爱你一生一世";
    String s2 = "爱你一生一世";
    bw.write(s1);
    bw.write("\r\n");
    bw.write(s2);
    
    bw.close();
    
    FileInputStream fis = new FileInputStream("qqq.txt");
    //字节流转字符流并指定编码
    InputStreamReader isr = new InputStreamReader(fis, "UTF-8");
    BufferedReader br = new BufferedReader(isr);
    String s = null;
    while(null != (s = br.readLine())) {
        System.out.println(s);
    }
    br.close();
}

在实际使用中,感觉文件中有中文的时候使用字符流很容易出现乱码,一般还是使用字节流。

RandomAccessFile

RandomAccessFile是Java输入/输出流体系中功能最丰富的文件内容访问类,既可以读取文件内容,也可以向文件输出数据。与普通的输入/输出流不同的是,RandomAccessFile支持跳到文件任意位置读写数据,RandomAccessFile对象包含一个记录指针,用以标识当前读写处的位置,当程序创建一个新的RandomAccessFile对象时,该对象的文件记录指针对于文件头(也就是0处),当读写n个字节后,文件记录指针将会向后移动n个字节。除此之外,RandomAccessFile可以自由移动该记录指针

RandomAccessFile包含两个方法来操作文件记录指针:

  • long getFilePointer():返回文件记录指针的当前位置。
  • void seek(long pos):将文件记录指针定位到pos位置。

RandomAccessFile类在创建对象时,除了指定文件本身,还需要指定一个mode参数,该参数指定RandomAccessFile的访问模式,该参数有如下四个值:

  • r:以只读方式打开指定文件。如果试图对该RandomAccessFile指定的文件执行写入方法则会抛出IOException。
  • rw:以读取、写入方式打开指定文件。如果该文件不存在,则尝试创建文件。
  • rws:以读取、写入方式打开指定文件。相对于rw模式,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备,默认情形下(rw模式下),是使用buffer的,只有cache满的或者使用RandomAccessFile.close()关闭流的时候儿才真正的写到文件。
  • rwd:与rws类似,只是仅对文件的内容同步更新到磁盘,而不修改文件的元数据。

下面进行一个项目实战例子。

package medium.io;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;

public class RandomAccessFileTest {

    public static void main(String[] args) throws IOException{
        String content = "测试文字啊";
        //创建临时文件
        File tempFile = File.createTempFile("temp", null);
        //虚拟机终止时,删除此文件
        tempFile.deleteOnExit();
        FileOutputStream fos = new FileOutputStream(tempFile);
        RandomAccessFile raf = new RandomAccessFile("123.txt", "rw");
        raf.seek(5);
        byte[] buffer = new byte[1024];
        int num = 0;
        int len = 0;
        while((len = raf.read(buffer)) != -1) {
            fos.write(buffer, num, len);
            num += len;
        }
        raf.seek(5);
        raf.write(content.getBytes());
        FileInputStream fis = new FileInputStream(tempFile);
        num = 0;
        while((len = fis.read(buffer)) != -1) {
            raf.write(buffer, num, len);
        }
    }

}

需要注意的是,由于是按字节偏移,中文有时候会出现乱码。

标准IO

标准IO指“程序所使用的单一信息流”,程序的所有输入都可以来自于标准输入,所有输出也都可以发送到标准输出,所有的错误信息都可以发送到标准输入。

标准IO的意义在于:我们很容易把程序串联在一起,一个程序的标准输出可以称为另一个程序的标准输入。

读取标准输入

Java提供了System.in、System.out和System.err。其中System.out和System.err已被包装成了PrintStream对象,可以直接使用,System.in却必须包装才可以使用。

public class Echo {

    public static void main(String[] args) throws IOException {
        BufferedReader stdin = new BufferedReader(new InputStreamReader(System.in));
        String s;
        while((s = stdin.readLine()) != null && s.length() != 0) {
            System.out.println(s);
        }
    }

}

IO重定向

可以使用方法对标准IO进行重定向:

  • setIn(PrintStream)
  • setOut(PrintStream)
  • setErr(PrintStream)

在程序大量输出难以直接阅读的时候,重定向输出很有用,在重复输入某个序列的时候,重定向输入很有用。

public class Redirecting {

    public static void main(String[] args) throws IOException{
        PrintStream console = System.out;
        BufferedInputStream in = new BufferedInputStream(new FileInputStream("123.txt"));
        PrintStream out = new PrintStream(new BufferedOutputStream(new FileOutputStream("234.txt")));
        //设置输入流和输出流
        System.setIn(in);
        System.setOut(out);
        System.setErr(out);
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String s;
        while((s = br.readLine()) != null) {
            System.out.println(s);
        }
        out.close();
        //恢复输出流
        System.setOut(console);
    }

}

你可能感兴趣的:(JAVA IO基本知识)