Java I/O流输入输出,序列化,NIO,NIO.2

Java IO流

 

File类:

File类是java.io包下代表和平台无关的文件和目录,File不能访问文件内容本身。

File类基本操作:

        System.out.println("判断文件是否存在:"+file.exists());//判断文件是否存在,返回Boolean值
        System.out.println("创建文件夹:"+file.mkdir());//创建文件夹,只能创建一层,返回Boolean值
        System.out.println("文件目录:"+file.getParent());//返回文件最后一级子目录
        System.out.println("创建文件夹:"+file2.mkdirs());//创建文件夹,创建多层,返回Boolean值
        System.out.println("创建新文件:"+file3.createNewFile());//创建新文件,此处需处理异常,返回Boolean值
        System.out.println("删除文件:"+file3.delete());//删除文件,返回Boolean值
        System.out.println("文件改名:"+file.renameTo(file4));//文件改名,传入另一个文件
        System.out.println("文件名:"+file.getName());//返回名
        System.out.println("文件路径:"+file.getPath());//返回文件路径
        System.out.println("绝对路径:"+file.getAbsolutePath());//返回绝对路径
        System.out.println("文件夹:"+file.isDirectory());//返回是否文件夹
        System.out.println("是否文件:"+file.isFile());//返回是否文件
        System.out.println("是否文件夹:"+file.isDirectory());//返回是否文件夹
        System.out.println("是否绝对路径:"+file.isAbsolute());//返回是否绝对路径
        System.out.println("文件长度:"+file.length());//返回文件长度
        System.out.println("最后修改时间:"+file.lastModified());//返回最后修改时间

        // 以当前路径来创建一个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);
        }

文件过滤器:

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

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

FilenameFilter接口内只有一个抽象方法,因此改接口也是一个函数式接口,可使用Lambda表达式创建实现该接口的对象。

Java的IO流概念

Java的IO流是实现输入输出的基础,在Java中把不同的输入输出源抽象表述为流,通过流的方式允许Java使用相同的方式来访问不同的输入输出源。

stream是从起源(source)到接收(sink)的有序数据。

IO(in / out)流的分类

  流向:

    输入流  读取数据

    输出流  写出数据

  数据类型:

    字节流

    一个字节占8位, 以一个字节为单位读数据  

    八大数据类型所占字节数:
    byte(1), short(2), int(4), long(8),float(4), double(8),boolean(1),char(2)

      字节输入流  读取数据  InputStream

                     

      字节输出流  写出数据  OutputStream

                     

    字符流

    一个字符占两个字节, 以一个字符为一个单位

      字符输入流  读取数据  Reader

                    

      字符输出流  写出数据  Writer

                    

 

字节流的基本抽象类

 InputStream    OutputStream

字符流的基本抽象类

Reader   Writer

  功能:
    节点流: 只有一个根管道套在文件上进行传输
    处理流: 将节点流处理一下, 增强管道的功能, 相当于在管道上套一层

 字节流和字符流:

InputStream和Reader是所有输入流的抽象基类,本身并不能创建实例来执行输入,但它们将成为所有输入流的模板,他们的方法是有输入流都可以使用的方法。

这两个基类的功能基本是一样的。

他们分别有一个用于读取文件的输入流:FileInputStream和FileReader,他们都是节点流,会直接和指定文件关联。

