Java进阶第八天

1、字节流

1.1 IO概述
硬盘:永久存储数据,不停地旋转使得硬盘的磁头可以读取到不同的文件。
内存:临时存储数据。
I/O流:
I:input 输入/读取 从硬盘到内存
O:output 输出/写入 从内存到硬盘
流:数据 分为字符和字节,1字符=2字节,1字节=8二进制位/比特位
字符输入流Reader、字符输出流Writer、字节输入流InputStream、字节输出流OutputStream
1.2 OutputStream类
计算机中所有数据都是以二进制形式存储的,以字节为单位
OutputStream是表示所有字节输出流的父类,是一个抽象类
方法:
  void close();关闭此输出流并释放相关资源
  void flush();刷新此输出流并强制写出所有缓冲的输出字节
  void write(byte[] b);
  void write(byte[] b, int off, int len);
  abstract void write(int b);
子类:ByteArrayOutputStream FileOutputStream FilterOutputStream
   ObjectOutputStream OutputStream PipedOutputStream
FileOutputStream 文件字节输出流 将内存中的数据写入到硬盘的文件中
构造方法:
  FileOutputStream(String name);参数是写入文件的路径
  FileOutputStream(File file);参数是写入文件
构造方法做了三件事:
(1)创建一个FileOutputStream对象
(2)根据构造方法传递的文件路径/文件,创建一个空文件
(3)将FileOutputStream对象指向创建好的文件
写入数据的原理:
  Java程序 -> JVM(Java虚拟机) -> OS -> OS调用写数据的方法 -> 把数据写入文件中
字节输出流的使用步骤:
 (1)创建FileOutputStream对象,在构造方法中传递数据写入的目的地
 (2)调用write方法,将数据写入到文件中
 (3)调用close方法,释放资源(流使用会占用一部分内存,影响效率)
  以上三个方法都有异常 抛出IOException
测试类
public class Demo02OutputStream {
  public static void main(String[] args) throws IOException {
    FileOutputStream os = new FileOutputStream(“day08-code\src\demo01IO\1.txt”);
    os.write(97);//写入数据时会转换为二进制的97,以字节为单位存入文件。任意的文本编辑器在打开文件时都会查询编码表,将字节转换为字符显示。0-127查询ASCII码表,其它值查询系统默认码表(中文系统查询GBK码表) 97就是a,所以打开文件显示a
    os.close();
 
    FileOutputStream os2 = new FileOutputStream(new File(“day08-code\src\demo01IO\2.txt”));
    //要在文件里显示100,需要写入几个字节?3个
    os2.write(49);//1
    os2.write(48);//0
    os2.write(48);//0
    os2.close();
     
    //写入多个字节的方法
    //如果写入的第一个字节是正数(0-127),会查询ASCII码表
    //如果写入的第一个字节是负数,会和第二个字节组成一个中文显示,查询系统默认码表(中文GBK)
    byte[] bytes = {65, 66, 67, 68, 69, 70};
    byte[] bytes2 = {-65, -66, -67, 68, 69, 70};//-65和-66组成一个中文,-67和68组成一个中文,69、70还是大写字母E、F 文件显示烤紻EF
    FileOutputStream os3 = new FileOutputStream(“day08-code\src\demo01IO\3.txt”);
    os3.write(bytes2);//ABCDEF
    os3.close();
 
    //写字节数组的一部分
    byte[] bytes3 = {65, 66, 67, 68, 69, 70};
    FileOutputStream os4 = new FileOutputStream(“day08-code\src\demo01IO\4.txt”);
    os4.write(bytes3, 1, 3);//从字节数组bytes3的索引1开始写入三个字节 文件显示BCD
    os4.close();
 
    //写入字符串 使用String类的方法getBytes将字符串转换为字节数组
    String s = “我们 你们 他们”;
    byte[] bytes4 = s.getBytes();
    FileOutputStream os5 = new FileOutputStream(“day08-code\src\demo01IO\5.txt”);
    os5.write(bytes4);//我们 你们 他们 UTF-8中一个中文3个字节,GBK中一个中文两个字节 6个中文2个空格一共20个字节
  }
}
1.3 文件的续写和换行
续写使用FileOutputStream的带两个参数的构造方法
  FileOutputStream(String name, boolean append);
  FileOutputStream(File file,boolean append);
    String name/File file 写入数据的目的地
    boolean append 是否追加写
换行符号:Windows \r\n
     Linux /n
     Mac /r
测试类
public class Demo03ContinueWrite {
  public static void main(String[] args) throws IOException {
    //追加写
    FileOutputStream fos = new FileOutputStream(“day08-code\src\demo01IO\1.txt”, true);
    fos.write(65);//aA
    //换行
    FileOutputStream fos2 = new FileOutputStream(“day08-code\src\demo01IO\2.txt”, true);
    for (int i = 0; i < 10; i++) {
      fos2.write(“hello”.getBytes());
      fos2.write("\r\n".getBytes());
    }
    //100hello
    //hello
    //hello
    //hello
    //hello
    //hello
    //hello
    //hello
    //hello
    //hello
    fos2.close();
  }
}
1.4 InputStream类
InputStream是表示字节输入流的所有类的父类,是一个抽象类
方法:
  void close();关闭此输入流并释放相关资源
  void read();从输入流中读取数据的下一个字节,一次读取一个字节,读取到末尾返回-1
  int read(byte[] b);从输入流中一次读取多个字节,并将其存储在缓冲区中数组b里
子类:AudioInputStream ByteArrayInputStream FileInputStream FilterInputStream
   InputStream ObjectInputStream PipedInputStream
   SequenceInputStream StringBufferInputStream
FileInputStream 文件字节输入流 将硬盘文件中的数据读取到内存中
构造方法:
  FileInputStream(String name);//文件路径
  FileInputStream(File file);//文件
构造方法做了两件事:
 (1)创建一个FileOutputStream对象
 (2)将FileOutputStream对象指向构造方法中要读取的文件
读取数据的原理:Java程序 -> JVM -> OS -> OS调用读取数据的方法 -> 读取文件
FileInputStream使用步骤:
 (1)创建FileInputStream,构造方法中绑定要读取的数据源
 (2)调用read方法,读取数据
 (3)调用close方法,释放资源
