Java 开发实践 IO流 详尽解析

来源:互联网 发布:淘宝网如何投诉买家 编辑:程序博客网 时间:2024/06/06 04:00

转载请注明出处:http://blog.csdn.net/smartbetter/article/details/51459349

Java的IO流使用了一种装饰器设计模式,它将IO流分为底层节点流和上层处理流。本篇重点在如何访问文件与目录、如何以二进制格式和文本格式来读写数据、对象序列化机制、还有Java7的“NIO.2”。

装饰设计模式:当想要对已有的对象进行功能增强时,可以定义类,将已有对象传入,基于已有的功能,并提供加强功能。那么自定义的该类称为装饰类。

装饰类通常会通过构造方法接收被装饰的对象。并基于被装饰的对象的功能,提供更强的功能。

IO的方式通常分为:BIO(同步阻塞)、NIO(同步非阻塞)、AIO(异步非阻塞),文章下面会有相关介绍。

1.File类

首先我们来看一下File类,java.io.File下代表与平台无关的文件和目录,程序操作文件和目录都可以通过File类来完成,File能新建、删除、重命名文件和目录,但是不能访问文件内容本身。如果需要访问文件内容本身则需要使用输入/输出流。File的常用方法如下:

File file = new File("."); // 以当前路径来创建一个File对象常用方法:file.getName(); // 获取文件名file.getParent(); // 获取相对路径的父路径file.getAbsoluteFile(); // 获取绝对路径file.getAbsoluteFile().getParent(); // 获取上一级路径创建临时文件:File tempFile = File.createTempFile("temp", ".txt", file); // 在当前路径下创建一个临时文件tempFile.deleteOnExit(); // 指定当JVM退出时删除该文件 创建新文件:File newFile = new File(System.currentTimeMillis() + ".txt"); // 以系统当前时间作为新文件名来创建新文件System.out.println(newFile.exists()); // 判断File对象所对应的文件或目录是否存在,存在返回truenewFile.createNewFile(); // 以指定newFile对象来创建一个文件System.out.println(newFile.mkdir()); // 以newFile对象来创建一个目录,因为newFile已经存在,所以下面方法返回false,即无法创建该目录

2.理解Java的IO流

下来看一下IO(输入/输出)的思维导图,来全面认识下IO流:

IO流

使用处理流的思路:使用处理流包装节点流,程序通过处理流来执行输入/输出功能,让节点流与底层IO设备、文件交互。
使用处理流的好处:简单;执行效率更高。
注意:处理流的使用必须建立在其他节点流的基础之上。

Java输入/输出流体系中常用的流分类:
除了这些还有RandomAccessFile,下面将有单独介绍。

这里写图片描述

3.字符流的用法

1.在硬盘上创建一个文件并写入一些文字数据

首先找到一个专门用于操作文件的Writer子类对象。FileWriter。 后缀名是父类名。 前缀名是该流对象的功能。

