对于IO部分,写的并不多,通常情况下都是复用现有的代码,致使很多细节都理解的并不透彻,现在梳理一下字节字符流。
前置:
File类
过去对这个类理解的并不透彻,一直把File类当成一个文件来看,却没有看到,它其实是用来处理文件和文件系统,我们可以用它来处理文件目录,也可以用它处理文件,集两大功能于一身。例如:
/**
**file示例
**/
public class FileDemo {
public static void main(String[] arg0) throws IOException{
File dir = new File("/test");
if(!dir.exists()){
dir.mkdirs();
}
System.out.println(dir.isDirectory());
File file = new File(dir,"test1.txt");
if(!file.exists()){
file.createNewFile();
}
System.out.println(file.isFile());
File[] files = dir.listFiles();
for(File f:files){
System.out.println(f.getAbsolutePath());
}
}
}
输出:
true
true
F:\test\test1.txt
如上所示,对于创建的File
类,有一个判断exists()
方法,如果该File
存在,则创建。因为File
类不单单是指代文件类型,所以创建的时候需要明确自己需要的是目录还是文件,对于目录来说,有两个方法,如上的mkdirs()
方法,它会将文件目录的所有结构都创建出来,不管父目录之前存不存在,而另一种则是mkdir()
,如果父目录不存在则会报错,必须一级一级创建;对于文件类型,则使用createNewFile()
方法创建一个新文件。如上代码中dir
是个目录,则可以使用listFiles()
方法获取目录下的所有子文件和目录,类型是File
;其中还有个list()
方法,使用它得出来的是个路径地址,类型是String
。
当然,不仅仅只是这几个操作,File
类型还可以获取文件的一系列属性,如length()
获取文件大小;需要强调的是File
在删除操作中,如果文件夹下还有其他文件夹或者文件,需要递归的删除下面的所有文件才可以使用delete()
删除这个文件夹,如下所示:
/**
**示例删除文件夹
**/
public class FileDemo {
public static void main(String[] arg0) throws IOException{
File dir = new File("/test");
if(!dir.exists()){
dir.mkdirs();
}
deleteFile(dir);
}
static void deleteFile(File file){
if(file.isFile()){
System.out.println("删除文件:"+file.getAbsolutePath());
file.delete();
}else if(file.isDirectory()){
File[] files = file.listFiles();
if(files!=null){
for(File f : files){
deleteFile(f);
}
}
System.out.println("删除文件夹:"+file.getAbsolutePath());
file.delete();
}
}
}
输出:
删除文件:F:\test\新建 Microsoft Excel 97-2003 工作表.xls
删除文件:F:\test\新建文件夹\新建 Microsoft Excel 97-2003 工作表.xls
删除文件夹:F:\test\新建文件夹\新建文件夹
删除文件夹:F:\test\新建文件夹
删除文件:F:\test\新建文本文档.txt
删除文件夹:F:\test
上面的listFiles()
或list()
方法会为我们列出所有的文件或者文件名,但是通常情况下,我们可能只需要获取满足我们要求的文件,而不需要列出全部。简单的方法:列出来之后,我们再加一层过滤就可以了,其实这个并不需要我们获取之后进行操作,Java提供了这样的一个过滤的接口FileFilter
和FileNameFilter
。我们直接在list()
或者listFiles()
中使用即可,如:
/**
**文件过滤示例
**/
public class FileDemo {
public static void main(String[] arg0) throws IOException{
File dir = new File("/test");
if(!dir.exists()){
dir.mkdirs();
}
File[] files = dir.listFiles(new FilenameFilter(){
@Override
public boolean accept(File dir, String name) {
// TODO Auto-generated method stub
if(name.contains("txt")) return true;
return false;
}});
for(File f:files){
System.out.println(f.getAbsolutePath());
}
}
}
输出:
4096
F:\test\test1.txt
F:\test\txt11
F:\test\新建文本文档.txt
在这个示例中,我的文件夹下创建了很多不是txt类型的文件,但是只返回了文件名包含txt的文件。对于接口中的dir
指的是文件目录,name
指的是文件的简单名,不包含路径。
IO
IO即是输入输出,通常有文件IO,网络IO,标准IO,和系统的内存数组IO。Java使用流来表示数据源对象或者数据接收设备,其中区分输入流InputStream
和输出流OutputStream
。在JDK 1.1又添加了reader
和writer
类来操作兼容 unicode 与面向字符的类。下面先从字节IO类说起。
InputStream与OutputStream
在InputStream
和OutputStream
中存在一系列子类,如下图所示:
其中,类InputStream
和OutputStream
属于抽象类,并不能将它实例化,如下:
/**
**InputStream和OutputStream的声明
**/
public abstract class InputStream implements Closeable {
/**field and method **/
}
public abstract class OutputStream implements Closeable, Flushable {
/**field and method **/
}
我们使用的都是他们的子类,由于并不多,可以一个一个的看过来:
FileInputStream&FileOutputStream
从类名就可以看出来,这个类操作的对象就是File
文件,在FileInputStream
中存在三个构造器,即,FileInputStream(String name)
、FileInputStream(File file)
和FileInputStream(FileDescriptor fdObj)
,通过传入一个文件的路径String
或者文件对象File
或FileDescriptor
对象来创建它。
ps:简单介绍一下类
FileDescriptor
,代表的是文件描述符,例如in
标准输入的描述符、out
标准输出的描述符、error
标准错误的输出符,如果FileDescriptor
表示的是一个文件的话,可以当做File
来对待,但是并不能直接操作,需要通过创建对应的FileInputStream
或FileOutputStream
,然后再进行操作。
/**
**这里的异常请使用try-catch,并在finally中释放资源,例子中为了方便没有进行这些操作
**/
public class IODemo {
public static void main(String[] arg0) throws IOException{
InputStream in = new FileInputStream(new File("/test/test.txt"));
OutputStream out = new FileOutputStream(new File("/test/demo.txt"));
int length;
byte[] buffer = new byte[1024];
while((length = in.read(buffer))!=-1){
out.write(buffer,0,length);
// System.out.println(new String(buffer,0,length));
}
}
}
在上面的代码中,会将test.txt
文件的内容写入demo.txt
文件中,使用byte[]
数组来进行缓存,其中length
的作用是避免最后一次读取缓存区有空余,造成异常IndexOutOfBoundsException
。看了下源码,read()
以及它重载方法是调用的native
方法,不过可以知道,它读取的是字节,而使用byte[]
之后,读取的就是这个数组大小的字节数。
ObjectInputStream&ObjectOutputStream
这两个类是直接对对象进行操作的,需要注意的是:对象必须实现Serializable
接口,在使用ObjectInputStream
时,很容易出异常,因为将实例信息直接存在文件中,如果不清楚文件包含了那些类,很容易出异常。如下所示:
/**
**ObjectInputStream示例
**/
static void testObjectInputStream() throws FileNotFoundException, IOException, ClassNotFoundException{
File file = new File("/test/test.txt");
if(!file.exists()){
file.createNewFile();
}
ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
Test test = new Test("demo");
Test test1 = new Test("demo1");
out.writeObject(test1);
out.writeObject(test);
ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
try {
Test t = (Test)in.readObject();
System.out.println(t.getName());
Test t1 = (Test)in.readObject();
System.out.println(t1.getName());
} catch (Exception e) {
e.printStackTrace();
}
}
class Test implements Serializable{
private String name;
public Test(String name){
this.name = name;
}
public String getName(){
return name;
}
}
其中类ObjectOutputStream
或ObjectInputStream
不能直接创建,需要传入一个OutputStream
或InputStream
,在读取的时候,直接使用readObject()
方法。当然咯,这个类也支持一些基本数据类型的操作。这里其实有些疑问,虽然我们可以读也可以取数据,但是怎么保证我取的数据就是我想要的数据呢?而且该如何判断文件中的数据已经被读完了,难道是以EOFException
来判断么?对于这两个问题,我想用个容器,把需要持久化的对象放进去,然后写这个容器就可以了,如果你有更好的方法,请告知。
ByteArrayInputStream&ByteArrayOutputStream
在《Thinking in Java》中有提到,这两个类是针对内存缓存进行操作的,它允许将内存缓存变成一个流。如:
/**
**ByteArrayInputStream&ByteArrayOutputStream示例
**/
static void testByteArray() throws IOException{
// ByteArrayInputStream bais = new ByteArrayInputStream();
int a=1;
int b = 2;
String str = "str";
ByteArrayOutputStream baos = new ByteArrayOutputStream();
baos.write(a);
baos.write(b);
baos.write(str.getBytes());
System.out.println(new String(baos.toByteArray()));
byte[] buffer = baos.toByteArray();
ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
int length = 0;
while((length=bais.read())!=-1){
System.out.println(length);
}
}
输出:
��str
1
2
115
116
114
可以看到,在上面的代码中,做了两种输出,对于int
类型的变量,在流内可以直接获取转换,但是对于byte[]
类型,如果和int
类型在一个ByteArrayOutputStream
一起使用,会出现解析的问题,因为拿到的是一个byte[]
,所以不知道在哪里才是变量的分界点。这时候我们可以选择和DataOutputStream
与DataInputStream
配合使用,如:
/**
**配合DataInputStream和DataOutputStream使用
**/
static void testByteArray() throws IOException{
int a=1;
int b = 2;
double k= 8.00;
String str = "str";
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(baos);
dos.write(a);
dos.write(b);
dos.writeDouble(k);
dos.writeUTF(str);
System.out.println(new String(baos.toByteArray()));
byte[] buffer = baos.toByteArray();
ByteArrayInputStream bais = new ByteArrayInputStream(buffer);
DataInputStream dis = new DataInputStream(bais);
// System.out.println(dis.readDouble());
System.out.println(dis.read());
System.out.println(dis.read());
System.out.println(dis.readDouble());
System.out.println(dis.readUTF());
}
输出:
��@
1
2
8.0
str
在上面的代码中,有点需要注意的是:read()
方法的获取顺序,要和存放的顺序一致,不然会出现转码问题或者异常。这样就解决了上面那个字节和字节数组冲突的问题,而且DataOutputStream
提供的操作方法并不仅仅是这两个,它提供了一系列的重载方法。
ps:
DataInputStream
和DataOutputStream
都属于FilterInputStream
和FilterOutputStream
下的子类,这个FilterIn/OutputStream
是IO实现装饰器模式的关键,在接下来的会说明。
FilterInputStream&FilterOutputStream
在上面那个例子中,使用了DataInputStram
和DataOutputStream
,所以这里来理解一下FilterInputStream
和FilterOutputStream
,因为这两个类确实非常重要,它是用来提供装饰器类接口以控制特定输入输出流,在FilterIn/OutputStream
下对应存在四个子类,其中LineNumberIn/OutputStream
已经过期。这里介绍一下:
DataInputStream&DataOutputStream
类DataOutputStream
能将基本数据类型或String
对象格式化到流中,就像上个例子一样,任何机器上的任何地方DataInputStream
都可以通过这个获取到的byte[]
把数据获取到。
BufferInputStream&BufferOutputStream
这两个类为IO提供了缓存的功能,它使得我们操作的对象变成BufferIn/OutputStream
中的成员byte[] buf
,在使用BufferInputStream
时,会将这个类的buf
数组填充满,使用read()
方法时,会先从这里读取,类似的BufferOutputStream
也是这样,操作的时候,从buf
中先操作好了,再write()
出去。
PushbakInputStream
这个类能弹出一个字节的缓冲区,因此能将读到的最后一个字节回退,通常作为编辑器的扫描器,之所以包含在这里是因为java编译器需要,了解一下就可以了。
PrintStream
这个类其实应该是最熟悉的,我们的System.out
就是使用的这个类,它提供了两个方法println()
和print()
。
在上面这几个类都是FilterInputStream
或者FilterOutputStream
子类。都可以传入相应的InputStream
和OutputStream
来构造新的类。
PipedInputStream&PipedOutputStream
管道输入输出流,这两个类配合可以实现线程间通信。实现的流程如下:
/**
**PipedInputStream&PipedOutputStream示例
**/
public class PipedDemo {
public static void main(String[] arg0) throws IOException{
PipedOutputStream out = new PipedOutputStream();
PipedInputStream in = new PipedInputStream();
out.connect(in);
new Thread(new Product(out)).start();
new Thread(new Consumer(in)).start();
}
}
class Product implements Runnable{
PipedOutputStream out;
public Product(PipedOutputStream out){
this.out = out;
}
@Override
public void run() {
// TODO Auto-generated method stub
String test = "hello world";
try {
out.write(test.getBytes());
out.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class Consumer implements Runnable{
PipedInputStream in;
public Consumer(PipedInputStream in){
this.in = in;
}
@Override
public void run() {
// TODO Auto-generated method stub
byte[] buff = new byte[1024];
try {
int len = in.read(buff);
System.out.println(new String(buff,0,len));
in.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
输出:
hello world
可以发现,我在其他线程中写入的数据,在另一个绑定了这个out
的线程PipedInputStream
中也能获取到对应的数据。个人感觉有一点不好的是,这里并不能将一个out
绑定多个in
,在多个线程中使用同一个绑定的in
也会出现异常StringIndexOutOfBoundsException
。
SequenceInputStream
这个类可以将多个InputStream
合并到一起操作,提供了两个构造方法,一个是SequenceInputStream(InputStream s1, InputStream s2)
,另一个使用枚举类型SequenceInputStream(Enumeration<? extends InputStream> e)
,这样可以达到合并流的效果。当然不是无缘无故合并的,SequenceInputStream
将与之相连接的流集组合成一个输入流并从第一个输入流开始读取,直到到达文件末尾,接着从第二个输入流读取,依次类推,直到到达包含的最后一个输入流的文件末尾为止。如下所示:
/**
**SequenceInputStream示例
**/
public class SequenceDemo {
public static void main(String[] arg0) throws IOException{
SequenceInputStream sis = null;
Enumeration<InputStream> inputStreamEnum;
Vector<InputStream> inputStreamV = new Vector<InputStream>();
inputStreamV.add(new FileInputStream("/test/test.txt"));
inputStreamV.add(new FileInputStream("/test/demo.txt"));
inputStreamEnum = inputStreamV.elements();
sis = new SequenceInputStream(inputStreamEnum);
byte[] buff = new byte[1024];
int len;
while((len=sis.read(buff))!=-1){
System.out.println(new String(buff,0,len));
}
}
}
输出:
sdasd
kkk
sdjanb
虽然上面的乱码是因为之前例子存放的是对象的实例,无法直接new String()
出来,但是可以把代码中的FileInputStream
换一下就好了。大体上可以看出,SequenceInputStream
类将多个InputStream
合并在一起,操作的时候顺序读取。
这里把大部分的字节流操作类给介绍了,在图中的StringBufferInputStream
没有介绍,因为已经过时了,并不推介去用,把字节流梳理完了,下面会看字符流操作。
这部分以前看过,但是理解的并不深刻,现在整理之后效果还是挺好的,由于本人能力有限,文中出现的问题请帮忙指正~
文章参考:《Thinking in Java》18章Java I/O系统
《Java程序设计语言》20章 IO包
Java IO最详解