测试类
public class Demo04InputStream {
public static void main(String[] args) throws IOException {
FileInputStream fis = new FileInputStream(“day08-code\src\demo01IO\1.txt”);
int num = fis.read();
System.out.println(num);//1.txt中第一个字节为a 输出97
num = fis.read();
System.out.println(num);//1.txt中第二个字节为A 输出65

    //使用while循环优化
    int num1 = 0;
    while((num1 = fis.read()) != -1) {
        //分为三步:读取下一个字节;将读取到的字节赋值给num;判断num是否不等于-1,若等于-1就跳出循环
        //read读取到一个字节后,会将指针后移一位
        System.out.println((char)num1);
    }
    fis.close();
    //读取原理:
    // 创建FileInputStream,并指向要读取的文件的第一个字节
    // 调用read方法,Java程序 -> JVM -> OS -> OS调用读取数据的方法读取一个字节 -> 返回给JVM ->返回给Java程序
    // 读取完一个字节后指针后移一位
    // 循环读取字节,直到OS读取到系统的结束标记(不可见)-> 返回给JVM结束标记 -> 返回给Java程序-1
    // 读取后指针后移一位
}

}
1.5 一次读取多个字节
int read(byte[] b)
  返回读取的有效字节个数
  byte数组起到缓冲作用,存储每次读取到的多个字节 长度一般定义为1024或1024的整数倍
测试类
public class Demo05ReadMulti {
  public static void main(String[] args) throws IOException {
    FileInputStream fis = new FileInputStream(“day08-code\src\demo01IO\6.txt”);
    byte[] bytes = new byte[2];
    int len = fis.read(bytes);
    System.out.println(len);//2
    System.out.println(new String(bytes));//AB 使用String类的构造方法将字节数组转换为字符串输出
    len = fis.read(bytes);
    System.out.println(len);//2
    System.out.println(new String(bytes));//CD
    len = fis.read(bytes);
    System.out.println(len);//1
    System.out.println(new String(bytes));//ED
    len = fis.read(bytes);
    System.out.println(len);//-1
    System.out.println(new String(bytes));//ED
    //读取原理:
    //创建FileInputStream对象,将其指向要读取的文件,刚开始指向第一个字节
    //创建大小为2的字节数组,默认值为 0 0(byte short int long默认值都是0)
    //读取数据。Java程序 -> JVM -> OS -> OS调用读取数据的方法 -> 读取AB返回给OS -> 返回给JVM -> 返回给Java程序 字节数组的值变为A B len=2
    //指针指向C 第二次读取:读取CD 字节数组的值变为C D len=2
    //指针指向E 第三次读取:读取E 发现下一个是结束标记,结束读取 字节数组变为E D len=1
    //指针指向结束标记 第四次读取:无数据可读取 字节数组还是E D len=-1
 
    //使用循环优化
    int len1 = 0;
    byte[] bytes1 = new byte[1024];
    while((len1 = fis.read(bytes1)) != -1) {
      System.out.println(len1);
      System.out.println(new String(bytes1));//直接将字节数组转换为字符串打印会打印出很多空格,因为数组长度为1024,而这里只读取了5个字节
      //System.out.println(new String(bytes, 0, len));//将部分数组转换为字符串进行打印,只打印读取的部分
    }
  }
}
1.6 字节流练习:文件复制
public class Demo06FileCopy {
  public static void main(String[] args) throws IOException {
    long start = System.currentTimeMillis();
    //创建文件字节输入流,指向要复制的文件,用于读取文件
    FileInputStream fis = new FileInputStream(“H:\图片\壁纸.jpeg”);
    //创建文件字节输出流,指向要复制到的目的地文件,用于写入文件
    FileOutputStream fos = new FileOutputStream(“G:\有的没的\壁纸.jpeg”);
    //read方法返回值,用于判断是否读取到文件末尾
    int len = 0;
    //定义字节数组,用于存储每次读取的多个字节
    byte[] bytes = new byte[1024];
    //循环读取文件
    while ((len = fis.read(bytes)) != -1) {
      //将每次读取到的字节数组写入到目的地文件中
      fos.write(bytes, 0, len);
    }
    //先关闭写的输出流,再关闭读的输入流,因为写完了肯定读完了
    fos.close();
    fis.close();
    long end = System.currentTimeMillis();
    long time = end - start;
    System.out.println(“复制文件共耗时:” + time + “ms”);
  }
}
1.7 使用字节流读取中文
  UTF-8:一个中文占用三个字节
  GBK:一个中文占用两个字节
  使用字节流读取无法读取中文,需要使用字符流
测试类
public class Demo07ReadChinese {
  public static void main(String[] args) throws IOException {
    FileInputStream fis = new FileInputStream(“day08-code\src\demo01IO\7.txt”);
    int len = 0;
    while ((len = fis.read()) != -1) {
      System.out.println(len);
      //228
      //189
      //160
      //229
      //165
      //189 读取了两个中文字符对应的6个字节,前三个字节组成你,后三个字节组成好
      System.out.println((char) len);//转为char类型会出现乱码
    }
    fis.close();
  }
}

2、字符流

2.1 Reader类
Reader类是所有字符输入流的父类,是一个抽象类
方法:
  int read();一次读取一个字符
  int read(char[] cbuf);一次读取多个字符,放入数组
  void close();释放资源
子类:BufferedReader CharArrayReader FilterReader InputStreamReader 
   PipedReader StringReader
FileReader是InputStreamReader的子类 读取字符文件 将硬盘中的文件以字符的形式读取到内存中
构造方法:
  FileReader(String filename);文件路径
  FileReader(File file);文件
构造方法做了两件事:
 (1)创建FileReader对象
 (2)将FileReader对象指向要读取的文件
使用步骤:
 (1)创建FileReader对象,参数指向要读取的文件
 (2)调用read方法以字形式读取文件
 (3)调用close方法释放资源
