Java基础 | IO 流

Java基础 | IO 流

File类

java.io.File 类,该类的一个对象就代表一个文件或目录(文件夹),File 类中涉及关于文件或文件目录的创建、删除、重命名、返回修改时间、文件大小等方法,但不涉及文件的读写,File 类的对象常作为参数传入流对象的构造器中。

构造器

  • File(String filePath)
  • File(String parentPath,String childPath)
  • File(File parentFile,String childPath)

注意

  • 形参可以是文件名或者文件夹的路径,可以是绝对路径或相对路径
  • Windows 和 Dos 使用 “ \ ” 作为路径分隔符,在字符串中要使用转义符
  • Unix 和 URL 使用 “ / ” 作为路径分隔符
  • 可以使用统一的分隔符常量 public static final String separator
File f1 = new File("d:" + File.separator + "github" + File.separator + "github.txt"); 
// d:\github\github.txt  
  • IDEA中,如果使用 JUnit 进行单元测试,相对路径从当前 Module 开始,如果使用 main() 函数进行测试,相对路径从当前 Project 开始

常用方法

获取

public String getAbsolutePublic();
// 获取绝对路径
public String getPath();
// 获取路径
public String getName();
// 获取名称
public String getParent();
// 获取上层文件目录路径,若无,返回 null
public long length();
// 获取文件长度,即字节数,不可获取目录的长度
public long lastModified();
// 获取最后一次修改实践

重命名

public boolean renameTo(File dest);
把文件重命名为指定的文件路径

判断

public boolean isDirectory();
// 判断是否是目录
public boolean isFile();
// 判断是否是文件
public boolean exists();
// 判断是否存在
public boolean canRead();
// 判断是否可读
public boolean canWrite();
// 判断是否可写
public boolean isHidden();
// 判断是否隐藏

创建

public boolean creatNewFile();
// 创建文件,若文件已存在,则不创建,返回 false
public boolean mkdir();
// 创建目录,若目录已存在,则不创建,若此目录上层目录不存在,也不创建

删除

public boolean delete();
// 删除文件或一个空的目录,不走回收站

IO 流原理及流的分类

相对于程序、内存来说,从外围设备读数据到内存为输入,从内存写数据到外围设备为输出

流的分类

  • 按操作数据单位:字节流(每 8 bit(1字节) 一个单位)、字符流(每 16 bit 一个单位)
  • 按数据的流向:输入流、输出流
  • 按流的角色:节点流(直接连接内存和文件)、处理流
  • 字符流:字节流读取文字字节数据后,不直接操作而是先查指定的编码表,获取对应的文字,再对这个文字进行操作,简单说就是字节流+编码表
抽象基类字节流字符流
输入流InputStreamReader
输出流OutputStreamWriter

IO流分类

抽象基类InputStreamOutputStreamReaderWriter
节点流FileInputStreamFileOutputStremFileReaderFileWriter
缓冲流BufferedInputStreamBufferedOutputStremBufferedReaderBufferedWriter

输入输出的标准化过程

输入过程

  1. 创建 File 类的对象,指明读取的数据的来源(要求此文件一定要存在)
  2. 创建相应的输入流对象,将 File 类的对象作为参数,传入流的构造器中
  3. 具体的读入过程,使用 read() 方法,根据字节流或字符流的类型创建相应的 buffer 数组 byte[] 或 char[]
  4. 关闭流资源

输出过程

  1. 创建File类的对象,指明写出的数据的位置(不要求此文件一定要存在)
  2. 创建相应的输出流,将 File 类的对象作为参数,传入流的构造器中
  3. 具体的写出过程,使用 write() 方法
  4. 关闭流资源

说明

程序中出现的异常需要使用 try-catch-finally 处理

节点流(文件流)

  • 输入操作(读),相应的 File 一定要存在,否则就会报 FileNotFoundException
  • 输出操作(写),相应的 File 可以不存在,不会报异常
  • 异常的处理:为了保证流资源一定可以执行关闭操作。需要使用try-catch-finally处理
  • 无参 read() 方法,每次读入一个单位内容,到达文件末尾返回-1,若缓冲区(一个数组)作为参数传入,每次读取尽量装满缓冲区,返回每次读入内容单位的个数,到达文件末尾返回-1
  • 写操作(FileWriter 和 FileOutputStream 同理)
    • File 对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建此文件。
    • File 对应的硬盘中的文件如果存在:
      • 如果流使用的构造器是:FileWriter(file,false) / FileWriter(file):对原文件的覆盖
      • 如果流使用的构造器是:FileWriter(file,true):不会对原文件覆盖,而是在原文件基础上追加内容
  • 对于文本文件(.txt,.java,.c,.cpp),建议使用字符流处理
  • 对于非文本文件(.jpg,.mp3,.mp4,.avi,.doc,.ppt,...),必须使用字节流处理

