天天看点

Java基础(五)——异常机制、IO类的使用与原理前言异常机制IO流彩蛋

版本 说明 发布日期
1.0 发布文章第一版 2020-12-04
1.1 【死缠烂打的finally!】小节中新增一个更复杂的例子 2021-02-03
新增小节【关于read的返回值为什么是int】
【文件访问流】的常用方法中新增以File为参数的构造方法

文章目录

  • 前言
  • 异常机制
    • 基本概念
    • 异常的分类
    • 死缠烂打的finally!
      • 先来个简单的
      • 再来个小学二年级难度的
    • 自定义异常类
  • IO流
    • IO流的亲戚——File类
      • 常用方法
    • IO流基本概念
    • 基本分类
    • IO大家族
    • 文件访问流
      • FileWriter
        • 常用方法
      • FileReader
        • 常用方法
        • 关于read的返回值为什么是int
      • FileOutputStream、FileInputStream
        • 文件拷贝案例
          • 第一种方式
          • 第二种方式
          • 第三种方式
    • 缓冲流
      • BufferedOutputStream
      • BufferedInputStream
      • 用缓冲流拷贝文件
      • BufferedWriter、BufferedReader
    • 打印流——PrintStream、PrintWriter
    • 转换流——OutputStreanWriter、InputStreamReader
      • 举栗子:模拟聊天日志功能
    • 数据流——DataOutputStream、DataInputStream
    • 对象流——ObjectOutputStream、ObjectInputStream
      • 说到对象流,就得说一说序列化
        • what is 序列化?
        • 序列化版本号
        • transient
      • 啃一块栗子
    • RandomAccessFile
      • 基本概念
      • 常用方法
      • 栗子↓
  • 彩蛋

前言

  • 这篇文章是我个人的学习笔记,可能无法做到面面俱到,也可能会有各种纰漏。如果任何疑惑的地方,欢迎一起讨论~
  • 如果想完整阅读这个系列的文章,欢迎关注我的专栏《Java基础系列文章》~
  • 哦对了!请不要吝啬->点赞、关注、收藏~

异常机制

基本概念

  • 异常机制可以说是大家日常接触最多的概念之一了,所以就只说重点了。
  • 所有异常类的超类时Exception,位于java.lang.Exception。
  • 异常类Exception继承自java.lang.Throwable,同样继承Throwable的还有Error。
    • Error通常描述JVM无法解决的严重错误,无法通过编码解决。例如JVM崩溃了。
    • Exception可以通过编码解决。
  • 异常的三种处理方式:
    • 通过条件语句,避免异常的发生;
    • try/catch/finally语句处理发生的异常;
    • throw抛出异常。

异常的分类

  • java.lang.Exception的子类:
    • RuntimeException:运行时异常,也叫非检测异常。编译阶段无法检测出来是否会发生此类异常。主要子类:
      • ArithmeticException:算术异常
      • ArrayIndexOutOfBoundsException:数组下标越界异常
      • NullPointerException:空指针异常
      • ClassCastException:类型转换异常
      • NumberFormatException:数字格式异常
    • IOException和其他异常:检测性异常,编译阶段能够被编译器(或者说现在通常都能被IDE检测)检测出来的异常。

死缠烂打的finally!

先来个简单的

  • 大家都知道finally是无论有没有异常都会执行的,但是你真的知道这家伙有多么死缠烂打么?比如下面这个例子,小伙伴们觉得结果是什么?
public class ExceptionTest {
    public static void main(String[] args) {
        System.out.println(finallyTest());
    }

    private static String finallyTest(){
        String a = "aAa";
        try{
            System.out.println(1);
            int b = 1/0;
            System.out.println(2);
        }catch (ArithmeticException e){
            System.out.println(3);
            return a = a.toUpperCase();
        }finally {
            System.out.println(4);
            return a = a.substring(1);
        }
    }
}
           
  • 结果如下。怎么样,有没有感受到finally的死缠烂打?
    • catch中return准备返回了,甚至

      a = a.toUpperCase()

      都执行完了,然后finally突然吼了一嗓子:桥豆麻袋!
    • 然后finally里面正常执行,甚至还return了。
    • 只留下catch里面的return,在原地傻了~
1
3
4
AA
           

再来个小学二年级难度的

  • 下面这个执行结果是多少?
public class ExceptionTest {
    public static void main(String[] args) {
        System.out.println(finallyTest());
    }

