面试官您好,请不要再问我Java中的io流

创作不易,如果觉得这篇文章对你有帮助,欢迎各位老铁点个赞呗,您的支持是我创作的最大动力!

1 Java中流的定义

流是一个很形象的概念,当程序需要读取数据的时候,就会开启一个通向数据源的流,这个数据源可以是文件,内存,或是网络连接。类似的,当程序需要写入数据的时候,就会开启一个通向目的地的流。这时候你就可以想象数据好像在这其中“流”动一样。

Java的核心库java.io提供了全面的IO接口,包括:文件读写、标准设备输出等。Java中IO是以流为基础进行输入输出的,所有数据被串行化写入输出流,或者从输入流读入。(百度百科

2 Java中流的分类

2.1 字节流和字符流

文件通常是由一连串的字节或字符构成,组成文件的字节序列称为字节流组成文件的字符序列称为字符流

2.2 输入流和输出流

Java中根据流的方向可以分为输入流输出流

输入流是将文件或其它输入设备的数据加载到内存的过程输出流则恰恰相反,是将内存中的数据保存到文件或其他输入设备,详见下图:
在这里插入图片描述

文件是由字符或字节构成,那么将文件加载到内存或再将文件从内存输出到文件,需要有输入和输出流的支持,那么在Java语言中又把输入流和输出流分为了两个,字节输入流和字节输出流,字符输入流和字符输出流,如下图所示:
在这里插入图片描述

2.2.1 InputStream(字节输入流)

InputStream是字节输入流,InputStream是一个抽象类,所有实现了inputStream的类都是字节输入流,核心的子类如下:
在这里插入图片描述
主要方法介绍:

 void	close() 
          关闭此输入流并释放与该流关联的所有系统资源。
abstract  int	read() 
          从输入流读取下一个数据字节。
 int	read(byte[] b) 
          从输入流中读取一定数量的字节并将其存储在缓冲区数组 b 中。
 int	read(byte[] b, int off, int len) 
          将输入流中最多 len 个数据字节读入字节数组。

2.2.2 OutputStream(字节输出流)

所有实现了OutputStream都是字节输出流
在这里插入图片描述
主要方法介绍:

void	close() 
          关闭此输出流并释放与此流有关的所有系统资源。
 void	flush() 
          刷新此输出流并强制写出所有缓冲的输出字节。
 void	write(byte[] b) 
          将 b.length 个字节从指定的字节数组写入此输出流。
 void	write(byte[] b, int off, int len) 
          将指定字节数组中从偏移量 off 开始的 len 个字节写入此输出流。
abstract  void	write(int b) 
          将指定的字节写入此输出流。

2.2.3 Reader(字符输入流)

所有实现了Reader都是字符输如流

在这里插入图片描述
主要方法介绍:

abstract  void	close() 
          关闭该流。
 int	read() 
          读取单个字符。
 int	read(char[] cbuf) 
          将字符读入数组。
abstract  int	read(char[] cbuf, int off, int len) 
          将字符读入数组的某一部分。

2.2.4 Writer(字符输出流)

所有实现了Writer都是字符输出流
在这里插入图片描述
主要方法介绍:

Writer	append(char c) 
          将指定字符追加到此 writer。
abstract  void	close() 
          关闭此流,但要先刷新它。
abstract  void	flush() 
          刷新此流。
 void	write(char[] cbuf) 
          写入字符数组。
abstract  void	write(char[] cbuf, int off, int len) 
          写入字符数组的某一部分。
 void	write(int c) 
          写入单个字符。
 void	write(String str) 
          写入字符串。
 void	write(String str, int off, int len) 
          写入字符串的某一部分。

3 文件流

文件流主要分为:文件字节输入流文件字节输出流文件字符输入流文件字符输出流

3.1 FileInputStream(文件字节输入流)

FileInputStream主要按照字节方式读取文件,例如我们准备读取一个文件,该文件的名称为test.txt
在这里插入图片描述
【代码示例】

public static void main(String[] args) {
    InputStream is = null;
    try {
        is = new FileInputStream("d:\\test.txt");

        int b = 0;
        while ((b = is.read()) != -1) {
            //直接打印
            //System.out.print(b);

            //输出字符
            System.out.print((char) b);
        }

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (is != null) {
                is.close();
            }
        } catch (IOException e) {

        }
    }
}

