Javase基础——IO流


旨在复习回顾,很多细碎的知识点请看文档:JDK8文档

一、File类

1、文件路径

正斜杠,又称左斜杠,符号是"/";反斜杠,也称右斜杠,符号是"\"

Unix/Linux中,路径的分隔采用正斜杠"/",比如"/home/hutaow";而在Windows中,路径分隔采用反斜杠"",比如"C:\Windows\System"

在Java当中反斜杠代表的是转义:

比如:

制表符(也叫制表位)的功能是在不使用表格的情况下在垂直方向按列对齐文本,就是咱们的Tab键。

- '\' 将双引号转义为真正的双引号

  • ‘\r’ (回车):即将光标回到当前行的行首(而不会换到下一行),之后的输出会把之前的输出覆盖
  • ‘\n’ 换行,换到当前位置的下一位置,而不会回到行首

2、案例:递归遍历文件

构造方法、方法过多,看文档即可

【案例】列出D:\Code\image文件夹下的所有的图片:

目录结构,aa,bb里边有一些图片和txt文件

在这里插入图片描述

思路:

遍历D:\Code\image下拿到**过滤后的**路径的路径数组,数组不为空且有文件的情况下,如果是文件夹递归则进去,直到不是文件夹,然后输出文件,否则输出当前目录下的文件!

File[]listFiles(FilenameFilter filter)通过过滤器过滤文件,过滤通过文件名过滤,返回文件数组
package com.io;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;

/**
 * 列出D:\Code\image文件夹下的所有的图片:
 */
public class Demon01 {
    public static void main(String[] args) throws IOException {
        String PATH = "d:\\Code\\image";
        File file = new File(PATH);// 拿到子文件对象
        listAll(file);

    }
    // 单独列出方法获取目录下的图片(递归遍历,过滤)
    public static void listAll(File parent)  {
        MyFilter myFilter = new MyFilter();
        File[] children = parent.listFiles(myFilter);
        // 过滤后 children数组中只有本次递归后的文件夹或者图片

        for (int i = 0; i < children.length; i++) {
            // 如果子文件是个文件夹,则递归调用
            if(!children[i].isFile()){
                listAll(children[i]);
            } else {
                System.out.println(children[i].getName());
            }
        }
    }

    // 自定义文件过滤类,实现FilenameFilter接口
    static class MyFilter implements FilenameFilter {
        @Override
        public boolean accept(File dir, String name) {
            // 注意过滤条件!!! : 这里的dir是当前的目录的file类(父路径),而不是子文件的. name是子文件的名称
            // 因此要判断子文件是否存在(子文件是否为文件夹) —————— 则要进到这个文件的目录里边,而不是直接判断dir的
            // 及 new File(dir, name) ===>
            // File(File parent,String child)	根据指定的父路径对象和文件路径创建一个新的File对象实例
            if(name.endsWith(".png") || new File(dir, name).isDirectory()){
                return true;
            }else {
                return false;
            }
        }
    }

}

同理:递归创建,删除文件也类似,都是递归遍历!

二、IO流分类与体系

流的分类

1、 按照流向分

  • 输入流: 只能从中读取数据,而不能向其写入数据。
  • 输出流:只能向其写入数据,而不能向其读取数据。

在这里插入图片描述

其实计算机在读取文件的时候是很麻烦的:

在这里插入图片描述

当然系统级别的方法调用我们可以暂时不用考虑。但是我们确确实实看到一个文件在传输过程中经历了很多次的拷贝,IO的性能本来就不是很高,所以后来又有了零拷贝、Nio等技术,这些知识点我们计划在附加课讲解。

2 、按照操作单元划分

  • 字节流:是一个字节一个字节的读取或写入
  • 字符流:是一个字符一个字符的读取或写入,一个字符就是两个字节,主要用来处理字符。

3、 按照角色划分

  • 节点流:直接从/向一个特定的IO设备(如磁盘,网络)读/写数据的流,称为节点流。
  • 处理流:“连接”在已存在的流(节点流或处理流)之上通过对数据的处理为程序提供更为强大的读写功能的流。

在这里插入图片描述

| 分类 | 字节输入流 |字节输出流|字符输入流|字符输出流|

