记录《疯狂Java讲义精粹》划线内容及理解(第十章-输入输出流(IO))

第十章-输入输出流(IO)

java的IO主要通过java.io包下的类和接口来实现,主要分为输入,输出两大类

每类都包含字节流(以字节为单位)和字符流(以字符为单位)


java的IO流使用了装饰器设计模式,它将IO流分成底层节点流(用于和物理存储节点直接关联)和上层处理流

java7在java.nio包下提供了一系列全新的API,是对原有IO的升级,陈为NIO2


10.1 File类

File类是java.io包下代表与平台无关的文件和目录,File可以新建,删除,重命名文件和目录,但不能访问文件本身


10.1.1 访问文件和目录

File类可以使用文件路径字符串来创建FIle实例,这个字符串路径可以是相对路径,也可以是绝对路径

相对路径默认依据是运行java虚拟机时,所在的路径(也就是当前项目存放的路径)

File类的常用方法

  1.访问文件名相关

     String      getName()                返回File对象的文件名或路径(最后一层文件夹名)

     String      getPath()                   返回File对象的路径(全路径)

     File           getAbsoluteFile()     返回FIle对象的绝对路径作为字符串参数的FIle对象

     String       getAbsolutePath()   返回File对象的绝对路径名

     String       getParent()                返回File对象所对应的目录的父目录名

     boolean   renameTo(File  newName)      重命名File对象所对应的文件名或目录,成功返回true,否则返回false

  2.文件检测相关

    boolean    exists()                判断File对象所对应的文件或目录是否存在

    boolean    canWrite()           判断File对象对应的文件或目录是否可写

    boolean    canRead()            判断FIle对象对应的文件或目录是否可读

    boolean     isFile()                  判断File对象是文件,而不是目录

    boolean      isDirectory()       判断File对象是目录,而不是文件

    boolean      isAbsolute()       判断File对象对应的文件或目录是否为绝对路径

        该方法消除了不同平台的差异

                        在UNIX/Linux/BSD系统上,绝对路径开头是一个斜线(/)

                        在windows系统上,绝对路径开头是盘符

 3.获取常规文件信息

        long    lastModified()        返回文件的最后修改时间

        long    length()                   返回文件内容长度

 4,文件操作相关

 boolean    mkdir()        创建一个File对象所对应的目录,该File必须是文件

 String[]  list()                 返回File对象的所有子文件名和路径名

 File[]   listFiles()             返回FIle对象所有子文件和路径

 static  File[]   listRoots()   返回系统所有的根路径

10.1.2 文件过滤器

实现文件过滤器,主要依靠FIle类的list方法,该方法可以接受一个FilenameFilter的参数,该参数可以列出符合条件的文件

这个FilenameFilter接口的功能和javax.swing.filechooser包下的FileFilter抽象类的功能非常相似,但FileFilter却没有实现(继承)FilenameFilter接口

FilenameFilter接口内包含一个accept(File dir,String name)方法,该方法在dir路径下搜索指定的文件name,存在返回true


但这是个抽象方法,所以具体的实现还是要靠调用FIle类提供的各种细碎方法来拼凑此功能


实现文件过滤器时,主要是在调用File类的list方法时,在参数中传入一个FilenamFilter的实现类,通过实现类的acctep方法来过滤文件


这是一种Command模式的设计,程序的文件判断规则,是从外界传入的,因为java7不允许传递代码块,所以采用了一个实现类的对象,在java8中,应该可以使用lambda表达式来闯入文件判断规则


10.2 理解Java的IO流

java将来自不同输入/输出源的数据,抽象为流,以便统一操作

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


10.2.1 流的分类

1.输入输出流

    输入流:使用InputStream和Reader作为基类(抽象类)

    输出流:使用OutputStream和Writer作为基类(抽象类)

    这里的输出流有些特别,因为它牵扯一个方向问题,从内存到硬盘,对于程序来说是输出流

2.字节字符流

    字节流:8位字节      InputStream和OutputStream作为基类

    字符流:16位字符    Reader和Writer作为基类

3.节点流和处理流(按流的角色来分)

    节点流(低级流):对一个特定的IO设备读/写数据的流,程序连接到实际数据源

    处理流(高级流,包装流):对一个已经存在的流进行  连接 或 封装,程序不会直接连接到数据源

通过处理流,可以对各种不同的IO流采用相同的操作,并且java在设计处理流时,使用了装饰器设计模式,消除了不同节点流的差异,提供通用方法来操作IO

处理流的功能主要体现在两个方面

        1.性能的提高:增加缓冲来提高效率

        2.操作的便捷:提供了方法进行批量操作

10.2.2 流的概念模型

java的IO有40多个类,但都是从4个基类派生出来的

Inputstream(字节输入流) ,OutputStream(字节输出流)