测试类
public class Demo01Reader {
  public static void main(String[] args) throws IOException {
    FileReader fr = new FileReader(“day08-code\src\demo02Reader\1.txt”);
    int len = 0;
 
    //一次读取一个字符
    while ((len = fr.read()) != -1) {
      System.out.print((char) len);//123你好hellojava再见???###
    }
 
    //一次读取多个字符
    char[] chars = new char[1024];
    while ((len = fr.read(chars)) != -1) {
      System.out.println(new String(chars, 0 ,len));//123你好hellojava再见???###
    }
    fr.close();
  }
}
2.2 Writer类
与字节输出流的最大区别:会将数据写入到内存中,而不是直接写入到文件中
Writer是所有字符输出流的父类,是一个抽象类
方法:
  void write(int c);
  void write(char[] cbuf);
  void write(String str);
  void write(String str, int off, int len);
  void flush();刷新该流的缓冲
  void close();
子类:BufferedWriter CharArrayWriter FilterWriter OutputStreamWriter
   PipedWriter PrintWriter StringWriter
FileWriter是OutputStreamWriter的子类 文件字符输出流 将内存中的字符数据写入硬盘文件中
  FileWriter(String filename);文件路径
  FileWriter(File file);文件
构造方法做了三件事:
 (1)创建FileWriter对象
 (2)根据参数传递的文件/文件路径创建一个文件
 (3)将FileWriter对象指向创建的文件
使用步骤:
 (1)创建FileWriter对象,参数指向要写入数据的文件
 (2)调用write方法,将数据以字符形式写入内存缓冲区(在内存中有一个将字符转换为字节的过程)
 (3)调用flush方法将内存缓冲区中的数据刷新到文件中
 (4)调用close方法释放资源(会先将内存缓冲区中的数据刷新到文件中,可以不写flush方法,但关闭流后不能继续使用)
测试类
public class Demo02Writer {
  public static void main(String[] args) throws IOException {
    FileWriter fw = new FileWriter(“day08-code\src\demo02Reader\2.txt”);
    //write(int c)
    fw.write(97);//a
    //write(char[] cbuf)
    char[] chars = {‘c’, ‘d’, 49, 48, 48, ‘1’, ‘我’, ‘你’, ‘%’, ‘?’};
    fw.write(chars);//acd1001我你%?
    //write(String str, int off, int len)
    fw.write(chars, 5, 3);//acd1001我你%?1我你
    //write(String str)
    fw.write(“hello java”);//acd1001我你%?1我你hello java
    //write(String str, int off, int len)
    fw.write(“hello world”, 2, 6);//acd1001我你%?1我你hello javallo wo
    fw.close();
  }
}
2.3 字符输出流的续写和换行
续写使用FileWriter的带两个参数的构造方法
  FileWriter(String filename, boolean append);
  FileWriter(File file, boolean append);
    append = true 续写
换行
  Windows \r\n
  Linux /n
  Mac /r
测试类
public class Demo03ContinueWrite {
  public static void main(String[] args) throws IOException {
    FileWriter fw = new FileWriter(“day08-code\src\demo02Reader\2.txt”,true);
    for (int i = 0; i < 5; i++) {
      //fw.write(“你好” + i);//acd1001我你%?1我你hello javallo wo你好0你好1你好2你好3你好4
      fw.write(“你好” + i + “\r\n”);
      //acd1001我你%?1我你hello javallo wo你好0你好1你好2你好3你好4你好0
      //你好1
      //你好2
      //你好3
      //你好4
    }
    fw.close();
  }
}
2.4 使用try-catch-finally处理流中的异常
(1)将FileWriter对象指向要读取的文件和调用write方法将数据写入文件这两步放入try
(2)将调用close方法释放资源这一步放入finally,否则放入try前面出现异常的话就不会释放资源了
(3)创建FileWriter对象这一步要放在try-catch之前,否则放入try的话,finally中无法使用这个对象
(4)创建FileWriter对象时初始化为null,否则try中创建对象失败的话,finally中无法使用这个对象
(5)调用close方法也可能有异常,要在finally中嵌套一个try-catch
(6)创建FileWriter对象失败的话,fw默认为null,null无法调用方法,所以在调用close方法之前要先判断是否为null,否则会抛出空指针异常
  (创建FileWriter对象失败,下面的代码不执行,所以不需要在调用write方法之前判断是否为null)
2.5 简化处理流异常的try-catch方法
  JDK7的新特性:可以在try后加一个(),里面创建流对象,try结束后会自动释放流对象,不用写finally
  JDK9的新特性:可以在try前面定义流对象,在try后的括号里引入对象名,try结束后会自动释放流对象,不用写finally,但在try前面定义流对象的语句也会有异常,所以还是需要throws抛出异常