使用FileInputStream读取文件自身:

    public static void main(String[] args) throws IOException
    {
        // 创建字节输入流
        FileInputStream fis = new FileInputStream(
                "D:\\idea_project\\mybootbill\\src\\main\\java\\com\\jiangwenzhang\\mybootbill\\learn\\FileIO\\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();
    }

需要注意,如果bbuf字节数组的长度较小,遇到中文时可能会乱码,因为如果文件本身保存时采用GBK编码方式,在这种方式下,每个中文字符占两个字节,如果read方法读取时只读取到了半个中文就会乱码。

程序里打开的文件IO资源不属于内存里的资源,垃圾回收机制无法回收该资源,所以要显示关闭文件IO资源。

使用FileReader:

    public static void main(String[] args)
    {
        try(
                // 创建字符输入流
                FileReader fr = new FileReader("D:\\idea_project\\mybootbill\\src\\main\\java\\com\\jiangwenzhang\\mybootbill\\learn\\FileIO\\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();
        }
    }

InputStream和Reader还支持如下几个方法来移动记录指针:

方法 释义
void mark(int readlimit) Marks the current position in this input stream.
boolean markSupported() Tests if this input stream supports the mark and reset methods.
void reset() Repositions this stream to the position at the time the mark method was last called on this input stream.
long skip(long n) Skips over and discards n bytes of data from this input stream.

OuputStream和Writer:

    public static void main(String[] args)
    {
        try(
                // 创建字节输入流
                FileInputStream fis = new FileInputStream(
                        "D:\\idea_project\\mybootbill\\src\\main\\java\\com\\jiangwenzhang\\mybootbill\\learn\\FileIO\\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();
        }
    }

注意:使用IO流执行输出时,不要忘记关闭输出流,关闭输出流处可以保证流的物流自愿被回收,还可以将输出流缓冲区的数据flush到物理节点里。

如果希望直接输出字符串内容,使用Writer更好:

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

输入输出流体系:

使用处理流的典型思路是,使用处理流来包装节点流,程序通过处理流来执行输入输入,让节点流与底层设备、文件交互。

使用PrintStream处理流包装OutStream:

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

通常如果要输出文本内容,都应将输出流包装成PrintStream后输出。

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

Java输入输出流体系常用流分类:

流分类 使用分类 字节输入流 字节输出流 字符输入流 字符输出流
  抽象基类 InputStream

OutputStream

Reader Writer
节点流 访问文件 FileInputStream FileOutStream FileReader FileWriter
访问数值 ByteArrayInputStream ByteArrayOutStream CharArrayReader CharArrayWriter
访问管道 PipedInputStream PipedOutStream PipedReader PipedWriter
访问字符串     StringReader StringWriter
处理流 缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
转换流     InputStreamReader OutputStreamWriter
对象流 ObjectInputStream ObjectOutputStream    
抽象基类(过滤) FilterInputStream FilterOutputStream FilterReader FilterWriter
打印流   PrintStream   PrintWriter
推回输入流 PushbackInputStream   PushbackReader  
特殊流 DataInputStream DataOutputStream    

通常来说,字节流的功能比字符流强大,因为计算机所有数据都是二进制的,字节流可以处理所有的二进制文件,但是需要使用合适的方式把这些字节转换成字符,通常:如果进行输入输出的内容是文本内容,则应该考虑使用字符流,如果是二进制内容,则使用字节流。

使用字符串作为物理节点:

    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.print(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();
        }
    }

和前面使用FileReader和FileWriter相似,只是创建对象时传入的是字符串节点,用于String是不可变得字符串对象,所以StringWriter使用StringBuffer作为输出节点。

转换流

InputStreamReader将字节输入流转换成字符输入流,OutputStreamWriter将字节输出流转换成字符输出流。

以键盘输入为例,java使用System.in代表标准输入也就是键盘输入,但这个标准输入流是InputStream类的实例,可以使用InputStreamReader将其转换成字符输入流,再讲普通的Reader再次包装成BufferedReader:

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

推回输入流:

PushbackInputStream和PushbackReader

推回输入流都带有一个推回缓冲区,当程序调用推回输入流的unread()方法,系统将会把指定数组内容推回到该缓冲区,而推回输入流每次调用read()方法总是先从推回缓冲区读取,只有完全读取了推回缓冲区的内容后,但还没有装满read()所需的数组时才会从原输入流读取。

    public static void main(String[] args)
    {
        try(
                // 创建一个PushbackReader对象,指定推回缓冲区的长度为64
                PushbackReader pr = new PushbackReader(new FileReader(
                        "D:\\idea_project\\mybootbill\\src\\main\\java\\com\\jiangwenzhang\\mybootbill\\learn\\FileIO\\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();
        }
    }

粗体下划线部分实现了将制定内容推回到推回缓冲区,于是程序再次调用read()方法时,实际上只是读取了推回缓冲区的部分内容,从而实现了只打印目标字符串前面内容的功能。

重定向标准输入输出:

        一般情况下,System.in代表的是键盘、System.out是代表的控制台(显示器)。当程序通过System.in来获取输入的时候,默认情况下,是从键盘读取输入;当程序试图通过System.out执行输出时,程序总是输出到显示器。如果我们想对这样的情况做一个改变,例如获取输入时,不是来自键盘,而是来自文件或其他的位置;输出的时候,不是输出到显示器上显示,而是输出到文件或其他位置,怎么实现?于是,java重定标准输入输出应运而生。

  static void setErr(PrintStream err)、重定向标准错误输出流

  static void setIn(InputStream in)、重定向标准输入流

  static void setOut(PrintStream out) 重定向标准输出流

 

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

 

    public static void main(String[] args)
    {
        try(
                FileInputStream fis = new FileInputStream("RedirectIn.java"))
        {
            // 将标准输入重定向到fis输入流
            System.setIn(fis);
            // 使用System.in创建Scanner对象,用于获取标准输入
            Scanner sc = new Scanner(System.in);
            // 增加下面一行将只把回车作为分隔符
            sc.useDelimiter("\n");
            // 判断是否还有下一个输入项
            while(sc.hasNext())
            {
                // 输出输入项
                System.out.println("键盘输入的内容是:" + sc.next());
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }

Java虚拟机读写其他进程数据

 读取其他进程的输出信息:

 

    public static void main(String[] args)
            throws IOException
    {
        // 运行javac命令,返回运行该命令的子进程
        Process p = Runtime.getRuntime().exec("javac");
        try(
                // 以p进程的错误流创建BufferedReader对象
                // 这个错误流对本程序是输入流,对p进程则是输出流
                BufferedReader br = new BufferedReader(new
                        InputStreamReader(p.getErrorStream())))
        {
            String buff = null;
            // 采取循环方式来读取p进程的错误输出
            while((buff = br.readLine()) != null)
            {
                System.out.println(buff);
            }
        }
    }

在Java程序中启动Java虚拟机运行另一个Java程序,并像另一个Java程序中输入数据:

package com.jiangwenzhang.mybootbill.learn.FileIO;

import java.io.*;
import java.util.*;

public class WriteToProcess
{
    public static void main(String[] args)
            throws IOException
    {
        // 运行java ReadStandard命令,返回运行该命令的子进程
        Process p = Runtime.getRuntime().exec("java ReadStandard");
        try(
                // 以p进程的输出流创建PrintStream对象
                // 这个输出流对本程序是输出流,对p进程则是输入流
                PrintStream ps = new PrintStream(p.getOutputStream()))
        {
            // 向ReadStandard程序写入内容,这些内容将被ReadStandard读取
            ps.println("普通字符串");
            ps.println(new WriteToProcess());
        }
    }
}
// 定义一个ReadStandard类,该类可以接受标准输入,
// 并将标准输入写入out.txt文件。
class ReadStandard
{
    public static void main(String[] args)
    {
        try(
                // 使用System.in创建Scanner对象,用于获取标准输入
                Scanner sc = new Scanner(System.in);
                PrintStream ps = new PrintStream(
                        new FileOutputStream("out.txt")))
        {
            // 增加下面一行将只把回车作为分隔符
            sc.useDelimiter("\n");
            // 判断是否还有下一个输入项
            while(sc.hasNext())
            {
                // 输出输入项
                ps.println("键盘输入的内容是:" + sc.next());
            }
        }
        catch(IOException ioe)
        {
            ioe.printStackTrace();
        }
    }
}

RandomAccessFile

 随机流(RandomAccessFile)不属于IO流,支持对文件的读取和写入随机访问。

RandomAccessFile允许自由定位文件记录指针,RandomAccessFile可以不从开始的地方开始输出,因为RandomAccessFile可以向已存在的文件后追加内容。

RandomAccessFile最大的局限就是只能读写文件,不能读写其他IO节点。

RandomAccessFile对象包含了一个记录指针,用以标识当前读写位置。RandomAccessFile可以自由移动该记录指针。

使用RandomAccessFile来访问指定的中间部分数据:

    public static void main(String[] args)
    {
        try(
                RandomAccessFile raf =  new RandomAccessFile(
                        "D:\\idea_project\\mybootbill\\src\\main\\java\\com\\jiangwenzhang\\mybootbill\\learn\\FileIO\\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();
        }
    }

该方法以只读方式打开文件,从300字节处开始读取。

像文件中追加内容,为了追加内容,程序应该先将记录指针移动到文件最后,然后项文件中输出内容。

    public static void main(String[] args)
    {
        try(
                //以读、写方式打开一个RandomAccessFile对象
                RandomAccessFile raf = new RandomAccessFile("out.txt" , "rw"))
        {
            //将记录指针移动到out.txt文件的最后
            raf.seek(raf.length());
            raf.write("追加的内容!\r\n".getBytes());
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }

指定文件,指定位置插入内容:

package com.jiangwenzhang.mybootbill.learn.FileIO;

import java.io.*;


public class InsertContent
{
    public static void insert(String fileName , long pos
            , String insertContent) throws IOException
    {
        File tmp = File.createTempFile("tmp" , null);
        tmp.deleteOnExit();
        try(
                RandomAccessFile raf = new RandomAccessFile(fileName , "rw");
                // 使用临时文件来保存插入点后的数据
                FileOutputStream tmpOut = new FileOutputStream(tmp);
                FileInputStream tmpIn = new FileInputStream(tmp))
        {
            raf.seek(pos);
            // ------下面代码将插入点后的内容读入临时文件中保存------
            byte[] bbuf = new byte[64];
            // 用于保存实际读取的字节数
            int hasRead = 0;
            // 使用循环方式读取插入点后的数据
            while ((hasRead = raf.read(bbuf)) > 0 )
            {
                // 将读取的数据写入临时文件
                tmpOut.write(bbuf , 0 , hasRead);
            }
            // ----------下面代码插入内容----------
            // 把文件记录指针重新定位到pos位置
            raf.seek(pos);
            // 追加需要插入的内容
            raf.write(insertContent.getBytes());
            // 追加临时文件中的内容
            while ((hasRead = tmpIn.read(bbuf)) > 0 )
            {
                raf.write(bbuf , 0 , hasRead);
            }
        }
    }
    public static void main(String[] args)
            throws IOException
    {
        insert("InsertContent.java" , 45 , "插入的内容\r\n");
    }
}

Java9改进的对象序列化

对象序列化的目标是将对象保存到磁盘中,或允许在网络中直接传输对象。所有分布式应用常常需要跨平台,跨网络,因此要求所有传的参数、返回值都必须实现序列化。

序列化:把Java对象转换为字节序列的过程。 

反序列化:把字节序列恢复为Java对象的过程。

对象的序列化是指将一个Java对象写入IO流中,对象的反序列化则是是指从IO流中恢复该Java对象。

Java9增强了对象序列化机制,他允许对读入的序列化数据进行过滤,这种过滤可以在反序列化之前对数据执行校验,从而提高安全性和健壮性。

让某个类可序列化,必须实现如下两个接口之一:

Serializable

Externailzable

Java的很多类已经实现了Serializable,这是一个标记接口,无需实现任何方法,只是表明该类的实例是可以序列化的。

所有可能在网络上传输的对象的类都应该是可序列化的。

使用对象流实现序列化:

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与age的setter和getter方法

    // 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 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对象:

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文件。

反序列化机制无需通过构造器来初始化Java对象。

如果使用序列化机制向文件中写入了多个Java对象,使用反序列化机制恢复对象必须按实际写入的顺序读取。

对象引用的序列化

如果某个类的成员变量的类型不是基本类型或String而是引用类型,那么这个引用类必须是可序列化的,否则拥有该类型成员变量的的类也是不可序列化的。

public class Teacher
    implements java.io.Serializable
{
    private String name;
    private Person student;
    public Teacher(String name , Person student)
    {
        this.name = name;
        this.student = student;
    }
    // 此处省略了name和student的setter和getter方法

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

    // student的setter和getter方法
    public void setStudent(Person student)
    {
        this.student = student;
    }
    public Person getStudent()
    {
        return this.student;
    }
}

特殊情况:

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

两种对象互相引用,这样如果先序列化t1,系统将t1对象引用的Person对象一起序列化,在序列化t2,程序将一样会序列化该t2对象,并且再次序列化Person对象,如果程序在显示序列化per对象,系统又一次序列化person对象。这个过程向输出流中输出三个Person对象。

这样程序从输入流中反序列化这些对象,将会得到三个person对象,从而引起t1和t2所引用的Person对象不是同一个对象。

Java序列化机制采用了一种特殊的序列化算法:

1、所有保存到磁盘中的对象都有一个序列化编号。

2、当程序试图序列化一个对象时,会先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机中)被序列化,系统才会将该对象转换成字节序列并输出。

3、如果对象已经被序列化,程序将直接输出一个序列化编号,而不是重新序列化。

通过以上算法,当第二次第三次序列化,程序不会再次将Person对象转换成字节序列并输出,而是仅仅输出一个序列化编号。

当多次调用wirteObject()方法输出同一个对象时,只有第一次调用wirteObject()方法才会将该对象转换成字节序列并输出。

    public static void main(String[] args)
    {
        try(
            // 创建一个ObjectOutputStream输出流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("teacher.txt")))
        {
            Person per = new Person("孙悟空", 500);
            Teacher t1 = new Teacher("唐僧" , per);
            Teacher t2 = new Teacher("菩提祖师" , per);
            // 依次将四个对象写入输出流
            oos.writeObject(t1);
            oos.writeObject(t2);
            oos.writeObject(per);
            oos.writeObject(t2);
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
    }
    public static void main(String[] args)
    {
        try(
            // 创建一个ObjectInputStream输出流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("teacher.txt")))
        {
            // 依次读取ObjectInputStream输入流中的四个对象
            Teacher t1 = (Teacher)ois.readObject();
            Teacher t2 = (Teacher)ois.readObject();
            Person p = (Person)ois.readObject();
            Teacher t3 = (Teacher)ois.readObject();
            // 输出true
            System.out.println("t1的student引用和p是否相同:"
                + (t1.getStudent() == p));
            // 输出true
            System.out.println("t2的student引用和p是否相同:"
                + (t2.getStudent() == p));
            // 输出true
            System.out.println("t2和t3是否是同一个对象:"
                + (t2 == t3));
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }

上面代码依次读取了序列化文件中的4个Java对象,通过比较可以看出t2和t3是同一个对象。

注意:

由于Java序列化机制使然,多次序列化同一个Java对象时,只有第一次序列化该对象才会把该Java对象转换成字节序列并输出,因此程序序列化一个可变对象之后,后面改变了对象的实例变量值,再次序列化也只是输出前面的序列化编号,改变的实例变量值也不会输出。

    public static void main(String[] args)
    {

        try(
            // 创建一个ObjectOutputStream输入流
            ObjectOutputStream oos = new ObjectOutputStream(
                new FileOutputStream("mutable.txt"));
            // 创建一个ObjectInputStream输入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("mutable.txt")))
        {
            Person per = new Person("孙悟空", 500);
            // 系统会per对象转换字节序列并输出
            oos.writeObject(per);
            // 改变per对象的name实例变量
            per.setName("猪八戒");
            // 系统只是输出序列化编号,所以改变后的name不会被序列化
            oos.writeObject(per);
            Person p1 = (Person)ois.readObject();    //
            Person p2 = (Person)ois.readObject();    //// 下面输出true,即反序列化后p1等于p2
            System.out.println(p1 == p2);
            // 下面依然看到输出"孙悟空",即改变后的实例变量没有被序列化
            System.out.println(p2.getName());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }

Java9增加的过滤功能:

Java9为ObjectInputStream增加了setObjectInputFilter()和getObjectInputFilter()两个方法,其中第一个方法用于为对象输入流设置过滤器。当程序通过ObjectInputStream反序列化时,过滤器的checkInput()方法会被自动激发,用于检查序列化数据是否有效。

    public static void main(String[] args)
    {
        try(
            // 创建一个ObjectInputStream输入流
            ObjectInputStream ois = new ObjectInputStream(
                new FileInputStream("object.txt")))
        {
            ois.setObjectInputFilter((info) -> {
                System.out.println("===执行数据过滤===");
                ObjectInputFilter serialFilter = ObjectInputFilter.Config.getSerialFilter();
                    if (serialFilter != null) {
                        // 首先使用ObjectInputFilter执行默认的检查
                        ObjectInputFilter.Status status = serialFilter.checkInput(info);
                        // 如果默认检查的结果不是Status.UNDECIDED
                        if (status != ObjectInputFilter.Status.UNDECIDED) {
                            // 直接返回检查结果
                            return status;
                        }
                    }
                    // 如果要恢复的对象不是1个
                    if(info.references() != 1)
                    {
                        // 不允许恢复对象
                        return ObjectInputFilter.Status.REJECTED;
                    }
                    if (info.serialClass() != null &&
                        // 如果恢复的不是Person类
                        info.serialClass() != Person.class)
                    {
                        // 不允许恢复对象
                        return ObjectInputFilter.Status.REJECTED;
                    }
                    return ObjectInputFilter.Status.UNDECIDED;
                });
            // 从输入流中读取一个Java对象,并将其强制类型转换为Person类
            Person p = (Person)ois.readObject();
            System.out.println("名字为:" + p.getName()
                + "\n年龄为:" + p.getAge());
        }
        catch (Exception ex)
        {
            ex.printStackTrace();
        }
    }

自定义序列化

通过在实例变量前使用transient关键字修饰,可以指定Java序列化时无需理会该实例变量。

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与age的setter和getter方法

    // 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();
        }
    }
}

程序将输出0

被transient修饰的的实例变量被完全隔离在序列化机制之外,这导致在反序列化恢复Java对象时无法取得该实例变量值。

Java提供了一种自定义序列化机制:

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与age的setter和getter方法

    // 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();
    }
}

还有一种更彻底的自定义机制,他可以在序列化对象时将该对象替换成其他对象。

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与age的setter和getter方法

    // 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()方法,如果方法返回的是另一个对象,则系统转为序列化另一个对象,

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

序列化机制里还有一个特殊的方法,他可以实现保护整个对象,

public class Orientation
    implements java.io.Serializable
{
    public static final Orientation HORIZONTAL = new Orientation(1);
    public static final Orientation VERTICAL = new Orientation(2);
    private int value;
    private Orientation(int value)
    {
        this.value = value;
    }
    // 为枚举类增加readResolve()方法
    private Object readResolve()throws ObjectStreamException
    {
        if (value == 1)
        {
            return HORIZONTAL;
        }
        if (value == 2)
        {
            return VERTICAL;
        }
        return null;
    }
}

Java的另一种自定义序列化机制:

Java还提供了另一种自定义序列化机制,这种序列化机制完全由程序员存储和恢复对象数据。需要实现Externalizable接口。

public class Person
    implements java.io.Externalizable
{
    private String name;
    private int age;
    // 注意必须提供无参数的构造器,否则反序列化时会失败。
    public Person(){}
    public Person(String name , int age)
    {
        System.out.println("有参数的构造器");
        this.name = name;
        this.age = age;
    }
    // 省略name与age的setter和getter方法

    // 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();
    }
}

NIO

NIO(新IO)和传统的IO具有相同的目的,都用于进行输入输出,但新IO采用了不同的方式来处理,NIO采用内存映射文件的方式来处理,NIO将文件或文件的一段区域映射到内存中,从而像访问内存一样访问文件。就像操作系统的虚拟内存概念。

Channel(通道)和Buffer(缓冲)是NIO的两个核心对象,Channel是对传统输入输出系统的模拟,在NIO中所有的数据都需要通过通道传输,Channel和传统的输入输出最大的区别在于它提供了一个map()方法,该方法可以直接将一块数据映射到内存中。

IO是面向流的,NIO是面向快(缓冲区)的。

Buffer可以被理解成一个容器,他的本质是数组,发送到Channel中的所有对象都必须首相放到Buffer中,从Channel中取出的数据也必须先放到Buffer中,Buffer可以一次次去Channel中取数据,也允许用Channel将文件的某块数据映射成Buffer。

NIO还提供了用于将Unicode字符串映射成字节序列以及逆映射操作的Charset累和用于支持非阻塞式输入输出的Selector类。

Buffer

Buffer就像一个数组可以保存多个类型相同的数据,Buffer是一个抽象类,最常用的子类是ByteBuffer,他可以在底层字节数组上进行getset操作。其他基本数据类型除了boolean都有相应的Buffer类。

在Buffer中有几个重要概念

属性 描述
Capacity 容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
Limit 上界,缓冲区中当前数据量
Position 位置,下一个要被读或写的元素的索引
Mark 标记,调用mark()来设置mark=position,再调用reset()可以让position恢复到标记的位置即position=mark

并遵循:capacity>=limit>=position>=mark>=0

Buffer的主要作用是装入数据然后输出数据,开始时Buffer的postiton为0,limit为capatity,程序通过put()方法像Buffer装入数据,或者从Channel中获取数据,Buffer的postion相应的后移。

Buffer装入数据后,调用Buffer的flip()方法,将limit位置设为postiton所在位置,并将postiton设为0,是的Buffer的读写指针移动到了开始的位置,为输出数据做好准备,当BUffer输出数据结束后,Buffer调用clear方法,不是清空数据,仅仅将postiton设置为0,两limit设置为capatity,为再次向Buffer中装入数据做好了准备。

Buffer常规操作:

ByteBuffer buffer=ByteBuffer.allocate(1024);       //非直接    大小为1024个字节  此时它的position=0 limit=1024 capacity=1024

ByteBuffer buf =ByteBuffer.allocateDirect(1024);     //直接    大小为1024个字节

buffer.put(“abcde”.getBytes());                       //将一个字节数组写入缓冲区 此时它的position=5 limit=1024 capacity=1024

buffer.put(“abcde”.getBytes()); //该数组会在之前position开始写 写完后 position=10 limit=1024 capacity=1024

buffer.flip();                                                   //这一步的作用是 使position=0 limit为可以操作的最大字节数 这里limit=10 capacity不变 还是1024

System.out.println(new String(byteBuffer.array(),0,2));          //它的结果在这里是:ab

这个方法的作用是什么呢?回到属性,执行下面语句:

System.out.println(byteBuffer.position());
System.out.println(byteBuffer.limit());
System.out.println(byteBuffer.capacity());

输出的结果是(以空格代替换行): 0      1024       1024

也就是说,现在这个缓冲区现在可以从索引0位置开始操作了。那就put试一试:

byteBuffer.put(“hhh”.getBytes());                               //注意这里只put3个字节

那么如果put之后的position、limit、capacity又是多少呢?

此时position=3    limit=1024  capacity=1024

对于上面的结果?也许会有疑问?先保留吧,接下来我们读取该buffer的内容:

byteBuffer.flip();         //读之前先翻转    翻转后position=3    limit=3  capacity=1024

System.out.println(new String(buffer.array(),0,buffer.limit()));

结果是:hhh

不知道小伙伴有没有疑问,不是说之前的数据还在吗?它去哪了呢?

被遗忘了,这里还是要把clear()方法再次提出来,之前说过,它并不会将缓冲区中的数据清空,也就是说缓冲区中之前的数据还在。执行clear后我们可以像操作一个空的缓冲区

一样从索引0位置开始来操作这个缓冲区。但是之前的数据还存在,只是被遗忘了。如果上面我们没有执行byteBuffer.flip(); 那么,结果就会是:hhhdeworld

所以啊,flip()一定不要忘了。

mark() :标记当前position

reset() :恢复position到mark标记的位置

hasRemaining :判断缓冲区中是否含有元素

get() :从缓冲区中读取单个字节

get(byte[] dst) :批量读取多个字节到dst数组

get(int index) : 读取指定位置的字节(position不会改变)

put(byte b) :将单个字节写入缓冲区position位置

put(byte[] dst) :将多个字节从缓冲区position位置开始写入

put(int index,byte b) : 将指定字节写入缓冲区索引位置,不会移动position

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

Buffer的创建成本很高,所以直接Buffer适用于长期生存的Buffer。

只有ByteBuffer提供allocateDirect()方法,所以只能在ByteBuffer级别上创建直接ByteBuffer,如果希望使用其他类型,将该Buffer转换成其他类型Buffer。

Channel

channel类似于传统流对象,区别:

channel可以直接将指定文件的部分或者全部映射成Buffer。

程序不能直接访问channel中的数据,channel只能和Buffer进行交互。

所有的channel都不应该通过构造器直接创建,而是通过传统节点InputStream、outPutstream的getChannel()方法返回对应的channel,不同的节点流获得的channel也不一样。

channel中最常用的三类方法map() , read() , write() 。map()用于将数据映射成ByteBuffer,另外两个有一系列重载,用于对Buffer读写数据。

将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());   //// 使用GBK的字符集来创建解码器
            Charset charset = Charset.forName("GBK");
            // 直接将buffer里的数据全部输出
            outChannel.write(buffer);     //// 再次调用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();
        }
    }
}
FileInputStream获取的 channel只能读,FileOutputStream获取的 channel只能写。RandomAccessFile中也包含getChannel方法,他是只读的还是读写的,取决于RandomAccessFile打开文件的模式。
public class RandomFileChannelTest
{
    public static void main(String[] args)
        throws IOException
    {
        File f = new File("a.txt");
        try(
            // 创建一个RandomAccessFile对象
            RandomAccessFile raf = new RandomAccessFile(f, "rw");
            // 获取RandomAccessFile对应的Channel
            FileChannel randomChannel = raf.getChannel())
        {
            // 将Channel中所有数据映射成ByteBuffer
            ByteBuffer buffer = randomChannel.map(FileChannel
                .MapMode.READ_ONLY, 0 , f.length());
            // 把Channel的记录指针移动到最后
            randomChannel.position(f.length());
            // 将buffer中所有数据输出
            randomChannel.write(buffer);
        }
    }
}

使用Channel和Buffer使用传统的IO的多次获取数据的方式:

public class ReadFile
{
    public static void main(String[] args)
        throws IOException
    {
        try(
            // 创建文件输入流
            FileInputStream fis = new FileInputStream("ReadFile.java");
            // 创建一个FileChannel
            FileChannel fcin = fis.getChannel())
        {
            // 定义一个ByteBuffer对象,用于重复取水
            ByteBuffer bbuff = ByteBuffer.allocate(256);
            // 将FileChannel中数据放入ByteBuffer中
            while( fcin.read(bbuff) != -1 )
            {
                // 锁定Buffer的空白区
                bbuff.flip();
                // 创建Charset对象
                Charset charset = Charset.forName("GBK");
                // 创建解码器(CharsetDecoder)对象
                CharsetDecoder decoder = charset.newDecoder();
                // 将ByteBuffer的内容转码
                CharBuffer cbuff = decoder.decode(bbuff);
                System.out.print(cbuff);
                // 将Buffer初始化,为下一次读取数据做准备
                bbuff.clear();
            }
        }
    }
}

字符集和Charset

Java默认采用Unicode字符集。JDK1.4提供了Charset来处理字节序列和字符序列之间的转换关系。该类提供了创建解码器和编码器的方法。

获取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对象,forName方法的参数是字符集别名。

获得了Charset对象之后,可以获得Charset的编码器和解码器,然后可以实现字节序列和字符序列的转换。

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

Charset本身也提供了编码解码方法,如果仅需编码解码操作,可以直接使用,不必创建编码器和解码器对象。

String的getBytes方法也是使用指定字符集将字符串转换成字节序列。

文件锁

如果多个程序需要并发修改同一个文件,程序需要某种机制来进行通信,使用文件锁可以有效地阻止多个进程并发修改同一个文件。

文件锁控制文件的全部或部分字节的访问。在NIO中Java提供FileLock来支持文件锁定功能。

lock():对文件从position开始,长度为size的内容加锁,阻塞。

tryLock():非阻塞。

当穿的shared参数是true时,表示这是共享锁,允许多个进程读取文件,但阻止其他进程获得对该文件的排它锁。

直接使用lock() tryLock()获取文件锁就是排它锁。

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
            Thread.sleep(10000);
            // 释放锁
            lock.release();
        }
    }
}

