已经介绍了集合类(Java语言程序设计— Java中集合类的使用),程序的主要任务是操作数据。在程序运行时,这些数据都必须位于内存中,并且属于特定的类型,程序才能操作它们。Java I/O系统负责处理程序的输入和输出,I/O类库位于java.io包中,它对各种常见的输入流和输出流进行了抽象。本文将对I/O流进行详细讲解。
Ø 流概述
流就是字节序列的抽象概念,能被连续读取数据的数据源和能被连续写入数据的接收端就是流,流机制是Java及C++中的一个重要机制,通过流开发人员可以自由控制文件、内存、I/O设备等数据的流向。而I/O流就是用于处理设备上的数据,如硬盘、内存、键盘录入等,就好像管道,将两个容器连接起来,如图所示。
I/O流有很多种,按操作数据单位不同可分为字节流(8 b)和字符流(16 b),按数据流的流向不同分为输入流和输出流,如表所示。
从表中可看出I/O流的大致分类,Java的I/O流共涉及40多个类,实际上这些类非常规则,都是从这4个抽象基类派生的,由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。接下来会详细讲解这些流的使用。
字节流
Ø 字节流的概念
在计算机中,所有的文件都能以二进制(字节)形式存在,Java的I/O中针对字节传输操作提供了一系列流,统称为字节流。字节流有两个抽象基类InputStream和OutputStream,分别处理字节流的输入和输出,所有的字节输入流都继承自InputStream类,所有的字节输出流都继承自OutputStream类。在这里,输入和输出的概念要有一个参照物,是站在程序的角度来理解这两个概念,如图所示。
在图中,从程序到文件是输出流(OutputStream),将数据从程序输出到文件。从文件到程序是输入流(InputStream),通过程序读取文件中的数据。这样就实现了数据的传输。
在Java中,提供了一系列用于操作文件读写的有关方法,接下来先了解一下InputStream类的方法,如表所示。
表中列出了InputStream类的方法,其中最常用的是三个重载的read()方法和close()方法,read()方法是从流中逐个读入字节,int read(byte[] b)方法和int read(byte[] b, int off, int len)方法是将若干字节以字节数组形式一次性读入,提高读数据的效率。操作I/O流时会占用宝贵的系统资源,当操作完成后,应该将I/O所占用的系统资源释放,这时就需要调用close()方法关闭流。
介绍完InputStream类的相关方法,接下来要介绍一下它所对应的OutputStream类的相关方法,如表所示。
表中,三个重载的write()方法都是向输出流写入字节,其中,void write(int b)方法是逐个写入字节;void write(byte[] b) 方法和void write(byte[] b, int off, int len) 方法是将若干个字节以字节数组的形式一次性写入,提高写数据的效率;flush()方法用于将当前流的缓冲区中数据强制写入目标文件;close()方法用来关闭此输出流并释放系统资源。
InputStream和OutputStream都是抽象类,不能实例化,所以要实现功能,需要用到它们的子类,接下来先了解一下这些子类。
如图所示。
从图中可看出,InputStream和OutputStream的子类虽然多,但都有规律可循,比如InputStream的子类都以InputStream为后缀,OutputStream的子类都以OutputStream为后缀。另外,InputStream和OutputStream的子类也相互对应,例如FileInputStream和FileOutputStream。接下来会详细讲解这些类的使用。
Ø 字节流读写文件
介绍了InputStream和OutputStream的众多子类,其中,FileInputStream和FileOutputStream是两个很常用的子类,FileInputStream用来从文件中读取数据,操作文件的字节输入流,接下来通过一个案例来演示如何从文件中读取数据。首先在D盘根目录下新建一个文本文件read.txt,文件内容如下。
创建文件完成后,开始编写代码,如例所示。
程序的运行结果如图所示。
在例中,第8行设定了读取的字节数为512,程序在读取时,一次性读取512个字符,所以图运行结果中1000phone后还有很多空格。
在例中,如果程序中途出现错误,程序将直接中断,所以一定要将关闭资源的close()方法写到finally中,因为finally中不能直接访问try中的内容,所以要将FileInputStream定义在try的外面。由于篇幅有限,后面的代码不再重复异常处理的标准写法,直接将异常抛出。
另外,当前目录下读取的文件一定要存在,否则会报FileNotFoundException异常,如图所示。
与FileInputStream对应的是FileOutputStream,它是用来将数据写入文件,操作文件字节输出流的,接下来通过一个案例来演示如何将数据写入文件,如例所示。
程序的运行结果如图所示。
在图中,运行结果显示将“.com”成功存入到了read.txt文件,此时文件内容如下。
如果文件不存在,文件输出流会先创建文件,再将内容输出到文件中,例中,read.txt已经存在,从运行后文件内容可看出,程序是先将之前的内容“1000phon”清除掉,然后写入了“.com”,如果想不清除文件内容,可以使用FileOutputStream类的构造方法FileOutputStream(String FileName,boolean append)来创建文件输出流对象,指定参数append为true。将read.txt文件内容重新修改为“1000phone”,修改例代码。
如例所示。
程序的运行结果如图所示。
在图中,运行结果显示成功将“.com”存入了read.txt文件,此时文件内容如下。
通过FileOutputStream类的构造方法指定参数append为true,内容成功写入文件,并且没有清除之前的内容,将内容写入到文件末尾。
Ø 文件的复制
详细讲解了文件输入流和文件输出流,实际开发中,往往都是二者结合使用,比如文件的复制。接下来通过一个案例来演示如何通过输入输出流实现文件的复制,首先在当前目录新建文件夹src和tar,将一张图片test.jpg存入src中,然后开始编写代码,如例所示。
程序的运行结果如图所示。
在图中,运行结果显示了文件复制的消耗时间,文件成功从src文件夹复制到了tar文件夹,如图所示。
另外,从图中可看出,复制文件消耗了71ms,由于计算机的性能差异等原因,复制文件的耗时可能每次都不相同。
在例中,如果在D盘中指定src和tar的目录用“/”,这是因为Windows目录用反斜杠“”表示,但Java中反斜杠是特殊字符,所以写成“/”指定路径,也可以使用“/”指定目录,例如“src/test.jpg”。
字节流
Ø 字节流的缓冲区
在9.2.3节中讲解了如何复制文件,但复制的方式是一个字节一个字节地复制,频繁操作文件,效率非常低,利用字节流的缓冲区可以解决这一问题,提高效率。缓冲区可以存放一些数据,例如,某出版社要从北京往西安运送教材,如果有一千本教材,每次只运送一本教材,就需要运输一千次,为了减少运输次数,可以先把一批教材装在车厢中,这样就可以成批地运送教材,这时的车厢就相当于一个临时缓冲区。当通过流的方式复制文件时,为了提高效率也可以定义一个字节数组作为缓冲区,将多个字节读到缓冲区,然后一次性输出到文件,这样会大大提高效率。接下来通过一个案例来演示如何在复制文件时应用缓冲区提高效率。如例所示。
程序的运行结果如图所示。
图中,运行结果可看出,与图相比,复制同样的文件,耗时大大降低了,说明应用缓冲区后,程序运行效率大大提高了,这是因为应用缓冲区后,操作文件的次数减少了,从而提高了读写效率。
Ø 装饰设计模式
装饰模式是在不必改变原类文件和使用继承的情况下,动态地扩展一个对象的功能。它是通过创建一个包装对象,也就是装饰来包裹真实的对象。例如,在北京买了一套房,冬天天气很冷,想在房子的客厅安装一台空调,这就相当于为这套新房填加了新的功能。
装饰对象和被装饰对象要实现同一个接口,装饰对象持有被装饰对象的实例,如图所示。
Ø 字节流缓冲流
讲解了装饰设计模式,实际上,在I/O中一些流也用到了这种模式,分别是BufferedInputStream类和BufferedOutputStream类,这两个流都使用了装饰设计模式。它们构造方法中分别接收InputStream和OutputStream类型的参数作为被装饰对象,在执行读写操作时提供缓冲功能,如图所示。
程序的运行结果如图所示。
在例中,被装饰类Source本身只有一个功能,在装饰前打印了功能1,经过装饰类Decorator装饰后,打印了功能1、功能2和功能3。Sourceable公共接口保证了装饰类实现了被装饰类实现的方法。创建被装饰类对象后,创建装饰类时,通过装饰类的构造方法,将被装饰类以参数形式传入,执行装饰类的方法,这样就达到了动态增加功能的效果。
例这样做的好处就是动态增加了对象的功能,而且还能动态撤销功能,继承是不能做到这一点的,继承的功能是静态的,不能动态增删。但这种方式也有不足,这样做会产生过多相似的对象,不易排错。
Ø 字符流定义及基本用法
前面讲解了字节流的相关内容,Java还提供了字符流,用于操作字符。与字节流相似,字符流也有两个抽象基类,分别是Reader和Writer,Reader是字符输入流,用于从目标文件读取字符,Writer是字符输出流,用于向目标文件写入字符。字符流也是由两个抽象基类衍生出很多子类,由子类来实现功能,先来了解一下它们的结构,如图所示。
在图中,可以看到程序和文件两个节点相互传输数据是节点流,比如前面提到的FileInputStream类和FileOutputStream类都是节点流。在节点流之外,封装着一层缓冲流,它是对一个已存在的流的连接和封装,比如BufferedInputStream类和BufferedOutputStream类,接下来通过一个案例来演示这两个流的使用,如例所示。
程序的运行结果如图所示。
在图中,运行结果打印了复制“test.jpg”文件消耗的毫秒数,与例相比,例只是应用了缓冲流,就将复制效率明显提升,因为这两个缓冲流内部定义了一个大小为8192的字节数组,当调用read()方法或write()方法操作数据时,首先将读写的数据存入定义好的字节数组,然后将数组中的数据一次性操作完成,和前面讲解的字节流缓冲区类似,都是对数据进行了缓冲,减少操作次数,从而提高程序运行效率。
字符流
Ø 字符流定义及基本用法
前面讲解了字节流的相关内容,Java还提供了字符流,用于操作字符。与字节流相似,字符流也有两个抽象基类,分别是Reader和Writer,Reader是字符输入流,用于从目标文件读取字符,Writer是字符输出流,用于向目标文件写入字符。字符流也是由两个抽象基类衍生出很多子类,由子类来实现功能,先来了解一下它们的结构,如图所示。
从中,可以看出,字符流与字节流相似,也是很有规律的,这些子类都是以它们的抽象基类为结尾命名的,并且Reader和Writer很多子类相对应,例如CharArrayReader和CharArrayWriter。接下来会详细讲解字符流的使用。
Ø 字符流操作文件
介绍了Reader和Writer的众多子类,其中,FileReader和FileWriter是两个很常用的子类,FileReader类是用来从文件中读取字符的,操作文件的字符输入流,接下来通过一个案例来演示如何从文件中读取字符。首先在当D盘根目录下新建一个文本文件read.txt,文件内容如下。
创建文件完成后,开始编写代码,如例所示。
程序的运行结果如图所示。
在例中,首先声明一个文件字符输入流,然后在创建输入流实例时,将文件以参数传入,读取到文件后,用变量len记录读取的字符,然后循环输出。这里要注意len是int类型,所以输出时要强转类型,第10行中将len强转为char类型。
与FileReader类对应的是FileWriter类,它是用来将字符写入文件,操作文件字符输出流的,接下来通过一个案例来演示如何将字符写入文件,如例所示。
程序的运行结果如图所示。
在图中,运行结果显示将“.com”成功存入到了read.txt文件,文件内容如下。
FileWriter与FileOutputStream类似,如果指定的目标文件不存在,则先新建文件,再写入内容,如果文件存在,会先清空文件内容,然后写入新内容。如果想在文件内容的末尾追加内容,则需要调用构造方法FileWriter (String FileName,boolean append)来创建文件输出流对象,将参数append指定为true即可,将例第5行代码修改如下。
再次运行程序,输出流会将字符追加到文件内容的末尾,不会清除文件本身的内容。
Ø 字符流的缓冲区
前面讲解了字节流的缓冲区,字符流也同样有缓冲区。字符流中带缓冲区的流分别是BufferedReader类和BufferedWriter类,其中,BufferedReader类用于对字符输入流进行包装,BufferedWriter类用于对字符输出流进行包装,包装后会提高字符流的读写效率。接下来通过一个案例演示如何在复制文件时应用字符流缓冲区,先在项目的根目录下创建一个src.txt文件,文件内容如下。
创建好文件后,开始编写代码,如例所示。
在例程序运行结束后,会在src根目录下生成一个tar.txt文件,内容与之前创建的src.txt文件内容相同,如图所示。
在图中,展示了文件复制前后的文件内容,可以看到,文件字符缓冲流成功复制了文件。在例中,第10行每次循环都用readLine()方法读取一行字符,然后通过write()方法写入目标文件。
例中,循环中调用了BufferedWriter的write()方法写字符时,这些字符首先会被写入缓冲区,当缓冲区写满时或调用close()方法时,缓冲区中字符才会被写入目标文件,因此在循环结束后一定要调用close()方法,否则可能会出现部分数据未写入目标文件。
Ø LineNumberReader
Java程序在编译或运行期间经常会出现一些错误,在错误中通常会报告出错的行号,为了方便查找错误,需要在代码中加入行号。JDK提供了一个可以跟踪行号的流——LineNumberReader,它是BufferedReader的子类。接下来通过一个案例来演示复制文件时,如何为文件内容加上行号,首先在当前目录新建一个文件code1.txt,文件内容如下。
创建好文件后,开始编写代码,如例所示。
例程序运行结束后,会在当前目录生成一个code2.txt文件,与code1.txt相比,文件内容增加了行号,如图所示。
在例的复制过程中,使用LineNumberReader类来跟踪行号,调用setLineNumber()方法设置行号起始值为0,从图中可看到,第一行的行号是1,这是因为LineNumberReader类在读取到换行符“n”、回车符“r”或者回车后紧跟换行符时,行号会自动加1。这就是LineNumberReader类的基本使用。
Ø 转换流
前面分别讲解了字节流和字符流,有时字节流和字符流之间也需要进行转换,在JDK中提供了可以将字节流转换为字符流的两个类,分别是InputStreamReader类和OutputStreamWriter类,它们被称为转换流,其中,OutputStreamWriter类可以将一个字符输出流转换成字节输出流,而InputStreamReader类可以将一个字节输入流转换成字符输入流,转换流的出现方便了对文件的读写,它在字符流与字节流之间架起了一座桥梁,使原本没有关联的两种流操作能够进行转化,提高了程序的灵活性。
通过转换流进行读写数据的过程如图所示。
在图中,程序向文件写入数据时,将输出的字符流变为字节流,程序从文件读取数据时,将输入的字节流变为字符流,提高了读写效率。接下来通过一个案例来演示转换流的使用,首先在当前项目的根目录下新建一个文本文件source.txt,文件内容如下。
创建文件完成后,开始编写代码,如例所示。
例程序运行结束后,会在src根目录下生成一个target.txt文件,如图所示。
在图中,显示文件成功复制。在例中实现了字节流与字符流之间的转换,将字节流转换为字符流,从而实现直接对字符的读写。这里要注意,如果用字符流取操作非文本文件,例如操作视频文件,可能会造成部分数据丢失。
其他I/O流
Ø ObjectInputStream和ObjectOutputStream
前面讲解了如何通过流读取文件,实际上通过流也可以读取对象,例如,将内存中的对象转换为二进制数据流的形式输出,保存到硬盘,这叫作对象的序列化。通过将对象序列化,可以方便地实现对象的传输和保存。
在Java中,并不是所有的类的对象都可以被序列化,如果一个类对象需要被序列化,则此类必须实现java.io.Serializable接口,这个接口内没有定义任何方法,是一个标识接口,表示一种能力。
Java提供了两个类用于序列化对象的操作,它们分别是ObjectInputStream类和ObjectOutputStream类。对象序列化和反序列化通过以下两步实现。
(1)创建OutputStream对象,封装在ObjectOutputStream对象中,只需调用writeObject即可将对象序列化。
(2)创建InputStream对象,封装在ObjectInputStream对象中,只需调用readObject即可将对象反序列化。
接下来通过一个案例演示对象如何通过ObjectOutputStream进行序列化,如例所示。
例程序运行结束后,会在当前目录生成一个student.txt文件,该文件中以二进制形式存储了Student对象的数据。
ObjectInputStream和ObjectOutputStream
与序列化相对应的是反序列化,Java提供的ObjectInputStream类可以进行对象的反序列化,根据序列化保存的二进制数据文件,恢复到序列化之前的Java对象。接下来通过一个案例演示对象的反序列化,如例所示。
程序的运行结果如图所示。
图中运行结果打印了Student对象的属性,打印出的结果与例中序列化存储到student.txt的数据一致,说明例中成功将student.txt中的二进制数据反序列化了。
被存储和被读取的对象都必须实现java.io.Serializable接口,否则会报NotSerializableException异常。
Ø DataInputStream和DataOutputStream
讲解了将对象序列化和反序列化,Java中还提供了将对象中的一部分数据进行序列化和反序列化的类,也就是将基本数据类型序列化和反序列化,它们分别是DataInputStream类和DataOutputStream类。
DataInputStream类和DataOutputStream类是两个与平台无关的数据操作流,它们不仅提供了读写各种基本数据类型数据的方法,而且还提供了readUTF()方法和writeUTF()方法,用于输入输出时指定字符串的编码类型为UTF-8,接下来通过一个案例演示这两个类如何读写数据。如例所示。
程序的运行结果如图所示。
在例中,将数据用DataOutputStream流存入data.txt文件,该文件生成在当前目录,这里要注意,读取数据的顺序要与存储数据的顺序保持一致,才能保证数据的正确。
Ø PrintStream
前面讲解了使用输出流输出字节数组,如果想直接输出数组、日期、字符等呢?Java中提供了PrintStream流来解决这一问题,它应用了装饰设计模式,使输出流的功能更完善,它提供了一系列用于打印数据的print()和println()方法,被称作打印流。接下来通过一个案例演示PrintStream流的用法,如例所示。
例程序运行结束后,会在当前目录生成一个print.txt文件,文件内容如下。
从文件内容可看出,例输出的内容都成功储存到print.txt文件,print()方法和println()方法区别在于print()方法输出数据后,不输出换行符。
Ø 标准输入输出流
Java中有3个特殊的流对象常量,如表所示。
表中列举了3个特殊的常量,它们被习惯性地称为标准输入输出流。其中,err是将数据输出到控制台,通常是程序运行的错误信息,是不希望用户看到的;out是标准输出流,默认将数据输出到命令行窗口,是希望用户看到的;in是标准输入流,默认读取键盘输入的数据。接下来通过一个案例演示这3个常量的使用。如例所示。
程序的运行结果如图所示。
在图中,程序运行时先输入一个字母,然后运行结果打印出了两条错误,一条是程序错误的堆栈信息,一条是自定义的错误信息。例中,先创建了标准输入流,读取从键盘输入的字母,用一个String类型的变量接收,然后试图将这个变量解析成Integer类型,程序出错,运行到catch代码块,用两种方式打印了错误信息。
有时程序会向命令行窗口输出大量的数据,例如程序运行中的日志,大量日志的输出会使命令行窗口快速滚动,浏览起来很不方便。
标准输入输出流
在System类中提供了一些静态方法来解决这一问题,将标准输入输出流重定向到其他设备,例如,将数据输出到硬盘的文件中,这些静态方法如表所示。
表中列举了重定向流的常用静态方法,接下来通过一个案例来演示这些静态方法的使用,首先在当前目录创建一个src.txt文件,文件内容如下。
创建好文件后,开始编写代码,如例所示。
例程序运行结束后,会在当前目录生成一个tar.txt文件,tar.txt和src.txt文件内容一致,如图所示。
在例中,使用setIn(InputStream in)方法将标准输入流重定向到File InputStream流,关联当前目录下的src.txt文件,使用setOut(PrintStream)方法将标准输出流重定向到一个PrintStream流,关联当前目录下的tar.txt文件,若文件不存在则创建文件,若文件存在,则清空里面内容,再写入数据,最后使用BufferedReader包装流进行包装,程序每次从src.txt文件读取一行,写入tar.txt文件。
Ø PipedArrayInputStream和ByteArrayOutputStream
在UNIX/LINUX中有一个很有用的概念——管道(pipe),它具有将一个程序的输出当作另一个程序的输入的能力。在Java中也提供了类似这个概念的管道流,可以使用管道流进行线程之间的通信,在这个机制中,输入流和输出流必须相连接,这样的通信有别于一般的共享数据,它不需要一个共享的数据空间。
管道流主要用于连接两个线程间的通信。管道流也分为字节流(PipedInputStream、PipedOutputStream)和字符流(PipedReader、PipedWriter),本节只讲解PipedInputStream类和PipedOutputStream类。接下来通过一个案例演示管道流的使用。
程序的运行结果如图所示。
在例中,Send类用于发送数据,Receive类用于接收其他线程发送的数据,main()方法创建Send类和Receive类实例后,分别调用send对象的getOutputStream()方法和recive对象的getInputStream()方法,返回各自的管道输出流和管道输入流对象,然后通过调用管道输出流对象的connect()方法,将两个管道连接在一起,最后通过调用start()方法分别开启两个线程。
ByteArrayInputStream和ByteArrayOutputStream
前面学习的输入和输出流都是程序与文件之间的操作,有时程序在运行过程中要生成一些临时文件,可以采用虚拟文件的方式实现。Java提供了内存流机制,可以实现将数据储存到内存中,称为内存操作流,它们分别是字节内存操作流(ByteArrayInputStream、ByteArrayOutputStream)和字符内存操作流(CharArrayWriter、CharArrayReader),本节只讲解字节内存操作流。接下来通过一个案例演示字节内存操作流的使用。
程序的运行结果如图所示。
例中,先将3个int类型变量用ByteArrayOutputStream流存入到内存中,然后将这些数据转为byte[]数组的形式,遍历打印,最后将这些数据用ByteArrayInputStream流从内存中读取出来并遍历打印。
Ø CharArrayReader和CharArrayWriter
讲解了字节内存操作流,与之对应的还有字符内存操作流,分别是CharArrayReader类和CharArrayWriter类。CharArrayWriter类可以将字符类型数据临时存入内存缓冲区中,CharArrayReader类可以从内存缓冲区中读取字符类型数据,接下来通过一个案例演示这两个类的使用,如例所示。
程序的运行结果如图所示。
在例中,先将3个字符串用CharArrayWriter存入到内存中,打印存入的数据,然后将这些数据转为char[]数组的形式,将内存中的char[]数组用CharArrayReader读取出来并遍历打印。
Ø SequenceInputStream
前面讲解的对文件进行操作都是通过一个流,Java提供了SequenceInputStream类可以将多个输入流按顺序连接起来,合并为一个输入流。当通过这个类来读取数据时,它会依次从所有被串联的输入流中读取数据,对程序来说就好像对同一个流操作。接下来通过一个案例演示SequenceInputStream类的使用,首先在当前目录创建file1.txt文件和file2.txt文件,其中file1.txt文件内容如下。
file2.txt文件内容如下。
创建文件完成后,开始编写代码,如例所示 。
例程序运行结束后,会在当前目录生成一个fileMerge.txt文件,3个文件的内容如图所示。
在例中,先创建两个文件输入流读取当前目录下的两个文件,然后创建SequenceInputStream对象用于合并两个文件输入流,接下来创建文件输出流生成fileMerge.txt文件
在例中,SequenceInputStream对象将两个流合并,SequenceInputStream对象还提供了合并多个流的构造方法,具体如下。
这个构造方法接收一个Enumeration对象作为参数,Enumeration对象会返回一系列InputStream类型的对象,提供给SequenceInputStream类读取。接下来通过一个案例演示SequenceInputStream类接收多个流的用法。
例程序运行结束后,会在当前目录生成一个fileMerge.txt文件,4个文件的内容如图所示。
在例中,先创建3个文件输入流读取当前目录下的3个文件,然后创建Vector对象用于存放3个流,接下来调用Vector的elements()方法返回Enumeration对象,创建SequenceInputStream对象并将Enumeration对象以参数形式传入,最后创建一个文件输出流生成fileMerge.txt文件,成功将3个文件的内容合并。
File类
http://Java.io中定义的大多数类是对文件内容进行流式操作的,但File类例外,它是唯一一个与文件本身有关的操作类。它定义了一些与平台无关的方法来操作文件,通过调用File类提供的各种方法,能够完成创建、删除文件,重命名文件,判断文件的读写权限及文件是否存在,设置和查询文件创建时间、权限等操作。File类除了对文件操作外,还可以将目录当作文件进行处理。
Ø File类的常用方法
使用File类进行操作,首先要设置一个操作文件的路径,File类有3个构造方法可以用来生成File对象并且设置操作文件的路径,如下所示。
如上所示构造方法中,“directoryPath”表示文件的路径名,“filename”是文件名,“dirObj”是一个指定目录的File对象。通过这3个构造方法可以创建File对象,如下所示。
如上所示创建3个File对象f1、f2和f3,在指定路径时,使用了“/”,Java能正确处理UNIX和Windows约定路径分隔符,所以在Windows下用“/”是可以正确指定路径的,如果在Windows下使用反斜杠“”作为路径分隔符,则需要转义,写两个反斜杠“/”。
在File中提供了一系列用于操作文件的有关方法,接下来先了解一下File类的常用方法,表参考。
在图中,运行结果打印了file.txt相关的信息。表中列举出了这些API的具体作用,这里就不再赘述。
Ø 遍历目录下的文件
在文件操作中,遍历某个目录下的文件是很常见的操作,File类中提供的list()方法就是用来遍历目录下所有文件的。接下来通过一个案例演示list()方法的使用,如例所示。
程序的运行结果如图所示。
在例中,先创建File对象,指定File对象的目录,然后判断file目录是否存在,若存在,则调用list()方法,以String数组的形式得到所有文件名,最后循环遍历数组内容并打印。
例遍历指定目录下的所有文件,如果目录下有还有子目录就不能遍历到,这时就需要用到File类的listFiles()方法,接下来通过一个案例演示如何遍历目录及目录下所有子目录的文件,如例所示。
程序的运行结果如图所示。
在例中,先创建File对象,遍历目录下所有文件后,循环判断遍历到的是否是目录,如果是目录,则再次调用方法本身,直到遍历到文件,这种方式也叫做递归调用。
Ø 文件过滤
讲解了如何遍历目录下的文件,调用File类的list()方法成功遍历了目录下的文件,但有时候可能只需要遍历某些文件,比如遍历目录下扩展名为“.java”的文件,这就需要用到File类的list(FilenameFilter filter)方法。接下来通过一个案例演示如何遍历目录下扩展名为“.java”的文件,如例所示。
程序的运行结果如图所示。
在例中,先创建了一个匿名内部类,实现accept方法,在list()方法中已经实现了基本的功能,在运行时采用FilenameFilter对象提供的策略来执行程序,这种方式也叫做策略设计模式,可以在FilenameFilter实现类中指定具体的执行策略。
Ø 删除文件及目录
前面讲解了文件的遍历和过滤,文件的删除操作也是很常见的,接下来通过一个案例演示如何删除文件及目录,如例所示。
程序的运行结果如图所示。
在例中,首先创建File对象,指定文件目录,在deleteFiles(File file)方法中将File对象以参数传入,然后遍历目录下所有文件,循环判断遍历到的是否是目录,如果是目录,继续递归调用方法本身,如果是文件则直接删除,删除文件完成后,将目录删除。
File类的delete()方法只是删除一个指定的文件,如果File对象的目录下还有子目录,则无法直接删除,要递归删除。另外,Java中是直接从虚拟机中将文件或目录删除,不经过回收站,文件无法恢复,所以删除要谨慎。
Ø RandomAccessFile
除了File类之外,Java还提供了RandomAccessFile类用于专门处理文件,它支持 “随机访问”的方式,这里“随机”是指可以跳转到文件的任意位置处读写数据。使用RandomAccessFile类,程序可以直接跳到文件的任意地方读、写文件,既支持只访问文件的部分内容,又支持向已存在的文件追加内容。
RandomAccessFile类在数据等长记录格式文件的随机(相对顺序而言)读取时有很大的优势,但该类仅限于操作文件,不能访问其他的I/O设备,如网络、内存影响等,接下来了解一下RandomAccessFile类的构造方法,具体示例如下。
如上所示,RandomAccessFile类的构造方法需要指定一个mode参数,该参数用于指定RandomAccessFile对象的访问模式,mode的具体值及对应含义如表所示。
表中列举了mode的具体值及含义,其中“r” 如果向文件写入内容,会报I/OException异常;“rw”支持文件读写,若文件不存在,则创建;“rws”与“rw”不同的是,还要对文件内容的每次更新都同步更新到潜在的存储设备中,这里的“s”表示同步(synchronous)的意思;
“rwd”与“rw”不同的是,还要对文件内容的每次更新都同步到潜在的设备中去,与“rws”不同的是,“rwd”仅将文件内容更新到存储设备中,不需要更新文件的元数据。
RandomAccessFile对象包含一个记录指针,用以标识当前读写的位置,它可以自由移动记录指针,RandomAccessFile对象操作指针的方法如表所示。
表中列举了RandomAccessFile对象操作指针的方法,接下来通过一个案例来演示这些方法的使用,如例所示。
程序的运行结果如图所示。
在例中,先按“rw”打开文件,若文件不存在,则创建文件,写入了10个long型数据,每个long型数据占8个字节,然后用seek(2*8)方法使读写指针从文件开头开始,跳过第2个数据,接下来通过writeLong(666)方法将原来的第6个数据覆蓋为666,调用seek(0)将读写指针定位到文件开头,读取文件中所有long型数据。
字符编码
Ø 常用字符集
大家在看谍战片时,经常会看到情报员将其得到的军事计划和命令等情报用密码本将文字翻译成秘密代码发出,敌人就算接收到该代码也要花很长时间进行破译,而队友就可以使用同样的密码本将收到的代码翻译成文字,计算机之间进行传输同样需要使用一种“密码本”,它叫做字符码表。计算机只能识别二进制数据,为了让它识别各个国家的文字,就将各个国家的文字用数字来表示,并一一对应,形成一张表,这就是编码表,编码表是一种可以让计算机识别的特定字符集,针对不同文字,每个国家都指定了自己的码表,接下来介绍几种常见的编码表。
1.ASCII
最早的也是最基本最重要的一种英美文字的字符集,也可以说是编码。ASCII被定为国际标准之后的代号为ISO-646。由于ASCII码只使用了低7位二进制位,其他的认为无效,它仅使用了0~127这128个码位。剩下的128个码位便可以用来做扩展,并且ASCII的字符集序号与存储的编码完全相同。
2.ISO-8859-*
随着西欧国家的崛起,在ASCII的基础上对剩余的码位做了扩展,就形成了一系列ISO-8859-*的标准。例如,为英语做了专门扩展的字符集编码标准编号ISO-8859-1,也叫做Latin-1。
由于西欧小国众多,稍有发言权的小国就纷纷在ASCII的基础上扩展形成自己的编码,这就是ISO-8859-*系列。很显然ISO-8859-*系列的码也是8位的,并且其字符集序号与存储的编码也完全相同。
3.GB2312
GB2312字集是简体字集,全称为GB2312(80)字集,共包括国标简体汉字6763个。
4.Unicode
国际标准组织于1984年4月成立ISO/IEC JTC1/SC2/WG2工作组,针对各国文字、符号进行统一性编码。1991年美国跨国公司成立Unicode Consortium,并于1991年10月与WG2达成协议,采用同一编码字集。
目前Unicode是采用16位编码体系,其字符集内容与 ISO10646的BMP(Basic Multilingual Plane)相同。Unicode于1992年6月通过DIS(Draf International Standard),目前版本V2.0于1996公布,内容包含符号6811个,汉字20902个,韩文拼音11172个,造字区6400个,保留 20249个,共计65534个。Unicode编码后的大小是一样的。例如,一个英文字母“a”和一个汉字“好”,编码后都是占用的空间大小是一样的,都是两个字节。
5.GBK
GBK字集包括了GB字集、BIG5字集和一些符号,共包括21003个字符。GBK编码是GB2312编码的超集,向下完全兼容 GB2312,同时GBK收录了Unicode基本多文种平面中的所有CJK汉字。
同 GB2312一样,GBK也支持希腊字母、日文假名字母、俄语字母等字符,但不支持韩语中的表音字符(非汉字字符)。GBK还收录了GB2312不包含的汉字部首符号、竖排标点符号等字符。
6. UTF-8
UTF- 8是用以解决国际上字符的一种多字节编码,它对英文使用8位(即一个字节),中文使用24位(三个字节)来编码。UTF-8包含全世界所有国家需要用到的字符,是国际编码,通用性强。UTF-8编码的文字可以在各国支持UTF8字符集的浏览器上显示。例如,使用UTF-8编码,则在外国人的英文IE上也能显示中文,它们无须下载IE的中文语言支持包,在实际开发中采用UTF-8编码是最常见的。
Ø 字符编码和解码
在前面讲解过Java的转换流,将字节流转换为字符流,或者将字符流转换为字节流,这实际上涉及到编码和解码,将字符流转换为字节流称为编码,便于计算机识别;将字节流转换为字符流称为解码,便于用户看懂。
在转换流中,有可能出现乱码的情况,出现这种情况原因一般是编码与解码字符集不统一,另外缺少字节数或长度丢失,也会出现乱码。接下来通过一个案例来演示字符的编码和解码,如例所示。
程序的运行结果如图所示。
在例中,先声明一个字符串,然后byte1以GBK形式编码,byte2以UTF-8形式编码,前两次打印的是按对应编码格式解码,正确输出字符串,后两次打印反之,出现乱码情况。
另外,Windows系统默认使用的字符集是GBK,接下来通过一个案例演示如何使用流读取文件并指定字符集,首先在D盘下新建一个“test.txt”文件,文件内容如下。
创建文件完成后,开始编写代码,如例所示。
程序的运行结果如图所示。
在例中,将D盘“test.txt”文件内容读取,指定GBK为解码方式,正确输出了文件内容,证明了上面提到的Windows系统默认使用的字符集是GBK,如果将例中第8行的GBK改为其他字符集,例如UTF-8,就会出现乱码的情况,程序的运行结果如图所示。
在图中,运行结果打印了乱码,因为修改了解码方式为UTF-8,与Windows系统默认使用的GBK不统一。
Ø 字符传输
前面讲解的I/O文件传输用的都是Windows系统默认编码字符集GBK,读写文件没有发生乱码问题。但如果读取一个编码格式为GBK的文件,将读取的数据写入一个编码格式为UTF-8的文件时,则会出现乱码的情况,接下来通过一个案例演示这种情况,示例代码参考。
在例中,分别以GBK字符集和UTF-8字符集创建两个文件file1.txt和file2.txt,将两个字符串分别写入两个文件,然后用字符输入流读取file2.txt的内容,最后用字符输出流将读取到的内容输出到file1.txt,生成文件如图所示。
在图中,显示了例运行后生成的两个文件,其中,file1.txt后半段乱码,就是因为file1.txt使用的是GBK字符集,file2.txt使用的是UTF-8字符集,将file2.txt的内容读取到写入file1.txt就出现了乱码,所以在文件传输时,一定要注意两个文件的字符集问题。
Ø NIO概述
NI/O( New Input/Output)也称为New I/O。是一种基于通道和缓冲区的I/O方式,与之前学习面向流的I/O相比,NI/O是面向缓存的,其效率会提高很多。而且,NI/O是一种同步非阻塞的I/O模型,会不断轮询I/O事件检查其是否准备就绪,在等待I/O的时候,可以同时做其他任务。
在NI/O中同步的核心就是Selector,Selector代替了线程本身轮询I/O事件,避免了阻塞的同时减少了不必要的线程消耗;非阻塞的核心就是通道和缓冲区,当I/O事件就绪时,可以通过写道缓冲区,保证I/O的成功,而无需线程阻塞式地等待。在J ava API中提供了两套NI/O,一套是针对标准输入输出NI/O,另一套就是网络编程NI/O。
面向流的I/O一次一个字节地处理数据,一个输入流产生一个字节,一个输出流就消费一个字节。但是面向流的I/O通常处理的很慢。面向块的I/O系统以块的形式处理数据。每一个操作都在一步中产生或消费一个数据块。按块要比按流快的多,但面向块的I/O缺少了面向流I/O所具有的简单性。
(1)I/O与NI/O对比区别如表所示。
(2)面向流与面向缓冲
Java NI/O和I/O之间第一个最大的区别是,I/O是面向流的,NI/O是面向缓冲区的。Java I/O面向流这就意味着每次从流中读一个字节,直至读取完所有字节,这些字节不会被缓存,此外,不能前后移动流中的数据。如果需要前后移动从流中读取的教据,需要先将它缓存到一个缓冲区。Java NI/O的缓冲导向方法略有不同,会将数据读取到一个缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆蓋缓冲区里尚未处理的数据。
(3)阻塞与非阻塞
Java I/O的流是阻塞的,每当一个线程调用read()方法或write()方法时,该线程都会被阻塞,直到有一些数据被读取,或者数据完全写入,该线程在此期间不能再做其他任何事情。Java NI/O的非阻塞模式下一个线程从某通道发送请求读取数据,只会请求得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变为可以读取的状态之前,该线程依旧能够继续做其他的事情,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。线程通常将非阻塞I/O的空闲时间用于在其它通道上执行I/O操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
Ø NIO基础
NI/O主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)。传统I/O基于字节流和字符流进行操作,而NI/O基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择区)用于监听多个通道的事件(比如连接打开,数据到达)。
Buffer是一个对象,它包含一些要写入或读出的数据。在NI/O中,数据是放入Buffer对象的,而在I/O中,数据是直接写入或者读到Stream对象的。应用程序不能直接对Channel 进行读写操作,而必须通过Buffer来进行,即Channel 是通过Buffer来读写数据的。在NI/O中,所有的数据都是用Buffer处理的,它是NI/O读写数据的中转池。
Buffer实质上是一个数组,通常是一个字节数据,但也可以是其他类型的数组。但一个缓冲区不仅仅是一个数组,重要的是它提供了对数据的结构化访问,而且还可以跟踪系统的读写进程。
使用 Buffer 读写数据一般遵循以下四个步骤。
(1)写入数据到 Buffer。
(2)调用 flip() 方法。
(3)从 Buffer 中读取数据。
(4)调用 clear() 方法或者 compact() 方法。
当向 Buffer 写入数据时,Buffer会记录下写了多少数据,一旦要读取数据,需要通过 flip()方法将Buffer从写模式切换到读模式。在读模式下,可以读取之前写入到 Buffer 的所有数据。一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用clear()方法或compact()方法。clear()方法会清空整个缓冲区,compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
1. Channel
Channel也是一个对象,可以通过它读取和写入数据。可以把它看做I/O中的流。但是它和流相比还有一些不同,Channel是双向的,既可以读又可以写,而流是单向的。Channel可以进行异步的读写,但是需要注意的是对Channel的读写必须通过buffer对象,正如上面提到的,所有数据都通过Buffer对象处理,所以永远不会将字节直接写入到Channel中,相反,是将数据写入到Buffer中;同样,也不会从Channel中读取字节,而是将数据从Channel读入Buffer,再从Buffer获取这个字节。因为Channel是双向的,所以Channel可以比流更好地反映出底层操作系统的真实情况。特别是在Unix模型中,底层操作系统通常都是双向的。
在Java NIO中Channel主要有如下几种类型。
FileChannel:从文件读取数据的。
DatagramChannel:读写UDP网络协议数据。
SocketChannel:读写TCP网络协议数据。
ServerSocketChannel:可以监听TCP连接。
2. Selector
Selector同样也是一个对象,它可以注册到很多个Channel上,监听各个Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个Channel,就可以处理大量网络连接了。有了Selector,可以利用一个线程来处理所有的channels。线程之间的切换对操作系统来说代价是很高的,并且每个线程也会占用一定的系统资源。所以,对系统来说使用的线程越少越好。
通过Selector selector = Selector.open();创建一个Selector,然后,就需要注册Channel到Selector了,通过调用 channel.register()方法来实现注册:
channel.configureBlocking(false);
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);
注意,注册的Channel 必须设置成异步模式才可以,,否则异步I/O就无法工作,这就意味着我们不能把一个FileChannel注册到Selector,因为FileChannel没有异步模式,但是网络编程中的SocketChannel是可以的。
Ø NIO中的读和写操作
I/O中的读和写,对应的是数据和Stream,NI/O中的读和写则对应的就是通道和缓冲区。NI/O中从通道中读取:创建一个缓冲区,然后让通道读取数据到缓冲区。NI/O写入数据到通道:创建一个缓冲区,用数据填充它,然后让通道用这些数据来执行写入。
1. 从文件中读取
通过前面的学习读者已经知道,在NI/O系统中,任何时候执行一个读操作,都是从Channel中读取,而不是直接从Channel中读取数据,因为所有的数据都必须用Buffer来封装,所以应该是从Channel读取数据到Buffer。因此,如果从文件读取数据的话,需要如下三步。
2. 写入数据到文件
步骤类似于从文件读数据。
第一步:获取一个通道。
第二步:创建缓冲区。
第三步:将数据从通道读到缓冲区。
2. 写入数据到文件
步骤类似于从文件读数据。
第一步:获取一个通道。
第二步:创建缓冲区,将数据放入缓冲区。
第三步:把缓冲区数据写入通道中。
3. 读写结合
CopyFile是一个非常好的读写结合的例子,可以通过CopyFile这个实例让读者体会NI/O的操作过程。CopyFile执行三个基本的操作:创建一个Buffer,然后从源文件读取数据到缓冲区,然后再将缓冲区写入目标文件。
Ø 注意事项
上面程序中有以下几个地方需要注意。
1. 检查状态
当没有更多的数据时,复制就算完成,此时 read() 方法会返回 -1 ,可以根据这个方法判断是否读完。
2. Buffer类的flip、clear方法
flip方法的源码:
在上面的FileCopy程序中,写入数据之前调用了Buffer.flip(); 方法,这个方法把当前的指针位置position设置成了limit,再将当前指针position指向数据的最开始端。现在可以将数据从缓冲区写入通道了。 position 被设置为 0,这意味着得到的下一个字节是第一个字节。 limit 已被设置为原来的 position,这意味着它包括以前读到的所有字节,并且一个字节也不多。
clear方法的源码:
在上面的FileCopy程序中,写入数据之后也就是读数据之前,调用了 buffer.clear();方法,这个方法重设缓冲区以便接收更多的字节。
小结:Java语言程序设计— I/O(输入/输出)流
通过学习,能够掌握Java输入、输出体系的相关知识。重点要理解的是输入流和输出流的区别,其中对输入流只能进行读操作,而对输出流只能进行写操作,程序中需要根据待传输数据的不同特性而使用不同的流。
上一篇