Java I/O总结

440 阅读10分钟

Java IO原理总结

Java IO Overview

文件相关的类

Files are a common source or destination of data in Java applications.

在Java应用程序中,文件是一种通用的数据源或者存储数据的媒介。

通过Java IO读文件

  • FileInputStream

    二进制文件 输入流

    可以从文件开始到文件末尾一次读取一个字节

  • FileReader

    文本文件 输入流

    可以从文件开始到文件末尾一次读取一个字符

你也不必一次性读取整个文件,相反你可以按顺序地读取文件中的字节和字符。

  • RandomAccessFile

    跳跃式地读取文件其中的某些部分

通过Java IO写文件

  • FileOutputStream
  • FileWriter

通过Java IO随机存取文件

  • RandomAccess

    随机存取并不意味着你可以在真正随机的位置进行读写操作,它只是意味着你可以跳过文件中某些部分进行操作,并且支持同时读写,不要求特定的存取顺序。

    这使得RandomAccessFile可以覆盖一个文件的某些部分、或者追加内容到它的末尾、或者删除它的某些内容,当然它也可以从文件的任何位置开始读取文件。

Stream(流)

Java IO流是既可以从中读取,也可以写入到其中的数据流。流通常会与数据源、数据流向目的地相关联,比如文件、网络等等。

流的特点

  1. 流和数组不一样,不能通过索引(下标)读写数据

    在流中,你也不能像数组那样前后移动读取数据,除非使用RandomAccessFile处理文件。流仅仅只是一个连续的数据流

  2. Java IO流通常是基于字节或者基于字符的。

    1. 字节流通常以“stream”命名,比如InputStreamOutputStream

      除了DataInputStreamDataOutputStream 还能够读写int, long, float和double类型的值以外,其他流在一个操作时间内只能读取或者写入一个原始字节

    2. 字符流通常以“Reader”或者“Writer”命名

字节流

InputStream

If you are writing a component that needs to read input from a stream, try to make our component depend on an InputStream, rather than any of it's subclasses (e.g. FileInputStream).

Doing so makes your code able to work with all types of input streams, instead of only the concrete subclass.

java.io.InputStream类是所有Java IO输入流的基类。如果你正在开发一个从流中读取数据的组件,请尝试用InputStream替代任何它的子类(比如FileInputStream)进行开发。

这么做能够让你的代码兼容任何类型而非某种确定类型的输入流。(更好的兼容性)

read()

Java Doc: Reads the next byte of data from the input stream. The value byte is returned as an int in the range 0 to 255.

If no byte is available because the end of the stream has been reached, the value -1 is returned.

This method blocks until input data is available, the end of the stream is detected,or an exception is thrown.

读数据:通常使用InputStream中的read()方法读取数据。

read()方法返回一个整数,代表了读取到的字节的内容(0 ~ 255)。当达到流末尾没有更多数据可以读取的时候,read()方法返回-1

 try (InputStream in = new FileInputStream(
            "D:\\gitRepository\\organized-learning\\deep-in-java\\stage-8\\helloworld.txt");
            OutputStream out = new FileOutputStream(
                "D:\\gitRepository\\organized-learning\\deep-in-java\\stage-8\\output.txt")) {
            
            int data;
            while ((data = in.read()) != -1) {
                out.write(data);
            }
        }
FileInputStream

重点了解构造器

 public FileInputStream(String name) throws FileNotFoundException {
        this(name != null ? new File(name) : null);
 }
 public FileInputStream(File file) throws FileNotFoundException {
     ...
 }
// 传入文件对象 如果文件本身不存在,FileInputStream创建时会报错的
File file = new File("c:\\data\\input-text.txt");
InputStream input = new FileInputStream(file);
// 或者传入路径
InputStream inputStream=new FileInputStream("c:\\data\\input-text.txt");
BufferedInputStream

BufferedInputStream能为输入流提供缓冲区,能提高很多IO的速度。

可以一次读取一大块的数据,而不需要每次从网络或者磁盘中一次读取一个字节。特别是在访问大量磁盘数据时,缓冲通常会让IO快上许多。

InputStream input = new BufferedInputStream(new FileInputStream("c:\\data\\input-file.txt"));

