使用Java实现面向对象编程——第八章 File IO

1、文件:文件可认为是相关记录或放在一起的数据的集合;

2、File类:名命空间:java.io

File对象即可表示文件,也可表示目录,

在程序中,一个File对象可以代表一个文件或目录,

利用他可以 用来对文件或目录进行基本操作;

  创建一个File文件的语法:

  File file = new File( String pathname );

    //pathname表示文件路径:

          格式:"c:\\test .txt"

    或

      "c:/test .txt"

扫描二维码关注公众号,回复: 2694767 查看本文章

File常用的方法:

方法名称

说      明

boolean exists( )

判断文件或目录是否存在

boolean isFile( )

判断是否是文件

boolean isDirectory( )

判断是否是目录

String getPath( )

返回此对象表示的文件的相对路径名

String getAbsolutePath( )

返回此对象表示的文件的绝对路径名

String getName( )

返回此对象表示的文件或目录的名称

boolean delete( )

删除此对象指定的文件或目录

boolean createNewFile( )

创建名称的空文件,不创建文件夹

long  length()

返回文件的长度,单位为字节, 如果文件不存在,则返回 0L

2、流:流是指一连串流动的字符,是以先进先出的方式发送和接收数据的通道;

    读文件:把文件中的数据读到内存中

    ★写文件:把内存中的数据写到文件中;

 ●Java流的分类:

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

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

字节流是 8 位通用字节流,1字节;

字符流是 16 位 Unicode 字符流:2字节

输入输出流是相对于计算机内存来说的:

 

字节流建议用与二进制数据(如图片),字符流用于文本;

下面的四个基类都是抽象类:

 

3、文件的读写:

    文本文件的读写:用FileInputStream和FileOutputStream读写文本文件、用BufferedReader和BufferedWriter读写文本文件

         二进制文件的读写:使用DataInputStreamDataOutputStream读写二进制文件

         使用字节流读取文件:

字节输入流InputStream类:将文件中的数据输入到内部存储器(内存)中;

  InputStream类常用方法

    int read( ):读取一个字节数据;

    ★无参的read():从输入流读取一个八位的字节,把它转换为0~255的整数返回;

    ★带参的read():从输入流批量读取若干个字节, 从文件或键盘读取数据时,采用下面两种方法可以减少进行物理读取文件或键盘的次数,提高输入输出效率

    int read(byte[] b) :将数据读取到字节数组中;

        int read(byte[] b,int off,int len):从输入流中读取最多len长度的字节,保存到字节数组b中,保存的位置从off开始;

    void close( ):关闭输入流;

    ◆int available():返回输入流读取的估计字节数;

字节输入流FileInputStream 类:

     子类FileInputStream常用的构造方法

    ◆FileInputStream(File file):file指定文件数据源。

               File file=new File(“C:\\test.txt”);

               InputStream fileObject=new FileInputStream(file)

    ◆FileInputStream(String name):name指定文件数据源,包含路径信息。

         InputStream in=new FileInputStream(“C:\\text.txt”)

使用FileInputStream 读文本文件步骤:

      引入相关的类

  ★构造文件输入流FileInputStream 对象

  ★读取文本文件的数据

  ★关闭文件流对象

 

Eg

 

输出结果:

 

Eg:省略了代码

 

输出结果:

 

    ●使用字节流写入文本文件:

字节输出流OutputStream类:把内存中的数据输出到文件中

   OutputStream类常用方法

    void write(int c):写入一个字节数据;

    void write(byte[] buf):写入数组buf’的所有字节;

    void write(byte[] b,int off,int len):将字节数组中从off位置开始,长度为len的字节数据输出到输出流中;

    void close( ):关闭输出流;

字节输出流FileOutputStream类:实现向文本文件写入数据;

  子类FileOutputStream常用的构造方法

    FileOutputStream (File file):file指定文件目标数据源;

      File file=new File(“C:\\test.txt”)

      FileOutputtream fos=new FileOutputStream(file)

    FileOutputStream(String name) :name指定文件目标数据源,包含路径信息;

      FileOutputStream fos=new FileOutputStream(“C:\\test.txt”);

    FileOutputStream(String name,boolean append)://name指定文件目标数据源,包含路径信息;//append表示是否在文件末尾添加数据;设置为true,则在文件末尾添加数据。

      FileOutputStream fos=new FileOutputStream(“C:\\test.txt”,true);

    注意:第一种和第二种构造方法在向文件写数据时将覆盖文件中原有的内容; 如果相应的文件不存在,就会自动创建一个空的文件; 若参数file和name表示的文件路径尽管存在,但是表示一个文件目录;则会抛出FileNotFoundException异常;

