05 Java IO

IO部分的知识比较零散,而且一般是配合其它程序来共同完成功能的。为了清楚起见,笔者还是按照知识点逐一列举代码,并写一些自己的评论。

1. 文件类File

文件类File是用来表示文件或者目录路径名的类,在新建、删除、重命名文件,新建、删除、查找目录,以及建立与文件或者目录有关的流时都会用到。

1.1 创建新文件

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        File f = new File("F:" + File.separator + "test.txt");
        f.createNewFile();
    }
}
这里的文件路径名没有像视频中使用\\分隔符,而是用File.separator这个File类中的一个静态属性字符串来作为分隔符。这样可以避免分隔符不兼容问题,而且路径分层更清晰一些。createNewFile()方法要特别注意,只有调用它的File类对象的路径名指向的文件不存在时,才会创建新文件。已有的文件不会被替换。

1.2 删除文件/目录

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //File f = new File("F:" + File.separator + "dir_to_be_deleted");
        File f = new File("F:" + File.separator + "test.txt");
        if(f.exists())
            f.delete();
        else
            System.out.println("File not found.");
    }
}
delete()方法不仅可以删除文件,也可以删除目录,不过这个目录必须是空的。这样做可以减少目录被误删除的可能性,但是也像后面例子里表示的,使得删除目录需要先递归删除文件,使得操作复杂化。

1.3 创建目录

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException 
    {
        File f = new File("F:" + File.separator + "dir_to_be_built");
        f.mkdir();
    }
}

1.4 列出目录中的下级文件和目录

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        File f = new File("F:" + File.separator);
        File[] str = f.listFiles();
        for (int i = 0; i < str.length; i++) 
        {
            System.out.println(str[i]);
        }
    }
}
listFiles()返回一个File类型的数组,数组元素就是要查找的下级文件和目录名。如果调用这个方法的文件类对象f使用的是绝对路径,则返回的也是绝对路径,否则是相对路径。

1.5 列出目录全部内容

这里需要用到递归,程序遇到目录时需要递归地查找其下级文件和目录。
import java.io.*;
class Test
{
    public static void main(String[] args) 
    {
        File f = new File("F:" + File.separator);
    }
    //递归开始
    public static void print(File f)
    {
        if(f != null)
        {
            if(f.isDirectory())     //判断为目录则递归查找所有下级文件和目录
            {
                File[] fileArr = f.listFiles();
                if(fileArr != null)
                {
                    for (int i = 0; i < fileArr.length; i++) 
                    {
                        //递归调用
                        print(fileArr[i]);
                    }
                }
            }
            else
            {
                System.out.println(f);
            }
        }
    }
}
递归时要特别注意排除引用为空的情况。当发生IO错误时listFiles()方法会返回null值。这里不能像前面程序在main方法上抛出异常,只能选择性地忽略null值继续查找直到所有文件和目录都查找完毕。

2. 文件字符流类 FileReader/FileWriter

2.1 向文件写入数据

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException 
    {
        //第二个参数默认为false,覆盖原文件。若为true,追加在文件后。   
        File f = new File("F:" + File.separator + "test.txt");
        Writer out = new FileWriter(f);  //向下转型
        String str = "hello";           
        out.write(str); //接收字符,字符数组或字符串作为参数
        out.close();    //关闭流,将缓冲区内容输出到文件
    }
}
如果使用FileWriter类的另一个构造方法,可以更简便写作:Writer out = new FileWriter("F:" + File.separator + "test.txt");

2.2 从文件中读取数据,无缓冲区

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        Reader r = new FileReader("F:" + File.separator + "test" + File.separator + "test2.java");
        int c;
        //read方法返回当前读到的单个字符的ASCII码,用int型表示
        while((c = r.read()) !=  - 1)       //读到 - 1表示结尾
        {
            System.out.print((char) c);     //显示字符需要将ASCII码转换回字符类型
        }
        r.close();
    }
}

2.3 从文件中读取数据,有缓冲区

