11.IO流--黑马程序员

File、IO流

D:\code\黑马\code\IO-app

  • File:读写,操作文件

  • IO流:读写、操作文件中的数据

一、File

1. 概念

File是java.io.包下的类,File类的对象用于代表当前操作系统中的文件(可以是文件、或文件夹)

File类只能对文件本身进行操作,不能读写文件里面存储的数据

2. 构造器

2.1常用构造器
构造器说明
public File(String pathname)根据文件路径创建文件对象
public File(String parent, String child)根据父路径和子路径名字创建文件对象
public File(File parent, String child)根据父路径对应文件对象和子路径名字创建文件对象
public class Test1 {
    public static void main(String[] args) {
        //创建File对象的三种方式

        //1. 通过文件路径创建File对象
        //public File(String pathname)
        File f1 = new File("E:/A/a.txt");
        System.out.println(f1);//E:\A\a.txt

        //2. 通过父路径和子路径创建File对象
        //public File(String parent, String child)
        File f2 = new File("E:/A", "a.txt");
        System.out.println(f2);//E:\A\a.txt

        //3. 通过父路径对象和子路径创建File对象
        //public File(File parent, String child)
        File parentFile = new File("E:/A");
        File f3 = new File(parentFile, "a.txt");
        System.out.println(f3);//E:\A\a.txt
    }
}
2.1.1 注意
  • File对象既可以代表文件、也可以代表文件夹
  • Fil封装的对象仅仅是一个路径名,这个路径可以是存在的,也允许是不存在的
public class Test2 {
    public static void main(String[] args) {
        //File对象既可以代表文件、也可以代表文件夹。

        //File对象代表文件
        File f1 = new File("E:/A/a.txt");
        System.out.println(f1.exists());//true
        //File对象代表文件夹
        File f2 = new File("E:/A");
        System.out.println(f2.exists());//true

        //Fil封装的对象仅仅是一个路径名,这个路径可以是存在的,也允许是不存在的。
        File f3 = new File("E:/A/dw");//路径下的文件夹dw不存在
        System.out.println(f3.exists());//false
        File f4 = new File("E:/A/d.txt");//路径下的文件d.txt不存在
        System.out.println(f4.exists());//false
    }
}
2.2 路径分隔符写法
  1. 使用’/'表示路径分隔符
  2. 使用’\\'表示路径分隔符,第一个\表示转义
  3. 使用File类提供的常量File.separator
public class Test3 {
    public static void main(String[] args) {
        //路径分隔符三种写法

        //1.使用'/'表示路径分隔符
        File f1 = new File("E:/A/a.txt");
        System.out.println(f1);//E:\A\a.txt

        //2.使用'\\'表示路径分隔符
        File f2 = new File("E:\\A\\a.txt");
        System.out.println(f2);//E:\A\a.txt

        //3.使用File.separator表示路径分隔符
        File f3 = new File("E:" + File.separator + "A" + File.separator + "a.txt");
        System.out.println(f3);//E:\A\a.txt
    }
}
2.3 绝对路径与相对路径
  • 绝对路径:从盘符开始
    • D:\code\黑马\code\IO-app\src\a.txt
  • 相对路径:不带盘符,默认直接到当前工程下的目录寻找文件
    • 项目名为IO-app,项目路径为D:\code\黑马\code\IO-app,所以项目下文件的相对路径可以直接写成下面格式,如果直接在项目路径下,直接写文件名即可
    • src\a.txt
      在这里插入图片描述
public class Test4 {
    public static void main(String[] args) {
        //绝对路径和相对路径

        //1. 绝对路径:从盘符开始的路径,这种路径一般都是写死的。
        File f1 = new File("D:\\code\\黑马\\code\\IO-app\\src\\a.txt");
        System.out.println(f1.exists());//true

        //2. 相对路径:相对于当前项目而言的,这种路径建议写活的。
        File f2 = new File("src\\a.txt");
        System.out.println(f2.exists());//true
        //如果直接在项目下,不在src下,直接写a.txt即可。
    }
}

3. 常用方法

3.1 判断文件类型、获取文件信息的方法
方法说明
public boolean exists( )判断当前文件对象对应的文件路径是否存在,存在返回true
public boolean isFile( )判断当前文件对象指代的是否是文件,是文件返回true
public boolean isDirectory( )判断当前文件对象指代的是否是文件夹,是文件夹返回true
public String getName( )获取文件的名称(包含后缀)
public long length( )获取文件的大小,返回字节个数
public long lastModified( )获取文件的最后修改时间。
public String getPath( )获取创建文件对象时所使用的路径
public String getAbsolutePath( )获取文件对象的绝对路径
public class Test5 {
    public static void main(String[] args) {
        //判断文件类型、获取文件信息的方法
        File file1 = new File("src/a.txt");//相对路径
        File file3 = new File("D:\\code\\黑马\\code\\IO-app\\src\\a.txt");//绝对路径
        File file2= new File("src");
        //1.判断路径是否存在
        //public boolean exists( ),存在返回true,不存在返回false
        System.out.println(file1.exists());//true

        //2.判断是否是文件
        //public boolean isFile( ),是文件返回true,不是文件返回false
        System.out.println(file1.isFile());//true

        //3.判断是否是文件夹
        //public boolean isDirectory( ),是文件夹返回true,不是文件夹返回false
        System.out.println(file1.isDirectory());//false
        System.out.println(file2.isDirectory());//true

        //4.获取文件或文件夹名
        //public String getName( ),返回文件或文件夹名
        System.out.println(file1.getName());//a.txt
        System.out.println(file2.getName());//src

        //5.获取文件或文件夹的大小
        //public long length( ),返回文件或文件夹的大小,单位是字节
        System.out.println(file1.length());//6

        //6.获取文件或文件夹的最后修改时间
        //public long lastModified( ),返回文件或文件夹的最后修改时间,单位是毫秒
        long time = file1.lastModified();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd " +
                "HH:mm:ss");
        System.out.println(simpleDateFormat.format(time));//2023-08-09 11:05:36

        //7.获取常见File对象时使用的路径
        //public String getPath( ),返回常见File对象时使用的路径
        System.out.println(file1.getPath());//src\a.txt
        System.out.println(file3.getPath());//D:\code\黑马\code\IO-app\src\a.txt

        //8.获取绝对路径
        //public String getAbsolutePath( ),返回绝对路径
//无论创建File对象时使用的是绝对路径还是相对路径,都可以使用getAbsolutePath()方法获取绝对路径
        System.out.println(file1.getAbsolutePath());//D:\code\黑马\code\IO-app\src\a.txt
        System.out.println(file3.getAbsolutePath());//D:\code\黑马\code\IO-app\src\a.txt
    }
}

3.2 创建文件、删除文件的方法
创建文件方法说明
public boolean createNewFile( )创建一个新的空文件
public boolean mkdir( )只能创建一级文件夹
public boolean mkdirs( )可以创建多级文件夹
删除创建文件方法说明
public boolean delete( )默认只能删除文件或空文件夹,无法删除非空文件夹,删除后的文件不会放入回收站,不允许使用相对路径
public class Test {
    public static void main(String[] args) throws IOException {
        //1. 创建新的空文件
        //public boolean createNewFile( )
        //创建成功返回true,如果已有该文件则返回false
        File f1 = new File("D:\\code\\黑马\\code\\IO-app\\src\\b.txt");//当前不存在该文件
        //创建File对象时传入的必须是绝对路径,相对路径会报错
        System.out.println(f1.createNewFile());//true
        System.out.println(f1.createNewFile());//false
        //第二次创建失败,因为已经存在该文件

        //2. 创建空的一级文件夹
        //public boolean mkdir( )
        //创建成功返回true,如果已有该文件夹则返回false
        File f2 = new File("D:\\code\\黑马\\code\\IO-app\\src\\c");
        System.out.println(f2.mkdir());//true
        System.out.println(f2.mkdir());//false
        //如果父目录也不存在,则属于创建二级文件夹,会创建失败
        File f3 = new File("D:\\code\\黑马\\code\\IO-app\\src\\d\\e");
        System.out.println(f3.mkdir());//false

        //3. 创建空的多级文件夹
        //public boolean mkdirs( )
        //创建成功返回true,如果已有该文件夹则返回false
        File f4 = new File("D:\\code\\黑马\\code\\IO-app\\src\\d\\e");
        System.out.println(f4.mkdirs());//true
        System.out.println(f4.mkdirs());//false

        //4. 删除文件或文件夹,如果是文件夹,则必须是空的才能删除
        //public boolean delete( )
        //删除成功返回true,如果不存在该文件或文件夹或文件夹不为空则返回false
        File f5 = new File("D:\\code\\黑马\\code\\IO-app\\src\\a.txt");
        System.out.println(f5.delete());//true 删除有内容的文件
        File f6 = new File("D:\\code\\黑马\\code\\IO-app\\src\\b.txt");
        System.out.println(f6.delete());//true 删除空文件
        File f7 = new File("D:\\code\\黑马\\code\\IO-app\\src\\c");
        System.out.println(f7.delete());//true 删除空文件夹
        File f8 = new File("D:\\code\\黑马\\code\\IO-app\\src");
        System.out.println(f8.delete());//false 删除非空文件夹
    }
}
3.3 遍历文件夹的方法
3.3.1 遍历当前File对象的一级目录的方法
方法(只能遍历一级目录)说明
public String[ ] list( )获取当前目录下所有的"一级文件名称“到一个字符串数组中去返回
public File[ ] listFiles( )获取当前目录下所有的"一级文件对象"到一个文件对象数组中去返回
public class Test6 {
    public static void main(String[] args) {
        //获取当前目录下所有的"一级文件名称“到一个字符串数组中去返回
        //返回所有当前路径文件夹下的文件夹和文件(隐藏文件也会返回),不包含子文件夹中的内容
        //public String[] list( )
        File f1 = new File("src\\file");
        String[] names = f1.list();
        System.out.println(Arrays.toString(names));
        //[Test.java, Test1.java, Test2.java, Test3.java, 
        // Test4.java, Test5.java, Test6.java]

        //获取当前目录下所有的"一级文件对象”到一个File数组中去返回
        //public File[] listFiles( )
        File[] files = f1.listFiles();
        System.out.println(Arrays.toString(files));//数组中的元素是File对象
        //[src\file\Test.java, src\file\Test1.java, src\file\Test2.java,
        // src\file\Test3.java, src\file\Test4.java, src\file\Test5.java, 
        // src\file\Test6.java]
        System.out.println(files[0].getAbsolutePath());
        //D:\code\黑马\code\IO-app\src\file\Test.java
    }
}