Reader(字符输入流),Writer(字符输出流)


对于输入流而言(InputStream/Reader),他们将输入设备抽象成一个水管,水管里,每个“水滴”依次排列,输入流使用一个隐式的记录指针,来表示要读“水滴”的位置,读取之后指针制动向后移动(InputStream和Reader也提供了方法来操作指针)


对于输出流而言(OutputStream/Writer),他俩也将输出设备抽象成水管,输出时,将“水滴”依次放入水管中,采用隐式的指针,来记录放入水管的“水滴”位置,输出一个或多个“水滴”之后,指针自动向后移动


10.3 字节流和字符流

字节字符流的操作非常相似,只是操作的数据单位不同

10.3.1 InputStream和Reader

InputStream类的方法

    1.int read()                     从输入流中读单个字节,返回读到的字节数据类型

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

        2.int  read(char[]  cbuf)

        3.int  read(char[]  cbuf,  int  off,   int  len)

Reader的这三个方法和InputStream类的方法功能完全相同,只是单位从字节变为字符


输入流的实现类

InputStream——FileInputStream

Rander——FileReader

这两个输入流的实现类都是节点流,直接和指定文件关联(连接数据源)

    简单说一下取数据的过程

            1.创建对象

            2.取值,可用while

            3.处理

在处理完成之后需要关闭对应的文件,并且创建的对象会抛出IOException异常,所以建议在rty……catch块中使用,

java7时增强了try块的功能,能在结束时自动关闭文件,但需要在开始时,在try关键字后面增加一个小括号,放入需要关闭的资源,资源要实现Autocloseable接口,但java7改写了系统自带的资源类,几乎都实现了Auto Closeable接口

例如:

public class IOTest {
    public static void main(String[] args){
//        字节输入流实现类FileInputStream测试
        FileInTest();
    }

    private static void FileInTest() {

        try(    //        创建一个字节输入流实例
                FileInputStream inFile = new FileInputStream("D:/学习相关/222.txt");) {

//        一个1024长度的字节数组
//            这里每次从文件取1024个字节,但文件并没有那么多,所以会一次取完,但如果一次取的比较少
//            假设一次只取32字节,这时有可能出现乱码,原因在于只取了汉字的前一个字节
//            GBK——一个汉字两字节
//            UTF-8——一个汉字三字节
            byte[] bbuf = new byte[1024];

//        记录读到数据个数
            int hasRead = 0;

//        取值,输出
            while ((hasRead = inFile.read(bbuf)) > 0) {
                System.out.println(new String(bbuf,0,hasRead));
            }

//        关闭文件,这里用try……catch更好,并且在java7加入了自动关闭,所以在try块内还可以自动关闭资源
//              inFile.close();
        }catch (IOException e){
            System.out.println("出错咯!");
            System.out.println(e.getMessage());
        }
    }
}

移动指针方法

    void    mark(int    readAheadLimit)    在指针当前位置记录一个标记

    boolean  markSupported()     判断输入流是否支持,添加标记

    void     resrt()        将此流上的指针,回退到上次标记的位置

    long     skip(long   n)   指针向前移动n个字节/字符

10.3.2 OutputStream和Writer

OutputStream/Writer提供的写方法(功能相同,只是单位不同)

    void   write(int  c)        将指定的数据(字节/字符)输出到输出流

    void   write(byte[]/char[]   buf)        将指定的数组数据输出到输出流中

    void   write(byte[]/char[]   buf ,  int  off,   int   len)        将数组从off位置开始,长度位len的数据,输出到输出流

以下两个方法是Writer独有的

    void    write(String  str)         将str里的字符,输出到流

    void    write(String   str,int  off,int  len)        将str里的字符,从off位置开始,len个字符,输出到流

在关闭输出流对象是,会将缓存区中的数据,flush(刷新)到物理节点

如果要输出字符串内容,用Writer效果会更好

windows平台的换行符——\r\n

UNIX/Linus/BSD平台换行符——\n


10.4 输入/输出流体系

10.4.1 处理流的用法

程序使用处理流来执行输入输出,节点流来与底层交互

好处在于:

    1.使用处理更简单方便

    2.使用处理流效率更高

小知识:一直使用的system.out的类型就是PrintStream

在system类中,out是一个PrintStream类型的常量,值为null,再通过这个常量,调用PrintStream类中的Println方法


当使用了处理流包装节点流,在关闭时只需要关闭处理流,系统会自动关闭该处理流对应的节点流


10.4.2 输入/输出流体系

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gIE0yX78-1661949356775)(D:\学习相关\images\IO流常用方法.png)]

粗体字标出的类代表节点流,必须直接与指定的物理节点关联;

斜体字标出的类代表抽象基类,无法直接创建实例。

关于字节,字符流的使用场景

    输入/输出是文本内容——字符流

    输入/输出是二进制内容——字节流