执行结果:
在这里插入图片描述
从以上执行结果可以看出,test.txt文件可以正确的读取,但是打印的结果乱码了。因为这个文件中,写入了两行汉字,在使用了字节输入流读取文件的时候,它是一个字节一个字节读取的,而汉字是两个字节,所以读出一个字节就打印,那么汉字是不完整的,所以就乱码了

3.2 FileOutputStream(文件字节输出流)

FileOutputStream主要按照字节方式写文件

例如: 我们做文件的复制,首先读取文件,读取后在将该文件另写一份保存到磁盘上,这就完成了备份。
在这里插入图片描述

【示例代码】

public static void main(String[] args) {
    InputStream is = null;
    OutputStream os = null;
    try {
        is = new FileInputStream("d:\\test.txt");
        os = new FileOutputStream("d:\\test.txt.bak");
        int b = 0;
        while ((b = is.read()) != -1) {
            os.write(b);
        }

        System.out.println("文件复制完毕!");
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (is != null) {
                is.close();
            }
            if (os != null) {
                os.close();
            }
        } catch (IOException e) {

        }
    }
}

3.3 FileReader(文件字符输入流)

FileReader以字符为单位读取文件,也就是说,一次读取一个字符,即一次读取两个字节。

【代码示例】

public static void main(String[] args) {
    Reader reader = null;
    try {
        reader = new FileReader("d:\\test.txt");

        int b = 0;
        while ((b = reader.read()) != -1) {
            //输出字符
            System.out.print((char) b);
        }

    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (reader != null) {
                reader.close();
            }
        } catch (IOException e) {

        }
    }
}

执行结果:

这是一段难忘的时光,在忙碌中度过了一年。
这是一段难忘的旅程,工作辛苦而充实。

因为以上是通过字符流读取的文件数据,所以没有出现乱码。

3.4 FileWriter(文件字符输出流)

【代码示例】

public static void main(String[] args) {
   Writer writer = null;
    try {
        //以下方式会将文件的内容进行覆盖
        //writer = new FileWriter("d:\\test.txt");
        //writer = new FileWriter("d:\\test.txt", false);

        //以下为true表示,在文件后面追加
        writer = new FileWriter("d:\\test.txt", true);
        writer.write("你好你好!!!!");
        //换行
        writer.write("\n");
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (writer != null) {
                writer.close();
            }
        } catch (IOException e) {

        }
    }
}

4 缓冲流

缓冲流主要是为了提高效率而存在的,减少物理读取磁盘的次数,以此来提高性能。

缓冲流主要有: BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter,并且BufferedReader提供了实用方法readLine(),可以直接读取一行,BufferWriter提供了newLine()可以用作换行符。

4.1 采用字节缓冲流改造文件复制代码

使用缓冲流,对以上代码测试用例,进行优化改造:
采用BufferedInputStreamInputStream进行装饰,BufferedInputStream会将数据先读到缓存里,Java程序再次读取数据时,直接到缓存中读取减少Java程序物理读取的次数,提高性能。

采用BufferedOutputStreamFileOutputStream进行装饰,每次写文件的时候,先放到缓存了,然后再一次性的将缓存中的内容保存到文件中,这样会减少写物理磁盘的次数,提高性能。

大致改造如下:
在这里插入图片描述

【改造后代码示例】

public static void main(String[] args) {
    InputStream is = null;
    OutputStream os = null;
    try {
        is = new BufferedInputStream(new FileInputStream("d:\\test.txt"));
        os = new BufferedOutputStream(new FileOutputStream("d:\\test.txt.bak"));

        int b = 0;
        while ((b = is.read()) != -1) {
            os.write(b);
        }
        //手动调用flush,将缓冲区中的内容写入到磁盘,也可以不用手动调用,缓存区满了自动会清除了
        //当输出流关闭的时候也会先调用flush()
        os.flush();
        System.out.println("文件复制完毕!");
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (is != null) {
                is.close();
            }
            if (os != null) {
                //在close前会先调用flush()
                os.close();
            }
        } catch (IOException e) {

        }
    }
}