使用ListFiles方法时的注意事项:

  • 当File对象是文件,或者路径不存在时,返回null
  • 当File对象是空文件夹时,返回一个长度为0的数组
  • 当File对象是一个有内容的文件夹时,将里面所有一级文件和文件夹的路径放在File数组中返回
  • 当File对象是一个文件夹,且里面有隐藏文件时,将里面所有文件和文件夹的路径放在File数组中返回,包含隐藏文件
  • 当File对象是一个文件夹,但是没有权限访问该文件夹时,返回null

案例

public class Test8 {
    public static void main(String[] args) {
        //将文件夹下的视频的序号从19开始重新命名
        //原文件格式: 1、... .mp4 2、... .mp4 3、... .mp4等

        //创建文件夹对象
        File f = new File("D:\\Java\\video");//该文件夹下有视频文件
        //获取文件夹下的所有文件
        File[] videos = f.listFiles();

        for (File video : videos) {
            String name = video.getName();
            String index = name.substring(0, name.indexOf("、"));
            String lastName = name.substring(name.indexOf("、"));
            String newName = (Integer.parseInt(index) + 18) + lastName;

            //重命名
            video.renameTo(new File(f,newName));
        }
    }
}

3.3.2 遍历当前File对象的多级子目录的方法(递归)
(1)前置知识:递归

概念

  • 递归是一种算法,在程序设计语言中广泛应用。
  • 从形式上说:方法调用自身的形式称为方法递归(recursion)。

递归的形式

  • 直接递归:方法自己调用自己。
  • 间接递归:方法调用其他方法,其他方法又回调方法自己。

递归三要素(解题的关键)

  1. 递归的公式
  2. 递归的终结点(出口)
  3. 递归的方向必须走向终结点

注意的问题

递归如果没有控制好终止,会出现递归死循环,导致栈内存溢出错误

public class tests {
    public static void main(String[] args) {
        say();//直接递归
        say1();//间接递归
    }
    
    //直接递归,下面代码会报错,因为递归太深了,没有出口,栈内存溢出
    public static void say(){
        System.out.println("say");
        say();
    }
    
    //间接递归,同样会报错,因为递归太深了,没有出口,栈内存溢出
    public static void say1(){
        System.out.println("say1");
        say2();
    }
    
    public static void say2(){
        say1();
    }
}

案例1:计算n的阶乘

需求:计算n的阶乘

分析:

  1. 公式:f(n) = f(n-1) * n
  2. 终结点:f(1) = 1
  3. 递归方向:根据传入参数从n减到1,到终结点后,根据返回的1逐步计算(必须走向终结点)
public class Test9 {
    public static void main(String[] args) {
        int reuslt = getJieCheng(5);
        System.out.println(reuslt);
    }

    public static int getJieCheng(int n) {
        if (n == 1) {
            return 1;
        }else {
            return n * getJieCheng(n - 1);
        }
    }
}

案例2:计算1到n的和

需求:计算1到n的和

分析:

  1. 公式:f(n) = f(n-1) + n
  2. 终结点:f(1) = 1
  3. 递归方向:根据传入参数从n减到1,到终结点后,根据返回的1逐步计算(必须走向终结点)
public class Test9 {
    public static void main(String[] args) {
        int reuslt = getSum(5);
        System.out.println(reuslt);
    }

    public static int getSum(int n) {
        if (n == 1) {
            return 1;
        }else {
            return n + getSum(n - 1);
        }
    }
}

案例3:猴子吃桃问题

需求:

  • 猴子第一天摘下若干桃子,当即吃了一半,觉得好不过瘾,于是又多吃了一个
  • 第二天又吃了前天剩余桃子数量的一半,觉得好不过瘾,于是又多吃了一个
  • 以后每天都是吃前一天剩余桃子数量的一半,觉得好不过瘾,又多吃了一个
  • 等到第10天的时候发现桃子只有1个了。
  • 需求:请问猴子第1天摘了多少个桃子?

分析:

  1. f(n) = f(n - 1)/2 - 1 => 2f(n + 1) = f(n) - 2 => f(n) = 2f(n + 1) + 2
    • 由于递归的方向要达到终结点,所以需要return的f中的参数必须每次+1
  2. 终结点:f(10) = 1
  3. 递归方向:从1加到10,达到终结点后逐步返回
public class Test9 {
    public static void main(String[] args) {
        int reuslt = peach(1);
        System.out.println(reuslt);
    }

    public static int peach(int n){
        if (n == 10) {
            return 1;
        }else {
            return (peach(n + 1) + 1) * 2;
        }
    }
}

案例4:啤酒问题

啤酒2元一瓶,4个盖子可以换一瓶,2个空瓶可以换一瓶,请问10元可以喝多少瓶?

public class T1 {
    public static void main(String[] args) {
        buy(10,0,0,0);
        //参数列表:钱数,总瓶数,剩余盖子数,剩余空瓶数
    }

    public static void buy(int money,int allNumber,int coverNumber,int bottleNumber ){
        int buyBottle = money / 2;//用钱买的瓶数
        money %= 2;
        allNumber += buyBottle;//总瓶数加上用钱买的瓶数
        coverNumber += buyBottle;//剩余盖子数加上用钱买的瓶数
        bottleNumber += buyBottle;//剩余空瓶数加上用钱买的瓶数

        if (coverNumber >= 4){//如果剩余盖子数大于4,可以换瓶
            buyBottle = coverNumber / 4;//用盖子换的瓶数
            allNumber += buyBottle;//总瓶数等于总瓶数加上用盖子换的瓶数
            coverNumber = coverNumber % 4 + buyBottle;
            //剩余盖子数等于剩余盖子数除以4的余数加上用盖子换的瓶数
            bottleNumber += buyBottle;//剩余空瓶数等于剩余空瓶数加上用盖子换的瓶数
        }

        if (bottleNumber >= 2){//如果剩余空瓶数大于2,可以换瓶
            buyBottle = bottleNumber / 2;//用空瓶换的瓶数
            allNumber += buyBottle;//总瓶数等于总瓶数加上用空瓶换的瓶数
            bottleNumber = bottleNumber % 2 + buyBottle;
            //剩余空瓶数等于剩余空瓶数除以2的余数加上用空瓶换的瓶数
            coverNumber += buyBottle;//剩余盖子数等于剩余盖子数加上用空瓶换的瓶数
        }

        if (coverNumber >= 4 || bottleNumber >= 2){
            //如果剩余盖子数大于4或者剩余空瓶数大于2,继续买
            buy(money,allNumber,coverNumber,bottleNumber);
        }else {
            System.out.printf("一共买了 %d 瓶%n", allNumber);
            System.out.println("剩余盖子数:" + coverNumber);
            System.out.println("剩余空瓶数:" + bottleNumber);
            return;
        }
    }
}

(2)遍历多级子目录案例1:搜索指定的文件

需求:从E盘中搜索“System4.0.exe”文件,找到后输出所在位置并启动