我们认为字节流比字符流更加强大,因为字节流可以处理二进制文件,但如果全用字节流,有点太复杂了


有一些特殊的IO流,不在IO包下,他们可以加密解密,压缩解压等(AudioInputStream,DeflaterInputStream)


字符数组/字节数组,也可当作节点流的物理节点,需要在创建节点流对象时,传入一个字符数组/字节数组

并且字符流还可以使用字符串作为物理节点(StringReader/StringWriter)


四个访问管道的流,这个四个流将实现进程之间的通信

输入流输出流
PipedInputStreamPipedOutStream
PipedReaderPipedWriter

缓冲流增加了缓冲功能

对象流用于实现对象序列化

10.4.3 转换流

IO中提供了两个转换流(InputStreamReader,OutputStreamWriter),这两个转换流用于将字节流转换为字符流

如果需要读文本,可以使用下面的方法

//将System.in对象包装为Reader对象
InputStreamReader   reader=new InputStreamReader(System.in);

//将普通的Reader包装为BufferedReader
BufferedReader br=new BufferedReader(reader)

//使用BufferedReader的readLine()方法,整行的读取数据
br.readLine();

10.4.4 推回输入流

特殊的流 PushbackInputStream,PushBackReader

两个流共有的方法:

            void  unread(byte[]/char[]   buf)      将一个字节/字符数组内容推回到推回缓冲区,从而重复读取刚刚读到的内容

            void  unread(byte[]/char[]   b,int   off,int  len)        将一个字节/字符数组中,从off开始,长度为len的数据,推回到推回缓冲区,从而重复读取

            void   unread(int  b)        将数据推回到推回缓冲区,从而重复读取

这三个方法和InputStream和Reader中的三个read()方法对应,

这两个推回输入流都带有一个推回缓冲区,当系统调用unread()方法之后,会将读到的内容,推回到该缓冲区,而系统每次调用read()方法,总是先从推回缓冲区中读取,只有完全读完推回缓冲区的内容,还没有装满数组时,才会从原输入流中读取

推回缓冲区的大小可以设置,在创建对象时,传入一个整数来指定大小,默认长度为1,如果推回的内容超过推回缓冲区的大小,将引发Pushbackbuffer overflow的异常

定义格式:

PushbackReader  pr=new  PushbackReader(new FileReader("文件名(可加路径)")64)

10.5 重定向标准输入/输出

标准输入 system.in 从键盘读取输入

标准输出 system,out 从屏幕输出

System类中提供的重定向标准输入输出方法

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

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

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

使用格式:

//创建PrintStream输出流
PrintStream  ps=new  PrintStream(new FileOutputStream("文件名");
//将标准输出重定向到ps输出流
System.setOut(ps);


//上面代码,将输出流定向到一个文件,具体由FileOutputStream的文件名指定用

使用文件作为标准输入流

//创建文件字节输入流
FileInputStream  fis=new  FileInputStream("文件名");
//将标准输入重定向到文件字节输入流
System.setIn(fis);
//创建标准输入对象,因为做了重定向,所以这里不从键盘获取输入
Scanner  sc=new Scanner(System.in);
//把回车作为分隔符
sc.useDelimiter("\n");

//上面代码将标准输入流重定向到了一个文件,运行时不会等待键盘输入
//而会直接读该文件

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

java的Runtime对象有一个exec()方法,可以运行平台上的其他程序,该方法会产生一个Process对象,

Process对象表示由该java程序启动的子进程

Process(加工)类提供的方法:

InputStream  getErrorStream()    获取子进程的错误流

InputStream  getInputStream()    获取子进程的输入流

OutputStream   getOutputStream()     获取子进程的输出流

这里的输入输出流非常容易混淆,就是使用错误,假设子程序要读程序中的数据,应该使用输出流,

使用Process类提供的输入输出流方法时,要站在主程序的角度看


只要使用程序的输入输出流,就应该站在运行本程序所在的内存角度,来衡量使用流


使用process类的方法

//运行javac命令,返回运行该命令的子进程
Process p=Runtime.getRuntime().exe("javac");
//以p进程的错误流,创建BufferedReader对象,
//下面这段代码应该放在try后的括号内,以便自动关闭资源
BufferedReader  br=new  BufferedReader(new InputStreamReader(p.getErrorStream()))
//解释一下上面这行代码
//缓冲区读取内容,避免中文乱码(将字节流转换为字符流(读取p进程的错误信息))
String  buff=br.readLine();


//上面程序,对于主程序来说是输入,对如子进程是输出

主程序如果要获取,流向子进程的数据流,应该使用getOutputStream()方法,因为流入子进程,对主程序来说是输出

这里有个有趣的小栗子,感兴趣可以自己看看,这里大概说说

创建了java类,但没有直接运行,而是使用 Runtime.getRuntime().exec(“程序名”) 来运行,并获取了子进程,使用该子进程的输出流方法(getOutputStreaam())创建了打印输出流,调用打印输出流的println方法输出信息

//运行程序,获得子进程
Process  p=Runtime.getRuntime().exec("java  ReadSt");
//创建打印输出流,值得注意的是,PrintStream永远不会抛出IOException异常
PrintStream ps=new PrintStream(p.getOutputStream())
//调用方法输出信息,这些信息将会被ReadSt读到
ps.println("任意信息");



//定义ReadSt类
//创建标准输入
Scanner  sc=new  Scanner(System.in);
//创建打印流
PrintStream  ps=new  PrintStream(new  FileOutputStream("a.txt"));
//增加回车做分隔符
sc.useDelimiter("\n");

//上面程序先是运行程序,获得子进程,之后包装子进程的输出流
//(虽然是子进程的输出流方法,但对子进程来说是输入流)包装为打印输出流,
//再打印信息

10.7 RandomAccessFile

该类可以写文件,读文件,可以自由访问文件(RandomAccessFile)的任意位置,并且可以向一个文件追加内容

RandomAccessFile类允许自由定位文件记录指针,所以对一个文件的读写非常自由

RandomAccessFile对象也包含一个记录指针,用于表示当前读写位置,指针的默认位置是文件头(0处),读写文件时,指针会自动向后移动对应个字节


RandomAccessFile类提供了移动指针的方法

    long    getFilePointer()    返回指针当前位置            

    void    seek(long  pos)    将指针定位到pos位置

RandomAccessFile类还提供了类似InputStream的3个read()方法,用于读

类似OutputStream的3个write()方法,用于写,还有各种readXxxx()和writeXxx()


RandomAccessFile的构造方法

该类有两个构造方法,基本差不多,一个传String,一个传File,也就是文件本身,另外还需要一个mode参数,该参数指定RandomAccessFile的访问模式

“r”            只读,要是写抛出IOException异常

“rw”         可读,可写,文件不存在则创建

“rws”        可读,可写,相对于"rw",同步文件的内容和元数据的更新

“rwd”        可读,可写,相对于“rw”,同步文件内容的更新

将指针移到文件末尾 实例.seek(实例.length())

向文件写内容,RandomAccessFile实例.write(字符串.getBytes())


RandomAccessFile不能直接向文件中间,指定位置插入内容,如果这样插入,将会覆盖文件中原有的内容,如实在需要向文件中间写,可以将插入点之后的内容的读入缓冲区,再写入内容,之后将缓冲区内容写入文件


10.8 对象序列化

10.8.1 序列化的含义和意义

如果需要让某个类支持序列化,则该类要实现 Serializable 或 Externalizable 接口

java很多类已经实现了Serializable,该接口只是一个标记接口(表明该类是可以被序列化的),没有任何实现方法

所有在网络上传输的类,都是可序列化的

需要保存在磁盘中的类,都要可序列化


javaEE的基础,RMI(Remote Method Invoke ,即远程方法调用),因为需要在网络上传输,所以RMI传递参数,返回值,需要实现序列化,所以序列化是javaEE的基础


10.8.2 使用对象流实现序列化

1.序列化

1)建立一个ObjectOutputStream实例,因为他是一个处理流,必须要建立在其他节点流上

objectoutputstream  oos=
new objectoutputstream(
new  Fileoutputstream("object.txt"))

2)调用ObjectOutputStream类的writeObject()方法,输出序列化对象


2.反序列化

1)建立一个ObjectInputStream输入流(处理流,要建立在节点流上)

ObjectInputStream   io=new  ObjectInputStream(
new  FileInputStream("文件名"))

2)用objectinputstream类的readObject()方法,读取输入流中的对象

TestClass a=io.readObject();


一些特殊的点

1.上面反序列化只能读取java对象的数据,要恢复java对象,需要提供class文件,

在objectinputstream类中的readobject()方法也声明了,会抛出classnotfoundException异常

2.反序列机制可以跳过构造方法,来生成一个对象(克隆一个对象)

3.被序列化类,它的父类有两种情况,可序列化或只有无参构造方法(否则反序列化时,将抛出InvalidClassException(无效类或不承认类))

其中带有无参构造,不能被序列化的父类,它的成员变量不会被序列化到二进制流种


10.8.3 对象引用的序列化

序列化一个类时,该类中的成员,如果不是基本或者引用(自定义类,类型),则需要该类型的类也是可序列化的

多个引用指向同一对象问题

如果A类被B,C类引用,当B类序列化后,A也将被序列化,当C序列化后,B会再次被序列化,当反序列化时,原本B,c指向同一个A,这时却分别指向不同的A实例