测试类
public class Demo05trycatchSimplify {
  public static void main(String[] args) throws IOException {
    //JDK7
    try(FileWriter fw = new FileWriter(“day08-code\src\demo02Reader\2.txt”,true)😉 {
      for (int i = 0; i < 5; i++) {
        fw.write(“你好” + i + “\r\n”);
      }
    } catch (IOException e) {
      System.out.println(e);
    }
    //JDK9
    FileWriter fw = new FileWriter(“day08-code\src\demo02Reader\2.txt”,true);
    try(fw) {
      for (int i = 0; i < 5; i++) {
        fw.write(“你好” + i + “\r\n”);
      }
    } catch (IOException e) {
      System.out.println(e);
    }
  }
}

3、Properties 属性集

3.1 Properties概述
Properties 属性集
  唯一和I/O流结合的集合 可以保存在流中或从流中加载 每个键和值都是字符串,不用写泛型
  Properties extends Hashtable<K,V>
  Hashtable<K,V> implements Map<K,V>
方法:
  void load(InputStream inStream);将硬盘中的文件(键值对)读取到集合中使用
  void load(Reader reader);
  void store(OutputStream out, String comments);字节输出流 不能写入中文
  void store(Writer writer, String comments);字符输出流 可以写入中文
操作字符串的特有方法:
  Object setProperty(String key, String value);底层调用Map的put方法
  String getProperty(String key);底层调用Map的get方法
  Set stringPropertyName();返回此属性列表中的键集 底层调用Map的keySet方法
测试类
public class Demo01Properties {
  public static void main(String[] args) {
    Properties p = new Properties();
    p.setProperty(“01”, “张三”);
    p.setProperty(“02”, “李四”);
    p.setProperty(“03”, “王五”);
    for (String key : p.stringPropertyNames()) {
      System.out.println(key + “:” + p.getProperty(key));//01:张三
                              //02:李四
                            //03:王五
    }
  }
}
3.2 store方法
将集合中的临时数据持久化写入到硬盘中存储
  void store(OutputStream out, String comments);字节输出流 不能写入中文
  void store(Writer writer, String comments);字符输出流 可以写入中文
  String comments 解释说明保存的文件是干什么的 不能使用中文,会产生乱码,因为默认是Unicode编码 而系统默认是GBK编码 一般使用空字符串""
使用步骤:
 (1)创建Properties对象
 (2)创建字节/字符输出流对象,绑定要输出到的文件
 (3)调用store方法
 (4)调用close方法释放资源
3.3 load方法
  void load(InputStream inStream);字节输入流 不能读取含有中文的键值对
  void load(Reader reader);字符输入流 可以读取含有中文的键值对
使用步骤:
 (1)创建Properties类的对象
 (2)调用load方法读取保存键值对的文件
 (3)遍历Properties对象检查是否读取成功
注意:存储键值对的文件中,可以用=、空格或其他符号连接键值对;
             可以用#注释键值对;
             键值对已经是字符串,不需要加引号
public class Demo03PropertiesLoad {
  public static void main(String[] args) throws IOException {
    Properties p = new Properties();
    //这里要读取的文件含有中文 使用字符输入流
    FileReader fr = new FileReader(“day08-code\src\demo03Properties\1.txt”);
    p.load(fr);
    System.out.println§;//{01=张三, 02=李四, 03=王五}
    for (String key : p.stringPropertyNames()) {
      System.out.println(key + “=” + p.getProperty(key));//01=张三
                               //02=李四
                              //03=王五
    }
  }
}

4、缓冲流

4.1 BufferedOutputStream类 字节缓冲输出流
缓冲流
  对基本流的增强
  给基本流增加一个缓冲区(数组),一次读取/写入多个字节/字符,提高读写效率
BufferedOutputStream 字节缓冲输出流
构造方法:
  BufferedOutputStream(OutputStream out);在字节输出流内部创建默认大小的缓冲区
  BufferedOutputStream(OutputAtream out, int size);在字节输出流内部创建指定大小的缓冲区
使用步骤:
 (1)创建OutputStream对象,绑定要输出到的文件
 (2)创建BufferedOutputStream对象,参数传递OutputStream对象
 (3)调用write方法,将数据写入BufferedOutputStream对象的内部缓冲区
 (4)调用flush方法,将内部缓冲区的数据刷新到文件中
 (5)调用close方法释放资源(会先调用flush方法刷新数据到文件中)
测试类
public class Demo01BufferedOutputStream {
  public static void main(String[] args) throws IOException {
    FileOutputStream fis = new FileOutputStream(“day08-code\src\demo04Buffered\1.txt”);
    BufferedOutputStream bis = new BufferedOutputStream(fis);
    bis.write(“写入数据到1.txt”.getBytes());
    bis.close();//关闭缓冲流会自动关闭基本流
  }
}
4.2 BufferedInputStream类 字节缓冲输入流
构造方法:
  BufferedInputStream(InputStream in);在字节输入流内部创建默认大小的缓冲区
  BufferedInputStream(InputAtream in, int size);在字节输入流内部创建指定大小的缓冲区
使用步骤:
 (1)创建InputStream对象,绑定要读取的文件
 (2)创建BufferedInputStream对象,参数传递InputStream对象
 (3)调用read方法,将数据读取到BufferedInputStream对象的内部缓冲区
 (4)调用close方法释放资源
测试类
public class Demo02BufferedInputStream {
  public static void main(String[] args) throws IOException {
    FileInputStream fis = new FileInputStream(“day08-code\src\demo04Buffered\1.txt”);
    BufferedInputStream bis = new BufferedInputStream(fis);
    //一次读取一个字节
    int len = 0;//记录每次读取到的字节
    while ((len = bis.read()) != -1) {
      System.out.println(len);
    }
    //一次读取多个字节
    int len1 = 0;//记录每次读取的有效字节个数
    byte[] bytes = new byte[1024];//记录每次读取到的多个字节
    while((len1 = bis.read(bytes)) != -1) {
      System.out.println(new String(bytes, 0, len1));//写入数据到1.txt
    }
    bis.close();
  }
}
4.3 使用字节缓冲流来复制文件
  使用字节流耗时5ms
  使用字节缓冲流耗时3ms
public class Demo03BufferedFileCopy {
  public static void main(String[] args) throws IOException {
    long start = System.currentTimeMillis();
    FileInputStream fis = new FileInputStream(“G:\有的没的\FCN摘要.PNG”);
    BufferedInputStream bis = new BufferedInputStream(fis);
    FileOutputStream fos = new FileOutputStream(“H:\图片\FCN摘要.PNG”);
    BufferedOutputStream bos = new BufferedOutputStream(fos);
    int len = 0;
    byte[] bytes = new byte[1024];
    while ((len = bis.read(bytes)) != -1) {
      bos.write(bytes, 0, len);
    }
    bos.close();
    bis.close();
    //  FileInputStream fis = new FileInputStream(“G:\有的没的\FCN摘要.PNG”);
    //  FileOutputStream fos = new FileOutputStream(“H:\图片\FCN摘要.PNG”);
    //  int len = 0;
    //  byte[] bytes = new byte[1024];
    //  while ((len = fis.read(bytes)) != -1) {
    //    fos.write(bytes, 0, len);
    //  }
    //  fos.close();
    //  fis.close();
    long end = System.currentTimeMillis();
    long time = end - start;
    System.out.println(“耗时” + time + “ms”);
  }
}
4.4 BufferedWriter类 字符缓冲输出流
构造方法:
  BufferedWriter(Writer out);默认缓冲区大小
  BufferedWriter(Writer out, int sz);指定缓冲区大小
  参数传递字节输出流,缓冲流会给其创建一个缓冲区,提高其写入效率
特有方法:
  void newLine();写入一个换行符,会自动根据不同的操作系统获取不同的换行符
         println调用的就是newLine方法
public class Demo04BufferedWriter {
  public static void main(String[] args) throws IOException {
    BufferedWriter bw = new BufferedWriter(new FileWriter(“day08-code\src\demo04Buffered\2.txt”));
    bw.write(“我是一个粉刷匠”, 0, 4);
    bw.newLine();//换行
    bw.write(“卖报的”);
    bw.close();//我是一个
         //卖报的
  }
}
4.5 BufferedReader类 字符缓冲输入流
构造方法:
  BufferedReader(Reader in);默认缓冲区大小
  BufferedReader(Reader in, int sz);指定缓冲区大小
  参数传递字节输入流,缓冲流会给其创建一个缓冲区,提高其写入效率
特有方法:
  String readLine();
  读一行文本 根据行终止符号:换行\r 回车\n 换行后跟着回车\r\n来确定一行是否结束
  返回一行文本内容,不包括换行符号,若已到达行末尾,返回null
测试类
public class Demo05BufferedReader {
  public static void main(String[] args) throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(“day08-code\src\demo04Buffered\2.txt”));
    int len = 0;
    char[] chars = new char[1024];
    while ((len = br.read(chars)) != -1) {
      System.out.println(new String(chars, 0, len));//我是一个
                            //卖报的
    }
    System.out.println(br.readLine());//我是一个
    //使用循环来读取数据
    String line= null;
    while ((line = br.readLine()) != null) {
      System.out.println(line);//我是一个
                 //卖报的
    }
    br.close();
  }
}
4.6 练习:对文本进行排序
(1)创建HashMap对象,key存储每行文本序号,value存储每行文本内容
(2)创建字符缓冲输入流对象
(3)创建字符缓冲输出流对象
(4)调用readLine方法每次读取一行文本
(5)对读取到的文本进行切割,得到文本序号和文本内容
(6)将切割好的文本存入HashMap对象,其会自动排序
(7)遍历HashMao对象,获取每一个键值对
(8)将每一个键值对拼接为每一行文本
(9)调用write方法写入每一行文本
(10)释放资源
注意:字符输入流可以正常读取IDE默认的编码格式UTF-8编码的文本,但读取系统默认编码(中文系统是GBK)的文本就会出现乱码,所以这里要读取的文本可以直接在idea里创建并转换为UTF-8编码
出师表.txt
1.先帝创业未半而中道崩殂(cú),今天下三分,益州疲(pí)弊,此诚危急存亡之秋也。然侍卫之臣不懈(xiè)于内,忠志之士忘身于外者,盖追先帝之殊遇,欲报之于陛下也。诚宜开张圣听,以光先帝遗(yí)德,恢弘志士之气,不宜妄自菲薄,引喻失义,以塞(sè)忠谏之路也。
9.今当远离,临表涕零,不知所云。
3.侍中、侍郎郭攸(yōu)之、费祎(yī)、董允等,此皆良实,志虑忠纯,是以先帝简拔以遗(wèi)陛下。愚以为宫中之事,事无大小,悉以咨之,然后施行,必能裨(bì)补阙漏,有所广益。
8.愿陛下托臣以讨贼兴复之效,不效,则治臣之罪,以告先帝之灵。若无兴德之言,则责攸之、祎、允等之慢,以彰其咎(jiù)。陛下亦宜自谋,以咨诹(zōu)善道,察纳雅言,深追先帝遗诏。臣不胜受恩感激!
6.臣本布衣,躬耕于南阳,苟全性命于乱世,不求闻(wén)达于诸侯。先帝不以臣卑(bēi)鄙(bǐ),猥(wěi)自枉屈,三顾臣于草庐之中,咨臣以当世之事,由是感激,遂许先帝以驱驰。后值倾覆,受任于败军之际,奉命于危难之间,尔来二十有(yòu)一年矣! 【“有”是通假字,通“又”,跟在数词后面表示约数。所以读yòu】
2.宫中府中,俱为一体,陟(zhì )罚臧(zāng)否(pǐ),不宜异同。若有作奸犯科及为忠善者,宜付有司论其刑赏,以昭陛下平明之理,不宜偏私,使内外异法也。
7.先帝知臣谨慎,故临崩寄臣以大事也。受命以来,夙(sù)夜忧叹,恐托付不效,以伤先帝之明,故五月渡(dù)泸,深入不毛。今南方已定,兵甲已足,当奖率三军,北定中原,庶(shù)竭驽(nú)钝,攘(rǎng)除奸凶,兴复汉室,还于旧都。此臣所以报先帝而忠陛下之职分也。至于斟酌损益,进尽忠言,则攸之、祎、允之任也。
5.亲贤臣,远小人,此先汉所以兴隆也;亲小人,远贤臣,此后汉所以倾颓也。先帝在时,每与臣论此事,未尝不叹息痛恨于桓(huán)、灵也。侍中、尚书、长(zhǎng)史、参军,此悉贞良死节之臣,愿陛下亲之信之,则汉室之隆,可计日而待也。
4.将军向宠,性行(xíng)淑均,晓畅军事,试用于昔日,先帝称之曰能,是以众议举宠为督。愚以为营中之事,悉以咨之,必能使行(háng )阵和睦,优劣得所。
测试类
public class Demo06FileSort {
  public static void main(String[] args) throws IOException {
    //1、创建HashMap对象,key存储每行文本序号,value存储每行文本内容
    HashMap<String, String> map = new HashMap<>();
    //2、创建字符缓冲输入流对象
    BufferedReader br = new BufferedReader(new FileReader(“day08-code\src\demo04Buffered\出师表.txt”));
    //3、创建字符缓冲输出流对象
    BufferedWriter bw = new BufferedWriter(new FileWriter(“day08-code\src\demo04Buffered\出师表排序后.txt”));
    //4、调用readLine方法每次读取一行文本
    String line = null;
    while ((line = br.readLine()) != null) {
      System.out.println(line);
      //5、对读取到的文本进行切割,得到文本序号和文本内容
      String[] linetext = line.split("\.");
      System.out.println(linetext[0]);
      //6、将切割好的文本存入HashMap对象,其会自动排序
      map.put(linetext[0], linetext[1]);
    }
    //7、遍历HashMao对象,获取每一个键值对
    for (String key : map.keySet()) {
      //8、将每一个键值对拼接为每一行文本
      String everyline = key + “.” + map.get(key);
      //9、调用write方法写入每一行文本
      //换行
      bw.write(everyline);
      bw.newLine();
    }
    //10、释放资源
    bw.close();
    br.close();
  }
}