分析

  1. 先找出E盘下的所有一级文件对象
  2. 遍历全部一级文件对象,判断文件对象的类型
  3. 如果是文件,判断是否是自己想要的文件
  4. 如果是文件夹,需要继续进入到该文件夹查找,重复上述过程
public class Test10 {
    public static void main(String[] args) throws Exception {
        File file = new File("E:\\");
        findFile(file, "System4.0.exe");
    }

    public static void findFile(File file, String name) throws Exception {
   //判断第一次传入的file是否存在或是否为文件,如果为文件并且文件名与name相同,则输出路径并打开
        if(!file.exists() || file == null || file.isFile()){
            if(file.isFile() && file.getName().equals(name)){
                System.out.println(file.getAbsolutePath());
                Runtime runtime = Runtime.getRuntime();
                //"cmd /c "是防止权限不够,无法打开文件
                runtime.exec("cmd /c " + file.getAbsolutePath());
            }
            return;
        }

        //当前传入的一定是一个文件夹
        File[] fs = file.listFiles();//获取当前文件夹下的所有文件和文件夹
        if (fs != null){
            for (File f : fs) {//遍历
                if (f.isFile()){//如果是文件
                    if(f.getName().equals(name)){//如果文件名与name相同,则输出路径并打开
                        System.out.println(f.getAbsolutePath());
                        Runtime runtime = Runtime.getRuntime();
                        runtime.exec("cmd /c " + f.getAbsolutePath());
                    }
                }else {
                    //如果是文件夹,重复上面的操作
                    findFile(f, name);
                }
            }
        }
    }
}
(3)遍历多级子目录案例2:删除非空文件夹

需求:删除非空文件夹

分析

  1. File默认不可以删除非空文件夹
  2. 遍历文件夹,先删除里面的内容,再删除自己
public class T {
    public static void main(String[] args) {
        File file = new File("E:\\B");
        delectDic(file);
    }

    public static void delectDic(File file){
        if (!file.exists() || file == null){
            return;//文件不存在
        }
        if (file.isFile()){//存在且为文件
            file.delete();
        }else {
            //存在且一定为文件夹
            if (file == null){
                return;//没有删除的权限,直接return
            }
            File[] files = file.listFiles();
//            if (files.length == 0){
//                file.delete();
//                return;
//            }
            for (File f : files) {
                if (f.isFile()){
                    f.delete();
                }else {
                    delectDic(f);//删除下级目录
                }
            }
            file.delete();//删除自己
            //无论当前文件夹下是否为空,经过上面的for后,
            //都已经没有下一级文件了,所以无需写上面注释的代码
        }
    }
}

二、IO流

用于读写数据(可以读写文件,或网络中的数据.…)

1. 前置知识

1.1 计算机存储单位

位(bit):计算机内部数据存储的最小单位,用b表示。11001100是一个八位二进制数

字节(byte): 计算机数据处理的基本单位,用B表示

  • 1TB = 1024G
  • 1G = 1024M
  • 1M = 1024KB
  • 1KB = 1024B
  • 1B = 8bit
1.2 常见字符集
1.2.1 标准ASCII字符集(美国码)
  • ASCll:美国信息交换标准代码,包括数字,字母和常见符号,不包含汉字
  • 标准ASC使用1个字节存储一个字符(即八位二进制数),首位是0,总共可表示128个字符
    • 0xxx xxxx 八位二进制数,以0开头
    • 2的7次方 = 128
1.2.2 GBK字符集(中国码)
  • 包含了2万多个汉字等字符,GBK中一个中文字符编码成**两个字节(16位二进制数)**的形式存储。
    • 16位除去第一位必须是1,还可以有15位二进制数,可以表示2的15次方个汉字
  • 兼容ASCII字符集ASCII字符集以0开头,占一个字节;汉字占两个字节,以1开头
    • 汉字:1xxxxxxx xxxxxxxxx
    • ASCII字符集:0xxxxxxxx
1.2.3 Unicode字符集(统一码)
  • Unicode是国际组织制定的,可以容纳世界上所有文字、符号的字符集。
(1)UTF-32
  • 用四个字节(32位二进制数)表示一个字符
  • 缺点:占用的存储空间太大,通信效率变低
(2)UTF-8(最常用)
  • 是Unicode字符集的一种编码方案,采取可变长编码方案,共分四个长度区:1个字节,2个字节,3个字节,4个字节
  • 兼容标准ASCll编码,ASCll编码中的英文字符、数字等只占1个字节
  • 汉字字符占三个字节
UTF-8编码格式(二进制形式,必须以表格中对应的数字开头)
1个字节的表示形式:0xxxxxxx(ASCll字符集)
2个字节的表示形式:110xxxxx 10xxxxxx
3个字节的表示形式:1110xxxx 10xxxxxx 10xxxxxx
4个字节的表示形式:11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
1.3 总结
  • ASCll字符集:只有英文、数字、符号等,占1个字节。
  • GBK字符集:汉字占2个字节,英文、数字占1个字节。
  • UTF-8字符集:汉字占3个字节,英文、数字占1个字节。
1.3.1 注意事项
  • 字符编码时使用的字符集,和解码时使用的字符集必须一致,否则会出现乱码
  • 英文,数字一般不会乱码,因为很多字符集都兼容了ASCll字符集编码。
例:“a我d”
  • 使用GBK编码
    • 0xxxxxx 1xxxxxxx xxxxxxxx 0xxxxxxx
  • 如果使用UTF-8解码
    • 0xxxxxx:以0开头,根据后面七位解码成a
    • 1xxxxxxx:以1开头根据上面表格无匹配项,返回?
    • xxxxxxxx:可能也没有匹配项,返回?
    • 0xxxxxxx:以0开头,根据后面七位解码成d
    • 最后解码为a??d
1.4 字符集的编码和解码
String提供的编码方法说明
byte[ ] getBytes( )使用平台的默认字符集将该String编码为字节数组
byte[ ] getBytes( String charsetName)使用入参指定的字符集将该String编码为字节数组
String提供的解码方法(构造器)说明
String( byte[ ] bytes )使用平台的默认字符集将输入的字节数组解码为字符串
String( byte[ ] bytes, String charsetName)使用入参指定的字符集将输入的字节数组解码为字符串
public class T2 {
    public static void main(String[] args) throws Exception {
        //字符串编码

        //1.使用平台的默认字符集将该String编码为字节数组
        //byte[ ] getBytes( )
        String s = "a你c";
        byte[] bytes1 = s.getBytes();
        System.out.println(Arrays.toString(bytes1));
        //[97, -28, -67, -96, 99]
        //第一个代表a的ASCII码,后面三个代表你的UTF-8编码,最后一个代表c的ASCII码
        //正常应该用二进制表示,这里自动转换成了十进制表示

        //2.使用指定的字符集将该String编码为字节数组
        //byte[ ] getBytes(String charsetName)
        byte[] bytes2 = s.getBytes("GBK");
        System.out.println(Arrays.toString(bytes2));
        //[97, -60, -29, 99]
        //GBK中ASCII码占一个字节,中文占两个字节
        //第一个代表a的ASCII码,后面两个代表汉字你的GBK编码,最后一个代表c的ASCII码

        //字符串解码

        //1.使用平台的默认字符集将字节数组解码为String
        //String(byte[] bytes)
        String s1 = new String(bytes1);
        System.out.println(s1);//a你c

        //2.使用指定的字符集将字节数组解码为String
        //String(byte[] bytes, String charsetName)
        String s2 = new String(bytes1, "GBK");
        System.out.println(s2);//a浣燾,因为编码与解码方式不同
        String s3 = new String(bytes2, "GBK");
        System.out.println(s3);//a你c,因为编码与解码方式相同
    }
}

2. IO流概述

2.1 IO流的概念
  • I指Input,称为输入流:负责把数据读到内存中去
  • O指Output,称为输出流:负责写数据出去
2.2 IO流的分类

在这里插入图片描述

  • 字节输入流:以内存为基准,来自磁盘文件/网络中的数据以字节的形式读入到内存中的流
  • 字节输出流:以内存为基准,把内存中的数据以字节写出到磁盘文件或者网络中的流。
  • 字符输入流:以内存为基准,来自磁盘文件/网络中的数据以字符的形式读入到内存中的流。
  • 字符输出流:以内存为基准,把内存中的数据以字符写出到磁盘文件或者网络介质中的流。
2.3 IO流的体系

在这里插入图片描述

3. 字节流(原始流)

适用于一切文件的文件的复制、转移;不适合读取中文内容输出。

3.1 字节输入流FileInputStream

作用:以内存为基准,可以把磁盘文件中的数据以字节的形式读入到内存中去。