    private static int finallyTest(){
        int x = 1;
        try {
            return ++x;
        } catch (Exception e) {
        } finally {
            ++x;
        }
        return x;
    }
}
           
  • 答案是2。我知道大多数人都答错了哈哈哈。下面来解释一下。
    1. 执行

      return ++x;

      ,++x已经执行完了此时x为2,但是发现有finally,于是跑去执行finally;
    2. 执行finally,x变为3。因为finally并没有返回,所以执行完之后,又跑回去执行

      return ++x;

    3. 注意了,return返回的是表达式的运算结果,而不是x的值。而

      return ++x;

      的结果在步骤1已经算出来了,是2!所以返回的结果是2,而不是3。
    4. 可能还是有小伙伴十脸懵逼。我觉得可以如下理解。也就是说,return的表达式的值算出来了,固定了,不会再改变了。
    public class ExceptionTest {
        public static void main(String[] args) {
            System.out.println(finallyTest());
        }
    
        private static int finallyTest(){
            int x = 1, y;
            try {
                return y = ++x;
            } catch (Exception e) {
            } finally {
                ++x;
            }
            return x;
        }
    }
               

自定义异常类

  • 对于Java官方没有提供的异常类,但业务场景需要对此类异常进行区分,就需要小伙伴们自己编写。
  • 自定义异常很简单,只需要以下步骤:
    • 继承Exception类或其子类;
    • 编写一个无参构造和带有String的有参构造。其中String是用来记录错误描述的。
    • 两个构造方法体中只需要调用父类的对应构造方法即可。
    • 定义序列化版本号serialVersionUID。至于这玩意儿是什么东西,等讲IO的时候再说~
  • 举例如下:
public class CustomException extends Exception {
    static final long serialVersionUID = -3387516993124229948L;

    public CustomException(){
        super();
    }

    public CustomException(String message){
        super(message);
    }

    public static void main(String[] args) {
        try{
            throw new CustomException("抛出去啦~");
        } catch (CustomException e) {
            e.printStackTrace();
        }
    }
}
           
  • 运行结果如下。
com.UsefulNativeClass.Exception.CustomException: 抛出去啦~
	at com.UsefulNativeClass.Exception.CustomException.main(CustomException.java:16)
           

IO流

IO流的亲戚——File类

  • 位于java.io.File。主要用于描述文件或目录路径的特征信息, 如:大小、文件名等。
  • 流相关的类其实在实例化的时候都是用到了File类的。

常用方法

方法声明 功能
File(String pathname) 根据指定的路径构造文件或者目录对象
File(String parent, String child) 根据指定的父路径和子路径构造文件或者目录对象
File(File parent, String child) 根据指定的父文件路径和子路径构造文件或者目录对象
boolean exists() 判断此路径表示的文件或目录是否存在
String getName() 获取文件或者目录的名称
long length() 返回文件的长度,单位是byte
long lastModified() 获取文件的最后一次修改时间。是long类型的时间戳
String getAbsolutePath() 获取绝对路径
boolean delete() 删除文件或目录,当删除目录时要求是空目录。删除成功则返回true。
boolean createNewFile() 创建新的空文件。创建成功则返回true。
boolean mkdir() 用于创建单级目录。当创建d:/A/B时,如果A不存在,则创建失败。
boolean mkdirs() 用于创建多级目录当创建d:/A/B时,如果A不存在,则A和B一起创建。
File[] listFiles() 获取该目录下的所有文件和目录。如果调用对象是文件,则会返回null。
boolean isFile() 判断是否为文件。是文件则返回true。
boolean isDirectory() 判断是否为目录。是目录则返回true。
File[] listFiles(FileFilter filter) 获取目录下满足筛选器的所有内容。FileFilter是一个接口类,定义了抽象方法accept,该方法要求想要的文件返回true,想被过滤的文件返回false。
  • 这些方法的使用还是说得比较清楚的,下面就实现一个遍历某目录下所有目录和java文件的例子吧:
public class FileTest {
    //实现过滤器,只寻找目录和.java文件
    private final FileFilter fileFilter = pathname -> {
        if(pathname.isFile()){
            return pathname.getName().endsWith(".java");
        }
        return true;
    };

    public static void main(String[] args) {
        FileTest fileTest = new FileTest();
        fileTest.listAllFile(new File("C:\\Users\\米\\Desktop\\模块四 Java核心类库(下)\\01任务一 异常机制和File类"));
    }

    private void listAllFile(File file){
        if(file.isFile()){
            return;
        }
        
        File[] listFile = file.listFiles(fileFilter);
        for (File f:listFile){
            if(f.isFile()){
                System.out.println(f.getName());
            }else if(f.isDirectory()){
                System.out.println("[" + f.getName() + "]");
                listAllFile(f);
            }
        }
    }
}
           
  • 运行结果如下。当然每个人电脑里面的文件不一样,所以执行结果也没啥参考价值。
[01_课件]
[02_图片]
[04_代码]
AgeException.java
ExceptionCatchTest.java
ExceptionFinallyTest.java
ExceptionMethod.java
ExceptionPreventTest.java
ExceptionTest.java
ExceptionThrowsTest.java
FileTest.java
Person.java
PersonTest.java
SubExceptionMethod.java
           