class Test
{
    public static void main(String[] args) throws IOException
    {
        Reader r = new FileReader("F:" + File.separator + "test" + File.separator + "test2.java");
        char[] buf = new char[100]; //读缓冲区
        int len = 0;
        //read方法在有数据写入缓冲区时返回写入数据的大小,没有数据写入缓冲区时(说明已到末尾)返回 -1
        while((len = r.read(buf)) !=  - 1)  
        {
            System.out.print(new String(buf, 0, len));
            System.out.println(len);
        }
        r.close();
    }
}
2.2和2.3两个例子涉及了两个重载的read方法。无参read方法返回的是当前读到的字符的ASCII码,-1为结尾。有参read方法接收缓冲区引用作为参数,返回的是当次读取所占缓冲区的大小,也是 -1为结尾。为提高效率,应尽量用缓冲区。

3. 文件字节流类FileInputStream/FileOutputStream

3.1 向文件写入数据

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        OutputStream o = new FileOutputStream("F:" + File.separator + "test.txt");  //加上第二个参数true则会追加,否则覆盖
        String str = "text";
        byte[] arr = str.getBytes();
        o.write(arr[0]);
        o.write(str.getBytes());    //接收字节或字节数组作为参数
        o.flush();                  //将写缓冲区内容输出到文件
        o.close();
    }
}

3.2 从文件中读取数据,无缓冲区

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        InputStream in = new FileInputStream("F:" + File.separator + "test.txt");
        int Byte = 0;
        while((Byte = in.read()) !=  - 1)   //无参read方法返回int型数据来表示当前读到的字节
        {
            System.out.print((char)Byte);   //如果是文本,字节数据是字符的ASCII码
        }
        in.close();
    }
}

3.3 从文件中读取数据,有缓冲区

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        InputStream in = new FileInputStream("F:" + File.separator + "test.txt");
        byte[] buf = new byte[256];         //字节数组作缓冲区
        int len = 0;                    
        while((len = in.read(buf)) !=  - 1) //有参read方法返回读完当前缓冲区后读取的长度
        {
            System.out.print(new String(buf, 0, len));
        }
        in.close();
    }
}

3. 字符流与字节流的比较

字节流一次读入或输出的是一个字节的数据,8位,而字符流是两个字节16位。字符流的输出需要用到缓冲区,所以在close()方法前要用到flush()方法,否则输出不完全。而字节流的输出不需要缓冲区,因而使用字节流输出字节数据是即时的。字节流可以用来读取硬盘中所有类型的文件,而字符流只能用于文本文件。所以在读写文件时要根据文件类型来决定使用哪一种流。

4. 转换流InputStreamReader/OutputStreamWriter

上面总结的文件字符流/字节流都是程序与文件直接交互传输时用到的流,成为节点流。另一类流并不直接与文件接触,而是像包装一样包住文件流,对其进行处理并利用自身与程序交互。包装流有很多类,包括转换流,缓冲流,数据操作流,合并流,Object流等。
转换流是沟通字符流和字节流的桥梁,使得我们可以将从硬盘/内存读入的字节流转换成易于处理的字符流,处理完毕后转回字节流写回硬盘/内存。

4.1 输出字符流转换为字节流

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //转换流作为文件字节输出流的包装被赋值给字符输出流
        Writer w = new OutputStreamWriter(new FileOutputStream("F:" + File.separator + "test.txt"));
        //后面的操作与字符输出流相同
        String str = "text";
        w.write(str);
        w.flush();                
        w.close();
    }
}
这里也用到了向下转型。OutputStreamWriter是Writer类的子类。

4.2 输入字节流转换为字符流

import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //转换流作为文件字节输入流的包装被赋值给字符输入流
        Reader in = new InputStreamReader(new FileInputStream("F:" + File.separator + "test.txt"));
        char[] buf = new char[256];         //字符数组作缓冲区
        int len = 0;                    
        while((len = in.read(buf)) !=  - 1) //有参read方法返回读完当前缓冲区后读取的长度
        {
            System.out.print(new String(buf, 0, len));
        }
        in.close();
    }
}
InputStreamReader是Reader类的子类。

5. 缓冲流类BufferedReader, BufferedWriter, BufferedInputStream, BufferedOutputStream

为了提高传输效率,可以使用四种缓冲流。缓冲流也是一种包装流,用于连接程序与节点流。如果单纯使用节点流,每次读写请求都会使得CPU中断以便程序访问硬盘,这样效率是很低的。而使用缓冲流,可以利用缓冲流中的缓冲区进行批量读写,提高效率。

5.1 字符缓冲流