java序列化算法

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

        2.当程序序列化一个对象时,程序将会查,该对象是否为首次序列化,如是,才会执行序列化

        3.如某个对象已经序列化了,程序会直接输出一个序列化编号

序列化算法带来的问题

存在一个A类,有成员变量age为19时,该类对象被序列化输出,之后改变age为30,再次序列化时,因为java使用的算法,改动的对象并不会被序列化,只是返回一个序列化编号,所以该类的改动不会被同步,反序列化后,这之间对该类的改动将被忽略


10.8.4 自定义序列化

递归序列化

当对某个对象序列化时,系统会自动将该类的所有成员变量进行序列化,如果有成员变量引用到了另一个对象,该对象也会被序列化

transient关键字

transient只能修饰成员变量

被修饰的成员变量,在序列化时会被忽略(被隔离),反序列化时,该元素值为空


自定义序列化需要实现一些特殊方法,

//写入特定类的实例状态,以便使用readObject()方法来恢复,在默认情况下,
//该方法会调用out.defaultWriteObject来保存对象的成员变量
private void writeObject(java.io.ObjectOutputStream out)throws IOException


//从流中读取并恢复成员变量,默认情况下,会调用in.defaultReadObject来恢复
//java对象的非静态和非瞬态成员变量
private void readObject(java.io.ObjectInputStream in)throws IOException, ClassNotFoundException;


//当序列化流不完整时,用来正确的初始化,序列化对象
private void readObjectNoData()throws ObjectStreamException;

值得注意的是,这些方法在序列化时,还可对变量做一定程度的加密,比如序列化时,先将变量转为StringBuffer流,再反转后写入


自定义序列化时,替换序列化对象

在序列化时,将序列化的某个对象替换为该对象,需要实现下列方法

// ANY-ACCESS-MODIFIER   意味所有权限都可修饰
ANY-ACCESS-MODIFIER  Object  writeReplace() throws  ObjectStreamException

值得注意的是,只要该方法权限允许,子类可以继承到该方法

假设A类具有该方法,系统保证在序列化A类之前,先调用该方法(优先于writeObject()方法),所以表面看起来是在序列化A类,实际在序列化writeReplace()方法中的类


反序列化时,替换对象(保护性复制整个对象)

 //该方法会在readObject()方法执行后被调用,会用该方法的返回值将会代替
 //原来的反序列化对象,而原来的readObject()反序列化对象将会被立刻抛弃
ANY-ACCESS-MODIFIER  Object  readResolve() throws  ObjectStreamException

如果权限允许,子类可以集成到该方法

对于单例类,枚举类,在实现序列化时,都应该提供readResolve()方法,保证序列化的正确性


由于以上两个方法在权限允许的情况下,可以被子类继承,如果子类忘了重写该方法,在序列化时,将会的到一个父类对象,所以建议在除了final类之外的所有类,readResolve和writeReplace都用private修饰


10.8.5 另一种自定义序列化机制(Externalizable接口)

实现Externalizable接口,序列化方式完全由开发人员决定(存储和恢复)

该接口内的两个方法

//用来反序列化,该方法调用  
//Datalnput(Objectlnput的父接口)的方法 恢复基本类型,
//Objectlnput的readObject()方法恢复引用类型
void  readExternal(Objectlnput    in)

//用来序列化,该方法调用
//DataOutput的方法来保存基本类型
//调用ObjectOutput的writeObject()方法保存引用类型
void  writeExternal(ObjectOutput   out)

关于Serializable(标准序列化)和Externalizable(自定义序列化)对比

Serializable接口Externalizable接口
序列化成员系统自动决定开发者决定
实现方法只需实现接口需要实现readExternal()和writeExternal()两个方法
性能略差略好

对象序列化注意事项

  1. 对象类名,成员变量(基本,数组,引用)都会被序列化,

    方法,静态成员(static Field),瞬态成员(transient Field)不会被序列化

  2. 让某个成员变量不被序列化的两种方法 (1).添加transient(瞬态) (2).添加static(静态,不推荐)

  3. 反序列化时,必须要有序列化对象的class文件

  4. 当序列化文件被传输后,必须按写入顺序读取


10.8.6 版本

版本升级带来的class文件兼容问题

java在反序列化时,需要提供对应的class文件,所以要考虑不同版本字节码文件兼容问题

java序列化机制为序列化类提供了一个 private static final long(私有不可变静态值)的serialVersionUID值,用于标识该java序列化的版本,如果一个类升级后,只要它的serialVaersionUID值保持不变,序列化机制也会把他当作同一个序列化版本

建议显示指定该值,如不指定,该值将由JVM根据类的相关信息计算,而修改后类的计算结果,与之前往往不同,从而导致反序列化时版本不兼容而失败


查看该类的UID值