使用FileOutputStream 写文本文件:

      引入相关的类

  ★输出流FileOutputStream对象

  ★把数据写入文件

  ★关闭文件流对象

 

Eg:

 

 

Eg:

 

      ●使用字节输入输出流进行复制操作:

Eg:关键代码

 

 

      ●使用字符流读取文件: Read()是一个抽象类;

字符输入流Reader类:

  字符输入流Reader类常用方法

    int read( ):从输入流中读取单个字符;

    int read(byte[] c):从输入流中读取c.length长度的字符,保存到字符数组c中,保存的位置从off位置开始,返回实际读取的字符数;

    read(char[] c,int off,int len):从输入流中读取c.length长度的字符,保存到字符数组c中,保存的位置从off位置开始,返回实际读取的字符长度;

    void close( ):关闭流;

字符输入流FileReader()类:

   子类FileReader()常用的构造方法:

        FileReader(String filrname) :filename指从中读取数的文件的名称;

使用FileIReader读文本文件步骤:

    引入相关的类

  ★创建一个FileReader对象

  ★利用FileReader类的方法读取文本文件的数据;

  ★关闭相应的流对象;

 

BufferedReader类:;BufferedReader类是Reader类的子类,BufferedReader类带有缓冲区,能够提高读取操作的效率;

  子类BufferedReader常用的构造方法

    BufferedReader(Reader in)

  子类BufferedReader特有的方法:用来按行读取内容;

    readLine()

使用 BufferedReader 读文本文件步骤:

  • 引入相关的类
  • 构造BufferedReader 对象和FileReader 对象
  • 调用readLine ()方法读取数据
  • 关闭文件流对象
 

Writer类常用方法

  write(String str):将str字符串里包含的字符输出到指定的输出流中;

  write(String str,int off,int len):将Str字符串里从off位置开始长度为len的字符输出到输出流中;

  void close():关闭输出流;

  void flush():刷新输出流;

使用使用FileWriter写文件步骤:

  1、引入相关的类

  2、创建FileWriter对象

  3、写文本文件

  4、关闭相关的流对象

    

子类BufferedWriter常用的构造方法

  ◆BufferedReader(Writer out)

使用 BufferedWriter 写文件步骤:

  • 引入相关的类
  • 构造BufferedWriter对象和FileWriter对象
  • 调用write()方法写数据据
  • 流对象的清空和关闭flush()close()
 

4、二进制文件的读写:

DataInputStream

   FileInputStream的子类

   与FileInputStream类结合使用读取二进制文件

DataOutputStream

  FileOutputStream的子类

  与FileOutputStream类结合使用写二进制文件

使用 DataInputStream 读二进制文件:

  • 引入相关的类
  • 构造数据输入流对象
  • 调用read ()方法读取二进制数据
  • 关闭数据输入流

 

 

使用 DataOutputStream写二进制文件:

  • 引入相关的类
  • 构造数据输出流对象
  • 调用write()方法写二进制文件的数据
  • 关闭数据输出流

 

 

5、 附加1:对象流:序列化、反序列化、序列化克隆;

       序列化(Serialization)和反序列化的过程:

                                   

序列化:将对象的状态写入到特定的流中的过程;

反序列化:从特定的流中获取数据重新构建对象的过程;

1、序列化与反序列化:

   序列化:指把堆内存中的 Java 对象数据,通过某种方式把对象存储到磁盘文件中或者传递给其他网络节点(在网络上传输)。这个过程称为序列化。通俗来说就是将数据结构或对象转换成二进制串的过程

  反序列化:把磁盘文件中的对象数据或者把网络节点上的对象数据,恢复成Java对象模型的过程。

也就是将在序列化过程中所生成的二进制串转换成数据结构或者对象的过程

