Java IO流 : 新手入门最全面总结

目录

一、File类的使用

1. 如何创建File类的实例

2. 路径分隔符

3. File类的常用方法

3.1 File类的获取功能

3.2 File类的重命名功能

3.3 File类的判断功能

3.4 File类的创建与删除功能

4. File类总结

二、IO流原理以及流的分类

1. IO流原理

2. 流的分类

3. 流的体系结构

三、节点流(或文件流)

1. FileReader的使用

2. FileWriter的使用

3. 使用字符流实现文本文件的复制

4. FileInputStream的使用

5. FileOutputStream的使用

四、缓冲流

1. 缓冲流(字节型)

2. 缓冲流(字符型)

3. 关于flush()方法

4. 总结

五、转换流

1. InputStreamReader的使用

2. OutputStreamWriter的使用

3. 字符编码

六、标准输入、输出流

七、打印流

八、数据流

九、对象流

1. 对象的序列化

2. 使用对象流序列化对象

3. 使用对象流反序列化对象

4. 自定义类的对象流操作

十、随机存取文件流

十一、NIO2.0


一、File类的使用

文件是作为数据从内存到硬盘或从硬盘到内存的一个端点,数据从内存中写到文件里,文件中的数据读取到内存中。所以这个点(文件)在Java层面要对应一个类的对象,一个文件在Java里用File类的对象来充当。File类的对象除了充当文件以外还可以充当文件目录 (文件夹)。

  • java.io.File类:文件和文件目录路径的抽象表示形式,与平台无关。

  • File 能新建、删除、重命名文件和目录,但 File 不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。

  • 想要在Java程序中表示一个真实存在的文件或目录,那么必须有一个File对象,但是Java程序中的一个File对象,可能没有一个真实存在的文件或目录。

  • File对象可以作为参数传递给流的构造器

1. 如何创建File类的实例

public File(String pathname) : 以pathname为路径创建File对象,可以是绝对路径或者相对路径,如果pathname是相对路径,则默认的当前路径在系统属性user.dir中存储。

  • 绝对路径:是一个固定的路径,从盘符开始

  • 相对路径:是相对于某个位置开始

public File(String parent,String child) : 以parent为父路径,child为子路径创建File对象。

public File(File parent,String child) : 根据一个父File对象和子文件路径创建File对象。

@Test
public void test01(){
    //此时创建的只是一个内存层面的对象
    //构造器1
    File file1 = new File("hello.txt");//相较于当前的module的路径 (通过单元测试方法调用的情况)
    //如果在main方法中调用 则是相较于当前工程下的路径
    //相对路径 相对的是当前调用者的路径
    
    File file2 = new File("D:\\CS\\Java\\Java高级\\JavaIO流\\Code\\hi.txt");//绝对路径
​
    System.out.println(file1);
    System.out.println(file2);
​
    //构造器2
    File file3 = new File("D:\\CS\\Java\\Java高级", "JavaIO流");
    //表示parent路径下的一个文件目录
    System.out.println(file3);
​
    //构造器3
    File file4 = new File(file3, "hi.txt");
    //表示file3目录下的一个txt文件
    System.out.println(file4);
}

说明 : 在IDEA和Eclipse相对路径的问题

  • 在IDEA中,如果开发使用JUnit中的单元测试方法测试,相对路径即为当前Module下。如果使用main()测试,相对路径即为当前的Project下。

  • 在Eclipse中,不管使用单元测试方法还是使用main()测试,相对路径都是当前的Project下。

2. 路径分隔符

路径中的每级目录之间用一个路径分隔符隔开。

路径分隔符和系统有关:

  • 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 file3 = new File("d:/atguigu");

3. File类的常用方法

3.1 File类的获取功能

  • public String getAbsolutePath() : 获取绝对路径

  • public String getPath() : 获取路径

  • public String getName() : 获取名称

  • public String getParent() : 获取上层文件目录路径。若无,返回null

  • public long length() : 获取文件长度(即:字节数)。不能获取目录的长度。

  • public long lastModified() : 获取最后一次的修改时间,毫秒值

  • public String[] list() : 获取指定目录下的所有文件或者文件目录的名称数组

  • public File[] listFiles() : 获取指定目录下的所有文件或者文件目录的File数组

    当在硬盘中没有真实存在文件时,某些属性会赋值为初始值 :

    @Test
    public void test03(){
        File file1 = new File("D:\\JavaIOTest\\hi.txt");//绝对路径
        File file2 = new File("hello.txt");//相对路径
    ​
        System.out.println("绝对路径 : ");
        System.out.println("absolutePath : " + file1.getAbsolutePath());//获取绝对路径
        //absolutePath : D:\JavaIOTest\hi.txt
        System.out.println("path : " + file1.getPath());//获取路径
        //path : D:\JavaIOTest\hi.txt
        System.out.println("name : " + file1.getName());//获取名称
        //name : hi.txt
        System.out.println("parent : " + file1.getParent());//获取上层文件目录路径。若无,返回null
        //parent : D:\JavaIOTest
        System.out.println("length : " + file1.length());//获取文件长度(即:字节数)。不能获取目录的长度。
        //length : 0
        System.out.println("lastModifiedTime : " + file1.lastModified());//获取最后一次的修改时间,毫秒值
        //lastModifiedTime : 0
        
        System.out.println("相对路径 : ");
        System.out.println("absolutePath : " + file2.getAbsolutePath());//获取绝对路径
        //absolutePath : D:\CS\Java\Java高级\JavaIO流\Code\hello.txt
        System.out.println("path : " + file2.getPath());//获取路径
        //path : hello.txt
        System.out.println("name : " + file2.getName());//获取名称
        //name : hello.txt
        System.out.println("parent : " + file2.getParent());//获取上层文件目录路径。若无,返回null
        //parent : null
        System.out.println("length : " + file2.length());//获取文件长度(即:字节数)。不能获取目录的长度。
        //length : 0
        System.out.println("lastModifiedTime : " + file2.lastModified());//获取最后一次的修改时间,毫秒值
        //lastModifiedTime : 0
    }

    当在硬盘中真实存在文件时 :

    @Test
    public void test04(){
        File file1 = new File("D:\\JavaIOTest\\hello.txt");//绝对路径
    ​
        System.out.println("绝对路径 : ");
        System.out.println("absolutePath : " + file1.getAbsolutePath());//获取绝对路径
        //absolutePath : D:\JavaIOTest\hello.txt
        System.out.println("path : " + file1.getPath());//获取路径
        //path : D:\JavaIOTest\hello.txt
        System.out.println("name : " + file1.getName());//获取名称
        //name : hello.txt
        System.out.println("parent : " + file1.getParent());//获取上层文件目录路径。若无,返回null
        //parent : D:\JavaIOTest
        System.out.println("length : " + file1.length());//获取文件长度(即:字节数)。不能获取目录的长度。
        //length : 10
        System.out.println("lastModifiedTime : " + new Date(file1.lastModified()));//获取最后一次的修改时间,毫秒值
        //lastModifiedTime : Tue Aug 30 16:07:38 CST 2022
    }

    最后两个方法适用于文件目录,作用相同,只是返回值类型不同 :

    @Test
    public void test05(){
        File file = new File("D:\\JavaIOTest");
    ​
        //获取指定目录下的所有文件或者文件目录的名称数组
        String[] list = file.list();
        System.out.println(Arrays.toString(list));
        //[hello.txt, JavaIOTest.txt]
    ​
        //获取指定目录下的所有文件或者文件目录的File数组
        File[] files = file.listFiles();
        System.out.println(Arrays.toString(files));
        //[D:\JavaIOTest\hello.txt, D:\JavaIOTest\JavaIOTest.txt]
        
    }