5、转换流

5.1 编码概述
编码:字符 -> 字节
解码:字节 -> 字符
字符编码:自然语言与二进制之间的对应规则
字符集/编码表:一个系统支持的所有字符的集合
一套字符集至少要有一套字符编码
  ASCII:显示字母、数字和常用符号 整数7位表示一个字符 负数8位表示一个字符
  ISO-8859-1:显示欧洲常用文字 单字节编码 兼容ASCII
  GB2312:简体中文
  GBK:双字节编码(任何字符占用两个字节) 兼容GB2312 繁体汉字 日韩汉字
  GB18030:多字节编码(每个字符占用1/2/4个字节) 少数民族汉字
  Unicode:万国码 最多使用4个字节表示字符 包括UTF-8 UTF-16 UTF-32
       ASCII 1个字节
       拉丁文 2个字节
       大部分语言(含中文) 3个字节
       极少使用的Unicode辅助字符 4个字节
5.2 OutputStreamWriter类
FileReader用来以字符的形式读取文件,其源码是调用了FileInputStream先以字节形式读取文件,再将读取到的字节转换为字符。但其只能查询IDE默认编码表UTF-8 只能正常读取UTF-8编码格式的文件 读取GBK格式的文件会出现乱码
其父类InputStreamReader 可以使用指定的字符集将字节转换为字符
            可以查询指定的编码表
   OutputStreamWriter 可以使用指定的字符集将字符转换为字节
            可以查询指定的编码表
