Java I/O系统

1、流家族图谱

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

2、InputStream/OutputStream

2.1 FileInputStream/FileOutputStream

基本的字节流

public class Main {
    public static void main(String[] args) {
        try(FileInputStream in = new FileInputStream("in.txt");
            FileOutputStream out = new FileOutputStream("out.txt")) {
            int len = 0;
            byte[] bytes = new byte[1024];
            while((len = in.read(bytes)) != -1) {
                out.write(bytes, 0, len);
            }
        }  catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.2 FilterInputStream/FilterOutputStream

① BufferedInputStream/BufferedOutputStream

对流使用缓冲区技术,每次向流读取/写入时,不必每次都进行实际的物理读取/写入操作

public class Main {
    public static void main(String[] args) {
        try(BufferedInputStream in = new BufferedInputStream(new FileInputStream("in.txt"));
            BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream("out.txt"))) {
            ...
        }  catch (IOException e) {
            e.printStackTrace();
        }
    }
}
② DataInputStream/DataOutputStream

允许从流读取/写入基本数据类型及字符串(跨平台)

public class Main {
    public static void main(String[] args) {
        try(DataOutputStream out = new DataOutputStream(
                new BufferedOutputStream(
                        new FileOutputStream("data.txt")))) {
            out.writeUTF("PI");
            out.writeDouble(3.14159);
        }  catch (IOException e) {
            e.printStackTrace();
        }
        try(DataInputStream in = new DataInputStream(
                new BufferedInputStream(
                        new FileInputStream("data.txt")))) {
            System.out.println(in.readUTF());
            System.out.println(in.readDouble());
        }  catch (IOException e) {
            e.printStackTrace();
        }
    }
}
③ PrintStream

继承自 FilterOutputStream,用于格式化输出

public class Main {
    public static void main(String[] args) {
        try (PrintStream out = new PrintStream("out.txt")) {
            out.println("Hello World!");
            out.printf("%s %s!", "Hello", "World");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.3 ByteArrayInputStream/ByteArrayOutputStream

对字节数组进行读取/写入,该流不用关闭

public class Main {
    public static void main(String[] args) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        out.write("Hello World!".getBytes());
        ByteArrayInputStream in = new ByteArrayInputStream(out.toByteArray());
        int c = 0;
        while((c = in.read()) != -1) {
            System.out.print((char)c);
        }
    }
}

2.4 ObjectInputStream/ObjectOutputStream

请看 7.1 节

3、Reader/Writer

在使用字符流时,注意保证字符的编码和解码方式的一致性。

3.1 InputStreamReader/OutputStreamWriter

① FileReader/FileWriter

基本的字符流,是 InputStreamReader/OutputStreamWriter 的快捷方式(用流构造对象时,不能使用此快捷方式)

public class Main {
    public static void main(String[] args) {
        try(FileReader in = new FileReader("in.txt");
            FileWriter out = new FileWriter("out.txt");) {
            int len = 0;
            char[] chars = new char[1024];
            while((len = in.read(chars)) != -1) {
                out.write(chars, 0, len);
            }
        }  catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.2 BufferedReader/BufferedWriter

对流使用缓冲区技术,每次向流读取/写入时,不必每次都进行实际的物理读取/写入操作(有readLine方法)

public class Main {
    public static void main(String[] args) {
        try(BufferedReader in = new BufferedReader(new FileReader("in.txt"));
            BufferedWriter out = new BufferedWriter(new FileWriter("out.txt"))) {
            ...
        }  catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3.3 PrintWriter

BufferedWriter 的快捷方式,并且包含格式化的方法

public class Main {
    public static void main(String[] args) {
        try (PrintWriter out = new PrintWriter("out.txt")) {
            ...
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

4、RandomAccessFile

类似于组合使用了 DataInputStream/DataOutputStream,并且可以利用seek()在文件中进行随机访问

public class Main {
    static String file = "data.txt";
    static void display() throws IOException {
        RandomAccessFile rf = new RandomAccessFile(file, "r");
        for (int i = 0; i < 7; i++) {
            System.out.printf("Value %d: %f\n", i, rf.readDouble());
        }
        System.out.println(rf.readUTF());
        rf.close();
    }

    public static void main(String[] args) {
        try (RandomAccessFile rf = new RandomAccessFile(file, "rw")) {
            for (int i = 0; i < 7; i++) {
                rf.writeDouble(i * 1.414);
            }
            rf.writeUTF("The end of the file");
            display();
        } catch (IOException e) {
            e.printStackTrace();
        }

        try (RandomAccessFile rf = new RandomAccessFile(file, "rw")) {
            // double占用8字节,5*8即第5个double的结尾
            rf.seek(5 * 8);
            rf.writeDouble(777.777);
            display();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

5、标准 I/O

System.in 和 System.err 类型为 PrintStream,可以直接使用;System.in 类型为 InputStream,对其包装后才能使用(源自 Java 编程思想 P548,待议:没有包装好像也可以使用)。

① 从标准输入中读取:

public class Main {
    public static void main(String[] args) throws IOException {
        BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
        String line;
        while((line = in.readLine()) != null && line.length() != 0) {
            System.out.println(line);
        }
    }
}

② 将标准输出转换为 PrintWriter:

public class Main {
    public static void main(String[] args) {
        PrintWriter out = new PrintWriter(System.out, true);
        out.println("Hello World!");
    }
}

③ 标准 I/O 重定向:可以使用 System 中的 setIn、setOut 和 setErr,对标准 I/O 进行重定向。

6、新 I/O(NIO)

To Be Continued~

7、对象序列化

Java 的对象序列化将那些实现了Serializable接口的对象转换成一个字节序列,并能够在以后将这个字节序列完全恢复为原来的对象。

7.1 保存和加载序列化对象

使用 ObjectInputStream 的writeObject方法将对象序列化,ObjectOutputStream 的readObject方法将对象反序列化。注意,对象所属的类需要实现 Serializable 接口。

class Employee implements Serializable {
    private String name;
    private double salary;

    public Employee() {}

    public Employee(String name, double salary) {
        this.name = name;
        this.salary = salary;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "name='" + name + '\'' +
                ", salary=" + salary +
                '}';
    }
}

public class Main {
    public static void main(String[] args) {
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data.txt"))) {
            out.writeObject("Hello World!");
            out.writeObject(new Employee("张三", 1800.0));
        } catch (IOException e) {
            e.printStackTrace();
        }

        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.txt"))) {
            String s = (String) in.readObject();;
            Employee e = (Employee) in.readObject();
            System.out.println(s + "\n" + e);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

反序列化时,要保证序列化的对象所属的类在类路径中,否则在类型转换时会抛出 ClassNotFoundException 异常。

7.2 修改默认的序列化机制

① Externalizable 接口

Externalizable接口继承了 Serializable 接口,实现该接口可以对序列化的过程进行控制。

需要注意,Externalizable 对象的序列化和反序列化与 Serializable 不同:

  • Serializable:将对象完全序列化,并且反序列化时不会调用构造器
  • Externalizable:序列化时调用 writeExternal 方法序列化字段,反序列化时首先调用公共的无参构造器,然后调用 readExternal 方法反序列化字段

示例代码:

class Employee implements Externalizable {
    ...
    @Override
    public void writeExternal(ObjectOutput out) throws IOException {
        out.writeObject(name);
//        out.writeObject(salary);
    }

    @Override
    public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
        name = (String)in.readObject();
//        salary = (Double)in.readObject();
    }
}

public class Main {
    public static void main(String[] args) {
        ...
    }
}

如上所示,序列化时对象时不会序列化 salary 字段。

② transient(瞬时)关键字

如果想对 Serializable 对象进行序列化的控制,可以使用transient关键字逐字段的关闭序列化:

private transient double salary;
③ 添加 writeObject 和 readObject 方法

如果不想实现 Externalizable 接口,我们还可以实现 Serializable 接口,并添加(并非覆盖或实现)名为writeObjectreadObject的方法,在序列化或反序列化时就会分别调用这两个方法。

注意,这两个方法必须按照如下格式定义:

private void writeObject(ObjectOutputStream stream) throws IOException {...}

private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {...}

如果想在 writeObject/readObject 方法中使用默认的序列化或反序列化机制,可以在其中调用defaultWriteObject/defaultReadObject方法。

7.3 序列化单例存在的问题

示例代码:

class Demo implements Serializable {
    public static final Demo DEMO;
    static { DEMO = new Demo(); }
    private Demo() {}
}

public class Main {
    public static void main(String[] args) {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        try (ObjectOutputStream out = new ObjectOutputStream(bout)) {
            out.writeObject(Demo.DEMO);
        } catch (IOException e) {
            e.printStackTrace();
        }
        try (ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(bout.toByteArray()))) {
            Demo demo = (Demo) in.readObject();
            System.out.println(demo == Demo.DEMO);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

false

可以发现,Demo 是单例的,但是反序列化得到了一个新的实例对象!

解决方案:添加readResolve方法,它会在反序列化时被调用。

class Demo implements Serializable {
    ...
    protected Object readResolve() {
        return Demo.DEMO;
    }
}

还可以在

7.4 序列号和序列化版本号

序列号:序列化时,每个对象都被赋予一个序列号,相同对象的出现将被存储为对这个对象序列号的引用。

示例代码:

class Employee implements Serializable {...}

public class Main {
    public static void main(String[] args) {
        try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data.txt"))) {
            Employee e = new Employee("张三", 1800.0);
            out.writeObject(e);
            out.writeObject(e);
        } catch (IOException e) {
            e.printStackTrace();
        }

        try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("data.txt"))) {
            Employee e1 = (Employee) in.readObject();
            Employee e2 = (Employee) in.readObject();
            System.out.println(e1 == e2);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

运行结果:

true

可以发现,如果将一个对象序列化两次,那么反序列化将得到两个相同的对象。

序列化版本号:当序列化对象时,对象所属的类也会被存储,其中就包含了序列化版本号,它是字段类型和方法签名的指纹(SHA 计算结果的前 8 个字节)。

当反序列化一个对象时,会拿其指纹与它所属类的当前指纹进行比较,如果不匹配就说明这个类的定义在写出该对象后发生了改变,这将会产生一个异常。

示例代码:

class Employee implements Serializable {
    ...
    public void foo() {}
}

运行结果:

java.io.InvalidClassException: com.company.Employee; local class incompatible: stream classdesc serialVersionUID = 5547933960923146683, local class serialVersionUID = -1727657969676927935

如上所示,我们为 Employee 添加一个方法,然后再进行反序列化,得到 InvalidClassException 异常。根据错误信息可知,这是由序列化版本号不匹配引发的异常。

这种情况下,如果我们想要反序列化成功,就需要在类的定义中手动添加旧版本类的指纹,以此表明它对旧版本兼容:

class Employee implements Serializable {
    ...
    public static final long serialVersionUID = 5547933960923146683L;
    public void foo() {}
}

8、操作文件

To Be Continued~

9、补充内容

9.1 Scanner

public class Main {
    public static void main(String[] args) {
        try (Scanner scanner = new Scanner(System.in)) {
            while(scanner.hasNext()) {
                String line = scanner.nextLine();
                System.out.println(line);
            }
        }
    }
}

9.2 性能比较(待完善)

测试复制 1GB 文件所需的时间:

方法时间
FileInputStream/FileOutputStream9 秒
BufferedInputStream/BufferedOutputStream5.5 秒
RandomAccessFile15.8 秒
使用 FileChannel 的 transferTo/transferFrom 方法3.8 秒

如有错误,欢迎指正。.... .- ...- . .- -. .. -.-. . -.. .- -.-- -.-.--

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值