分类字节输入流字节输出流字符输入流字符输出流
抽象基类InputStreamOutputStreamReaderWriter
访问文件FileInputStreamFileOutputStreamFileReaderFileWriter
访问数组ByteArrayInputStreamByteArrayOutputStreamCharArrayReaderCharArrayWriter
访问字符串StringReaderStringWriter
缓冲流(处理)BufferedInputStreamBufferedOutputStreamBufferedReaderBufferedWriter
操作对象ObjectInputStreamObjectOutputStream

在这里插入图片描述

三、流的案例

1、继承结构

InputStream和OutputStream

在这里插入图片描述

Reader和Writer

在这里插入图片描述

2、流到底怎么使用?

1)将一个流对象插在一个节点上

其实通过名字我们就可以很好的理解了:FileInputStream就是怼在文件上的输入流啊!

public abstract class InputStream implements Closeable

InputStream本身是抽象类,我们需要使用它的子类去构造对象:

InputStream inputStream = new FileInputStream(file);

既然是输入流就要一点一点的往内存里读数据!

在这里插入图片描述

其实inputStream的方法并不多,关键在于几个read方法,管子已经插上了,接下来就是读了。

// 读一个字节
int read = inputStream.read();

// 一次性读1024个字节到那个内存数组
int read = inputStream.read(new byte[1024]);

// 从第0个字节开始读,读120个字节
int read = inputStream.read(new byte[1024],0,120);

(2)使用read()方法读取</font>

第一种读取方式:单个字节读取——效率不高!

【案例】读取D:\code.txt

在这里插入图片描述

package com.io;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

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

        // 拿到文件字节流对象
        InputStream ins = new FileInputStream("d:/code.txt");
        int data;
        while((data = ins.read())  != -1){
            System.out.println(data);
        }


    }
}

read就是每次读出的字节,直到-1就停止。

使用read(byte[] byte)读取

第二种读取方式:一次读取多个字节,多一个字节数组!

package com.io;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;

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

        // 拿到文件字节流对象
        InputStream ins = new FileInputStream("d:/code.txt");
        byte[] bf = new byte[3];// 每次读三个字节
        int cnt = 0;
        while((cnt = ins.read(bf)) != -1){// cnt返回读了多少个字节
            System.out.println(cnt);
            System.out.println(new String(bf, 0, cnt));
        }
    }
}

在这里插入图片描述

(4)输出流的使用

输出流即写

注: 在定义文件输出流时,有两个参数,第二个如果是true代表追加文件,如果false代表覆盖文件,意思就是如果人家这个文件原来有内容,就覆盖的没了,这一点要注意。

package com.io;

import java.io.*;

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

        // true:追加,false覆盖
        OutputStream outputStream = new FileOutputStream("d:/code.txt",false);
        // 一个一个字节的写入
        outputStream.write(97);// a
    }
}

原来的内容被a覆盖

在这里插入图片描述

(5)资源的释放

