美文网首页
Java I/O 学习(一)

Java I/O 学习(一)

作者: 狮_子歌歌 | 来源:发表于2017-02-14 22:15 被阅读205次

    Java.io package

    通过数据流,序列化和文件系统提供系统输入和输出。

    File

    概述

    一种文件或目录的路径名抽象表示。

    用户界面和操作系统使用以来系统的路径字符串来命名文件和目录。

    File类表达一种与系统无关的分层路径名视图抽象,这种抽象主要作用是以不依赖操作系统的方式处理很多文件和路径名依赖操作系统的复杂问题。这种抽象路径由两部分组件:

    1. 一个可选的依赖系统的首字符串,例如磁盘驱动说明符,"/"Unix根目录,"\\"表示Windows UNC路径名;
    2. 一个空的或者多个"name"的序列。

    在抽象路径中name可以表示以下几种含义:第一个name可能是目录名,或者是hostname(Windows UNC的一部分);后续的每一个name(除了最后一个)都表示目录名;最后一个name表示目录名或者文件名。

    路径分类

    无论是字符串形式,还是File的抽象表示,路径总是分为绝对路径和相对路径。

    绝对路径名是完整的,包含文件名和它的完整路径以及磁盘驱动说明符,是依赖操作系统的。不需要其他信息来定位它所表示的文件。

    而相对路径必须根据从其他路径获取的信息来解释。默认情况java.io包下的类都是一句当前用户目录来解决相对路径。这个目录时根据系统属性user.dir来命名,而且通常是JVM被调用的目录(执行java className命令的目录就是当前目录)。

    使用绝对路径不利于代码平台移植,所以尽量使用相对路径。相对路径目录分隔符是斜杠(/)。

    路径中的首字符串

    这个prefix概念是依赖系统的,例如表示Unix根目录,Windows的磁盘驱动器说明符和根目录...

    在Unix系统中,"/"表示跟目录;在Windows中,磁盘字母+:形式,如果是绝对路径,后面还会跟"\"。

    路径字符串中的符号

    分隔符

    依赖系统的路径字符串和File路径抽象表示之间的转换都涉及到分隔符。其作用是将路径中各name分隔,形成分层。不同操作系统的分隔符都是不一样的。

    依赖系统特定的分隔符可以通过给System.getProperty()传入关键词file.separator获取(更多关键词可以去查看文档)。

    除了目录分隔符还有路径分隔符和行分隔符,它们都是依赖系统的(关键词分别是:path.separator和line.separator)。

    其他符号

    符号 描述
    "." 表示当前目录
    ".." 表示上一层目录
    "../../" 表示上一层目录的上一层目录
    "/" 表示根目录
    "~/" 表示用户目录的根目录(表示当前虚拟目录下)

    注意表格中使用的分隔符(/)可以替换成任一系统特定的分隔符。

    参考CSDN问题贴绝对路径和相对路径当前路径和上级目录的表示方法

    FileSystem Hierarchy

    FileSystem Hierarchy的主要作用是规定了操作系统各层次目录的作用。

    好处在于软件可以预测已安装文件和文件夹的位置;用户可以预测已安装文件和文件夹的位置。这就好比MacOS目录有/Library,/Application,/Users...一个Android项目也有相应的路径分别存放Java代码和资源文件。

    详细规定可以看文件系统层次结构

    empty abstract name

    文档中介绍File的一种抽象形式empty abstract name,它不包含任何prefix和name sequence。那么表示哪个文件或目录呢?

    public class TestPathCharacter {
        public static void main(String[] args) {
            File file = new File("");
            if(!file.exists()) {
                System.out.println("no such file or directory");
                //打印了,说明无法定位到指定文件或目录
            }
            //猜想表示当前用户目录
            //File f = new File(file, "/TestPathString.java"); //执行后打印异常,无法找到相应文件
            File f = new File("./", "TestPathString.java");//执行后正常打印,证明不需要使用绝对路径
            if(!f.exists()) {
                System.out.println("no such file or directory");
            }
            Scanner input = null;
            try {
                input = new Scanner(f);
                String line = null;
                while(input.hasNextLine()) {
                    line = input.nextLine();
                    System.out.println(line);
                }
            }catch(IOException e) {
                e.printStackTrace();
            }finally{
                input.close();
            }
        }
    }
    

    执行结果可以看注释,证明不表示任何文件或目录。

    Path文档中empty path定位到的是file system默认的目录。

    与java.nio.file互通性

    java.nio.file包定义了让JVM访问文件,文件属性和文件系统的接口和类。这些API克服了File类的限制。调用一个File对象的toPath()可以得到一个Path去定位相应文件。返回得到的Path配合Files使用,提供了更高效和更广泛的额外文件操作,文件属性和有利于诊断文件操作时错误的I/O异常的访问。

    Path是一个可以用于在file system中定位文件得到类。它一般代表依赖系统的文件路径。一个Path对象表示一个分层的,由被系统特定的分隔符分隔的目录和文件名元素的序列组成。也有可能会包含一个标示着系统层次的根组件。

    FileSystem是一个提供file system的接口,同时是对象访问file system中的文件和其他对象的工厂。通过FileSystems.getFileSystem()文档介绍,自己对file system初步理解:

    This method iterates over the installed providers to locate the provider that is identified by the URI scheme of the given URI.

    给一个对象提供外部访问接口,也就是提供程序。可以比作Android系统中为了安全考虑程序之间数据共享使用ContentProvider,根据特定的uri去定位文件。而这个ContentProvider就是该程序的file system。

    java.nio.file包中还提供了几个名称类似的类,它们的对应关系:

    类名 描述
    Path 一个可以用于在file system中定位文件得到类。它一般代表依赖系统的文件路径。
    Paths 只包含通过转换路径字符串或URI返回Path的静态方法
    FileSystem 一个提供file system的接口
    FileSystems 是file system的工厂方法,用于获取或者构建file system
    Files 只包含对文件,目录或其他类型文件操作的静态方法

    FileSystems.getDefault()返回的file system是指JVM可用的。而工作目录就是当前用户目录,被系统属性命名(System.getProperty(user.dir)),这就允许了和java.io.File的互通性。

    API理解

    大部分File的方法直接看文档就可以,一下几个自己理解有误,实验了一下。

    • mkdir()&mkdirs(),两者都是创建目录,注意不会创建文件。但是使用mkdir()时,File对象指示的路径中有不存在的目录,导致创建失败。此时可以用mkdirs()。
    • getParent(),返回File对象的父目录字符串。
    • list()&listFiles()&listRoots(),list()和listFiles()都是返回当前目录下所有文件和目录,前者返回字符串数组,后者返回File数组;listRoots()返回系统可用的系统盘。
    • getPath()&getAbsolutePath()&getCanonicalPath(),getPath()返回构建File对象时路径的字符串形式(完全不改动);getAbsolutePath()返回File对象指示的绝对路径;getCanonicalPath()返回File对象的绝对路径,但是不包括路径中的符号(除了分隔符)。

    FilenameFilter,用于过滤文件。

    RandomAccessFile

    文档概述

    RandomAccessFile实例支持随机访问文件。随机访问文件的行为类似存储在file system中大字节数组。该隐含的数组中有一种光标或索引,称为文件指针。读取操作是在指针处读取开始读取字节,并且推进指针至下一个字节。如果使用"rw"模式构建的对象,写操作一样可用。写入操作过程和读取类似。一旦写入数据超过隐含的数组长度后,数组被扩展。文件指针可以被getFilePointer()获取当前位置,通过seek()修改位置。

    作用

    java.io包下的其它流都是只读或只写。而且它们的外部文件都是顺序的,意思是读操作时从文首到文末、写操作时覆盖或者从文末追加,无法更新文件(可以删除重新创建)。而RandomAccessFile可以做到。

    二进制IO中的字符与字符串

    DataOutput

    该接口规定了从任何Java基本类型数据到一系列字节的转换,然后将这些字节写入二进制流中。同时也提供字符串对象转换成改进版UTF-8格式,并且将这些字节写出。

    DataInput定义了读取基本数据类型和字符串的方法。

    • writeChar(int),将UTF-16字节写入输出流中;
    • writeChars(String),将字符串中所有字符的UTF-16字节写入输出流中;
    • writeBytes(String),将字符串中所有字符的UTF-16低字节写入输出流,高字节抛弃。该方法适用于纯ASCII码字符组成的字符串。
    • writeUTF(String),将字符串中字符转换成改进版UTF-8格式,然后前两个字节存储转换后的字节数,后面接上转换后的字节系列。

    改进版UTF-8编码规则

    改进版的UTF-8方案采用一个字节,二个字节或三个字节来存储字符。

    1. 字符Unicode编码小于或等于0x7F,该字符编码大小为一个字节,且首位为0标示为一个字节存储;
    2. 字符Unicode编码大于0x7F且小于或等于0x7FF,该字符编码大小为二个字节,且前三位是110标示为两个字节中第一位;
    3. 字符unicode编码大于0x7FF,该字符编码大小为三个字节,且前四位是1110标示为三个字节中第一个。

    实战

    public class TestDataOutput {
        public static void main(String[] args) {
            String s = "我是\uD835\uDD46";
            String result = null;
            System.out.println(s);
            System.out.println("字符串字符个数: " + s.length());
            System.out.println("字符串code point个数: " + s.codePointCount(0, s.length()));
            RandomAccessFile file = null;
            byte[] b = null;
            try {
                try {
                    file = new RandomAccessFile("../file/TestUTF8Stream.txt", "rw");
                    file.setLength(0);
                    file.writeUTF(s);
                    int count = (int)(file.length());
                    System.out.println("随机访问文件字节数有:" + count + "个.");
                    b = new byte[count];
                    file.seek(0);
                    file.read(b, 0, count);
                    for(byte i : b) {
                        System.out.println(i & 0xff);
                    }
                    file.seek(0);
                    result = file.readUTF();
                    System.out.println(result);
                }finally {
                    file.close();
                }
            }catch(FileNotFoundException e) {
                e.printStackTrace();
            }catch(IOException e) {
                e.printStackTrace();
            }
        }
    }
    

    Console打印输出:

    屏幕快照 2017-02-12 下午5.57.24.png

    上图中打印了字符串,字符串代码单元数量以及代码点数量,写入文件后转换的字节系列个数,分别打印该系列,最后使用readUTF()读取写入文件的字符串。

    这里遇到三个误区:

    第一,当写完数据后,直接从文件中获取所有字节系列,是无法获取到的。这是因为此时文本指针在文本末尾。同理,使用readUTF()前也需要调用seek(0)

    第二,当文件关闭后对文件重新写入数据(程序重启),会覆盖文本内的内容。这里需要注意了如果第二次输入的数据字节长度小于第一次,只会覆盖第二次输入数量的字节,剩下的会保留。为了避免误操作,可以在每一次写入时调用setLength(0)清空。

    第三,使用RandomAccessFile构造函数实例化对象,如果给定的是文件路径字符串或者File,没有该文件自动创建。但是给定的是包含未创建的目录或者目录路径字符串或者File,文件/目录不会自动创建,并且抛出FileNotFounException异常。

    RandomAccessFile中读写操作调用需要对应起来。也就是在文本写入时,哪里调用了writeUTF()就在读取过程中的相应顺序调用readUTF()

    只支持文件,且没有只写模式

    RandomAccessFile类只能够为文件创建访问流,目录等其他类型对象不可以。

    RandomAccessFile构造器有两个:

    • RandomAccessFile(File file, String mode)
    • RandomAccessFile(String name, String mode)

    都会抛出FileNotFoundException异常,原因有以下几种:

    1. 如果模式是"r",给定的文件不是固定存在,会抛出。
    2. 如果模式是"rw"开头,给定的文件不是一个存在且可写的固定文件,若不存在又无法创建或者创建,打开文件时出现错误,会抛出。

    File类可以抽象的表示文件或者目录路径,操作时无需处理系统依赖问题。FileSystem是file system接口,且提供了对象访问file system中文件或其他对象方法(不仅仅只有文件和目录)。而Path则是FileSystem中定位文件或其他对象的路径,与系统相关。

    模式

    RandomAccessFile模式常用的有两种:

    • "r",只读模式。如果使用该模式实例去调用任何write重载方法会抛出IOException异常。
    • "rw",读写模式。如果文件不存在,会去尝试创建一个新文件。

    还有两种模式,看文档不是很理解:

    • "rws"
    • "rwd"

    同样是支持读写操作,还有别的作用,不太理解。

    注意不支持只写模式。

    方法

    RandomAccessFile的方法主要是向文本记录数据。每一条数据大小不必相等,但是它们大小和位置必须可知。这样做的目的,一方面为了后期读取时,必须按照写入时顺序调用相应方法;另一方面可以通过seek()方法准确定位到想要的数据。所以,她只能操作文件。

    RandomAccessFile虽然是二进制流,但是和InputStream和OutputStream没有继承关系。它直接继承Object,实现DataOutput和DataInput接口的方法。除了这两个接口的读写方法,还实现了特有方法。

    • getFilePointer(),返回从文本头部到指针处(下一个可读字节)字节偏移量。
    • seek(),设置文件指针的偏移量,到下一个可读或写的字节。偏移量从文首开始计算。偏移量可以超出文末,但是不会改变文本长度属性。
    • length(),返回文本长度,即字节数。
    • setLength(),设置文本长度,一般传入0可以用于清空文本。

    效率

    RandomAccessFile的读写方法都有native关键词。说明每调用一次读或写方法,都需要与磁盘进行一次I/O操作。如果读取大文件,效率非常低。

    在文档中介绍RandomAccessFile行为好比内存中有一个巨大数组存储文件字节,其实并没有。

    在java.io包中解决办法是给流添加一个缓冲区,减少与磁盘I/O操作次数,从而提高效率。例如BufferedInputStream。

    但是这样没有了RandomAccessFile的特性-随机访问文件。可以使用jdk 1.4 nio中的内存映射替换RandomAccessFile,或者扩展RandomAccessFile实现带有Buffer的RandomAccessFile。

    可以参考花1K内存实现高效I/O的RandomAccessFile类

    内存映射文件概念:由于文件过大,无法直接放入内存中进行I/O操作,所以一般都是通过缓冲区来提高效率。而有了内存映射概念,可以认为通过一次与磁盘的I/O操作,把文件放入内存,存储在一个数组中进行访问,大大提高了效率。

    问题

    boolean类型数据占几个字节,值是多少?

    boolean类型数据占一个字节,true的字节值为1,false的字节值为0。

    相关文章

      网友评论

          本文标题:Java I/O 学习(一)

          本文链接:https://www.haomeiwen.com/subject/xwefwttx.html