public synchronized int read() throws IOException {
        if (pos >= count) {
            fill();
            if (pos >= count)
                return -1;
        }
        return getBufIfOpen()[pos++] & 0xff;
}

读的时候会一次性将byte[]数组填满

OutputStream

java.io.OutputStream是Java IO中所有输出流的基类。

write(int)

用于把单个字节写入到输出流中。

OutputStreamwrite(int)方法将一个包含了待写入数据的int变量作为参数进行写入。

只有int类型的第一个字节会被写入,其余位会被忽略。(写入低8位,忽略高24位)。

write(byte[])
  • write(byte[]) 把字节数组中所有数据写入到输出流中。

  • write(byte[], int offset, int length) 把字节数据中从offset位置开始,length个字节的数据写入到输出流。

flush()

将所有写入到OutputStream的数据冲刷到相应的目标媒介中。

比如,如果输出流是FileOutputStream,那么写入到其中的数据可能并没有真正写入到磁盘中。即使所有数据都写入到了FileOutputStream这些数据还是有可能保留在内存的缓冲区中

通过调用flush()方法,可以把缓冲区内的数据刷新到磁盘(或者网络,以及其他任何形式的目标媒介)中。

it does not guarantee that they are actually written to a physical device such as a disk drive.(不能确保一定写入磁盘这样的物理设备中)

FileOutputStream
文件内容覆盖&&追加
	OutputStream output = new FileOutputStream("c:\\data\\output-text.txt", true);//appends to file  

// 默认情况下 是覆盖
OutputStream output = new FileOutputStream("c:\\data\\output-text.txt", false);  //overwrites file 
BufferedOutputStream

BufferedInputStream类似,BufferedOutputStream可以为输出流提供缓冲区。

OutputStream output = new BufferedOutputStream(new FileOutputStream("c:\\data\\output-file.txt"));

字符流

Reader

Reader是字符输入流的基类,主要有下列子类。

  • BufferedReader
  • InputStreamReader
  • FileReader
  • FilterReader
  • PushbackReader
  • PipedReader
  • StringReader
  • CharArrayReader

Reader与InputStream类似,不同点在于,Reader基于字符而非基于字节。换句话说,Reader用于读取文本,而InputStream用于读取原始字节。

Reader#read

InputStream#read()方法返回一个字节,意味着这个返回值的范围在0到255之间(当达到流末尾时,返回 -1);

Reader#read()方法返回一个字符,这个返回值的范围是在0到65535之间(当达到流末尾时,同样返回-1)。

这并不意味着Reader只会从数据源中一次读取2个字节,Reader会根据文本的编码,一次读取一个或者多个字节。

InputStreamReader

原文:A Java Reader can be combined with an InputStream. If you have an InputStream and want to read characters from it, you can wrap it in an InputStreamReader.

一个Reader可以和一个InputStream相结合。如果你有一个InputStream输入流,并且想从其中读取字符,可以把这个InputStream包装到InputStreamReader中。

Reader reader = new InputStreamReader(inputStream);

Writer

Writer是所有字符输出流的父类。

子类:

  • BufferedWriter
  • CharArrayWriter
  • FilterWriter
  • OutputStreamWriter
  • FileWriter
  • PipedWriter
  • PrintWriter
  • StringWriter
write(int c)

Writer的write(int c)方法,会将传入参数的低16位写入到Writer中,忽略高16位的数据。

public void write(int c) throws IOException {
    synchronized (lock) {
        if (writeBuffer == null){
            writeBuffer = new char[WRITE_BUFFER_SIZE];
        }
        writeBuffer[0] = (char) c;
        write(writeBuffer, 0, 1);
    }
}
OutputStreamWriter

tips: A Java Writer can be combined with an OutputStream just like Readers and InputStream's. Wrap the OutputStream in an OutputStreamWriter and all characters written to the Writer are passed on to the OutputStream.

such as : Writer writer = new OutputStreamWriter(outputStream);

ReaderInputStream类似,一个Writer可以和一个OutputStream相结合。把OutputStream包装到OutputStreamWriter中,所有写入到OutputStreamWriter的字符都将会传递给OutputStream。(字符流 -> 字节流)。

组合流(Combining Streams)