第一种:调用资源.close()方法,记得抛出异常即可!

 public static void main(String[] args) {
        // 定义资源
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            inputStream = new FileInputStream("D:/code/a.txt");
            outputStream = new FileOutputStream("D:/code/b.txt",true);

            byte[] buf = new byte[3];
            int len;
            while ((len =inputStream.read(buf))  != -1){
                outputStream.write(buf,0,len);
            }
        }  catch (IOException e) {
            e.printStackTrace();
         // 最终无论如何,都要释放资源
        } finally {
            
            // 这里判空非常的关键!!!!!!!
            // 如果你是空的,你还要释放吗,本来无异常空的在释放就是错的!!!!!!!
            
            if(inputStream != null){
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(outputStream != null){
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

第二种:

以上代码如此繁杂,jdk1.7之后,很多资源类的类都实现了AutoCloseable接口

实现了这个接口的类可以在try中定义资源,并会主动释放资源:

ublic static void main(String[] args) {
    try(InputStream inputStream = new FileInputStream("D:/code/a.txt");
        OutputStream outputStream= new FileOutputStream("D:/code/b.txt",true)) {
        byte[] buf = new byte[3];
        int len;
        while ((len =inputStream.read(buf))  != -1){
            outputStream.write(buf,0,len);
        }
    }  catch (IOException e) {
        e.printStackTrace();
    }
}
案例:文件拷贝
package com.io;

import java.io.*;

/**
 * 文件拷贝:将d盘的code.txt复制到e盘下
 *          1.先读d盘的code.txt
 *          2.边度边写
 *          3.最后释放资源
 */

public class TestInputStream {
    public static void main(String[] args) throws IOException {
        // 1.读
        InputStream inputStream = new FileInputStream("d:/code.txt");
        OutputStream outputStream = new FileOutputStream("e:/code.txt");

        byte[] bf = new byte[1024];// 1K 1K的读
        int len;
        while((len = inputStream.read(bf)) != -1){
            outputStream.write(bf, 0, len);
        }

    }
}

案例:字符流读取文件
    /**
     * 字符流读取文件
     */
    @Test
    public void testReader() throws IOException {
        Reader reader = new FileReader("d:/code.txt");// 拿到文件对象
        BufferedReader br = new BufferedReader(reader);// 赋予文件对象读的操作权力
        String str;
        
        while((str = br.readLine()) != null){// 一行一行的读
            System.out.println(str);
        }
        reader.close();
        br.close();

    }
案例:字符流写文件
// 因为有输入,放在主函数中测试
public class TestReader {
    public static void main(String[] args) throws IOException {
        Writer write = new FileWriter("d:/code.txt",true);// 可追加
        BufferedWriter bw = new BufferedWriter(write);
        Scanner scan = new Scanner(System.in);

        int tmp = 5;
        while (tmp > 0){
            System.out.print("请输入内容:");
            String words = scan.next();
            tmp --;
            bw.write(words);
            bw.newLine();// 换行
            bw.flush();
        }
    }

四、序列化与反序列化

序列化与反序列化:Java中对对象进行读写操作

  • 序列化:将对象写入到IO流中,说的简单一点就是将内存模型的对象变成字节数字,可以进行存储和传输。
  • 反序列化:从IO流中恢复对象,将存储在磁盘或者从网络接收的数据恢复成对象模型。
  • 使用场景:所有可在网络上传输的对象都必须是可序列化的,否则会出错;所有需要保存到磁盘的Java对象都必须是可序列化的。

该对象必须实现Serializable接口,才能被序列化。

1、对象序列化与反序列化

对象代码:

class  Student implements Serializable {
    private String name;
    private int age;

    public Student(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 "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

序列化:

    /**
     * 使用ObjectOutputStream实现对象的序列化————写入对象
     * 要求:序列化类必须实现接口
     */
    @Test
    public void testObjectOuptStream() throws Exception{
        //1. 获取到将要写入的文件对象,并创建序列化对象
        OutputStream fos = new FileOutputStream("d:\\code.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);


        //2. 序列化(写入操作)
        Student student = new Student("张三",19);
        oos.writeObject(student);

        //3. 关闭(自带flush()方法了)
        oos.close();
        System.out.println("序列化完毕");
    }

2、序列化版本号

我们知道,反序列化必须拥有class文件,但随着项目的升级,class文件也会升级,序列化怎么保证升级前后的兼容性呢?

Java序列化提供了一个``private static final long serialVersionUID` 的序列化版本号,只要版本号相同,即使更改了序列化属性,对象也可以正确被反序列化回来。

public class Student implements Serializable {
    //序列化版本号
    private static final long serialVersionUID = 1111013L;
    private String name;
    private int age;
    
    private int money;// 修改
    //省略构造方法及get,set
}

如果反序列化使用的版本号与序列化时使用的不一致,反序列化会报InvalidClassException’异常。

在这里插入图片描述

序列化版本号可自由指定,如果不指定,JVM会根据类信息自己计算一个版本号,这样随着class的升级、代码的修改等因素无法正确反序列化;

不指定版本号另一个明显隐患是,不利于jvm间的移植,可能class文件没有更改,但不同jvm可能计算的规则不一样,这样也会导致无法反序列化。

什么情况下需要修改serialVersionUID呢:

  • 如果只是修改了方法,反序列化不容影响,则无需修改版本号;
  • 如果只是修改了静态变量,瞬态变量(transient修饰的变量),反序列化不受影响,无需修改版本号。

IDEA配置该操作可以看看这篇博客:(8条消息) IDEA 配置Serializable的快捷键快速生成serialVersionUID_程序之大道至简的博客-CSDN博客_idea生成serialversionuid快捷键

3、总结

  1. 所有需要网络传输的对象都需要实现序列化接口。
  2. 对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。
  3. 如果想让某个变量不被序列化,使用transient修饰。
  4. 序列化对象的引用类型成员变量,也必须是可序列化的,否则,会报错。
  5. 反序列化时必须有序列化对象的class文件。
  6. 同一对象序列化多次,只有第一次序列化为二进制流,以后都只是保存序列化编号,不会重复序列化。
  7. 建议所有可序列化的类加上serialVersionUID 版本号,方便项目升级。

五、综合案例

写一个程序,能够给一个商品文件进行增、删、改、查。

在这里插入图片描述

第一列是编号,第二列是商品名称,第三列是价格。

骨架代码:

import java.util.Scanner;

public class Shop {

    private static Scanner scanner = new Scanner(System.in);

    public static void main(String[] args) {

        while (true) {
            System.out.println("请选择功能:1、插入新商品 2、删除商品 3、修改商品 4、显示所有商品 5、查询一个商品 6、退出");
            int function = scanner.nextInt();
            switch (function){
                case 1:
                    insert();
                    break;
                case 2:
                    delete();
                    break;
                case 3:
                    update();
                    break;
                case 4:
                    displayAll();
                    break;
                case 5:
                    findone();
                    break;    
                case 6:
                    System.exit(-1);
                    break;
            }
        }
    }

    private static void findOne() {
        System.out.println("请输入商品编号:");
        int id = scanner.nextInt();
        // 思路一:一行一行的读,找到为止

        // 思路二:全部读到内存,内存里找
    }

    private static void update() {
        System.out.println("请输入商品编号:");
        // 全部拿出来,更新后覆盖
    }

    private static void delete() {
        System.out.println("请输入商品编号:");
        // 全部拿出来,删除后覆盖
    }

    private static void insert() {
        System.out.println("请输入商品编号:");
        // 最简单,直接追加
    }

    private static class Goods{
        private int id;
        private String name;
        private int price;

        public Goods() {
        }

        public Goods(int id, String name, int price) {
            this.id = id;
            this.name = name;
            this.price = price;
        }

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

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

        public int getPrice() {
            return price;
        }

        public void setPrice(int price) {
            this.price = price;
        }
    }
}

【Code】:

Shop类:

package com.test;

public  class Goods{
    private int id;
    private String name;
    private int price;

    public Goods() {
    }

    public Goods(int id, String name, int price) {
        this.id = id;
        this.name = name;
        this.price = price;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

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

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    @Override
    public String toString() {
        return "Goods{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", price=" + price +
                '}';
    }
}


核心代码:

package com.test;

import java.io.*;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Scanner;

public class Shop {

    static final String PATH = "d:\\code.txt";
    public static Scanner scanner = new Scanner(System.in);

    public static void main(String[] args) {
        while (true) {
            System.out.println("请选择功能:1、插入新商品 2、删除商品 3、修改商品 4、显示所有商品 5、查询一个商品 6、退出");
            int function = scanner.nextInt();
            switch (function){
                case 1:
                    insert();
                    break;
                case 2:
                    delete();
                    break;
                case 3:
                    update();
                    break;
                case 4:
                    displayAll();
                    break;
                case 5:
                    findOne();
                    break;
                case 6:
                    System.exit(-1);
                    break;
            }
        }
    }

    /**
     * 插入操作:直接写入磁盘即可,可以特判id(主键)是否重复维护程序的合理行
     */
    /**
     * 1.这里一不留意调了一天的bug:注意插入时若文本为空(没有任何数据要特批,不然死循环)————论idedebug的强大功能!!
     * 2.若:刚刚开始你若没在文本时预填有任何数据(即为空),那么一定就要保证你的文本是空的(一个空格\换行都不行)————没完全清空文本,找了一天
     *      不然findAllGoods()函数里就会爆异常,读的时候你以为它是空的,本应不会进入到while进行读取,其实不然(存在空白的空格符号),
     *      那么就导致读文本不为空,那么就存在三个数据中为空的现在,你把空转为整形或者字符串都是错的!!!
     */
    private static void insert() {
        boolean flag = true;
        Integer id = null;
        List<Goods> allGoods = findAllGoods();
        while (flag){
            System.out.println("请输入你要添加的商品编号:");
            id = scanner.nextInt();

            // 刚刚开始可能为空
            if(allGoods.size() == 0) break;

            for (Goods goods : allGoods) {
                System.out.println(goods.getId());
                flag = (goods.getId() == id);
            }
            if(flag){
                System.out.println("商品已经存在");
            }
        }
        System.out.println("请输入你要添加的商品名称:");
        String name = scanner.next();
        System.out.println("请输入你要添加的商品的价格:");
        int price = scanner.nextInt();

        List<Goods> new_list = new ArrayList<>();
        new_list.add(new Goods(id, name, price));
        writeAll(new_list, true);
    }

    /**
     * 删除操作:由于不能直接在磁盘中删除
     *         先将磁盘的数据读出,存到链表
     *         在遍历链表找到要删除的id,删掉该节点
     *         然后再将经过删除后的链表写回磁盘即可
     */
    private static void delete() {
        List<Goods> allGoods = findAllGoods();
        System.out.println("请输入你想删除的物品编号:");
        int id = scanner.nextInt();

        Iterator<Goods> iterator = allGoods.iterator();
        while (iterator.hasNext()){
            Goods goods = iterator.next();
            if(goods.getId() == id){
                iterator.remove();
            }
        }
        // 写回磁盘
        writeAll(allGoods, false);

    }

    /**
     * 更新操作:根据id进行更新,可更新出id外的其他信息(同删除操作)
     */
    private static void update() {
        List<Goods> allGoods = findAllGoods();
        System.out.println("请输入你想更新的物品编号:");
        int id = scanner.nextInt();
        // 读出
        Iterator<Goods> iterator = allGoods.iterator();
        // 更新
        while (iterator.hasNext()){
            Goods goods = iterator.next();
            if(goods.getId() == id){
                System.out.println("请输入新的物品名称:");
                String name = scanner.next();
                System.out.println("请输入新的物品价格:");
                int price = scanner.nextInt();
                goods.setName(name);
                goods.setPrice(price);
            }
        }
        // 写回磁盘
        writeAll(allGoods, false);
    }

    /**
     * 展示所有商品信息:读取所有信息,打印即可
     */
    private static void displayAll() {
        for (Goods good : findAllGoods()) {
            System.out.println(good.getId() + " " + good.getName() + " " + good.getPrice());
        }
    }

    /**
     * 查询操作:根据id在链表中查找即可,然后输出
     */
    private static void findOne() {
        System.out.println("请输入你想查询的物品编号:");
        int id = scanner.nextInt();
        for (Goods good : findAllGoods()) {
            if(good.getId() == id){
                System.out.println(good.getId() + " " + good.getName() + " " + good.getPrice());
            }
        }
    }

    /**
     * 将链表中的所有商品写到磁盘中(字符流来写, 一行一行的写)
     * @param opt
     */
    private static void writeAll(List<Goods> list, boolean opt) {
        Writer writer = null;
        BufferedWriter bw = null;
        try {
            writer = new FileWriter(Shop.PATH, opt);
            bw = new BufferedWriter(writer);

            for (Goods goods : list) {
                bw.write(goods.getId() + " " + goods.getName() + " " + goods.getPrice());
                bw.newLine();
                bw.flush();
            }
            
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            try {
                bw.close();
                writer.close();
            }catch (IOException e){
                e.printStackTrace();
            }
        }
    }

    /**
     * 从磁盘中读取数据,存到链表中(字符流一行一行的读)
     * @return
     */
    private static List<Goods> findAllGoods() {
        List<Goods> goodList = new ArrayList<>();
        Reader reader = null;
        BufferedReader bf = null;
        try{
            reader = new FileReader(Shop.PATH);
            bf = new BufferedReader(reader);
            String goodsStr;
            while((goodsStr = bf.readLine()) != null){
                String[] goodsElem = goodsStr.split(" ");
                Goods goods = new Goods(
                        Integer.parseInt(goodsElem[0]),
                        goodsElem[1],
                        Integer.parseInt(goodsElem[2]));
                goodList.add(goods);
            }
        }catch (IOException e){
            e.printStackTrace();
        }finally {
            if(bf != null){// ?
                try {
                    bf.close();
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
        }

        return goodList;
    }
}

总结:报错要善于利用Idea的debug调试工具,十分的关键!!!!

参考学系:有深度的javase课程(下),java入门进阶必须_哔哩哔哩_bilibili

  • 4
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值