3.2 File类的重命名功能

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

    注意 : 要想重命名成功,则必须保证file1在硬盘中存在且file2在硬盘中不存在,重命名的过程可以改变文件的存放路径,如果只是单纯的重命名文件,那么要保证文件路径不变。

    @Test
    public void test06(){
        File file1 = new File("test.txt");//存在
        File file2 = new File("D:\\JavaIOTest\\dest.txt");//不存在
    ​
        boolean renameTo = file1.renameTo(file2);
        System.out.println(renameTo);
    ​
    }

3.3 File类的判断功能

  • public boolean isDirectory() : 判断是否是文件目录

  • public boolean isFile() : 判断是否是文件

  • public boolean exists() : 判断是否存在

  • public boolean canRead() : 判断是否可读

  • public boolean canWrite() : 判断是否可写

  • public boolean isHidden() : 判断是否隐藏

    @Test
    public void test07(){
        File file = new File("test.txt");
        //针对文件
        System.out.println("isDirectory : " + file.isDirectory());//判断是否是文件目录
        //isDirectory : false
        System.out.println("isFile : " + file.isFile());//判断是否是文件
        //isFile : true
        System.out.println("exists : " + file.exists());//判断是否存在
        //exists : true
        System.out.println("canRead : " + file.canRead());//判断是否可读
        //canRead : true
        System.out.println("canWrite : " + file.canWrite());//判断是否可写
        //canWrite : true
        System.out.println("isHidden : " + file.isHidden());//判断是否隐藏
        //isHidden : false
        //如果文件不存在 则上述方法都是false
    ​
        System.out.println();
    ​
        //针对文件目录
        File file1 = new File("D:\\JavaIOTest");
    ​
        System.out.println("isDirectory : " + file1.isDirectory());//判断是否是文件目录
        //isDirectory : true
        System.out.println("isFile : " + file1.isFile());//判断是否是文件
        //isFile : false
        System.out.println("exists : " + file1.exists());//判断是否存在
        //exists : true
        System.out.println("canRead : " + file1.canRead());//判断是否可读
        //canRead : true
        System.out.println("canWrite : " + file1.canWrite());//判断是否可写
        //canWrite : true
        System.out.println("isHidden : " + file1.isHidden());//判断是否隐藏
        //isHidden : false
        //如果文件目录不存在 则上述方法都是false
    ​
    }

3.4 File类的创建与删除功能

  • public boolean createNewFile() : 创建文件。若文件存在,则不创建,返回false。

  • public boolean mkdir() : 创建文件目录。如果此文件目录存在,就不创建了。如果此文件目录的上层目录不存在,也不创建。

  • public boolean mkdirs() : 创建文件目录。如果上层文件目录不存在,一并创建。

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

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

@Test
public void test08() throws IOException {
    File file1 = new File("hi.txt");
​
    if (!file1.exists()){
        //创建文件
        file1.createNewFile();
        System.out.println("创建成功 ! ");
    } else {
        //删除文件
        file1.delete();
        System.out.println("删除成功 ! ");
    }
}

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

 

4. File类总结

File类中涉及到关于文件或文件目录的创建、删除、重命名、修改时间、文件大小等方法,并未涉及到写入或读取文件内容的操作。如果需要读取或写入文件内容,必须使用IO流来完成

后续File类的对象常会作为参数传递到流的构造器中,指明读取或写入的"终点"。

File类练习题代码 :

public class FileExer {

    @Test
    public void test01() throws IOException {
        //创建一个与该文件同目录下的另一个文件
        File file = new File("D:\\JavaIOTest\\io\\hello.txt");
        File destFile = new File(file.getParent(), "haha.txt");

        boolean newFile = destFile.createNewFile();
        if (newFile) System.out.println("创建成功 ! ");
        else System.out.println("创建失败 ! ");
    }

    /**
     * @Description 判断指定目录下是否有后缀名为.jpg的文件 如果有则打印其文件名
     * @author hjc
     * @param dir
     * @returnType void
     */
    public void printJPGFileName(File dir){

        File[] files = dir.listFiles();
        if (files != null) {
            for (File file : files) {
                if (file.getName().endsWith(".jpg"))
                    System.out.println(file.getName());
            }
        }
    }

    /**
     * @Description 删除指定目录下的指定文件
     * @author hjc
     * @param dir destFileName
     * @returnType boolean
     */
    public boolean deleteFileFromDir(File dir, String destFileName) {
        //如果文件夹不存在 返回false
        if (! dir.exists()) return false;

        File destFile = new File(dir, destFileName);

        if (! destFile.exists()) throw new RuntimeException("目标文件不存在 ! ");

        return destFile.delete();
    }