NIO.2

Java7 NIO.2对NIO进行了重大改进,主要包括:

提供了全文见IO和文件系统访问支持。

基于异步的Channel的IO。

Path、Paths、Files

Path接口代表和平台无关的平台路径。

public class PathTest
{
    public static void main(String[] args)
        throws Exception
    {
        // 以当前路径来创建Path对象
        Path path = Paths.get(".");
        System.out.println("path里包含的路径数量:"
            + path.getNameCount());
        System.out.println("path的根路径:" + path.getRoot());
        // 获取path对应的绝对路径。
        Path absolutePath = path.toAbsolutePath();
        System.out.println(absolutePath);
        // 获取绝对路径的根路径
        System.out.println("absolutePath的根路径:"
            + absolutePath.getRoot());
        // 获取绝对路径所包含的路径数量
        System.out.println("absolutePath里包含的路径数量:"
            + absolutePath.getNameCount());
        System.out.println(absolutePath.getName(3));
        // 以多个String来构建Path对象
        Path path2 = Paths.get("g:" , "publish" , "codes");
        System.out.println(path2);
    }
}

Files是一个操作文件的工具类:

public class FilesTest
{
    public static void main(String[] args)
        throws Exception
    {
        // 复制文件
        Files.copy(Paths.get("FilesTest.java")
            , new FileOutputStream("a.txt"));
        // 判断FilesTest.java文件是否为隐藏文件
        System.out.println("FilesTest.java是否为隐藏文件:"
            + Files.isHidden(Paths.get("FilesTest.java")));
        // 一次性读取FilesTest.java文件的所有行
        List<String> lines = Files.readAllLines(Paths
            .get("FilesTest.java"), Charset.forName("gbk"));
        System.out.println(lines);
        // 判断指定文件的大小
        System.out.println("FilesTest.java的大小为:"
            + Files.size(Paths.get("FilesTest.java")));
        List<String> poem = new ArrayList<>();
        poem.add("水晶潭底银鱼跃");
        poem.add("清徐风中碧竿横");
        // 直接将多个字符串内容写入指定文件中
        Files.write(Paths.get("pome.txt") , poem
            , Charset.forName("gbk"));
        // 使用Java 8新增的Stream API列出当前目录下所有文件和子目录
        Files.list(Paths.get(".")).forEach(path -> System.out.println(path));
        // 使用Java 8新增的Stream API读取文件内容
        Files.lines(Paths.get("FilesTest.java") , Charset.forName("gbk"))
            .forEach(line -> System.out.println(line));
        FileStore cStore = Files.getFileStore(Paths.get("C:"));
        // 判断C盘的总空间,可用空间
        System.out.println("C:共有空间:" + cStore.getTotalSpace());
        System.out.println("C:可用空间:" + cStore.getUsableSpace());
    }
}