3.1.1 构造器
构造器说明
public FileInputstream(File file)根据输入的File对象创建字节输入流管道与源文件接通
public FileInputstream(String pathname)根据输入的路径创建字节输入流管道与源文件接通
public class Test1 {
    public static void main(String[] args) throws FileNotFoundException {
        
        //1.根据输入的File对象创建字节输入流管道与源文件接通
        //public FileInputstream(File file)
        File file = new File("src/a.txt");
        InputStream fis1 = new FileInputStream(file);
        
        //2.根据输入的字符串路径创建字节输入流管道与源文件接通(推荐使用)
        //public FileInputStream(String path)
        InputStream fis2 = new FileInputStream("src/a.txt");
    }
}

3.1.2 常用方法
常用方法说明
public int read( )每次读取一个字节返回,如果发现没有数据可读会返回-1
public int read(byte[ ] buffer)每次用一个字节数组去读取数据,返回字节数组读取了多少个字节,如果发现没有数据可读会返回-1
public byte[ ] readAllBytes( )直接将当前字节输入流对应的文件对象的字节数据装到一个字节数组返回
(1) 使用public int read( )每次读取单个字节 (不推荐)
public class Test1 {
    public static void main(String[] args) throws Exception {
        //a.txt中的内容:abcddwadawdna我
        InputStream fis = new FileInputStream("src/a.txt");

        //每次读取一个字节
        //public int read( )
        //每次返回一个字节即八位二进制数,读取到文件末尾返回-1
        int temp;
        while ((temp = fis.read()) != -1){
            System.out.print((char) temp);//abcddwadawdna我
            //如果不使用(char)强制转换,输出的是八位的二进制数转换成的十进制数
            //不使用(char)强制转换的十进制数为
            // 97 98 99 100 100 119 97 100 97 119 100 110 97 230 136 145
        }
        
		/*
		public int read( )方法的缺点:
		*/
        //1.每次只能读取一个字节,效率低,因为需要频繁的读取磁盘,
        //  每读取一个字节就要读取一次磁盘
        
        //2.读取的如果是中文,会出现乱码,不可避免
        //因为使用UTF-8时一个中文占三个字节,而一个字节只能读取一个字节,
        //一个中文被强制拆分成三个字节,所以会出现乱码
        //上面的输出结果中230 136 145对应的二进制应该表示”我“,
        //但是被拆分成了三个字节分开读,导致了乱码我
        
        //关闭流
        fis.close();
    }
}

缺点

  • 每次只能读取一个字节,效率低,因为需要频繁的读取磁盘,假设读取30个字节,就要读取30次磁盘
  • 读取的如果是中文,会出现乱码,不可避免,因为中文在任何编码规则下都不是一个字节
(2) 使用public int read(byte[ ] buffer)每次读取自定义数量的字节
public class Test2 {
    public static void main(String[] args) throws Exception {
        //a.txt中的内容:abcddw的ada哈哈哈wdna我
        InputStream inputStream = new FileInputStream("src/a.txt");

        //使用public int read(byte[ ] buffer)每次读取自定义数量的字节
        byte[] buffer = new byte[6];//每次读取6个字节,一个字节表示8位二进制数
        int len;//每次读取的字节数
        while ((len = inputStream.read(buffer)) != -1){
            String str = new String(buffer,0,len);
            System.out.print(str);
            //结果:abcddw的ada哈哈哈wdna我
        }
        inputStream.close();
    }
}

/*
	深度理解public int read(byte[ ] buffer)方法给byte数组赋值的机制
	如果读取的字节数达不到数组的长度,剩余长度的数组保持上次的值
	使用String的public String(byte[] bytes,int offset,int length)构造方法可以解决输出重复的问题
*/
public class Test4 {
    public static void main(String[] args) throws Exception {
        //a.txt中的内容:abcdefd
        InputStream inputStream = new FileInputStream("src/a.txt");
        //给byte数组赋值的机制是将读到的字节赋值给数组的每一个元素,
        // 但是如果读到的字节数不足数组长度时,剩余的元素不会被赋值,会保持上一次赋值的状态
        byte[] bytes = new byte[5];
        int len1 = inputStream.read(bytes);
        System.out.println(new String(bytes));//abcde
        System.out.println(len1);//5
        System.out.println(Arrays.toString(bytes));//[97, 98, 99, 100, 101]

        int len2 = inputStream.read(bytes);
        System.out.println(new String(bytes));
        //fbcde,后面的元素没有被赋值,保持上一次的状态
        System.out.println(len2);//2
        System.out.println(Arrays.toString(bytes));
        //[102, 100, 99, 100, 101],后面的元素没有被赋值,保持上一次的状态

        //保证输出正确,不会输出重复内容的解决方案
        //每次根据获取到的字节长度创建用于解码的String对象
        System.out.println(new String(bytes,0,len2));//fd
        inputStream.close();
    }
}

优点:解决了每次读取单个文件时效率低的问题,读取性能得到了提升。因为原来读30个字节需要操作系统调用30次磁盘,现在则是根据传入数组的长度,如果数组长度为3,则调用10次磁盘即可,因为一次读取3个字节

缺点:中文乱码问题还是没有得到解决

(3) 一次读取文件中全部字节
  • 方式一:使用public int read(byte[ ] buffer),自定义数组长度为文件中数据的字节数

  • 方式二:使用**public byte[ ] readAllBytes( )**直接返回所有的字节

public class Test3 {
    public static void main(String[] args) throws Exception {
        //a.txt的内容:abcddw的ada哈哈哈wdna我
        //一次读取所有字节

        //方式一:使用public int read(byte[ ] buffer)
        InputStream inputStream = new FileInputStream("src/a.txt");
        long size = new File("src/a.txt").length();
        //获取文件的大小,单位是字节
        byte[] buffer1 = new byte[(int) size];
        //创建一个和文件大小一样的字节数组
        int len = inputStream.read(buffer1);
        //将文件中的内容读取到字节数组中,返回值为读取到的字节数
        System.out.println(new String(buffer1));
        //将字节数组解码成字符串输出,使用默认的UTF-8解码
        //输出结果:abcddw的ada哈哈哈wdna我
        inputStream.close();

        //方式二:使用public byte[ ] readAllBytes( )直接返回所有的字节
        InputStream inputStream2 = new FileInputStream("src/a.txt");
        //必须重写定义一个FileInputStream对象,
        //因为上面的FileInputStream对象已经读取到文件末尾了,不能再读取了
        byte[] buffer2 = inputStream2.readAllBytes();//JDK9之后才有
        System.out.println(new String(buffer2));
        //将字节数组解码成字符串输出,使用默认的UTF-8解码
        //输出结果:abcddw的ada哈哈哈wdna我
        inputStream2.close();
    }
}

优点:读取速度得到了明显提升并且解决了读取中文乱码的问题

缺点:如果文件过大,就会导致创建的字节数组也会过大,可能会导致内存溢出

3.2 字节输出流FileOutputStream

作用:以内存为基准,把内存中的数据以字节的形式写出到文件中

3.2.1 构造器
构造器说明
public FileOutputStream(File file)创建字节输出流管道与源文件对象接通,默认会清空原来文件中数据
public FileOutputStream(String filepath)创建字节输出流管道与源文件路径接通,默认会清空原来文件中数据
public FileOutputStream(File file, boolean append)创建字节输出流管道与源文件对象接通,如果后面参数为true则允许追加数据,不会清空原有数据
public FileOutputStream(String filepath, boolean append)创建字节输出流管道与源文件路径接通,如果后面参数为true则允许追加数据,不会清空原有数据
public class Test5 {
    public static void main(String[] args) throws Exception {
        //public FileOutputStream(String filepath),会覆盖原文件,每次写入都会清空原文件
        OutputStream fo1 = new FileOutputStream("src\\B.txt");
        fo1.close();
        //public FileOutputStream(String filepath, boolean append)
        // 如果append为true则不会覆盖原文件,每次写入都会在原文件末尾追加
        OutputStream fo2 = new FileOutputStream("src\\B.txt", true);
        fo2.close();
    }
}
3.2.2 常用方法
方法说明
public void write( int a )写一个字节到硬盘指定位置
public void write( byte[ ] buffer )写一个字节数组到硬盘指定位置
public void write( byte[ ] buffer, int pos , int len)写一个字节数组的一部分到硬盘指定位置,pos为开始索引,len为要写入的长度
public class Test6 {
    public static void main(String[] args) throws Exception {
        FileOutputStream fo = new FileOutputStream("src\\B.txt", true);
        //常用方法
        //1.public void write(int b):写入一个字节
        fo.write(97);
        fo.write('b');//'b'会被自动转为int类型,对应的ASCII码为98
        fo.write('哈');
        //'哈'会被自动转为int类型,但是有三个字节,所以只会写入第一个字节,会乱码
        //当前文件内容::ab�

        //写入回车:\r\n
        fo.write("\r\n".getBytes());

        //2.public void write(byte[] b):写入一个字节数组
        byte[] b1 = "一起传入就不会乱码了".getBytes();
        fo.write(b1);
        byte[] b2 = {98, 99, 'a'};
        fo.write(b2);
        fo.write("\r\n".getBytes());
        //当前文件内容:
        // ab�
        //一起传入就不会乱码了bca

        //3.public void write(byte[] b, int off, int len):写入一个字节数组的一部分
        byte[] b3 = "一起传入就不会乱码了".getBytes();
        fo.write(b3, 0, 3);//写入前三个字节,也就是一个字,因为一个字在UTF-8中占三个字节
        //当前文件内容:
        // ab�
        //一起传入就不会乱码了bca一
        //一
        
        fo.close();
    }
}
3.2.3 案例:文件复制

