Java笔记11
这篇将会讲Java的IO。
Java的IO通过java.io包下的类和接口来支持,在java.io包下主要包括输入、输出两种IO流,每种输入、输出流又可分为字节流和字符流两大类。其中字节流以字节为单位来处理输入、输出操作,而字符流则以字符来处理输入、输出操作。除此之外,Java的IO流使用了一种装饰器设计模式,它将IO流分成底层节点流和上层处理流,其中节点流用于和底层的物理存储节点直接关联—不同的物理节点获取节点流的方式可能存在一定的差异,但程序可以把不同的物理节点流包装成统一的处理流,从而允许程序使用统一的输入、输出代码来读取不同的物理存储节点的资源。
Java7在java.nio及其子包下提供了一系列全新的API,这些API是对原有新IO的升级,因此也被称为NIO2,通过这些NIO2,程序可以更高效地进行输入、输出操作。
除此之外,本文还会介绍Java对象的序列化机制,使用序列化机制可以把内存中的Java对象转换成二进制字节流,这样就可以把Java对象存储到磁盘里,或者在网络上传输Java对象。这也是Java提供分布式编程的重要基础。
File类
File类是java.io包下在程序中操作文件和目录的工具。File类能新建,删除,重命名文件和目录,而不能访问文件本身。如果需要访问文件本省,则需要使用输入\输出流。
访问文件和目录
File类可以使用文件路径字符串来创建File实例,该文件路径字符串既可以是绝对路径,也可以是相对路径。相对路径一般是在JVM运行时所在的路径,IDEA中是在相对应的项目文件夹中。也就是说,File类的事例代表的是一个路径或者一个文件。
访问文件名相关的方法
- String getName():返回此File对象所表示的文件名或路径名(如果是路径,则返回最后一级子 路径名)。
- String getPath():返回此File对象所对应的路径名。
- File getAbsoluteFile():返回此File对象的绝对路径。
- String getAbsolutePath(): 返回此File对象所对应的绝对路径名。
- String getParent():返回此File对象所对应目录(最后一级子目录)的父目录名。
- boolean renameTo(File newName):重命名此File对象所对应的文件或目录,如果重命名成功, 则返回true;否则返回false。
文件检测相关的方法
- boolean exists():判断File对象所对应的文件或目录是否存在。
- boolean canWrite():判断File对象所对应的文件和目录是否可写。
- boolean canRead():判断File对象所对应的文件和目录是否可读。
- boolean isFile():判断File对象所对应的是否是文件,而不是目录。
- boolean isDirectory():判断F*ile对象所对应的是否是目录,而不是文件。
- boolean isAbsolute():判断File对象所对应的文件或目录是否是绝对路径。该方法消除了不同平台的差异,可以直接判断File对象是否为绝对路径。在 UNIX/Linux/BSD等系统上,如果路径名开头是一条斜线(/),则表明该n ie对象对应一个绝对路径;在 Windows等系统上,如果路 径开头是盘符,则说明它是一个绝对路径。
获取常规文件信息
- long lastModified():返回文件的最后修改时间。
- long length():返回文件内容的长度。
文件操作相关的方法
- boolean createNewFile():当此File对象所对应的文件不存在时,该方法将新建一个该File对象所指定的新文件,如果创建成功则返回true;否则返回false。
- boolean delete():删除File对象所对应的文件或路径。
- static File createTempFile(String prefix, String suffix):在默认的临时文件目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和给定后缀作为文件名。这是一个静态方法,可以直接通过File类来调用。prefix参数必须至少是3字节长。建议前缀使用一个短的、有意义的字符串,比如 “hjb”或”mail”。suffix参数可以为null,在这种情况下,将使用默认的后缀“ .tmp”。
- static File createTempFile(String prefix, String suffix, File directory): 在 directory所指定的目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和给定后缀作为文件名。这是一个静态方法,可以直接通过File类来调用。
- void deleteOnExit():注册一个删除钩子,指定当Java虚拟机退出时,删除File对象所对应的文件和目录。
目录操作相关的方法
- boolean mkdir():试图创建一个File对象所对应的目录,如果创建成功,则返回true;否则返回false。调用该方法时File对象必须对应一个路径,而不是一个文件。
- String[] list():列出File对象的所有子文件名和路径名,返回String数组。
- File[] listFiles():列出File对象的所有子文件和路径,返回File数组。
- static File[] listRoots():列出系统所有的根路径。这是一个静态方法,可以直接通过File类来调用。
1 | import java.io.File; |
文件过滤器
在File类中list()方法可以接受一个FilenameFilter参数,通过该参数只列出符合条件的文件。
1 | public String[] list(FilenameFilter filter) { |
进一步查看源码,发现FilenameFilter是一个接口(函数式接口)。接口中包含了一个accept(File dir,String name)方法,该方法将依次对指定File的所有子目录或文件进行迭代。如果该方法返回true,那么list()方法将会列出该子目录或者文件。
1 | import java.io.File; |
这里在对这个Lambda表达式解释一下,(dir, name)
的目标是FilenameFilter接口,而这个接口是一个函数式接口,accept()抽象方法的返回值是boolean型的,所以经过Lambda表达式的转换,最终一个boolean类型的值传入到list()方法中参与执行。
理解Java的IO流
Java的IO流是实现输入输出的基础,它可以方便的实现数据的输入输出。在Java中把不同的输入输出源抽象表示为“流(stream)”,通过流的方式允许Java以相同的方式来访问不同的输入输出源。stream是从起源(source)到(sink)的有序数据。
流的分类
输入流和输出流
按照方向来分,从内存向硬盘中写入数据,对于硬盘来说这是输入流,对于内存来说,这是输出流。但是,程序运行是在内存的,所以划分输入输出的角度还是要从内存来考虑。Java的输入流主要有InputStream和Reader作为基类,输出流有OutputStream和Writer作为基类。它们都是抽象类。
字节流和字符流
字节流和字符流的用法几乎一样,字节流操作的数据单元是8bit,一个字节,而字符流是16bit,两个字节。前者主要是由InputStream和OutputStream作为基类,后者主要是由Reader和Writer作为基类。
节点流和处理流
可以从/向一个特定的IO设备中读/写数据的流称为节点流也被成为低级流。当程序使用节点流输入输出时,程序直接连接到实际的数据源,和实际的输入输出节点链接。
处理流则用于对一个已存在的流进行连接或封装,通过封装后的流来实现数据读写的功能,也被成为高级流。处理流不连接实际的数据,没有和实际的输入输出节点连接。它是面向节点流的,这是一种典型的装饰器设计模式。处理流来包装处理节点流,而程序则不用对不同的节点流做出调整,只要处理流将节点流进行包装,消除了“差异”。这样可以用一套代码访问多种数据源。节点流因此也被成为包装流。
流的概念模型
Java的IO流共涉及40多个类,这些类都是从前面提到过的四个基类派生出来的。
- InputStream/Reader:所有输入流的基类,前者是字节流,后者是字符流
- OutputStream/Writer:所有输出流的基类,前者是字节流,后者是字符流
对于InputStream和Reader而言,它们把输入设备想象成一个水管,每一个字节或字符就是一个水滴,同时有个隐式的指针表示当前正准备读取哪个水滴。每当程序取出一个或多个水滴时,指针就会相应的移动。同时,这两个类中也提供方法来控制指针。而OutputStream和Writer于此类似,只不过水管的下一部分是空的,即即将放入水滴的地方是空地,而指针记录的就是水滴即将放的地方。
处理流模型的功能体现在一下两个方面:
- 性能的提高,主要以增加缓冲的方式来提高输入输出的效率
- 操作的便捷,处理流可能提供了方法来一次输出输入大量“水滴”
处理流可以嫁接在任何已存在的流的基础之上,将不同的节点流包装成处理流,这就允许Java应用使用相同的代码来读写不同的输入输出设备的数据。
探讨字节流和字符流
InputStream和Reader
InputStream和Reader是所有输入流的抽象基类,本身并不能创建实例来执行输入。
InputStream里包含如下三个方法:
- int read():从输入流中读取一个字节(一滴水),返回读取的字节数据
- int read(byte[] b):从输入流中最多读取b.length个字节的数据,并将其存储在字节数组b中,返回实际读取的字节数
- int read(byte[] b,int off,int len):从输入流中最多读取len个字符的数据,并将其存储在数组b中,且放入位置是off指定的位置
Reader里包含如下三个方法:
- int read():从输入流中读取一个字节(一滴水),返回读取的字节数据
- int read(char[] cbuf):从输入流中最多读取cbuf.length个字符的数据,并将其存储在字节数组cbuf中,返回实际读取的字符数
- int read(char[] cbuf,int off,int len):从输入流中最多读取len个字符的数据,并将其存在字符数组cbuf中,存储位置是off,返回实际读取的字符数
当read(char[] cbuf)或read(byte[] b)方法返回-1,即表明到了输入流的结束点。
InoutStream和Reader都是抽象类,它们分别有一个用于读取文件的输入流:FileInputStream和FileReader,它们都是节点流,直接和文件关联。
1 | import java.io.FileInputStream; |
在最后,使用fis.close
语句关闭资源,因为IO资源不属于内存中的资源。也可以使用在异常一节提到的Java7改进的try语句自动关闭IO流。
1 | import java.io.FileReader; |
除此之外,InputStream和 Reader还支持如下几个方法来移动记录指针。
- void mark(int readAheadLimit):在记录指针当前位置记录一个标记(mark)。
- boolean markSupported():判断此输入流是否支持mark()操作,即是否支持记录标记。
- voidreset():将此流的记录指针重新定位到上一次记录标记(mark)的位置。
- long skip(long n):记录指针向前移动n个字节/字符。
OutputStream和Writer
OutputStream和Writer与上面的非常相似,两个流都提供了如下的方法:
- void write(int c):将指定的字节/字符输出到输出流中,其中c既可以代表字节,也可以代表字符。
- void write(byte[]/char[] buf):将字节数组/字符数组中的数据输出到指定输出流中。
- void write(byte[]/char[] buf, int off, int len):将字节数组/字符数组中从off位置开始,长度为len的字节/字符输出到输出流中。
因为字符流直接以字符作为操作的单位,所以Writer可以用字符串来代替字符数组:
- void write(String str):将str字符串里包含的字符输出到指定输出流中。
- void write(String str,int off,int len):将str字符串里从off位置开始,长度为len的字符输出到指定输出流中。
下面的代码使用FileInputStream来执行输入,用FileOutputSteam执行输出,实现复制文件内容的功能:
1 | import java.io.FileInputStream; |
使用Java的IO流执行输出时,不能忘记关闭输出流,关闭输出流,这样除了可以保证流的物理资源被回收之外,还可以将输出缓冲区的数据flush到物理节点去(因为在执行close()方法之前自动执行flush()方法)。
如果希望直接输出字符串内容,则使用Writer效果更好:
1 | import java.io.FileWriter; |
输入/输出流体系
上面介绍的四种都是节点流,都稍显繁琐,所以下面使用处理流简化代码。
处理流的用法
先直接看代码:
1 | import java.io.FileOutputStream; |
使用处理流操作更简单,执行效率更高。只要流的构造器参数不是一个物理节点,而是已经存在的流,那么这个流就是处理流。上面代码的try语句中,先定义了一个节点输出流FileOutputStream,然后使用PrintStream包装了这个流。关于PrintStream的相关用法参见连接https://www.cnblogs.com/skywang12345/p/io_16.html
在关闭IO资源时,只要关闭最上层的处理流即可。
关于输入/输出流体系
Java的输入输出流体系提供了将近40个类,但是这中间是有着明显的体系结构的,看下图:
注意表中的粗体字标出的类代表节点流,斜体字标出的类代表抽象基类
表中还列出了一种以数组为物理节点的节点流,字节流以字节数组为节点,字符流以字符数组为节点;这种以数组为物理节点的节点流除了在创建节点流对象时需要传入一个字节数组或者字符数组之外,用法上与文件节点流完全相似。与此类似的是,字符流还可以使用字符串作为物理节点,用于实现从字符串读取内容,或将内容写入字符串(用StringBuffer充当字符串)的功能。下面程序示范了使用字符串作为物理节点的字符输入/输出流的用法。
1 | import java.io.IOException; |
转换流
输入输出流体系中还提供了两个转换流,InputStreamReader和OutputStreamWriter用于将字节流转换为字符流。
以获取键盘输入为例。System.in代表标准输入,即键盘输入,但是这个标准输入流是抽象字节流基类InputStream的实例,使用不太方便,而且由于键盘输入的一般是字符,所以可以使用InputStreamReader将其转换为字符流。然后将普通的Reader再次包装为BufferedReader,利用BufferedReader的readLine()方法一次读取一行内容。
1 | import java.io.BufferedReader; |
将System.in包装成BufferedReader,BufferedReader流具有缓冲功能,它可以一次读取一行文本—以换行符为标志,如果它没有读到换行符,则程序阻塞,等到读到换行符为止。
推回输入流
在输入输出流体系中有两个特殊的流,就是PushbackInputStream和PushbackReader。它们都提供下面三个方法:
- void unread(byte[]/char[] buf):将一个字节/字符数组内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。
- void unread(byte[]/char[] b, int off, int len ):将一个字节/字符数组里从off开始,长度为len字节/字符的内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。
- void unread(int b):将一个字节/字符(b)推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。
可以看出这里的三个方法与InputStream和Reader的方法相对应。
这两个推回输入流都带有一个推回缓冲区,当程序调用这两个推回输入流的unread()方法时,系统将会把指定数组的内容推回到该缓冲区里,而推回输入流每次调用read()方法时总是先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后,但还没有装满read()所需的数组时才会从原输入流中读取。
当程序创建一个PushbacklnputStream和PushbackReader时需要指定推回缓冲区的大小,默认的推回缓冲区的长度为1。如果程序中推回到推回缓冲区的内容超出了推回缓冲区的大小,将会引发Pushback buffer overflow的IOException异常。
下面的代码师徒找出程序中的”new PushbackReader”字符串,找到后只打印目标字符串之前的内容。
1 | import java.io.FileReader; |
阅读代码发现,pr冒出来一个read()方法,也就是说,这两个推回输入流不是仅仅有上面三个方法。进一步查看源码可知,这个read()方法实际还是用Reader基类的代码,只不过中间经过一层FilterReader类,加了点料。这个代码暗含一个问题,如果,缓冲区长度不是“篮子”长度的两倍,那么有可能产生Pushback buffer overflow异常。
重定向标准输入/输出
java的标准输入输出分别通过System.in和System.out实现,前者是从键盘读取输入,后者是从屏幕打印输出。System类中提供了三个重定向标准输入/输出的方法。
- static void setErr(PrintStream err):重定向“标准”错误输出流。
- static void setIn(InputStream in):重定向“标准”输入流。
- static void setOut(PrintStream out):重定向“标准”输出流。
下面的程序通过重定向标准输入输出流。将System.out的输出重定向到文件输出,而不是在屏幕上。
1 | import java.io.FileOutputStream; |
下面程序重定向标准输入,从而可以将System.in重定向到指定文件,而不是键盘输入。
1 | import java.io.FileInputStream; |
Java虚拟机读写其他进程的数据
使用Runtime对象的exec()方法可以运行平台上的其他程序,该方法产生一 个Process对象,Process对象代表由该Java程序启动的子进程。Process类提供了如下三个方法,用于 让程序和其子进程进行通信。
- InputStream getErrorStream():获取子进程的错误流。
- InputStream getInputStream(): 获取子进程的输入流。
- OutputStream getOutputStream():获取子进程的输出流。
1 | import java.io.BufferedReader; |
1 | import java.io.FileInputStream; |