Java 的 IO 笔记——拨开云雾见月明
Java 的 IO 笔记
首先 ,IO 是指输入和输出,它是所有程序都必不可少的部分,程序使用输入机制,允许程序读取外部数据(包括来自磁盘、光盘等存储设备的数据)、用户输入数据;程序使用输出机制,允许程序记录运行状态,将程序数据输出到磁盘、光盘等存储设备中。
Java 的 IO 通过 java . io 包下的类和接口支持,在 java . io 包下主要包括输入、输出两种 IO 流,每种输入、输出流又分为字节流和字符流两大类。其中字节流以字节为单位来处理输入、输出操作,而字符流则以字符来处理输入、输出操作。除此之外,Java 的 IO 流使用了一种装饰者设计模式,它将 IO 流分成底层节点流和上层处理流,其中节点流用于和底层的物理存储节点直接关联——不同的物理节点获取节点流的方式可能存在一定的差异,但程序可以把不同的物理节点流包装成统一的处理流,从而允许程序使用统一的输入、输出代码来读取不同的物理存储节点的资源。
1 . File 类
File 类是 java . io 包下代表与平台无关的文件和目录,也就是说,如果希望在程序中操作文件和目录,都可以通过 File 类来完成。值得指出的是,不管是文件还是目录都是使用 File 来操作,File 能新建、删除、重命名文件和目录,File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入、输出流。
1 . 1 访问文件和目录
File 类可以使用文件路径字符串来创建 File 实例,该文件路径字符串既可以是绝对路径,也可以是相对路径。在默认情况下,系统总是依据用户的工作路径来解释相对路径,这个路径由系统属性“ user.dir ” 指定,通常也就是运行 Java 虚拟机时所在的路径。
一旦创建了 File 对象后,就可以调用 File 对象的方法来访问,File 类提供了很多方法来操作文件和目录,下面列出一些比较常用的方法。
(1)dian访问文件名相关的方法
- String getName( ) : 返回此 File 对象所表示的文件名或路径名(如果是路径,则返回最后一级子路径名)。
- String getPath( ) : 返回此 File 对象所对应的路径名。
- File getAbsoluteFile( ) : 返回此 File 对象的绝对路径。
- String getAbsolutePath ( ) : 返回此 File 对象所对应的绝对路径名。
- String getParent ( ) : 返回此 File 对象所对应目录(最后一级子目录)的父目录名。
- boolean renameTo ( File newName ) : 重命名此 File 对象所对应的文件或目录,如果重命名成功,则返回 true ,否则返回 false 。
(2)文件的检测相关的方法
- boolean exists ( ) : 判断 File 对象所对应的文件或目录是否存在。
- boolean canWrite ( ) : 判断 File 对象所对应的文件和目录是否可写。
- boolean canRead ( ) : 判断 File 对象所对应的文件和目录是否可读。
- boolean isFile ( ) : 判断 File 对象所对应的是否是文件,而不是目录。
- boolean isDirectory ( ) : 判断 File 对象所对应的是否是目录,而不是文件。
- boolean is Absolute ( ) : 判断 File 对象所对应的文件或目录是否是绝对路径。该方法消除了不同平台的差异,可以直接判断 File 对象是否为绝对路径。在 UNIX/Linux/BSD 等系统上,如果路径名开头是一条斜线( / ),则表明该 File 对象对应一个绝对路径;在 Windows 等系统上,如果路径开头是盘符,则说明它是一个绝对路径。
(3)获取常规文件信息
- long lastModified ( ) : 返回文件的最后修改时间。
- long length ( ) : 返回文件内容的长度。
(4)晚间操作相关的方法
- 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 对象所对应的文件和目录。
(5)目录操作相关的方法
- boolean mkdir ( ) : 试图创建一个 File 对象所对应的目录,如果创建成功,则返回 true ,否则返回 false 。调用该方法时,File 对象必须对应一个路径,而不是一个文件。
- String [ ] list ( ) : 列出 File 对象的所有子文件名和路径名,返回 String 数组。
- File [ ] listFiles ( ) : 列出 File 对象的所有子文件和路径,返回 File 数组。
- static File [ ] listRoots ( ) : 列出系统所有的根路径。这是一个静态方法,可以直接通过 File 类来调用。
下面通过具体的代码来一一说明,代码如下:
public class FileDemo {
public static void main(String[] args) throws IOException{
File file=new File(".");//以当前路径来创建一个 File 对象
System.out.println(file.getName());//直接获取文件名,输出一点
System.out.println(file.getParent());//获取相对路径的父路径可能出错,输出 null
System.out.println(file.getAbsoluteFile());//获取绝对路径
System.out.println(file.getAbsoluteFile().getParent());//获取上一级路径
File tmpFile=File.createTempFile("aaa", ".txt",file);//在当前路径下创建一个临时文件
tmpFile.deleteOnExit();//指定当 JVM 退出时删除该文件
File newFile =new File (System.currentTimeMillis()+"");//以系统当前时间作为新文件名来创建新文件
System.out.println("newFile 对象是否存在:"+newFile.exists());
newFile.createNewFile();//以指定newFile对象来创建一个文件
//以newFile对象来创建一个目录,因为 newFile已经存在,所以下面方法返回false,即无法创建该目录
newFile.mkdir();
//使用 list() 方法列出当前路径下的所有文件和路径
String [] fileList=file.list();
System.out.println("====当前路径下所有文件和路径如下:");
for(String fileName:fileList)
{
System.out.println(fileName);
}
File[] roots=File.listRoots();//静态方法列出所有的磁盘根路径
System.out.println("====系统所有根路径如下:");
for(File root:roots)
{
System.out.println(root);
}
}
}
输出结果如下:
运行上面程序,可以看到程序列出当前路径的所有文件和路径时,列出了程序创建的临时文件,但程序运行结束后,aaa.txt 临时文件并不存在,因为程序指定虚拟机退出时自动删除该文件。
上面程序还有一点需要注意,当使用相对路径的 File 对象来获取父路径时可能引起错误,因为该方法返回将 File 对象所对应的目录名、文件名里最后一个子目录名、子文件名删除后的结果。
1 . 2 文件过滤器
在 File 类的 list ( ) 方法中可以接受一个 FilenameFilter 参数,通过该参数可以只列出符合条件的文件。这里的 FilenameFilter 接口和 javax.swing.filechooser 包下的 FileFilter 抽象类的功能非常相似,可以把 FileFilter 当成 FilenameFilter 的实现类。
FilenameFilter 接口里包含了一个 accept ( File dir , String name ) 方法,该方法将依次对指定 File 的所有子目录或者文件进行迭代,如果该方法返回 true ,则 list ( ) 方法会列出该子目录或者文件。
下面举个例子,代码如下:
public class FileDemo {
public static void main(String[] args) throws IOException{
File file=new File(".");
String [] nameList=file.list(new FilenameFilter(){
@Override
public boolean accept(File dir, String name) {
if(name.endsWith(".java") || new File(name).isDirectory())
{
return true;
}else
{
return false;
}
}
});
for(String name:nameList)
{
System.out.println(name);
}
}
}
输出结果如下:
上面程序实现了 accept ( ) 方法,该方法就是指定自己的规则,指定那些文件应该由 list ( ) 方法列出。
2 . Java 的 IO 流
Java 的 IO 流是实现输入、输出的基础,它可以方便地实现数据输入、输出操作,在 Java 中把不同的输入、输出源(键盘、文件、网络连接等)抽象表述为 “流”( stream ),通过流的方法允许 Java 程序使用相同的方式来访问不同的输入、输出源。stream 是从起源到节后的有序数据。
Java 把所有传统的流类型(类或抽象类)都放在 java.io 包中,用以实现输入、输出功能。
2 . 1 流的分类
按照不同的分类方式,可以将流分为不同的类型。下面从不同的角度来对流进行分类,它们在概念上可以存在重叠的地方。
(1)输入流和输出流
按照流的流向来分,可以分为输入流和输出流。
- 输入流:只能从中读取数据,而不能向其写入数据。
- 输出流:只能向其写入数据,而不能从中读取数据。
此处的输入、输出涉及一个方向问题,也就是说,所谓的输入、输出是站在程序运行所在的内存的角度来分析问题的,下面通过两个例子来说明这个方向问题:
上图中数据从内存到硬盘,所以站在内存的角度出发分析,内存端是输出流;如果数据从硬盘到内存,内存端就变成了输入流了。
上图中,数据从服务器通过网络流向客户端,在这种情况下,Server 端的内存负责将数据输出到网络里,因此,Server 端的程序使用输出流;Client 端的内存负责从网络里读取数据,因此 Client 端的程序应该使用输入流。
Java 的输入流主要由 InputStream 和 Reader 作为基类,而输出流则主要由 OutputStream 和 Writer 作为基类,它们都是一些抽象基类,无法直接创建实例。
(2)字节流和字符流
字节流和字符流的用法几乎完全一样,区别在于字节流和字符流所操作的数据单元不同,即字节流操作的数据单元是8位的字节,而字符流操作的数据单元是16位的字符。
字节流主要由 InputStream 和 OutputStream 作为基类,而字符流则主要由 Reader 和 Writer 作为基类。
(3)节点流和处理流
按照流的角色来分,可以分为节点流和处理流。
可以从/向一个特定的 IO 设备(如磁盘、网络)读/写数据的流,成为节点流,节点流也被称为低级流,如下图所示:
从上图可以看到,当使用节点流进行输入、输出时,程序直接连接到实际的数据源,和实际的输入、输出节点连接。
处理流则用于对一个已存在的流进行连接或封装,通过封装后的流来实现数据读/写功能。处理流也被称为高级流,如下图所示:
从上图可以看出,当使用处理流进行输入、输出时,程序并不会直接连接到实际的数据源,没有和实际的输入、输出节点连接。使用处理流的一个明显好处是:只要使用相同的处理流,程序就可以采用完全相同的输入、输出代码来访问不同的数据源,随着处理流所包装节点流的变化,程序实际所访问的数据源也相应地发生变化。
实际上,Java 使用处理流来包装节点流是一种典型的装饰者模式,通过使用处理流来包装不同的节点流,既可以消除不同节点流的实现差异,也可以提供更方便的方法来完成输入、输出功能。因此处理流也被称为包装流。
2 . 2 流的概念模式
Java 把所有设备里的有序数据抽象成流模型,简化了输入、输出,处理,理解了流的概念模型也就了解了 Java IO 。
Java 的 IO 流共涉及40多个类,这些类看上去杂乱无章,但实际上非常规则,而且彼此之间存在着非常紧密的联系。Java 的 IO 流的40多个类都是从如下4个抽象基类派生的。
- InputStream / Reader : 所有输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream / Writer : 所有输出流的基类,前者是字节输出流,后者是字符输出流。
对于 InputStream 和 Reader 而言,它们把输入设备抽象成一个“水管”,这个水管里的每个“水滴”依次排列,如下:
从上图可以看出,字节流和字符流的处理方式非常相似,知识它们处理的输入、输出单位不同而已。输入流使用隐式的记录指针来表示当前正准备从哪个“水滴”开始读取,每当程序从 InputStream 或 Reader 里取出一个或多个“水滴”后,记录指针自动向后移动;除此之外,InputStream 和 Reader 里都提供了一些方法来控制记录指针的移动。
对 OutputStream / Writer 而言,它们同样把输出设备抽象成一个“水管”,只是这个水管里没有任何水滴,如下图:
上图中,当执行输出时,程序相当于依次把“水滴”放入到输出流的水管中,输出流同样采用隐式的记录指针来标识当前水滴即将放入的位置,每当程序向 OutputStream 或 Writer 里输出一个或多个水滴后,记录指针自动向后移动。
上面两个图均显示了 Java IO 流的基本概念模型,除此之外,Java 的处理流模型则体现了 Java 输入、输出设计的灵活性。处理流的功能主要体现在以下两个方面:
- 性能的提高:主要以增加缓冲的方式来提高输入、输出。
- 操作的便捷:处理流提供了一系列便捷的方法来一次输入、输出大批量的内存,而不是输入、输出一个或多个“水滴”。
处理流可以“嫁接”在任何已存在的基础之上,这就允许 Java 应用程序采用相同的代码。透明的方式来访问不同的输入、输出设备的数据流。下图显示了处理流的模型:
通过使用处理流,Java 程序无须理会输入、输出节点是磁盘、网络还是其他的输入、输出设备,程序只要将这些节点流包装成处理流,就可以使用相同的输入、输出代码来读写不同的输入、输出设备的数据。
3 . 字节流和字符流
字节流操作的数据单元是字节,字符流操作的数据单元是字符,它们的操作方式几乎完全一样,区别只是操作的数据单元不同而已。下面我们一起来学习。
3 . 1 InputStream 和 Reader
InputStream 和 Reader 是所有输入流的抽象基类,本身并不能创建实例来执行输入,但它们将成为所有输入流的模版,所以它们的方法是所有输入流都可使用的方法。
在 InputStream 里包含如下三个方法:
- int read ( ) : 从输入流中读取单个字节(相当于输入流模型的水管中取出一个水滴),返回所读取的字节数据(字节数据可直接转换为 int 类型)。
- int read ( byte [ ] b ) : 从输入流中最多读取 b.length 个字节的数据,并将其存储在字节数组 b 中,返回实际读取的字节数。
- int read ( byte [ ] , int off , int len ) : 从输入流中最多读取 len 个字节的数据,并将其存储在数据 b 中,放入数组 b 中时,并不是从数组起点开始,而是从 off 位置开始,返回实际读取的字节数。
在 Reader 里包含如下三个方法:
- int read ( ) : 从输入流中读取单个字符(相当于输出流模型的水管中取出一个水滴),返回所读取的字符数据(字符数据可直接转换为 int 类型)。
- int read (char [ ] cbuf) : 从输入流中最多读取 cbuf.length 个字符的数据,并将其存储在字符数组 cbuf 中,返回实际读取的字符数。
- int read ( char [ ] cbuf , int off , int len ) : 从输入流中最多读取 len 个字节的数据,并将其存储在数据 中,放入数组 cbuf 中时,并不是从数组起点开始,而是从 off 位置开始,返回实际读取的字符数。
对比 InputStream 和 Reader 所提供的方法,就不难发现这两个基类的功能基本是一样的。InputStream 和 Reader 都是将输入数据抽象成水管,所以程序即可以通过 read ( ) 方法每次读取一个“水滴”,也可以通过 int read ( byte [ ] b ) 或 int read ( char [ ] cbuf) 方法来读取多个 “水滴”。当使用数组作为 read ( ) 方法的参数时,可以理解为使用一个 “竹筒”到水管中取水,如下图:
read ( char [ ] cbuf ) 方法中数组可理解成一个“竹筒”,程序每次调用输入流的 int read ( byte [ ] b ) 或 int read ( char [ ] cbuf ) 方法就相当于用“竹筒”从输入流中取出一桶“水滴”,程序得到“竹筒”里的“水滴”后,转换成相应的数组即可,程序多次重复这个“取水”过程,直到最后。程序如何判断取水取到最后了呢?直到 int read ( byte [ ] b ) 或 int read (char [ ] cbuf) 方法返回 -1 ,即表明到了输入流的结束点。
正如前面提到的, InputStream 和 Reader 都是抽象类,本身不能创建实例,但它们分别有一个用于读取文件的输入流:FileInputStream 和 FileReader ,它们都是节点流,会直接和指定文件关联。通过下面的代码来分析这个输入流:
public class FileDemo {
public static void main(String[] args) throws IOException{
//创建字节输入流
FileInputStream fis=new FileInputStream("FileDemo.java");
//创建一个长度为1024的“竹筒”
byte [] bbuf=new byte[1024];
//用于保存实际读取的字节数
int hasRead =0;
//使用循环来重复“取水”过程
while((hasRead=fis.read(bbuf))>0)
{
//取出“竹筒”中的水滴(字节),将字节数组转换成字符串输入
System.out.print(new String(bbuf,0,hasRead));
}
//关闭文件输入流,放在 finally块里更安全
fis.close();
}
}
上面程序中使用 FileInputStream 循环“取水”过程,运行上面程序,将会输出上面程序的源代码。程序最后使用了 fis.close( ) 来关闭该文件输入流,与 JDBC 编程一样,程序里打开的文件 IO 资源不属于内存里的资源,垃圾回收机制无法回收该垃圾,所以应该显示关闭文件 IO 资源。Java 7 改写了所有的 IO 资源类,它们都实现了 AutoCloseable 接口,因此都可通过自动关闭资源的 try 语句来关闭这些 IO 流。下面通过代码来介绍 FileReader,如下:
public class FileDemo {
public static void main(String[] args) throws IOException{
try{
//创建字符输入流
FileReader fr=new FileReader("FileDemo.java");
//创建一个长度为32的“竹筒”
char[] cbuf=new char[32];
//用于保存实际读取的字符数
int hasRead =0;
//使用循环来重复“取水”过程
while((hasRead=fr.read(cbuf))>0)
{
//取出“竹筒”中的水滴(字节),将字符数组转换成字符串输入
System.out.print(new String(cbuf,0,hasRead));
}
}catch(IOException ex){
ex.printStackTrace();
}
}
}
上面两个程序并没有太大的不同,程序只是将字符数组的长度改为32,这意味着程序需要多次调用 read ( ) 方法才可以完全读取输入流的全部数据。程序最后使用了自动关闭资源的 try 语句来关闭文件输入流,这样可以保证输入流一定会被关闭。
除此之外, InputStream 和 Reader 还支持如下几个方法来移动记录指针:
- void mark ( int readAheadLimit ) : 在记录指针当前位置记录一个标记 ( mark )。
- boolean markSupported ( ) : 判断此输入流是否支持 mark ( ) 操作,即是否支持记录标记。
- void reset ( ) : 将此流的记录指针重新定位到上一次记录标记 ( mark ) 的位置。
- long skip ( long n ) : 记录指针向前移动 n 个字节/字符。
3 . 2 OutputStream 和 Writer
OutputSream 和 Writer 也是非常相似,它们提供了如下三个方法:
- void write ( int c ) : 将指定的字节/字符数组中的数据输出到指定输出流中。
- void write ( byte [] / char [] buf ) : 将字节数组/字符数据中的数据输出到指定输出流中。
- void write (byte [] / char [] buf , int off, int len ) : 将字节数组/字符数组中从 off 位置开始,长度为 len 的字节/字符输出到输出流中。
因为字符流直接以字符作为操作单位,所以 Writer 可以用字符串来代替字符数据,即以 String 对象作为参数,Writer 里还包含如下两个方法:
- void write ( String str ) : 将 str 字符串里包含的字符输出到指定输出流中。
- void write ( String str , int off , int len ) : 将 str 字符串里从 off 位置开始,长度为 len 的字符输出到指定输出流中。
下面使用 FileInputStream 来执行输入,并使用 FileOutputStream 来执行输出,用以实现复制文件的功能。
public class FileDemo {
public static void main(String[] args) throws IOException{
try{
//创建字节输入流
FileInputStream fis=new FileInputStream("FileDemo.java");
//创建字节输出流
FileOutputStream fos=new FileOutputStream("newFile.txt");
byte [] bbuf =new byte [32];
int hasRead=0;
//循环从输入流中取出数据
while((hasRead=fis.read(bbuf))>0)
{
fos.write(bbuf,0,hasRead);
}
}catch(IOException ioe)
{
ioe.printStackTrace();
}
}
}
运行上面程序,将看到系统当前路径下多了一个 newFile.txt 文件,该文件的内容和 File Demo.java 文件的内容完全相同。
4 . 输入、输出流体系
通过前面的介绍可以发现,使用节点流过程有些繁琐,而且效率也不高,如果想简化程序,这就需要借助于处理流了。处理流具有操作简单,执行效率高的优点。下面我们一起看看什么是处理流。
4 .1 处理流的用法
上面已经给出了处理流模型图,可以看出它可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入、输出方法,使得开发只需要关心高级流的操作。
使用处理流时的典型思路是:使用处理流来包装节点流,程序通过处理流来执行输入、输出功能,让节点流与底层的 IO设备、文件交互。
处理流有一定的特点,很容易识别,那就是只要流的构造器参数不是一个物理节点,而是已经存在的流,那么这种流就一定是处理流了,而所有节点流都是直接以物理 IO 节点作为构造器参数的。
下面使用 PrintStream 处理流来包装 OutputStream ,使用处理流后的输出流在输出时将更加方便,代码如下:
public class FileDemo {
public static void main(String[] args) {
try{
FileOutputStream fos=new FileOutputStream("test.txt");
PrintStream ps=new PrintStream(fos);
//使用 PrintStream 执行输出
ps.println("普通字符串");
//直接使用 PrintStream 输出对象
ps.println(new FileDemo());
}catch(IOException ioe)
{
ioe.printStackTrace();
}
}
}
上面程序先定义了一个节点输出流 FileOutputStream ,然后使用 PrintStream 包装了该节点输出流,最后使用 PrintStream 输出字符串、输出对象,PrintStream 的输出功能非常强大,通常如果需要输出文本内容,都应该将输出流包装成 PrintStream 后进行输出。
4 . 2 输入、输出体系
Java 的输入、输出流体系提供了近40个类,这些类看上去杂乱而没有规律,但如果将其按功能进行分类,则不难发现其是非常规律的,下表显示了 Java 输入、输出流体系中常用的流分类:
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
---|---|---|---|---|
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
访问管道 | PipedInputStream | PipedOutputSteam | PipedReader | PipedWriter |
访问字符串 | StringReader | StringWriter | ||
缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
转换流 | InputStreamReader | OutputStreamWriter | ||
对象流 | ObjectInputStream | ObjectOutputStream | ||
抽象基类 | FileInputStream | FileOutputStream | FileReader | FileWriter |
打印流 | PrintStream | PrintWriter | ||
推回输入流 | PushbackInputStream | PushbackReaer | ||
特殊流 | DataInputStream | DataOutputStream |
从上表可以看出,Java 的输入、输出流体系之所以复杂,主要是因为 Java 为了实现更好的设计,它把 IO 流按功能分成了许多类,而每类中又分别提供了字节流和字符流(当然有些流无法提供字节流,有些流无法提供字符流),字节流和字符流里又分别提供了输入流和输出流两大类,所以导致整个输入、输出体系格外复杂。
通常来说,字节流的功能比字符流的功能强大,因为计算机里所有的数据都是二进制的,而字节流可以处理所有的二进制文件,但问题是,如果使用字节流来处理文本文件,则需要使用合适的方式把这些字节转换成字符,这就增加了编程的复杂度。所以通常有一个规律:如果进行输入、输出的内容是文本内容,则应该考虑使用字符流;如果进行输入、输出的内容是二进制内容,则应该考虑使用字节流。
4 . 3 转换流
输入、输出流体系中还提供了两个转换流,这两个转换流用于实现将字节流转换成字符流,其中 InputStreamReader 将字节输入流转换成字符输入流,OutputStreamWriter 将字节输出流转换成字符输出流。
下面以获取键盘输入为例来介绍转换流的用法。Java 使用 System.in 代表标准输入,即键盘输入,但这个标准输入是 InputStream 类的实例,使用不太方便,而且键盘输入内容都是文本内容,所以可以使用 InputStreamReader 将其转换成字符输入流,普通的 Reader 读取输入内容时依旧不太方便,可以将普通的 Reader 再次包装成 BufferedReader ,利用 BufferedReader 的 readLine ( ) 方法可以一次读取一行内容,代码如下:
public class FileDemo {
public static void main(String[] args) {
try{
//将 System.in 对象转换成 Reader 对象
InputStreamReader reader=new InputStreamReader(System.in);
//将普通的 Reader 包装成 BufferedReader
BufferedReader br=new BufferedReader(reader);
String line=null;
//采用循环方式逐行地读取
while((line=br.readLine())!=null)
{
//如果读取到字符串为“exit”,程序退出
if(line.equals("exit"))
{
System.exit(1);
}
}
//打印读取的内容
System.out.println("输入内容为:"+line);
}catch(IOException ioe)
{
ioe.printStackTrace();
}
}
}
上面程序中将 System.in 包装成 BufferedReader ,BufferedReader 流具有缓冲功能,它可以一次读取一行文本,以换行符为标志,如果它没有读到换行符,则程序阻塞,等到读到换行符为止。在控制台执行输入时,只有按下回车键,程序才会打印出刚刚输入的内容。
4 . 4 推回输入流
在输入、输出流体系中,有两个特殊的流与众不同,就是 PushbackInputStream 和 PushbackOutputStream ,它们都提供了如下三个方法,其实这三个方法与 InputStream 和 Reader 中的三个 read ( ) 方法是一一对应的:
- void unread ( byte[] / char [] buf ) : 将一个字节/字符数组内容推回缓冲区里,从而允许重复读取刚刚读取的内容。
- void unread ( byte[] / char [] buf , int off , int len ) : 将一个字节/字符数组从 off 开始,长度为 len 字节/字符的内容推回缓冲区里,从而允许重复读取刚刚读取的内容。
- void unread ( int b ) :将一个字节/字符内容推回缓冲区里,从而允许重复读取刚刚读取的内容。
这两个推回输入流都带有一个推回缓冲区,当程序调用这两个推回输入流的 unread ( ) 方法时,系统将会把指定数组的内容推回到该缓冲区里,而推回输入流每次调用 read ( ) 方法时总是先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后,但还没有装满 read ( ) 所需的数组时才会从原输入流中读取。推回输入流的处理示意图如下:
上图可以知道,当程序创建了一个 PushbackInputStream 和 PushbackReader 时需要指定推回缓冲区的大小,默认的推回缓冲区的长度为1。如果程序中推回到推回缓冲区的内容超出了推回缓冲区的大小,将会引发 Pushback buffer overflow 的 IOException 异常。
下面程序试图找出程序中的“ new PushbackReader ”字符串,当找到该字符串后,程序只是打印出目标字符串之前的内容,代码如下:
public class FileDemo {
public static void main(String[] args) {
try{
//创建一个 PushbackReader对象,指定推回缓冲区的长度为64
PushbackReader pr=new PushbackReader(new FileReader("FileDemo.java"),64);
char [] buf=new char[32];
//用以保存上次读取的字符串内容
String lastContent = " ";
int hasRead=0;
//循环读取文件内容
while((hasRead=pr.read(buf))>0)
{
//将读取的内容转换成字符串
String content=new String(buf,0,hasRead);
int targetIndex=0;
//将上次读取的字符串和本次读取的字符串拼起来
//查看是否包含目标字符串,如果包含目标字符串
if((targetIndex=(lastContent+content).indexOf("new PushbackReader"))>0)
{
//将本次内容和上次内容一起推回缓冲区
pr.unread((lastContent+content).toCharArray());
//重新定义一个长度为 targetIndex 的 char 数组
if(targetIndex>32)
{
buf=new char[targetIndex];
}
//再次读取指定长度的内容(也就是目标字符串之前的内容)
pr.read(buf,0,targetIndex);
System.out.print(new String(buf,0,targetIndex));
System.exit(0);
}else
{
//打印善意读取的内容
System.out.print(lastContent);
//将本次内容设为上次读取的内容
lastContent=content;
}
}
}catch(IOException ioe)
{
ioe.printStackTrace();
}
}
}
上面程序实现了将指定内容推回到推回缓冲区,于是当程序再次调用 read ( ) 方法时,实际上只是读取了推回缓冲区的部分内容,从而实现了只打印目标字符串前面内容的功能。
4 . 5 重定向标准输入、输出
Java 的标准输入、输出分别通过 System.in 和 System.out 来代表,在默认情况下它们分别代表键盘和显示器,当程序通过System.in 来获取输入时,实际上是从键盘读取输入;当程序试图通过 System.out 执行输出时,程序总是输出到屏幕。
在 System 类里提供了如下三个重定向标准输入、输出的方法:
- static void setErr ( PrintStream err ) : 重定向“标准”错误输出流。
- static void setInt ( InputStream in ) : 重定向“标准”输入流。
- static void setOut ( PrintStream out ) : 重定向“标准”输出流。
下面通过重定向标准输出流,将 System.out 的输出重定向到文件输出,而不是在屏幕上输出,代码如下:
public class FileDemo {
public static void main(String[] args) {
try{
//一次性创建 PrintStream 输出流
PrintStream ps=new PrintStream(new FileOutputStream("printout.txt"));
//将标准输出重定向到 ps 输出流
System.setOut(ps);
//向标准输出输出一个字符串
System.out.println("普通字符串");
//向标准输出输出一个对象
System.out.println(new FileDemo());
}catch(IOException ex)
{
ex.printStackTrace();
}
}
}
上面程序创建了一个 PrintStream 输出流,并将系统的标准输出重定向到 PrintStream 输出流。运行上面程序时将看不到任何输出,这意味着标准输出不再输出到屏幕,而是输出到 printout.txt 文件,运行结束后,打开系统当前路径下的 printout.txt 文件,即可看到文件里的内容,正好与程序中的输出一致。
下面再通过一个例子,说明重定向标准输入,从而可以将 System.in 重定向到指定文件,而不是键盘输入,代码如下:
public class FileDemo {
public static void main(String[] args) {
try{
FileInputStream fis=new FileInputStream("FileDemo.java");
//将标准输入重定向到 fis 输入流
System.setIn(fis);
//使用 System.in 创建 Scanner 对象,用于获取标准输入
Scanner sc=new Scanner(System.in);
//增加下面一行只把回车作为分隔符
sc.useDelimiter("\n");
//判断是否还有下一个输入项
while(sc.hasNext())
{
//输出输入项
System.out.println("键盘输入的内容是:"+sc.next());
}
}catch(IOException ex)
{
ex.printStackTrace();
}
}
}
上面程序创建了一个 FileInputStream 输入流,并使用 System 的 setIn ( ) 方法将系统标准输入重定向到该文件输入流。运行上面程序,程序不会等待用户输入,而是直接输出了 FileDemo.java 文件的内容,这表明程序不再使用键盘作为标准输入,而是使用 FileDemo.java 文件作为标准输入源。
5 . Java虚拟机读写其他进程的数据
使用 Runtime 对象的 exec ( ) 方法可以运行平台上的其他程序,该方法产生一个 Process 对象,Process 对象代表由该 Java 程序启动的子进程。Process 类提供了如下三个方法,用于让程序和子进程进行通信。
- InputStream getErrorStream ( ) : 获取子进程的错误流。
- InputStream getInputStream ( ) : 获取子进程的输入流。
- OutputStream getOutputStream ( ) : 获取子进程的输出流。
下面程序示范了读取其他进程的输出信息,代码如下:
public class FileDemo {
public static void main(String[] args) throws IOException {
//运行 javac 命令,返回运行该命令的子进程
Process p=Runtime.getRuntime().exec("javac");
//以 p 进程的错误流创建 BufferedReader 对象
//这个错误流对本程序是输入流,对 p 进程则是输出流
BufferedReader br=new BufferedReader(new InputStreamReader(p.getErrorStream()));
String buff=null;
//采用循环方式来读取 p 进程的错误输出
while((buff=br.readLine())!=null)
{
System.out.println(buff);
}
}
}
上面程序中使用 Runtime 启动了 javac 程序,获得了运行该程序对应的字进程;以 p 进程的错误输入流创建了 BufferedReader ,这个输入流的流向如下图所示:
数据流对 p 进程 ( Javac 进程 ) 而言,它是输出流;但对本程序 ( FileDemo ) 而言,它是输入流,衡量输入、输出时总是站在运行本程序所在内存的角度,所以该数据流应该是输入流,运行结果如下:
不仅如此,也可以通过 Process 的 getOutputStream ( ) 方法获得向进程输入数据的流(该流对 Java 程序是输出流,对子进程则是输入流),下面程序实现了 Java 程序中启动 Java 虚拟机运行另一个 Java 程序,并向另一个 Java 程序中输入数据,代码如下:
public class FileDemo {
public static void main(String[] args) throws IOException {
//运行 Java ReadStandard 命令,返回运行该命令的子进程
Process p=Runtime.getRuntime().exec("java ReadStandard");
// 以 p 进程的输出流创建 PrintStream 对象
//这个输出流对本程序是输出流,对 p 进程则是输入流
PrintStream ps=new PrintStream(p.getOutputStream());
//向 ReadStandard 程序写入内容,这些内容将被 ReadStandard 读取
ps.println("普通字符串");
ps.println(new FileDemo());
}
}
//定义一个 ReadStandard 类,该类可以接受标准输入
//并将标准输入写到 printout.txt 文件
class ReadStandard
{
public static void main(String [] arge) throws IOException{
//使用 System.in 创建 Scanner 对象,用于获取标准输入
Scanner sc=new Scanner(System.in);
PrintStream ps=new PrintStream(new FileOutputStream("printout.txt"));
//增加下面一行只把回车作为分隔符
sc.useDelimiter("\n");
//判断是否还有下一个输入流
while(sc.hasNext())
{
//输出输入项
System.out.println("键盘输入的内容为:"+sc.next());
}
}
}
上面程序中的 ReadStandard 是一个使用 Scanner 获取标准输入的类,该类提供了 main ( ) 方法,可以被运行,但这里不打算直接运行该类,而是由 FileDemo 类来运行 ReadStandard 类。程序使用 Runtime 的 exec ( ) 方法运行了 java ReadStandard 命令,该命令将运行 ReadStandard 类,并返回运行该程序的子进程;获得进程 p 的输出流,该输出流对进程 p 是输入流,只是对本程序是输出流,程序通过该输出流向进程 p 也就是 ReadStandard 程序输出数据,这些数据将被 ReadStandard 类读取。
运行上面的程序,将看到产生了一个 printout.txt 文件,该文件由 ReadStandard 类产生,该文件的内容由FileDemo 类写入 ReadStandard 进程里,并由ReadStandard 读取这些数据并将这些数据保存到 printout.txt 文件中。
7 . RandomAccessFile
RandomAccessFile 是 Java 输入、输出流体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,它既可以读取文件内容,也可以向文件输出数据。与普通的输入、输出流不同的是 ,RandomAccessFile 支持“随机访问”的方式,也就是程序可以直接跳转到文件的任意地方来读写数据。正因为如此,当程序需要访问文件部分内容,而不是简单的把文件从读到尾,就可以使用 RandomAccessFile 了,同时,RandomAccessFile 还可以向已存在的文件后追加内容,因为 RandomAccessFile 允许自由定位文件记录指针,它可以不从开始的地方开始输出。
RandomAccessFile 尽管功能强大,但存在一个很大的不足,就是只能读写文件,不能读写其他 IO 节点。
RandomAccessFile 对象包含了一个记录指针,用以标识当前读写处的位置,当程序新创建一个 RandomAccessFile 对象时,该对象的文件记录指针位于文件头,也就是0处,当读/写了 n 个字节后,文件记录指针将会向后移动 n 个字节。除此之外,RandomAccessFile 可以自由移动该记录指针,既可以向前移动,也可以向后移动。其中,RandomAccessFile 包含了如下两个方法来操作文件记录指针:
- long getFilePointer ( ) : 返回文件记录指针的当前位置。
- void seek ( long pos ) : 将文件记录指针定位到 pos 位置。
RandomAccessFile 既可以读文件,也可以写,所以它既包含了完全类似于 InputStream 的三个read ( ) 方法,其用法和 InputStream 的三个read ( ) 方法完全一样,也包含了完全类似于 OutputStream 的三个 write ( ) 方法,其用法和 OutputStream 的三个 write ( ) 方法完全一样。除此之外,RandomAccessFile 还包含了一系列的 readXxx ( ) 和 writeXxx ( ) 方法来完成输入、输出。
RandomAccessFile 类由两个构造器,其实这两个构造器基本相同,只是指定文件的形式不同而已,即一个使用 String 参数来指定文件名,一个使用 File 参数来指定文件本身。除此之外,创建 RandomAccessFile 对象时还需要指定一个 mode 参数,该参数指定 RandomAccessFile 的访问模式,该参数有如下4个值。
- " r " : 以只读方式打开指定文件。如果试图对该 RandomAccessFile 执行写入方法,都将抛出 IOException 异常。
- " rw " : 以读、写方式打开指定文件。如果该文件尚不存在,则尝试创建该文件。
- " rws " : 以读、写方式打开指定文件。相对于" rw " 模式,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
- " rwd " : 以读、写方式打开指定文件。相对于" rw " 模式,还要求对文件的内容的每个更新都同步写入到底层存储设备。
下面程序使用 RandomAccessFile 来访问指定的中间部分数据,代码如下:
public class FileDemo {
public static void main(String[] args) throws IOException {
RandomAccessFile raf=new RandomAccessFile("FileDemo.java","r");
//获取 RandomAccessFile 对象文件指针的位置,初始位置是0
System.out.println("RandomAccessFile 的文件指针的初始位置是:"+raf.getFilePointer());
//移动 raf 的文件记录指针的位置
raf.seek(300);
byte[] bbuf=new byte[1024];
//用于保存实际读取的字节数
int hasRead=0;
//使用循环来重复“取水”过程
while((hasRead=raf.read(bbuf))>0){
//取出“竹筒”中的水滴(字节),将字节数组转换成字符串输入
System.out.println(new String(bbuf,0,hasRead));
}
}
}
上面程序创建了一个 RandomAccessFile 对象,该对象以只读方式打开了 FileDemo.java 文件,这意味着该 RandomAccessFile 对象只能读取文件内容,不能执行写入。程序将文件记录指针定位到300处,也就是说,程序将从300字节处开始读、写,程序接下来的部分于使用 InputStream 读取并没有太大的区别。
下面程序使用 RandomAccessFile 向指定文件后追加内容,代码如下:
public class FileDemo {
public static void main(String[] args) throws IOException {
//以读、写方式打开一个 RandomAccessFile 对象
RandomAccessFile raf=new RandomAccessFile("printout.txt","rw");
//将记录指针移动到 printout.txt 文件的最后
raf.seek(raf.length());
raf.write("追加的内容!\r\n".getBytes());
}
}
上面程序以读、写方式创建了一个 RandomAccessFile 对象,然后将 RandomAccessFile 对象的记录指针移动到最后,接下来使用 RandomAccessFile 执行输出,与使用 OutputStream 或 Writer 执行输出并没有太大区别。每执行一次程序,printout.txt文件中就多一行“追加的内容!”字符串,程序在该字符串后使用“\r\n”是为了控制换行。
RandomAccessFile 不能向文件的指定位置插入内容,如果直接将文件记录指针移动到中间某位置后开始输出,则新输出的内容会覆盖文件中原有的内容。如果需要向指定位置插入内容,程序需要先把插入点后面的内容读入缓冲区,等把需要插入的数据写入文件后,再将缓冲区的内容追加到文件后面。
下面程序实现向指定文件、指定位置插入内容的功能。代码如下:
public class FileDemo {
public static void insert(String fileName,long pos,String insertContent) throws IOException{
File tmp=File.createTempFile("tmp", null);
tmp.deleteOnExit();
RandomAccessFile raf=new RandomAccessFile(fileName,"rw");
//使用临时文件来保存插入点后的数据
FileOutputStream tmpOut=new FileOutputStream(tmp);
FileInputStream tmpIn=new FileInputStream(tmp);
raf.seek(pos);
//将插入点后的内容读入临时文件中保存
byte [] bbuf=new byte[64];
//用于保存实际读取的字节数
int hasRead=0;
//使用循环方式读取插入点后的数据
while((hasRead=raf.read(bbuf))>0)
{
//将读取的数据写入临时文件
tmpOut.write(bbuf,0,hasRead);
}
//插入内容
//把文件记录指针重新定位到 pos 位置
raf.seek(pos);
//追加需要插入的内容
raf.write(insertContent.getBytes());
//追加临时文件中的内容
while((hasRead=tmpIn.read(bbuf))>0)
{
raf.write(bbuf,0,hasRead);
}
}
public static void main(String[] args) throws IOException {
insert("FileDemo.java",45,"需要插入的内容\r\n");
}
}
上面程序使用 File 的 creatTempFile( String prefix , String suffix ) 方法创建了一个临时文件(该临时文件将在 JVM 退出时被删除),用以保存被插入文件的插入点后面的内容。程序先将文件中插入点后的内容读入临时文件中,然后重新定位到插入点,将需要插入的内容添加到文件后面,最后将临时文件的内容添加到文件后面,通过这个过程就可以向指定文件、指定位置插入内容。每次运行上面程序,FileDemo.java中都会插入一行字符串。