FileReader 和 FileWriter

FileRead 和 FileWriter 实现问本文件的复制

import java.io.*;

public class FileTest {
    public static void main(String[] args) {
        // 1. 创建 File 类的对象,指明数据来源,路径是相对于 Project 的
        File srcFile = new File("FileIO/hello1.txt");
        File destFile = new File("FileIO/hello2.txt");

        FileReader fr = null;
        FileWriter fw = null;
        try {
            // 2. 创建输入流对象,并将 secFile 作为参数传入构造器
            fr = new FileReader(srcFile);
            // 创建输出流对象,并将 destFile 作为参数传入构造器
            // 如果没有该文件,则新建
            fw = new FileWriter(destFile,true);

            // 字符串用于存储所以文本内容
            StringBuffer str = new StringBuffer();
            // 读取操作的缓冲区
            char[] cbuf = new char[5];
            // 记录本次读取所读的字符个数
            int len;

            // 3. 读入数据同时写出数据
            while((len=fr.read(cbuf))!=-1) {
                str.append(cbuf,0,len);
                fw.write(cbuf,0,len);
                //String s = new String(cbuf,0,len);
                //System.out.print(s);
            }
            System.out.println(str);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 关闭流资源
            try {
                if(fr!=null)
                    fr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if(fw!=null)
                    fw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

FileInputStream 和 FileOutputStream

FileInputStream 和 FileOutputStream 实现视频文件的拷贝,并封装到方法,测定拷贝时间

import java.io.*;

public class FileInputStreamOutputStreamTest {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        copyFile("FileIOStream/视频1.mp4","FileIOStream/视频2.mp4");
        long endTime = System.currentTimeMillis();
        System.out.println("复制完成,花费时间:" + (endTime-startTime)/1000.0 + "秒");
    }

    public static void copyFile(String src,String dest) {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            // 1. 创建 File 的对象,指明数据来源
            File srcFile = new File(src);
            File destFile = new File(dest);

            // 2. 创建字节流对象,传入 File 参数
            fis = new FileInputStream(srcFile);
            fos = new FileOutputStream(destFile);

            // 字节流缓冲区
            byte[] buffer = new byte[1024];
            // 本次读入的字节数
            int len;
            // 3. 边读边写
            while((len=fis.read(buffer))!=-1) {
                fos.write(buffer,0,len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 4. 必须关闭资源流
            try {
                if(fis!=null)
                    fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if(fos!=null)
                    fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

缓冲流

  • 缓冲流是处理流的一种,可以提高流的读取写入的速度,把节点流的对象作为参数传入缓冲流构造器即可
  • 原因:内部提供了一个缓冲区,默认大小 DEFAULT_BUFFER_SIZE 为 8192 字节(8 KB)
  • 在输出过程中可以调用输出流的 flush() 方法手动刷新内置缓冲区,立刻将缓冲区内容写入文件。

BufferedInputStream 和 BufferedOutputStream

使用BufferedInputStream 和 BufferedOutputStream 复制视频

import java.io.*;

public class BufferedTest {
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        copyFile("Buffered/视频1.mp4","Buffered/视频2.mp4");
        long endTime = System.currentTimeMillis();
        System.out.println("复制完成,花费时间:" + (endTime-startTime)/1000.0 + "秒");
    }

    public static void copyFile(String src,String dest) {
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            // 1. 创建 File 对象
            File srcFile = new File(src);
            File destFile = new File(dest);

            // 2. 创建节点流对象
            FileInputStream fis = new FileInputStream(srcFile);
            FileOutputStream fos = new FileOutputStream(destFile);

            // 3. 创建缓冲流对象,并传入其他流作为参数
            bis = new BufferedInputStream(fis);
            bos = new BufferedOutputStream(fos);

            byte[] buffer = new byte[1024];
            int len;

            // 4. 读写细节
            while((len=bis.read(buffer))!=-1) {
                bos.write(buffer,0,len);
            }


        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 5. 关闭流资源
            // 要求先关闭外层的流,再关闭内层的流
            // 关闭外层流的同时,内层流也会自动的进行关闭
            // 所以内层流的关闭可以省略
            try {
                if(bis!=null)
                    bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                if(bis!=null)
                    bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

自己测试拷贝一段 400M 大小的 MP4 视频,使用节点流,三次平均 4.26 s,使用缓冲流,三次平均 1.45 s。

BufferedReader 和 BufferedWriter

BufferedReader 多了 readLine() 方法,即一次性读取一行内容

import java.io.*;

public class BufferedRead {
    public static void main(String[] args) {
        BufferedReader br = null;
        BufferedWriter bw = null;

        // 方法一
        /*
        try {
            br = new BufferedReader(new FileReader(new File("Buffered/hello1.txt")));
            bw = new BufferedWriter(new FileWriter(new File("Buffered/hello2.txt")));

            char[] cbuf = new char[10];
            int len;
            while((len=br.read(cbuf))!=-1) {
                bw.write(cbuf,0,len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(br!=null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(bw!=null) {
                try {
                    bw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        */

        // 方法二:使用readline()方法和newline()方法
        try {
            br = new BufferedReader(new FileReader(new File("Buffered/hello1.txt")));
            bw = new BufferedWriter(new FileWriter(new File("Buffered/hello2.txt")));

            String buffer;
            while((buffer=br.readLine())!=null) {
                bw.write(buffer);
                bw.newLine();
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(br!=null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(bw!=null) {
                try {
                    bw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

转换流

转换流属于字符流、处理流,提供字节流和字符流之间的转换

  • InputStreamReader 用于将一个字节的输入流转换为字符输入流
  • OutputStreamWriter 用于将一个字符的输出流转换为字节的输出流
  • 编码:字节,字节数组--->字符数组、字符串
  • 解码:字符数组、字符串--->字节,字节数组
  • 文件编码的方式(比如:GBK),决定了解析时使用的字符集(也只能是GBK)

转换流

// 将输入的字节流转换成输入的字符流,并指定读入的字符集为 utf-8,然后将内容写入另一个文件,并指定字符集为 gbk
import java.io.*;

public class ConvertTest {
    public static void main(String[] args) {
        InputStreamReader isr = null;
        OutputStreamWriter osw = null;
        try {
            File srcFile = new File("ConvertTest/hello1.txt");
            File destFile = new File("ConvertTest/hello2.txt");
            FileInputStream fis = new FileInputStream(srcFile);
            FileOutputStream fos = new FileOutputStream(destFile);
            isr = new InputStreamReader(fis,"utf-8");
            osw = new OutputStreamWriter(fos,"gbk");
            char[] cbuf = new char[10];
            int len;
            while((len=isr.read(cbuf))!=-1) {
                String str = new String(cbuf,0,len);
                System.out.print(str);
                osw.write(cbuf,0,len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(isr!=null) {
                try {
                    isr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(osw!=null) {
                try {
                    osw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

常见编码集

  • ASCII:美国标准信息交换码,用一个字节的7 位表示
  • ISO8859-1:拉丁码表、欧洲码,表用一个字节的 8 位表示
  • GB2312:中国的中文编码表,最多两个字节编码所有字符
  • GBK:中国的中文编码表升级,融合了更多的中文文字符号,最多两个字节编码
  • Unicode:国际标准码,融合了目前人类使用的所有字符,为每个字符分配唯一的字符码,所有的文字都用两个字节来表示
  • UTF-8:变长的编码方式,可用 1-4 字节来表示一个字符

一般要求从前端到后台到数据库统一编码字符集,UTF-8

标准输入输出流

  • System.in:标准输入流:从键盘输入
  • System.out:标准输出流:显示器/控制台输出
// 不使用 Scanner 实现从键盘输入,转换成大写后打印
import java.io.*;

public class Standard {
    public static void main(String[] args) {
        BufferedReader br = null;
        try {
            InputStreamReader isr = new InputStreamReader(System.in);
            br = new BufferedReader(isr);
            while(true) {
                System.out.println("请输入字符串:");
                String str = br.readLine().toUpperCase();
                if("e".equalsIgnoreCase(str)||"exit".equalsIgnoreCase(str)) {
                    return;
                }
                System.out.println("转换成大写为"+str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(br!=null) {
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

对象流

ObjectInputStream 和 ObjectOutputStream,用于存储和读取基本数据类型对象的处理流,可以把 Java 对象写入数据源,也可从数据源还原对象。

ObjectOutputStream:内存中的对象--->存储中的文件、通过网络传输出去:序列化过程
ObjectInputStream:存储中的文件、通过网络接收过来 --->内存中的对象:反序列化过程

对象序列化机制

对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。//当其它程序获取了这种二进制流,就可以恢复成原来的 Java 对象

序列化:使用 ObjectOutputStream 类保存基本数据类型或对象的机制
反序列化:使用 ObjectInputStream 类读取基本数据类型或对象的机制
不能序列化 static 和 transient 修饰的成员变量

自定义类的序列化

若要一个类可以序列化,必须满足

  • 实现 Serializable 接口,这是一个标识接口,没有需要实现的抽象方法
  • 类中提供一个序列版本号
public static final long serialVersionUID = -136974127607965289L;
  • 除了当前类需要实现 Serializable 接口之外,还必须保证其内部所有属性也必须是可序列化的
import java.io.*;

// 实现 Serializable 接口的自定义类
class Person implements Serializable {
    // 提供序列号,一个全局静态long型常量
    private static final long serialVersionUID = -136974127607965289L;
    // 成员也是可序列化的
    private String name;
    private  int age;

    public Person() {
    }

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

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

public class ObjectStreamTest {
    public static void main(String[] args) {
        String str1 = new String("hello,world");
        Person p = new Person("小明",25);

        ObjectOutputStream oos = null;
        try {
            // 指明保存对象用的文件,并创建一个节点输出流
            FileOutputStream fos = new FileOutputStream(new File("ObjectStream\\hello.dat"));
            // 创建对象输出流
            oos = new ObjectOutputStream(fos);
            // 将 String 输出到文件
            oos.writeObject(str1);
            // 每次输出都要flush
            oos.flush();
            // 将对象输出到文件
            oos.writeObject(p);
            oos.flush();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(oos!=null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        ObjectInputStream ois = null;
        String res1 = null;
        Person res2 = null;
        try {
            FileInputStream fis = new FileInputStream(new File("ObjectStream\\hello.dat"));
            ois = new ObjectInputStream(fis);
            // 强转
            res1 = (String)ois.readObject();
            res2 = (Person)ois.readObject();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if(ois!=null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        System.out.println(res1);
        System.out.println(res2);
    }
}

随机存取文件流

  • java.io.RandomAccessFile 直接继承于 java.lang.Object,实现了 DataInput 和 DataOutput 接口,既可以作为一个输入流,又可以作为一个输出流,支持随机访问,也就是可以直接跳到文件的任意位置进行读写操作
  • RandomAccessFile 作为输出流时,目标文件如果不存在,则在执行过程中自动创建,如果目标文件存在,则会对原文件内容进行覆盖(默认情况下,从头开始覆盖)
  • RandomAccessFile 对象包含一个记录指针,用以标示当前读写位置
// 获取文件记录指针的当前位置
long getFilePointer()

// 将文件记录指针移动到 pos 位置
void seek(long pos)
  • 构造器,注意创建对象时需要指定mode,即访问模式
    • r:以只读方式打开,如果要读的文件不存在不会创建,而是报异常
    • rw:以读写方式打开,如果目标文件不存在,则创建文件
    • rwd:以读写方式打开,同步文件内容的更新
    • rws:以读写方式打开,同步文件内容和元数据的更新
public RandomAccessFile(File file,String mode)
public RandomAccessFile(String name,String mode)
  • 实现文件的复制
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

public class RandomAccessFileTest {
    public static void main(String[] args) {
        File file1 = new File("RandomAccessFileTest/视频1.mp4");
        File file2 = new File("RandomAccessFileTest/视频2.mp4");
        RandomAccessFile raf1 = null;
        RandomAccessFile raf2 = null;
        try {
            raf1 = new RandomAccessFile(file1,"r");
            raf2 = new RandomAccessFile(file2,"rw");
            byte[] buffer = new byte[1024];
            int len;
            while((len=raf1.read(buffer))!=-1) {
                raf2.write(buffer,0,len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(raf1!=null) {
                try {
                    raf1.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(raf2!=null) {
                try {
                    raf2.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
  • 使用文件位置指针实现插入操作
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;

public class RandomAccessFileTest {
    public static void main(String[] args) {
        File file = new File("hello.txt");
        RandomAccessFile raf = null;
        try {
            raf = new RandomAccessFile(file,"rw");
            StringBuilder str = new StringBuilder("hello,world!");
            raf.write(str.toString().getBytes());
            raf.seek(6);
            raf.writeBytes(str.toString());
            System.out.println(raf.getFilePointer()); // 18
            raf.seek(0);
            str = new StringBuilder(raf.readLine()); // hello,hello,world!
            System.out.println(str);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(raf!=null) {
                try {
                    raf.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}