可以显示的调用flush(),flush()方法的含义是刷新缓冲区,也就是将缓存区中的数据写到磁盘上,不再放到内存里了,在执行os.close()关闭流资源时,其实默认执行了os.flush(),我们在这里可以不用显示的调用flush()。

4.2 采用字符缓冲流改造文件复制代码

【改造后的代码示例】

public static void main(String[] args) {
    BufferedReader r = null;
    BufferedWriter w = null;
    try {
        r = new BufferedReader(new FileReader("d:\\test.txt"));
        w = new BufferedWriter(new FileWriter("d:\\test.txt.bak"));

        String s = null;
        while ((s = r.readLine()) != null) {
            w.write(s);
            //w.write("\n");
            //BufferedReader提供了实用方法readLine(),可以直接读取一行,可以采用如下方法换行
            w.newLine();
        }

        System.out.println("文件复制完毕!");
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (r != null) {
                r.close();
            }
            if (w != null) {
                //在close前会先调用flush
                w.close();
            }
        } catch (IOException e) {

        }
    }
}

5 转换流

转换流主要有两个: InputStreamReaderOutputStreamWriter

  • InputStreamReader主要是将字节流输入流,转换成字符输入流
  • OutputStreamWriter主要是将字节流输出流,转换成字符输出流

回顾字符缓冲流:

  • BufferedReader提供了实用方法readLine(),可以直接读取一行,BufferWriter提供了newLine()可以用作换行符。

5.1 InputStreamReader

  • InputStreamReader主要是将字节流输入流,转换成字符输入流
    在这里插入图片描述
    【代码示例】

    /**
     * <p>
     * 对FileInputStreamTest01.java进行改造,使用字符流
     * <p/>
     *
     * @param args
     * @return void
     * @Date 2020/6/5 20:58
     */
    public static void main(String[] args) {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new InputStreamReader(new FileInputStream("d:\\test.txt")));
    
            String s = null;
            //BufferedReader提供了实用方法readLine(),可以直接读取一行
            while ((s = br.readLine()) != null) {
                System.out.println(s);//你好你好!!!!
            }
    
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException e) {
    
            }
        }
    }
    

5.2 OutputStreamReader

  • OutputStreamWriter主要是将字节流输出流,转换成字符输出流
    【代码示例】

    public static void main(String[] args) {
        BufferedWriter bw = null;
        try {
            bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("d:\\test.txt")));
            bw.write("hello world");
            bw.newLine();
            bw.write("奋斗的青春");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (bw != null) {
                    bw.close();
                }
            } catch (IOException e) {
            }
        }
    }
    

6 打印流

打印流主要包含两个: PrintStream 打印字节流PrintWriter 打印字符流

6.1 完成屏幕打印的重定向

System.out其实对应的就是PrintStream打印字节流,默认输出到控制台,我们可以重定向它的输出,可以定向到文件,也就是执行System.out.println("hello")不输出到屏幕,而是输出到文件。
【代码示例】

/**
 * @author smilehappiness
 * 打印字节流
 * @version 1.0
 * @ClassName PrintStreamTest
 * @Date 2020/6/5 21:30
 */
public class PrintStreamTest {

    /**
     * <p>
     * 重定向它的输出,打印信息记录到文件,不再打印到控制台
     * <p/>
     *
     * @param args
     * @return void
     * @Date 2020/6/5 21:21
     */
    public static void main(String[] args) {
        OutputStream os = null;
        try {
            os = new FileOutputStream("d:/log.txt");
            System.setOut(new PrintStream(os));
            System.out.println("hello world");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (os != null) {
                    os.close();
                }
            } catch (IOException e) {

            }
        }
    }

}

6.2 接受屏幕输入

System.in可以接收屏幕输入
【示例代码】