将一张图片复制到磁盘的另一个位置

PS:下面的两个程序可以用于复制一切文件

//方法一
public class Test7 {
    public static void main(String[] args) throws Exception{
        // 读取图片为字节数组
        FileInputStream inputStream = new FileInputStream("E:\\Pictures\\Camera Roll" +
                "\\1.jpg");
        byte[] picture = inputStream.readAllBytes();
        // 将读取到的字节数组写入指定路径
        FileOutputStream outputStream = new FileOutputStream("src/1.jpg");
        outputStream.write(picture);
        outputStream.close();
        inputStream.close();
    }
}
//方法二
public class Test8 {
    public static void main(String[] args) throws Exception{
        FileInputStream inputStream = new FileInputStream("E:\\Pictures\\Camera Roll" +
                "\\1.jpg");
        FileOutputStream fileOutputStream = new FileOutputStream("src/2.jpg");
        byte[] picture = new byte[1024];// 一次读取1024个字节即1KB
        int len;
        while ((len = inputStream.read(picture)) != -1) {
            fileOutputStream.write(picture, 0, len);
        }
        fileOutputStream.close();
        inputStream.close();
    }
}
3.2.4 资源释放的两种方案

目的:因为调用流的close( )方法在最后,如果中间代码出现异常会导致程序异常终止,close方法不会被执行,流无法被正常释放

(1)try-catch-finally

特点:无论try中的程序是正常执行了,还是出现了异常,最后都一定会执行finally区,除非JVM终止。

作用:一般用于在程序执行完成后进行资源的释放操作(专业级做法)

案例:使用try-catch-finally优化上面文件复制案例的代码**(优化后代码过于臃肿)**