使用FileVisitor遍历文件和目录:

FileVisitor代表一个文件访问器,

public class FileVisitorTest
{
    public static void main(String[] args)
        throws Exception
    {
        // 遍历g:\publish\codes\15目录下的所有文件和子目录
        Files.walkFileTree(Paths.get("g:", "publish" , "codes" , "15")
            , new SimpleFileVisitor<Path>()
        {
            // 访问文件时候触发该方法
            @Override
            public FileVisitResult visitFile(Path file
                , BasicFileAttributes attrs) throws IOException
            {
                System.out.println("正在访问" + file + "文件");
                // 找到了FileInputStreamTest.java文件
                if (file.endsWith("FileInputStreamTest.java"))
                {
                    System.out.println("--已经找到目标文件--");
                    return FileVisitResult.TERMINATE;
                }
                return FileVisitResult.CONTINUE;
            }
            // 开始访问目录时触发该方法
            @Override
            public FileVisitResult preVisitDirectory(Path dir
                , BasicFileAttributes attrs) throws IOException
            {
                System.out.println("正在访问:" + dir + " 路径");
                return FileVisitResult.CONTINUE;
            }
        });
    }
}

使用WatchService监控文件变化

以前的Java中,需要监控文件变化,可以考虑启动一个后台线程,每隔一段时间遍历一次指定目录,如果发现遍历结果和上次不同则认为文件发生了变化。