    /**
     * @Description 打印指定文件夹下的所有文件名称以及其子文件夹下的文件
     * @author hjc
     * @param dir
     * @returnType void
     */
    public void printAllFileName(File dir) {

        File[] files = dir.listFiles();

        System.out.println(dir.getName() + "目录下 : ");

        if (files != null) {
            for (File file : files) {
                if (file.isFile()) System.out.println(file.getAbsolutePath());
                else printAllFileName(file);
            }
        } else return;

    }
}

二、IO流原理以及流的分类

1. IO流原理

IO : 输入(Input)、输出(Output)

  • 输入 : 将持久化的数据(硬盘上的数据)读取到内存中,使其可以在程序中操作

  • 输出 : 将内存中的数据写入到硬盘上(文件中 : .txt、.jpg、.avi等)

I/O技术是非常实用的技术,用于处理设备之间的数据传输。如读/写文件,网络通讯等。

Java程序中,对于数据的输入/输出操作以 “ 流(stream) ” 的方式进行。

java.io包下提供了各种 “ 流 ” 类和接口,用以获取不同种类的数据,并通过标准的方法输入或输出数据。

2. 流的分类

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

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

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

        

节点流 : 直接从数据源或目的地读写数据

处理流 : 不直接连接到数据源或目的地,而是“连接”在已存在的流(节点流或处理流)之上,通过对数据的处理为程序提供更为强大的读写功能。(包在节点流上,提供更多的功能等)

3. 流的体系结构

IO流有四个抽象基类 :

  • 字节输入流 : InputStream

  • 字节输出流 : OutputStream

  • 字符输入流 : Reader

  • 字符输出流 : Writer

(抽象基类)字节流字符流
输入流InputStreamReader
输出流OutputStreamWriter

Java的IO流共涉及40多个类,实际上非常规则,都是从如下4个抽象基类派生的。由这四个类派生出来的子类名称都是以其父类名作为子类名后缀。

IO流体系 : 其中,蓝色的为我们需要重点关注的。

节点流(或文件流) :

  • FileInputSteam

  • FileOutputStream

  • FileReader

  • FileWriter

缓冲流 (处理流的一种) :

  • BufferedInputStream

  • BufferedOutputStream

  • BufferedReader

  • BufferedWriter

三、节点流(或文件流)

1. FileReader的使用