原文:You can combine streams into chains to achieve more advanced input and output operations. For instance, reading every byte one at a time from a file is slow. It is faster to read a larger block of data from the disk and then iterate through that block byte for byte afterwards.

翻译:我们可以将流整合起来以便实现更高级的输入和输出操作。

比如,一次读取一个字节是很慢的(每次都会触发磁盘访问、网络活动或其他一些相对昂贵的操作),所以可以从磁盘中一次读取一大块数据,然后从读到的数据块中获取字节。

InputStream input = new BufferedInputStream(
                        new FileInputStream("d:\\data\\input-file.txt"));

缓冲同样可以应用到OutputStream中。我们也可以实现将大块数据批量地写入到磁盘(或者相应的流)中,这个功能由BufferedOutputStream实现。

序列化与反序列化

被序列化和反序列化的对象 对应的类,必须实现Serializable接口。

核心类:ObjectInputStream && ObjectOutputStream

  • ObjectInputStream能够让你从输入流中读取Java对象,而不需要每次读取一个字节。(反序列化)

    你可以把InputStream包装到ObjectInputStream中,然后就可以从中读取对象了。

  • ObjectOutputStream能够让你把对象写入到输出流中,而不需要每次写入一个字节。(序列化)

    你可以把OutputStream包装到ObjectOutputStream中,然后就可以把对象写入到该输出流中了。

传统Java文件系统

文件对象API - java.io.File

  1. 检查文件是否存在

  2. 获取文件长度

  3. 删除文件

  4. 检查路径 对应的是 文件还是目录

    • true表示该路径存在并且是一个目录
    • 如果返回false也有可能是路径不存在而不是指向文件。
  5. 读取目录中的文件列表

        String  printInfoPrefix = "[" + TEST_SOURCE_FILE_PATH + "]路径对应的文件";
        File    file            = new File(TEST_SOURCE_FILE_PATH);
        boolean exists          = file.exists();
        String  existInfo       = exists ? "存在" : "不存在";
        // [D:\gitRepository\organized-learning\deep-in-java\stage-8\helloworld.txt]目录对应的文件存在
        System.out.println(printInfoPrefix + existInfo);

        System.out.println(printInfoPrefix + "长度为:[" + file.length() + "]字节");

        // true表示该路径存在并且是一个目录,
        // 如果返回false也有可能是路径不存在而不是指向文件。
        String pathInfo = file.isDirectory() ? "目录" : "文件";
        System.out.println("[" + TEST_SOURCE_FILE_PATH + "]路径 对应的是一个" + pathInfo);

        // 获取上一层路径
        File parentFile = file.getParentFile();
        // 读取路径直接对应的 文件或目录
        String[] childFileArr = parentFile.list();
        for (String childFile : childFileArr) {
            // 遍历 打印
            System.out.println(childFile);
        }

[D:\gitRepository\organized-learning\deep-in-java\stage-8\helloworld.txt]路径对应的文件存在 [D:\gitRepository\organized-learning\deep-in-java\stage-8\helloworld.txt]路径对应的文件长度为:[12612]字节 [D:\gitRepository\organized-learning\deep-in-java\stage-8\helloworld.txt]路径 对应的是一个文件

file.test file.txt helloworld.txt output.txt outputchar.txt pom.xml stage-8.iml stage8-lesson1

File类的不足之处

Prior to the Java SE 7 release, the java.io.File class was the mechanism used for file I/O, but it had several drawbacks.

  • Many methods didn't throw exceptions when they failed, so it was impossible to obtain a useful error message. For example, if a file deletion failed, the program would receive a "delete fail" but wouldn't know if it was because the file didn't exist, the user didn't have permissions, or there was some other problem.
  • The rename method didn't work consistently across platforms.
  • There was no real support for symbolic links(快捷方式).
  • More support for metadata was desired, such as file permissions, file owner, and other security attributes.
  • Accessing file metadata was inefficient.
  • Many of the File methods didn't scale. Requesting a large directory listing over a server could result in a hang. Large directories could also cause memory resource problems, resulting in a denial of service.
  • It was not possible to write reliable code that could recursively walk a file tree and respond appropriately if there were circular symbolic links.

文件系统API - java.io.FileSystem

  • java.io.WinNTFileSystem