OutputStreamWriter
构造方法:
  OutputStreamWriter(OutputStream out);默认字符集 将字节写入文件中
  OutputStreamWriter(OutputStream out, String charsetName);String charsetName 指定字符集 不区分大小写
测试类
public class Demo02OutputStreamWriter {
  public static void main(String[] args) throws IOException {
    FileOutputStream fis = new FileOutputStream(“day08-code\src\demo05Convert\gbk.txt”);
    OutputStreamWriter ow = new OutputStreamWriter(fis, “GBK”);
    ow.write(“这是GBK编码格式的文件”);//写入了GBK编码格式的文件,打开文件会默认以UTF-8的格式查看,会出现乱码,需要设置为GBK格式
    ow.close();
  }
}
5.3 InputStreamReader类
构造方法:
  InputStreamReader(InputStream in);默认字符集 InputStream in 读取文件中保存的字节
  InputStreamReader(InputStream in, String charsetName);String charsetName 指定字符集 不区分大小写
public class Demo03InputStreamReader {
  public static void main(String[] args) throws IOException {
    InputStreamReader ir = new InputStreamReader(new FileInputStream(“day08-code\src\demo05Convert\gbk.txt”), “GBK”);
    int len = 0;
    char[] chars = new char[1024];
    while ((len = ir.read(chars)) != -1) {
      System.out.println(new String(chars, 0, len));//这是GBK编码格式的文件
    }
  }
}
5.4 练习:转换文件编码格式 GBK -> UTF-8
  使用绑定GBK的输入转换流,读取GBK编码格式的文件
  使用绑定UTF-8的输出转换流,将读取到的数据写入UTF-8编码格式的文件
public class Demo04ConvertEncoding {
  public static void main(String[] args) throws IOException {
    InputStreamReader ir = new InputStreamReader(new FileInputStream(“day08-code\src\demo05Convert\gbk.txt”), “GBK”);
    OutputStreamWriter ow = new OutputStreamWriter(new FileOutputStream(“day08-code\src\demo05Convert\utf8.txt”), “UTF-8”);
    int len = 0;
    char[] chars = new char[1024];
    while ((len = ir.read(chars)) != -1) {
      ow.write(chars, 0, len);
    }
    ow.close();
    ir.close();
  }
}

6、序列化

6.1 对象的序列化流/反序列化流
(1)将对象以流的形式写入文件,称为写对象/对象的序列化
   对象中不仅仅包含字符,也包含字符,所以使用字节流
   ObjectOutputStream 对象的序列化流
(2)将文件中的对象以流的形式读取出来,称为读对象/对象的反序列化
   读取的文件保存数据的形式都是字节,所以使用字节流
   ObjectInputStream 对象的反序列化流
6.2 ObjectOutputStream 对象的序列化
ObjectOutputStream 对象的序列化
  构造方法:ObjectOutputStream(OutputStream out);参数传递字节输出流
  特有方法:writeObject();
  序列化和反序列化时,会抛出没有序列化异常NotSerializableException,要进行序列化的类必须实现Serializable接口以启用序列化功能
  Serializable接口也被称为标记型接口,源码没有任何东西,会给实现该接口的类加上一个标记,用于序列化时判断其是否可以序列化
  一个文件只能写入一个对象
Person类
public class Person implements Serializable {
  private String name;
  private int age;
 
  public Person() {
  }
 
  public Person(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 “Person{” +
       “name=’” + name + ‘’’ +
       “, age=” + age +
       ‘}’;
  }
}
测试类
public class Demo02ObjectOutputStream {
  public static void main(String[] args) throws IOException {
    ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(“day08-code\src\demo06Serialize\1.txt”));
    Person p = new Person(“张三”, 19);
    //Person p1 = new Person(“李四”, 16);
    //oo.writeObject(p1);
    oo.writeObject§;
    oo.close();
  }
}
6.3 ObjectInputStream 对象的反序列化
  构造方法:ObjectInputStream(InputStream in);参数传递字节输入流
  特有方法:readObject();会声明抛出class文件找不到异常ClassNotFoundException
  序列化和反序列化时,会抛出没有序列化异常NotSerializableException,要进行序列化的类必须实现Serializable接口以启用序列化功能
  Serializable接口也被称为标记型接口,源码没有任何东西,会给实现该接口的类加上一个标记,用于序列化时判断其是否可以序列化
  反序列化前提:(1)要反序列化的类必须实现Serializable接口
         (2)必须存在类对应的class文件
测试类
public class Demo03ObjectInputStream {
  public static void main(String[] args) throws IOException, ClassNotFoundException {
    ObjectInputStream oi = new ObjectInputStream(new FileInputStream(“day08-code\src\demo06Serialize\1.txt”));
    Object o = oi.readObject();
    System.out.println(o);//Person{name=‘张三’, age=19}
    Person p = (Person) o;
    System.out.println(p.getName() + p.getAge());//张三19
    oi.close();
  }
}