作用 : 将硬盘文件中的内容按字符读入到程序中

  1. 实例化File类的对象,指明要操作的文件

  2. 提供具体的流

  3. 数据的读入操作

  4. 流资源的关闭操作

    /*
    将该module下的"hello.txt"文件内容读入到程序中并输出到控制台
     */
    @Test
    public void testFileReader() {
        //1. 实例化File类的对象,指明要操作的文件
        File file = new File("hello.txt");//相较于当前module

        //2. 提供具体的流
        FileReader fr = null;

        try {
            fr = new FileReader(file);

            //3. 数据的读入
            //read() : 返回读入的一个字符。如果到达文件末尾,返回-1
            //方式一 :
//        int data = fr.read();
//        while (-1 != data){
//            System.out.print((char) data);
//            data = fr.read();
//        }
            //方式二 : 针对方式一的语法上的修改 建议使用
            int data;
            while((data = fr.read()) != -1){
                System.out.println(data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fr != null){
                //4. 流的关闭操作
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

说明 :

  • 对于数据读入方式二,只是改变了语法,少写一行代码,看起来简洁美观

  • read()的理解 : 返回读入的一个字符的ASCII码值。如果到达文件末尾,返回-1

  • 对于异常处理方式的选择,选择try-catch-finally的处理方式,是为了避免打开流资源 ( 流资源涉及物理连接,JVM不会自动关闭 ) 以后,read()方法阻塞读不出来数据出现异常无法关闭资源的情况,使用finally执行close()方法,关闭流资源,防止造成内存泄漏

  • 对于流关闭前判断 fr 是否为null的操作,是为了避免在new流对象时出现FileNotFoundException等异常导致没有创建对象,进而在执行finally的close()方法时出现空指针的情况。

  • 读入的文件一定要存在,否则就会报FileNotFoundException异常。

对read()方法的操作升级 : 使用read()的重载方法 减少交互

  //对read()方法的操作升级 : 使用read()的重载方法
    @Test
    public void testFileReader1(){

        //1. File类的实例化
        File file = new File("hello.txt");

        FileReader fr = null;
        try {
            //2. FileReader类的实例化
            fr = new FileReader(file);

            char[] cbuf = new char[5];
            int len;
            //3. 读入的操作
            //read(char[] cbuf) : 返回每次读入cbuf数组中的字符的个数。如果达到文件末尾,返回-1
            while ((len = fr.read(cbuf)) != -1){
                //方式一
                //错误的写法 : 最后一次读出的char数组不一定是5个字符,可能是三个
                // 而后两个是上一次读入的未替换的字符
//                for (int i = 0; i < cbuf.length; i++) {
//                    System.out.print(cbuf[i]);
//                }
                //result : helloworld123ld
                //正确的写法 :
//                for (int i = 0; i < len; i++) {
//                    System.out.print(cbuf[i]);
//                }
                //result : helloworld123
                //方式二 : 使用String
                //错误的写法,对应方式一的错误写法
//                String str = new String(cbuf);
                //正确的写法
                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();
                }
            }
        }
    }

2. FileWriter的使用

作用 : 从内存中写出数据按字符存到硬盘的文件里

  1. 提供File类的对象,指明写出到的文件

  2. 提供FileWriter的对象,用于数据的写出

  3. 数据的写出操作

  4. 流资源的关闭操作

    /*
    从内存中写出数据到硬盘的文件里
     */
    @Test
    public void testFileWriter(){
        //1. 提供File类的对象,指明写出到的文件
        File file = new File("haha.txt");

        FileWriter fw = null;
        try {
            //2. 提供FileWriter的对象,用于数据的写出
//            fw = new FileWriter(file);
//            fw = new FileWriter(file, false);
            //在原有文件基础上追加内容
            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. 流资源的关闭
            if (fw != null){
                try {
                    fw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

说明 :

  • 输出操作,对应的File可以不存在的,并不会报异常

  • File对应的硬盘中的文件如果不存在,在输出的过程中,会自动创建此文件。

  • File对应的硬盘中的文件如果存在 :

    • 如果流使用的构造器时 : FileWriter(file, false) / FileWriter(file) : 对原有文件的覆盖

    • 如果流使用的构造器时 : FileWriter(file, true) : 不会覆盖原有的文件,而是在原有文件的基础上追加内容

3. 使用字符流实现文本文件的复制

  1. 创建File类对象,指明读入和写出的文件

  2. 创建输入流和输出流对象

  3. 数据的读入和写出的操作

  4. 关闭流资源

 /*
    使用FileReader和FileWriter实现对文件的复制
     */
    @Test
    public void testFileReaderFileWriter(){
        //1. 创建File类对象,指明读入和写出的文件
        File srcFile = new File("haha.txt");
        File destFile = new File("copyhaha.txt");

        //2. 创建输入流和输出流对象
        FileReader fr = null;
        FileWriter fw = null;

        try {
            fr = new FileReader(srcFile);
            fw = new FileWriter(destFile);

            3. 数据的读入和写出的操作
            char[] cbuf = new char[5];
            int len;
            //读出
            while ((len = fr.read(cbuf)) != -1){
                //写入 : 每次写出len个字符
                fw.write(cbuf, 0, len);

            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //4. 关闭流资源
            //方式一
            if (fw != null){
                try {
                    fw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fr != null){
                try {
                    fr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            
            //方式二
//            try {
//                if (fw != null)
//                fw.close();
//            } catch (IOException e) {
//                e.printStackTrace();
//            } finally {
//                if (fr != null)
//                    try {
//                        fr.close();
//                    } catch (IOException e) {
//                        e.printStackTrace();
//                    }
//            }
        }
    }

说明 : 不能使用字符流来处理图片等字节数据,只能是文本文件

4. FileInputStream的使用

作用 : 将硬盘文件中的内容按字节读入到程序中

/*
使用字节流FileInputStream来处理文本文件可能造成乱码
*/
@Test
public void testFileInputStream(){
    //1. 造文件
    File file = new File("hello.txt");

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

        byte[] buffer = new byte[5];
        int len;
        //3. 读数据
        while ((len = fis.read(buffer)) != -1){
            String str = new String(buffer, 0, len);
            System.out.print(str);
        }
        //helloworld123��德���
        //因为字母和数字占一个字节就够了 所以能正常用字节流读出
        //一个汉字一般需要三个字节来展示 所以使用字节流如果数组开小了会将汉字分开导致不能正常解析
        //会出现乱码的情况

    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4. 关闭流资源
        if (fis != null)
            try {
                fis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
    }
    
}

说明 :

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

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

5. FileOutputStream的使用

作用 : 从内存中写出数据按字节存到硬盘的文件里

/*
实现对图片的复制操作
 */
@Test
public void testFileInputOutputStream(){
    //1. 造文件
    File srcFile = new File("Java高级.jpg");
    File destFile = new File("CopyJava高级.jpg");

    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
        //2. 造流
        fis = new FileInputStream(srcFile);
        fos = new FileOutputStream(destFile);

        //3. 复制的过程
        byte[] buf = new byte[1024];
        int len;
        while ((len = fis.read(buf)) != -1){
            fos.write(buf, 0, len);
        }

        if (destFile.exists()) System.out.println("图片复制成功 ! ");

    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //4. 流关闭
        if (fos != null)
            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 srcName = "D:\\JavaIOTest\\testfile.zip";
    String destName = "D:\\JavaIOTest\\copytestfile.zip";

    copyFile(srcName, destName);

    long end = System.currentTimeMillis();

    System.out.println("复制文件花费的时间为 : " + (end - start) + "毫秒");//624毫秒
}

public void copyFile(String srcFileName, String destFileName){
    //1. 造文件
    File srcFile = new File(srcFileName);
    File destFile = new File(destFileName);

    FileInputStream fis = null;
    FileOutputStream fos = null;
    try {
        //2. 造流
        fis = new FileInputStream(srcFile);
        fos = new FileOutputStream(destFile);

        //3. 复制的过程
        byte[] buf = new byte[1024];//最合适的数值1024 交互的次数和使用内存的大小适中
        int len;
        while ((len = fis.read(buf)) != -1){
            fos.write(buf, 0, len);
        }

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

说明 :

  • 读取数据的byte数组大小最好为1024,比较合适,交互的次数和使用内存的大小适中

  • 字节流可以复制文本文件,不会出现乱码问题。(只是搬运工,并没有在程序中查看)

四、缓冲流

缓冲流属于处理流的一种

作用 : 提高流的读取,写入的速度

1. 缓冲流(字节型)

使用BufferedInputStream和BufferedOutputStream实现图片的复制 :

   /*
    实现非文本文件的复制
     */
    @Test
    public void BufferedStreamTest(){

        //1. 造文件
        File srcFile = new File("Java高级.jpg");
        File destFile = new File("copyJava高级.jpg");
        FileInputStream fis = null;
        FileOutputStream fos = null;
        BufferedInputStream bis = null;
        BufferedOutputStream bos = null;

        try {
            //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 (bos != null){
                try {
                    bos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (bis != null) {
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

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

说明 :

  • 关闭外层流的同时,内层流也会自动的进行关闭,关于内层流的关闭,我们可以省略

将缓冲流实现文件复制封装为一个方法 :

@Test
public void testCopyFileByBuffered(){

    long start = System.currentTimeMillis();

    File srcFile = new File("D:\\JavaIOTest\\testfile.zip");
    File destFile = new File("D:\\JavaIOTest\\buffercopytestfile.zip");

    copyFileByBuffered(srcFile, destFile);

    long end = System.currentTimeMillis();
    System.out.println("文件复制完成,用时 : " + (end - start) + "ms");
    //文件复制完成,用时 : 108ms

}

public void copyFileByBuffered(File srcFile, File destFile){

    FileInputStream fis = null;
    FileOutputStream fos = null;
    BufferedInputStream bis = null;
    BufferedOutputStream bos = null;

    try {
        //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 (bos != null){
            try {
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (bis != null) {
            try {
                bis.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

说明 :

  • 不加缓冲流时复制zip文件时间为624毫秒,加缓冲流时复制zip文件时间为108毫秒

  • 提高读写速度的原因 : 在其内部有一个缓冲区大小默认可以存放8个1024字节,等缓冲区满了统一写出去

  • bos.flush() : 刷新缓冲区,数据清空,将数据写出去,如果手动调这个方法,就不再等缓冲区满,直接写

2. 缓冲流(字符型)

使用BufferedReader和BufferedWriter实现文本文件的复制 :

   /*
    Buffered实现文本文件的复制
     */
    @Test
    public void testBufferedReaderWriter(){
        //1. 创建各种流对象
        BufferedReader br = null;
        BufferedWriter bw = null;
        try {
            br = new BufferedReader(new FileReader(new File("矩阵乘法.cpp")));
            bw = new BufferedWriter(new FileWriter(new File("copy矩阵乘法.cpp")));

            //2. 复制操作
            //方式一 : 传统方式
//            char[] cbuf = new char[10];
//            int len;
//            while ((len = br.read(cbuf)) != -1){
//
//                bw.write(cbuf, 0, len);
//
//            }

            //方式二 : 使用BufferedReader中的readLine()方法
            String data;
            while ((data = br.readLine()) != null){
                //方法一 :
                bw.write(data + "\n");//data中不包含换行符
                //方法二 :
                bw.write(data);//data中不包含换行符
                bw.newLine();//提供写出换行的操作
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            //3. 流资源的关闭
            if (bw != null){
                try {
                    bw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (br != null){
                try {
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }

    }

说明 :

  • BufferedWriter中有readLine()方法,支持每一行的读写,返回每一行的字符串,不包含换行符,如果读到结尾,返回null。

  • BufferedWriter中有newLine()方法,单独写入一个换行符

3. 关于flush()方法

flush()方法是针对于输出流的 并且只针对于有缓冲区的输出流

关于带缓冲区的输出流的缓冲方式有两种 : 支持动态清空缓冲区 vs 不支持动态清空缓冲区

  • 支持动态清空缓冲区 : 当我们的缓冲区最后一次装数据没有装满,这时也会自动将缓冲区清空,将数据输出。

  • 不支持动态清空缓冲区 : 这时,我们只有装满缓冲区后缓冲区才会清空,否则就需要手动清空。

可见,不支持动态清空缓冲区的输出流就有可能出现缓冲区中有残留数据的现象。

输出流在写出数据的时候都会自动调用flush()方法来清空缓冲区,但是输出流在写出数据的时候一般都是在写满缓冲区以后才自动清空缓冲区,这时,如果不支持动态清空缓冲区的输出流在文件读取完毕以后,缓冲区没有填满,就会出现有一部分数据没有写出的情况,这一部分数据残留在了缓冲区中。

但是我们在写程序时,即使没有对输出流进行flush()清空缓冲区,发现文件内容还是完好无损的,是因为在关闭流资源时会自动调用一次flush()方法来清空缓冲区,所以一直没有手动调用flush()清空缓冲区,而文件内容完好无损。

虽然会在关闭流资源时自动调用一次flush()方法,但我们在关闭带缓冲区的输出流对象之前最好是对其调用一次flush()方法,进行一次缓冲区的刷新。

4. 总结

抽象基类		节点流
InputStream	   FileInputStream (read(byte[] buffer))
OutputStream   FileOutputStream (write(byte[] buffer,0,len))
Reader		   FileReader (read(char[] cbuf))
Writer		   FileWriter (write(char[] cbuf,0,len))

缓冲流(处理流的一种)
BufferedInputStream (read(byte[] buffer))
BufferedOutputStream (write(byte[] buffer,0,len)\flush())
BufferedReader (read(char[] cbuf)\readLine())
BufferedWriter (write(char[] cbuf,0,len)\newLine()\flush)

练习题 : 获取文本上每一个字符出现的次数

/*
统计(矩阵乘法.cpp)文件中每个字符出现的次数
 */
@Test
public void test01(){
    //使用Map容器存储每个字符及其对应的个数
    HashMap<Character, Integer> map = new HashMap<>();

    BufferedReader br = null;
    BufferedWriter bw = null;
    try {
        br = new BufferedReader(new FileReader("矩阵乘法.cpp"));
        bw = new BufferedWriter(new FileWriter("charCount.txt"));

        //将每个字符读出来放进char数组
        char[] cbuf = new char[20];
        int len;
        while ((len = br.read(cbuf)) != -1){

            for (int i = 0; i < len; i++) {
                //判断是否包含此字符
                boolean isContains = map.containsKey(cbuf[i]);

                if (isContains){
                    int count = map.get(cbuf[i]);
                    map.put(cbuf[i], ++count);
                } else {
                    map.put(cbuf[i], 1);
                }

            }
        }
        //将Map容器中的数据写入charCount.txt文件中
        Set<Map.Entry<Character, Integer>> entrySet = map.entrySet();
        for (Map.Entry<Character, Integer> entry : entrySet) {
            Character ch = entry.getKey();
            Integer conut = entry.getValue();

            switch (ch){
                case ' ' :
                    bw.write("[空格]=" + entry.getValue() + "times");
                    break;
                case '\t' :
                    bw.write("[tab键]=" + entry.getValue() + "times");
                    break;
                case '\r' :
                    bw.write("[回车]=" + entry.getValue() + "times");
                    break;
                case '\n' :
                    bw.write("[换行]=" + entry.getValue() + "times");
                default :
                    bw.write("[" + entry.getKey() + "]=" + entry.getValue() + "times");
                    break;
            }
            bw.newLine();//换行
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (bw != null){
            try {
                bw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (br != null){
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

五、转换流

转换流的作用 : 提供了在字节流和字符流之间的转换,也是处理流的一种,属于字符流

Java API提供了两个转换流:

  • InputStreamReader : 将nputStream转换为Reader 将一个字节的输入流转换为字符的输入流 : 从文件中读出的字节 以字符方式读入到程序

  • OutputStreamWriter : 将Writer转换为OutputStream 将一个字符的输出流转换为字节的输出流 : 从程序中读出的字符 以字节方式读入到文件

字节流中的数据都是字符时,转成字符流操作更高效。

很多时候我们使用转换流来处理文件乱码问题。实现编码和解码的功能。

  • 解码 : 字节、字节数组 --- 字符、字符数组

  • 编码 : 字符、字符数组 --- 字节、字节数组

编码决定了解码的方式

1. InputStreamReader的使用

实现字节的输入流到字符的输入流的转换 :

public class InputStreamReaderTest {
    public static void main(String[] args) {

        File file = new File("矩阵乘法.cpp");

        InputStreamReader isr = null;
        FileInputStream fis = null;
        try {
            fis = new FileInputStream(file);
            //使用系统默认的字符集
//            isr = new InputStreamReader(fis);
            //参数2指明了字符集,具体使用哪个字符集,要看源文件保存时指明的字符集
            isr = new InputStreamReader(fis, "utf-8");

            char[] cbuf = new char[20];
            int len;
            while ((len = isr.read(cbuf)) != -1){
                String str = new String(cbuf, 0, len);
                System.out.print(str);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (isr != null) {
                try {
                    isr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

说明 :

  • 选择InputStreamReader的构造器时,一个参数构造器按照系统默认字符集进行解码

  • 字符集的选择要选源文件保存时指定的字符集

2. OutputStreamWriter的使用

将文件以utf-8解码读入程序,以gbk编码读出程序 :

public class OutputStreamWriterTest {
    public static void main(String[] args) {

        File srcFile = new File("srcfile.txt");
        File encodingFile = new File("encodingFile.txt");

        InputStreamReader isr = null;
        OutputStreamWriter osw = null;
        try {
            //按照utf-8解码
            isr = new InputStreamReader(new FileInputStream(srcFile), "utf-8");
            //按照gbk编码
            osw = new OutputStreamWriter(new FileOutputStream(encodingFile), "gbk");

            char[] cbuf = new char[20];
            int len;
            while ((len = isr.read(cbuf)) != -1){
                osw.write(cbuf, 0, len);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (osw != null){
                try {
                    osw.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (isr != null) {
                try {
                    isr.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

3. 字符编码

编码表的由来 :

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

常见的编码表 :

  • ASCII : 美国标准信息交换码,用一个字节的7位可以表示(空了一位共表示128个字符)

  • ISO8859-1 : 拉丁码表。欧洲码表,用一个字节的8位表示。

  • GB2312 : 中国的中文编码表。最多两个字节编码所有字符

  • GBK : 中国的中文编码表升级,融合了更多的中文文字符号。最多两个字节编码

  • Unicode : 国际标准码,融合了目前人类使用的所有字符。为每个字符分配唯一的字符码。所有的文字都用两个字节来表示。

  • UTF-8 : 变长的编码方式,可用1-4个字节来表示一个字符。Unicode编码表的实现方案。

在Unicode出现之前,所有的字符集都是和具体编码方案绑定在一起的 (即字符集 ≈ 编码方式) ,都是直接将字符和最终字节流绑定死了。 但是Unicode的实现方案是由UTF-8字符集来实现的。

GBK等双字节编码方式,用最高位是1或0表示两个字节和一个字节。

Unicode不完美,这里就有三个问题,一个是,我们已经知道,英文字母只用一个字节表示就够了,第二个问题是如何才能区别Unicode和ASCII?计算机怎么知道两个字节表示一个符号,而不是分别表示两个符号呢?第三个,如果和GBK等双字节编码方式一样,用最高位是1或0表示两个字节和一个字节,就少了很多值无法用于表示字符,不够表示所有字符。Unicode在很长一段时间内无法推广,直到互联网的出现。

面向传输的众多 UTF(UCS Transfer Format)标准出现了,顾名思义,UTF-8就是每次8个位传输数据,而UTF-16就是每次16个位。这是为传输而设计的编码,并使编码无国界,这样就可以显示全世界上所有文化的字符了。

Unicode只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的Unicode编码是UTF-8和UTF-16。

Unicode编码的实现方案 (UTF-8) :

让两个字节的Unicode字符集编码为变长的编码方式 : 当一个字符需要用11位存储时,我们采用两个字节来表示,用第一个字节的前三位和第二个字节的前两位来标识出这是一个使用两个字节表示的字符,其余的位放该字符真正的编码,这样识别出来以后取对应位上的数值拼凑在一起就可以解码为该字符,其余情况道理相同。

ANSI编码,通常指的是平台的默认编码,例如英文操作系统中是ISO-8859-1,中文系统是GBK。

Unicode字符集只是定义了字符的集合和唯一编号,Unicode编码,则是对UTF-8、UCS-2/UTF-16等具体编码方案的统称而已,并不是具体的编码方案。

六、标准输入、输出流

System.in和System.out分别代表了系统标准的输入和输出设备

默认输入设备是:键盘,输出设备是:显示器

  • System.in的类型是InputStream,标准的输入流,默认从键盘输入

  • System.out的类型是PrintStream,其是OutputStream的子类FilterOutputStream 的子类,标准的输出流,默认从控制台输出

重定向:通过System类的setIn,setOut方法对默认设备进行改变。

  • public static void setIn(InputStream in)

  • public static void setOut(PrintStream out)

练习题 : 从键盘输入字符串,要求将读取到的整行字符串转成大写输出。然后继续进行输入操作,直至当输入“e”或者“exit”时,退出程序。

方法一 : 使用Scanner实现,调用next()方法返回一个字符串。

方法二 : 使用System.in实现,System.in --- InputStreamReader --- BufferedReader --- 调用readLine()方法

public static void main(String[] args) {

    InputStreamReader isr = new InputStreamReader(System.in);
    BufferedReader br = new BufferedReader(isr);

    try {
        while (true){
            
            System.out.print("请输入字符串 : ");
            String data = br.readLine();

            if ("e".equalsIgnoreCase(data) || "exit".equalsIgnoreCase(data)){
                System.out.println("程序退出 ! ");
                break;
            } else{
                String upperCase = data.toUpperCase();
                System.out.println(upperCase);
            }

        }

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

}

说明 : InputStreamReader的构造器中放的System.in还是一个流,只不过以前的终点是文件,现在变成了键盘,从键盘中读入数据,而不是从文件中读入数据了。

练习题 : Create a program named MyInput.java: Contain the methods for reading int, double, float, boolean, short, byte and String values from the keyboard.

package com.atguigu.exer.stream;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class MyInput {
    //Read a string from the keyboard
    public static String readString(){
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        //Declare and initialize the string
        String string = "";

        //Get the string from the keyboard
        try {
            string = br.readLine();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                br.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        //Return the string obtained from keyboard
        return string;
    }

    public static int readInt(){
        return Integer.parseInt(readString());
    }

    public static double readDouble(){
        return Double.parseDouble(readString());
    }

    public static byte readByte(){
        return Byte.parseByte(readString());
    }

    public static long readLong(){
        return Long.parseLong(readString());
    }

    public static float readFloat(){
        return Float.parseFloat(readString());
    }

    public static short readShort(){
        return Short.parseShort(readString());
    }
}

七、打印流

打印流 : PrintStream 和 PrintWriter 实现将基本数据类型的数据格式转化为字符串输出,提供了一系列重载的print()和println()方法。

System.out的类型是PrintStream,其是OutputStream的子类FilterOutputStream 的子类,标准的输出流,默认从控制台输出。

System.out.println();

System.out就是一个打印流,打印流对象里有许多重载的println、print方法。

练习 : 打印ASCII码值,使用打印流打印到文件中保存。

@Test
public void test02(){
    PrintStream ps = null;
    try {
        FileOutputStream fos = new FileOutputStream(new File("printStreamTest.txt"));
        // 创建打印输出流,设置为自动刷新模式(写入换行符或字节 '\n' 时都会刷新输出缓冲区)
        ps = new PrintStream(fos, true);//本质是读出到文件
        
        if (ps != null) {// 把标准输出流(控制台输出)改成文件
            System.setOut(ps);//重定向 public static void setOut(PrintStream 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();
        } }

}

八、数据流

DataInputStream 和 DataOutputStream

作用 : 用于读取或写出基本数据类型的变量或字符串

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即可。

练习 : 将内存中的字符串、基本数据类型的变量写出到文件中。

/*
练习 : 将内存中的字符串、基本数据类型的变量写出到文件中。
 */
@Test
public void testDataOutputStream(){
    DataOutputStream dos = null;
    try {
        dos = new DataOutputStream(new FileOutputStream("data.txt"));

        dos.writeUTF("侯德阳");
        dos.flush();//显示的刷新 将缓冲区中的数据读入文件
        dos.write(20);
        dos.flush();
        dos.writeBoolean(true);
        dos.flush();

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

}

说明 : 这时的data文件不是用来打开查看的,会出现乱码。该文件是用来当程序下次启动时使用DataInputStream将数据重新读入程序中的。每次执行read方法最好显示调用flush方法刷新一下,让数据从缓冲区写入文件。

练习 : 将文件中存储的基本数据类型变量和字符串读取到内存中,保存在变量里。

/*
练习 : 将文件中存储的基本数据类型变量和字符串读取到内存中,保存在变量里。
 */
@Test
public void testDataInputStream(){

    DataInputStream dis = null;
    try {
        dis = new DataInputStream(new FileInputStream("data.txt"));

        String name = dis.readUTF();
        int age = dis.read();
        boolean isMale = dis.readBoolean();

        System.out.println("name = " + name);
        System.out.println("age = " + age);
        System.out.println("isMale = " + isMale);

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

说明 : 读取不同类型的数据的顺序要与当初写入文件,保存数据的顺序一致,并且要拿相同的方法来写入内存(read和write互换)

九、对象流

对象流 : ObjectInputStream和OjbectOutputSteam

作用 : 用于存储和读取基本数据类型数据或对象的处理流。它的强大之处就是可以把Java中的对象写入到数据源中,也能把对象从数据源中还原回来。

  • 序列化 : 用ObjectOutputStream类保存基本类型数据或对象的机制

  • 反序列化 : 用ObjectInputStream类读取基本类型数据或对象的机制

注意 : ObjectOutputStream和ObjectInputStream不能序列化static和transient修饰的成员变量,如果类内有static和transient的成员变量,整个类是可以序列化的,但是反序列化后得到的static和transient修饰的属性将会是默认值,代表其属性没有被序列化。

static : static修饰的属性是归于类所有的,不可以被序列化

transient : 表示该属性不能被序列化,如果不想某个属性被序列化传输,则可以使用此关键字。

1. 对象的序列化

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

序列化的好处在于可将任何实现了Serializable接口的对象转化为字节数据,使其在保存和传输时可被还原

序列化是 RMI(Remote Method Invoke – 远程方法调用)过程的参数和返回值都必须实现的机制,而 RMI 是 JavaEE 的基础。因此序列化机制是JavaEE 平台的基础。

如果需要让某个对象支持序列化机制,则必须让对象所属的类及其属性是可序列化的,为了让某个类是可序列化的,该类必须实现如下两个接口之一。否则,会抛出NotSerializableException异常

  • Serializable

  • Externalizable

凡是实现Serializable接口的类都有一个表示序列化版本标识符的静态变量 :

private static final long serialVersionUID;

serialVersionUID用来表明的不同版本间的兼容性。简言之,其目的是以序列化对象进行版本控制,有关各版本反序列化时是否兼容。也就是标识这个类,反序列化时通过这个uid来判断是哪一个类的对象。

如果类没有显示定义这个静态常量,它的值是Java运行时环境根据类的内部细节自动生成的。若类的实例变量做了修改,serialVersionUID 可能发生变化。故建议,显式声明。

简单来说,Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。(InvalidCastException)

2. 使用对象流序列化对象

序列化过程 : 将内存中的java对象保存到磁盘中或通过网络传输出去,使用ObjectOutputStream实现

/*
序列化过程 : 将内存中的java对象保存到磁盘中或通过网络传输出去
使用ObjectOutputStream实现
 */
@Test
public void testObjectOutputStream(){
    //造一个String对象来测试
    String str = new String("我爱北京天安门 ! ");

    ObjectOutputStream oos = null;
    try {
        //1. 造文件造流
        oos = new ObjectOutputStream(new FileOutputStream("object.bat"));
        //2. 写出操作
        oos.writeObject(str);
        oos.flush();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        //3. 关闭流
        if (oos != null) {
            try {
                oos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

3. 使用对象流反序列化对象

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

/*
反序列化 : 将磁盘文件中的对象还原为内存中的一个java对象
使用ObjectInputStream来实现
 */
@Test
public void testObjectInputStream(){
    ObjectInputStream ois = null;
    try {
        ois = new ObjectInputStream(new FileInputStream("object.bat"));

        Object object = ois.readObject();
        String str = (String) object;
        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();
            }
        }
    }
}

4. 自定义类的对象流操作

自定义类实现序列化和反序列化的操作 :

要想一个Java对象是可序列化的,需要满足相应的要求

  • 需要实现接口 : Serializable (标识类,标识这个类是可以进行序列化的)

  • 需要当前类提供一个全局常量 : serialVersionUID

  • 除了当前Person类需要实现serializable接口之外,还必须保证其内部所有属性也必须是可序列化的。(默认情况下,基本数据类型可序列化)

注意 : 如果可序列化类没有显式声明 serialVersionUID,则序列化运行时将根据类的各个方面为该类计算默认的 serialVersionUID 值。但是,强烈建议所有可序列化的类都显式声明 serialVersionUID 值,因为默认的 serialVersionUID 计算对类的细节高度敏感,这些细节可能因编译器实现而异,因此可能在反序列化期间导致意外的 InvalidClassExceptions。因此,为了保证在不同的 java 编译器实现中具有一致的 serialVersionUID 值,可序列化的类必须声明一个显式的 serialVersionUID 值。还强烈建议显式 serialVersionUID 声明尽可能使用 private 修饰符,因为此类声明仅适用于立即声明的类——serialVersionUID 字段不能用作继承成员。数组类不能显式声明 serialVersionUID,因此它们始终具有默认计算值,但数组类无需匹配 serialVersionUID 值。

十、随机存取文件流

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

RandomAccessFile 类支持 “随机访问” 的方式,程序可以直接跳到文件的任意地方来读、写文件 :

  • 支持只访问文件的部分内容

  • 可以向已存在的文件后追加内容

RandomAccessFile 对象包含一个记录指针,用以标示当前读写处的位置。RandomAccessFile 类对象可以自由移动记录指针 :

  • long getFilePointer():获取文件记录指针的当前位置

  • void seek(long pos):将文件记录指针定位到 pos 位置

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

  • r : 以只读方式打开

  • rw : 打开以便读取和写入;数据不会立即写入硬盘

  • rwd : 打开以便读取和写入;同步文件内容的更新,数据会立即写入硬盘

  • rws : 打开以便读取和写入;同步文件内容和元数据的更新,数据会立即写入硬盘

如果模式为只读r。则不会创建文件,而是会去读取一个已经存在的文件,如果读取的文件不存在则会出现异常。 如果模式为rw读写。如果文件不存在则会去创建文件,如果存在则不会创建。

练习 : 使用RandomAccessFile实现对图片的复制

/*
使用随机存取文件流实现对图片的复制
 */
@Test
public void testInput(){
    RandomAccessFile rafr = null;
    RandomAccessFile rafrw = null;
    try {
        //创建一个只读的流
        rafr = new RandomAccessFile("Java高级.jpg", "r");
        //创建一个可读可写的流
        rafrw = new RandomAccessFile("RomdomCopyJava高级.jpg", "rw");

        //复制操作
        byte[] buffer = new byte[1024];
        int len;
        while ((len = rafr.read(buffer)) != -1){
            rafrw.write(buffer, 0, len);
        }
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (rafrw != null) {
            try {
                rafrw.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (rafr != null) {
            try {
                rafr.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

练习 : 用RandomAccessFile来操作文本文件

执行程序之前hello.txt文件内容 :

helloworld123侯德阳
@Test
public void testRomdomAccessFile(){

    RandomAccessFile raf = null;
    try {
        raf = new RandomAccessFile("hello.txt", "rw");

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

执行程序之后hello.txt文件内容 :

xyzloworld123侯德阳

说明 : 如果RandomAccessFile作为输出流时,写出到的文件如果不存在,则在执行过程中自动创建。如果写出到的文件存在,则会对原有文件内容进行覆盖。(默认情况下,从头覆盖)

练习 : 使用RandomAccessFile实现对文本文件数据的插入效果

执行程序之前hello.txt文件内容 :

helloworld123侯德阳
/*
使用RandomAccessFile实现对文本文件数据的插入效果
 */
@Test
public void testRandomAccessFileInsert(){

    RandomAccessFile raf = null;
    try {
        raf = new RandomAccessFile("hello.txt", "rw");

        //将指针定位到要插入数据的位置
        raf.seek(3);
        //造一个StringBuilder对象实现对原有数据的保存
        StringBuilder builder = new StringBuilder((int) new File("hello.txt").length());
        byte[] buffer = new byte[10];
        int len;
        while ((len = raf.read(buffer)) != -1){
            //将原有的数据保存
            builder.append(Arrays.toString(buffer));
        }
        //将指针调回要插入数据的位置
        raf.seek(3);

        raf.write("xyz".getBytes());
        //将StringBuilder中的数据写回文件中
        raf.write(builder.toString().getBytes());

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

执行程序之前hello.txt文件内容 :

helxyzloworld123侯德阳

应用举例 :

我们可以用RandomAccessFile这个类,来实现一个多线程断点下载的功能,用过下载工具的朋友们都知道,下载前都会建立两个临时文件,一个是与被下载文件大小相同的空文件,另一个是记录文件指针的位置文件,每次暂停的时候,都会保存上一次的指针,然后断点下载的时候,会继续从上一次的地方下载,从而实现断点下载或上传的功能,有兴趣的朋友们可以自己实现下。

十一、NIO2.0

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。

|-----java.nio.channels.Channel
	|-----FileChannel:处理本地文件
	|-----SocketChannel:TCP网络编程的客户端的Channel
	|-----ServerSocketChannel:TCP网络编程的服务器端的Channel
	|-----DatagramChannel:UDP网络编程中发送端和接收端的Channel

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

早期的Java只提供了一个File类来访问文件系统,但File类的功能比较有限,所提供的方法性能也不高。而且,大多数方法在出错时仅返回失败,并不会提供异常信息。

NIO. 2为了弥补这种不足,引入了Path接口,代表一个平台无关的平台路径,描述了目录结构中文件的位置。Path可以看成是File类的升级版本,实际引用的资源也可以不存在。

//以前的IO操作
File file = new File("index.html");
//Java7中
Path path = Paths.get("index.html");

同时,NIO.2在java.nio.file包下还提供了Files、Paths工具类,Files包含了大量静态的工具方法来操作文件;Paths则包含了两个返回Path的静态工厂方法。

  • static Path get(String first, String … more) : 用于将多个字符串串连成路径

  • static Path get(URI uri) : 返回指定uri对应的Path路径

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

侯静川

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值