IO流

107 阅读13分钟

一、IO流

I代表输入,O代表输出

输入:数据从数据源加载到内存。
输出:数据从内存写回到数据源。
Input代表将磁盘数据加载到内存中的过程
Output代表将内存数据写回磁盘的过程
1660042376213.png

二、io流分类

1. 文件流

image.png

2. 字符流和字节流

字符流通常比较适合用于读取一些文本数据

字节流通常适合读取图片,MP4资源等二进制的数据。

在基于字节流和字符流的基础上又扩展出了以下四种类别:

分类字节输入流字节输出流字符输入流字符输出流
抽象接口InputStreamOutputStreamReaderWriter
访问文件FileInputStreamFileOutputStreamFileReaderFileWriter
访问数组ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter
访问管道PipedInputStreamPipedOutputStreamPipedReaderPipedWriter
访问字符串StringReaderStringWriter

3. 节点流和处理流

节点流和处理流就是对已有的这些io流做一些优化改善。

节点流:直接对特定的数据源执行读写操作。
处理流:对一个已经存在的流做一些二次封装操作,其功能更加比原先更加强大。
分类字节输入流字节输出流字符输入流字符输出流类型
抽象接口InputStreamOutputStreamReaderWriter  
访问文件FileInputStreamFileOutputStreamFileReaderFileWriter  
访问数组ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter  
访问管道PipedInputStreamPipedOutputStreamPipedReaderPipedWriter  
访问字符串StringReaderStringWriter  
缓冲流BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter  
打印流PrintStreamPrintWriter  
对象流ObjectInputStreamObjectOutputStream  
转换流InputStreamReaderOutputStreamWriter  

三、文件流

1. 创建文件对象相关构造器和方法

new File(String pathname) //根据路径构建一个File对象
new File(File parent,String child) //根据父目录文件+子路径构建
new File(String parent,String child) //根据父目录+子路径构建

CreateNewFile 创建新文件

2. 获取文件的相关信息

文件名字:getName()
文件绝对路径:getAbsolutePath()
文件父级目录:getParent()
文件大小(字节):length())
文件是否存在:exists()
是不是一个文件:isFile()
是不是一个目录: isDirectory()

3. 目录的操作和文件删除

创建一级目录 mkdir() 
创建多级目录 mkdirs()
删除空目录或文件 delete()

四、常用节点流

1. FileInputStream 和 FileOutputStream

FileInputStream流会用于以字节的方式去读取文件信息(代表从磁盘读取数据)

FileInputStream类

