从本文开始,讲述Java中文件和IO的相关知识。在做企业web开发时,文件操作相对涉及的比较少,在桌面系统开发中比较常见。正因如此,导致很多人对文本文件、二进制文件、字节流、字符流等概念性的东西都不能完全搞清楚。其实在我在写这篇文章之前,也是分不清的,但是因为日常工作中也很少涉及相关的内容,所以一直也没深究。本篇文章就来科普一下一些基本概念,同时介绍一下Java IO体系。
1. 基本概念
1.1 文件概述
提到文件,我们可以自然联想到常见的可执行文件、图片文件、视频文件、Word文件、txt文件、pdf文件等,这些文件在计算机中都是以二进制存储的,平时我们能看到文件的内容,是因为计算机使用了相应的应用程序对二进制文件进行了解析。
举个例子,我们保存存在一个txt文件,保存的内容是”hello 你好”,在notepad++中使用HEX-Editor插件可以看到在计算机磁盘中保存的形式如下图所示:
文件编码使用的是UTF-8,一个英文字母占一个字节,一个中文占三个字节,68就是字符h的UTF-8编码(16进制),e4bda0三个字节就是中文字符”你”的UTF-8编码,e5a5bd就是中文字符”好”的UTF-8编码。关于编码规则,我在之前的一篇文章Java编程拾遗『搞定编码』讲的很清楚,建议去了解一下,对于理解本文有帮助。另外给一个工具网站,可以用来实践字符编码在线编码转换工具。
上面展示了文本文件在磁盘中的二进制存储,文件中每个二进制字节都是某个可打印字符的一部分,都可以用最基本的文本编辑器进行查看和编辑,如Windows上的notepad, Linux上的vi;但是如果不是文本文件,文件中每个二进制字节就不一定是可打印字符的一部分了,一个字节有可能表示颜色、可能表示字体、可能表示声音大小等,这就是为什么非文本文件使用文本编辑器打开会看到一堆乱码的原因,因为文本编辑器是按照特定文本编码方式解析二进制内容的(比如用记事本打开word文件)。
文件一般可以分为两种类型,一种是文本文件,另一种是二进制文件。比如txt文件、.java文件、html文件等都属于文本文件;而zip文件、pdf文件、 mp3文件、 excel文件、word文件等都是二进制文件 。二进制文件必须使用特定的应用程序解析才能正确地看到其内容,而文本文件比较随意,任何文本编辑器都可以查看。
至此,我们知道了文件可以分为文本文件和二进制文件,上面也举了一些文件后缀和文件类型的关系,比如.txt文件是文本文件、.pdf文件是二进制文件。但是这种方式其实是错误的,给文件加正确的后缀名是一种惯例,但并不是强制的,比如我是用notepad编辑了一个文本文件,在保存时后缀名保存为.doc,但是当使用word软件打开时,是会报错的。
一个文件是文本文件还是二进制文件,跟文件的后缀名没有直接关系。当我们讲一个文件是文本文件时,说明这个文件在磁盘中二进制存储的每个字节都是可打印字符的一部分,所有字节在文本编辑器中都是可以正常查看的。可以做个试验,在linux下,新建一个test.doc文件,并在其中存储文本内容,然后将文件拿到windows系统下通过记事本打开,可以发现内容是可以正常显示的(之所以在linux下新建doc文件,是因为在windows下,当记事本保存为doc文件时,会添加一些附加信息)。如果一个文件在磁盘中的二进制存储的二进制字节存在不可打印字符,那么文件不是文本文件,比如Java序列化得到的文件,即使文件后缀名为.txt,该txt文件也不是文本文件。本质上讲文本文件也是二进制文件,只不过它比较特殊的是,二进制的每个字节都是可打印字符的一部分。
可以看到,即使文件后缀为doc,但是存储内容仍然只是文本”卓立123″的UTF-8编码,并没有一些word文件的控制信息。最后要提一下的是,最后一个字节0a是linux下的换行符\n,在windows下换行符为\r\n,关于linux和windows下换行符的关联,请参考这篇文章彻底解读剪不断理还乱的\r\n和\n, 以Windows和Linux为例。
1.2 文件系统
各种操作系统都会隐藏物理硬盘概念,提供一个逻辑上的统一结构。在windows中,可以有多个逻辑盘,比如C、D、E等,每个盘可以被格式化为一种不同的文件系统,常见的文件系统有FAT32和NTFS。在linux中,只有一个逻辑的根目录,用斜线/表示,linux支持多种不同的文件系统,如Ext2/Ext3/Ext4等。不同的文件系统有不同的文件组织方式,不过一般编程时,编程语言会提供了统一的API,屏蔽不同文件系统的底层细节。
在逻辑上,windows中有多个根目录,linux只有一个根目录,每个根目录下就是一颗子目录和文件构成的树。每个文件都有文件路径的概念,路径有两种形式,一种是绝对路径,另一种是相对路径。
绝对路径就是从根目录开始到当前文件的完整路径,在windows中,目录之间用反斜线分隔,如”C:\code\hello.java”,在linux中,目录之间用正斜线分隔,如”/home/zhuoli/code/hello.java”。在Java中,java.io.File类定义了一个静态变量File.separator,表示路径分隔符,编程时应使用该变量而避免硬编码。
相对路径是相对于当前目录而言的,在命令行终端上,通过cd命令进入到的目录就是当前目录,在Java中,通过System.getProperty(“user.dir”)可以得到运行Java程序的当前目录,相对路径不以根目录开头,比如在windows上,当前目录为”D:\zhuoli”,相对路径为”code\hello.java”,则完整路径为”D:\zhuoli\code\hello.java”。
每个文件除了有具体内容,还有元数据信息,如文件名、创建时间、修改时间、文件大小等。文件还有一个是否隐藏的性质,在linux系统中,如果文件名以.开头,则为隐藏文件,在windows系统中,隐藏是文件的一个属性,可以进行设置。大部分文件系统,每个文件和目录还有访问权限的概念,对所有者、用户组可以有不同的权限,权限具体包括读、写、执行。
在windows中,一般是大小写不敏感的,而linux则一般是大小写敏感的,同一个目录下,”abc.txt”和”ABC.txt”在windows中被视为同一个文件,而linux视为不同的文件。
1.3 文件读写
文件是放在硬盘上的,程序处理文件需要将文件读入内存,修改完成后,再写回硬盘。操作系统提供了对文件读写的基本API,不同操作系统的接口和实现是不一样的,Java封装了不同操作系统文件读写的API,提供了统一的API。
为了提升文件操作的效率,操作系统经常使用一种常见的策略,即使用缓冲区。读文件时,即使目前只需要少量内容,但还会接着读取,就一次读取比较多的内容,放到读缓冲区,下次读取时,缓冲区有,就直接从缓冲区读,减少访问操作系统和硬盘。写文件时,先写到写缓冲区,写缓冲区满了之后,再一次性的调用操作系统写到硬盘。其实就是尽可能减少磁盘访问次数,因为每一次磁盘操作就需要一次磁盘IO,设计磁盘机械寻道等操作,是比较耗时的。
操作系统操作文件一般有打开和关闭的概念,打开文件会在操作系统内核建立一个有关该文件的内存结构,这个结构一般通过一个整数索引来引用,这个索引一般称为文件描述符,这个结构是消耗内存的,操作系统能同时打开的文件一般也是有限的,在不用文件的时候,应该记住关闭文件,关闭文件一般会同步缓冲区内容到硬盘,并释放占据的内存结构。
操作系统一般还支持一种称之为内存映射文件的高效的随机读写大文件的方法,将文件直接映射到内存,操作内存就是操作文件,在内存映射文件中,只有访问到的数据才会被实际拷贝到内存,且数据只会拷贝一次,被操作系统以及多个应用程序共享。
2. Java文件概述
在Java中,文件不是单独处理的,而是视为输入输出(IO – Input/Output)设备的一种。Java使用流处理所有的IO,包括键盘、显示终端、网络等。Java中的流分为输入流和输出流,输入流就是可以从中获取数据,输入流的实际提供者可以是键盘、文件、网络等,输出流就是可以向其中写入数据,输出流的实际目的地可以是显示终端、文件、网络等。我本人都是这么理解的,输入与输出都是针对内存而言的,输入流的作用就是向内存中输入数据,输出流的作用是将内存中的数据输出到外部,输入流和输出流就是上述动作的一个连接器。
Java IO的基本类大多位于包java.io中,类InputStream表示输入流,OutputStream表示输出流。有了流的概念,就有了很多面向流的代码,比如对流做加密、压缩、计算信息摘要、计算检验和等,这些代码接受的参数和返回结果都是抽象的流。一些实际上不是IO的数据源和目的地也转换为了流,以方便参与这种协作,比如字节数组,也包装为了流ByteArrayInputStream和ByteArrayOutputStream。Java中的流的概念主要存在以下几种:
- InputStream/OutputStream: 基类,抽象类。
- ByteArrayInputStream/ByteArrayOutputStream: 输入源和输出目标是字节数组的流。
- FileInputStream/FileOutputStream: 输入源和输出目标是文件的流。
- FilterInputStream/FilterOutputStream,所有包装流的父类,一些“特殊”功能的流,比如DataInputStream/DataOutputStream、BufferedInputStream/BufferedOutputStream都继承了改类。
- ObjectInputStream/ObjectOuputStream:输入源和输出目标是对象的流,用于实现Java序列化。
上面介绍了Java中用于处理IO的流的概念,可以以字节为单位处理文件,但是对于最基础也是最常见的文本文件通过字节为单位处理很明显是不方便的,所以Java中另外提供了一套方便处理文本文件的Reader和Writer,可以以字符(Java中的char)为单位处理文件,Reader和Writer是抽象类,它们有很多子类:
- Reader/Writer:基类,抽象类。
- BufferedReader/BufferedWriter:装饰类,用于缓冲基本Reader/writer。
- CharArrayReader/CharArrayWriter:输入源和输出目标是char数组的Reader/writer。
- InputStreamReader/OutputStreamWriter:适配器类,输入是InputStream,输出是OutputStream,将字节流转换为字符流,最常用的处理文件的FileReader/FileWriter就是继承自该类。
- StringReader/StringWriter:输入源和输出目标是字符串的Reader/writer。
Java IO体系如下图所示:
参考链接
1. 《Java编程的逻辑》