IO流基本概念

  • 上面以迅雷不及掩耳盗铃之势讲完了异常机制,然后顺道还擦边讲了个File。接下来才是这篇文章的主角——IO stream。
  • IO相关类都位于java.io包。
  • 为什么叫IO流呢?首先IO是in和out的意思,这个大家应该都知道,至于stream,可能java的coder们觉得数据能有几多愁,恰似一江春水向东流吧~
  • 不开玩笑了,其实流实例化的过程,就像是给水桶中插了根水管;对流进行读入、写出的过程,就像是给水桶抽水、加水的过程;刷新流,就像是把水管中残留的水分给清干净;关闭流,就像是把水管从水桶中抽出来。

基本分类

  • 按照读写数据的基本单位不同,分为字节流和字符流。
    • 字节流:主要指以字节为单位进行数据读写的流,可以读写任意类型的文件。
    • 字符流:主要指以字符(2个字节)为单位进行数据读写的流,只能读写文本(字符)文件。
  • 按照读写数据的方向不同,分为输入流和输出流。
    • 输入流:主要指从文件中读取数据内容输入到程序中,也就是读文件。
    • 输出流:主要指将程序中的数据内容输出到文件中,也就是写文件。
  • 按照流的角色不同分为节点流和处理流。
    • 节点流:主要指和输入输出源直接对接的流。
    • 处理流:主要指建立在节点流的基础之上的流。处理流与文件是间接关联的。

IO大家族

分类 字节输入流 字节输岀流 字符输入流 字符输岀流
抽象基类 InputStream OutputStream Reader Writer
访问文件 FileInputStream FileOutputStream FileReader FileWriter
访问数组 ByteArrayInputStream ByteArrayOutpuStream CharArrayReader CharArrayWriter
访问管道 PipedlnputStream PipedOutputStream PipedReader PipedWriter
访问字符串 StringReader StringWriter
缓冲流 BufferedlnputStream BufferedOutputStream BufferedReader BufferedWriter
转换流 InputStreamReader OutputStreamWriter
对象流 ObjectInputStream ObjectOutputStream
过滤流 FilterInputStream FiIterOutputStream FilterReader FilterWriter
打印流 PrintStream PrintWriter
推回输入流 PushbackinputStream PushbackReader
特殊流 DatalnputStream DataOutputStream

文件访问流

FileWriter

  • 主要用于对文件进行数据写入。

常用方法

  • 如果构造流对象的时候,文件不存在,则会自动创建对应文件。
方法声明 功能
FileWriter(String fileName) 根据参数指定的路径构造对象。实例化之后,会清空文件原有数据。
FileWriter(File file) 根据参数指定的文件构造对象。实例化之后,会清空文件原有数据。
FileWriter(String fileName, boolean append) 根据参数指定的路径来构造对象。传入true时,表示不清空原有数据,写出数据追加在末尾。
void write(int c) 写入单个字符。如果传入的是整数,则代表的是Unicode值。
void write(char[] cbuf, int off, int len) 将指定字符数组,从下标off开始的,len个字符写入文件。
void write(char[] cbuf) 将指定字符数组写入文件。
void flush() 刷新流。因为各种机制的原因,write操作并不一定会立即写入数据。而刷新的作用就是将目前打算写入,但还未写入的数据,全部立即写入。
void close() 关闭流对象并释放有关的资源。关闭流自带刷新流的功能。
  • 老规矩,栗子来了:
public class FileWriterTest {
    public static void main(String[] args) {
        FileWriterTest test = new FileWriterTest();
        test.baseUse();
    }