WatchService

public class WatchServiceTest
{
    public static void main(String[] args)
        throws Exception
    {
        // 获取文件系统的WatchService对象
        WatchService watchService = FileSystems.getDefault()
            .newWatchService();
        // 为C:盘根路径注册监听
        Paths.get("C:/").register(watchService
            , StandardWatchEventKinds.ENTRY_CREATE
            , StandardWatchEventKinds.ENTRY_MODIFY
            , StandardWatchEventKinds.ENTRY_DELETE);
        while(true)
        {
            // 获取下一个文件改动事件
            WatchKey key = watchService.take();    //
            for (WatchEvent<?> event : key.pollEvents())
            {
                System.out.println(event.context() +" 文件发生了 "
                    + event.kind()+ "事件!");
            }
            // 重设WatchKey
            boolean valid = key.reset();
            // 如果重设失败,退出监听
            if (!valid)
            {
                break;
            }
        }
    }
}

访问文件属性

NIO.2中提供了大量工具类,可以简单的读取修改文件属性

 

public class AttributeViewTest
{
    public static void main(String[] args)
        throws Exception
    {
        // 获取将要操作的文件
        Path testPath = Paths.get("AttributeViewTest.java");
        // 获取访问基本属性的BasicFileAttributeView
        BasicFileAttributeView basicView = Files.getFileAttributeView(
            testPath , BasicFileAttributeView.class);
        // 获取访问基本属性的BasicFileAttributes
        BasicFileAttributes basicAttribs = basicView.readAttributes();
        // 访问文件的基本属性
        System.out.println("创建时间:" + new Date(basicAttribs
            .creationTime().toMillis()));
        System.out.println("最后访问时间:" + new Date(basicAttribs
            .lastAccessTime().toMillis()));
        System.out.println("最后修改时间:" + new Date(basicAttribs
            .lastModifiedTime().toMillis()));
        System.out.println("文件大小:" + basicAttribs.size());
        // 获取访问文件属主信息的FileOwnerAttributeView
        FileOwnerAttributeView ownerView = Files.getFileAttributeView(
            testPath, FileOwnerAttributeView.class);
        // 获取该文件所属的用户
        System.out.println(ownerView.getOwner());
        // 获取系统中guest对应的用户
        UserPrincipal user = FileSystems.getDefault()
            .getUserPrincipalLookupService()
            .lookupPrincipalByName("guest");
        // 修改用户
        ownerView.setOwner(user);
        // 获取访问自定义属性的FileOwnerAttributeView
        UserDefinedFileAttributeView userView = Files.getFileAttributeView(
            testPath, UserDefinedFileAttributeView.class);
        List<String> attrNames = userView.list();
        // 遍历所有的自定义属性
        for (String name : attrNames)
        {
            ByteBuffer buf = ByteBuffer.allocate(userView.size(name));
            userView.read(name, buf);
            buf.flip();
            String value = Charset.defaultCharset().decode(buf).toString();
            System.out.println(name + "--->" + value) ;
        }
        // 添加一个自定义属性
        userView.write("发行者", Charset.defaultCharset()
            .encode("疯狂Java联盟"));
        // 获取访问DOS属性的DosFileAttributeView
        DosFileAttributeView dosView = Files.getFileAttributeView(testPath
            , DosFileAttributeView.class);
        // 将文件设置隐藏、只读
        dosView.setHidden(true);
        dosView.setReadOnly(true);
    }
}