java io之输入篇

        不知不觉java的I/O系统已经忘得差不多了,不知道你是不是也有同样的感受,在做项目时某些代码写起来很熟练,可是过了一段时间突然用到类似功能的时候总是“提笔忘字”,不是这少些一点就是那忘记一块的。
        谁也不是天才或者记忆力超群,难免会经常忘记学过,用过的东西,何况当时更有可能理解的并不透彻,只是简单用用而已。
        其实避免上述问题最好的办法就是将代码的用法及原理理解深刻,总结一套适用自己的记忆方式,不应该死记硬背,死记硬背那是书呆子,世界上有那么多种编程语言,谁又能全精通?学会了融会贯通就会过目不忘,这点当然是最难的!既然都不是天才,那么我们经常复习一下,勤能补拙还是很必要的。
        首先我们打开jdk的api文档,找的java.io包(io包解释:通过数据流、序列化和文件系统提供系统输入和输出):http://docs.oracle.com/javase/1.5.0/docs/api/java/io/package-summary.html
        中文版的可以在我的博客中下载到:http://g21121.iteye.com/blog/1513867
        从jdk1.4之后,java又添加了nio包(nio包解释:定义作为数据容器的缓冲区,并提供其他 NIO 包的概述),添加目的是为了改进性能和功能,但对于初学者应该从java.io包学起,因为这里面有最“原始”的I/O开发类。
        java.io包中有很多接口和类,刚刚接触的话难免不知所措,我们从其中最重要和最经常使用的类学起:

File(java.io.File)类:字面意思是“文件”,但java中File并不单单是文件那么简单,因为File既可以表示单个文件有可以表示一个文件列表,甚至是表示一个文件夹路径等等。
        我们看到api中是这样描述File的:文件和目录路径名的抽象表示形式。
单个文件:

String fileName="c:\\test.txt";
File file=new File(fileName);

        多个文件:

//假设“c:\\test\\”文件夹下有很多文件
String fileName="c:\\test\\";
File file=new File(fileName);
//获取文件夹下所有文件
File[] fileList=file.listFiles();

        文件夹:

String fileName="c:\\test\\";
File file=new File(fileName);
//如果是"文件夹"返回true结果
boolean isDir=file.isDirectory();

        在这里我们需要注意的是:,"/" 表示 UNIX 中的根目录,"\\\\" 表示 Microsoft Windows UNC 路径名。
        注意:其实这里"\\\\"和"\\"没有区别,只不过是转义了一下,即使你使用的是"\\"File也会识别的。这里不比为转义符号而担心,但是我建议采用一致的、转以后的"\\\\",这样才能避免不必要的麻烦。


        下面我们仔细研究下File中方法:
        1.File中有一些返回值为boolean的方法,用来判断此文件或文件夹是否具备某些权限,例如:可写,可读,可执行,是否存在,是否删除成功等等,这些方法都比较简单,大家可以自己查看api或代码。
        2.File中有一些返回值为String的方法,用来获取文件的相关信息:例如:文件名,文件路径,文件上级路径等等。
        3.createNewFile()方法:

String fileName="c:\\\\test\\\\1.txt";
File file=new File(fileName);
boolean success=file.createNewFile();
System.out.println(success);

        当目录不存在时会抛出IOException异常:

Exception in thread "main" java.io.IOException: 系统找不到指定的路径。
	at java.io.WinNTFileSystem.createFileExclusively(Native Method)
	at java.io.File.createNewFile(File.java:883)
	at test.Te.main(Te.java:40)

        此时我们就需要手动去创建文件夹test:

String dirName = "c:\\\\test\\\\";
String fileName = "c:\\\\test\\\\1.txt";
File file = new File(fileName);
File dir = new File(dirName);
// 创建所有依赖的目录结构
boolean dirSuccess = dir.mkdir();
//如果文件夹创建成功
if (dirSuccess) {
	// 创建文件
	boolean fileSuccess = file.createNewFile();
	System.out.println(fileSuccess);
}

        如果由于种种原因你不想一个一个的去创建文件夹,则可以调用dir.mkdirs()方法创建出所有依赖的文件夹:

//创建所有依赖的目录结构
file.mkdirs();
原代码修改为:
String dirName = "c:\\\\test\\\\test2";
String fileName = "c:\\\\test\\\\1.txt";
File file = new File(fileName);
File dir = new File(dirName);
// 创建所有依赖的目录结构
boolean dirSuccess = dir.mkdirs();
//如果文件夹创建成功
if (dirSuccess) {
	// 创建文件
	boolean fileSuccess = file.createNewFile();
	System.out.println(fileSuccess);
}

        创建文件是不是如此简单啊~
        4.String[] list()、String[] list(FilenameFilter filter)和方法:
        调用list()方法可以返回目录下的所有文件名列表,例如:

String dirName = "c:\\\\test\\\\";
File dir = new File(dirName);
String[] dirs = dir.list();
for (String dn : dirs)
	System.out.println(dn);

        我们可以看到test目录下有test2文件夹和1.txt文件,值得注意的是list()返回的是文件名列表,不包含路径。
        list(FilenameFilter filter)方法也有同样的功能,只不过为返回列表按FilenameFilter做了排序:

String dirName = "c:\\\\test\\\\";
File dir = new File(dirName);
//这里我们想过滤文件名包含‘1’的文件
FileNameFilter f=new FileNameFilter("1");
String[] dirs = dir.list(f);
for (String dn : dirs)
	System.out.println(dn);

        FileNameFilter代码:

import java.io.File;
import java.io.FilenameFilter;
/**
 * 
 * 类描述:代码很简单,实现了FilenameFilter接口,重写了accept方法
 * @author ming.li<br/> 
 *		<a href="http://g21121.iteye.com">iteye bolg</a>
 * @time 2012-5-15 上午11:07:22
 */
public class FileNameFilter implements FilenameFilter {
	private String str;

	public FileNameFilter(String str) {
		this.str = str;
	}
	
	@Override
	public boolean accept(File dir, String name) {
		// 返回包含'name'的文件
		return name.indexOf(str) >= 0;
	}

	public String getStr() {
		return str;
	}

	public void setStr(String str) {
		this.str = str;
	}

}

        FileNameFilter编写起来并不麻烦,只要实现FilenameFilter接口,在重新的accept方法中编写自己要比较的方法即可,网上不同过滤条件的例子有很多大家可以自己搜索看看。
        5.File中还有几个返回值为File[]的listFiles方法,具体用法跟上面大同小异也比较简单,而且返回值为File对象,我们可以做更多的事情。
        File几个比较重要的方法基本都学习了,当然单单只用File是无法做复杂事情的,还需要很多类和工具配合。

        InputStream(java.io.InputStream)和OutputStream(java.io.OutputStream)类:InputStream和OutputStream也是非常重要的类,我们会经常用到他们。字面意思就是“流的读取”和“流的输出”,初学者往往会把这两者弄混,因为在某些情况下InputStream和OutputStream并不会结合着“文件”来用,所以有时候我们会迷茫不知道该用哪个流,其实区别很简单。只要记住:InputStream是用来读取信息,OutputStream是用来输出信息,就非常好区分和记忆了!


        下面首先说说InputStream:
        InputStream这个类的方法很少也很简单,但用起来对于初学者绝对头疼,因为里面的read和方法居然返回的是int,理想中应该是String型啊?
        我们查看api发现InputStream类是表示字节输入流的所有类的超类,是一个抽象类。它的直接子类有:AudioInputStream, ByteArrayInputStream, FileInputStream, FilterInputStream, InputStream, ObjectInputStream, PipedInputStream, SequenceInputStream, StringBufferInputStream.
        因为InputStream提供的read方法是从输入流中读取数据的下一个字节。返回 0 到 255 范围内的 int 字节值。如果因为已经到达流末尾而没有可用的字节,则返回值 -1。在输入数据可用、检测到流末尾或者抛出异常前,此方法一直阻塞。
        也就是说InputStream面向的是“字节流”,字节流就是由字节组成的,字符流是由字符组成的。
        Java里字符由两个字节组成,字节流是最基本的,所有的InputStream和OutputStream的子类都是,主要用在处理二进制数据,
        所以我们在使用InputStream时需要结合其代码实现的子类来用,下面就是一个文件内容读取的例子:
        1.txt的文件内容是:

第一行
A

        程序代码:

String fileName = "C:\\test\\1.txt";
File file = new File(fileName);
InputStream in = new FileInputStream(file);
int i = -1;
while ((i = in.read()) != -1) {
	System.out.println(i);
}

        打印内容:

181
218
210
187
208
208
13
10
65

        显然打印结果不是我们想要的内容,我们就需要对代码进行修改:

String fileName = "C:\\test\\1.txt";
File file = new File(fileName);
InputStream in = new FileInputStream(file);
int i = -1;
while ((i = in.read()) != -1) {
	System.out.println((char)i);
}

        打印结果:

?
?
?
?
?
?




A

        显然这又不是我们想要的,那么就继续修改:

String fileName = "C:\\test\\1.txt";
File file = new File(fileName);
InputStream in = new FileInputStream(file);
InputStreamReader reader = new InputStreamReader(in);
//当出现乱码,或编码不同时可以采用下面的方法
//InputStreamReader reader = new InputStreamReader(in,"UTF-8");
BufferedReader buffReader = new BufferedReader(reader);
String line = null;
while ((line = buffReader.readLine()) != null) {
	System.out.println(line);
}

        打印结果:

第一行
A

        这是正确的结果,我们看到代码中又增加了两个类InputStreamReader和BufferedReader,继续看api:
InputStreamReader:是字节流通向字符流的桥梁:它使用指定的 charset 读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集。InputStreamReader居然有这么大的作用,通过它我们可以一步一步的将难懂的字节码转换成字符。
        BufferedReader:从字符输入流中读取文本,缓冲各个字符,从而实现字符、数组和行的高效读取。
api还说:每次调用 InputStreamReader 中的一个 read() 方法都会导致从底层输入流读取一个或多个字节。要启用从字节到字符的有效转换,可以提前从底层流读取更多的字节,使其超过满足当前读取操作所需的字节。
        为了达到最高效率,可要考虑在 BufferedReader 内包装 InputStreamReader。例如:
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        所以InputStreamReader一般都是结合着BufferedReader来使用,才能发挥最大功效,之后的事就简单多了,只要调用readLine()一行一行的处理就行了。当然这只是java io最简单的一种应用,而且无法处理更加复杂的场景,下面就学习学习一些高级些的应用。
        我们从jdk文档中发现,InputStream也有一个叫BufferedInputStream的缓存形式InputStream。
BufferedInputStream:为另一个输入流添加一些功能,即缓冲输入以及支持 mark 和 reset 方法的能力。在创建 BufferedInputStream 时,会创建一个内部缓冲区数组。在读取或跳过流中的字节时,可根据需要从包含的输入流再次填充该内部缓冲区,一次填充多个字节。mark 操作记录输入流中的某个点,reset 操作使得在从包含的输入流中获取新字节之前,再次读取自最后一次 mark 操作后读取的所有字节。
        也就是说BufferedInputStream为输入流提供了一个缓存区,可以缓冲指定空间大小的数据内容,这样做的好处就是,加快了流的读取速度,缺点就是占有了空间。
        BufferedInputStream使用的代码很简单:

String fileName = "C:\\test\\1.txt";
File file = new File(fileName);
InputStream in = new FileInputStream(file);  
BufferedInputStream bin = new BufferedInputStream(in); 
int i=-1;
while((i=bin.read())!=-1){
	System.out.println(i);
}

        此时大家肯定会想到,这其实跟用InputStream没什么区别嘛,那干嘛还用BufferedInputStream呢?下面我们做一个实验:
        写两段相同的代码,都去读取一个较大的txt文件(我实验的文件大小是6MB),看两者读取完成的时间是多少。

        代码一,用普通的InputStream读取: 

// 添加时间标记
System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()));
String fileName = "C:\\test\\1.txt";
File file = new File(fileName);
InputStream in = new FileInputStream(file);
while (in.read() != -1) {
	// 这里不做任何输出显示
}
System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()));

        打印结果:

14:53:42
14:54:00

        读取这个文件花费了20秒左右时间。

        代码二,采用BufferedInputStream来读取:

System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()));
String fileName = "C:\\test\\1.txt";
File file = new File(fileName);
InputStream in = new FileInputStream(file);  
BufferedInputStream bin = new BufferedInputStream(in); 
while(bin.read()!=-1){
	//这里不做任何输出显示
}
System.out.println(new SimpleDateFormat("HH:mm:ss").format(new Date()));

        打印结果:

14:54:41
14:54:41

        读取时间居然连1秒都没到...
        两者的区别大家应该明白了吧,在读取大文件时使用BufferedInputStream会使读取速度得到极大的改善。

        FileInputStream:从文件系统中的某个文件中获得输入字节。哪些文件可用取决于主机环境。FileInputStream 用于读取诸如图像数据之类的原始字节流。要读取字符流,请考虑使用 FileReader。 与BufferedInputStream相同,FileInputStream同样是InputStream的子类。

        我们之前已经使用过FileInputStream来读取文件的字节流信息,这里要注意的是:在读取文件内容前应该先判断文件是否存在或是否可读等等必要信息(例如:file.isFile()、file.canRead()),否则会抛出异常信息。
        DataInputStream:与BufferedInputStream相同,DataInputStream也是FilterInputStream的子类。DataInputStream允许应用程序以与机器无关方式从底层输入流中读取基本Java数据类型。应用程序可以使用数据输出流写入稍后由数据输入流读取的数据。

        Reader:用于读取字符流的抽象类。子类必须实现的方法只有 read(char[], int, int) 和 close()。但是,多数子类将重写此处定义的一些方法,以提供更高的效率和/或其他功能。我们之前用到了BufferedReader用来按行读取文件内容,BufferedReader是Reader的直接子类,其他直接子类还有CharArrayReader, FilterReader, InputStreamReader, PipedReader, StringReader,其中InputStreamReader我们已经学习过了,也是很重要的类。
        Reader与InputStream一样都是抽象类,只是定义了一些行为,所以我们是无法直接使用Reader的。例如如下代码是无法编译的:

Reader r=new Reader();

 
        因为和InputStream一样Reader的默认构造方法也是protected的。

        Reader的子类有BufferedReader, CharArrayReader, FilterReader, InputStreamReader, PipedReader, StringReader
        我们之前是利用的File和FileInputStream来读取的文件内容,但是jdk中还提供了RandomAccessFile这个类,下面我们来学习一下RandomAccessFile。
        RandomAccessFile:此类的实例支持对随机访问文件的读取和写入。随机访问文件的行为类似存储在文件系统中的一个大型 byte 数组。存在指向该隐含数组的光标或索引,称为文件指针;输入操作从文件指针开始读取字节,并随着对字节的读取而前移此文件指针。如果随机访问文件以读取/写入模式创建,则输出操作也可用;输出操作从文件指针开始写入字节,并随着对字节的写入而前移此文件指针。写入隐含数组的当前末尾之后的输出操作导致该数组扩展。该文件指针可以通过 getFilePointer 方法读取,并通过 seek 方法设置。 通常,如果此类中的所有读取例程在读取所需数量的字节之前已到达文件末尾,则抛出 EOFException(是一种 IOException)。如果由于某些原因无法读取任何字节,而不是在读取所需数量的字节之前已到达文件末尾,则抛出 IOException,而不是 EOFException。需要特别指出的是,如果流已被关闭,则可能抛出 IOException。
        我们假定C:\\test\\1.txt的文件内容为:

1234567890
ABCDEF

        编写一个RandomAccessFile读取的代码,看看与File有什么不同:

String fileName = "C:\\test\\1.txt";
File file = new File(fileName);
RandomAccessFile rfile=new RandomAccessFile(file,"r");
System.out.println(rfile.readLine());

        打印结果:

1234567890

        我们发现RandomAccessFile其实跟File的读取并没有什么不同嘛,但是我们仔细查看api,会发现RandomAccessFile有一个seek方法,seek:直译为寻找,其实这里的意思为“移动”,即将指针移动到指定位置。这样RandomAccessFile才名副其实,可以任意访问、编辑文件指定位置的内容,就像插入一样。
        此时我们再去理解api对RandomAccessFile的解释,我们就可以认为:RandomAccessFile解决了File无法在指定位置去读或写的局限性。
        下面是一段最简单的RandomAccessFile代码:

String fileName = "C:\\test\\1.txt";
File file = new File(fileName);
RandomAccessFile rfile=new RandomAccessFile(file,"r");
System.out.println(rfile.readLine());
//将指针移动到第3个字符,注意这里的0才是起始位
rfile.seek(2);
System.out.println(rfile.readLine());
//将指针移动到第13个字符
rfile.seek(12);
System.out.println(rfile.readLine());

        打印结果:

1234567890
34567890
ABCDEF

        我们看到指针移到哪里,readLine就从哪里开始读(写),创建一个RandomAccessFile对象需要传递一个File实例和mode属性,所谓mode属性其实就是你的操作类型,下面就是api对mode类型的解释:
        "r" 以只读方式打开。调用结果对象的任何 write 方法都将导致抛出 IOException。 
        "rw" 打开以便读取和写入。如果该文件尚不存在,则尝试创建该文件。 
        "rws" 打开以便读取和写入,对于 "rw",还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。 
        "rwd"   打开以便读取和写入,对于 "rw",还要求对文件内容的每个更新都同步写入到底层存储设备。 