    private void baseUse(){
        FileWriter fileWriter = null;
        try {
            fileWriter = new FileWriter("D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\1.txt");
            fileWriter.write('a');
            String str = "啊哦一";
            fileWriter.write(str, 1, 2);
            System.out.println("执行成功");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(fileWriter != null){
                try {
                    fileWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        try {
            fileWriter = new FileWriter("D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\1.txt", true);
            fileWriter.write('a');
            String str = "啊哦一";
            fileWriter.write(str, 1, 2);
            System.out.println("执行成功");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(fileWriter != null){
                try {
                    fileWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
           
  • 文件内容如下。为了方便演示,创建了两次流操作。可以看到,不管代码执行多少次,文件里面的内容始终都是一样的。因为第一次的流操作,会清空文件数据。
a哦一a哦一
           

FileReader

  • 主要用于对文件内容进行读操作。

常用方法

方法声明 功能
FileReader(String fileName) 根据参数指定路径构造对象。
FileReader(File file) 根据参数指定文件构造对象。
int read() 读取单个字符的数据并返回,返回-1表示一个字符都没读到。
int read(char[] cbuf, int offset, int length) 将最多length个字符的数据读入一个字符数组中,从下标位置offset开始放(注意不要下标越界)。返回读取到的字符个数,返回-1表示一个字符都没读到。
int read(char[] cbuf) 将最多cbuf.length个字符的数据读入字符数组中。返回读取到的字符个数,返回-1表示一个字符都没读到。
void close() 关闭流对象并释放有关的资源
  • 栗子来啦:
public class FileReaderTest {
    public static void main(String[] args) {
        FileReaderTest test = new FileReaderTest();
        test.baseUse();
    }

    private void baseUse(){
        FileReader fr = null;
        try {
            fr = new FileReader("D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\1.txt");
            int res;
            while((res = fr.read()) != -1){
                System.out.println("读取的内容是:" + (char)res);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(null != fr){
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

        try {
            fr = new FileReader("D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\1.txt");
            int res;
            char[] chars = new char[3];
            res = fr.read(chars, 1, 2);
            System.out.println("读取到了:"+ res + "个字符,读取结果是:" + Arrays.toString(chars));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(null != fr){
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
           
  • 结果如下。如果第二段栗子中,读取的个数改为3,则会有下标越界异常。
读取的内容是:a
读取的内容是:哦
读取的内容是:一
读取的内容是:a
读取的内容是:哦
读取的内容是:一
读取到了:2个字符,读取结果是:[ , a, 哦]
           

关于read的返回值为什么是int

  • 我们知道,Java中一个char固定占2个字节,因为其用Unicode来编码的。但为什么返回值要用int呢?int可是得用4个字节呢。这就涉及到一个设计思想的问题了。
    • 假设我们只用char来作为返回值(2个字节),那么我们怎么表示没有读到任何数据呢?没办法表示吧?
    • 所以Java就用int来作为返回值,用-1表示没有读取到任何值。而正常读取到的字符对应的二进制,只会用到低16位,并不会和-1(1111 1111 1111 1111)冲突。
    • 下面要讲的字节流,也是同样的道理~是不是觉得Java的设计师们很聪明呢?

FileOutputStream、FileInputStream

  • 上面介绍的是字符流,操作文本(字符)文件妥妥的。但如果操作的不是文本文件,那么用字符流就会导致文件不正常或者损坏。这个时候就需要我们的字节流上场了。
  • 常用方法和字符流几乎一样,只是之前的char[]变成了byte[]。
  • FileInputStream多了一个比较常用的方法:

    int available()

    。用于获取文件的大小(单位为字节)。

文件拷贝案例

  • 既然都说了字节流和字符流的使用基本一样,那我就不费口舌来介绍怎么使用字节流了。我们直接来玩一个有意思的东西——文件拷贝。
第一种方式
public class FileCopy {
    public static void main(String[] args) {
        FileCopy test = new FileCopy();
        test.fileCopyFirst("D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background.jpg",
                "D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background-back.jpg");
    }

    private void fileCopyFirst(String srcPath, String destPath) {
        OutputStream out = null;
        InputStream in = null;
        try {
            out = new FileOutputStream(destPath);
            in = new FileInputStream(srcPath);

            System.out.println("文件大小:" + in.available());

            //记个时
            long beginTime = System.currentTimeMillis();
            System.out.println("开始copy");

            int result;
            while (-1 != (result = in.read())) {
                out.write(result);
            }

            long endTime = System.currentTimeMillis();
            System.out.println("结束copy");
            System.out.println("耗时:" + (endTime - beginTime));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != out) {
                    out.close();
                }
                if (null != in) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
           
  • 这种方式运行结果如下。大家可以感受到一个问题。对于一个2MB的图片,拷贝过程需要将近10秒,这是一件非常夸张的事情。
文件大小:2153023
开始copy
结束copy
耗时:16107
           
  • 为什么会这样呢?因为每一次执行read或者write的时候,其实JVM是与计算机底层硬件做了很多交互的,这个过程开支比较大。如果一个字节就进行一次read和write,总共需要多少次?amazing!
  • 这个感觉就像什么呢?老妈让你去买50个鸡蛋,然后你去菜市场,每次就买1个鸡蛋回家。然后来回跑了50次!amazing!
第二种方式
  • 你吸取教训了,这次打算一次性把所有需要的鸡蛋给买回去,于是代码变成了这样:
public class FileCopy {
    public static void main(String[] args) {
        FileCopy test = new FileCopy();
        test.fileCopyFirst("D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background.jpg",
                "D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background-back.jpg");
    }

    private void fileCopySecond(String srcPath, String destPath) {
        OutputStream out = null;
        InputStream in = null;
        try {
            out = new FileOutputStream(destPath);
            in = new FileInputStream(srcPath);

            //记个时
            long beginTime = System.currentTimeMillis();

            //读取数据至缓存
            System.out.println("开始读数据");
            byte[] buffer = new byte[in.available()];
            int result = in.read(buffer);
            System.out.println("实际读取的字节数:" + result);

            //从缓存写出数据
            System.out.println("开始写数据");
            out.write(buffer);

            long endTime = System.currentTimeMillis();
            System.out.println("耗时:" + (endTime - beginTime));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != out) {
                    out.close();
                }
                if (null != in) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
           
  • 运行结果如下。我去,这也太快了。然后你又觉得你自己行了,于是决定:以后不管多少个鸡蛋,我就一次性捞回去完事儿~
开始读数据
实际读取的字节数:2153023
开始写数据
耗时:30
           
  • 结果小区里面的大爷大妈都觉得你行了,于是都找你帮忙买鸡蛋。于是,你一天需要买一万个鸡蛋。然后你试着一次性买回去一万个鸡蛋…你又发现你不行了。
第三种方式
  • 你痛并思痛,想到为什么我不能每次就买50个呢?虽然需要多跑几次,但总比被直接压死强是吧!于是代码变成了这样:
public class FileCopy {
    public static void main(String[] args) {
        FileCopy test = new FileCopy();
        test.fileCopyThird("D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background.jpg",
                "D:\\WorkSpace\\Work\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background-back.jpg");
    }

    private void fileCopyThird(String srcPath, String destPath) {
        OutputStream out = null;
        InputStream in = null;
        try {
            out = new FileOutputStream(destPath);
            in = new FileInputStream(srcPath);

            //记个时
            long beginTime = System.currentTimeMillis();

            //读取数据至缓存
            System.out.println("开始拷贝数据");
            byte[] buffer = new byte[102400];
            int result;
            //只要还没有读到文件末尾,就一直循环
            while((result = in.read(buffer)) != -1){
                out.write(buffer, 0, result);//注意,因为最后一次写入,可能并没有放满buffer,所以,每一次放的时候,应该是buffer有多少,就放多少
                System.out.println("实际读取的字节数:" + result);
            }

            long endTime = System.currentTimeMillis();
            System.out.println("耗时:" + (endTime - beginTime));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != out) {
                    out.close();
                }
                if (null != in) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
           
  • 代码执行结果如下。你觉得你双行了。好吧,这次确实行了。但是机智的java官方早已看穿一切,于是提供了缓冲流。缓冲流的原理基本就是如此。
开始拷贝数据
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:2623
耗时:37
           

缓冲流

  • 缓冲流提供了一个缓冲区,用于对流数据进行缓冲。就像上面文件拷贝的例子三那样。
  • 缓冲流的使用还是大同小异,但是需要注意一点:缓冲流是处理流,所以其构造方法的入参是InputStream和OutputStream。
  • 关闭缓冲流的时候,与其关联的流也会自动关闭。所以每次操作完成后,close缓冲流即可。

BufferedOutputStream

  • 常用构造方法
方法声明 功能
BufferedOutputStream(OutputStream out) 根据参数指定的输出流来构造对象,默认缓冲大小为8192字节。
BufferedOutputStream(OutputStream out, int size) 根据参数指定的输出流来构造对象,并手动指定缓冲区大小

BufferedInputStream

  • 常用构造方法
方法声明 功能
BufferedInputStream(InputStream in) 根据参数指定的输入流构造对象,默认缓冲大小为8192字节。
BufferedInputStream(InputStream in, int size) 根据参数指定的输入流构造对象,并手动指定缓冲区大小

用缓冲流拷贝文件

  • 还是上面那个例子的文件,我们再用缓冲流来试一试:
public class BufferTest {
    public static void main(String[] args) {
        BufferTest test = new BufferTest();
        test.fileCopyThird("E:\\Gitee repository\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background.jpg",
                "E:\\Gitee repository\\java-practice\\JavaPractice\\src\\com\\UsefulNativeClass\\IO\\FileIO\\background-back.jpg");
    }

    private void fileCopyThird(String srcPath, String destPath) {
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try {
            out = new BufferedOutputStream(new FileOutputStream(destPath), 102400);
            in = new BufferedInputStream(new FileInputStream(srcPath), 102400);

            System.out.println("文件大小:" + in.available());

            //记个时
            long beginTime = System.currentTimeMillis();

            //读取数据至缓存
            System.out.println("开始拷贝数据");
            byte[] buffer = new byte[102400];
            int result;
            //只要还没有读到文件末尾,就一直循环
            while ((result = in.read(buffer)) != -1) {
                out.write(buffer, 0, result);//注意,因为最后一次写入,可能并没有放满buffer,所以,每一次放的时候,应该是buffer有多少,就放多少
                System.out.println("实际读取的字节数:" + result);
            }
            System.out.println("实际读取的字节数:" + result);

            long endTime = System.currentTimeMillis();
            System.out.println("耗时:" + (endTime - beginTime));
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != out) {
                    out.close();
                }
                if (null != in) {
                    in.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
           
  • 运行结果如下。你大爷还是你大爷,果然牛逼是吧~注意啦,可以看到虽然我用了缓冲流,但我还是使用的第三种方式拷贝,即自己手动写了个缓冲。实践证明这样的效率是最高的。如果我一次性只读一个字节,即使使用缓冲流,耗时大概会在90左右。
文件大小:2153023
开始拷贝数据
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:102400
实际读取的字节数:2623
实际读取的字节数:-1
耗时:9
           

BufferedWriter、BufferedReader

  • 字符流依然有缓冲流,同样也是效率很高的一种流。
  • BufferedWriter常用方法:
方法声明 功能
BufferedWriter(Writer out) 根据参数指定的输出流来构造对象。默认缓冲大小为8192。
BufferedWriter(Writer out, int sz) 根据参数指定的输出流来构造对象。并指定缓冲大小。
void write(int c) 写出单个字符
void write(char[] cbuf, int off, int len) 将字符数组cbuf中,从下标off开始的,len个字符写出
void write(char[] cbuf) 将字符数组cbuf的所有内容写出
void write(String s, int off, int len) 将字符串s中,下标从off开始的,len个字符写出
void write(String str) 将字符串str的所有内容写出
void newLine() 写出行分隔符。对于WINDOWS而言,即写出"\r\n"
void flush() 刷新流
void close() 关闭流对象并释放有关的资源
  • BufferedReader常用方法:
方法声明 功能
BufferedReader(Reader in) 根据参数指定的输入流来构造对象。默认缓冲大小为8192.
BufferedReader(Reader in, int sz) 根据参数指定的输入流来构造对象。并指定缓冲大小。
int read() 从输入流读取单个字符并返回。如果读取到末尾则返回-1。
int read(char[] cbuf, int off, int len) 从输入流中读取len个字符,放入数组cbuf中,放入的位置从下标off开始。返回实际读取到的字符个数。如果读到末尾,则返回-1
int read(char[] cbuf) 从输入流中读满整个数组cbuf。返回实际读取到的字符个数。如果读到末尾,则返回-1
String readLine() 读取一行字符串并返回,返回null表示读取到末尾
void close() 关闭流对象并释放有关的资源

打印流——PrintStream、PrintWriter

  • 打印流也是一个处理流。
  • 这个流其实小伙伴们都非常非常非常非常非常非常熟悉,为什么呢?因为我们天天在用的

    System.out

    的这个out,其实就是一个PrintStream。哈哈哈!意不意外?
  • 只是说System的这个out呢,都是向控制台写出数据。但实际上,PrintStream是一个处理流。所以可以关联OutputStream,从而向其他文件写数据。
  • PrintStream常用方法如下:
方法声明 功能
PrintStream(OutputStream out) 根据参数指定的输出流来构造对象。
void print(String s) 输出字符串内容。
void println(String x) 输出字符串内容,并追加换行符。
void flush() 刷新流
void close() 用于关闭输出流并释放有关的资源
  • PrintWriter除了构造方法入参是字符输出流,其他的方法基本一致。

转换流——OutputStreanWriter、InputStreamReader

  • 用于将字节流转换为字符流。是一种处理流。
  • OutputStreamWriter的主要方法如下:
方法声明 功能
OutputStreamWriter(OutputStream out) 根据参数指定的字节流来构造对象
OutputStreamWriter(OutputStream out, String charsetName) 根据参数指定的字节流和编码构造对象
void write(String str) 将参数指定的字符串写入
void flush() 刷新流
void close() 用于关闭输出流并释放有关的资源
  • 可以看到,依然大同小异,只是构造方法的参数是字节流。输出流也是同理,就不列举啦~

举栗子:模拟聊天日志功能

  • 我们可以通过缓冲字节流、打印流、转换流,来实现一个聊天日志的功能。而聊天的内容通过控制台输入。输入bye表示聊天结束,结束程序。
  • 代码如下:
public class ChatRoom {
    private void chatLog() {
        BufferedReader reader = null;
        PrintWriter writer = null;
        try {
            //首先,System.in是一个获取键盘输入的标准方法,但是这个in呢,它是一个字节输入流。
            //然后呢,我们要获取的是字符,所以理所应当用缓冲字符输入流。
            //所以,我们需要在二者之间加一个隔壁老王——转换流
            reader = new BufferedReader(new InputStreamReader(System.in));
            //因为聊天一般都是一个换行符作为一段话,所以正好可以用打印流
            writer = new PrintWriter(new FileWriter("D:\\logs.txt", true));

            //用于判断该谁说话勒
            boolean flag = false;

            String content;
            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyy-MM-dd hh:mm:ss");
            while (true) {
                System.out.println(flag ? "张三说:" : "李四说:");
                content = reader.readLine();
                if ("bye".equals(content)) {
                    writer.println("聊天结束");
                    System.out.println("聊天结束");
                    break;
                } else {
                    writer.println(formatter.format(LocalDateTime.now()) + (flag ? " 张三说:" : " 李四说:") + content);
                    flag = !flag;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (null != reader) {
                    reader.close();
                }
                if (null != writer) {
                    writer.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ChatRoom chatRoom = new ChatRoom();
        chatRoom.chatLog();
    }
}
           
  • 控制台内容如下:
李四说:
hello, little jade
张三说:
who?
李四说:
sorry
张三说:
bye
聊天结束
           
  • 文件中内容如下:
2020-12-02 10:19:24 李四说:hello, little jade
2020-12-02 10:19:28 张三说:who?
2020-12-02 10:19:33 李四说:sorry
聊天结束
           
  • 这个栗子中,因为始终是对聊天内容的处理,所以最外层都是选择字符流进行操作。而控制台输入是一个字节流,因此用到了转换流。
  • 通常情况下,不论是输出还是输入,都应当使用一个缓冲流来提高效率。
  • 而这个栗子中,为什么输出还套了个打印流呢?因为println这个方法真香,哈哈哈~

数据流——DataOutputStream、DataInputStream

  • 用于对基本数据类型进行读写。是一个处理流。
  • DataOutputStream常用方法:
方法声明 功能
DataOutputStream(OutputStream out) 根据参数指定的输出流构造对象。
void writeInt(int v) 将一个整数一次性写出。同理,所有基本类型都有一个对应的write方法
void close() 用于关闭文件输出流并释放有关的资源。
  • DataInputStream
方法声明 功能
DataInputStream(InputStream in) 根据参数输入流来构造对象。
int readInt() 一次性读取一个整数数据。同理,别的类型也有
void close() 用于关闭文件输出流并释放有关的资源
  • 数据流虽然看起来很简单,但是有一个坑,第一次接触的小伙伴那是前仆后继地往里跳。下面来填一下:
public class DataIOTest {
    public static void main(String[] args) throws IOException {
        try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("d:/data.txt"));
             DataInputStream dis = new DataInputStream(new FileInputStream("d:/data.txt"))) {

            int num = 69;//0000 0000 0000 0000 0000 0000 0100 0011
            dos.writeInt(num);
            System.out.println("数据写入成功");

            System.out.println(dis.readInt());
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
           
  • 运行结果如下。风平浪静,波澜不惊。但是再去看一下txt文件,小伙伴们可能会有点懵
数据写入成功
69
           
  • data.txt内容如下。这是什么意思呀?其实道理很简单:
    • 我们知道int是4个字节,所以69对应的二进制编码如上面代码里面的注释所示。
    • 但是我们把文件的类型设置为了txt,也就是说当成文本文件来看了。
    • 当我们打开文件的时候,将二进制编码以默认编码UTF-8进行文本解析。
    • 解析结果就是:空字符(0000 0000)、空字符(0000 0000)、空字符(0000 0000)、E(0100 0011)。
    • 注意空字符和空格不是一个意思哈,虽然看起来是一样的。
E
           
  • 数据流还有一个小坑:
    • 如果文件里面就只有1个字节的内容。
    • 此时我们readInt(),那么就会发生EOFException(文件末尾异常)。
    • 这个异常有点数组下标越界异常内味儿,但还是不一样的哈。

对象流——ObjectOutputStream、ObjectInputStream

  • 将对象作为一个整体,进行读写操作。是一个处理流。
  • ObjectOutputStream常用方法:
方法声明 功能
ObjectOutputStream(OutputStream out) 根据参数指定的输出流来构造对象
void writeObject(Object obj) 将一个对象整体写出
void close() 用于关闭输出流并释放有关的资源
  • ObjectInputStream常用方法:
方法声明 功能
ObjectInputStream(InputStream in) 根据参数指定的输入流来构造对象。
Object readObject() 读取一个对象。无法通过返回值来判断是否读取到文件的末尾。
void close() 用于关闭输入流并释放有关的资源。
  • 可以看到,对象流的写出和读入方法,都是对对象进行操作的。并且,操作的对象必须要启用序列化,否则会抛出NotSerializableException(不可序列化异常)。
  • 因为readObject()无法判断是否读取到文件末尾,所以我个人建议一个序列化文件中就存放一个对象。
    • 那如果有一个文件中存放多个对象的需求怎么办呢?很简单啦,可以采用集合存储,然后序列化集合对象喽~

说到对象流,就得说一说序列化

what is 序列化?

  • 所谓序列化,是指将一个对象需要存储的相关信息通过一定规则,有效地组织成字节序列的过程。
  • 而反序列化,是指将有效组织的字节序列,按照一定规则,恢复成对象的过程。
  • 而实现序列化的方式很简单,只需要实现Serializable接口即可,不需要重写任何方法。因为其实所有类已经是可以序列化的,但默认情况都未启用。只有当实现了接口,才会告诉JVM:这个类启用序列化。

序列化版本号

  • 为了在一定程度保证安全性,序列化机制通过serialVersionUID来验证版本一致性的。
  • 在进行反序列化时,JVM会把传来的流中的serialVersionUID与本地相应类的serialVersionUID进行比较。
    • 如果相同就认为是一致的,可以进行反序列化。
    • 如果不一致,就会抛出InvalidCastException(序列化版本不一致)异常。

transient

  • 是Java语言的关键字。当给属性加上该关键字后,该属性将不会参与序列化。

啃一块栗子

public class Person implements Serializable {
    private static final long serialVersionUID = 13412341324L;
    private String name;
    private transient String gender;

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

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

    public static void main(String[] args) {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/se.txt"));
             ObjectInputStream ois = new ObjectInputStream((new FileInputStream("d:/se.txt")))) {
            Person person = new Person("angel", "糙汉子");
            oos.writeObject(person);
            System.out.println("写出成功");

            Object newPer = ois.readObject();
            System.out.println(newPer);
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
           
  • 运行结果如下。这个栗子有几点说一下:
    • 文件内容就是一堆乱码,因为不是按照字符写的文件嘛。
    • 可以看到,序列化主要是用于将类中的属性写入文件。所以序列化主要适用于Bean。
    • 序列化版本号其实不写也不会报错。但是上面说了,出于安全性考虑,建议添加序列化版本号。
    • readObject的返回值其实用到了多态。虽然返回值是Object,但是实例依然是对应的类的实例。
    • 这个栗子中,我把gender用了transient关键字修饰。可以发现gender没有序列化,所以是String的默认值null。
写出成功
Person{name='angel', gender='null'}
           

RandomAccessFile

基本概念

  • 这个类支持对文件的随机读写。
    • 这里所谓的随机,是指在文件中读写的位置是任意的。而不是像通常的流,固定从开头往后读。

常用方法

方法声明 功能
RandomAccessFile(String name, String mode) 根据参数指定的路径和模式构造对象。如果文件不存在则报错。
RandomAccessFile(File file, String mode) 根据参数指定的File对象和模式构造对象。
int read() 读取单个字节的数据
void seek(long pos) 将读写位置设置成距文件开头pos个字节的位置
void write(int b) 写出单个字节的数据,写出的数据覆盖当前位置的数据。
void close() 用于关闭流并释放有关的资源
  • 构造方法中的这个模式啊,简单来说有几种:
    • r:以只读方式打开;
    • rw:以读写方式打开;
    • rwd:以读写方式打开,且同步文件内容的更新;
    • rws:以读写方式打开,且同步文件内容和元数据的更新。
  • 什么叫同步呢?意思是每进行一次写操作,就会将内容及时写入磁盘。
    • 这样做的好处是当系统发生崩溃时,已经调用write()方法的内容不会丢失。
    • 坏处也很明显,就是降低了写出的速度。
    • 而rwd和rws有什么区别呢?目前我只知道rws需要同步的东西更多,速度更慢。具体什么是元数据,我还没有研究。
  • 这个类还有很多东西比较复杂,但是平时工作中基本不会用到,所以就不展开讲了。

栗子↓

public class RandomAccessTest {
    public static void main(String[] args) {
        //这个类不会自动创建文件,记得先用别的方式创建一个
        try (RandomAccessFile file = new RandomAccessFile("d:/ra.txt", "rw")) {
            //为了测试方便,先向文件中写一点内容。
            //建议不要写中文,这个类的写方法对非ASCII码的字符没有太好的支持
            file.writeBytes("what are you doing?");

            //然后我们来读“doing”这个单词。
            //注意,seek偏移的是字节量。而一个ASCII码字符是1个字节。
            //所以可以算出,“doing”在文件中,需要偏移13个字节
            file.seek(13);
            System.out.println(file.readLine());

            //然后再来测试一下把you改成she。
            //不要忘了这个类的写方法是覆盖的。
            file.seek(9);
            file.writeBytes("she");

            //展示一下结果
            file.seek(0);
            System.out.println(file.readLine());

        } catch (IOException e) {
            e.printStackTrace();
        }

    }
}
           
  • 执行结果如下。重要的信息我都写在代码注释里面了,小伙伴们可以自行欣赏~~~~
doing?
what are she doing?
           

彩蛋

  • 刚刚发文章的时候,看到CSDN系统给我发了个段子哈哈哈。分享一下:
  • 论Java变量赋值的鄙视链
    Java基础(五)——异常机制、IO类的使用与原理前言异常机制IO流彩蛋