public static void readFile() {
    FileInputStream fileInputStream = null;
    try {
        fileInputStream = new FileInputStream(new File("e:\\text1.txt"));
        byte[] readData = new byte[10];
        int bufferSize = 0;
        //如果读取正常,会返回实际读取的字节数
        while ((bufferSize = fileInputStream.read(readData)) != -1){
            System.out.print(new String(readData,0,bufferSize));
        }
        System.out.println();
    }catch (Exception e){
        e.printStackTrace();
    } finally {
        //关闭文件流,释放资源
        try {
            //这个异常只要保证的确执行了close操作即可
            fileInputStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

使用注意点:

-   流需要进行关闭操作
-   使用read函数的时候,当文件读取正常时候会返回实际的字节数

FileOutputStream是文件的字节输出流(代表往磁盘中写入数据)

FileOutputStreampublic static void writeFileVersion1() {
        String filePath = "e:\\text2.txt";
        FileOutputStream fileOutputStream = null;
        try {
            //如果通过new FileOutputStream(filePath)的方式执行写入,实际上数据是会覆盖原先的内容,所以根据构造函数的第二参数来实现追加的效果
            fileOutputStream = new FileOutputStream(filePath,true);
            fileOutputStream.write("input sth".getBytes());
            fileOutputStream.write("input sth2".getBytes());
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            try {
                fileOutputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

注意:

-   在`FileOutputStream`的构造函数中,可以通过定义第二个参数为true,去设置写入的模式(覆盖模式还是追加模式)。
-   使用字符输出流的时候,即使没有走到close函数,数据也会被持久化写入到磁盘。

2. FileReader 和 FileWriter

FileReader

image.png

public static void readFile(){
    FileReader fileReader = null;
    try {
        fileReader = new FileReader("e:\\text.txt");
        char[] chars = new char[3];
        int readLen = 0;
        while ((readLen=fileReader.read(chars))!=-1){
            System.out.println(new String(chars,0,readLen));
        }
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        try {
            fileReader.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

FileWriter

image.png

/**
     * 记得追加模式和close/flush
     */
    public static void writeFileVersion1(){
        FileWriter fileWriter = null;
        try {
            //默认是覆盖模式
            //fileWriter = new FileWriter("e:\\text_3.txt");
            fileWriter = new FileWriter("e:\\text_3.txt",true);
            fileWriter.write("this is test\n");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                //底层使用了sun.nio.cs.StreamEncoder.writeBytes,
                //底层其实是使用了Nio 的 bytebuffer做数据实际刷盘操作
                //所以其实底层还是使用了byte作为基本单位来进行操作
                fileWriter.close(); 
                //flush也会写入数据,但是没有执行close操作
//                fileWriter.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
注意点:
-   使用完毕后如果不执行close操作或者flush操作则并不会将实际的内容持久化输出到磁盘当中。
-   在使用`FileWriter`的构造函数中,第二个参数,用于声明当前的数据写入之后是否会覆盖原先的内容。
-   关闭文件流, 等价 flush() + 关闭

3. PipedInputStream 和 PipedOutputStream

管道流通常会被我们用在不同的线程之间进行消息通讯中使用。

例如下边这段代码,定义了一个子线程,该线程会往PipedOutputStream中写入数据,然后main线程会去PipedInputStream中读取子线程写入的数据。

/**
 * out底层是用了一个字节缓冲数组buffer接收数据传输,当这个字节缓冲数组满了之后通过使用notifyAll唤醒in内部读数据的线程
 * 所以这里面的底层原理还是离不开sync和notifyall
 * @param args
 */
public static void main(String[] args) {
    try (PipedOutputStream out = new PipedOutputStream()) {
        PipedInputStream in = new PipedInputStream(out);
        new Thread(() -> {
            try {
                Thread.sleep(2000);
                String item = new String();
                for (int i=0;i<1000;i++){
                    item = item + "1";
                }
                out.write(item.getBytes(StandardCharsets.UTF_8));
                out.close();
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        int receive = 0;
        System.out.println("try");
        byte[] temp = new byte[1024];
        //等待子线程往pipedOutputStream内部写入数据
        while ((receive = in.read(temp)) != -1) {
            String outStr = new String(temp,0,receive);
            System.out.println(outStr);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

五、常用处理流

1. 缓冲流:BufferedInputStream 和 BufferedOutputStream

缓冲字节流的出现就是通过引入一个缓冲Buffer的来优化这种多次读写操作导致的io性能不足的设计。

缓冲字节处理流的内部存在一个叫做buffer的缓冲区,每次写入数据的时候都会往缓冲区中写入数据,
当缓冲区积攒了足够多的数据后再一次性写回到数据源中。这种设计思路相比原先的一次写一次io要高效更多。
1660051245701.png

BufferedInputStream和BufferedOutputStream如何使用?

//基于字节为单位实现文件拷贝效果
public static void fileCopyByByte(String sourceFile,String destPath) throws IOException {
    BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream(sourceFile));
    BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(destPath));
    byte[] tempByte = new byte[1024];
    int tempByteLen = 0;
    while ((tempByteLen = bufferedInputStream.read(tempByte)) != -1) {
        bufferedOutputStream.write(tempByte,0,tempByteLen);
    }
    bufferedOutputStream.close();
    bufferedInputStream.close();
}

注意点:

-  `BufferedInputStream`和`BufferedOutputStream`内部采用了修饰器模式,
通过构造函数注入一个`java.io.InputStream/java.io.OutputStream`对象,从而达到可以注入多种字节输入输出流。

-  当关闭流的时候只需要关闭外界包装的字节缓冲流即可,它的close操作会触发对应的字节流内部的close函数。
-  使用缓冲处理流的时候要记得调用close或者flush操作才能将实际数据真正写入到磁盘当中。

BufferedReader和BufferedWriter的使用

  1. BufferedReader
字符缓冲输入流:BufferedReader。
作用:提高字符输入流读取数据的性能,除此之外多了按照行读取数据的功能。
方法:public String readLine() 读取一行数据返回,如果读取没有完毕,无行可读返回null
BufferedReader类的使用

public class BufferedReader_ {
    public static void main(String[] args) throws Exception {

        String filePath = "e:\a.java";
        //创建bufferedReader
        BufferedReader bufferedReader = new BufferedReader(new FileReader(filePath));
        //读取
        String line; //按行读取, 效率高
        //说明
        //1. bufferedReader.readLine() 是按行读取文件
        //2. 当返回null 时,表示文件读取完毕
        while ((line = bufferedReader.readLine()) != null) {
            System.out.println(line);
        }

        //关闭流, 这里注意,只需要关闭 BufferedReader ,因为底层会自动的去关闭 节点流
        //FileReader。
        /*
            public void close() throws IOException {
                synchronized (lock) {
                    if (in == null)
                        return;
                    try {
                        in.close();//in 就是我们传入的 new FileReader(filePath), 关闭了.
                    } finally {
                        in = null;
                        cb = null;
                    }
                }
            }

         */
        bufferedReader.close();
    }
}
  1. BufferedWriter
字符缓冲输出流:BufferedWriter。
作用:提高字符输出流写取数据的性能,除此之外多了换行功能
构造器:public BufferedWriter​(Writer w) 可以把低级的字符输出流包装成一个高级的缓冲字符输出流管道,从而提高字符输出流写数据的性能
方法: public void newLine() 换行操作
public class BufferedWriter_ {
    public static void main(String[] args) throws IOException {
        String filePath = "e:\\ok.txt";
        //创建BufferedWriter
        //说明:
        //1. new FileWriter(filePath, true) 表示以追加的方式写入
        //2. new FileWriter(filePath) , 表示以覆盖的方式写入
        BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(filePath));
        bufferedWriter.write("hello!");
        bufferedWriter.newLine();//插入一个和系统相关的换行
        bufferedWriter.write("hello2!");
        bufferedWriter.newLine();
        bufferedWriter.write("hello3!");
        bufferedWriter.newLine();

        //说明:关闭外层流即可 , 传入的 new FileWriter(filePath) ,会在底层关闭
        bufferedWriter.close();

    }
}

2. 打印流:PrintStream 和 PrintWriter

作用:打印流可以实现方便、高效的打印数据到文件中去。
打印流一般是指:PrintStream,PrintWriter两个类。

可以实现打印什么数据就是什么数据,
例如打印整数97写出去就是97,打印booleantrue,写出去就是true
  1. PrintStream

image.png

image.png

public class PrintStream_ {
    public static void main(String[] args) throws IOException {

        PrintStream out = System.out;
        //在默认情况下,PrintStream 输出数据的位置是 标准输出,即显示器
        /*
             public void print(String s) {
                if (s == null) {
                    s = "null";
                }
                write(s);
            }
         */
        out.print("john, hello");
        //因为print底层使用的是write , 所以我们可以直接调用write进行打印/输出
        out.write("你好".getBytes());
        out.close();

        //我们可以去修改打印流输出的位置/设备
        //1. 输出修改成到 "e:\f1.txt"
        //2. "hello,~" 就会输出到 e:\f1.txt
        //3. public static void setOut(PrintStream out) {
        //        checkIO();
        //        setOut0(out); // native 方法,修改了out
        //   }
        System.setOut(new PrintStream("e:\f1.txt"));
        System.out.println("hello, ~");
    }
}
  1. PrintWriter

image.png image.png

public class PrintWriter_ {
    public static void main(String[] args) throws IOException {
        //PrintWriter printWriter = new PrintWriter(System.out);
        PrintWriter printWriter = new PrintWriter(new FileWriter("e:\\f2.txt"));
        printWriter.print("hi, 北京你好~~~~");
        printWriter.close(); //flush + 关闭流, 才会将数据写入到文件..
    }
}
  1. PrintStream和PrintWriter的区别
打印数据功能上是一模一样的,都是使用方便,性能高效(核心优势)
PrintStream继承自字节输出流OutputStream,支持写字节数据的方法。
PrintWriter继承自字符输出流Writer,支持写字符数据出去。

3. 转换流:InputStreamReader 和 OutputStreamWriter

image.png

1. InputStreamReader

在读取文件时,当代码编码和文件编码不一致时,就会导致乱码,字符输入转换流就是解决乱码而生的。

字符输入转换流的作用:

使用字符输入转换流可以提取文件(GBK)的原始字节流,原始字节不会存在问题。
然后把字节流以指定编码转换成字符输入流,这样字符输入流中的字符就不乱码
字符输入转换流:InputStreamReader,可以把原始的字节流按照指定编码转换成字符输入流。

构造器
public InputStreamReader(InputStream is) 可以把原始的字节流按照代码默认编码转换成字符输入流。几乎不用,与默认的FileReader一样。

public InputStreamReader(InputStream is ,String charset)  可以把原始的字节流按照指定编码转换成字符输入流,这样字符流中的字符就不乱码了(重点)
/**
 * @author Maple
 * @version 1.0
 * 演示使用 InputStreamReader 转换流解决中文乱码问题
 * 将字节流 FileInputStream 转成字符流  InputStreamReader, 指定编码 gbk/utf-8
 */
public class InputStreamReader_ {
    public static void main(String[] args) throws IOException {

        String filePath = "e:\\a.txt";
        //解读
        //1. 把 FileInputStream 转成 InputStreamReader
        //2. 指定编码 gbk
        //InputStreamReader isr = new InputStreamReader(new FileInputStream(filePath), "gbk");
        //3. 把 InputStreamReader 传入 BufferedReader
        //BufferedReader br = new BufferedReader(isr);

        //将2 和 3 合在一起
        BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(filePath), "gbk"));

        //4. 读取
        String s = br.readLine();
        System.out.println("读取内容=" + s);
        //5. 关闭外层流
        br.close();
    }
}

2. OutputStreamWriter

字符输入转换流:OutputStreamWriter

可以把字节输出流按照指定编码转换成字符输出流。

构造器

1. public OutputStreamWriter(OutputStream os)  可以把原始的字节输出流按照代码默认编码转换成字符输出流。几乎不用。
2. public OutputStreamWriter(OutputStream os,String charset) 可以把原始的字节输出流按照指定编码转换成字符输出流(重点)
/**
 * @author Maple
 * @version 1.0
 * 演示 OutputStreamWriter 使用
 * 把FileOutputStream 字节流,转成字符流 OutputStreamWriter
 * 指定处理的编码 gbk/utf-8/utf8
 */
public class OutputStreamWriter_ {
    public static void main(String[] args) throws IOException {
        String filePath = "e:\maple.txt";
        String charSet = "utf-8";
        OutputStreamWriter osw = new OutputStreamWriter(new FileOutputStream(filePath), charSet);
        osw.write("hi");
        osw.close();
        System.out.println("按照 " + charSet + " 保存文件成功~");
    }
}

4. 对象流:ObjectInputStream 和 ObjectOutputStream

1. ObjectOutputStream

对象序列化(对象字节输出流,写对象数据到磁盘文件):
作用:以内存为基准,把内存中的对象存储到磁盘文件中去,称为对象序列化。
使用到的流是对象字节输出流:ObjectOutputStream

image.png ObjectOutputStream序列化方法

image.png

public class ObjectOutStream_ {
    public static void main(String[] args) throws Exception {
        //序列化后,保存的文件格式,不是存文本,而是按照他的格式来保存
        String filePath = "e:\\data.dat";

        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(filePath));

        //序列化数据到 e:\\data.dat
        oos.writeInt(100);// int -> Integer (实现了 Serializable)
        oos.writeBoolean(true);// boolean -> Boolean (实现了 Serializable)
        oos.writeChar('a');// char -> Character (实现了 Serializable)
        oos.writeDouble(9.5);// double -> Double (实现了 Serializable)
        oos.writeUTF("Maple");//String
        //保存一个dog对象
        oos.writeObject(new Dog("旺财", 10, "日本", "白色"));
        oos.close();
        System.out.println("数据保存完毕(序列化形式)");
    }
}

总结:

1. 对象序列化的含义是什么?

    把对象数据存入到文件中去。
    
2. 对象序列化用到了哪个流?

    对象字节输出流ObjectOutputStram
    public void writeObject(Object obj)
    
3. 序列化对象的要求是怎么样的?
    
    对象必须实现序列化接口

2. ObjectInputStream

对象反序列化(将磁盘文件的数据恢复到内存中):
作用:以内存为基准,把存储到磁盘文件中去的对象数据恢复成内存中的对象,称为对象反序列化。

image.png ObjectInputStream序列化方法

image.png

public class ObjectInputStream_ {
    public static void main(String[] args) throws IOException, ClassNotFoundException {

        //指定反序列化的文件
        String filePath = "e:\\data.dat";

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filePath));

        //读取
        //1. 读取(反序列化)的顺序需要和你保存数据(序列化)的顺序一致
        //2. 否则会出现异常

        System.out.println(ois.readInt());
        System.out.println(ois.readBoolean());

        System.out.println(ois.readChar());
        System.out.println(ois.readDouble());
        System.out.println(ois.readUTF());


        //dog 的编译类型是 Object , dog 的运行类型是 Dog
        Object dog = ois.readObject();
        System.out.println("运行类型=" + dog.getClass());
        System.out.println("dog信息=" + dog);//底层 Object -> Dog

        //这里是特别重要的细节:

        //1. 如果我们希望调用Dog的方法, 需要向下转型
        //2. 需要我们将Dog类的定义,放在到可以引用的位置
        Dog dog2 = (Dog)dog;
        System.out.println(dog2.getName()); //旺财..

        //关闭流, 关闭外层流即可,底层会关闭 FileInputStream 流
        ois.close();
    }
}

总结:

1. 对象反序列化的含义是什么?
    
    把磁盘中的对象数据恢复到内存的Java对象中。

2. 对象反序列化用到了哪个流?

    对象字节输入流ObjectInputStram
    public Object readObject()

六、Properties

image.png

image.png image.png

  1. Properties属性集对象

    其实就是一个Map集合,但是我们一般不会当集合使用,因为HashMap更好用。
    
  2. Properties核心作用:

    1. Properties代表的是一个属性文件,可以把自己对象中的键值对信息存入到一个属性文件中去。
    2. 属性文件:后缀是.properties结尾的文件,里面的内容都是 key=value,后续做系统配置信息的。
    
            比如:
                ip=127.0.0.1
                user=root
                password=admin
    
  3. Properties的API:

    Properties和IO流结合的方法:

    image.png

案例

image.png

  1. 使用properties类完成对mysql.properties的读取
public class Properties01 {
    public static void main(String[] args) throws IOException {
        //读取mysql.properties 文件,并得到ip, user 和 pwd
        BufferedReader br = new BufferedReader(new FileReader("src\mysql.properties"));
        String line = "";
        while ((line = br.readLine()) != null) { //循环读取
            String[] split = line.split("=");
            //如果我们要求指定的ip值
            if("ip".equals(split[0])) {
                System.out.println(split[0] + "值是: " + split[1]);
            }
        }
        br.close();
    }
}
  1. 使用properties类添加key-value到新文件 mysql2.properties中
public class Properties02 {
    public static void main(String[] args) throws IOException {
        //使用Properties 类来读取mysql.properties 文件

        //1. 创建Properties 对象
        Properties properties = new Properties();
        //2. 加载指定配置文件
        properties.load(new FileReader("src\mysql.properties"));
        //3. 把k-v显示控制台
        properties.list(System.out);
        //4. 根据key 获取对应的值
        String user = properties.getProperty("user");
        String pwd = properties.getProperty("pwd");
        System.out.println("用户名=" + user);
        System.out.println("密码是=" + pwd);
    }
}
  1. 使用properties类完成对mysql2.properties的读取,并修改某个key-value
public class Properties03 {
    public static void main(String[] args) throws IOException {
        //使用Properties 类来创建 配置文件, 修改配置文件内容

        Properties properties = new Properties();
        //创建
        //1.如果该文件没有key 就是创建
        //2.如果该文件有key ,就是修改
        /*
            Properties 父类是 Hashtable , 底层就是Hashtable 核心方法
            public synchronized V put(K key, V value) {
                // Make sure the value is not null
                if (value == null) {
                    throw new NullPointerException();
                }

                // Makes sure the key is not already in the hashtable.
                Entry<?,?> tab[] = table;
                int hash = key.hashCode();
                int index = (hash & 0x7FFFFFFF) % tab.length;
                @SuppressWarnings("unchecked")
                Entry<K,V> entry = (Entry<K,V>)tab[index];
                for(; entry != null ; entry = entry.next) {
                    if ((entry.hash == hash) && entry.key.equals(key)) {
                        V old = entry.value;
                        entry.value = value;//如果key 存在,就替换
                        return old;
                    }
                }

                addEntry(hash, key, value, index);//如果是新k, 就addEntry
                return null;
            }

         */
        properties.setProperty("charset", "utf8");
        properties.setProperty("user", "汤姆");//注意保存时,是中文的 unicode码值
        properties.setProperty("pwd", "888888");

        //将k-v 存储文件中即可
        properties.store(new FileOutputStream("src\mysql2.properties"), null);
        System.out.println("保存配置文件成功~");
    }
}

总结:

1. 可以存储Properties属性集的键值对数据到属性文件中去:
    void store​(Writer writer, String comments)

2. 可以加载属性文件中的数据到Properties对象中来:
    void load​(Reader reader)