7、

7.1 transient关键字
  static关键字:静态优先于非静态加载到内存中(优先于对象进入到内存中)
        被static修饰的成员变量是不能被序列化的 序列化的都是对象
  transient关键字:被transient修饰的成员变量是不能被序列化的 不希望某个成员变量被序列化可以加上transient关键字
  如:Person类的age加上transient 序列化再反序列化输出的age是默认值0,不是我们设置的初始值19
7.2 InvalidClassException异常
  当JVM反序列化对象时,能找到class文件,但class文件在序列化对象之后发生了修改,反序列化操作也会失败并抛出异常InvalidClassException
  如:序列化Person对象生成1.txt文件之后,修改age的类型 private变为public,对应的class文件也就发生了变化,再反序列化就会抛出异常,
  原因是txt文件里的序列号和class文件中的序列号不一样,发生冲突
原理:
(1)编译器javac会把源文件Person.java编译生成字节码文件Person.class
(2)Person类实现了Serializable接口,会根据类的定义(定义的属性、方法)给Person.class文件里添加上一个序列号serialVersionUID
(3)序列化,将对象写入1.txt文件,txt文件中也会有序列号serialVersionUID
(4)反序列化,会对比txt文件和class文件中的序列号,一样则反序列化成功
(5)修改类的定义,会重新编译生成class文件,根据类的定义会更新序列号
(6)反序列化,两个文件的序列号不一样,反序列化失败,抛出异常InvalidClassException
问题:每次修改类的定义,都会生成新的序列号
解决:
  无论是否修改类的定义,都不重新生成新的序列号,在类中手动添加一个不变的序列号
   static final long serialVersionUID = ***L;
7.3 练习:序列化集合
当希望在文件中保存多个对象时,可以将多个对象存入集合,将集合序列化
Person类
public class Person implements Serializable {
  private String name;
  private int age;
 
  public Person() {
  }
 
  public Person(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 “Person{” +
       “name=’” + name + ‘’’ +
       “, age=” + age +
       ‘}’;
  }
}
测试类
public class Demo03SerializeArray {
  public static void main(String[] args) throws IOException, ClassNotFoundException {
    ObjectOutputStream oo = new ObjectOutputStream(new FileOutputStream(“day08-code\src\demo07\1.txt”));
    ObjectInputStream oi = new ObjectInputStream(new FileInputStream(“day08-code\src\demo07\1.txt”));
    Person p1 = new Person(“张三”, 19);
    Person p2 = new Person(“李四”, 29);
    Person p3 = new Person(“王五”, 15);
    ArrayList<Person> list = new ArrayList<>();
    list.add(p1);
    list.add(p2);
    list.add(p3);
    oo.writeObject(list);
    Object obj = oi.readObject();
    System.out.println(obj);//[Person{name=‘张三’, age=19}, Person{name=‘李四’, age=29}, Person{name=‘王五’, age=15}]
    ArrayList list2 = (ArrayList) obj;
    for (Person person : list) {
      System.out.print(person);
      System.out.println(person.getName() + person.getAge());
    }
    oo.close();
    oi.close();
  }
}
7.4 打印流PrintStream
打印流PrintStream extends OutputStream 为其它输出流提供方便地打印各种数据值的功能
   System.out.println中的out就是一个打印流,println就是打印流中的方法
特点:
 (1)只负责数据的输出,不负责读取
 (2)永远不会抛出IOException(会抛出文件找不到异常)
特有方法:print println 可以输出任意类型的值
构造方法:
  PrintStream(File file);
  PrintStream(String filename);
  PrintStream(OutputStream out);
注意:
 (1)如果使用继承自父类的write方法写入数据,那么查看数据的时候会查询编码表 97就是a
 (2)如果使用自己特有的方法print println写入数据,写的数据原样输出 97就是97
  可以使用System类中的setOut方法设置输出语句的目的地,将目的地改为参数中传递的打印流的目的地