BufferedReader类用于字符输入缓冲,继承于Reader类。BufferedWriter类用于字符输出缓冲,继承于Writer类。所以可以用字符节点流的方法来操作字符缓冲流。
import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //标准输入流是字节流,需要先转换成字符流再连接字符缓冲流
        BufferedReader bufr = new BufferedReader(new InputStreamReader(System.in));
        //标准输出流是字节流,需要先转换成字符流再连接字符缓冲流
        BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(System.out));
        String str = null;
        while((str = bufr.readLine()) != null)  //一次读取一行
        {
            if(str.equals("end"))   //从屏幕输入时要自定义结束标志
                break;
            bufw.write(str);        
            bufw.write("\n");       //或者用bufw.newLine()来输入行分隔符
            bufw.flush();           
        }
        bufr.close();
        bufw.close();
    }
}
两种字符缓冲流有8192字符的缓冲区。BufferedReader在读取文本文件时,会先尽量从文件中读入字符并置入缓冲区,之后调用read()方法,会先从缓冲区中读取。如果缓冲区数据不足,才会从文件中读取。BufferedWriter在写入文件时,会将数据先写入缓冲区,缓冲区满后再写入到文件。

5.2 字节缓冲流

BufferedInputStream用于字节输入缓冲,继承于FilterInputStream类,后者继承于InputStream类。BufferedOutputStream用于字节输出缓冲,继承于FilterOutputStream类,后者继承于OutputStream类。所以可以用字节节点流的方法操作字节缓冲流。可以用节点流和缓冲流实现文件复制,比如复制一张图片:
import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //原文件
        File src = new File("E:" + File.separator + "src.jpg");
        //复制到
        File dest = new File("F:" + File.separator + "dest.jpg");
        //用字节输入缓冲流包装字节输入节点流
        BufferedInputStream bufi = new BufferedInputStream(new FileInputStream(src));
        //用字节输出缓冲流包装字节输出节点流
        BufferedOutputStream bufo = new BufferedOutputStream(new FileOutputStream(dest));
        //暂存读到的字节
        int tmp = 0;
        //循环读写直到遇到文件结束标志
        while((tmp = bufi.read()) !=  - 1)
        {
            bufo.write(tmp);
        }
        bufi.close();
        bufo.close();
    }
}

6.其它包装流

6.1 数据操作流DataOutputStream, DataInputStream类

数据操作流可以以机器无关的方式读写基本类型的数据, 包括byte类型和byte数组类型。这两个类直接继承于Object类,故不可用字节流的方法操作,而是由专门的方法。
import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //数据输出流包装了文件输出流以便向其写入基本类型数据
        DataOutputStream out = new DataOutputStream(new FileOutputStream("F:" + File.separator + "test.txt"));

        //用一个字节写入boolean值
        boolean b = true;
        out.writeBoolean(b);
        //用一个字节写入byte值
        byte B = 2;
        out.writeByte(B);
        //用两个字节写入char值
        char c = 'A';
        out.writeChar(c);
        //用四个字节写入int值
        int i = 0;
        out.writeInt(i);
        //用四个字节写入float值
        float f = 2.0f;
        out.writeFloat(f);
        //用八个字节写入double值
        double d = 2.0;
        out.writeDouble(d);

        out.close();
    }
}
用DataInputStream包装字节输入节点流可以读出这些基本类型数据:
import java.io.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //数据输入流包装了文件输入流以便从其读入基本类型数据
        DataInputStream in = new DataInputStream(new FileInputStream("F:" + File.separator + "test.txt"));

        //用一个字节读入boolean值
        boolean b = in.readBoolean();
        System.out.println(b);
        //用一个字节读入byte值
        byte B = in.readByte();
        System.out.println(B);
        //用两个字节读入char值
        char c = in.readChar();
        System.out.println(c);
        //用四个字节读入int值
        int i = in.readInt();
        System.out.println(i);
        //用四个字节读入float值
        float f = in.readFloat();
        System.out.println(f);
        //用八个字节读入double值
        double d = in.readDouble();
        System.out.println(d);

        in.close();
    }
}
输出:
true
2
A
0
2.0
2.0

6.2 逻辑串联流SequenceInputStream