"rws" 和 "rwd" 模式的工作方式极其类似 FileChannel 类的 force(boolean) 方法,分别传递 true 和 false 参数,除非它们始终应用于每个 I/O 操作,并因此通常更为高效。如果该文件位于本地存储设备上,那么当返回此类的一个方法的调用时,可以保证由该调用对此文件所做的所有更改均被写入该设备。这对确保在系统崩溃时不会丢失重要信息特别有用。如果该文件不在本地设备上,则无法提供这样的保证。
        "rwd" 模式可用于减少执行的 I/O 操作数量。使用 "rwd" 仅要求更新要写入存储的文件的内容;使用 "rws" 要求更新要写入的文件内容及其元数据,这通常要求至少一个以上的低级别 I/O 操作。         
        RandomAccessFile的seek方法参数为long型,即指针的位置,记住这里和其他java方法一样是以0为起始的。
        下面我们在看一段代码:

String fileName = "C:\\test\\1.txt";
File file = new File(fileName);
RandomAccessFile rfile = new RandomAccessFile(file, "r");
System.out.println(rfile.readLine());
// 将指针移动到第100个字符
rfile.seek(99);
System.out.println(rfile.readLine());

        打印结果:

1234567890
null

        当我调用seek时如果传递了错误的参数,就得不到读取的内容,只会返回一个null;我们再来看一段代码:

String fileName = "C:\\test\\1.txt";
File file = new File(fileName);
RandomAccessFile rfile = new RandomAccessFile(file, "rw");
System.out.println(rfile.readLine());
// 将指针移动到第3个字符,注意这里的0才是起始位
rfile.seek(2);
System.out.println(rfile.readLine());
System.out.println("当前指针位置:" + rfile.getFilePointer());
// 将指针移动到第13个字符
rfile.seek(12);
System.out.println(rfile.readLine());
System.out.println("当前指针位置:" + rfile.getFilePointer());

        打印结果:

1234567890
34567890
当前指针位置:12
ABCDEF
当前指针位置:18

        我们明明只是把指针移动到了2的位置,为什么第一个打印出来的居然是12?
        原来1234567890后面还隐藏了换行符加起来正好是12,你自己可以用read打印出来看下。至于末尾为什么是18,这就更简单了,因为末尾已经没有换行符号了,已经结束了,加起来不正好是18。我们还可以做个试验,多加几行试下:
        我们修改1.txt内容为:

1234567890
1234567890
1234567890
1234567890

        修改我们的代码:

String fileName = "C:\\test\\1.txt";
File file = new File(fileName);
RandomAccessFile rfile = new RandomAccessFile(file, "rw");
System.out.println(rfile.readLine());
// 将指针移动到第3个字符,注意这里的0才是起始位
rfile.seek(2);
System.out.println(rfile.readLine());
System.out.println("当前指针位置:" + rfile.getFilePointer());
// 将指针移动到第13个字符
rfile.seek(38);
System.out.println(rfile.readLine());
System.out.println("当前指针位置:" + rfile.getFilePointer());

        将指针移动到第38,也就是第4行的位置,打印结果:

1234567890
34567890
当前指针位置:12
34567890
当前指针位置:46

        果然如我们所料,最大位移是46,查看截图也有这样的发现。


        其实这时我们在做一个实验就会很清楚seek的工作过程了:

String fileName = "C:\\test\\1.txt";
File file = new File(fileName);
RandomAccessFile rfile = new RandomAccessFile(file, "rw");
while (rfile.read() != -1) {
	System.out.println("当前指针位置:" + rfile.getFilePointer());
}

 打印结果:

当前指针位置:1
当前指针位置:2
当前指针位置:3
当前指针位置:4
.
.
.
.

当前指针位置:42
当前指针位置:43
当前指针位置:44
当前指针位置:45
当前指针位置:46

        从结果我们就可以清楚的明白,seek每当读取一段内容(可能是一个字节,可能是一个字符,也可能是一行)后,指针就会移动到这段内容之后,前面那个例子就是每读取一个字节内容,指针就向后位移1。
 

猜你喜欢

转载自286.iteye.com/blog/1530672