2、为什么要做序列化:

  ①、在分布式系统中,此时需要把对象在网络上传输,就得把对象数据转换为二进制形式,需要共享的数据的 JavaBean 对象,都得做序列化。

  ②、服务器钝化:如果服务器发现某些对象好久没活动了,那么服务器就会把这些内存中的对象持久化在本地磁盘文件中(Java对象转换为二进制文件);如果服务器发现某些对象需要活动时,先去内存中寻找,找不到再去磁盘文件中反序列化我们的对象数据,恢复成 Java 对象。这样能节省服务器内存。

3、Java 怎么进行序列化:

  ①、需要做序列化的对象的类,必须实现序列化接口:Java.lang.Serializable 接口(这是一个标志接口,没有任何抽象方法),

Java 中大多数类都实现了该接口,比如:String,Integer

  ②、底层会判断,如果当前对象是 Serializable 的实例,才允许做序列化,Java对象 instanceof Serializable 来判断。

  ③、在 Java 中使用对象流来完成序列化和反序列化

    ObjectOutputStream:通过 writeObject()方法做序列化操作

    ObjectInputStream:通过 readObject() 方法做反序列化操作

4、什么时候需要序列化:

  ◆当你想把内存中的对象状态保存到一个文件中或者数据库中;

  ◆当你想用套接字在网络上传送对象的时候

  ◆当你想通过RMI传输文件的时候;

5、打开生成的文件乱码的原因:

   序列化和反序列化都是基于二进制流的,在将Students的相关信息转化为二进制存储在了Student.bin文件中,用编辑器打开自然是乱码的, 只有通过反序列化才能将存储的二进制文件读取出来,然后显示在控制台上;

   ◆注意:不是因为写出的编码和文本编辑器的默认编码采用了不一样的字符集;

序列化的步骤:(使用集合保存对象,可以将集合中的所有对象序列化)

   实现Serializable接口;

   创建对象输出流;

   调用writeObject()方法将对象写入文件;

   关闭对象输出流;

注意:错误一:如果新建的 Person 对象没有实现 Serializable 接口,那么上面的操作会报错:

 

问题1:如果某些数据不需要做序列化,比如密码,

解决办法:在字段面前加上 transient

Eg:private String name;//需要序列化

transient private int age;//不需要序列化

 那么我们在反序列化的时候,打印出来的就是Person [name=vae, age=0],整型数据默认值为 0

问题2:序列化版本问题,在完成序列化操作后,由于项目的升级或修改,可能我们会对序列化对象进行修改,

比如增加某个字段,那么我们在进行反序列化就会报错:

 

解决办法:在 JavaBean 对象中增加一个 serialVersionUID 字段,用来固定这个版本,无论我们怎么修改,版本都是一致的,就能进行反序列化了;

Eg:private static final long serialVersionUID = 8656128222714547171L;

代码:

/**

 * 反序列化学生对象

 * @author 逆風〠飛翔

 *

 */

public class SerizaableTest1 {

 

   public static void main(String[] args) {

     //创建对象输入流

     FileInputStream fs=null;

     ObjectInputStream ois=null;

     try {

        fs=new FileInputStream("E:\\Student.bin");

        ois=new ObjectInputStream(fs);

        Students stu=(Students)ois.readObject();

        System.out.println("学生姓名:"+stu.getName()+",学生年龄:"+stu.getAge()+

             ",学生性别:"+stu.getGender()+"\n读取信息成功!");       

     } catch (FileNotFoundException e) {

        e.printStackTrace();

     } catch (IOException e) {

        e.printStackTrace();

     } catch (ClassNotFoundException e) {

        e.printStackTrace();

     }finally {

        if(fs!=null) {

           try {

             fs.close();

           } catch (IOException e) {

             e.printStackTrace();

           }

        }

        if(ois!=null) {

           try {

             ois.close();

           } catch (IOException e) {

             e.printStackTrace();

           }

        }

     }

   }

}

Eg

/**

 * 序列化学生对象

 * @author 逆風〠飛翔

 *

 */

public class SerizableTest {