可以通过JDK安装目录bin下的 serialver.exe工具来查看该类的serialVersionUID值,在该命令运行时,指定一个show选项,还有命令行界面

反序列化失败的情况

如果修改类时修改了非静态Field、非瞬态Field,则可能导致序列化版本不兼容。

  • 如果对象流中的对象和新类中包含同名的Field,而Field类型不同,则反序列化失败,类定义应该更新serialVersionUID Field值。

  • 如果对象流中的对象比新类中包含更多的Field,则多出的Field值被忽略,序列化版本可以兼容,类定义可以不更新serialVersionUID Field值;

  • 如果新类比对象流中的对象包含更多的Field,则序列化版本也可以兼容,类定义可以不更新serialVersionUID Field值;

  • 但反序列化得到的新对象中多出的Field值都是null(引用类型Field)或0(基本类型Field

  • 如果新类比对象流中的对象包含更多的Field,则序列化版本也可以兼容,类定义可以不更新serialVersionUID Field值;


10.9 NIO

在java.io包下的输入输出流,都是阻塞式IO,并且底层通过字节来处理,效率

不高(IO每次只能处理一字节)

JDK1.4开始,java对IO进行了改进,陈为NIO(新IO),放在java.nio包下


10.9.1 Java新IO概述(NIO JDK1.4加入)

NIO对输入输出使用了内存映射的方式来处理IO,提高效率

Channel(通道)是对传统IO的模拟,与lnputstream和OutputStream最大区别是,Channel提供了一个map()方法,可以将一块数据映射到内存中

Buffer可以理解为一个容器,本质是一个数组,所有对Channel中数据存取,都要先通过Buffer

Charset类,将Unicode字符串映射为字节序列及逆映射操作

Selector类,支持非阻塞式IO

10.9.2 使用Buffer

Buffer是一个抽象类

最常用的子类是ByteBuffer,它可在底层字节数组上进行get/set操作

除此之外,对其他基本数据类型都有相应的Buffer类(除boolean)

  • CharBuffer

  • ShortBuffer

  • IntBuffer

  • LongBuffer

  • FloatBuffer

  • DoubleBuffer

获得对象

allocate(容量),创建一个指定容量的Buffer对象


ByteBuffer的子类MapperByteBuffer

用于表示Channel将磁盘文件全部或部分,映射到内存中的结果

通常该对象由Channel的map()方法返回


Buffer中的三个概念

容量(capacity):表示Buffer的最大数据容量,不可为负,创建后不能改变

界限(limit):缓存区读写界限,该索引位置之后的数据不可被读写

位置(position):他是一个索引,用于指向缓冲区中,马上要被读写的位置

Buffer中支持一个可选标记(mark),Buffer允许将position定位到标记处

上面四个值满足如下关系

0 <= mark <= position <= limit <= capacity


Buffer类的读写准备(filp/clear)

Buffer类的put()方法,向Buffer中放入数据(或从Channer中读数据),放入数据后,Buffer的position相应后移

Buffer类的flip()方法,将limit设为position所在位置,并将position设为0(开始位置),读数据准备

Buffer类的clear()方法,将position设为0,将limit设为capacity,写数据准备


  • Buffer的常用方法

  • int capacity():返回Buffer的capacity大小

  • boolean hasRemaining():判断当前位置(position)和界限(limit)之间是否还有元素可供处理

  • int limit():返回Buffer的界限(limit)的位置

  • Buffer limit(int newLt):重新设置界限(limit)的值,并返回一个具有新界限的缓冲区对象

  • Buffer mark():设置Buffer的mark位置,只能位于o和position之间

  • int posttion():返回Buffer中的,position值

  • Buffer posttion(int newPs):设置Buffer中的position,并返回一个position被修改后的Buffer对象

  • int remaining():返回当前位置和界限(limit)之间的元素个数

  • Buffer reset():将位置(position)转到mark所在位置

  • Buffer rewind():将位置(position)设为0,取消设置的mark

  • put():向Buffer中放数据

  • get():从Buffer中取数据


put/get访问数据的模式

  • 相对(Relative)从Buffer的当前position位置开始读写,将position按处理元素的个数增加

  • 绝对(Absolute)根据索引,从Buffer中读写,position不变


直接创建Buffer对象(仅限ByteBuffer)

ByteBuffer类的allocatrDirect()方法,用于直接创建Buffer,对比allocate()方法创建成本高,但读取效率也高


10.9.3 使用Channel

Channel与传统IO的区别

  • Channel可直接将指定文件全部,或部分,直接映射为Buffer

  • Channel只与Buffer交互,程序不能直接访问Channel


Channel的实现类

  • 支持线程通信的管道 Pipe.SinkChannel,Pipe.SourceChannel

  • 支持TCP网络通信的管道 ServerSockChannel,SocketChannel

  • 支持UDP网络通信的管道 DatagramChannel


获得Channel对象

不应通过构造器来获得对象,而是通过InputStream,OutputStream的getChannel()方法,来返回对应的Channel

Channel的映射方法map()

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

第一个参数执行映射时的模式,分别有只读、读写等模式;

而第二个、第三个参数用于控制将Channel的哪些数据映射成ByteBuffer。

注意的是,RandomAccessFile类中也有一个getChannel()方法,返回的FileChannel是只读还是只写,取决于RandomAccessFile文件的打开方式


10.9.4 字符集和Charset

编码(Encode),解码(Decode)

获取系统字符集

java默认使用Unicode字符集,在JDK1.4时,提供了charset来处理字节序列和字符序列之间的转换关系,该类包含创建解码器和编码器的方法,并且是一个不可变类

availableCharsets():获取当前JDK所支持的所有字符集方法

该方法获得的,是一个map键值对集合

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

查看文件编码格式,创建对应编码/解码器

System类中的getProperties(文件名.后缀名)方法可以访问本地系统的文件编码格式

可以使用charset类的forName(编码格式)方法来创建对应的Charset对象,获得该对象之后,就能创建对应的编码器(CharsetEncode()方法)和解码器(CharsetDecode()方法)

字符序列和字节序列互转

将CharBuffer(字符序列)/String转换为ByteBuffer(字节序列)

       **编码器(Encode)** 的encode()方法

将ByteBuffer(字节序列)转换为CharBuffer(字符序列)

        **解码器(Decode)** 的decode()方法        

charset类提供的字符/字节转换方法

将ByteBuffer中的字节序列转换为字符序列

        CharBuffer    decode(ByteBuffer   bb)

将CharBuffer中的字符序列转换为字节序列

        ByteBuffer    encode(CharsetBuffer   cb)

将string中的字符序列转为字节序列

        ByteBuffer    encode(String   str)

StandardCharsets类

该类包含了UTF_8,UTF_16等静态成员变量,这些成员变量就代表了最常用字符集对应的Charset对象


10.9.5 文件锁

文件锁类似资源锁,在一个进程使用该资源时,别的进程不能访问

Java在NIO中提供了FileLock来支持文件锁,

获得文件锁对象(FileLock)

FileChannel(文件映射)类的lock()或tryLock()方法,可以获得文件锁FileLock对象,从而锁定文件

Lock/tryLock方法

lock()方法,该方法视图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞,获得锁为排他锁


tryLock()方法是尝试锁定文件,该方法不会阻塞,获得文件锁则返回锁,否则返回null,获得锁为排他锁


lock(long position,long size,boolean shared)

对文件从position开始,长度为size的内容加锁,该方法是阻塞式的

tryLock(long position,long size,boolean shared)

非阻塞式部分加锁方法,参数含义同上

当shared为true时,表明该锁为共享锁,否则为排他锁

共享锁:允许多个进程来读取该文件,但阻止其他进程获得该文件的排他锁

排他锁:将锁住对该文件的读写


查看锁类型

FileLock类得isShared()方法判断,是否为共享锁

关于文件锁

  • 文件锁可以用来控制并发访问,但对于高并发,还是用数据库

  • 某些平台,文件锁只是建议性得,也就是说,程序不获得文件锁,也可对该文件进行读写

  • 某些平台上,不能同步锁住一个文件,并把它映射到内存中

  • 文件锁为java虚拟机所持有,同一虚拟机的两个程序不能对同一文件加锁

  • 在某些平台上,关闭FileChannel时,会释放java虚拟机在该文件上的所有锁,因此应避免,对同一个被锁定的文件打开多给FileChannel


10.10 Java 7的NIO.2(AIO)

java7对原有的NIO进行了改进

  • 提供了全面的文件IO和文件系统访问支持(新增的java.nio.file包及子包)

  • 基于异步Channel的IO(java.nio.channels包下增加的Asynchronous开头的Channel接口和类)


10.10.1 Path、Paths和Files核心API

NIO.2引入了Path接口,代表平台无关的路径

提供了工具类Files(静态工具方法),Paths(包含两个返回Path的静态工厂方法)

Files和Paths两个工具类非常符合Java一贯的命名风格,比如前面介绍的操作数组的工具类为Arrays,操作集合的工具类为Collections,这种一致的命名风格可以让读者快速了解这些工具类的用途。

这两个类有大量的操作方法,如果需要,可以翻看API


10.10.2 使用FileVisitor遍历文件和目录

在没有Files工具类时,java遍历指定目录下的文件和子目录,只能使用递归遍历


//遍历start路径下的所有文件和子目录。
walkFileTree(Path start,  FileVisitor<?super Path>visitor)

//与上一个方法的功能类似。该方法最多遍历maxDepth深度的文件。
walkFileTree(Path start,  Set<FileVisitOption>options,
int maxDepth,  FileVisitor<?super Path>visitor)

上面两个方法都需要FileVisitor参数,该参数代表一个文件访问器

walkFileTree()方法会自动遍历start路径下的所有文件和子目录,遍历时会触发FileVisitor中的对应方法

  • FileVisitResult postVisitDirectory(T dir, IOException exc) 访问子目录后触发该方法

  • FileVisitResult preVisitDirectory(T dir, BasicFileAttributes e attrs) 访问子目录之前触发该方法

  • FileVisitResult visitFile(T file, BasicFileAttributes attrs):访问file文件时触发该方法。

  • FileVisitResult visitFileFailed(T file, IOException exc):访问file文件失败时触发该方法。


上面方法的返回值,FileVisitResult 是一个枚举类,代表了访问之后的后续行为,它定义了如下几种后续行为

  • COUNTINUE:继续访问,的后续行为

  • SKIP_SIBLINGS:继续访问,的后续行为,但不访问该文件或目录的兄弟文件或目录

  • SKIP_SUBTREE:继续访问,的后续行为,但不访问该文件或目录的子目录树

  • TERMINATE:中止访问,的后续行为


使用上述方法查找指定文件

 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 + "文件");   

