Surpass Day——Java 缓冲流

50 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第10天,点击查看活动详情

4.2 缓冲流专属

4.2.1 BufferedReader 读文件

  • 带有缓冲区的字符输入流
  • 构造方法只能传入字符流
  • 使用这个流的时候不需要自定义char数组,或者说不需要自定义byte数组,自带缓冲
package com.java.javase.IO;
​
import java.io.BufferedReader;
import java.io.FileReader;
​
public class BufferedReaderTest01 {
    public static void main(String[] args) throws Exception{
        // 创建一个使用默认大小输入缓冲区的缓冲字符输入流
        FileReader reader=new FileReader("E:\01javaSE\test\test.txt");
        // 当一个流的构造方法中需要一个流的时候,这个被传入的流叫做: 节点流
        // 外部负责包装的流叫做包装流,也称做处理流
        // 就当前这个程序来说,FileReader就是一个节点流,BufferedReader就是包装流/处理流
        BufferedReader br=new BufferedReader(reader);
​
        // 读取一个文本行(不带有换行符),返回值是一个字符串,当读取到文件末尾没有数据可以读取时,返回值为null
        String s=null;
        while ((s=br.readLine())!=null){
            System.out.println(s);
        }
     
        // 对于包装流来说,只需要关闭最外层的流就行,里面的节点流会自动关闭
        br.close();
    }
}

在上面使用构造方法创建BufferedReader对象的时候,构造方法中只能传入字符流,那如果我要传入的是FileInputStream文件字节输入流呢?答案肯定是不行的,肯定会报错!怎么解决呢?可以通过转换流转换

4.2.2 InputStreamReader转换流

package com.java.javase.IO;
​
import java.io.*;
​
public class BufferedReaderTest01 {
    public static void main(String[] args) throws Exception{
        // BufferedReader的构造方法中只能传入字符流,但是这里我想要传入一个字节流
        FileInputStream in=new FileInputStream("E:\01javaSE\test\test.txt");
​
        // 通过转换流转换,将字节流转换为字符流
        // in在这里是一个节点流,reader是包装流
        InputStreamReader reader=new InputStreamReader(in);
     
        // 这个构造方法只能传一个字符流,不能传字节流,但是上面我们已经通过转换流转换了
        // reader在这里是一个节点流,br是包装流
        BufferedReader br=new BufferedReader(reader);
     
        String line=null;
        while ((line=br.readLine())!=null){
            System.out.println(line);
        }
     
        // 关闭最外层
        br.close();
    }
}

将代码合并的写法

package com.java.javase.IO;
​
import java.io.*;
​
public class BufferedReaderTest01 {
    public static void main(String[] args) throws Exception{
        BufferedReader br=new BufferedReader(new InputStreamReader(new FileInputStream("E:\01javaSE\test\test.txt")));
​
        String line=null;
        while ((line=br.readLine())!=null){
            System.out.println(line);
        }
     
        // 关闭最外层
        br.close();
    }
}

4.2.3 BufferedWriter读文件

  • 带有缓冲区的字符输出流
  • 用法与BufferedReader相似
package com.java.javase.IO;
​
import java.io.*;
​
public class BufferedWriterTest {
    public static void main(String[] args) throws Exception{
        // 以追加的方式写入
        BufferedWriter bw=new BufferedWriter(new OutputStreamWriter(new FileOutputStream("E:\01javaSE\test\test.txt",true)));
​
        bw.write("today is Monday!");
     
        // 刷新
        bw.flush();
        // 关闭最外层
        bw.close();
    }
}

4.3 数据流专属

4.3.1 DataOutputStream

这个流可以将数据连同数据的类型一并写入文件(该文件不是普通的文本文档)

package com.java.javase.IO;
​
import java.io.DataOutputStream;
import java.io.FileOutputStream;
// DataOutputStream写的文件,只能使用DataOutputStream去读,并且读的时候需要提前知道写入的顺序
// 读的顺序需要和写的顺序一致,才可以正常取出数据
​
public class DataOutputStreamTest01 {
    public static void main(String[] args)throws Exception {
        DataOutputStream dos=new DataOutputStream(new FileOutputStream("E:\01javaSE\test\test1.txt"));
        byte b=100;
        short s=200;
        int i=10;
        long f=44L;
        float g=3.0F;
        double d=3.14;
        boolean sex=false;
        char c='a';
        
​
        // 将数据以及数据的类型一并写入文件当中
        dos.writeByte(b);
        dos.writeShort(s);
        dos.writeInt(i);
        dos.writeLong(f);
        dos.writeFloat(g);
        dos.writeDouble(d);
        dos.writeBoolean(sex);
        dos.writeChar(c);
     
        dos.flush();
        dos.close();
    }
​
}