   public static void main(String[] args) {

     //创建一个需要序列化的学生对象

     Students stu=new Students("小明", "男", 20);

     //创建一个对象输出流

     OutputStream os=null;

     ObjectOutputStream oos=null;

     try {

        os=new FileOutputStream("E:\\Student.bin");

        oos=new ObjectOutputStream(os);

        oos.writeObject(stu);

        System.out.println("创建Students.bin文件成功!");

     } catch (FileNotFoundException e) {

        e.printStackTrace();

     } catch (IOException e) {

        e.printStackTrace();

     }finally {

        if(os!=null) {

           try {

             os.close();

           } catch (IOException e) {

             e.printStackTrace();

           }

        }

        if(oos!=null) {

           try {

             oos.close();

           } catch (IOException e) {

             e.printStackTrace();

           }

        }

     }

    

   }

}

Eg:学生类

public class Students implements Serializable{

   private String name;

  private int age;

//不想被序列化的关键字:transient

   transient private String gender;

   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 String getGender() {

     return gender;

   }

 

   public void setGender(String gender) {

     this.gender = gender;

   }

 

   /**

    * 带参构造函数

    * @param name 姓名

    * @param gender 性别

    * @param age 年龄

    */

   public Students(String name,String gender,int age) {

     this.name=name;

     this.age=age;

     this.gender=gender;

   }

}

Eg:用集合进行序列化

import java.io.FileNotFoundException;

import java.io.FileOutputStream;

import java.io.IOException;

import java.io.ObjectOutputStream;

import java.io.OutputStream;

import java.util.ArrayList;

import java.util.List;

/**

 * 序列化学生对象

 * @author 逆風飛翔

 *

 */

public class SerializableTest {

    public static void main(String[] args) {

          //创建五个需要序列化的学生对象

          Students stu=new Students("小明", "男", 20);

          Students stu1=new Students("小明1", "男", 21);

          Students stu2=new Students("小明2", "男", 22);

          Students stu3=new Students("小明3", "男", 23);

          Students stu4=new Students("小明4", "男", 24);

          //标记元素类型

          List<Students> student = new ArrayList<Students>();

          student.add(stu);

          student.add(stu1);

          student.add(stu2);

          student.add(stu3);

          student.add(stu4);

          //创建一个对象输出流

          OutputStream os=null;

          ObjectOutputStream oos=null;

          try {

                os=new FileOutputStream("E:\\Student.bin");

                oos=new ObjectOutputStream(os);

                oos.writeObject(student);

                System.out.println("创建Students.bin文件成功!");

          } catch (FileNotFoundException e) {

                e.printStackTrace();

          } catch (IOException e) {

                e.printStackTrace();

          }finally {

                if(os!=null) {

                     try {

                           os.close();

                     } catch (IOException e) {

                           e.printStackTrace();

                     }

                }

                if(oos!=null) {    

                     try {

                           oos.close();

                     } catch (IOException e) {

                           e.printStackTrace();

                     }

                }

          }

    }

}

Eg:用集合进行反序列化

import java.io.FileInputStream;

import java.io.FileNotFoundException;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.util.List;

 

/**

 * 反序列化学生对象

 * @author 逆風飛翔

 *

 */

public class SerializableTest1 {

 

    public static void main(String[] args) {

          //创建对象输入

          FileInputStream fs=null;

          ObjectInputStream ois=null;

          try {

                fs=new FileInputStream("E:\\Student.bin");

                ois=new ObjectInputStream(fs);

               

                List<Students> listStu = (List<Students>) ois.readObject();

                System.out.println("学生的人数:"+listStu.size());

                for (Students stu : listStu) {

                     System.out.println("学生姓名:"+stu.getName()+",

学生性别:"+stu.getGender()+",学生年龄:"+stu.getAge());    

                }

                System.out.println("读取信息成功!");

                          

          } catch (FileNotFoundException e) {

                e.printStackTrace();

          } catch (IOException e) {

                e.printStackTrace();

          } catch (ClassNotFoundException e) {

                e.printStackTrace();

          }finally {

                if(fs!=null) {

                     try {

                           fs.close();

                     } catch (IOException e) {

                           e.printStackTrace();

                     }

                }

                if(ois!=null) {

                     try {

                           ois.close();

                     } catch (IOException e) {

                           e.printStackTrace();

                     }

                }

          }

    }

}

       为克隆实现序列化:java序列化可以将一个对象的状态写入到byte流里,并且能够把byte流里的数据读取出来,重新构造一个相同的对象

               

public class SerialCloneTest {

