Java高级编程——IO流

1.File类的使用

1.1 对File类的理解

  1. File类的一个对象,代表一个文件或一个文件目录(俗称:文件夹)
  2. File类声明在java.io包下
  3. File类中涉及到关于文件或文件目录的创建、删除、重命名、查看修改时间和文件大小等方法, 并未涉及到写入或读取文件内容的操作。如果需要读取或写入文件内容,必须使用IO流来完成。
  4. 后续File类的对象常会作为参数传递到流的构造器中,指明读取或写入的"终点"。

1.2 File的实例化

File类提供了三个构造器,分别为:

  1. File(String filePath)
  2. File(String parentPath,String childPath)
  3. File(File parentFile,String childPath)

相对路径:相较于某个路径下,指明的路径。
绝对路径:包含盘符在内的文件或文件目录的路径

 public void test1(){
        //构造器1
        File file1 = new File("hello.txt");     //相对于当前module
        File file2 = new File("D:\\IDEA_Java_Code\\JavaSenior1\\day08");

        System.out.println(file1);
        System.out.println(file2);

        //构造器2
        File file3 = new File("D:\\IDEA_Java_Code","JavaSenior1");
        System.out.println(file3);

        //构造器3
        File file4 = new File(file3, "hi.txt");
        System.out.println(file4);
}

单元测试结果如下:

注:这个时候的File类的对象file1和file2只是在内存层面,硬盘中并不会创建出hello.txt
有关"路径分隔符"
路径分隔符和系统有关 :

  • windows和DOS系统默认使用“\”来表示
  • UNIX和URL使用“/”来表示

Java程序支持跨平台运行,因此路径分隔符要慎用。
为了解决这个隐患,File类提供了一个常量:

public static final String separator          //根据操作系统,动态的提供分隔符。

//举例
File file1 = new File("d:\\atguigu\\info.txt");
File file2 = new File("d:" + File.separator + "atguigu" + File.separator + "info.txt");  //使用File类提供的路径分隔符
File file3 = new File("d:/atguigu");

1.3 File类的常用方法

方法名用途
public String getAbsolutePath()获取绝对路径
public String getPath()获取路径
public String getName()获取名称
public String getParent()获取上层文件目录路径。若无,返回null
public long length()获取文件长度(即:字节数)。不能获取目录的长度。
public long lastModified()获取最后一次的修改时间,毫秒值
public void test2(){
        File file1 = new File("hello.txt");      //相对于当前module(当前module是day08)
        File file2 = new File("d:\\io\\hi.txt");

        System.out.println(file1.getAbsoluteFile());
        System.out.println(file1.getPath());
        System.out.println(file1.getName());
        System.out.println(file1.getParent());       //null
        System.out.println(file1.length());           
        System.out.println(new Date(file1.lastModified()));

        System.out.println("-------------------------");

        System.out.println(file2.getAbsoluteFile());
        System.out.println(file2.getPath());         //写的是什么路径,就会返回什么路径。(因为在创建file2对象的时候传入的是绝对路径)
        System.out.println(file2.getName());
        System.out.println(file2.getParent());
        System.out.println(file2.length());
        System.out.println(new Date(file2.lastModified()));

    }

测试结果如下:
image.png
可见:file1 和 file2 的文件长度和修改时间的结果都是0, 这是因为此时的两个对象还是内存层面的,即: 在硬盘中还不存在这两个文件,所以文件大小和修改时间都为0。
image.png
接下来在当前的module(day08)下创建出hello.txt文件;在D盘下创建io文件夹和文件hi.txt ; 并利用Date类把修改时间由毫秒数转换为标准时间,如下:

public void test2(){
        File file1 = new File("hello.txt");      //相对于当前module(当前module是day08)
        File file2 = new File("d:\\io\\hi.txt");

        System.out.println(file1.getAbsoluteFile());
        System.out.println(file1.getPath());
        System.out.println(file1.getName());
        System.out.println(file1.getParent());       //null
        System.out.println(file1.length());           
        System.out.println(new Date(file1.lastModified()));

        System.out.println("-------------------------");

        System.out.println(file2.getAbsoluteFile());
        System.out.println(file2.getPath());         //写的是什么路径,就会返回什么路径。(因为在创建file2对象的时候传入的是绝对路径)
        System.out.println(file2.getName());
        System.out.println(file2.getParent());
        System.out.println(file2.length());
        System.out.println(new Date(file2.lastModified()));

    }

测试结果如下:
image.png
打开两个文件的属性查看文件大小:
image.png
image.png
image.png** 如下的两个方法适用于文件目录: **

public String[] list()获取指定目录下的所有文件或者文件目录的名称数组
public File[] listFiles()获取指定目录下的所有文件或者文件目录的File数组
@Test
public void test3(){
    File file1 = new File("D:\\CodeWarehouse\\java_code\\IDEA_Java_Code\\JavaSenior1"); //这个目录要存在,因为要在它的下一级找文件或文件目录

    String[] list = file1.list();

    for (String s : list){
        System.out.println(s);
    }

    System.out.println();

    File[] files = file1.listFiles();
    for(File f: files){
        System.out.println(f);
    }
}

image.png
image.png
文件重命名

public boolean renameTo(File dest)把文件重命名为指定的文件路径

比如:以 file1.renameTo(file2)为例: 要想保证返回 true ,需要 file1 在硬盘中是存在的,且file2不能在硬盘中存在。

@Test
public void test4(){
 File file1 = new File("hello.txt");
 File file2 = new File("D:\\io\\hi.txt");

 boolean renameTo = file1.renameTo(file2);   //这就把当前目录下(即D:\IDEA_Java_Code\JavaSenior1目录下)的hello.txt文件放在了D:\io之下并且重命名为了hi.txt
 System.out.println(renameTo);
}

由于前面已经在D盘的io文件夹下创建了hi.txt文件,所以为了保证这里运行成功,需要先把io下的hi.txt文件删掉; 再完成测试,如下:
image.png
结果就是:原来位置处的hello.txt文件没有了,io文件目录下多了一个hi.txt文件,内容为原来hello.txt中的内容。(相当于:移动 + 重命名)

1.4 File类的常用方法2

1.4.1 File类的判断功能:

public boolean isDirectory()判断是否是文件目录
public boolean isFile()判断是否是文件
public boolean exists()判断是否存在(是否在硬盘中存在)
public boolean canRead()判断是否可读
public boolean canWrite()判断是否可写
public boolean isHidden()判断是否隐藏
@Test
public void test5(){
     File file1 = new File("hello.txt");

     System.out.println(file1.isDirectory());     //false (hello.txt不是文件目录(文件夹))
     System.out.println(file1.isFile());          //true  (hello.txt是一个文件)
     System.out.println(file1.exists());          //true  (hello.txt真实存在在硬盘里,之前创建过了)
     System.out.println(file1.canRead());         //true
     System.out.println(file1.canWrite());        //true
     System.out.println(file1.isHidden());        //false  (hello.txt没有被隐藏)

     System.out.println("----------------------------");

     File file2 = new File("d:\\io");
     System.out.println(file2.isDirectory());     //true
     System.out.println(file2.isFile());          //fase
     System.out.println(file2.exists());          //true
     System.out.println(file2.canRead());         //true
     System.out.println(file2.canWrite());        //true
     System.out.println(file2.isHidden());        //false
 }

image.png

1.4.2 File类的创建功能

public boolean createNewFile()创建文件。若文件存在,则不创建,返回false
public boolean mkdir()创建文件目录。如果此文件目录存在,就不创建了。 如果此文件目录的上层目录不存在,也不创建。
public boolean mkdirs()创建文件目录。如果上层文件目录不存在,一并创建。

注:如果你创建文件或者文件目录没有写盘符路径,那么,默认在项目路径下。

