**概述
**
Java中输入、输出的处理通过java.io包下的类和接口来支持,在这个包下主要包括输入、输出两种IO流,每种输入、输出流又可以分为字节流和字符流。字节流以字节为单位来处理输入输出,字符流则以字符为单位。除此之外,Java的IO流使用了一种装饰器设计模式,它将IO流分成底层节点流和上层处理流。节点流用于和底层物理存储节点直接关联,不同物理节点获取节点流的方式可能存在一定差异,但程序可以把不同的物理节点流包装成统一的处理流,从而允许程序使用统一的输入、输出代码来读取不同物理存储节点的资源。
● 输入流和输出流 (按照数据流的方向)
Java的输入流主要由InputStream和Reader作为基类,输出流主要由OutputStream和Writer作为基类。
● 字节流和字符流 (按照处理数据的单位不同)
字节流和字符流的用法几乎完全一致,区别在于它们所操作的数据单元不同,字节流(8位)、字符流(16位),字节流主要由InputStream和OutputStream作为基类,字符流主
要由Reader和Writer作为基类。
● 节点流和包装流(处理流) (按照功能的不同)
从/向一个特定的I/0设备(磁盘、网络等)读写数据的流称为节点流,也常被称为低级流。
处理流则对于一个已存在的节点流进行连接或封装,常被称为高级流(装饰器设计模式)。
处理流的功能主要体现在:
1、性能的提高:主要以增加缓冲的方式来提高输入/输出的效率
2、操作的便捷:提供了系列便捷的方法来一次输入/输出大批量内容
JDK 1.4以后,Java在java.nio包下提供了系列的全新API,这就是Java新IO(new io)。用以更高效的进行输入、输出操作的处理。
File类
public class File extends Object
implements Serializable, Comparable<File>,文件和目录路径名的抽象表示形式。
File类是java.io包下代表与平台无关的文件和目录,在程序中用来操作文件和目录。使用文件路径字符串来创建File示例,该文件路径字符串既可以是绝对路径,也可以是相对路径。File能新建、删除和重命名文件和目录,但是不能访问文件内容本身。访问文件内容本身的操作需要使用IO流。
package cn.nevo.io;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
public class DirList2 {
public static void main(String[] args) {
//必须要保证父目录hello的存在
File path = new File("c:\\hello\\hello.txt");
if(!path.getParentFile().exists()) {
path.getParentFile().mkdir();
}
if(!path.exists()) {
try {
path.createNewFile();
System.out.println("文件创建成功!");
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
byte[] s = new byte[]{'h', 'e', 'l', 'l', 'o'};
try {
OutputStream out = new FileOutputStream(path);
out.write(s);
} catch (Exception e) {
e.printStackTrace();
}
}
}
实现FilenameFilter接口的类实例用于过滤文件名,在File的list方法中可以接收一个FilenameFilter参数,用于列出符合条件的文件。
package java.io;
public interface FilenameFilter {
boolean accept(File dir, String name);
}
示例:目录列表器
package cn.nevo.io;
import java.io.File;
import java.io.FilenameFilter;
import java.util.Arrays;
import java.util.regex.Pattern;
//查看目录列表
public class DirList {
public static void main(String[] args) {
//"/"代表该资源所处的根目录,此处为F:\
//"."代表该资源所在项目所处目录,此处为F:\workspace\JavaIoTest\.
File path = new File(".");
//返回此抽象路径名的绝对路径名字符串。
System.out.println(path.getAbsolutePath());
String[] list;
//没有指定运行参数,获取File对象包含的全部列表
if(args.length == 0) {
//list()方法返回一个字符串数组,这些字符串指定此抽象路径名表示的目录中的文件和目录。
list = path.list();
}
//获取受限制列表
else {
//list(FilenameFilter filter)返回满足指定过滤器条件的目录和文件列表
//args:".*\.project"
//list方法会为此目录对象下的每个文件名调用accept方法,来判断该文件是否包含再内
list = path.list(new DirFilter(args[0]));
}
//对查询结果按字母排序
Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
//输出数组列表
for(String dirItem : list) {
System.out.println(dirItem);
}
}
}
//实现FilenameFilter接口的类实例可用于过滤文件名
class DirFilter implements FilenameFilter {
private Pattern pattern;
public DirFilter(String regex) {
//compile(String regex)将给定的正则表达式编译到模式中。
this.pattern = Pattern.compile(regex);
}
@Override
public boolean accept(File dir, String name) {
//创建匹配给定输入与此模式的匹配器,尝试将整个区域与模式匹配。
//当且仅当整个区域序列匹配此匹配器的模式时才返回 true
boolean flag = pattern.matcher(name).matches();
System.out.println(flag);
return flag;
}
}
不指定参数运行情况:
F:\workspace\JavaIoTest\.
.classpath
.project
.settings
bin
src
指定参数运行(显示所有后缀名为.project的文件名)
F:\workspace\JavaIoTest\.
false
true
false
false
false
.project
list方法会为此目录对象下的每个资源调用accept方法,来判断该文件是否包含在内。
采用内部类修改上述示例:
package cn.nevo.io;
import java.io.File;
import java.io.FilenameFilter;
import java.util.Arrays;
import java.util.regex.Pattern;
public class DirList3 {
public static void main(final String[] args) {
File path = new File(".");
String[] list;
if(args.length == 0) {
list = path.list();
} else {
list = path.list(new FilenameFilter() {
//参数args必须是final的,内部类要求
//pattern知道匹配模式
private Pattern pattern = Pattern.compile(args[0]);
@Override
public boolean accept(File dir, String name) {
return pattern.matcher(name).matches();
}
});
}
Arrays.sort(list, String.CASE_INSENSITIVE_ORDER);
for(String dirItem : list) {
System.out.println(dirItem);
}
}
}
目录的检查及创建
File类不仅仅只代表存在的文件或目录,还可以用来创建新的目录或尚不存在的整个目录路径。还可以查看文件特性(如大小、读/写),检查某个File对象代表的是一个文件还是一个目录,并删除。注意查看API文档。
package cn.nevo.io;
import java.io.File;
public class MakeDirectories {
//无参数指定
public static void usage() {
System.err.println(
"Usage:MakeDirectories path1 ... \n" +
"Creates each path\n" +
"Usage:MakeDirectories -d path1 ... \n" +
"Deletes each path\n" +
"Usage:MakeDirectories -r path1 path2\n" +
"Renames from path1 to path2"
);
System.exit(1);
}
public static void fileData(File f) {
System.out.println(
"Absolute path:" + f.getAbsolutePath() +
"\n Can read:" + f.canRead() +
"\n Can write:" + f.canWrite() +
"\n getName:" + f.getName() +
"\n getParent:" + f.getParent() +
"\n getPath:" + f.getPath() +
"\n length:" + f.length() +
"\n lastModified:" + f.lastModified()
);
if(f.isFile()) {
System.out.println("It's a file");
} else if(f.isDirectory()) {
System.out.println("It's a directory");
}
}
public static void main(String[] args) {
//无参数输入,调用usage,给出提示
if(args.length < 1) {
usage();
}
//重命名文件,指定三个输入参数
if(args[0].equals("-r")) {
if(args.length !=3) {
usage();
}
File old = new File(args[1]),
rname = new File(args[2]);
old.renameTo(rname);
fileData(old);
fileData(rname);
return;
}
//新建和删除
int count = 0;
boolean del = false;
if(args[0].equals("-d")) {
count ++;
del = true;
}
count--;
while(++count < args.length) {
File f = new File(args[count]);
if(f.exists()) {
System.out.println(f + " exists");
if(del) {
System.out.println("deleting..." + f);
f.delete();
}
}else { //不存在
if(!del) {
f.mkdirs();
System.out.println("created " + f);
}
}
fileData(f);
}
}
}
输入和输出 (对文件内容进行操作)
输入输出使用流这个抽象概念,它屏蔽了实际的I/O设备中处理数据的细节。
字节流和字符流
它们的操作方式几乎完全一样,只是操作的数据单元不同而已
1、InputStream和Reader
InputStream和Reader是所有输入流的基类,它们是两个抽象类,是所有输入流的模版,其中定义的方法在所有输入流中都可以使用。
对比InputStream和Reader所提供的方法,可以看出这两个基类的功能基本相似。 返回结果为-1时表明到了输入流的结束点。
1.1、 FileInputStream和FileReader
InputStream和Reade都是抽象的,不能直接创建它们的实例,可以使用它们的子类,如FileInputStream和FileReader,它们都是节点流,直接和指定文件关联。
示例:
public class FileInputStreamTest {
public static void main(String[] args) {
InputStream in = null;
try {
in = new FileInputStream(".\\src\\cn\\nevo\\io\\FileInputStreamTest.java");
byte[] by = new byte[1024];
int hasRead = 0;
while((hasRead = in.read(by)) > 0) {
System.out.println(new String(by, 0, hasRead));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e2) {
e2.printStackTrace();
} finally {
try {
in.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
程序中打开的文件IO资源不属于内存中的资源,垃圾回收无法回收,需要显示关闭。
2、OutputStream和Writer
OutputStream和Writer也非常相似。
在Writer中包含如下方法:
因为字符流直接以字符作为操作单位,所以Writer可以用字符串来代替字符数组,即以String对象来作为参数。
2.1、FileOutputStream和FileWriter
示例:实现复制功能
public class FileOutputStreamTest {
public static void main(String[] args) {
FileInputStream input = null;
FileOutputStream output = null;
try {
//字节输入流
input = new FileInputStream(".\\src\\cn\\nevo\\io\\FileOutputStreamTest.java");
output = new FileOutputStream(".\\FileOutputStreamTest.txt");
byte[] by = new byte[1024];
int hasRead = 0;
while((hasRead = input.read(by)) > 0) {
output.write(by, 0, hasRead);
}
System.out.println("文件复制成功!");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e2) {
e2.printStackTrace();
} finally {
try {
input.close();
output.close();
}catch (IOException e) {
e.printStackTrace();
}
}
}
}
3、处理流
上面介绍了输入输出流的四个基本抽象类,并介绍了其子类访问文件的节点流的用法,下面我们来看一看处理流,它可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入输出方法,让程序员只需关心高级流的操作。
使用处理流的典型思路是:使用处理流来包装节点流,程序通过处理流来执行输入输出功能,让节点流与底层的IO设备、文件交互。
处理流的识别非常简单,只要流的构造器参数不是一个物理节点,而是已经存在的流,那么这种流就一定是处理流。所有节点流都是直接以物理IO节点作为构造器参数的。
PrintStream示例:
//处理流
public class PrintStreamTest {
public static void main(String[] args) {
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream("text.txt");
ps = new PrintStream(fos);
ps.println("处理流示例");
ps.append("PrintStream!");
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
使用处理流只需要在创建处理流时传入一个节点流构造参数即可。当我们使用处理流包装底层节点流之后,关闭输入输出资源时,只需要关闭最上层的处理流即可。
上表中的缓冲流主要是用于提高输入、输出的效率,但增加缓冲功能后一定要使用flush()才可以将缓冲区中的内容写入实际的物理节点。
对象流主要用于实现对象的序列化,管道流用于实现进程之间通信功能。
通常来说,我们认为字节流的功能比字符流的功能强大,因为计算机里所有的数据都是二进制的,而字节流可以处理所有的二进制文件。
如果需要进行输入输出操作的是文本内容,考虑使用字符流,如果需要进行输入输出操作的是二进制内容,考虑使用字节流。
4、转换流(InputStreamReader和OutputStreamWriter)
用于实现将字节流转换为字符流。
public class KeyInTest {
public static void main(String[] args) {
//java使用System.in代表标准输入,是InputStream(字节流)的实例
BufferedReader br = null;
try {
//将字节流转换为字符流
InputStreamReader isr = new InputStreamReader(System.in);
//将普通的字符流包装成缓冲流
br = new BufferedReader(isr);
String buffer;
while((buffer = br.readLine()) != null) {
if(buffer.equals("exit")) {
System.exit(1);
}
//打印读取内容
System.out.println(buffer);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
BufferedReader具有缓冲功能,可以一次读取一行文本,以换行符为标志,如果没有读到换行符,则程序阻塞,等到读到换行符为止。当我们在控制台进行输入时,只有按下回车时才会打印输入的结果。
由于BufferedReader有一个readLine方法,经常把读取文本内容的输入流包装为BufferedReader,以方便的读取输入流的文本内容。
5、重定向标准输入输出
java的标准输入、输出分别通过 System.in和 System.out来代表,默认情况下它们分别代表键盘和显示器,当程序通过System.in来获取输入时,实际上是从键盘读取输入;当程序试图通过System.out执行输出时,程序总是输出到屏幕。
System类:
java.lang.Object
java.lang.System
public final class System extends Object
示例,重定向标准输出:
public class RedirectOut {
public static void main(String[] args) {
//问重定向标准输出之前输出到控制台
System.out.println("输出到控制台");
PrintStream ps = null;
try {
FileOutputStream fos = new FileOutputStream("out.txt");
ps = new PrintStream(fos);
//重定向标准输出到PrintStream
System.setOut(ps);
//此时便不再输出到控制台
System.out.println("不再输出到控制台");
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
ps.close();
}
}
}
6、RandomAccessFile
与普通的文件输入输出不同的是,RandomAccessFile支持任意访问的方式。程序可以直接跳转的文件的任意地方来读写数据。如果我们希望只访问文件的部分内容,而不是把文件从头读到尾,这是更好的选择。RandomAccessFile既可以读文件也可以写。
注意:RandomAccessFile依然不能向文件的指定位置插入内容,如果这样做,新输出的内容将覆盖文件中原有的内容。如果需要插入,可以先把插入点内容读入缓冲区,等把需要插入的内容写到文件后再将缓冲区内容追加到文件后面。
RandomAccessFile类有两个构造器,一个使用String参数来指定文件名,一个使用File参数来指定文件本身。除此之外,创建RandomAccessFile对象时还需要指定一个mode参数,该参数指定其访问模式,具有以下四个值:
示例,使用RandomAccessFile访问中间部分数据:
public class RandomAccessFileTest {
public static void main(String[] args) {
File file = new File(".\\src\\cn\\nevo\\io\\RandomAccessFileTest.java");
RandomAccessFile raf = null;
try {
//以只读方式打开一个RandomAccessFile对象
raf = new RandomAccessFile(file, "r");
//文件指针的初始位置0
System.out.println(raf.getFilePointer());
//从文件指针为300的位置开始读
raf.seek(300);
int hasRead = 0;
byte[] by = new byte[1024];
while((hasRead = raf.read(by)) > 0) {
System.out.println(new String(by, 0, hasRead));
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e2) {
e2.printStackTrace();
} finally {
try {
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
向指定文件后追加内容,应该先将文件记录指针定位到文件最后,模式改为“rw”,然后再开始向文件中输入内容。
多线程、断点的网络下载工具就可以使用RandomAccessFile类来实现。
7、对象序列化
对象序列化的目标是将对象保存在磁盘中,或在网络中直接传输对象。对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久保存在磁盘上,通过网络将这种二进制流传输到另一个网络节点,其它程序一旦获得了这种二进制流,都可以将这种二进制流恢复成原来的java对象。序列化机制使得对象可以脱离程序的运行而独立存在。
对象的序列化(Serialize)是指将一个java对象写入IO流中,对象的反序列化(Deserialize)则指从IO流中恢复该java对象。
为了让某个类是可序列化的,该类必须实现如下两个接口之一: Serializable、Externalizable。通常建议,程序创建的每个JavaBean类都实现Serializable接口,该接口是一个标记接口,无须实现任何方法,它是指表明该类的实例是可序列化的。
通过在属性值前面加上 transient关键字(只能用于修饰属性),可以指定java序列化时无须理会该属性值。