java学习笔记 IO

时间:2023-02-25 21:21:20

——最近在学习整理java IO,主要来自于《疯狂java讲义》

一,File类

1.File访问文件和目录

File类是java.io包下代表与平台无关的文件和目录,如果希望在程序中操作文件和目录,都可以通过File类来完成。不管是文件还是目录都是使用File来操作的,File能新建、删除、重命名文件和目录,但是不能访问文件内容本身,如果需要访问文件内容本身,需要使用输入输出流。
File类提供了很多方法操作文件和目录,可在帮助文档中查看。

public class FileTest
{
    public static void main(String[] args)
        throws IOException
    {
        // 以当前路径来创建一个File对象
        File file = new File(".");
        // 直接获取文件名,输出一点
        System.out.println(file.getName());
        // 获取相对路径的父路径可能出错,下面代码输出null
        System.out.println(file.getParent());
        // 获取绝对路径
        System.out.println(file.getAbsoluteFile());
        // 获取上一级路径
        System.out.println(file.getAbsoluteFile().getParent());
        // 在当前路径下创建一个临时文件
        File tmpFile = File.createTempFile("aaa", ".txt", file);
        // 指定当JVM退出时删除该文件
        tmpFile.deleteOnExit();
        // 以系统当前时间作为新文件名来创建新文件
        File newFile = new File(System.currentTimeMillis() + "");
        System.out.println("newFile对象是否存在:" + newFile.exists());
        // 以指定newFile对象来创建一个文件
        newFile.createNewFile();
        // 以newFile对象来创建一个目录,因为newFile已经存在,
        // 所以下面方法返回false,即无法创建该目录
        newFile.mkdir();
        // 使用list()方法来列出当前路径下的所有文件和路径
        String[] fileList = file.list();
        System.out.println("====当前路径下所有文件和路径如下====");
        for (String fileName : fileList)
        {
            System.out.println(fileName);
        }
        // listRoots()静态方法列出所有的磁盘根路径。
        File[] roots = File.listRoots();
        System.out.println("====系统所有根路径如下====");
        for (File root : roots)
        {
            System.out.println(root);
        }
    }
}

运行上面程序,可以看到程序列出当前路径的所有文件和路径时,列出了程序创建的临时文件,但程序运行结束后,aaa.txt临时文件并不存在,因为程序指定虚拟机退出时自动删除该文件。
注意:Windows路径分隔符使用反斜线(\),而java程序中的犯些小表示转义字符,所以需要再Windows的路径下包括反斜线,应该使用两条,eg:F:\abc\test.txt

2.文件过滤器

File类的list()方法接收一个FilenameFilter参数,通过该参数可以只列出符合条件的文件。FilenameFilter接口里包含了一个accept(File dir, String name)方法,该方法依次对指定File的所有子目录或者文件进行迭代,如果该方法返回true,则list()方法会列出该子目录或者文件

public class FilenameFilterTest {
    public static void main(String[] args)
    {
        File file = new File(".");
        // 使用Lambda表达式(目标类型为FilenameFilter)实现文件过滤器。
        // 如果文件名以.java结尾,或者文件对应一个路径,返回true
        String[] nameList = file.list((dir, name) -> name.endsWith(".java")
            || new File(name).isDirectory());
        for(String name : nameList)
        {
            System.out.println(name);
        }
    }

}
/* .settings bin src */

运行上面程序可以看出,当前路径下所有的.java文件以及文件夹被列出

二,IO流概述

在java中把不同的输入/输出源(键盘、文件、网路连接等)抽象表述为“流”(stream),通过流的方式允许java程序使用相同的方式来访问不同的输入/输出源。java把所有传统的流类型(类或抽象类)都放在java.io包中,因为java提供了这种IO流的抽象,所以开发者可以使用一致的IO代码去读写不同的IO流节点。

1.输入流和输出流

输入流:只能从中读取数据,而不能向其写入数据,主要由InputStream和Reader作为基类
输出流:只能向其写入数据,而不能从中读取数据,主要由OutputStream和Writer作为基类
是按照流的流向来分,涉及一个方向问题,划分输入/输出流是从程序运行所在内存的角度来考虑的,如图所示的数据流向,数据从服务器通过网络流向客户端,在这种情况下,Server端的内存负责将数据输出到网络里,因此Server端的程序使用输出流;Client端的内存负责从网络里读取数据,因此Client端的程序使用输入流。
java学习笔记 IO

2.字节流和字符流

用法几乎完全一样,区别在于字节流和字符流操作的数据单元不同:
字节流:操作的数据单元是8位的字节,主要由InputStream和OutputStream作为基类
字符流:操作的数据单元是16位的字符,主要由Reader和Writer作为基类

3.节点流和处理流

从一个特定的IO设备(如磁盘、网络)读写数据的流称为节点流,节点流也被称为低级流,如图所示,当使用节点流进行输入输出时,程序直接连接到实际的数据源,和实际的输入输出节点连接。
java学习笔记 IO

处理流则用于对于一个已经存在的流进行连接或封装,通过封装后的流来实现数据读写功能。处理流也被称为高级流。如图所示,使用处理流的一个明显好处是,只要使用相同的处理流,程序就可以采用完全相同的输入输出代码来访问不同的数据源,随着处理流所包装节点流的变化,程序实际所访问的数据源也相应地发生变化。
java学习笔记 IO

三,字节流和字符流

1.InputStream和Reader

InputStream和Reader是所有输入流的抽象基类,本身并不能创建实例来执行输入,但它们将成为所有输入流的模板,它们的方法是所有输入流都可以使用的方法。
InputStream包含如下三个方法:
(1)int read():从输入流中读取单个字节,返回所读取的字节数据(字节数据可直接转换为int类型),读到文件末尾返回-1,可以通过read方法的返回值是否是-1控制循环次数