@Test
public void test6() throws IOException {
    File file1 = new File("hi.txt");

    if (!file1.exists()){
        file1.createNewFile();         //创建文件
        System.out.println("创建成功");
    }else{  //文件存在
        System.out.println("创建失败");

    }
}

image.png

public void test7(){
   //文件目录的创建
    File file1 = new File("d:\\io\\io1");   //此时io文件目录是存在的,所以使用mkdir方法是可以创建出io1的

    boolean mkdir = file1.mkdir();     
    if(mkdir){
        System.out.println("创建成功");
    }
}

image.png
io文件目录存在的时候,使用mkdir()和mkdirs() 的作用是一样的。
下面我们把创建的 io1文件目录 删掉,保留为D:\io 的样子,然后分别使用mkdir()创建文件目录 D:\io\io1\io3 ; 使用mkdirs()创建文件目录 D:io\io1\io4 。(要创建的文件目录io3和io4的上层目录io1是不存在的,如下:)
image.png

@Test
public void test8(){
    //文件目录的创建
    File file1 = new File("d:\\io\\io1\\io3");

    boolean mkdir = file1.mkdir();
    if(mkdir){
        System.out.println("创建成功1");
    }

    File file2 = new File("d:\\io\\io1\\io4");

    boolean mkdir1 = file2.mkdirs();
    if (mkdir1){
        System.out.println("创建成功2");
    }
}

image.png
可见,使用mkdirs()创建文件目录io4创建成功了,但使用mkdir()创建io3没有创建出来:
image.png

1.4.2 File类的删除功能

public boolean delete()删除文件或者文件夹

删除注意事项: Java中的删除不走回收站。
要删除一个文件目录,请注意该文件目录内不能包含文件或者文件目录。

2. IO流原理及流的分类

2.1 Java IO 原理

I/O是Input/Output的缩写, I/O技术是非常实用的技术,用于 处理设备之间的数据传输。 如读/写文件,网络通讯等。
Java程序中,对于数据的输入/输出操作以“流(stream)” 的 方式进行。
java.io包下提供了各种“流”类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。

  • 输入 input : 读取外部数据(磁 盘、光盘等存储设备的数据)到程序(内存)中。
  • **输出 output **:将程序(内存) 数据输出到磁盘、光盘等存储设备中。

image.png
注:输入和输出是相对的概念,前面给出的输入input、输出output的定义,我们是站位在程序(内存)这一边的。

2.2 流的分类

  • 按操作数据单位不同分为:字节流(8 bit),字符流(16 bit)

    (所谓的字节流就是一个一个的byte, 字符流就是一个一个的char)

  • 按数据流的流向不同分为:输入流,输出流

  • 按流的角色的不同分为:节点流,处理流

下面给出图形化的解释:
image.png
** 节点流和处理流 **
节点流:直接从数据源或目的地读写数据。
image.png
处理流:不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。
image.png

2.3 IO流体系

Java的 IO流 共涉及40多个类,实际上非常规则,都是从如下4个抽象基类派生的。

** (抽象基类) **字节流字符流
输入流** InputStream **** Reader **
输出流** OutputStream **** Writer **

由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。
IO流体系如下:
image.png
image.png

2.4 节点流

2.4.1 FileReader类

使用 FileReader类 实现将文件读入到内存中,并输出到控制台。
要用到的方法:read()
image.png

/*
 将day09下的hello.txt文件内容读入程序中,并输出到控制台。  (就是从文件到内存的过程 ——> 输入)
* */
@Test
public void testFileReader() throws IOException {

    //1.实例化File类的对象,指明要操作的文件
    File file = new File("hello.txt");      //相较于当前Module (当前的Module是day09)
    System.out.println(file.getAbsolutePath());

    //2.提供具体的流(这一步相当于为文件file安装了一个可以传输到内存的管道,接下来只需要完成:开启管道的操作)
    FileReader fr = new FileReader(file);

    //3.数据的读入 (相当于开启管道的开关)
    //read(): 返回读入的一个字符。如果达到文件末尾,返回-1
    int data = fr.read();
    while (data != -1){
        System.out.print((char)data);    //转字符后输出
        data = fr.read();
    }

    //4.流的关闭操作
    fr.close();
}

image.png
为什么要关流?
垃圾回收机制只回收 jvm堆内存里的对象空间。
对其他物理连接,比如:数据库连接、输入输出流、socket连接无能为力。
如何处理异常?
我们这里用的是throws将异常抛出,但是这种处理方式会出现内存泄漏的问题:假设程序在执行步骤2的时候没有抛出异常(即能找到文件对象file),就会创建出fr 这个文件输入流,而在步骤3 fr流在调用 read() 的时候可能会出现阻塞导致数据读取不过来(即在步骤3抛出了异常),就会在这个地方创建出一个IOException对象,进而把这个对象抛出去,从而导致步骤4关流的操作没有执行,出现资源浪费的问题。

我们希望在创建一个流之后,不管后续有没有出现异常,在结束时都要把这个流关闭,防止出现内存泄漏。

所以在上述情况下应该使用 try-catch-finally 把关流的操作放在finally中,修改如下:

/*
 将day09下的hello.txt文件内容读入程序中,并输出到控制台。  (就是从文件到内存的过程 ——> 输入)

 说明点:
 1. read()的理解:返回读入的一个字符。如果达到文件末尾,返回-1
 2.异常的处理:为了保证流资源一定可以执行关闭操作。需要使用try-catch-finally处理。
 3.读入的文件一定要存在,否则就会报FileNotFoundException。

* */
@Test
public void testFileReader(){
    FileReader fr = null;

    try {
        //1.实例化File类的对象,指明要操作的文件
        File file = new File("hello.txt");  //相较于当前Module
        System.out.println(file.getAbsolutePath());

        //2.提供具体的流(这一步相当于为文件file安装了一个可以传输到内存的管道,接下来只需要完成:开启管道的操作)
        fr = new FileReader(file);

        //3.数据的读入 (相当于开启管道的开关)
        //read(): 返回读入的一个字符。如果达到文件末尾,返回-1
        int data = fr.read();
        while (data != -1){
            System.out.print((char)data);    //转字符后输出
            data = fr.read();
        }

    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.流的关闭操作
        try {
            if(fr != null)  //如果没有创建流对象,即异常是出现在"创建流"之前的,那么如果不加判断就调用close方法关流会出现空指针异常。
                fr.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

对read()操作的升级:使用read的重载方法
image.png
image.png
可见,该函数是把文件中的字符读入到一个数组当中。

@Test
public void testFileReader1()  {
    FileReader fr = null;
    try {
        //1.File类的实例化
        File file = new File("hello.txt");

        //2.FileReader流的实例化
        fr = new FileReader(file);

        //3.读入的操作
        //read(char[] buff): 返回每次读入cbuf数组中的字符的个数。如果达到文件末尾,返回-1
        char[] cbuf = new char[5];
        int len;
        while((len = fr.read(cbuf)) != -1){
            //方式一:
            //错误的写法
//                for(int i = 0;i < cbuf.length;i++){    //这个数组的长度一直都是5
//                    System.out.print(cbuf[i]);
//                }
            //正确的写法
               for(int i = 0;i < len;i++){
                   System.out.print(cbuf[i]);
               }
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.资源的关闭
        if (fr != null){
            try {
                fr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
    }

}

image.png
上述代码块中错误的写法会出现的问题:
查看这个 read 的重载方法可知,该方法是把文件中的字符读入到一个数组当中,我们再通过遍历数组把数组中的每个字符输出。而我们这里设定的数组长度为5,也就是每次可以从文件中读取5个字符,那么在遇到文件的字符总数不是5的倍数的时候就会出现数组中后面位置的元素没有被更新依然保留的是上次的旧值(每次read()读取是对旧数组元素的覆盖,而最后一次只会覆盖前几个,后面的几个其实不需要了但一直还保留着),这样就会导致输出错误。
下面改用String对象,不使用字符数组来当缓冲区:

@Test
    public void testFileReader1()  {
        FileReader fr = null;
        try {
            //1.File类的实例化
            File file = new File("hello.txt");

            //2.FileReader流的实例化
            fr = new FileReader(file);

            //3.读入的操作
            //read(char[] buff): 返回每次读入cbuf数组中的字符的个数。如果达到文件末尾,返回-1
            char[] cbuf = new char[5];
            int len;
            while((len = fr.read(cbuf)) != -1){
                
                //方式二:
                //错误的写法,对应着方式一的错误的写法
//                String str = new String(cbuf);
//                System.out.print(str);
                //正确的写法
               String str = new String(cbuf,0,len);    
               System.out.print(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //4.资源的关闭
            if (fr != null){
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }

    }

对String构造函数的说明,api文档如下:
image.png

2.4.2 FileWriter类

从内存中写出数据到硬盘的文件里。
输出操作,对应的File可以不存在
File对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建此文件。
File对应的硬盘中的文件如果存在:
如果流使用的构造器是:FileWriter(file,false) / FileWriter(file): 对原有文件的覆盖。即第二个参数默认是false
如果流使用的构造器是:FileWriter(file,true) : 不会对原有文件覆盖,而是在原有文件的基础上追加内容。

@Test
public void testFileWriter(){
    FileWriter fw = null;  //参数append为true,表明是在原有文件的基础上追加内容

    try {
        //1.提供File类的对象,指明写出到的文件
        File file = new File("hello1.txt");      //内容要写到hello1.txt这个文件中

        //2.提供FileWriter的对象,用于数据的写出(类比FileReader类的对象,这一步就当于为"内存到文件hello1.txt之间"安装了一个传输到数据的管道,接下来只需完成开启管道的操作)
        fw = new FileWriter(file, true);

        //3.写出的操作
        fw.write("I have a dream!\n");
        fw.write("you need to have a dream!");
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.流资源的关闭
        try {
            if(fw != null)
                fw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

image.png
接下来综合使用FileReader类 和 FileWriter类,实现复制文件的操作。
把 hello.txt 的内容复制到 hello2.txt 中:

@Test
public void testFileReadFileWriter() {
    //1.创建File类的对象,指明读入和写出的文件
    FileReader fr = null;
    FileWriter fw = null;
    try {
        File srcFile = new File("hello.txt");
        File destFile = new File("hello2.txt");

        //2.创建输入流和输出流的对象
        fr = new FileReader(srcFile);
        fw = new FileWriter(destFile);


        //3.数据的读入和写出操作
        char[] cbuf = new char[5];
        int len;   //记录每次读入到cbuf数组中的字符的个数
        while ((len = fr.read(cbuf)) != -1){
            //每次写出len个字符
            fw.write(cbuf, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.关闭流资源
        try {
            if(fr != null)
                fr.close();
        } catch (IOException e) {
            e.printStackTrace();
        }

        //try-catch是为处理异常而生的,所以后面的代码是会执行的,所以也可以不把下面关fw流的操作放在上一个try-catch的finally中
        try {
            if(fw != null)
                fw.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

}

测试结果如下:
image.png
可以看到hello2.txt 中的内容是和hello.txt中一样的:
image.png
从文件属性中可以看到两个文件的大小也是一样的,完全是对hello.txt文件的复制。
image.png

2.4.3 FileInputStream类 & FileOutputStream类

  • 对于文本文件(.txt,.java,.c,.cpp),使用字符流处理;

  • 对于非文本文件(.jpg,.mp3,.mp4,.avi,.doc,.ppt,…),使用字节流处理。

    然而文本文件也可以使用字节流来处理,但是当文件中含有中文时,可能会出现乱码。下面给出使用字节流 FileInputStream处理文本文件的例子。

@Test
public void testFileInputStream() {
   FileInputStream fis = null;
   try {
       //1.造文件
       File file = new File("hello.txt");

       //2.造流
       fis = new FileInputStream(file);

       //3.读数据
       byte[] buffer = new byte[5];
       int len;       //记录每次读取的字节的个数
       while ((len = fis.read(buffer)) != -1){
          String str = new String(buffer, 0, len);
           System.out.print(str);
       }
   } catch (IOException e) {
       e.printStackTrace();
   } finally {
       if(fis != null){
           //4.关流
           try {
               fis.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
   }

}

测试结果如下:
image.png
可见,在控制台的输出中,“凯"字乱码了,而"旋"字没有影响。
这是因为在UTF-8编码下,一个汉字占用3个字节,而我们定义的缓冲数组每次读取5个字符,这样的话,前两次会分别从hello.txt文件中读取"hello"和"world”,第三次读取的是"123"和"凯"的前半部分,接着第四次读取的是"凯"的后半部分和"旋"字。所以这就把"凯"字分成了两部分,所以它出现了乱码。读取过程如下图所示:
image.png
使用字节流 FileInputStream处理非文本文件,下面给出 实现图片复制的操作:

/*
    实现对图片的复制操作
 */
@Test
public void testFileInputOutputStream()  {
     FileOutputStream fos = null;
     FileInputStream  fis = null;
     try {
         //1.准备文件
         File srcFile = new File("puxiaopu.jpg");
         File destFile = new File("puxiaopu1.jpg");   //puxiaopu1.jpg不存在,输出后自动创建

         //2.准备流
         fis = new FileInputStream(srcFile);

         fos = new FileOutputStream(destFile);

         //3.复制过程
         byte[] buffer = new byte[5];
         int len;
         while ((len = fis.read(buffer)) != -1){
             fos.write(buffer, 0, len);
         }
         System.out.println("图片复制成功");
     } catch (IOException e) {
         e.printStackTrace();
     } finally {
         if(fos != null){
             //4.关闭流
             try {
                 fos.close();
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
         if(fis != null){
             try {
                 fis.close();
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
     }

}

测试结果如下:
image.png
image.png
打开两图片的属性查看文件大小,如下:
image.png
根据上述实现图片复制的过程,封装一个对指定路径下文件进行复制的函数,并使用一个视频文件测试:

//指定路径下文件的复制
public void copyFile(String srcPath, String destPath){
     FileOutputStream fos = null;
     FileInputStream  fis = null;
     try {
         //1.准备文件
         File srcFile = new File(srcPath);
         File destFile = new File(destPath);

         //2.准备流
         fis = new FileInputStream(srcFile);

         fos = new FileOutputStream(destFile);

         //3.复制过程
         byte[] buffer = new byte[1024];
         int len;
         while ((len = fis.read(buffer)) != -1){
             fos.write(buffer, 0, len);
         }
     } catch (IOException e) {
         e.printStackTrace();
     } finally {
         if(fos != null){
             //4.关闭流
             try {
                 fos.close();
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
         if(fis != null){

             try {
                 fis.close();
             } catch (IOException e) {
                 e.printStackTrace();
             }
         }
     }

 }

@Test
public void testCopyFile(){
    long start = System.currentTimeMillis();

    String srcPath = "C:\\Users\\14205\\Desktop\\王冰冰.mp4";
    String destPath = "C:\\Users\\14205\\Desktop\\王冰冰跟小学生吵架.mp4";

    copyFile(srcPath, destPath);

    long end = System.currentTimeMillis();

    System.out.println("复制操作花费的时间为:" + (end - start));
}

测试结果如下:
image.png
可见,复制该视频的操作用了147毫秒。
下面查看一下两个视频文件的占用空间大小:
image.png

2.5 处理流

2.5.1 缓冲流

四种缓冲流及其作用:

BufferedInputStream处理非文本文件(字节)
BufferedOutputStream处理非文本文件(字节)
BufferedReader处理文本文件(字节)
BufferedWriter处理文本文件(字节)

缓冲流的作用:提高流的读取、写入的速度。
提高读写速度的原因:内部提供了一个“缓冲区”

2.5.1.1 BufferedInputStream类 和 BufferedOutputStream类

下面给出一个使用BufferedInputStream和BufferedOutputStream实现非文本文件(一张图片)的复制操作:

//实现非文本的复制
@Test
public void BufferStreamTest(){
    FileInputStream fis = null;
    FileOutputStream fos = null;
    BufferedInputStream bis = null;
    BufferedOutputStream bos = null;
    try {
        //1.造文件
        File srcFile = new File("bing.jpg");
        File destFile = new File("bing3.jpg");

        //2.造流
        //2.1造节点流
        fis = new FileInputStream(srcFile);
        fos = new FileOutputStream(destFile);
        //2.2 造缓冲流
        bis = new BufferedInputStream(fis);
        bos = new BufferedOutputStream(fos);

        //3.复制的细节: 先读取,再写入
        byte[] buffer = new byte[10];
        int len;
        while ((len = bis.read(buffer)) != -1){
            bos.write(buffer, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.资源关闭
        //要求:先关闭外层的流,再关闭内层的流
        if(bis != null){
            try {
                bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(bos!= null){

            try {
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        //说明:关闭外层流的同时,内层流也会自动的进行关闭。关于内层流的关闭,我们可以省略.
//         fos.close();
//         fis.close();
    }

}

注: 关闭外层流的同时,内层流也会自动的进行关闭。关于内层流的关闭,我们可以省略。
测试结果如下:
image.png
查看一下两图片文件的大小:
image.png
接下来使用BufferedInputStream和BufferedOutputStream实现一个视频的复制,拿这两个缓冲流和字节流对比复制的速度:

//实现文件复制的方法  (复制一个视频,和使用字节流做一个对比)
    public void copyFileWithBuffered(String srcPath, String destPath){
        FileInputStream fis = null;
        FileOutputStream fos = null;
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;
        try {
            //1.造文件
            File srcFile = new File(srcPath);
            File destFile = new File(destPath);

            //2.造流
            //2.1造节点流
            fis = new FileInputStream(srcFile);
            fos = new FileOutputStream(destFile);
            //2.2造缓冲流
            bis = new BufferedInputStream(fis);
            bos = new BufferedOutputStream(fos);

            //3.复制的细节: 先读取,再写入
            byte[] buffer = new byte[1024];
            int len;
            while ((len = bis.read(buffer)) != -1){
                bos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //4.资源关闭
            if(bis != null){
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(bos!= null){

                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

    @Test
    public void testCopyFileWithBuffered(){
        long start = System.currentTimeMillis();

        String srcPath = "C:\\Users\\14205\\Desktop\\王冰冰.mp4";
        String destPath = "C:\\Users\\14205\\Desktop\\王冰冰和小学生.mp4";

        copyFileWithBuffered(srcPath, destPath);

        long end = System.currentTimeMillis();

        System.out.println("复制操作花费的时间为:" + (end - start));
    }

image.png
可见,使用缓冲流只用了47毫秒;如果复制的视频更大的话,效果会更加明显。
接着查看一下两视频文件的属性,可见大小也是完全一样的。
image.png

2.5.1.2 BufferedReader类 和 BufferedWriter类

BufferedReader和BufferedWriter分别作用于FileReader和FileWriter, 所以现在使用BufferedReader类和BufferedWriter类实现文本文件的复制,我们在该module下准备一个dbcp.txt文件。

//使用BufferedReader和BufferedWriter实现文本文件的复制
@Test
public void testBufferReaderBufferWriter() {
    BufferedReader br = null;
    BufferedWriter bw = null;
    try {
        //创建文件和相应的流
        br = new BufferedReader(new FileReader(new File("dbcp.txt")));  //把准备文件、节点流和缓冲流的操作合在了一起
        bw = new BufferedWriter(new FileWriter(new File("dbcp1.txt")));

        //读写操作
        //方式一:使用char[] 数组
        char[] cbuf = new char[1024];
        int len;
        while ((len = br.read(cbuf)) != -1){
            bw.write(cbuf, 0, len);
            bw.flush();  //不写出来也行,是会自动调用的函数

        }
          //方式二:使用String
//            String data;
//            while ((data = br.readLine()) != null){
//                //方法一:
//       //         bw.write(data +"\n");  //data中不包含换行符
//                //方法二:
//                bw.write(data);
//                bw.newLine();    //提供换行的操作
//
//            }

    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //关闭流
        if(br != null){
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(br != null){
            try {
                bw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

image.png
下面查看一下两文件的属性大小:
image.png
关于节点流和缓冲流的总结:
image.png
缓冲流是作用在前面各自的节点流之上的,对应的方法基本是一样的,其中BufferReader多一个按行读取的方法readLine(),BufferedOutputStream和BufferedWriter有一个自动执行的flush()方法。

2.5.1.3 练习题

1.实现图片加密解密操作:
提示:使用异或(^)操作处理输入流读取到的每一个字节。

异或运算规则:对两个操作数进行位的异或运算,相同取0,相反取1。即两操作数相同时,互相抵消。

加密操作

 //图片的加密
    @Test
    public void test1() {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try {
            //1.&2. 准备文件和流
            fis = new FileInputStream(new File("puxiaopu.jpg"));
            fos = new FileOutputStream(new File("puxiaopu_secret.jpg"));

            byte[] buffer = new byte[20];
            int len;
            //3.加密并写出到puxiaopu3文件当中
            while ((len = fis.read(buffer)) != -1){
                //字节数组进行修改
                for (int i = 0; i < len; i++) {
                    buffer[i] = (byte) (buffer[i] ^ 5);
                }
                fos.write(buffer, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //4.关闭流
            if(fis != null){

                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if(fos != null){
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

测试结果如下:
image.png
image.png
可以看到,加密后的图片是无法打开的。
解密操作
根据异或运算的特点,解密操作就是把加密后的图片puxiaopu_secret.jpg当作原文件再执行一次上述的加密过程。

//4.图片的解密
@Test
public void test2(){
    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
        //1.&2. 准备文件和流
        fis = new FileInputStream(new File("puxiaopu_secret.jpg"));
        fos = new FileOutputStream(new File("puxiaopu_desecret.jpg"));

        byte[] buffer = new byte[20];
        int len;
        //3.加密并写出到bing4文件当中
        while ((len = fis.read(buffer)) != -1){
            //字节数组进行修改
            //
            for (int i = 0; i < len; i++) {
                buffer[i] = (byte) (buffer[i] ^ 5);
            }
            fos.write(buffer, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4.关闭流
        if(fis != null){

            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(fos != null){
            try {
                fos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}  

测试结果如下:
image.png
image.png
可见,解密后的图片就可以打开了。
2.获取文本上每个字符出现的次数
提示:遍历文本的每一个字符:字符及出现的次数保存在Map中;将Map中数据写入文件。

2.5.2 转换流

转换流 属于 字符流。它和缓冲流一样,是处理流的一种。

InputStreamReader将一个字节的输入流转换为字符的输入流
OutputStreamWriter将一个字符的输出流转换为字节的输出流

转换流的作用:提供字节流与字符流之间的转换。
解码: 字节、字节数组 —> 字符数组、字符串
编码: 字符数组、字符串 —> 字节、字节数组

2.5.2.1 InputStreamReader类的使用

InputStreamReader的作用是实现字节的输入流到字符的输入流的转换。
为什么要使用 InputStreamReader 来读取文本文件?
在 2.4.3节 我们提到,文本文件也可以使用字节流来处理,但是当文件中含有中文时,可能会出现乱码。所以读取文本文件时(将文本文件的内容打印输出到控制台)只使用FileInputStream这个字节输入流是不够的,所以我们就引入InputStreamReader这一转换流将FileInputStream流对象转换为字符流对象。这样的话,使用转换过的流来读取文本文件就相当于是:直接使用字符流来处理文本文件了。
下面给出 InputStreamReader流 的使用,实现读取当前模块下的文本文件:

/*
* InputStreamReader的使用,实现字节的输入流到字符的输入流的转换。
* */
@Test
public void test1(){
   InputStreamReader isr = null;
   try {
       FileInputStream fis = new FileInputStream("dbcp.txt");
       
//  参数2指明了字符集,具体使用哪种字符集,取决于文件dbcp.txt保存时使用的字符集
//     isr =  new InputStreamReader(fis, "UTF-8");
       isr = new InputStreamReader(fis);    //使用系统默认的字符集(可在IDEA中进行设置)
//为什么使用UTF-8的编码? 因为dbcp.txt保存的时候是以UTF-8保存的。
       
       char[] cbuf = new char[20];     //这里使用的是字符数组,因为isr就是一个字符流,可以处理字符数组
       int len;
       while ((len = isr.read(cbuf)) != -1){
           String str = new String(cbuf, 0, len);
           System.out.println(str);
       }
   } catch (IOException e) {
       e.printStackTrace();
   } finally {
       //关闭流
       if(isr != null){
           try {
               isr.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
   }

}

测试结果如下:
image.png

2.5.2.2 OutputStreamWriter类的使用

OutputStreamWriter流的作用是实现字符输出流到字节输出流的转换。
现在我们综合使用InputStreamReader 和 OutputStreamWriter 这两个流实现更改文本文件的编码格式。如下图所示:
image.png
有一个编码格式为UTF-8的文本文件utf8.txt , 我们使用InputStreamReader 流时指定使用UTF-8读取该文件,然而在写出的时候,指定使用GBK字符集,相当于最终生成的文件gbk.txt是使用GBK的字符集方式进行存储的。设计如下:

/*
* 综合使用InputStreamReader 和 OutputStreamWriter
* */
@Test
public void test2(){
   InputStreamReader isr = null;
   OutputStreamWriter osw = null;
   try {
       //1.造文件,造流
       File file1 = new File("dbcp.txt");
       File file2 = new File("dbcp_gbk.txt");

       FileInputStream fis = new FileInputStream(file1);
       FileOutputStream fos = new FileOutputStream(file2);

       //将字节输入流fis转换为字符输入流InputStreamReader
       isr = new InputStreamReader(fis, "utf-8");
       //将字符输出流fos转换为字节输出流OutputStreamWriter
       osw = new OutputStreamWriter(fos, "gbk");

       //2.读写过程:
       char[] cbuf = new char[20];
       int len;
       while ((len = isr.read(cbuf)) != -1){
           osw.write(cbuf, 0, len);
       }
   } catch (IOException e) {
       e.printStackTrace();
   } finally {
       //3.关闭资源
       if(isr != null){
           try {
               isr.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
       if(osw != null){
           try {
               osw.close();
           } catch (IOException e) {
               e.printStackTrace();
           }
       }
   }

}

测试结果如下:
image.png
打开生成的dbcp_gbk.txt文件:
image.png
可以看到,IDEA提示我们文件在加载时使用了错误的编码方式。因为我们是用GBK方式写的,但是是用UTF-8读的。

2.5.2.3 字符编码(补充内容)

编码表的由来

计算机只能识别二进制,早期由来是电信号。为了方便应用计算机,让它可以识别各个国家的文字。就将各个国家的文字用数字来表示,并一一对应,形成一张表。这就是编码表。

常见的编码表

  • ASCII: 美国标准信息交换码。用一个字节的7位可以表示。
  • ISO8859-1:拉丁码表。(欧洲码表):用一个字节的8位表示。
  • GB2312:中国的中文编码表。最多两个字节编码所有字符。
  • GBK:中国的中文编码表升级,融合了更多的中文文字符号(比如支持繁体字)。最多两个字节编码。
  • Unicode:国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的 字符码。所有的文字都用两个字节来表示。
  • UTF-8: 变长的编码方式,可用1-4个字节来表示一个字符。

GB2312和GBK最多使用两个字节编码, 为什么是“最多”两个?
这是因为 ASCII码 被别的字符集所兼容,所以说在遇到英文字符时,比如a,b,c,d…, GB2312和GBK还是使用一个字节编码的,而对于一个中文字符是使用两个字节编码。(所以我们会看到一个现象,在一个乱码的字符文件当中,英文字符是不会乱码的)
那么如何区分两个连续的字节空间是表示两个英文字符,还是表示为一个中文字符呢?
只需看字节的最高位是0还是1。
image.png
Unicode编码的不完美之处:

  • 英文字母只用一个字节表示就够了。

  • 如何才能区别Unicode和ASCII?计算机怎么知道两个字节表示一个字符,而不是分别表示两个符号呢?

  • 如果和GBK等双字节编码方式一样,用最高位是1或0表示两个字节和一个字节,就少了很多值无法用于表示字符,不够表示所有字符。Unicode在很长一段时间无法推广,直到互联网的出现。

    因此,面向传输的众多UTF(UCS Transfer Format)标准出现了,顾名思义,UTF-8就是每次8个位传输数据,而UTF-16就是每次16个位。这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。
    Unicode只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的Unicode编码是UTF-8和UTF-16。
    

下面给出UTF-8编码的方式:
image.png
可以看到,在UTF-8编码格式下:

  • 如果一个字符使用一个字节编码,则按照ASCII码的标准,最高位填0.
  • 如果一个字符需要两个字节编码,则前一字节的高三位标记为110,后一字节的高两位标记为10。
  • 如果一个字符需要三个字节编码,则第一字节的高四位标记为1110,第二字节和第三字节的高两位标记为10。
  • 如果一个字符需要四个字节编码,则第一字节的高五位标记为11110,后面三字节的高两位标记为10。

UTF-8编码中,一个汉字是占用三个字节的。下面以一个中文字符"尚"字来举个例子:
image.png
"尚"字在Unicode中的编码值是23587,转换为二进制就是0010 1100 0001 1010,将这16位二进制数按照上述UTF-8编码的规则分别填入响应的位置处,如上图所示。
(在标准UTF-8编码中,超出基本多语言范围(BMP-Basic Multilingual Plane)的字符被编码为4字节格式,但是在修正的UTF-8编码中,他们由代理编码对(surrogatepairs)表示,然后这些代理编码对在序列中分别重新编码。结果UTF-8编码中需要4个字节的字符,在修正后的UTF-8编码中将需要6个字节。)
总结:字符编码的发展历程
QLQYOO)IOB_(OAT(II~QZRA_edit_362847234143197.png

2.5.3 标准输入、输出流(了解)

1.标准输入、输出流

  • System.in : 标准的输入流,代表了系统标准的输入设备,所以默认从键盘输入;
  • System.out: 标准的输出流,代表了系统标准的输出设备,所以默认从控制台输出。

2.标准输入流、输出流的类型

  • System.in的类型是InputStream, 属于字节输入流;
  • System.out的类型是PrintStream,而PrintStream是OutputStream的子类,所以System.out属于字节输出流。

3.可以使用System类的setIn(InputStream is) / setOut(PrintStream ps)方式重新指定输入和输出的设备。

public static void setIn(InputStream in)
public static void setOut(PrintStream out)

4.例题: 从键盘输入字符串,要求将读取到的整行字符串转成大写输出。然后继续进行输入操作,直至当输入“e”或者“exit”时,退出程序。
方法一:使用Scanner实现,调用next()返回一个字符串
方法二:使用System.in实现。System.in —> 转换流 —> BufferedReader的readLine()
由于题目中说要读取输入的整行字符,想到BufferedReader流中有符合要求的readLine()函数,但它是一个字符流,而System.in是个字节流,因此要想使用BufferedReader流需要先用转换流InputStreamReader将System.in这个字节输入流转换为字符输入流,这样就可以使用BufferedReader流了。
实现过程如下:

public static void main(String[] args) {
    BufferedReader br = null;
    try {
        InputStreamReader isr = new InputStreamReader(System.in);
        br = new BufferedReader(isr);

        while (true){
            System.out.println("请输入字符串:");
            String data = br.readLine();
            if("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)){
                System.out.println("程序结束");
                break;
            }

            String upperCase = data.toUpperCase();
            System.out.println(upperCase);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //关流
        if(br != null){

            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

测试结果如下:
image.png

2.5.3 打印流(了解)

从IO流体系中可以看出,打印流只有输出流:PrintStream 和 PrintWriter
image.png
作用:实现将基本数据类型的数据格式转化为字符串输出。
**PrintStream 和 PrintWriter **

  • 提供了一系列重载的print()和println()方法,用于多种数据类型的输出
  • PrintStream和PrintWriter的输出不会抛出IOException异常
  • PrintStream和PrintWriter有自动flush功能
  • PrintStream 打印的所有字符都使用平台的默认字符编码转换为字节。 在需要写入字符而不是写入字节的情况下,应该使用 PrintWriter 类。
  • System.out返回的是PrintStream的实例 。

System.out.println() 中的out就是一个PrintStream(打印流的一种),它是System的一个静态属性。而PrintStream类中有很多重载的print()方法和println()方法。
image.png
下面的例子中给出了将标准输出流System.out从控制台改成输出到文件,使用 System类的setOut方法对默认设备进行改变。
image.png
设计如下:

@Test
public void test2() {
    PrintStream ps = null;
    try {
        FileOutputStream fos = new FileOutputStream(new File("D:\\IO\\text.txt"));

        // 创建打印输出流,设置为自动刷新模式(写入换行符或字节 '\n' 时都会刷新输出缓冲区)
        ps = new PrintStream(fos, true);
        if (ps != null) {   // 把标准输出流(控制台输出)改成文件(输出到文件)
            System.setOut(ps);   //使用setOut方法对默认设备进行改变,原来是输出到控制台的,也就是:System.out默认是从控制台输出
        }

        for (int i = 0; i <= 255; i++) { // 输出ASCII字符
            System.out.print((char) i);
            if (i % 50 == 0) { // 每50个数据一行
                System.out.println(); // 换行
            }
        }
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } finally {
        if (ps != null) {
            ps.close();
        }

    }
}

测试结果如下:
image.png

2.5.4 数据流(了解)

数据流的作用: 为了方便地操作Java语言的基本数据类型和字符串。(比如我们为了保存一些变量,这些变量放在内存中不太靠谱,可以把他们持久化到文件里;在需要查看时再从文件中读取)
数据流有两个类: **DataInputStream **和 **DataOutputStream **
(这两个流分别“套接”在 InputStream 和 OutputStream 子类的流上 )
DataInputStream中的方法:

boolean readBoolean()byte readByte()
char readChar()float readFloat()
double readDouble()short readShort()
long readLong()int readInt()
String readUTF()void readFully(byte[] b)

DataOutputStream中的方法 : 将上述的方法的read改为相应的write即可。
下面给出一个例子,将内存中的字符串、基本数据类型的变量写到文件中。文件位置为当前的module下:

/*3.数据流
* 3.1 DataInputStream 和 DataOutputStream
* 3.2 作用:用于读取或写出基本数据类型的变量或字符串
*
* 练习:将内存中的字符串、基本数据类型的变量写到文件中。
* */
@Test
public void test3(){
    DataOutputStream dos = null;
    try {
        dos = new DataOutputStream(new FileOutputStream("data.txt"));

        dos.writeUTF("郭凯旋");
        dos.flush();    //刷新操作,将内存中的数据写入文件
        dos.writeInt(23);
        dos.flush();
        dos.writeBoolean(true);
        dos.flush();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(dos != null){
            try {
                dos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

测试结果如下:
image.png
当前module下就会生成一个data.txt文件,但是这个文件不是让我们直接双击来打开查看的,要想查看需要使用DataInputStream流完成,接下来使用DataInputStream流读取该文件中的内容:

/*
* 将文件中存储的基本数据类型变量和字符串读取到内存中,保存在变量中。
*
* 注意:读取不同类型的数据的顺序要与当时写入文件时保存的数据的顺序一致。
* */
@Test
public void test4(){
    DataInputStream dis = null;
    try {
        dis = new DataInputStream(new FileInputStream("data.txt"));

        String name = dis.readUTF();
        int age = dis.readInt();
        boolean isFemale = dis.readBoolean();

        System.out.println("name = " + name);
        System.out.println("age = " + age);
        System.out.println("isFemale = " + isFemale);
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(dis != null){
            try {
                dis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

需要注意的是:读取不同类型的数据的顺序要与当时写入文件时保存的数据的顺序一致。
测试结果如下:
image.png

2.5.5 对象流

对象流的作用: 用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可 以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。
对象流有两个: ObjectInputStream 和 **OjbectOutputSteam **
序列化:把数据从内存中保存到数据源中。 用ObjectOutputStream类保存基本类型数据或对象的机制 。
反序列化:用ObjectInputStream类读取基本类型数据或对象的机制。

对象序列化机制允许把内存中的Java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,或通过网络将这种二进制流传输到另一个网络节点。当其它程序获取了这种二进制流,就可以恢复成原来的Java对象

1.基本数据类型和String类型的序列化和反序列化
序列化过程:将内存中的java对象保存到磁盘中或通过网络传输出去,下面给出序列化的例子:

/*
序列化过程:将内存中的java对象保存到磁盘中或通过网络传输出去
使用ObjectOutputStream实现
 */
@Test
public void testObjectOutputStream(){
  ObjectOutputStream oos = null;

  try {
     //1.
     oos = new ObjectOutputStream(new FileOutputStream(new File("object.dat")));

     //2.
     oos.writeObject(new String("凯旋"));
     oos.flush();  //刷新操作,写出一次,操作flush()一次

  } catch (IOException e) {
     e.printStackTrace();
  } finally {
     //3.关闭流
     if(oos != null){

        try {
           oos.close();
        } catch (IOException e) {
           e.printStackTrace();
        }
     }
  }

}

注:每写出一次,操作flush()一次
测试结果如下:
image.png
这样就会生成一个object.dat 文件,但不是用来直接打开的,一般是打不开的(打开后也是乱码)。
反序列化:将磁盘文件中的对象还原为内存中的一个java对象。

/反序列化:将磁盘文件中的对象还原为内存中的一个java对象
//使用ObjectInputStream来实现

@Test
public void testObjectInputStream(){
  ObjectInputStream ois = null;
  try {
     ois = new ObjectInputStream(new FileInputStream("object.dat"));

     Object obj = ois.readObject();
     String str = (String)obj;

     System.out.println(str);

  } catch (IOException e) {
     e.printStackTrace();
  } catch (ClassNotFoundException e) {
     e.printStackTrace();
  } finally {
     //关闭流
     if(ois != null){
        try {
           ois.close();
        } catch (IOException e) {
           e.printStackTrace();
        }
     }
  }

}

测试结果如下:
image.png
2.其他(非String)引用类型对象的序列化
凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态常量:

private static final long serialVersionUID;  

Serializable接口我们通常称它为"标识接口",因为这个接口的内部没有任何的抽象方法需要实现。因此只要一个类是实现了这个接口,那么该类的实例就是可序列化的。
serialVersionUID常量:
这个常量的作用是什么?可以理解为是为了识别到底是哪个类的对象,以便在反序列化的时候按照相应的还原规则进行,保证反序列化不出错。比如如果是Person类的对象,那么该对象在经过序列化后(变成了二进制流),在还原的时候可以通过检查其serialVersionUID来决定怎样还原这个对象,是采用还原Person对象的方式进行还原,而不是采用Dog的方式。
下面我们把一个Person类的对象序列化,Person类的对象要想是可序列化的,就要满足上面说的要求,给出Person类的实现和序列化设计:
Person类

/*
* Person需要满足如下的要求,方可序列化。
*  1.需要实现接口:Serializable
*  2.当前类提供一个全局常量: serialVersionUID
*  3.除了当前Person类需要实现Serializable接口之外,还必须保证其内部所有属性也必须是
*    可序列化的。(默认情况下,基本数据类型可序列化)
*
*  补: ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量。
*                                               (所以可以把不想被序列化的部分贴上这两个标签中的一个)
* */

import java.io.Serializable;

public class Person implements Serializable{
    public static final long serialVersionUID = 444456782L;  //这个常量的值可以任意指定
    private String name;
    private int age;


    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }


    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", 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;
    }
}

Person对象的序列化:

 @Test
   public void testObjectOutputStream(){
      ObjectOutputStream oos = null;

      try {
         //1.
         oos = new ObjectOutputStream(new FileOutputStream(new File("object.dat")));

         oos.writeObject(new Person("郭凯旋", 23));
         oos.flush();

      } catch (IOException e) {
         e.printStackTrace();
      } finally {
         //3.关闭流
         if(oos != null){

            try {
               oos.close();
            } catch (IOException e) {
               e.printStackTrace();
            }
         }
      }

   }

测试结果如下:
image.png
接下来对读入的Person对象反序列化(还原Person对象)

@Test
public void testObjectInputStream(){
  ObjectInputStream ois = null;
  try {
      
     ois = new ObjectInputStream(new FileInputStream("object.dat"));
     Person p = (Person) ois.readObject();
     System.out.println(p);
      
  } catch (IOException e) {
     e.printStackTrace();
  } catch (ClassNotFoundException e) {
     e.printStackTrace();
  } finally {
     //关闭流
     if(ois != null){
        try {
           ois.close();
        } catch (IOException e) {
           e.printStackTrace();
        }
     }
  }

}

测试结果如下:
image.png
3.在上述的例子当中, serialVersionUID 常量的值我们是显示声明了出来的;如果Person类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自动生成的。也就是说,如果我们没有指定它的值,系统会帮我们分配一个值。然而如果这个类发生了变化,serialVersionUID可能发生变化 。
下面给出一个例子说明一下,在上面的例子中Person类有两个属性:name 和 age , 现在我们测试一下在序列化Person类的对象后,改变Person类本来的结构,再把未改变时序列化的那个对象进行反序列化,由此看一下不显示指定serialVersionUID常量会出现什么问题:

  • 首先去掉Person类中serialVersionUID,并对此时的Person类对象做一个序列化操作:
  • 改变Person的结构,添加一个id属性(int型)
  • 对第一步中序列化的对象作反序列。

改变后的Person类如下:

import java.io.Serializable;

public class Person implements Serializable{

    private String name;
    private int age;
    private int id;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

    public int getId() {
        return id;
    }

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

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

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

反序列化的结果如下:
image.png
可见,这样就还原不出来了,因为序列化的对象所属的Person类的serialVersionUID和修改后的Person类的serialVersionUID的值是不一样的,修改后再去还原之前的类的对象,就会找不到原来的那个serialVersionUID值,也就是不知道该使用哪种规则进行还原。
4.上述Person类中的属性都是基本数据类型和String,这些默认都是可序列化的。然而一个类要想是可序列化的,还必须保证其内部所有属性也必须是可序列化的。接下来为Person类再添加一个引用数据类型的属性Account;

package com.atguigu.java;

/*
* Person需要满足如下的要求,方可序列化。
*  1.需要实现接口:Serializable
*  2.当前类提供一个全局常量: serialVersionUID
*  3.除了当前Person类需要实现Serializable接口之外,还必须保证其内部所有属性也必须是
*    可序列化的。(默认情况下,基本数据类型可序列化)
*
*  补: ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量。
*                                               (所以可以把不想被序列化的部分贴上这两个标签中的一个)
* */

import java.io.Serializable;

public class Person implements Serializable{
    public static final long serialVersionUID = 444456782L;
    private String name;
    private int age;
    private int id;
    private Account acct;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(String name, int age, int id, Account acct) {
        this.name = name;
        this.age = age;
        this.id = id;
        this.acct = acct;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", id=" + id +
                ", acct=" + acct +
                '}';
    }

    public int getId() {
        return id;
    }

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

    public Account getAcct() {
        return acct;
    }

    public void setAcct(Account acct) {
        this.acct = acct;
    }

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


class Account implements Serializable{
    public static final long serialVersionUID = 4745932L;
    private double balance;

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    public Account(double balance) {

        this.balance = balance;
    }

    @Override
    public String toString() {
        return "Account{" +
                "balance=" + balance +
                '}';
    }
}

然后再对这个类的对象作序列化操作:

@Test
public void testObjectOutputStream(){
  ObjectOutputStream oos = null;

  try {
     //1.
     oos = new ObjectOutputStream(new FileOutputStream(new File("object.dat")));
      
     oos.writeObject(new Person("郭凯旋", 23));
     oos.flush();

     oos.writeObject(new Person("大冰", 24, 1001, new Account(500)));
     oos.flush();


  } catch (IOException e) {
     e.printStackTrace();
  } finally {
     //3.关闭流
     if(oos != null){

        try {
           oos.close();
        } catch (IOException e) {
           e.printStackTrace();
        }
     }
  }

}

反序列化过程如下:

@Test
public void testObjectInputStream(){
  ObjectInputStream ois = null;
  try {
     ois = new ObjectInputStream(new FileInputStream("object.dat"));

     Person p = (Person) ois.readObject();
     Person p1 = (Person) ois.readObject();

     System.out.println(p);
     System.out.println(p1);

  } catch (IOException e) {
     e.printStackTrace();
  } catch (ClassNotFoundException e) {
     e.printStackTrace();
  } finally {
     //关闭流
     if(ois != null){
        try {
           ois.close();
        } catch (IOException e) {
           e.printStackTrace();
        }
     }
  }

}

测试结果如下:
image.png
5.ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量。 (所以可以把不想被序列化的部分贴上这两个标签中的一个)
下面我们把Person类中的name和age属性分别贴上static和transient标签:

import java.io.Serializable;

public class Person implements Serializable{
    public static final long serialVersionUID = 444456782L;
    private static String name;
    private transient int age;
    private int id;
    private Account acct;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public Person(String name, int age, int id) {
        this.name = name;
        this.age = age;
        this.id = id;
    }

    public Person(String name, int age, int id, Account acct) {
        this.name = name;
        this.age = age;
        this.id = id;
        this.acct = acct;
    }

    public int getId() {
        return id;
    }

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

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", id=" + id +
                ", acct=" + acct +
                '}';
    }

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


class Account implements Serializable{
    public static final long serialVersionUID = 4745932L;
    private double balance;

    public double getBalance() {
        return balance;
    }

    public void setBalance(double balance) {
        this.balance = balance;
    }

    public Account(double balance) {

        this.balance = balance;
    }

    @Override
    public String toString() {
        return "Account{" +
                "balance=" + balance +
                '}';
    }
}

贴上了标签以后,再次对该类的对象进行序列化和反序列化:

/*
序列化过程:将内存中的java对象保存到磁盘中或通过网络传输出去
使用ObjectOutputStream实现
 */
@Test
public void testObjectOutputStream(){
  ObjectOutputStream oos = null;

  try {
     //1.
     oos = new ObjectOutputStream(new FileOutputStream(new File("object.dat")));

     oos.writeObject(new Person("郭凯旋", 23));
     oos.flush();

     oos.writeObject(new Person("大冰", 24, 1001, new Account(500)));
     oos.flush();


  } catch (IOException e) {
     e.printStackTrace();
  } finally {
     //3.关闭流
     if(oos != null){

        try {
           oos.close();
        } catch (IOException e) {
           e.printStackTrace();
        }
     }
  }

}

//反序列化:将磁盘文件中的对象还原为内存中的一个java对象
//使用ObjectInputStream来实现·

@Test
public void testObjectInputStream(){
  ObjectInputStream ois = null;
  try {
     ois = new ObjectInputStream(new FileInputStream("object.dat"));

     Person p = (Person) ois.readObject();
     Person p1 = (Person) ois.readObject();

     System.out.println(p);
     System.out.println(p1);

  } catch (IOException e) {
     e.printStackTrace();
  } catch (ClassNotFoundException e) {
     e.printStackTrace();
  } finally {
     //关闭流
     if(ois != null){
        try {
           ois.close();
        } catch (IOException e) {
           e.printStackTrace();
        }
     }
  }

}

测试结果如下:
image.png
可见,name 和age 属性没有被还原回来,因为他们是不可序列化的。

2.5.6 随机存取文件流( RandomAccessFile 类 )

RandomAccessFile 声明在java.io包下,但直接继承于java.lang.Object类。并 且它实现了DataInput、DataOutput这两个接口,也就意味着这个类既可以读也可以写。
RandomAccessFile 类的构造器:

  • public RandomAccessFile(File file, String mode)
  • public RandomAccessFile(String name, String mode)

创建 RandomAccessFile 类实例需要指定一个 mode 参数,该参数指定 RandomAccessFile 的访问模式:

  • r: 以只读方式打开
  • rw:打开以便读取和写入
  • rwd: 打开以便读取和写入;同步文件内容的更新
  • rws: 打开以便读取和写入;同步文件内容和元数据的更新

如果模式为只读r。则不会创建文件,而是会去读取一个已经存在的文件, 如果读取的文件不存在则会出现异常。 如果模式为rw读写。如果文件不 存在则会去创建文件,如果存在则不会创建。
1.下面给出一个使用 RandomAccessFile 读写文件的例子:

@Test
public void test1(){
    RandomAccessFile raf1 = null;
    RandomAccessFile raf2 = null;
    try {
        //1.造流的对象
        raf1 = new RandomAccessFile(new File("puxiaopu.jpg"),"r");
        raf2 = new RandomAccessFile(new File("puxiaopu1.jpg"),"rw");

        //2.读写过程
        byte[] buffer = new byte[1024];
        int len;
        while ((len = raf1.read(buffer)) != -1){
            raf2.write(buffer, 0, len);

        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //3.关闭流
        if(raf1 != null){

            try {
                raf1.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if(raf2 != null){

            try {
                raf2.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

测试结果如下:
image.png
image.png
2.如果 RandomAccessFile 作为输出流时,写出到的文件如果不存在,则在执行过程中自动创建;如果写出到的文件存在,则会对原有文件内容进行覆盖。(默认情况下,从头覆盖,如果原文件的内容比较长,那么没有被覆盖的内容还是原来的内容。)
下面我们给出一个例子验证:
首先准备一个文本文件hello.txt,内容为英文字母表的字母。
image.png
使用表示输入输出的 RandomAccessFile流写出文件:

@Test
public void test2()  {

    RandomAccessFile raf1 = null;
    try {
        raf1 = new RandomAccessFile("hello.txt", "rw");  //模式为读取和写入

        raf1.write("xyz".getBytes());
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(raf1 != null){

            try {
                raf1.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

测试结果如下:
image.png
查看一下hello.txt文件中的内容:
image.png
可以看到abc被覆盖为了xyz。
如果hello.txt不存在的话,在执行过程中自动创建,现在我们把hello.txt删除,测试结果如下:
image.png
所生成的hello.txt文件中的内容为写入的"xyz"。
3. RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。 RandomAccessFile 类对象可以自由移动记录指针:

  • long getFilePointer():获取文件记录指针的当前位置
  • void seek(long pos):将文件记录指针定位到 pos 位置,默认是0,也就是文件开头。

下面我们使用seek函数把def处的字符替换为xyz。
image.png可见,应该从索引位置3处开始替换。

@Test
public void test2()  {

    RandomAccessFile raf1 = null;
    try {

        raf1 = new RandomAccessFile("hello.txt", "rw");

        raf1.seek(3);

        raf1.write("xyz".getBytes());
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if(raf1 != null){

            try {
                raf1.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

测试结果如下:
image.png

3. NIO.2中Path、Paths、Files类的使用

3.1 Java NIO 概述

Java NIO (New IO,Non-Blocking IO)是从Java 1.4版本开始引入的一套新 的IO API,可以替代标准的Java IO API。NIO与原来的IO有同样的作用和目 的,但是使用的方式完全不同,NIO支持面向缓冲区的(IO是面向流的)、基于 通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。
Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
image.png

3.2 NIO.2

随着 JDK 7 的发布,Java对NIO进行了极大的扩展,增强了对 文件处理和文件系统特性的支持,以至于我们称他们为 NIO.2。 因为 NIO 提供的一些功能,NIO已经成为文件处理中越来越重要的部分。

3.3 Path、Paths和Files核心API

早期的Java只提供了一个File类来访问文件系统,但File类的功能比较有限,所提供的方法性能也不高。而且,大多数方法在出错时仅返回失败,并不会提供异常信息。
NIO. 2为了弥补这种不足,引入了Path接口,代表一个平台无关的平台路径,描述了目录结构中文件的位置。Path可以看成是File类的升级版本,实际引用的资源也可以不存在。

  • 19
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以为你介绍一下如何使用 Java 编写程序实现文件拷贝。 Java 提供了非常方便的 I/O 类来处理文件读写操作。文件拷贝的基本思路是读取源文件中的数据,然后将数据写入到目标文件中。下面是一个简单的文件拷贝程序示例: ```java import java.io.*; public class FileCopy { public static void main(String[] args) throws IOException { // 源文件路径 String srcFilePath = "C:/temp/source.txt"; // 目标文件路径 String destFilePath = "C:/temp/dest.txt"; // 创建输入和输出 FileInputStream fis = new FileInputStream(srcFilePath); FileOutputStream fos = new FileOutputStream(destFilePath); // 定义缓冲区,每次读取 1024 个字节 byte[] buffer = new byte[1024]; int len; // 循环读取源文件中的数据,并将数据写入到目标文件中 while ((len = fis.read(buffer)) != -1) { fos.write(buffer, 0, len); } // 关闭输入和输出 fis.close(); fos.close(); System.out.println("文件拷贝完成!"); } } ``` 在上面的示例中,我们首先定义了源文件路径和目标文件路径。然后创建了输入和输出,用于读取源文件和向目标文件写入数据。我们定义了一个缓冲区,每次从输入中读取 1024 个字节,并将这些字节写入到输出中。最后,我们关闭输入和输出,并输出一条完成信息。 需要注意的是,上面的代码中使用了 try-catch 语句来捕获可能出现的 IOException 异常。这是因为在文件读写过程中可能会出现异常,比如文件不存在、文件无法读取等等。为了保证程序的健壮性,我们需要使用 try-catch 语句来处理这些异常。 希望这个简单的示例可以帮助你了解如何使用 Java 编写文件拷贝程序。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值