// 找到了FileVisitorTest.java文件   
 if (file.endsWith("FileVisitorTest.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;    

 }               
    });     
        }  
       }

10.10.3 使用WatchService监控文件变化

在java7之前,监控文件变化需要启动一条线程,每隔一段时间遍历一次文件,如遍历结果与上次不同,则认为发生变化


NIO.2提供的监听文件系统变化

//Path类下的方法
//用watcher监听该path代表的目录下的文件变化。events参数指定要监听哪些类型的事件。
register( WatchService watcher,  WatchEvent.Kind<?>...events)
上面

上面方法中的WatchService代表一个文件系统监听服务,负责监听path代表的目录下的文件变化

使用register()方法完成注册后,就可调用WatchService的方法获取被监听文件的变化事件

  • WatchKey poll():获取下一个WatchKey,如不存在,返回null

  • WatchKey poll( long timeout, TimeUnit unit):尝试等待timeout时间去获取下一个WatchKey。

  • WatchKey take():获取下一个WatchKey,如果没有WatchKey发生就一直等待。

一直监视文件变化:用take()方法

指定监视事件:poll()方法


10.10.4 访问文件属性

  • XxxAttributeView:代表某种文件属性的“视图”。

  • XxxAttributes:代表某种文件属性的“集合”,程序一般通过XxxAttributeView对象来获取XxxAttributes。