public class FileInputStreamTest {
    private static void showContent(String path) throws IOException {
        // 打开流
        FileInputStream fis = new FileInputStream(path);

        int len;
        while ((len = fis.read()) != -1) {
            System.out.print((char) len);
        }
        // 使用完关闭流
        fis.close();
    }

}

(2)int read(byte[] b):从输入流中最多读取b.length个字节数据,并将其存储在字节数组b中,返回实际读取的字节数。使用缓冲区,读一次处理一次,一次性操作数组,可以提高效率
(3)int read(byte[] b, int off, int len):从输入流中最多读取len个字节数据,并将其存储在数组b中,放入数组b中时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字节数

Reader包含如下三个方法:
(1)int read():从输入流中读取单个字符,返回所读取的字符数据(字符数据可直接转换为int类型)
(2)int read(char[] cbuf):从输入流中最多读取cbuf.length个字符数据,并将其存储在字节数组cbuf中,返回实际读取的字符数
(3)int read(char[] cbuf, int off, int len):从输入流中最多读取len个字符的数据,并将其存储在字符数组cbuf中,放入数组cbuf中时,并不是从数组起点开始,而是从off位置开始,返回实际读取的字符数
对比可以看出,两个基类的功能基本是一样的。

InputStream和Reader都是抽象类,本身不能创建实例,但它们分别有一个用于读取文件的输入流:FileInputStream和FileReader,它们都是节点流——会直接和指定文件关联,eg:

public class FileInputStreamTest {
    public static void main(String[] args) throws IOException
    {
        // 创建字节输入流
        FileInputStream fis = new FileInputStream(
            "D:\\eclipse\\workespaces\\CrazyProject\\src\\File\\FileInputStreamTest.java");
        // 创建一个长度为1024的“竹筒”
        byte[] bbuf = new byte[1024];
        // 用于保存实际读取的字节数
        int hasRead = 0;
        // 使用循环来重复“取水”过程
        while ((hasRead = fis.read(bbuf)) > 0 )
        {
            // 取出“竹筒”中水滴(字节),将字节数组转换成字符串输入!
            System.out.print(new String(bbuf , 0 , hasRead ));
        }
        // 关闭文件输入流,放在finally块里更安全
        fis.close();
    }

}

运行上面程序,则会输出上面程序源代码

上面程序使用fis.close()关闭该文件输入流,和JDBC编程一样,程序打开的文件IO资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以应该显式关闭文件IO资源。java7改写了所有IO资源类,它们都实现了AutoCloseable接口,因此可通过自动关闭资源的try语句来关闭这些IO流,eg:

public class FileReaderTest
{
    public static void main(String[] args)
    {
        try(
            // 创建字符输入流
            FileReader fr = new FileReader("FileReaderTest.java"))
        {
            // 创建一个长度为32的“竹筒”
            char[] cbuf = new char[32];
            // 用于保存实际读取的字符数
            int hasRead = 0;
            // 使用循环来重复“取水”过程
            while ((hasRead = fr.read(cbuf)) > 0 )
            {
                // 取出“竹筒”中水滴(字符),将字符数组转换成字符串输入!
                System.out.print(new String(cbuf , 0 , hasRead));
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

上述程序最后使用了自动关闭资源的try语句来关闭文件输入流,这样可以保证输入流一定会被关闭。

2.OutputStream和Writer

OutputStream和Writer也非常相似,都提供了如下三个方法:
(1)void write(int c):将指定的字节/字符输出到输出流中,c既可以代表字节,也可以代表字符
(2)void write(byte[] / char[] buf):将字节数组/字符数组中的数据输出到指定输出流中
(3)void write(byte[] / char[] buf, int off, int len):将字节数组/字符数组从off位置开始,长度为len的字节/字符输出到输出流中
因为字符流直接以字符作为操作单位,Writer可以用字符串代替字符数组,即以String对象作为参数,Writer还包含如下两个方法:
void write(String str):将str字符串里包含的字符输出到指定输出流中
void write(String str, int off, int len):将str字符串里从off位置开始,长度为len的字符输出到指定输出流中
eg:使用FileInputStream执行输入,并使用FileOutputStream执行输出,实现复制

public class FileOutputStreamTest
{
    public static void main(String[] args)
    {
        try(
            // 创建字节输入流
            FileInputStream fis = new FileInputStream(
                "FileOutputStreamTest.java");
            // 创建字节输出流
            FileOutputStream fos = new FileOutputStream("newFile.txt"))
        {
            byte[] bbuf = new byte[32];
            int hasRead = 0;
            // 循环从输入流中取出数据
            while ((hasRead = fis.read(bbuf)) > 0 )
            {
                // 每读取一次,即写入文件输出流,读了多少,就写多少。
                fos.write(bbuf , 0 , hasRead);
            }
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }
}

注意:使用java的IO流执行输出时,不要忘记关闭输出流,关闭输出流除了保证流的物理资源被回收之外,还可以将输出流缓冲区中的数据flush到物理节点里,因为在执行close方法之前,自动执行输出流的flush方法。
如果希望直接输出字符串内容,使用Writer会有更好的效果。

public class FileWriterTest
{
    public static void main(String[] args)
    {
        try(
            FileWriter fw = new FileWriter("poem.txt"))
        {
            fw.write("锦瑟 - 李商隐\r\n");
            fw.write("锦瑟无端五十弦,一弦一柱思华年。\r\n");
            fw.write("庄生晓梦迷蝴蝶,望帝春心托杜鹃。\r\n");
            fw.write("沧海月明珠有泪,蓝田日暖玉生烟。\r\n");
            fw.write("此情可待成追忆,只是当时已惘然。\r\n");
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }
}

四,输入输出流

1.处理流的用法

处理流,它可以隐藏底层设备上节点流的差异,并对外提供更加方便的输入输出方法,让程序员只关心高级流的操作。
典型思路是:使用处理流包装节点流,程序通过处理流来执行输入输出功能,让节点流与底层的IO设备、文件交互。实际识别处理流时,只要流的构造器参数不是一个物理节点,而是已经存在的流,那么这种流就一定是处理流;而所有节点流都是直接以物理IO节点作为构造器参数的。
eg:使用PrintStream处理流来包装OutputStream

public class PrintStreamTest
{
    public static void main(String[] args)
    {
        try(
            FileOutputStream fos = new FileOutputStream("test.txt");
            PrintStream ps = new PrintStream(fos))
        {
            // 使用PrintStream执行输出
            ps.println("普通字符串");
            // 直接使用PrintStream输出对象
            ps.println(new PrintStreamTest());
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }
}

注意:使用处理流包装底层节点流,关闭输入输出流资源时,只要关闭最上层的处理流即可,系统会自动关闭被该处理流包装的节点流。

2.输入输出流体系
java为了实现更好的设计,它把IO流按功能分成了许多类,如下表所示:
java学习笔记 IO

上表仅仅总结了输入输出流体系中位于java.io包下的流,还有一些诸如AudioInputStream、CipherInputStream、DeflaterInputStream、ZipInputStream等具有访问音频文件、加密解密、压缩解压缩等功能的字节流,它们具有特殊的功能,位于JDK的其他包下。
下面程序示范了使用字符串作为物理节点的字符输入输出流的用法,在创建StringReader和StringWriter对象时传入的是字符串节点而不是文本节点,由于String是不可变的字符串对象,所以StringWrite使用StringBuffer作为输出节点。

public class StringNodeTest {
    public static void main(String [] args){
        String src = "从明天起,做一个幸福的人\n"
                + "喂马,劈柴,周游世界\n"
                + "从明天起,关心粮食和蔬菜\n"
                + "我有一所房子,面朝大海,春暖花开\n"
                + "从明天起,和每一个亲人通信\n"
                + "告诉他们我的幸福\n";

        char[] buffer=new char[32];
        int hasRead=0;
        try(
                StringReader sr=new StringReader(src))
        {
            while((hasRead=sr.read(buffer))>0){
                System.out.println(new String(buffer,0,hasRead));
            }
        }catch(IOException ioe){
            ioe.printStackTrace();
        }

        try(
                // 创建StringWriter时,实际上以一个StringBuffer作为输出节点
                // 下面指定的20就是StringBuffer的初始长度
                StringWriter sw = new StringWriter())
            {
                // 调用StringWriter的方法执行输出
                sw.write("有一个美丽的新世界,\n");
                sw.write("她在远方等我,\n");
                sw.write("哪里有天真的孩子,\n");
                sw.write("还有姑娘的酒窝\n");
                System.out.println("----下面是sw的字符串节点里的内容----");
                // 使用toString()方法返回StringWriter的字符串节点的内容
                System.out.println(sw.toString());
            }
            catch (IOException ex)
            {
                ex.printStackTrace();
            }
    }

}

3.转换流

输入输出体系提供了两个转换流,用于实现将字节流转换成字符流,其中InputStreamReader将字节输入流转换成字符输入流,OutputStreamWriter将字节输出流转换成字符输出流。
Eg:以获取键盘输入为例,java使用System.in代表标准输入,即键盘输入,但是这个标准输入是InputStream的实例,而键盘输入是文本内容,所以可以使用InputStreamReader将其转换成字符输入流,普通的Reader读取输入内容时依然不太方便,可以将普通Reader再次包装成BufferedReader,利用BufferedReader的readLine()方法可以一次读取一行内容

package File;
import java.io.*;

public class KeyinTest {
    public static void main(String[] args)
    {
        try(
            // 将Sytem.in对象转换成Reader对象
            InputStreamReader reader = new InputStreamReader(System.in);
            // 将普通Reader包装成BufferedReader
            BufferedReader br = new BufferedReader(reader))
        {
            String line = null;
            // 采用循环方式来一行一行的读取
            while ((line = br.readLine()) != null)
            {
                // 如果读取的字符串为"exit",程序退出
                if (line.equals("exit"))
                {
                    System.exit(1);
                }
                // 打印读取的内容
                System.out.println("输入内容为:" + line);
            }
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }

}
/* I love Autumn 输入内容为:I love Autumn but Winter is also beautiful 输入内容为:but Winter is also beautiful */

4.推回输入流

在输入输出体系中,有两个特殊的流,PushbackInputStream和PushbackReader,这两个推回输入流都带有一个推回缓冲区,当程序调用这两个推回输入流的unread()方法时,系统会把指定数组的内容推回到该缓冲区里推回输入流每次调用read()方法时总是先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后,但还没有装满read所需的数组时才会从原输入流中读取。
当创建一个PushbackInputStream和PushbackReader时,需要指定推回缓冲区的大小,默认为1,如果程序中推回到推回缓冲区的内容超出了推回缓冲区的大小,将会引发Pushback buffer overflow的IOException异常。
Eg:找出程序中“new PushbackReader”字符串,找到后,打印出目标字符串之前的内容

public class PushbackTest {
    public static void main(String[] args)
    {
        try(
            // 创建一个PushbackReader对象,指定推回缓冲区的长度为64
            PushbackReader pr = new PushbackReader(new FileReader(
                "PushbackTest.java") , 64))
        {
            char[] buf = new char[32];
            // 用以保存上次读取的字符串内容
            String lastContent = "";
            int hasRead = 0;
            // 循环读取文件内容
            while ((hasRead = pr.read(buf)) > 0)
            {
                // 将读取的内容转换成字符串
                String content = new String(buf , 0 , hasRead);
                int targetIndex = 0;
                // 将上次读取的字符串和本次读取的字符串拼起来,
                // 查看是否包含目标字符串, 如果包含目标字符串
                if ((targetIndex = (lastContent + content)
                    .indexOf("new PushbackReader")) > 0)
                {
                    // 将本次内容和上次内容一起推回缓冲区
                    pr.unread((lastContent + content).toCharArray());
                    // 重新定义一个长度为targetIndex的char数组
                    if(targetIndex > 32)
                    {
                        buf = new char[targetIndex];
                    }
                    // 再次读取指定长度的内容(就是目标字符串之前的内容)
                    pr.read(buf , 0 , targetIndex);
                    // 打印读取的内容
                    System.out.print(new String(buf , 0 ,targetIndex));
                    System.exit(0);
                }
                else
                {
                    // 打印上次读取的内容
                    System.out.print(lastContent);
                    // 将本次内容设为上次读取的内容
                    lastContent = content;
                }
            }
        }
        catch (IOException ioe)
        {
            ioe.printStackTrace();
        }
    }

}

5.重定向标准输入输出

java标准输入输出分别通过System.in和System.out代表,在System类提供了如下三个重定向标准输入输出方法
(1)static void setErr(PrintStream err):重定向标准错误输出流
(2)static void setIn(InputStream in):重定向标准输入流
(3)static void setOut(PrintStream out):重定向标准输出流
Eg:通过重定向标准输出流,将System.out的输出重定向到文件输出,而不是屏幕上输出

package File;
import java.io.*;

public class ReadirectOut {
    public static void main(String [] args){
        try(
                PrintStream ps=new PrintStream(new FileOutputStream("out.txt"))
                )
        {
            // 将标准输出重定向到ps输出流
            System.setOut(ps);
            // 向标准输出输出一个字符串
            System.out.println("普通字符串");
            // 向标准输出输出一个对象
            System.out.println(new ReadirectOut());
        }catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }

}
/* out.txt中的输出内容: 普通字符串 File.ReadirectOut@15db9742 */

6.RandomAccessFile

RandomAccessFile是java输入输出流体系中功能最丰富的文件内容访问类,它提供了众多方法访问文件内容,既可以读取文件内容,也可以向文件输出数据。它支持随机访问的方式,程序可以直接跳到文件的任意地方来读写数据。
RandomAccessFile的方法虽多,但有一个最大的局限,就是只能读写文件,不能读写其他IO节点。
RandomAccessFile包含了一个记录指针,用来标识当前读写出的位置,可以*移动该记录指正,既可以向前移动,也可以向后移动,如下两个方法:
(1)long getFilePointer():放回文件记录指针的当前位置
(2)void seek(long pos):将文件记录指针点位到pos位置
RandomAccessFile可以读写文件,所以包含了完全类似于InputStream的三个read方法和OutputStream的三个write方法,并且用法一致,除此之外,RandomAccessFile还包含了一系列的readXxx()和writeXxx()完成输入输出。
RandomAccessFile有两个构造器,基本相同,只是指定文件的形式不同而已—-一个使用String参数指定文件名,一个使用File参数指定文件本身。
创建RandomAccessFile对象时需要指定一个mode参数,该参数指定RandomAccessFile的访问模式,有4个值:
(1)“r”:只读方式打开指定文件
(2)“rw”:读写方式打开指定文件
(3)“rws”:读写方式打开指定文件,与“rw”相比,要求对文件的内容或元数据的每个更新都同步写入到底层存储设备
(4)“rwd”:读写方式打开指定文件,与“rw”相比,要求对文件内容的每个更新都同步写入到底层存储设备

Eg:访问指定的中间部分数据

public class RandomAccessFileTest
{
    public static void main(String[] args)
    {
        try(
        //只读方式打开文件
            RandomAccessFile raf =  new RandomAccessFile(
                "RandomAccessFileTest.java" , "r"))
        {
            // 获取RandomAccessFile对象文件指针的位置,初始位置是0
            System.out.println("RandomAccessFile的文件指针的初始位置:"
                + raf.getFilePointer());
            // 移动raf的文件记录指针的位置
            raf.seek(300);
            byte[] bbuf = new byte[1024];
            // 用于保存实际读取的字节数
            int hasRead = 0;
            // 使用循环来重复“取水”过程
            while ((hasRead = raf.read(bbuf)) > 0 )
            {
                // 取出“竹筒”中水滴(字节),将字节数组转换成字符串输入!
                System.out.print(new String(bbuf , 0 , hasRead ));
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

五,对象的序列化

1.概述

对象序列化的目标是将对象保存到磁盘中,或允许在网络中直接传输对象。对象序列化机制允许把内存中的java对象转换成平台无关的二进制流,从而允许把这种二进制流持久地保存在磁盘上,通过网络将这种二进制流传输到另一个网路节点。其他程序一旦获得了这种二进制流(无论是从磁盘中获取的还是通过网络获取),都可以将这种二进制流恢复成原来的java对象。

如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的(Serializable),为了让某个类是可序列化的,该类必须实现如下两个接口之一:Serializable、Externalizable。java的很多类已经实现了Serializable,该接口是一个标记接口,实现该接口无须实现任何方法,它只是表明该类的实例是可序列化的。

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

可以通过如下两个步骤来序列化对象
(1)创建一个ObjectOutputStream,这个输出流是一个处理流,所以必须建立在其他节点流的基础上
(2)调用ObjectOutputStream对象的writeObject()方法输出可序列化对象
Eg:定义一个普通Person类,实现Serializable接口,使用ObjectOutputStream将一个Person对象写入磁盘文件

public class Person implements java.io.Serializable{
    private String name;
    private int age;
    // 注意此处没有提供无参数的构造器!
    public Person(String name , int age)
    {
        System.out.println("有参数的构造器");
        this.name = name;
        this.age = age;
    }
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // age的setter和getter方法
    public void setAge(int age)
    {
        this.age = age;
    }
    public int getAge()
    {
        return this.age;
    }

}
public class WriteObject {
    public static void main(String[] args)
    {
        try(
            // 创建一个ObjectOutputStream输出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("object.txt")))
        {
            Person per = new Person("孙悟空", 500);
            // 将per对象写入输出流
            oos.writeObject(per);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }

}

如果希望从二进制流中恢复java对象,需要反序列化,步骤如下:
(1)创建一个ObjectInputStream输入流,这是一个处理流,所以必须建立在其他节点流的基础之上
(2)调用ObjectInputStream对象的readObject()方法读取流中对象,该方法返回一个Object类型的java对象,如果程序知道该java对象的类型,则可以将该对象强制类型转化为真实的类型
Eg:从刚刚新建的object.txt文件中读取Person对象

public class ReadObject
{
    public static void main(String[] args)
    {
        try(
            // 创建一个ObjectInputStream输入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("object.txt")))
        {
            // 从输入流中读取一个Java对象,并将其强制类型转换为Person类
            Person p = (Person)ois.readObject();
            System.out.println("名字为:" + p.getName()
                + "\n年龄为:" + p.getAge());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

注意:反序列化读取的是java对象的数据,而不是java类,因此采用反序列化恢复java对象时,必须提供该java对象所属类的class文件,否则会引发ClassNotFoundException异常。
如果使用序列化机制向文件中写入了多个java对象,使用反序列化机制恢复对象时必须按实际写入的顺序来读取。

3.对象引用的序列化
例子:Teacher类持有一个Person类的引用,只有Person类是可序列化的,Teacher类才是可序列化的,如果Person类不可序列化,则无论Teacher是否实现Serilizable、Externallizable接口,Teacher类都是不可序列化的。
假设有如下情况:程序有两个Teacher对象,它们的student实例变量都引用到同一个Person对象,而且该Person对象还有一个引用变量引用它。
如图所示java学习笔记 IO

Person per = new Person("孙悟空", 500);
Teacher t1 = new Teacher("唐僧" , per);
Teacher t2 = new Teacher("菩提祖师" , per);

这里产生一个问题–如果先序列化t1对象,则系统将该t1对象所引用的Person对象一起序列化;如果程序再序列化t2对象,系统将一样会序列化该t2对象,并将再次序列化该t2对象所引用的Person对象;如果程序再显式序列化per对象,系统会再次序列化该Person对象,这个过程似乎会向输出流中输出三个Person对象。
如果系统向输出流中写入了三个Person对象,当程序从输入流中反序列化这些对象时,将会得到三个Person对象,从而引起t1和t2所引用的Person对象不是同一个对象。

所以,java序列化机制采用了一种特殊的序列化算法:
a. 所有保存到磁盘中的对象都有一个序列化编号
b. 当程序试图序列化一个对象时,程序会先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列并输出
c. 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象。
上述问题如下图所以得以解决:
java学习笔记 IO

当第二次、第三次序列化Person对象时,程序不会再次将Person对象转换成字节序列并输出,而是仅仅输出一个序列化编号。所以当使用java序列化机制序列化可变对象时一定注意,只有第一次调用writeObject()方法来输出对象时才会将对象转换成字节序列,并写入到ObjectOutputStream;在后面程序中即使该对象的实例变量发生了改变,再次调用writeObject()方法输出该对象时,改变后的实例变量也不会被输出。

4.自定义序列化

通过在实例变量前面使用transient关键字,可以指定java序列化时无需理会该实例变量。
Eg:Person类的age使用了transient关键字修饰(transient关键字只能用于修饰实例变量,不可修饰java程序的其他成分),另一个程序先序列化一个Person对象,然后再反序列化该Person对象,得到反序列化的Person对象后程序输出该对象的age实例变量,发现输出为0.

public class Person implements java.io.Serializable {
    private String name;
    private transient int age;
    // 注意此处没有提供无参数的构造器!
    public Person(String name , int age)
    {
        System.out.println("有参数的构造器");
        this.name = name;
        this.age = age;
    }

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // age的setter和getter方法
    public void setAge(int age)
    {
        this.age = age;
    }
    public int getAge()
    {
        return this.age;
    }
}
public class TransientTest
{
    public static void main(String[] args)
    {
        try(
            // 创建一个ObjectOutputStream输出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("transient.txt"));
            // 创建一个ObjectInputStream输入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("transient.txt")))
        {
            Person per = new Person("孙悟空", 500);
            // 系统会per对象转换字节序列并输出
            oos.writeObject(per);
            Person p = (Person)ois.readObject();
            System.out.println(p.getAge());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

使用transient关键字修饰实例变量虽然简单方便,但被transient修饰的实例变量将被完全隔离在序列化机制之外,这导致在反序列化恢复java对象时无法取得该实例变量值。java提供了一种自定义序列化机制,通过这种自定义序列化机制可以让程序控制如何序列化各实例变量,甚至完全不序列化某些实例变量,方法:

java学习笔记 IO

writeObject方法负责写入特定类的实例状态,以便相应的readObject方法可以恢复它,通过重写该方法,程序员可以完全获得对序列化机制的控制,可以自主决定哪些实例变量需要序列化,需要怎样序列化;readObject方法负责从流中读取并恢复对象实例变量。
当序列化流不完整时,readObjectNoData()方法可以用来正确地初始化反序列化的对象。例如,接收方使用的反序列化类的版本不同于发送发或者接收方版本扩展的类不是发送发版本扩展的类,或者序列化流被篡改时,系统都会调用该方法来初始化反序列化的对象。
Eg:Person类的writeObject方法在Person对象保存时将name实例变量包装成StringBuffer,并将其字符序列反转后写入;在readObject方法中处理name的策略与之对应–先将读取的数据强制类型转换成StringBuffer,再将其发转后赋给name实例变量

public class Person implements java.io.Serializable {
    private String name;
    private int age;
    // 注意此处没有提供无参数的构造器!
    public Person(String name , int age)
    {
        System.out.println("有参数的构造器");
        this.name = name;
        this.age = age;
    }

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // age的setter和getter方法
    public void setAge(int age)
    {
        this.age = age;
    }
    public int getAge()
    {
        return this.age;
    }

    private void writeObject(java.io.ObjectOutputStream out)
        throws IOException
    {
        // 将name实例变量的值反转后写入二进制流
        out.writeObject(new StringBuffer(name).reverse());
        out.writeInt(age);
    }
    private void readObject(java.io.ObjectInputStream in)
        throws IOException, ClassNotFoundException
    {
        // 将读取的字符串反转后赋给name实例变量
        this.name = ((StringBuffer)in.readObject()).reverse()
            .toString();
        this.age = in.readInt();
    }
}

注意:writeObject()方法存储实例变量的顺序应该和readObject()方法中恢复实例变量的顺序一致,否则将不能正常恢复该java对象

还有一种更彻底的自定义机制,它甚至可以在序列化对象时将该对象替换成其他对象,如果需要实现序列化某个对象时替换该对象,则应为序列化类提供如下特殊方法:

ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException;

Eg:Person类提供writeReplace()方法,在写入Person对象时将该对象替换成ArrayList

public class Person implements java.io.Serializable {
    private String name;
    private int age;
    // 注意此处没有提供无参数的构造器!
    public Person(String name , int age)
    {
        System.out.println("有参数的构造器");
        this.name = name;
        this.age = age;
    }

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // age的setter和getter方法
    public void setAge(int age)
    {
        this.age = age;
    }
    public int getAge()
    {
        return this.age;
    }

    // 重写writeReplace方法,程序在序列化该对象之前,先调用该方法
    private Object writeReplace()throws ObjectStreamException
    {
        ArrayList<Object> list = new ArrayList<>();
        list.add(name);
        list.add(age);
        return list;
    }
}

java序列化机制保证在序列化对象之前,先调用该对象的writeReplace方法,如果该方法返回另一个java对象,则系统转为序列化另一个对象。
Eg:下程序表面是序列化Person对象,实际是序列化ArrayList

public class ReplaceTest
{
    public static void main(String[] args)
    {
        try(
            // 创建一个ObjectOutputStream输出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("replace.txt"));
            // 创建一个ObjectInputStream输入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("replace.txt")))
        {
            Person per = new Person("孙悟空", 500);
            // 系统将per对象转换字节序列并输出
            oos.writeObject(per);
            // 反序列化读取得到的是ArrayList
            ArrayList list = (ArrayList)ois.readObject();
            System.out.println(list);
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }
}

详细见P695

5.另一种序列化机制

java还提供了另一种序列化机制,这种序列化方式完全由程序员决定存储和恢复对象数据,要实现该目标,java类必须实现Externalizable接口,该接口定义了两个方法:
(1)void readExternal(ObjectInput in):需要序列化的类实现该方法实现反序列化,该方法调用DataInput(ObjectInput的父接口)的方法来恢复基本类型的实例变量值,调用ObjectInput的readObject()方法来恢复引用类型的实例变量值
(2)void writeExternal(ObjectOutput out):需要序列化的类实现该方法保存对象的状态
Eg:Person类实现了Externalizable接口,并实现了该接口里提供的两个方法,实现自定义序列化

public class Person implements java.io.Externalizable {
    private String name;
    private int age;
    // 注意此处没有提供无参数的构造器!
    public Person(String name , int age)
    {
        System.out.println("有参数的构造器");
        this.name = name;
        this.age = age;
    }

    // name的setter和getter方法
    public void setName(String name)
    {
        this.name = name;
    }
    public String getName()
    {
        return this.name;
    }

    // age的setter和getter方法
    public void setAge(int age)
    {
        this.age = age;
    }
    public int getAge()
    {
        return this.age;
    }

    public void writeExternal(java.io.ObjectOutput out)
        throws IOException
    {
        // 将name实例变量的值反转后写入二进制流
        out.writeObject(new StringBuffer(name).reverse());
        out.writeInt(age);
    }
    public void readExternal(java.io.ObjectInput in)
        throws IOException, ClassNotFoundException
    {
        // 将读取的字符串反转后赋给name实例变量
        this.name = ((StringBuffer)in.readObject()).reverse().toString();
        this.age = in.readInt();
    }
}

6.两种序列化机制对比

java学习笔记 IO

虽然实现Externalizable接口能带来一定的性能提升,但是由于实现Externalizable接口导致了编程复杂度的增加,所以大部分时候都是采用实现Serializable接口方式实现序列化。

7.版本问题

根据前面的介绍可以看出,反序列化java对象时必须提供该对象的class文件,但随着项目的升级,系统的class文件也会升级,java如何保证两个class文件的兼容性?
java序列化机制允许为序列化类提供一个private static final的serialVersionUID值,该类变量的值用于标识该java类的序列化版本,也就是说,如果一个类升级后,只要它的serialVersionUID类变量值保持不变,序列化机制也会把它们当成同一个序列化版本。
为了在反序列化时确保序列化版本的兼容性,最好在要序列化的类中加入private static final long serialVersionUID这个类变量,具体数值自己定义,这样即使在某个对象被序列化之后,它所对应的类被修改了,该对象也依然可以被正确地反序列化。

六,NIO

前面介绍的输入输出流都是阻塞式的输入输出,而且传统的输入输出流都是通过字节的移动来处理的,通常效率不高,从JDK1.4开始,java提供了一系列改进的输入输出处理的新功能,新增了许多用于处理输入输出的类,放在java.nio包以及子包下。
新IO使用内存映射文件的方式来处理输入输出,将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样访问文件,速度会快得多。
Channel(通道)和Buffer(缓冲)是新IO中两个核心对象。新IO还提供了用于将Unicode字符串映射成字节序列以及逆映射操作的Charset类,也提供了用于支持非阻塞式输入输出的Selector类。

1.Buffer

可以被理解为一个容器,它的本质是一个数组,可以保存多个类型相同的数据。

Buffer中三个重要概念:容量capacity、界限limit和位置position
(1)容量capacity:表示该Buffer的最大数据容量,即最多可以存储多少数据,不能为负,创建后不能改变
(2)界限limit:第一个不应该被读出或者写入的缓冲区位置索引,位于limit后的数据既不可以读也不可以写
(3)位置position:用于指明下一个可以被读出的或者写入的缓冲区位置索引
Buffer还支持一个可选的标记mark,允许直接将position定位到该mark处,这些值满足如图关系:
java学习笔记 IO

下图显示了某个Buffer读入一些数据后的示意图:
java学习笔记 IO

Buffer装入数据结束后,调用Buffer的flip()方法,将limit设置为position所在位置,并将position设为0,这就使得Buffer的读写指针又移到了开始位置,即Buffer调用flip()方法之后,Buffer为输出数据做好准备;当Buffer输出数据结束后,Buffer调用clear(),该方法不是清空Buffer的数据,它仅仅将position置为0,将limit置为capacity,这样为再次向Buffer中装入数据做好准备。

还有两个方法,put()和get(),用于向Buffer中放入数据和从Buffer中取出数据,该过程中Buffer既支持对单个数据的访问,也支持对批量数据的访问。
当使用put()和get()访问Buffer中的数据时,有两种方式:
相对Relative:从Buffer的当前position处开始读取或写入数据,然后将位置的值按处理元素的个数增加
绝对Absolute:直接根据索引向Buffer中读取或写入数据,使用绝对方式访问Buffer里的数据时并不会影响position的值

Eg:Buffer的一些常规操作

package File;
import java.nio.*;

public class BufferTest
{
    public static void main(String[] args)
    {
        // 创建Buffer
        CharBuffer buff = CharBuffer.allocate(8); // ①
        System.out.println("capacity: " + buff.capacity());
        System.out.println("limit: " + buff.limit());
        System.out.println("position: " + buff.position());
        // 放入元素
        buff.put('a');
        buff.put('b');
        buff.put('c'); // ②
        System.out.println("加入三个元素后,position = "
            + buff.position());
        // 调用flip()方法
        buff.flip(); // ③
        System.out.println("执行flip()后,limit = " + buff.limit());
        System.out.println("position = " + buff.position());
        // 取出第一个元素
        System.out.println("第一个元素(position=0):" + buff.get()); // ④
        System.out.println("取出一个元素后,position = "
            + buff.position());
        // 调用clear方法
        buff.clear(); // ⑤
        System.out.println("执行clear()后,limit = " + buff.limit());
        System.out.println("执行clear()后,position = "
            + buff.position());
        System.out.println("执行clear()后,缓冲区内容并没有被清除:"
            + "第三个元素为:" +  buff.get(2)); // ⑥
        System.out.println("执行绝对读取后,position = "
            + buff.position());
    }
}
/* capacity: 8 limit: 8 position: 0 加入三个元素后,position = 3 执行flip()后,limit = 3 position = 0 第一个元素(position=0):a 取出一个元素后,position = 1 执行clear()后,limit = 8 执行clear()后,position = 0 执行clear()后,缓冲区内容并没有被清除:第三个元素为:c 执行绝对读取后,position = 0 */

分析:①创建了一个capacity为8的CharBuffer,如图所示
java学习笔记 IO
②处,程序向CharBuffer中放入了3个数值,之后的CharBuffer如图
java学习笔记 IO
③处,调用flip(),该方法把limit设为position,把position设为0,如图
java学习笔记 IO
④处,取出一个元素,取出一个元素后position向后移动一位,即取出一个元素后,position = 1
⑤处,Buffer调用clear()方法将position设为0,将limit设为与capacity相等,如图
java学习笔记 IO
⑥处,因为clear()方法并不是清空Buffer数据,所以可以取出位置为2的值

2.Channel

Channel类似于传统的流对象,但与流对象有两个主要区别:
(1)Channel可以直接将指定文件的部分或全部直接映射为Buffer
(2)程序不能直接访问Channel中的数据,包括读取、写入都不行,Channel只能与Buffer进行交互。

所有的Channel都不应该通过构造器直接创建,而是通过传统节点的getChannel()方法来返回对应的Channel。
Channel常用的三类方法是map()、read()和write(),map()用于将Channel对应的部分或全部数据映射成ByteBuffer,read()和write()都有一系列重载形式,用于从Buffer中读取数据或向Buffer中写入数据。

map()方法的方法签名:第一个参数执行映射时的模式,分别有只读、读写等模式,第二个、第三个用于控制将Channel的哪些数据映射成ByteBuffer

MappedByteBuffer map(FileChannel.MapMode mode, long position, long size);

Eg:直接将FileChannel的全部数据映射成ByteBuffer

public class FileChannelTest
{
    public static void main(String[] args)
    {
        File f = new File("FileChannelTest.java");
        try(
            // 创建FileInputStream,以该文件输入流创建FileChannel
            FileChannel inChannel = new FileInputStream(f).getChannel();
            // 以文件输出流创建FileBuffer,用以控制输出
            FileChannel outChannel = new FileOutputStream("a.txt")
                .getChannel())
        {
            // 将FileChannel里的全部数据映射成ByteBuffer
            MappedByteBuffer buffer = inChannel.map(FileChannel
                .MapMode.READ_ONLY , 0 , f.length());   //将指定Channel中的全部数据映射成ByteBuffer
            // 使用GBK的字符集来创建解码器
            Charset charset = Charset.forName("GBK");
            // 直接将buffer里的数据全部输出
            outChannel.write(buffer);     // 直接将整个ByteBuffer的全部数据写入一个输出FileChannel中
            // 再次调用buffer的clear()方法,复原limit、position的位置
            buffer.clear();
            // 创建解码器(CharsetDecoder)对象
            CharsetDecoder decoder = charset.newDecoder();
            // 使用解码器将ByteBuffer转换成CharBuffer
            CharBuffer charBuffer =  decoder.decode(buffer);
            // CharBuffer的toString方法可以获取对应的字符串
            System.out.println(charBuffer);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
}

3.字符集和Charset

计算机底层只是忠实地记录每个文件的二进制序列,当需要保存文本文件时,程序必须先把文件中的每个字符翻译成二进制序列;当需要读取文本文件,程序必须把二进制序列转换为一个个字符。
JDK1.4提供Charset处理字节序列和字符序列之间的转换关系,该类包含了用于创建解码器和编码器的方法,还提供了获取Charset所支持字符集的方法,Charset类是不可变的。

Eg:可用下程序获取该JDK所支持的全部字符集

public class CharsetTest
{
    public static void main(String[] args)
    {
        // 获取Java支持的全部字符集
        SortedMap<String,Charset>  map = Charset.availableCharsets();
        for (String alias : map.keySet())
        {
            // 输出字符集的别名和对应的Charset对象
            System.out.println(alias + "----->"
                + map.get(alias));
        }
    }
}

知道了字符集的别名之后,程序就可以调用Charset的forName()方法来创建对应的Charset对象,参数是相应字符集的别名。获得了Charset对象之后,就可以通过该对象的newDecoder()、newEncoder()这两个方法分别返回CharsetDecoder和CharsetEncoder对象,代表该Charset的解码器和编码器。
调用CharsetDecoder的decode()方法可以将ByteBuffer(字节序列)转换成CharBuffer(字符序列),调用CharsetEncoder的encode()方法可以将CharBuffer或String转换成ByteBuffer。
Eg:使用CharsetEncoder和CharsetDecoder完成ByteBuffer和CharBuffer之间的转换

public class CharsetTransform
{
    public static void main(String[] args)
        throws Exception
    {
        // 创建简体中文对应的Charset
        Charset cn = Charset.forName("GBK");
        // 获取cn对象对应的编码器和解码器
        CharsetEncoder cnEncoder = cn.newEncoder();
        CharsetDecoder cnDecoder = cn.newDecoder();
        // 创建一个CharBuffer对象
        CharBuffer cbuff = CharBuffer.allocate(8);
        cbuff.put('孙');
        cbuff.put('悟');
        cbuff.put('空');
        cbuff.flip();
        // 将CharBuffer中的字符序列转换成字节序列
        ByteBuffer bbuff = cnEncoder.encode(cbuff);
        // 循环访问ByteBuffer中的每个字节
        for (int i = 0; i < bbuff.capacity() ; i++)
        {
            System.out.print(bbuff.get(i) + " ");
        }
        // 将ByteBuffer的数据解码成字符序列
        System.out.println("\n" + cnDecoder.decode(bbuff));
    }
}

4.文件锁

在NIO中,java提供了FileLock支持文件锁定功能,在FileChannel中提供的lock()/tryLock()方法可以获得文件锁对象,两个方法主要区别是:lock()试图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞;而tryLock()是尝试锁定文件,它将直接返回而不是阻塞,如果获得了文件锁,该方法则返回该文件锁,否则将返回null。

如果FileChannel只想锁定文件的部分内容而不是全部,可使用如下方法:

java学习笔记 IO

当参数shared是true时,表明该锁是一个共享锁,它允许多个进程来读取该文件,但阻止其他进程获得对该文件的排他锁;当shared为false,表明该锁是一个排他锁,它锁住对该文件的读写,可以调用FileLock的isShared判断它获得的锁是否为共享锁(直接使用lock或tryLock获取的文件锁是排他锁)
Eg:使用FileLock锁定文件

public class FileLockTest
{
    public static void main(String[] args)
        throws Exception
    {

        try(
            // 使用FileOutputStream获取FileChannel
            FileChannel channel = new FileOutputStream("a.txt")
                .getChannel())
        {
            // 使用非阻塞式方式对指定文件加锁
            FileLock lock = channel.tryLock();
            // 程序暂停10s,在这10s内,其他程序无法对a.txt文件进行修改
            Thread.sleep(10000);
            // 释放锁
            lock.release();
        }
    }
}

5.NIO.2

java7对原有的NIO进行了重大改进,主要包括两个内容:
(1)提供了全面的文件IO和文件系统访问支持
(2)基于一部Channel的IO