当我们使用记事本打开写入数据的文件时,会出现乱码,无法正常显示文件(如下图),如果要正常读取我们用DataOutputStream写入的数据,就要用到下面要讲的DataInputStream

4.3.2 DataInputStream 数据字节输入流

  • DataOutputStream写的文件,只能使用DataInputStream去读,并且读的时候你需要提前知道写入的顺序
  • 读的顺序需要和写的顺序一致,才可以正常取出数据
package com.java.javase.IO;
​
import java.io.DataInputStream;
import java.io.FileInputStream;
// DataInputStream ————数据字节输入流
​
public class DataInputStreamTest01 {
    public static void main(String[] args)throws Exception {
        DataInputStream dis =new DataInputStream(new FileInputStream("E:\01javaSE\test\test1.txt"));
        byte a=dis.readByte();
        short b=dis.readShort();
        int c=dis.readInt();
        long d=dis.readLong();
        float e=dis.readFloat();
        double f=dis.readDouble();
        boolean g=dis.readBoolean();
        char h=dis.readChar();
​
        System.out.println(a);
        System.out.println(b);
        System.out.println(c);
        System.out.println(d);
        System.out.println(e);
        System.out.println(f);
        System.out.println(g);
        System.out.println(h);
    }
}

输出结果

100
200
10
44
3.0
3.14
false
a

4.4 PrintStream标准输出流

标准的字节输出流 默认输出到控制台

package com.java.javase.IO;
​
import java.io.PrintStream;
​
public class PrintStreamTest01 {
    public static void main(String[] args) {
        // 联合起来写
        System.out.println("hello world!");
​
        // 分开写
        PrintStream ps=System.out;
        ps.println("helo zhangsan");
        ps.println(100);
     
        // 标准输出流不需要手动关闭
    }
​
}

System这个类中有这样一个属性

因此System.out实际上代表一个PrintStream对象

改变标准输出流的方向

package com.java.javase.IO;
​
​
import java.io.FileOutputStream;
import java.io.PrintStream;
​
public class PrintStreamTest01 {
    public static void main(String[] args) throws Exception{
        // 改变标准输出流的方向,使其输出不再指向控制台,而指向我们指定的文件,需要用到System类的setOut()方法
        PrintStream printStream=new PrintStream(new FileOutputStream("E:\01javaSE\test\test1.txt"));
        System.setOut(printStream);
        System.out.println("hello world");
        System.out.println(999999999);
    }
}
一个好用的日志工具的编写

package com.java.javase.IO;
​
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;
​
/**
    日志工具
 */
public class LogUtil {
    public static void log(String msg) throws Exception {
        try{
            // 指向一个日志文件
            PrintStream out=new PrintStream(new FileOutputStream("E:\01javaSE\test\log.txt",true));
            // 改变输出方向
            System.setOut(out);
            // 当前时间格式化
            Date nowTime=new Date();
            SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
            String strTime=sdf.format(nowTime);
            // 输出到日志文件
            System.out.println(strTime+" : "+msg);
        }catch (FileNotFoundException e){
            e.printStackTrace();
        }
    }
}

测试类

package com.java.javase.IO;
​
public class Test {
    public static void main(String[] args) throws Exception{
        LogUtil.log("用户开始登录!");
        LogUtil.log("用户登陆中!");
        LogUtil.log("用户退出登录!");
    }
}

4.5 File类

  • File类和四大家族没有关系,所以File类不能完成文件的读和写
  • File对象表示文件和目录路径名的抽象表示形式
  • E:\01javaSE\test 是一个File对象
  • E:\01javaSE\test\log.txt 也是一个File对象
  • 一个File对象有可能对应的是目录,也可能是文件

我们需要掌握File类中常用的方法

package com.java.javase.IO;
​
import java.io.File;
​
public class FileTest01 {
    public static void main(String[] args) throws Exception{
        // 创建一个File对象
        File f1=new File("E:\01javaSE\test\log1.txt");
​
        // 判断是否存在
        System.out.println(f1.exists());
     
        // 如果E:\01javaSE\test不存在,则以文件的形式创建出来
        if(!f1.exists()){
            // 以文件形式新建
            f1.createNewFile();
        }
    }
​
}

创建多重目录

package com.java.javase.IO;
​
import java.io.File;
​
public class FileTest01 {
    public static void main(String[] args) throws Exception{
        // 创建一个File对象
        File f1=new File("E:/01javaSE/test/a/b/c/d");
        if (!f1.exists()){
            // 以多重目录的形式创建
            f1.mkdirs();
        }
    }
}