//释放资源的标准写法
public class Test8 {
    public static void main(String[] args){
        FileInputStream is = null;
        FileOutputStream os = null;
        try {
            is = new FileInputStream("E:\\Pictures\\Camera Roll" +
                    "\\1.jpg");
            os = new FileOutputStream("src/2.jpg");
            byte[] picture = new byte[1024];// 一次读取1024个字节即1KB
            int len;
            while ((len = is.read(picture)) != -1) {
                os.write(picture, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
            if (os != null) os.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
            if (is != null) is.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
(2)try-with-resource

作用与try-catch-finally相同,只不过使代码更加简洁

资源对象:资源对象是指资源对象对应的类必须实现AutoCloseable接口并且重写close方法

try-with-resource格式

try(定义资源对象1,定义资源对象2...)
{
  方法体
}catch(){
    
}

注意

  • try( )的括号中只能放置资源对象,即实现AutoCloseable接口并且重写close方法的类定义的对象
  • 在资源使用完毕后,无论try中的代码是否出现异常,都会自动调用资源对象的close( )方法来完成对资源的释放

案例:再次改进try-catch-finally优化的代码,减少代码的臃肿

public class Test8 {
    public static void main(String[] args){
        try (
             FileInputStream is  = new FileInputStream("E:\\Pictures\\Camera Roll" +
                     "\\1.jpg");
             FileOutputStream os = new FileOutputStream("src/2.jpg");
             ){
            byte[] picture = new byte[1024];// 一次读取1024个字节即1KB
            int len;
            while ((len = is.read(picture)) != -1) {
                os.write(picture, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

自定义资源类

允许自定义资源类,只需要让类实现AutoCloseable接口并且重写close方法接口

//自定义资源类
public class resource implements AutoCloseable{
    public void use(){
        System.out.println("资源被使用了");
    }
    @Override
    public void close() {
        System.out.println("资源被关闭了");
    }
}
public class Test9 {
    public static void main(String[] args) {
        try(
            resource s = new resource();
        ){
            s.use();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
/*
输出结果:
        资源被使用了
        资源被关闭了
*/

4. 字符流(原始流)

适用于读写文件的内容

构造器与方法与字节流基本一致,但是由于每次读取到的是字符,所以不会出现乱码问题

相当于根据当前文件的编码方式读取字符

  • 如果是UTF-8编码,则读取一个英文字符会返回一个字节即八位二进制数
  • 读取一个中文字符会返回三个字节即24位二进制数,不会出现将3个字节拆分的问题,作为整体返回
4.1 字符输入流FileReader

作用:以内存为基准,可以把文件中的数据以字符的形式读入到内存中去。

4.1.1 构造器
构造器说明
public FileReader(File file)根据输入的File对象创建字符输入流管道与源文件接通
public FileReader(String pathname)根据输入的路径创建字符输入流管道与源文件接通
4.1.2 常用方法
常用方法说明
public int read( )每次读取一个字符返回,如果发现没有数据可读会返回-1
public int read(char[ ] buffer)每次用一个字符数组去读取数据,返回字符数组读取了多少个字符,如果发现没有数据可读会返回-1
public class Test1 {
    public static void main(String[] args) {
        //a.txt中的内容为:ab我的你打c打完了电脑defd
        try (
            Reader fileReader1 = new FileReader("src/a.txt");
            Reader fileReader2 = new FileReader("src/a.txt");
        ) {
            // 每次读取一个字符
            // public int read()
            // 每次返回一个字符,读取到文件末尾返回-1
            int temp;
            while((temp = fileReader1.read()) != -1){
                System.out.print((char) temp);
                //返回的是每个字符对应的十进制数,需要强制转换成char
                //结果:ab我的你打c打完了电脑defd
                //因为每次读取的是一个字符,所以不会出现乱码问题
            }
            System.out.println();

            // 每次读取一个字符数组
            // public int read(char[] buffer)
            // 每次返回读取到的字符个数,读取到文件末尾返回-1
            char[] buffer = new char[4];//每次读取四个字符
            int len;
            while ((len = fileReader2.read(buffer)) != -1){
                System.out.print(new String(buffer, 0, len));
                //输出结果:ab我的你打c打完了电脑defd
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
4.2 字符输出流FileWriter

作用:以内存为基准,把内存中的数据以字符的形式写出到文件中

4.2.1 构造器
构造器说明
public FileWriter(File file)创建字符输出流管道与源文件对象接通,默认会清空原来文件中数据
public FileWriter(String filepath)创建字符输出流管道与源文件路径接通,默认会清空原来文件中数据
public FileWriter(File file, boolean append)创建字符输出流管道与源文件对象接通,如果后面参数为true则允许追加数据,不会清空原有数据
public FileWriter(String filepath, boolean append)创建字符输出流管道与源文件路径接通,如果后面参数为true则允许追加数据,不会清空原有数据
4.2.2 常用方法
方法说明
public void write( int a )写一个字符到硬盘指定位置
public void write(String str)写一个字符串到硬盘指定位置
public void write(String str, int off , int len)写一个字符串的一部分到磁盘的指定位置
public void write( char[ ] buffer )写一个字符数组到硬盘指定位置
public void write( char[ ] buffer, int off , int len)写一个字符数组的一部分到硬盘指定位置,pos为开始索引,len为要写入的长度
public class Test2 {
    public static void main(String[] args) {
        try (
            Writer fileWriter = new FileWriter("src/b.txt")
            
            )
        {
            //常用方法

            //1.public void write(int c),每次写入一个字符
            fileWriter.write(97);
            fileWriter.write('b');
            fileWriter.write('哈');//不会乱码,因为将三个字节打包为一个字符写入
            fileWriter.write("\r\n");//写入回车
            /*
            当前文件内容:ab哈
             */

            //2.public void write(String str),每次写入一个字符串
            fileWriter.write("字符输出流!\r\n");
            /*
            当前文件内容:
                        ab哈
                        字符输出流!
             */

            //3.public void write(String str, int off, int len),每次写入一个字符串一部分
            fileWriter.write("字符输出流!\r\n", 2, 6);
            //从索引为2的字符开始写入,写入3个字符
            /*
            当前文件内容:
                        ab哈
                        字符输出流!
                        输出流!
             */

            //4. public void write(char[] cbuf),每次写入一个字符数组
            char[] chars = {'a', '我', 'c', 97, 'e','\r','\n'};
            fileWriter.write(chars);
            /*
            当前文件内容:
                        ab哈
                        字符输出流!
                        输出流!
                        a我cae
             */

            //5.public void write(char[] cbuf, int off, int len),每次写入字符数组的一部分
            fileWriter.write(chars, 1, 3);//从索引为1的字符开始写入,写入3个字符
            /*
            当前文件内容:
                        ab哈
                        字符输出流!
                        输出流!
                        a我cae
                        我ca
             */
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
4.2.3 注意事项(很重要)

字符输出流写出数据后,必须刷新流,或者关闭流,写出去的数据才能生效

在调用write方法将数据写入磁盘,在遇到刷新流( public void flush( ) )或关闭流( public void close( ) )的方法前都会先将write传入的数据先放入缓冲区中,在遇到刷新流或关闭流的方法后才会调用系统的资源将缓冲区中的内容一次写入到磁盘中,这样做可以避免浪费系统资源,提高整体的性能

刷新流与关闭流的区别

  • 刷新流可以多次使用,每使用一次就将之前的缓冲区的内容写入磁盘中
  • 关闭流包含刷新流操作,但是只能使用一次使用后流关闭,无法再使用该流。

5. 缓冲流

作用:对原始流(字节流、字符流)讲行包装,通过减少磁盘调用量以提高原始流读写数据的性能

5.1 字节缓冲流BufferedInputStream/BufferedOutputStream
5.1.1 分类

字节缓冲输入流和字节缓冲输出流,分别用于包装字节输入流和字节输出流

5.1.2 作用

通过增加缓冲池减少调用磁盘的次数来提高字节流读写数据的性能

5.1.3 原理:
  • 字节缓冲输入流相比于字节输入流自带了8KB缓冲池;
  • 字节缓冲输出流相比于字节输入流也自带了8KB缓冲池。
5.1.4 原始字节流与字节缓冲流的原理对比
(1)原始字节流

在这里插入图片描述

(2)字节缓冲流

在这里插入图片描述

构造器
方法说明
public BufferedInputStream( InputStream is )把低级的字节输入流包装成一个高级的缓冲字节输入流,入参为字节输入流对象,继承了所有字节输入流的方法
public BufferedOutputStream( OutputStream os )把低级的字节输出流包装成一个高级的缓冲字节输出流,入参为字节输出流对象,继承了所有字节输出流的方法
//原始的字节输入输出流拷贝数据的方法
public class Test8 {
    public static void main(String[] args){
        try (
             FileInputStream is  = new FileInputStream("E:\\Pictures\\Camera Roll" +
                     "\\1.jpg");
             FileOutputStream os = new FileOutputStream("src/3.jpg");
             ){
            byte[] picture = new byte[1024];// 一次读取1024个字节即1KB
            int len;
            while ((len = is.read(picture)) != -1) {
                os.write(picture, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
//将原始的字节输入输出流包装成字节缓冲流后拷贝数据的方法
public class Test1 {
    public static void main(String[] args){
        try (
                FileInputStream is  = new FileInputStream("E:\\Pictures\\Camera Roll" +
                        "\\1.jpg");
                //包装成字节缓冲输入流
                BufferedInputStream bis = new BufferedInputStream(is);
            
                FileOutputStream os = new FileOutputStream("src/3.jpg");
                //包装成字节缓冲输出流
                BufferedOutputStream bos = new BufferedOutputStream(os);
                //BufferedOutputStream bos = new BufferedOutputStream(os,1024 * 16);
                //允许指定缓冲区大小,默认为8192,即8KB,上面自定义为16KB
        ){
            byte[] picture = new byte[1024];
            int len;
            //使用字节缓冲流对象代替原始字节流对象
            while ((len = bis.read(picture)) != -1) {
                bos.write(picture, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
5.2 字符缓冲流
5.2.1 字符缓冲输入流BufferedReader
(1)作用

包装字符输入流,继承了所有字符输入流的方法,自带8K(8192)的字符缓冲池,可以提高字符输入流读取字符数据的性能。

(2)原理

同字节缓冲输入流

在这里插入图片描述

在这里插入图片描述

(3)构造器与新增方法
构造器说明
public BufferedReader( Reader r )把低级的字符输入流包装成一个高级的缓冲字符输入流,入参为字符输入流对象,继承了所有字符输入流的方法
方法说明
public String readLine( )每次读取一行数据返回,如果没有数据可读了,会返回null
public class Test3 {
    public static void main(String[] args) {
        try (
                Reader re = new FileReader("src/a.txt");
                BufferedReader br = new BufferedReader(re)
        ){
            String line;
            //一次读一行
            while ((line = br.readLine()) != null){
                System.out.println(line);
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}
5.2.2 字符缓冲输出流BufferedWriter
(1)作用

包装字符输出流,继承了所有字符输出流的方法,自带8K的字符缓冲池,可以提高字符输出流向磁盘写字符数据的性能。虽然原始的字符输出流也自带了缓冲池,但是达不到8k的大小且不能自定义

(2)原理

同字节缓冲输出流

在这里插入图片描述

在这里插入图片描述

(3)构造器与新增的功能
构造器说明
public BufferedWriter( Writer r )把低级的字符输出流包装成一个高级的缓冲字符输出流,入参为字符输出流对象,继承了所有字符输出流的方法
方法说明
public void newLine( )换行写入
public class Test2 {
    public static void main(String[] args) {
        try (
                FileWriter fw = new FileWriter("src/b.txt");
                BufferedWriter bf = new BufferedWriter(fw)
        ) {
            bf.write("Hello World");
            bf.newLine();// 换行
            bf.write("Hello World");
            bf.write("ddd");
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

5.2.3 案例:拷贝出师表到另一个文件,恢复顺序

需求:把《出师表》的文章顺序进行恢复到一个新文件中。

分析:

  • 定义一个缓存字符输入流管道与源文件接通。
  • 定义一个TreeSet集合存储读取的每行数据。
  • 定义一个循环按照行读取数据,存入到TreeSet集合中去。
  • 定义一个缓存字符输出管道与目标文件接通。
  • 遍历TreeSet集合中的每个元素,用缓冲输出管道写出并换行。
3.侍中、侍郎郭攸之、费祎、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必能裨补阙漏,有所广益。
1.先帝创业未半而中道崩殂,今天下三分,益州疲弊,此诚危急存亡之秋也。然侍卫之臣不懈于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞忠谏之路也。
5.亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓、灵也。侍中、尚书、长史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。
2.宫中府中,俱为一体,陟罚臧否,不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。
4.将军向宠,性行淑均,晓畅军事,试用于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行阵和睦,优劣得所。
6.臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻达于诸侯。先帝不以臣卑鄙,猥自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有一年矣。
9.今当远离,临表涕零,不知所言。
7.先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙夜忧叹,恐托付不效,以伤先帝之明,故五月渡泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶竭驽钝,攘除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。
8.愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎;陛下亦宜自谋,以咨诹善道,察纳雅言,深追先帝遗诏,臣不胜受恩感激。
public class Test3 {
    public static void main(String[] args) {

        try (
                BufferedReader br = new BufferedReader(new FileReader("src/c.txt"));
                BufferedWriter bw = new BufferedWriter(new FileWriter("src/d.txt"))
        ) {
            TreeSet<String> tree = new TreeSet<>();
            String line;
            while ((line = br.readLine()) != null) {
                tree.add(line);
            }
            for (String s : tree) {
                bw.write(s);
                bw.newLine();
            }
            System.out.println("拷贝成功");
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

6. 原始流与缓冲流性能对比(重点)

6.1 测试用例

分别使用原始的字节流,以及字节缓冲流复制一个很大视频。

6.2 测试步骤
  • 使用低级的字节流按照一个一个字节的形式复制文件。(不使用,淘汰)
  • 使用低级的字节流按照字节数组的形式复制文件。(通过改变数组长度可以实现高效率复制)
  • 使用高级的缓冲字节流按照一个一个字节的形式复制文件。(不推荐使用)
  • 使用高级的缓冲字节流按照字节数组的形式复制文件。
public class Test4 {
    private static final String COPY_PATH = "E:/TASHISHUI/长风渡/1.mp4";
    private static final String FINAL_PATH1 = "E:/TASHISHUI/长风渡/2.mp4";
    private static final String FINAL_PATH2 = "E:/TASHISHUI/长风渡/3.mp4";
    private static final String FINAL_PATH3 = "E:/TASHISHUI/长风渡/4.mp4";
    private static final String FINAL_PATH4 = "E:/TASHISHUI/长风渡/5.mp4";
    public static void main(String[] args) {
        //文件大小为1.5G
        //copy1();//特别慢,根本无法接受,淘汰
        copy2();//使用低级的字节流按照字节数组的形式复制文件耗时:6秒
        copy3();//使用字节缓冲流按照一个一个字节的形式复制文件耗时:4秒
        copy4();//使用字节缓冲流按照字节数组的形式复制文件耗时:1秒
        copy5();//使用低级的字节流按照字节数组的形式复制文件并更改数组大小为8kb耗时:1秒
    }

    public static void copy1() {
        long start = System.currentTimeMillis();
        try (
                FileInputStream is = new FileInputStream(COPY_PATH);
                FileOutputStream os = new FileOutputStream(FINAL_PATH1);
            ){
            int temp;
            while ((temp = is.read()) != -1) {
                os.write(temp);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("使用低级的字节流按照一个一个字节的形式复制文件耗时:"
                + (end - start)/1000 + "秒");
    }

    public static void copy2(){
        long start = System.currentTimeMillis();
        try (
                FileInputStream is = new FileInputStream(COPY_PATH);
                FileOutputStream os = new FileOutputStream(FINAL_PATH2);
            ){
            byte[] buffer = new byte[1024];
            int len;
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("使用低级的字节流按照字节数组的形式复制文件耗时:"
                + (end - start)/1000 + "秒");
    }

    public static void copy3(){
        long start = System.currentTimeMillis();
        try (
                BufferedInputStream bis = new BufferedInputStream(
                    new FileInputStream(COPY_PATH));
                BufferedOutputStream bos = new BufferedOutputStream(
                    new FileOutputStream(FINAL_PATH3));
            ){
            int temp;
            while ((temp = bis.read( )) != -1) {
                bos.write(temp);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("使用字节缓冲流按照一个一个字节的形式复制文件耗时:"
                + (end - start)/1000 + "秒");
    }

    public static void copy4(){
        long start = System.currentTimeMillis();
        try (
                BufferedInputStream bis = new BufferedInputStream(
                    new FileInputStream(COPY_PATH));
                BufferedOutputStream bos = new BufferedOutputStream(
                    new FileOutputStream(FINAL_PATH4));
        ){
            byte[] buffer = new byte[1024];
            int len;
            while ((len = bis.read(buffer)) != -1) {
                bos.write(buffer, 0, len);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("使用字节缓冲流按照字节数组的形式复制文件耗时:"
                + (end - start)/1000 + "秒");
    }
    public static void copy5(){
        long start = System.currentTimeMillis();
        try (
                FileInputStream is = new FileInputStream(COPY_PATH);
                FileOutputStream os = new FileOutputStream(FINAL_PATH4);
        ){
            byte[] buffer = new byte[8192];
            int len;
            while ((len = is.read(buffer)) != -1) {
                os.write(buffer, 0, len);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        long end = System.currentTimeMillis();
        System.out.println("使用低级的字节流按照字节数组的形式复制文件" + 
                           + "并更改数组大小为8kb耗时:" + (end - start)/1000 + "秒");
    }
}

7. 转换流

7.1 问题:不同编码读取出现乱码问题
  • 如果代码编码和被读取的文本文件的编码是一致的,使用字符流读取文本文件时不会出现乱码!

  • 如果代码编码和被读取的文本文件的编码是不一致的,使用字符流读取文本文件时就会出现乱码!

public class Test1 {
    public static void main(String[] args) {
        //a.txt与b.txt文件内容相同,但是编码不同
        readSame();//txt文件使用UTF8编码,读取时使用UTF8编码
        /*
        使用UTF8编码
        ab我efd
        dwa单d
        dbrdb
        drgs
         */
        readDif();//txt文件使用GBK编码,读取时使用UTF8编码
        /*
        ʹ��GBK����
        ab��efd
        dwa��d
        dbrdb
        drgs
         */
    }

    public static void readSame() {
        try (
            Reader fr = new FileReader("src/a.txt");
            BufferedReader br = new BufferedReader(fr)
        ) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void readDif(){
        try (
                Reader fr = new FileReader("src/b.txt");
                BufferedReader br = new BufferedReader(fr)
        ) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}
7.2 字符输入转换流InputStreamReader
  • 解决不同编码时,字符流读取文本内容乱码的问题
  • 解决思路:先获取文件的原始字节流,再将其按真实的字符集编码转成字符输入流,这样字符输入流中的字符就不乱码了。
构造器说明
public InputStreamReader( InputStream is )把传入的原始的字节输入流,按照代码默认编码转成字符输入流(与直接用FileReader的效果一样)
public InputStreamReader( Inputstream is , String charset )把传入的原始的字节输入流,按照指定字符集编码转成字符输入流**(经常使用)**
public class Test2 {
    public static void main(String[] args) {
        //默认使用UTF8编码,b.txt使用GBK编码
        try (
                //获取原始字节流
                InputStream is = new FileInputStream("src/b.txt");
                //将原始字节流转换为字符流,编码为GBK
                InputStreamReader isr = new InputStreamReader(is, "GBK");
                //将字符流包装为缓冲流
                BufferedReader br = new BufferedReader(isr);
        ) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/*
        使用GBK编码
        ab我efd
        dwa单d
        dbrdb
        drgs
 */
7.3 字符输出转换流OutputStreamWriter

7.3.1 问题引入

如何控制写出的字符使用指定的字符集编码?

  1. 使用String提供的getBytes方法
String data = "我爱你中国abd";
byte[] bytes = data.getBytes("GBK")
  1. 使用字符输出转换流

7.3.2 作用

  • 作用:可以控制写出去的字符使用什么字符集编码。
  • 解决思路:获取字节输出流,再按照指定的字符集编码将其转换成字符输出流,以后写出去的字符就会用该字符集编码了。

7.3.2 构造器

构造器说明
public OutputstreamWriter( OutputStream os )可以把原始的字节输出流,按照代码默认编码转换成字符输出流。
public OutputStreamWriter( OutputStream os , String charset )可以把原始的字节输出流,按照指定编码转换成字符输出流**(经常使用)**
public class Test3 {
    public static void main(String[] args) {
        try (
              OutputStream os = new FileOutputStream("src/e.txt");
              OutputStreamWriter osw = new OutputStreamWriter(os, "GBK");
              BufferedWriter bw = new BufferedWriter(osw);
        ) {
            bw.write("我是中国人");
            bw.newLine();
            bw.write("我爱中国");
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

8. 打印流PrintStream/PrintWriter

作用:打印流可以实现更方便、更高效的打印数据出去

  • PrintStream继承自字节输出流OutputStream,因此write方法只支持写字节类型数据出去
  • PrintWriter继承自字符输出流Writer,因此write方法支持写字符类型数据出去
8.1 PrintStream
构造器说明
public PrintStream( OutputStream/File/String )打印流直接通向字节输出流/文件/文件路径
public Printstream( String fileName, Charset charset )可以指定写出去的字符编码
public PrintStream( Outputstream out, boolean autoFlush )可以指定实现自动刷新
public PrintStream( Outputstream out, boolean autoFlush, String encoding )可以指定实现自动刷新,并可指定字符的编码
方法说明
public void println( Xxx )打印任意类型的数据出去,并自动换行
public void write( int/byte[ ]/byte[ ]中的一部分 )只支持写字节数据出去
public class Test1 {
    public static void main(String[] args) {
        try (
            PrintStream ps = new PrintStream("src/f.txt", Charset.forName("GBK"))
        ) {
            ps.println("普通字符串");
            ps.println(23);
            ps.write(88);
            //ps.write("d");//会报错,因为无法写入字符
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/*
        普通字符串
        23
        X
 */
8.2 PrintWriter
构造器说明
public PrintWriter(Outputstream/Writer/File/String)打印流直接通向字节输出流/文件/文件路径
public Printwriter(String fileName,Charset charset)可以指定写出去的字符编码
public PrintWriter(Outputstream out/Writer,boolean autoFlush)可以指定实现自动刷新
public Printwriter(Outputstream out,boolean autoFlush,String encoding)可以指定实现自动刷新,并可指定字符的编码
方法说明
public void println(Xxx)打印任意类型的数据出去,并自动换行
public void write(int/String/char[ ]/…)可以支持写字符数据出去
public class Test1 {
    public static void main(String[] args) {
        try (
            PrintWriter pw = new PrintWriter("src/f.txt", Charset.forName("GBK"))
        ) {
            pw.println("普通字符串");
            pw.println(23);
            pw.write(88);//88的ASCII码为X
            pw.write('\n');
            pw.write("d");//不会报错,因为PrintWriter的write方法允许写入字符
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/*
        普通字符串
        23
        X
        d
 */

补充:在原有内容的基础上追加数据

public class Test1 {
    public static void main(String[] args) {
        try (
            //需要使用低级流来指定追加,高级流不允许直接追加
            PrintWriter pw = new PrintWriter(new FileWriter("src/f.txt",true))
        ) {
            pw.println("新增");
            pw.println(23);
            pw.println('X');
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/*
        普通字符串
        23
        X
        d新增
        23
        X
 */
8.3 应用:输出语句的重定向

将输出到控制台的内容输出到文件中去

public class Test2 {
    public static void main(String[] args) {
        System.out.println("Hello World!");
        System.out.println("sda");
        try (
            PrintStream pw = new PrintStream("src/1.txt")
        ) {
            // 重定向输出流, 将输出流重定向到文件中
            System.setOut(pw);
            System.out.println("dasda");
            System.out.println("grng");
        }catch (Exception e) {
            e.printStackTrace();
        }
    }
}

在这里插入图片描述

在这里插入图片描述

9. 数据流

9.1 数据输出流DataOutputStream

将数据与数据类型一起写入到指定文件中,但是并不是给我们看的,只有使用DataInputStream才能看到写入的实际内容

构造器说明
public DataOutputstream( OutputStream out )创建数据输出流包装传入的基础的字节输出流
方法说明
public final void writeByte( int v )将byte类型的数据写入基础字节流对应的文件
public final void writeInt( int v )将int类型的数据写入基础字节流对应的文件
public final void writeDouble( Double v )将double类型的数据写入基础字节流对应的文件
public final void writeUTF( String str )将字符串数据以UTF-8编码成字节写入基础字节流对应的文件
public void write(int/byte[ ]/byte[ ]的一部分)将字节数据写入基础字节流对应的文件
public class Test1 {
    public static void main(String[] args) {
        try (DataOutputStream dos =
                     new DataOutputStream(new FileOutputStream("src/1.txt")))
        {
            dos.writeInt(10);
            dos.writeInt(-10);
            dos.writeLong(10L);
            dos.writeDouble(10.5);
            dos.writeUTF("中国");
            dos.writeChar('中');
            dos.writeBoolean(true);
            dos.writeByte(0x41);// A
            dos.writeShort(0x3a);// 58
            dos.writeChar('A');
            dos.writeFloat(10.5f);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

在这里插入图片描述

必须使用数据输入流来读取

9.2 数据输入流DataInputStream

读取数据输出流写入的数据

构造器说明
public DataInputStream(InputStream is)创建数据输入流包装传入的基础的字节输入流
方法说明
Public final byte readByte( )读取字节数据并返回
public final int readInt( )读取int类型的数据并返回
public final double readDouble( )读取double类型的数据并返回
public final String readUTF( )读取字符串类型数据(UTF8)并返回
public int readInt( )/read(byte[ ])读取字节数据
public class Test1 {
    public static void main(String[] args) {
        try (DataInputStream dis =
                     new DataInputStream(new FileInputStream("src/1.txt")))
        {
            System.out.println(dis.readInt());
            System.out.println(dis.readInt());
            System.out.println(dis.readLong());
            System.out.println(dis.readDouble());
            System.out.println(dis.readUTF());
            System.out.println(dis.readChar());
            System.out.println(dis.readBoolean());
            System.out.println(dis.readByte());// A
            System.out.println(dis.readShort());// 58
            System.out.println(dis.readChar());
            System.out.println(dis.readFloat());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
/*
        10
        -10
        10
        10.5
        中国
        中
        true
        65
        58
        A
        10.5
 */

10. 序列化流

对象序列化:把Java对象写入到文件中去
对象反序列化:把文件里的Java对象读出来

10.1 对象字节输出流ObjectOutputStream

可以把Java对象进行序列化:把]ava对象存入到文件中去。

构造器方法
public objectoutputstream( Outputstream out )创建对象字节输出流,包装基础的字节输出流
方法说明
public final void writeobject( object o )将对象写入文件中

注意事项

  • 对象想要进行序列化,必须在对象对应的实体类中实现接口(java.io.Serializable)
  • 如果希望对象中的某个成员变量不参与序列化,需要将该成员变量使用transient修饰
//想要被序列化必须实现序列化接口
public class Student implements Serializable {
    private String name;
    private int age;
    private transient Double score;//使用transient关键字修饰的属性不会被序列化

    public Student() {
    }

    public Student(String name, int age, Double score) {
        this.name = name;
        this.age = age;
        this.score = score;
    }

    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;
    }

    public Double getScore() {
        return score;
    }

    public void setScore(Double score) {
        this.score = score;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", score=" + score +
                '}';
    }
}
public class Test1 {
    public static void main(String[] args) {
        try (
               ObjectOutputStream oos = 
                        new ObjectOutputStream(new FileOutputStream("src/2.txt"))
        ){
            Student s1 = new Student("张三", 18, 99.9);
            oos.writeObject(s1);
            System.out.println("序列化成功");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

在这里插入图片描述

10.2 对象字节输入流ObjectInputStream

可以把]ava对象进行反序列化:把存储在文件中的]ava对象读入到内存中来。

构造器说明
public objectInputstream( InputStream is )创建对象字节输入流,包装基础的字节输入流
方法说明
public final object readObject( )读取对象字节输出流写到文件中的对象
public class Test2 {
    public static void main(String[] args) {
        try (
                ObjectInputStream ois =
                        new ObjectInputStream(new FileInputStream("src/2.txt"))
        ){
            Student s1 = (Student) ois.readObject();
            System.out.println(s1);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
//Student{name='张三', age=18, score=null} score没有被序列化

一次序列化多个对象

用一个ArrayList集合存储多个对象,然后直接对集合进行序列化即可

ArrayList集合已经实现了序列化接口

public class Test3 {
    public static void main(String[] args) {
        try (
                ObjectOutputStream oos =
                        new ObjectOutputStream(new FileOutputStream("src/2.txt"))
        ){
            List<Student> students = new ArrayList<>();
            Student s1 = new Student("张三", 18, 99.9);
            Student s2 = new Student("李四", 19, 98.9);
            Student s3 = new Student("王五", 20, 97.9);
            students.add(s1);
            students.add(s2);
            students.add(s3);
            oos.writeObject(students);
            System.out.println("序列化成功");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
public class Test4 {
    public static void main(String[] args) {
        try (
                ObjectInputStream ois =
                        new ObjectInputStream(new FileInputStream("src/2.txt"))
        ){
            List<Student> s1 = (List)ois.readObject();
            System.out.println(s1);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
//[Student{name='张三', age=18, score=null}, 
// Student{name='李四', age=19, score=null}, 
// Student{name='王五', age=20, score=null}]

11. 补充知识:IO框架

11.1 框架
  • 解决某类问题,编写的一套类、接口等,可以理解成一个半成品,大多框架都是第三方研发的。
  • 好处:在框架的基础上开发,可以得到优秀的软件架构,并能提高开发效率
  • 框架的形式:一般是把类、接口等编译成class形式,再压缩成一 个jar结尾的文件发行出去。
11.2 IO框架

封装了JAVA提供的对文件、数据进行操作的代码,对外提供了更简单的方式来对文件进行操作,对数据进行读写等。

11.2.1Commons-io

Commons-io是apache开源基金组织提供的一组有关IO操作的小框架,目的是提高1O流的开发效率。

FileUtils类提供的部分方法说明
public static void copyFile( File srcFile, File destFile )复制文件
public static void copyDirectory( File srcDir,File destDir )复制文件夹
public static void deleteDirectory(File directory)删除文件夹,无论是否为空
public static String readFileTostring(File file, String encoding)读数据
public static void writeStringToFile(File file, String data, String charname, boolean append)写数据
IOUtils类提供的部分方法说明
public static int copy( Inputstream inputstream,Outputstream outputstream )字节流复制文件
public static int copy( Reader reader ,Writer writer )字符流复制文件
public static void write(String data, OutputStream output, String charsetName)写数据

(1)集合框架到项目中

  • 搜索Commons-io,找到apach官网的下载路径,下载并解压

在这里插入图片描述

在这里插入图片描述

  • 在项目的src下新建文件夹,起名为lib,并将commons-io-2.13.0.jar复制到lib中

在这里插入图片描述

  • 在项目中右键该jar包,导入到项目中

在这里插入图片描述

(2)代码实例

public class test1 {
    public static void main(String[] args) throws IOException {
        // 复制文件,从a.txt到b.txt
        FileUtils.copyFile(new File("E:/A/a.txt"),new File("E:/A/b.txt"));
        // 复制文件夹,从A到B
        FileUtils.copyDirectory(new File("E:/A"),new File("E:/B"));
        //删除文件夹,无论文件夹是否为空
        FileUtils.deleteDirectory(new File("E:/B"));
        //向文件中写入数据
        FileUtils.writeStringToFile(new File("E:/A/a.txt"),"你好","UTF-8",true);
        //读取文件中数据
        String msg = FileUtils.readFileToString(new File("E:/A/a.txt"),"UTF-8");
        System.out.println(msg);
    }
}

public class Test2 {
    public static void main(String[] args) throws Exception {
        //字节流复制文件
        IOUtils.copy(new FileInputStream("E:/A/a.txt"),new FileOutputStream("E:/A/b.txt"));
        //字符流复制文件
        IOUtils.copy(new FileReader("E:/A/a.txt"),new FileWriter("E:/A/b.txt"));
        //向文件中写入数据
        IOUtils.write("HHHHH",new FileOutputStream("E:/A/a.txt"),"UTF-8");
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值