/**
 * @author smilehappiness
 * System.in可以接收屏幕输入
 * @version 1.0
 * @ClassName PrintStreamTest02
 * @Date 2020/6/5 10:05
 */
public class PrintStreamTest02 {

    public static void main(String[] args) {
        BufferedReader br = null;
        try {
            br = new BufferedReader(new InputStreamReader(System.in));
            String s = null;
            while ((s = br.readLine()) != null) {
                System.out.println(s);
                //输入hello的时候,退出循环
                if ("hello".equals(s)) {
                    break;
                }
            }

        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException e) {

            }
        }
    }

}

7 对象流

对象流可以将Java对象转换成二进制写入磁盘,这个过程通常叫做序列化,并且还可以从磁盘读出完整的Java对象,而这个过程叫做反序列化

对象流主要包括:ObjectInputStreamObjectOutputStream

7.1 序列化

如果实现序列化,该类必须实现序列化接口java.io.Serializable,该接口没有任何方法,该接口只是一种标记(标识)接口,标记这个类是可以序列化的。

【测试用例】

/**
 * <p>
 * 序列化测试类
 * <p/>
 *
 * @author smilehappiness
 * @Date 2020/6/5 22:05
 */
public class ObjectStreamTest01 {

    public static void main(String[] args) {
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("d:/Person.dat"));
            Person person = new Person();
            person.setName("张三");
            oos.writeObject(person);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (oos != null) {
                    oos.close();
                }
            } catch (IOException e) {

            }
        }
    }
}

/**
 * <p>
 * 实现序列化接口
 * <p/>
 *
 * @author smilehappiness
 * @Date 2020/6/5 22:05
 */
@Getter
@Setter
class Person implements Serializable {
    private String name;
}

如果Person不实现Serializable接口,不能实现序列化,对序列化的类是有要求的,这个序列化的类必须实现一个Serializable接口,这个接口没有任何方法声明,它是一个标识接口,如:java中的克隆接口Cloneable,也是起到了一种标识性的作用。

7.2 反序列化

把序列化的对象,从磁盘中完整的读出来,就是反序列化。

【代码示例】

public static void main(String[] args) {
    ObjectInputStream ois = null;
    try {
        ois = new ObjectInputStream(new FileInputStream("d:/Person.dat"));
        //反序列化
        Person person = (Person) ois.readObject();
        System.out.println(person.getName());
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        try {
            if (ois != null) {
                ois.close();
            }
        } catch (IOException e) {
        }
    }
}

7.3 serialVersionUID属性

7.3.1 反序列化错误场景演示

【示例代码】
在person对象中加入一个成员属性age,然后再读取person.dat文件

package cn.smilehappiness.io;

import java.io.*;

/**
 * <p>
 * serialVersionUID属性测试类
 * <p/>
 *
 * @author smilehappiness
 * @Date 2020/6/5 22:40
 */
public class ObjectStreamTest03 {

    public static void main(String[] args) {
        //writeObject();
        readObject();
    }

    /**
     * <p>
     * 反序列化对象
     * <p/>
     *
     * @param
     * @return void
     * @Date 2020/6/5 22:58
     */
    private static void readObject() {
        ObjectInputStream ois = null;
        try {
            ois = new ObjectInputStream(new FileInputStream("d:/Person.dat"));
            //反序列化
            Person person = (Person) ois.readObject();
            System.out.println(person.getName());
            System.out.println(person.getAge());
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (ois != null) {
                    ois.close();
                }
            } catch (IOException e) {
            }
        }
    }

    /**
     * <p>
     * 序列化对象
     * <p/>
     *
     * @param
     * @return void
     * @Date 2020/6/5 22:58
     */
    private static void writeObject() {
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(new FileOutputStream("d:/Person.dat"));
            Person person = new Person();
            person.setName("张三");
            person.setAge("20");

            oos.writeObject(person);
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (oos != null) {
                    oos.close();
                }
            } catch (IOException e) {

            }
        }
    }

}