获取文件的父路径

package com.java.javase.IO;
​
import java.io.File;
​
public class FileTest01 {
    public static void main(String[] args) throws Exception {
        // 创建一个File对象
        File f1 = new File("E:\01javaSE\test\log.txt");
        // 获取文件的父路径
        String parentPath = f1.getParent();
        System.out.println(parentPath);
    }
}

获取当前目录下的所有子文件

package com.java.javase.IO;
import java.io.File;
​
/**
​
 * File中的listFiles方法
   */
public class FileTest02 {
    public static void main(String[] args) {
        File f1=new File("E:\01javaSE\test");
            // 获取当前目录下所有的子文件
            File[] files=f1.listFiles();
            for(File file:files){
                System.out.println(file.getAbsolutePath());
            }
    }
}

其它常用方法

package com.java.javase.IO;
​
​
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.Date;
​
public class FileTest02 {
    public static void main(String[] args) {
        File f1=new File("E:\01javaSE\test\log.txt");
        // 获取文件名
        System.out.println("文件名:"+f1.getName());
​
        // 判断是否是一个目录
        System.out.println(f1.isDirectory());
     
        // 判断是否是一个文件
        System.out.println(f1.isFile());
     
        // 获取文件最后一次修改时间
        // 这个毫秒数是从1970年到当前时间的总毫秒数
        long haoMiao=f1.lastModified();
     
        // 将总毫秒数转换成日期
        Date time=new Date(haoMiao);
        SimpleDateFormat sdf=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
        String strTime=sdf.format(time);
        System.out.println(strTime);
     
        // 获取文件大小(多少字节)
        System.out.println(f1.length());
    }
​
}

练习:拷贝目录

package com.java.javase.IO;
​
import java.io.*;
​
public class Copy {
    public static void main(String[] args) {
        // 拷贝源
        String pathName1="E:\01javaSE\02-JavaSE进阶版";
        File srcFile=new File(pathName1);
​
        // 拷贝目标
        String pathName2="E:\01javaSE\dest";
        File destFile=new File(pathName2);
     
        // 调用方法拷贝
        copyDir(pathName1,pathName2,srcFile);
    }
     