class  FileWriterTest {    public static void main(String[] args) throws IOException {        //FileWriter对象一被初始化就必须要明确被操作的文件,该文件会被创建到指定目录下。如果已有同名文件,将被覆盖        FileWriter fw = new FileWriter("test.txt");        //调用write方法,将字符串写入到流中        fw.write("abcdef");        //刷新流对象中的缓冲中的数据,将数据刷到目的地中        //fw.flush();        //关闭流资源,但是关闭之前会刷新一次内部的缓冲中的数据,将数据刷到目的地中        //和flush区别:flush刷新后,流可以继续使用,close刷新后,会将流关闭        fw.close();    }}

对已有文件的数据续写

class  FileWriterTest2 {    public static void main(String[] args) throws IOException {        //传递一个true参数,代表不覆盖已有的文件。并在已有文件的末尾处进行数据续写        FileWriter fw = new FileWriter("test.txt",true);        fw.write("halou\r\nnihaoma");        fw.close();    }}

2.文本文件的两种读取方式

class  FileReaderTest {    public static void main(String[] args) throws IOException {        //创建一个文件读取流对象,和指定名称的文件相关联。要保证该文件是存在的,如不存在,会发生FileNotFoundException        FileReader fr = new FileReader("test.txt");        /**         * 第一种方式         */        //调用读取流对象的read方法        //read():一次读一个字符。而且会自动往下读        int ch = 0;        while((ch=fr.read())!=-1) {            System.out.println("ch="+(char)ch);        }        /**         * 第二种方式:通过字符数组进行读取         */        //定义一个字符数组。用于存储读到字符。该read(char[])返回的是读到字符个数        char[] buf = new char[1024];        int num = 0;        while((num=fr.read(buf))!=-1) {            System.out.println(new String(buf,0,num));        }        fr.close();    }}

3.拷贝文本文件

复制的原理:其实就是将C盘下的文件数据存储到D盘的一个文件中
步骤:

在D盘创建一个文件。用于存储C盘文件中的数据;定义读取流和C盘文件关联;通过不断的读写完成数据存储;关闭资源。

为了更加清晰易懂,我把复制过程用图画了出来,是不是就清楚多了呢。

这里写图片描述

class CopyTextTest {    public static void main(String[] args) {        copy();    }    public static void copy() {        FileWriter fw = null;        FileReader fr = null;        try {            fw = new FileWriter("test_copy.txt"); // 创建目的地            fr = new FileReader("test.java"); // 与已有文件关联            char[] buf = new char[1024];            int len = 0;            while((len=fr.read(buf))!=-1) {                fw.write(buf,0,len);            }        } catch (IOException e) {            throw new RuntimeException("读写失败");        } finally {            if(fr!=null)                try {                    fr.close();                } catch (IOException e) {                }            if(fw!=null)                try {                    fw.close();                } catch (IOException e) {                }        }    }//  /**//   * 从C盘读一个字符,就往D盘写一个字符,不建议//   *///  public static void copy()throws IOException {//      FileWriter fw = new FileWriter("test_copy.txt"); //      FileReader fr = new FileReader("test.java");//      int ch = 0;//      while((ch=fr.read())!=-1) {//          fw.write(ch);//      }//      fw.close();//      fr.close();//  }}

4.字符读取流缓冲区

缓冲区的出现是为了提高流的操作效率而出现的,所以在创建缓冲区之前,必须要先有流对象。字符流缓冲区中提供了一个跨平台的换行符:newLine();
字符读取流缓冲区:该缓冲区提供了一个一次读一行的方法readLine,方便于对文本数据的获取。当返回null时,表示读到文件末尾。readLine方法返回的时候只返回回车符之前的数据内容。并不返回回车符。

这里写图片描述

1.创建buffered.txt并写入数据

class  BufferedWriterTest {    public static void main(String[] args) throws IOException {        FileWriter fw = new FileWriter("buffered.txt"); // 创建一个字符写入流对象        // 为了提高字符写入流效率。加入了缓冲技术        // 只要将需要被提高效率的流对象作为参数传递给缓冲区的构造函数即可        BufferedWriter bufw = new BufferedWriter(fw);        for(int x=1; x<5; x++) {            bufw.write("abcdef"+x);            bufw.newLine();            bufw.flush();        }        //bufw.flush(); // 记住,只要用到缓冲区,就要记得刷新        bufw.close(); // 其实关闭缓冲区,就是在关闭缓冲区中的流对象    }}

2.采用字符读取流缓冲区读取数据

class  BufferedReaderTest {    public static void main(String[] args) throws IOException {        FileReader fr = new FileReader("buffered.txt"); //创建一个读取流对象和文件相关联        //为了提高效率。加入缓冲技术。将字符读取流对象作为参数传递给缓冲对象的构造函数        BufferedReader bufr = new BufferedReader(fr);        String line = null;        while((line=bufr.readLine())!=null) {            System.out.print(line);        }        bufr.close();    }}

5.通过缓存区复制文本文件

class  CopyTextByBuffered {    public static void main(String[] args) {        BufferedReader bufr = null;        BufferedWriter bufw = null;        try{            bufr = new BufferedReader(new FileReader("BufferedWriterTest.java"));            bufw = new BufferedWriter(new FileWriter("BufferedWriterTest_copy.txt"));            String line = null;            while((line=bufr.readLine())!=null) {                bufw.write(line);                bufw.newLine();                bufw.flush();            }        } catch (IOException e) {            throw new RuntimeException("读写失败");        } finally {            try {                if(bufr!=null)                    bufr.close();            } catch (IOException e) {                throw new RuntimeException("读取关闭失败");            } try {                if(bufw!=null)                    bufw.close();            } catch (IOException e) {                throw new RuntimeException("写入关闭失败");            }        }    }}

4.字节流的用法

1.字节流File读写操作-复制一张图片

想要操作图片数据,字符流就无法满足需求了,这时就要用到字节流。
复制一张图片的思路:

用字节读取流对象和图片关联;用字节写入流对象创建一个图片文件。用于存储获取到的图片数据;通过循环读写,完成数据的存储;关闭资源;
class  CopyPic {    public static void main(String[] args) {        FileOutputStream fos = null;        FileInputStream fis = null;        try {            fos = new FileOutputStream("d:\\pic_copy.bmp");            fis = new FileInputStream("d:\\pic.bmp");            byte[] buf = new byte[1024];            int len = 0;            while((len=fis.read(buf))!=-1) {                fos.write(buf,0,len);            }        } catch (IOException e) {            throw new RuntimeException("复制文件失败");        } finally {            try {                if(fis!=null)                    fis.close();            } catch (IOException e) {                throw new RuntimeException("读取关闭失败");            } try {                if(fos!=null)                    fos.close();            } catch (IOException e) {                throw new RuntimeException("写入关闭失败");            }        }    }}

2.通过字节流的缓冲区演示mp4的复制

class  CopyMp4 {    public static void main(String[] args) throws IOException {        long start = System.currentTimeMillis();        copy();        long end = System.currentTimeMillis();        System.out.println((end-start)+"毫秒");    }    //通过字节流的缓冲区完成复制。    public static void copy()throws IOException {        BufferedInputStream bufis = new BufferedInputStream(new FileInputStream("c:\\video.Mp4"));        BufferedOutputStream bufos = new BufferedOutputStream(new FileOutputStream("c:\\video_copy.Mp4"));        int by = 0;        while((by=bufis.read())!=-1){            bufos.write(by);        }        bufos.close();        bufis.close();    }}

3.转换流

IO体系只提供了将字节流向字符流转换的转换流,InputStreamReader将字节输入流转换成字符输入流,OutputStreamWriter将字节输出流转换成字节输出流。

下面以获取键盘输入为例来介绍转换流的用法(注意:readLine是字符流BufferedReader中的方法,而键盘录入的read方法是字节流InputStream的):

class  KeyinTest {    public static void main(String[] args) throws IOException {        BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); // 键盘的最常见写法//      BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));        String line = null;        while((line=br.readLine())!=null) {            if(line.equals("exit")) {                System.exit(1); // 程序退出            }            System.out.println("输入内容为:" + line); // 打印输入内容        }        br.close();    }}

5.Properties的用法

Properties是hashtable的子类(它具备map集合的特点),它里面存储的key-value都是字符串,是集合和IO技术相结合的集合容器。
该对象的特点:可以用于key-value形式的配置文件,那么在加载数据时,需要数据有固定格式:key = value。

限制程序运行次数。当运行次数到达5次时,给出,请您注册的提示。并不再让该程序执行。

1.设置和获取元素

class PropertiesTest1 {    public static void main(String[] args) throws IOException {        Properties prop = new Properties();        prop.setProperty("zhangsan","80");        prop.setProperty("lisi","90");        System.out.println(prop);        String value = prop.getProperty("lisi");        System.out.println(value);        prop.setProperty("lisi",100+"");        Set<String> names = prop.stringPropertyNames();        for(String s : names) {            System.out.println(s+":"+prop.getProperty(s));        }        /**         * 运行结果:         * {zhangsan=80, lisi=90}         * 90         * zhangsan:80         * lisi:100         */    }}

2.演示如何将流中的数据存储到集合中

test.txt文本文件内容如下:

#This is a Properties Test#11:43:59zhangsan=80lisi=90wangwu=95

我们开始将test.txt中的key-value数据存储到集合中

class PropertiesDemo {    public static void main(String[] args) throws IOException {        Properties prop = new Properties();        FileInputStream fis = new FileInputStream("test.txt");        //将流中的数据加载进集合。        prop.load(fis);        prop.setProperty("wangwu","60");        System.out.println(prop);        prop.list(System.out);        fis.close();        /**         * 运行结果:         * {zhangsan=80, lisi=90, wangwu=60}         * -- listing properties --         * zhangsan=80         * lisi=90         * wangwu=60         */    }}

6.对象序列化

序列化机制:允许把内存中的Java对象转换成字节序列(与平台无关的二进制流)
为了让某个类是可序列化的,该类必须实现Serializable和Externalizable两接口之一,Java很多类已经实现Serializable(只是一个标记接口,实现该接口无须实现任何方法)。
通常建议:JavaEE中程序的每个JavaBean类都实现Serializable。

1.使对象流实现序列化

下面程序使用ObjectOutputStream将一个对象写入磁盘文件:

class Person implements Serializable {    private String name;    private int age;    // 注意此处没有提供无参数的构造器!    public Person(String name , int age) {        this.name = name;        this.age = age;    }    public String getName() {        return name;    }    public int getAge() {        return age;    }}public class WriteObject {    public static void main(String[] args) {        try(            // 创建一个ObjectOutputStream输出流            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("personObject.txt")))         {            Person per = new Person("zhangsan", 500);            oos.writeObject(per); // 将per对象写入输出流        }        catch (IOException ex) {            ex.printStackTrace();        }    }}

2.反序列化

反序列化读取的仅仅是Java对象的数据,而不是Java类,采用反序列化恢复Java对象必须提供该Java对象所属类的class文件,否则引发ClassNotFoundException异常。
下面简单来实现一下反序列化:

public class ReadObject {    public static void main(String[] args) {        try(            // 创建一个ObjectInputStream输入流            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("personObject.txt")))        {            // 从输入流中读取一个Java对象,并将其强制类型转换为Person类            Person p = (Person)ois.readObject();            System.out.println("名字为:" + p.getName() + "\n年龄为:" + p.getAge());        }        catch (Exception ex) {            ex.printStackTrace();        }        /**         * 运行结果:         * 名字为:zhangsan         * 年龄为:500         */    }}

7.管道流

Java IO中的管道为运行在同一个JVM中的两个线程提供了通信的能力,所以管道也可以作为数据源以及目标媒介。在Java中管道流实现了线程间的数据传送。
注意:当使用两个相关联的管道流时,务必将它们分配给不同的线程,read()方法和write()方法调用时会导致流阻塞,这意味着如果你尝试在一个线程中同时进行读和写,可能会导致线程死锁。

class Read implements Runnable {    private PipedInputStream in; // 管道字节输入流    Read(PipedInputStream in) {        this.in = in;    }    public void run() {        try {            byte[] buf = new byte[1024];            System.out.println("读取前..没有数据阻塞");            int len = in.read(buf);            System.out.println("读到数据..阻塞结束");            String s= new String(buf,0,len);            System.out.println(s);            in.close();        } catch (IOException e) {            throw new RuntimeException("管道读取流失败");        }    }}class Write implements Runnable {    private PipedOutputStream out; // 管道字节输出流    Write(PipedOutputStream out) {        this.out = out;    }    public void run() {        try {            System.out.println("开始写入数据,等待5秒后..");            Thread.sleep(5000);            out.write("piped is here".getBytes());            out.close();        } catch (Exception e) {            throw new RuntimeException("管道输出流失败");        }    }}class  PipedStreamTest {    public static void main(String[] args) throws IOException {        PipedInputStream in = new PipedInputStream(); // 管道字节输入流        PipedOutputStream out = new PipedOutputStream(); // 管道字节输出流        in.connect(out); // 关联        Read r = new Read(in);        Write w = new Write(out);        // 开启两个线程        new Thread(r).start();        new Thread(w).start();    }    /**     * 运行结果:     * 读取前..没有数据阻塞     * 开始写入数据,等待5秒后..     * 读到数据..阻塞结束     * piped is here     */}

8.RandomAccessFile

RandomAccessFile(该类不是算是IO体系中子类,而是直接继承自Object,但是它是IO包中成员)是Java IO体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,既可以读取文件内容,也可以向文件输出数据。与普通IO流不同的是RandomAccessFile支持“随机访问的方式”(内部封装了一个数组,而且通过指针对数组的元素进行操作,可以通过getFilePointer获取指针位置,同时可以通过seek改变指针的位置),程序可以直接跳转到文件的任意地方来读写数据。所以,如果只需要访问文件部分内容,使用RandomAccessFile是更好的选择。
RandomAccessFile完成读写的原理就是内部封装了字节输入流和输出流。

class RandomAccessFileTest {    public static void main(String[] args) throws IOException {        writeFile();        readFile();        System.out.println(Integer.toBinaryString(258)); //以二进制无符号形式返回    }    public static void readFile()throws IOException {        RandomAccessFile raf = new RandomAccessFile("ran.txt","r");        //调整对象中指针        //raf.seek(8*1);        //跳过指定的字节数        raf.skipBytes(8);        byte[] buf = new byte[4];        raf.read(buf);        String name = new String(buf);        int age = raf.readInt();        System.out.println("name="+name);        System.out.println("age="+age);        raf.close();    }    public static void writeFile()throws IOException {        RandomAccessFile raf = new RandomAccessFile("ran.txt","rw");        raf.seek(8*1); // 移动raf的文件记录指针的位置        System.out.println("当前指针的位置是:"+raf.getFilePointer());        raf.write("张三".getBytes());        raf.writeInt(97); // 读用readInt(),97表示ASCII码,代表字符“a”        raf.close();    }    /**     * 运行结果:     * 当前指针的位置是:8     * name=张三     * age=97     * 100000010     */}

9.NIO(同步非阻塞)

前面介绍的BufferedReader时提到一个特征,当BufferedReader读取输入流中的数据时,如果没有读到有效数据,程序将会在此处阻塞该进程的执行(使用InputStream的read()方法从流中读取数据时,如果数据源没有数据,它也会阻塞该线程),也就是说前面介绍的输入流、输出流都是阻塞式的输入、输出。
从JDK1.4开始,Java提供了很多改进IO的新功能,称为新IO(New IO,简称NIO),新增了许多用于处理输入/输出的类(这些类在java.nio包及其子包下)。
从Java7开始,对NIO进行了重大改进,改进主要包括:提供了全面的文件IO和文件系统访问支持;基于异步Channel的IO。
Java7把这种改进称为NIO.2。

10.AIO(异步非阻塞)

从Java7开始,Java增加了AIO新特性,基本上所有的Java服务器都重写了自己的网络框架以通过NIO来提高服务器的性能。目前很多的网络框架(如Mina),大型软件(如Oracle DB)都宣布自己已经在新版本中支持了AIO的特性以提高性能。
下面就来看一下AIO的基本原理:
AIO主要是针对进程在调用IO获取外部数据时,是否阻塞调用进程而言的,一个进程的IO调用步骤大致如下:
1)进程向操作系统请求数据;2)操作系统把外部数据加载到内核的缓冲区中;3)操作系统把内核的缓冲区拷贝到进程的缓冲区,进程获得数据完成自己的功能 。
当操作系统在把外部数据放到进程缓冲区的这段时间(即第2、3步),如果应用进程是挂起等待状态,那么就是同步IO,反之,就是异步IO,也就是AIO。

17 0
原创粉丝点击