Java梳理之理解IO(一)

对于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提供了这样的一个过滤的接口FileFilterFileNameFilter。我们直接在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又添加了readerwriter类来操作兼容 unicode 与面向字符的类。下面先从字节IO类说起。

InputStream与OutputStream

InputStreamOutputStream中存在一系列子类,如下图所示:

InputStream&OutputStream.png

其中,类InputStreamOutputStream属于抽象类,并不能将它实例化,如下:

/**
**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或者文件对象FileFileDescriptor对象来创建它。

ps:简单介绍一下类FileDescriptor,代表的是文件描述符,例如in标准输入的描述符、out标准输出的描述符、error标准错误的输出符,如果FileDescriptor表示的是一个文件的话,可以当做File来对待,但是并不能直接操作,需要通过创建对应的FileInputStreamFileOutputStream,然后再进行操作。

/**
**这里的异常请使用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;
    }   
}

其中类ObjectOutputStreamObjectInputStream不能直接创建,需要传入一个OutputStreamInputStream,在读取的时候,直接使用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[],所以不知道在哪里才是变量的分界点。这时候我们可以选择和DataOutputStreamDataInputStream配合使用,如:

/**
**配合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:DataInputStreamDataOutputStream都属于FilterInputStreamFilterOutputStream下的子类,这个FilterIn/OutputStream是IO实现装饰器模式的关键,在接下来的会说明。

FilterInputStream&FilterOutputStream

在上面那个例子中,使用了DataInputStramDataOutputStream,所以这里来理解一下FilterInputStreamFilterOutputStream,因为这两个类确实非常重要,它是用来提供装饰器类接口以控制特定输入输出流,在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子类。都可以传入相应的InputStreamOutputStream来构造新的类。

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最详解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,406评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,732评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,711评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,380评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,432评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,301评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,145评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,008评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,443评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,649评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,795评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,501评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,119评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,731评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,865评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,899评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,724评论 2 354

推荐阅读更多精彩内容

  • 一、流的概念和作用。 流是一种有顺序的,有起点和终点的字节集合,是对数据传输的总成或抽象。即数据在两设备之间的传输...
    布鲁斯不吐丝阅读 10,038评论 2 95
  • 在经过一次没有准备的面试后,发现自己虽然写了两年的android代码,基础知识却忘的差不多了。这是程序员的大忌,没...
    猿来如痴阅读 2,839评论 3 10
  • 之所以写这个是因为Hadoop的IO与·这个类似 但要比这个深入,而且都还涉及到网络传输原理的五个层次。所以,一...
    起个什么呢称呢阅读 1,002评论 0 6
  • 1 IONo18 1.1IO框架 【 IO:Input Output 在程序运行的过程中,可能需要对一些设备进...
    征程_Journey阅读 960评论 0 1
  • 临颜真卿【多宝塔】 临欧阳询【九成宫醴泉铭】
    Khitan阅读 766评论 1 4