public class Demo04PrintStream {
  public static void main(String[] args) throws FileNotFoundException {
    PrintStream ps = new PrintStream(new FileOutputStream(“day08-code\src\demo07\print.txt”));
    ps.write(97);
    ps.print(97);
    ps.close();//a97
    //改变输出语句的目的地
    System.out.println(“输出在控制台”);
    PrintStream ps2 = new PrintStream(“day08-code\src\demo07\改变后的输出目的地.txt”);
    System.setOut(ps2);
    System.out.println(“输出在改变后的输出目的地文件中”);
  }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1JAVA SE 1.1深入JAVA API 1.1.1Lang包 1.1.1.1String类和StringBuffer类 位于java.lang包中,这个包中的类使用时不用导入 String类一旦初始化就不可以改变,而stringbuffer则可以。它用于封装内容可变的字符串。它可以使用tostring()转换成string字符串。 String x=”a”+4+”c”编译时等效于String x=new StringBuffer().append(“a”).append(4).append(“c”).toString(); 字符串常量是一种特殊的匿名对象,String s1=”hello”;String s2=”hello”;则s1==s2;因为他们指向同一个匿名对象。 如果String s1=new String(“hello”);String s2=new String(“hello”);则s1!=s2; /*逐行读取键盘输入,直到输入为“bye”时,结束程序 注:对于回车换行,在windows下面,有'\r'和'\n'两个,而unix下面只有'\n',但是写程序的时候都要把他区分开*/ public class readline { public static void main(String args[]) { String strInfo=null; int pos=0; byte[] buf=new byte[1024];//定义一个数组,存放换行前的各个字符 int ch=0; //存放读入的字符 system.out.println(“Please input a string:”); while(true) { try { ch=System.in.read(); //该方法每次读入一个字节的内容到ch变量中。 } catch(Exception e) { } switch(ch) { case '\r': //回车时,不进行处理 break; case '\n': //换行时,将数组总的内容放进字符串中 strInfo=new String(buf,0,pos); //该方法将数组中从第0个开始,到第pos个结束存入字符串。 if(strInfo.equals("bye")) //如果该字符串内容为bye,则退出程序。 { return; } else //如果不为bye,则输出,并且竟pos置为0,准备下次存入。 { System.out.println(strInfo); pos=0; break; } default: buf[pos++]=(byte)ch; //如果不是回车,换行,则将读取的数据存入数组中。 } } } } String类的常用成员方法 1、构造方法: String(byte[] byte,int offset,int length);这个在上面已经用到。 2、equalsIgnoreCase:忽略大小写的比较,上例中如果您输入的是BYE,则不会退出,因为大小写不同,但是如果使用这个方法,则会退出。 3、indexOf(int ch);返回字符ch在字符串中首次出现的位置 4、substring(int benginIndex); 5、substring(int beginIndex,int endIndex); 返回字符串的子字符串,4返回从benginindex位置开始到结束的子字符串,5返回beginindex和endindex-1之间的子字符串。 基本数据类型包装类的作用是:将基本的数据类型包装成对象。因为有些方法不可以直接处理基本数据类型,只能处理对象,例如vector的add方法,参数就只能是对象。这时就需要使用他们的包装类将他们包装成对象。 例:在屏幕上打印出一个*组成的矩形,矩形的宽度和高度通过启动程序时传递给main()方法的参数指定。 public class testInteger { public static void main(String[] args) //main()的参数是string类型的数组,用来做为长,宽时,要转换成整型。 { int w=new Integer(args[0]).intValue(); int h=Integer.parseInt(args[1]); //int h=Integer.valueOf(args[1]).intValue(); //以上为三种将字符串转换成整形的方法。 for(int i=0;i<h;i++) { StringBuffer sb=new StringBuffer(); //使用stringbuffer,是因为它是可追加的。 for(int j=0;j<w;j++) { sb.append('*'); } System.out.println(sb.toString()); //在打印之前,要将stringbuffer转化为string类型。 } } } 比较下面两段代码的执行效率: (1)String sb=new String(); For(int j=0;j<w;j++) { Sb=sb+’*’; } (2) StringBuffer sb=new StringBuffer(); For(int j=0;j<w;j++) { Sb.append(‘*’); } (1)和(2)在运行结果上相同,但效率相差很多。 (1)在每一次循环中,都要先将string类型转换为stringbuffer类型,然后将‘*’追加进去,然后再调用tostring()方法,转换为string类型,效率很低。 (2)在没次循环中,都只是调用原来的那个stringbuffer对象,没有创建新的对象,所以效率比较高。 1.1.1.2System类与Runtime类 由于java不支持全局函数和全局变量,所以java设计者将一些与系统相关的重要函数和变量放在system类中。 我们不能直接创建runtime的实例,只能通过runtime.getruntime()静态方法来获得。 编程实例:在java程序中启动一个windows记事本程序的运行实例,并在该运行实例中打开该运行程序的源文件,启动的记事本程序5秒后关闭。 public class Property { public static void main(String[] args) { Process p=null; //java虚拟机启动的进程。 try { p=Runtime.getRuntime().exec("notepad.exe Property.java"); //启动记事本并且打开源文件。 Thread.sleep(5000); //持续5秒 p.destroy(); //关闭该进程 } catch(Exception ex) { ex.printStackTrace(); } } } 1.1.1.3Java语言中两种异常的差别 Java提供了两类主要的异常:runtime exception和checked exception。所有的checked exception是从java.lang.Exception类衍生出来的,而runtime exception则是从java.lang.RuntimeException或java.lang.Error类衍生出来的。    它们的不同之处表现在两方面:机制上和逻辑上。    一、机制上    它们在机制上的不同表现在两点:1.如何定义方法;2. 如何处理抛出的异常。请看下面CheckedException的定义:    public class CheckedException extends Exception    {    public CheckedException() {}    public CheckedException( String message )    {    super( message );    }    }    以及一个使用exception的例子:    public class ExceptionalClass    {    public void method1()    throws CheckedException    {     // ... throw new CheckedException( “...出错了“ );    }    public void method2( String arg )    {     if( arg == null )     {      throw new NullPointerException( “method2的参数arg是null!” );     }    }    public void method3() throws CheckedException    {     method1();    }    }    你可能已经注意到了,两个方法method1()和method2()都会抛出exception,可是只有method1()做了声明。另外,method3()本身并不会抛出exception,可是它却声明会抛出CheckedException。在向你解释之前,让我们先来看看这个类的main()方法:    public static void main( String[] args )    {    ExceptionalClass example = new ExceptionalClass();    try    {    example.method1();    example.method3();    }    catch( CheckedException ex ) { } example.method2( null );    }    在main()方法中,如果要调用method1(),你必须把这个调用放在try/catch程序块当中,因为它会抛出Checked exception。    相比之下,当你调用method2()时,则不需要把它放在try/catch程序块当中,因为它会抛出的exception不是checked exception,而是runtime exception。会抛出runtime exception的方法在定义时不必声明它会抛出exception。    现在,让我们再来看看method3()。它调用了method1()却没有把这个调用放在try/catch程序块当中。它是通过声明它会抛出method1()会抛出的exception来避免这样做的。它没有捕获这个exception,而是把它传递下去。实际上main()方法也可以这样做,通过声明它会抛出Checked exception来避免使用try/catch程序块(当然我们反对这种做法)。    小结一下:    * Runtime exceptions:    在定义方法时不需要声明会抛出runtime exception;    在调用这个方法时不需要捕获这个runtime exception;    runtime exception是从java.lang.RuntimeException或java.lang.Error类衍生出来的。    * Checked exceptions:    定义方法时必须声明所有可能会抛出的checked exception;    在调用这个方法时,必须捕获它的checked exception,不然就得把它的exception传递下去;    checked exception是从java.lang.Exception类衍生出来的。    二、逻辑上    从逻辑的角度来说,checked exceptions和runtime exception是有不同的使用目的的。checked exception用来指示一种调用方能够直接处理的异常情况。而runtime exception则用来指示一种调用方本身无法处理或恢复的程序错误。    checked exception迫使你捕获它并处理这种异常情况。以java.net.URL类的构建器(constructor)为例,它的每一个构建器都会抛出MalformedURLException。MalformedURLException就是一种checked exception。设想一下,你有一个简单的程序,用来提示用户输入一个URL,然后通过这个URL去下载一个网页。如果用户输入的URL有错误,构建器就会抛出一个exception。既然这个exception是checked exception,你的程序就可以捕获它并正确处理:比如说提示用户重新输入。 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值