文件描述符 - java.io.FileDescriptor

public static void main(String[] args) throws Exception {
        displayFileDescriptor(FileDescriptor.in);
        displayFileDescriptor(FileDescriptor.out);
        displayFileDescriptor(FileDescriptor.err);
    }

    private static void displayFileDescriptor(FileDescriptor fileDescriptor)
        throws NoSuchFieldException, IllegalAccessException {
        Integer fd     = getFieldValue(fileDescriptor, "fd");
        Long    handle = getFieldValue(fileDescriptor, "handle");
        Boolean closed = getFieldValue(fileDescriptor, "closed");
        System.out.printf("FileDescriptor=[ fd: %d  , handle : %d  , closed: %s]\n",fd,handle,closed);
    }

    private static <T> T getFieldValue(FileDescriptor fileDescriptor, String fieldName)
        throws NoSuchFieldException, IllegalAccessException {
        Field field = FileDescriptor.class.getDeclaredField(fieldName);
        field.setAccessible(true);
        return (T)field.get(fileDescriptor);
    }

文件输入输出流

  • FileInputStream
  • FileOutputStream

文件过滤器

java.io.FileFilter

**
 * 计算一个目录占据的空间
 *
 * @author ajin
 */
public class DirectorySpaceDemo {

    /**
     * 根目录
     * */
    private final File            rootDirectory;
    /**
     * {@link Predicate}
     * */
    private final Predicate<File> filter;

    public DirectorySpaceDemo(File rootDirectory, FileFilter... fileFilters) {
        this.rootDirectory = rootDirectory;
        this.filter = new FilePredicate(fileFilters);
    }


    private class FilePredicate implements Predicate<File> {

        private final FileFilter[] fileFilters;

        public FilePredicate(FileFilter[] fileFilters) {
            this.fileFilters = fileFilters;
        }

        @Override
        public boolean test(File file) {

            for (FileFilter fileFilter : fileFilters) {
                if (!fileFilter.accept(file)) {
                    return false;
                }
            }

            return true;
        }
    }

    private interface FilePredicateAdapter extends Predicate<File>, FileFilter {

        @Override
        default boolean accept(File pathname) {
            return test(pathname);
        }

    }

    /**
     * 获取路径对应 的 大小 比如:d:\\test 占据内存 128kb
     */
    public long getSpace() {
        if (rootDirectory.isFile()) {
            return rootDirectory.length();
        } else if (rootDirectory.isDirectory()) {
            File[] subFiles = rootDirectory.listFiles();
            long   space    = 0L;
            if (null == subFiles) {
                return space;
            }
            // 累加当前目录的文件
            space += Stream.of(subFiles).filter(File::isFile).filter(filter).map(File::length).reduce(Long::sum).orElse(
                0L);

            // 递归当前子目录
            space += Stream.of(subFiles).filter(File::isDirectory).filter(filter).map(DirectorySpaceDemo::new).map(
                DirectorySpaceDemo::getSpace).reduce(Long::sum).orElse(0L);

            return space;
        }
        return -1L;
    }

    public static long calculateSpace(File file) {
        Objects.requireNonNull(file);

        if (file.isFile()) {
            return file.length();
        } else if (file.isDirectory()) {

            long filesSpace = 0L;

            File[] subFiles = file.listFiles();
            if (null == subFiles) {
                return filesSpace;
            }

            for (File subFile : subFiles) {
                filesSpace += calculateSpace(subFile);
            }
            return filesSpace;
        }
        return -1L;
    }

    public static void main(String[] args) {

        System.out.println(
            new DirectorySpaceDemo(new File(System.getProperty("user.home")), file -> file.getName().endsWith(".log"))
                .getSpace() / 1024);

    }
}

java.io.FileNameFilter

@FunctionalInterface
public interface FilenameFilter {
  
    boolean accept(File dir, String name);
}

函数式接口,没什么特殊的。

参考资料

  1. Lesson: Basic I/O

  2. Java IO Tutorial

  3. Java IO教程

  4. 一入Java深似海 第八期 第一节 Java I/O流

  5. 一入Java深似海 第八期 第二节 Java 文件系统

  6. Legacy File I/O Code