    /**
     * 拷贝目录
     * @param srcFile 拷贝源
     */
    private static void copyDir(String pathName1,String pathName2,File srcFile) {
        // 获取拷贝源下面的所有File对象(可能是文件也可能是目录)
        File[] files=srcFile.listFiles();
        if (files==null){
            return;
        }
        for(File file:files){
            // 第一种情况:如果遇到的是一个文件则拷贝
            if(file.isFile()){
                FileInputStream fis=null;
                FileOutputStream fos=null;
                try {
                    int n=pathName1.length();
                    String resultPath=pathName2+file.getAbsolutePath().substring(n);
                    System.out.println(resultPath);
                    File f1=new File(resultPath);
                    if (!f1.exists()){
                        f1.createNewFile();
                    }
                    fis=new FileInputStream(file.getAbsolutePath());
                    fos=new FileOutputStream(resultPath,true);
                    // 一次拷贝1MB
                    byte[]bytes=new byte[1024*1024];
                    int readCount=0;
                    while((readCount=fis.read(bytes))!=-1){
                        fos.write(bytes,0,readCount);
                    }
                    fos.flush();
     
                } catch (FileNotFoundException e) {
                    e.printStackTrace();
                }catch (IOException e){
                    e.printStackTrace();
                }finally {
                    if(fis!=null){
                        try {
                            fis.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    if(fos!=null){
                        try {
                            fos.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
     
            // 第二种情况:如果遇到的是一个文件夹则递归遍历,并且要创建相应的文件夹
            // 创建文件夹目录
            if(file.isDirectory()){
                int n=pathName1.length();
                // 拿到目标目录
                /**
                 * E:\01javaSE\dest\a
                 * E:\01javaSE\dest\a\b
                 * E:\01javaSE\dest\a\b\c
                 * E:\01javaSE\dest\a\b\c\d
                 */
                String resultPath=pathName2+file.getAbsolutePath().substring(n);
                File f1=new File(resultPath);
                if (!f1.exists()){
                    f1.mkdirs();
                }
                // 递归调用
                copyDir(pathName1,pathName2,file);
            }
     
        }
    }
}

4.6 ObjectInputStream和ObjectOutputStream

在介绍ObjectInputStream和ObjectOutputStream之前,我们先来看一下什么是序列化和反序列化

ObjectOutputStream是负责序列化的,ObjectInputStream是负责反序列化的

首先准备一个学生类

package com.java.javase.xuLieHua;
​
import java.io.Serializable;
​
public class Student implements Serializable {
    private  int no;
    private String name;
​
    public Student() {
    }
     
    public Student(int no, String name) {
        this.no = no;
        this.name = name;
    }
     
    public int getNo() {
        return no;
    }
     
    public void setNo(int no) {
        this.no = no;
    }
     
    public String getName() {
        return name;
    }
     
    public void setName(String name) {
        this.name = name;
    }
     
    @Override
    public String toString() {
        return "Student{" +
                "no=" + no +
                ", name='" + name + ''' +
                '}';
    }
​
}

如果你希望某一个属性不参与序列化,可以在该属性前加关键字 transient (表示游离的,不参与序列化)

private  int no;
private transient String name;
序列化

package com.java.javase.xuLieHua;
​
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
// java.io.NotSerializableException: Student对象不支持序列化!
// 参与序列化和反序列化的对象必须实现Serializable接口————只是一个标志接口,起到标识的作用
// Serializable这个标志接口是给java虚拟机参考的,java虚拟机看到这个接口之后,会为该类自动生成一个序列化版本号
public class ObjectOutputStreamTest01 {
    public static void main(String[] args) throws Exception{
        Student s=new Student(110,"张三");
​
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("E:\01javaSE\test\xuliehua.txt"));
        oos.writeObject(s);
     
        oos.flush();
        oos.close();
    }
​
}

反序列化

package com.java.javase.xuLieHua;
​
import java.io.FileInputStream;
import java.io.ObjectInputStream;
// 反序列化public class ObjectInputStreamTest01 {
    public static void main(String[] args)throws Exception {
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream("E:\01javaSE\test\xuliehua.txt"));
        Object obj=ois.readObject();
        System.out.println(obj.toString());
        ois.close();
    }
}

一次性序列化多个对象

package com.java.javase.xuLieHua;
​
import java.io.FileOutputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
// 一次性序列化多个对象
// 可以将对象放到集合当中,序列化集合
// 参与序列化的集合以及集合中的元素都需要实现java.io.Serializable接口public class ObjectOutputStreamTest02 {
    public static void main(String[] args) throws Exception{
        List<Student> studentList=new ArrayList<>();
        studentList.add(new Student(110,"lisi"));
        studentList.add(new Student(111,"zhangsan"));
        studentList.add(new Student(112,"wangwu"));
        ObjectOutputStream oos=new ObjectOutputStream(new FileOutputStream("E:\01javaSE\test\xuliehua.txt"));
        oos.writeObject(studentList);
​
        oos.flush();
        oos.close();
    }
​
}

与之对应的,一次性反序列化多个对象

package com.java.javase.xuLieHua;
​
import java.io.FileInputStream;
import java.io.ObjectInputStream;
import java.util.List;
​
public class ObjectInputStreamTest02 {
    public static void main(String[] args)throws Exception {
        ObjectInputStream ois=new ObjectInputStream(new FileInputStream("E:\01javaSE\test\xuliehua.txt"));
        List<Student> studentList=(List<Student>)ois.readObject();
        for(Student student:studentList){
            System.out.println(student);
        }
​
        ois.close();
    }
​
}

关于序列化版本号的理解

  • 当我们首次编写好一个学生类,使用序列化手段将对象存储到硬盘中,在该过程中,Java虚拟机看到Serializable接口之后,会自动帮我们生成一个序列化版本号
  • 过了很多很多年,Student这个类的源码改动了(比如增加了某个属性),源代码改动之后,需要重新编译,编译之后生成了全新的字节码文件
  • class文件再次运行的时候,java虚拟机生成的序列化版本号也会发生相应的改变
  • 这时再次运行反序列化操作的时候就会报出错误

因此序列化版本号有什么作用呢?可以用来区分类

Java语言中区分类的机制:

1、首先通过类名进行对比,如果类名不一样,则肯定不是同一个类 ​ 2、如果类名一样,再根据序列化版本号进行区分

自动生成序列化版本号的缺点:

一旦代码确定之后,不能进行后续的修改,因为只要修改,必然会重新编译,此时会生成全新的序列化版本号,这个时候java虚拟机会认为这是一个全新的类

结论:

凡是一个类实现了Serializable接口,建议给该类提供一个固定不变的序列化版本号,这样,以后即使这个类改变了,但是版本号不变,java虚拟机会认为是同一个类

private static final long serialVersionUID=1L;

4.7 IO+Properties的联合使用

IO流:文件的读和写

Properties:是一个集合,key和value都是String类型

新建一个classinfo.properties配置文件

name=lisi
password=123456

编写读取代码