    public static void main(String[] args) {

        Employee employee=new Employee("小明", 20000, 2018,7,3);

        Employee employeeOne=(Employee) employee.clone();

        employee.raiseSalary(10);

        System.out.println(employee);

        System.out.println(employeeOne);

    }

}

public class Employee extends SerialCloneable{

   private String name;

   private double salary;

   private Date hireDay;

   public Employee(String name, double salary, int year,int month,int day) {

     super();

     this.name = name;

     this.salary = salary;

//提供了世界上大多数国家使用的标准日历系统。

     GregorianCalendar calendar=new GregorianCalendar(year, month-1, day);    this.hireDay=calendar.getTime();

   }

   public String getName() {

     return name;

   }

   public void setName(String name) {

     this.name = name;

   }

   public double getSalary() {

     return salary;

   }

   public void setSalary(double salary) {

     this.salary = salary;

   }

   public Date getHireDay() {

     return hireDay;

   }

   public void setHireDay(Date hireDay) {

     this.hireDay = hireDay;

   }

   @Override

   public String toString() {

     StringBuilder sb = new StringBuilder();

        sb.append("姓名:" + name + "\n");

        sb.append("薪资:" + salary + " \n");

        sb.append("出生日期:" + hireDay );

        return sb.toString();

   }

   public void raiseSalary(double byPercent){

     double raise=salary*byPercent/100;

     salary+=raise;

   }

}

import java.io.ByteArrayInputStream;

import java.io.ByteArrayOutputStream;

import java.io.IOException;

import java.io.ObjectInputStream;

import java.io.ObjectOutputStream;

import java.io.Serializable;

 

public class SerialCloneable implements Cloneable, Serializable {

   public Object clone() {

     try {

        ByteArrayOutputStream bout = new ByteArrayOutputStream();

        ObjectOutputStream out = new ObjectOutputStream(bout);

        out.writeObject(this);

        out.close();

        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());

        ObjectInputStream in = new ObjectInputStream(bin);

        Object ret = in.readObject();

        in.close();

        return ret;

     } catch (IOException e) {

        return null;

     } catch (ClassNotFoundException e) {

        e.printStackTrace();

     }

     return null;

   }


6、 
附加2:

bin(二进制文件)

二进制文件,其用途依系统或应用而定。一种文件格式binary的缩写。一个后缀名为".bin"的文件,只是表明它是binary格式。比如虚拟光驱文件常用".bin"作为后缀,但并不意味着所有的bin文件都是虚拟光驱文件。一般来讲是机器代码,汇编语言编译后的结果(磁盘操作系统下汇编语言编译后与".com"文件相类似),用debugWINHEX,U_EDIT等软件可以打开(通常不一定能看得懂是些什么除非学习过汇编语言)。这类 所有的文件,无论后缀名是什么,一律分为两种格式".text" 和".binary"。

二进制文件

概述

二进制文件,其用途依系统或应用而定。

也就是说,一般来讲是机器代码,汇编语言编译后的结果,(磁盘操作系统下汇编语言编译后与".com"文件相类似),用debug、WINHEX,U_EDIT等软件可以打开(通常不一定能看得懂是些什么除非学习过汇编语言)。这类 所有的文件,无论后缀名是什么,一律分为两种格式".text" 和".binary"。

一种文件格式binary的缩写。一个后缀名为".bin"的文件, 只是想表明它是binary格式,但并不表明它与某种应用程序有必然的联系性。

实例

比如虚拟光驱文件常用".bin"作为后缀,但并不意味着所有".bin"文件都是虚拟光驱文件。

如果你的daemon无法正常安装它,说明它很可能不是虚拟光盘

另外在软件的安装后文件夹中大部分软件或服务器软件都有个 bin 文件夹。

因为 BINBINary)其中文是:二进制

里面存放的一般是可执行的二进制文件,所以我们通常使用较大型的软件时都会发现有这个名称的文件夹。

BIN文件还有一种最可能的是步步高之类学习机的学习文件或者是点读文件,这类文件只有在制定的硬件或者条件下运行,其他一切方法一概都不能打开或者运行该文件。

此外,在linux平台下,其支持多分区结构,bin就是其中之一。