AclFileAttributeView:为特定文件设置ACL(Access Control List 访问控制列表)及文件所有者属性。

它的getAcl()方法返回List对象,该返回值代表了该文件的权 限集。

通过setAcl(List)方法可以修改该文件的ACL。


BasicFileAttributeView:它可以获取或修改文件的基本属性,包括文件的最后修改时间、最后访问时间、创建时间、大小、是否为目录、是否为符号链接等。

它的readAttributes()方法返回一个BasicFileAttributes对象,对文件夹基本属性的修改是通过BasicFileAttributes对象完成的。


DosFileAttributeView:它主要用于获取或修改文件DOS相关属性,比如文件是否只读、是否隐藏、是否为系统文件、是否是存档文件等。

它的readAttributes()方法返回一个DosFileAttributes对象,对这些属性的修改其实是由DosFileAttributes对象来完成的。


FileOwnerAttributeView:它主要用于获取或修改文件的所有者。

它的getOwner()方法返回一个UserPrincipal对象来代表文件所有者;也可调用setOwner(UserPrincipal owner)方法来改变文件的所有者。


PosixFileAttributeView:它主要用于获取或修改POSIX(Portable Operating System Interface of INIX 可移植操作系统接口)属性

它的readAttributes()方法返回一个PosixFileAttributes对象,该对象可用于获取或修改文件的所有者、组所有者、访问权限信息(就是UNIX的chmod命令负责干的事情)。这个View只在UNIX、Linux等系统上有用。


UserDefinedFileAttributeView:它可以让开发者为文件设置一些自定义属性。

这里有一个非常常见的面试题,BIO,NIO,AIO的区别

BIO,同步阻塞
NIO,同步非阻塞
AIO,非同步非阻塞
如需进一步了解 稀土掘金-BIO,NIO,AIO


后言

昨天搬了新房子,31层,巨高,
周围莫名其妙的繁琐事让我烦心,未来巨大的迷茫和现在巨大的压力
很累
昨天晚上失眠,凌晨两点我坐在床上看着窗外的西安灯火阑珊,看着城市在呼吸
一种不可遏制的自我毁灭之火,映射出的火光,反复闪过我的脑海

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值