该包装流可以将有序集合中的多个节点输入流按照顺序合并组成新的输入流。有续集合中如果只有不超过两个节点输入流,可以将它们在SequenceInputStream的构造方法中作为参数指定,否则需要建立枚举类去包含一个含有这些节点流的框架类,将枚举类对象作为参数输入。
SequenceInputStream继承于InputStream类,故可以使用read()等方法用串联流读取串联后的数据:
import java.io.*;
import java.util.*;
class Test
{
    public static void main(String[] args) throws IOException
    {
        //创建串联流
        SequenceInputStream sis = null;
        //创建输出流
        BufferedOutputStream bufo = null;
        //创建Vector框架, 将要合并的节点输入流加入框架中
        Vector<InputStream> v = new Vector<InputStream>();
        //新的流依次添加到Vector的末尾
        v.addElement(new FileInputStream("F:" + File.separator + "sub1.txt"));
        v.addElement(new FileInputStream("F:" + File.separator + "sub2.txt"));
        v.addElement(new FileInputStream("F:" + File.separator + "sub3.txt"));
        //返回Vector的枚举给枚举接口用于生成枚举对象
        Enumeration<InputStream> e = v.elements();

        //串联流来自于枚举对象
        sis = new SequenceInputStream(e);
        //输出流输出到指定文件
        bufo = new BufferedOutputStream(new FileOutputStream("F:" + File.separator + "sum.txt"));

        //字节缓冲区
        byte[] buf = new byte[1024];
        //当前一次读取的数据量
        int len = 0;
        //读取合并后的流并写入目标文件
        while((len = sis.read(buf)) !=  - 1)
        {
            bufo.write(buf, 0, len);
            bufo.flush();       //输出缓冲区的内容
        }
    }
}

6.3 对象序列化流ObjectInputStream, ObjectOutputStream

对象序列化流可以实现对象和二进制数据流的互相转化。当然这要求对象所属类实现Serializable接口以便具有序列化的能力。注意Serializable接口没有任何字段或方法,单纯用作可序列化的标志。
import java.io.*;
public class Test
{
    public static void main(String[] args) throws Exception
    {
        Student stu1 = new Student("Jack", 21);
        Student stu2 = new Student("Tom", 22);
        //序列化流包装文件输出流以便将序列化的Student类对象输出到文件
        ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream("F:" + File.separator + "object.txt"));
        //输出对象
        oo.writeObject(stu1);
        oo.writeObject(stu2);
        //关闭流
        oo.close();
        //序列化流包装文件输入流以便将序列化的Student类对象信息从文件读入到对应引用
        ObjectInputStream oi = new ObjectInputStream(new FileInputStream("F:" + File.separator + "object.txt"));
        //读入对象, readObject()方法返回Object类型,必须强制转换
        Student r1 = (Student) oi.readObject();
        Student r2 = (Student) oi.readObject();
        //显示读入的内容
        System.out.println(r1);
        System.out.println(r2);
        //关闭流
        oi.close();
    }
}

class Student implements Serializable   //实现序列化接口,具有序列化能力
{
    String name = null;
    int age = 0;

    public Student(String name, int age)
    {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString()
    {
        return "name : " + name + ", age : " + age;
    }
}
输出:
name : Jack, age : 21
name : Tom, age : 22

6.4 打印流PrintStream

PrintStream继承于FilterOutputStream类,后者继承于OutputStream类。我们经常用到的System.out.println()语句其实就是在取得out这个System类中的指向PrintStream对象的引用后,调用PrintStream类的println方法来实现的。默认情况下这个out引用指向的PrintStream对象会将流打印在屏幕上。我们可以修改out的指向,让它指向一个我们自定义的PrintStream对象以便重定向标准输出:
import java.io.*;
public class Test
{
    public static void main(String[] args) throws IOException
    {
        //此时输出到屏幕
        System.out.println("screen");
        //out是个static类型的引用,只需修改一次, 将重定向目标作为构造参数传给PrintStream构造方法
        System.setOut(new PrintStream(new FileOutputStream("F:" + File.separator + "redirect.txt")));
        System.out.println("redirected to file");
    }
}

7. 总结

IO这部分的知识还是较好整理的,关键要掌握各个类之间的父子关系,也可以根据流的名字来推断。另外就是要多比较易混淆的重名的方法,它们往往有重载关系,比如有参和无参的read()方法。另一个要注意的地方是异常处理,一定要抛出或者捕获IOException。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章