运行结果:
在这里插入图片描述
34行错误的原因:在序列化存储Person时,它会为该类生成一个serialVersionUID= -8840000405364729590,而我们在该类中加入了一个age属性后,那么在使用的时候他就会为该类生成一个新的serialVersionUID= 234231939624627535,这个两个UID(-8840000405364729590234231939624627535)不同,所以Java认为是不兼容的两个类,导致反序列化生成Person对象时报错。

注意:实际开发中,也会遇到这种问题,比如说,开发完成后,发布了测试服务,然后由于变动,在一些实体类增加了新的属性,这时候在反序列化获取对象信息时,就会出现版本不一致问题,导致程序反序列化错误

7.3.2 解决序列化版本号不一致问题

通常在实现序列化的类中增加如下定义:
private static final long serialVersionUID = -111111111111111111L;

如果在序列化类中定义了成员域serialVersionUID,系统会把当前serialVersionUID成员域的值作为类的序列号(类的版本号),这样不管你的类如何升级,那么他的序列号(版本号)都是一样的,就不会产生类的兼容问题

@Getter
@Setter
class Person implements Serializable {
    //加入版本号,防止序列化兼容问题
    private static final long serialVersionUID = -111111111111111111L;

    private String name;
    private String age;
}

以上不再出现序列化的版本问题,因为他们有统一的版本号:-111111111111111111L

用一个图来加深对序列化id的理解:
在这里插入图片描述
请老铁们记住:serialVersionUID就和序列化有关。

8 File类

请参考我另一篇博文的详细介绍:Java中File类,你知道有哪些api方法吗?

9 缓冲器的作用原理

下面这段代码,摘自第8节,File详细介绍中(Java中File类,你知道有哪些api方法吗?)的一段代码,这段代码,使用缓冲字节输入流读取文件的数据,然后使用缓冲字节输出流,写数据到另一个磁盘文件中
在这里插入图片描述
对以上代码中,bytes=new Byte[1024]进行分析:
我的最初想法:
1,bufferedInputStream每读取一个字节,都要给i赋值,不到文档末尾i不会是-1,所以每次都要输出一个越来越长的String,直到该文件内容全部输出。
2,如果文件字节数大于1024,那么i=1024时bytes就被填满了,所以有可能只能输出文件的前1024个字节。

测试结果说明上面两个想法都是错误的,控制台输出的是整个文件的内容,并不是“越来越长”的部分内容。在文件字节大于1024时,依然能正常输出。

下面这段解释摘自原文:https://blog.csdn.net/zzuwlan_high/article/details/78553193

合理的解释:
引用API文档:“public int read(byte[] b) throws IOException:从此输入流中将最多 b.length 个字节的数据读入一个 byte 数组中。在某些输入可用之前,此方法将阻塞。
注意“阻塞”二字,bufferedInputStream执行read()方法时,不会每读一个字节就对i赋值,所以while循环就被一直堵在判断语句中,直到bytes被赋满出现异常,读取的阻塞释放,i终于被赋了一个值1024,接下来就执行循环体的打印。

bytes字节数组第一次塞满时,文件被读到的地方会有一个记录,所以当循环体执行完后,bufferedInputStream会从上次循环结束的记录向下读文件,又把while循环阻塞在判断语句,直到读完最后一个字节,读取阻塞再次被释放,这次i再次被赋值,该值为这次bufferedInputStream读到的字节数,然后,执行循环体打印。

最后,bufferedInputStream再次尝试读取文件,这时候已经没有字节可读,故返回-1赋给i,循环体不再执行,循环结束。

简单来理解,i=bufferedInputStream.read(b)的意思是:从输入流中读取bytes字节数组大小的字节,并将其存储在bytes里面,返回读取的字节数i,每循环一次bytes就被重新赋值一次

写博客是为了记住自己容易忘记的东西,另外也是对自己工作的总结,希望尽自己的努力,做到更好,大家一起努力进步!

如果有什么问题,欢迎大家评论,一起探讨,代码如有问题,欢迎各位大神指正!

给自己的梦想添加一双翅膀,让它可以在天空中自由自在的飞翔!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值