bin 在linux系统分区中表示存放标准系统实用程序;

sbin 存放标准系统管理文件;

tmp 存放临时文件。

还有一种较为简单的方式:直接将bin文件强制改名为iso文件即可直接用虚拟光驱打开。

7、 总结:

 

8、附加3:

Eg

 

分析:根据给定的 File 对象构造一个 FileWriter 对象。

用FileWriter字符流的时候一定要调用flush()方法写进文件

或者是调用close();关闭流,才能写进文件。

Eg

 

分析:FileOutputStream的write方法有三类参数的重载,其中一个为int类型参数。

就这个程序片段,语法和调用参数等都没有问题,

其中fos.write(‘a’),会自动进行转换’a’为int类型(ascii编码),

当运行结束,并用记事本打开文件时,其中应该为字符a,答案为C

Eg

 

分析:该题考查InputStream及其子类的使用。

InputStream为抽象类,不能实例化,只能实例化其子类对象。

Eg

分析:首先,File类可以表示路径,所以A答案错误,

其次,程序中的getParentFile方法是返回上一级目录(project目录)的Flie对象,所以list方法返回的应该是上一级,也就是project目录下的对象数组,答案B错误,

最后,list方法返回值是包括了目录和对象的,所以答案D错误。

Eg

 

分析:使用FileInputStream类中读取数据read方法,

read(byte[] arg),具体是讲文本文件中的字符数据读取到byte数组中,

与FileOutputStream的write方法不同的是,读取字符得到的ascii码值。但是本题中的输出System.out.pring(bt),却是将数组名称对应的内存位置索引值进行输出,

Eg

 

分析:使用FileInputStream类中读取数据read方法,

read(byte[] arg),具体是讲文本文件中的字符数据读取到byte数组中,

与FileOutputStream的write方法不同的是,读取字符得到的ascii码值。

同时,skip(int off)方法是在读取数据之前,会将读取位置偏移off,本题偏移一个字符。

Eg

 

分析:首先read(数组)这种读法,是先读到数组,等数组满了,一次性读到指定文件里,比如把东西先一个个搬到框里,然后再把框搬走。

read在这里是一次读若干个字符,也就是说至少读2个以上的字母,比如拿到了china,但是这个数组只有一个空间,所以读了好几个字母却只能放下一个,其他字母hina就丢失了,不会又从h读取,只能进入一次循环;

Eg

 

分析:DataOutputStream类的写入数据的方法基本以write开头,

针对字符串写入,有三个方法

字符数组writeBytes(String arg),

多字符 writeChars(String arg),

以及unicode字符 writeUTF(String arg)。

本题答案中只出现了writeUTF方法,答案为D

Eg

 

分析:UTF是双字节编码,而writeChars方法写入的是按照字符格式写入的,

在文件中的占位要小于以Unicode编码的同样字符串,

所以,使用readUTF方法读取时,会出现EOF错误;

 

readutf():读取括号内参数的同时,将该参数转化为utf-8编码,

前提是这个也必须是writeutf()方法写下的,编码格式一致才能读取

eof异常:当输入过程中意外到达文件或流的末尾时,抛出此异常

  • ●使用流,是为了简化数据的读写操作,让程序员能专注在有效合理的数据处理上,而不是底层的数据写入对应的物理地址,磁盘驱动器的状态等等方面。
  • ●输入流负责读取数据;

输出流负责写数据;

●OutputStream是基类

●read()方法返回的是int类型

●BufferedReader只能用Reader的实现类对象进行实例化

●FileWriter为写入文件的字符输出流,其父类为java.io.OutputStreamWriter,

而java.io.OutputStreamWriter继承自java.io.Writer,这几个字符输出流都具有几个重载的write()方法完成写的操作,

其中write(String)方法可以直接写一个字符串,

  • ●FileReader是继承自InputStreamReader,在Reader的方法read( )中,其返回值为-1的,表示当前读取到文件流的末尾,此时读取完毕当前流(如果是文件操作,表示读取到了文件末尾)。
  • ●Java中的输入输出流,汉字的处理,字符流更加高效,

创建输入输出流对象时要注意路径的写法,

 

 

猜你喜欢

转载自www.cnblogs.com/HQING/p/9459378.html
今日推荐