java 输入/输出(学习笔记)

输入/输出(IO流)

java的IO通过java.io包下的类和接口来支持,在java.io包下主要包括输入、输出两种IO流,每种输入、输出流又可分为字节流和字符流两大类。其中字节流以字节为单位来处理输入、输出操作,而字符流则以字符来处理输入、输出操作。除此之外,java的IO流使用了一种装饰器设计模式,它将IO流分成底层节点流和上层处理流,其中节点流用于和底层的物理存储节点直接关联——不同的物理节点获取节点流的方式可能存在一定的差异,但程序可以把不同的物理节点流包装成统一的处理流,从而允许程序使用统一的输入、输出代码来读取不同的物理存储节点的资源。

File类

File类是java.io包下代表与平台无关的文件和目录,如果希望在程序中操作文件和目录,都可以通过File类完成。值得提出的是,不管是文件还是目录都是使用File来操作的,但File不能访问文件内容本身。如果需要访问文件内容本身,则需要使用输入/输出流。

访问文件和目录

File类可以使用文件路径字符串来创建File实例,该文件路径字符串既可以是相对路径,也可以是绝对路径。在默认情况下,系统总是根据用户的工作路径来解释相对路径,这个路径由系统属性“user.dir”指定,通常也就是运行java虚拟机所存在的路径。

下面列举常用的方法

太多不写

注意:
Windows的路径分隔符使用反斜线(),而java程序中的反斜线表示转义字符,所以如果需要在Windows的路径下包括反斜线,则应该使用两条反斜线,如F:\abc\test.txt,或者直接使用斜线(/)也可以,java程序支持将斜线当成平台无关的路径分隔符。

文件过滤器

在File类的list()方法中可以接收一个FilenameFilter参数,通过该参数可以只列出符合条件的文件。

FilenameFilter接口里包含了一个accept(File dir,String name)方法,该方法将依次对指定File的所有子目录或者文件进行迭代,如果该方法返回true,则list()方法会列出该子目录或者文件。

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

String[] nameList=file.list((dir,name)-> name.endsWith(".java")
||new File(name).isDirectory());
for(String name:nameList){
	System.out.println(name);
}

该程序会列出所有*.java文件和文件夹。

理解java的IO流

java的IO流是实现输入/输出的基础,它可以方便地实现数据地输入/输出操作,在java中把不同地输入/输出源(键盘、文件、网络连接等)抽象表述为“流”(stream),通过流的方式来访问不同的输入/输出源。stream是从起源(source)到接收(sink)的有序数据。

java把所有传统的流类型(类或抽象类)都放在java.io包中,用以实现输入/输出功能。

因为java提供了这种IO流的抽象,所以开发者可以使用一致的IO代码去读写不同的IO流节点。

流的分类

根据不同的方式,可以将流分为不同的类型。

1.输入流和输出流
输入流:只能从中读取数据,而不能向其写入数据。
输出流:只能向其写入数据,而不能从中读取数据。

数据从内存到硬盘,通常称为输出流;从硬盘到内存,通常称为输入流。这里的输入、输出都是从程序运行所在内存的角度来划分的。

java的输出流主要由InputStream和Reader作为基类,而输出流则主要由OutputStream和Writer作为基类。它们都是抽象基类,无法直接创建实例。

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

3.节点流和处理流
按照流的角色来分,可以分为节点流和处理流。

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

处理流则用于对一个已存在的流进行连接或封装,通过封装后的流来实现数据读/写功能。处理流也被称为高级流。
当使用处理流进行输入/输出时,程序并不会直接连接到实际的数据源,没有和实际的输入/输出节点连接。使用处理流的一个明显好处是,只要使用相同的处理流,程序就可以采用完全相同的输入/输出代码来访问不同的数据源,随着处理流所包装节点流的变化,程序实际所访问的数据流也相应地发生变化。

流的概念模型

java把所有设备里的有序数据抽象成流模型,简化了输入/输出处理,理解了流的概念模型也就了解了javaIO。

java的IO流的40多个类都是从如下4个抽象基类诞生的:

1.InputStream/Reader:所有输入流的基类,前者是字节输入流后者是字符输入流
2.OutputStream/Writer:所有输出流的基类,前者是字节输出流后者是字符输出流

对于InputStream和Reader而言,它们把输入设备抽象成一个水管,水管中的水滴依次排列。输入流使用隐式的记录指针来标识当前正准备从哪个水滴开始读取,每当程序从InputStream和Reader中取出一个或多个水滴后,记录指针自动向后移动。

对于OutputStream和Writer而言,它们同样把输出设备抽象成一个水管,只是水管里没有任何水滴。当执行输出时,程序相当于依次把水滴放入当输出流的水管当中,输出流同样采用隐式的记录指针来标识当前水滴即将放入的位置,每当程序向OutputSteam和Writer里输出一个或多个水滴后,记录指针自动向后移动。

字节流和字符流

字节流和字符流操作方式几乎完全一样,区别只是操作的数据单元不同——字节流操作的数据单元是字节,字符流操作的数据单元是字符。

InputStream和Reader

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

InputStream里包含三种方法:

  1. int read():从输入流中读取单个字节(相当于取出一个水滴),返回所读取的字节数据(字节数据可直接转为int型)
  2. int read(byte[] b):从输入流中最多读取b.length个字节的数据,并将其存储在字节数组b中,返回实际读取的字节数。
  3. int read(byte[] b,int off,int len):从输入流中最多读取len个字节的数据,并将其存储在数组b中,放入数组b时,并不是从起点开始而是从off位置开始。返回实际读取的字节数。

Reader里包含的方法和InputStream中的方法相同,只是以字符为单位。

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

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

  1. void mark(int readAheadLimit):在记录指针当前位置记录一个标记(mark)。
  2. boolean markSupported():判断此输入流是否支持mark()操作,即是否支持记录标记
  3. void reset():将此流的记录指针重新定位到上一次记录标记(mark)的位置。
  4. long skip(long n):记录指针向前移动n个字节/字符。

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可以用字符串来代替字符数组。Writer里还包含如下方法

  1. void write(String str):将str字符串里包含的字符输出到指定输出流中。
  2. void write(String str,int off,int len):将str字符串里从off位置开始,长度为len的字符输出到指定输出流中。

Windows平台的通用的换行符是\r\n,如果是UNIX/Linux/BSD等平台,使用\n作为换行符。

输入/输出流体系

处理流的用法

处理流可以隐藏底层设备上节点流的差异,并对外提供更加方便地输入/输出方法,让程序员只需关心高级流地操作。

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

实际识别处理流非常简单,只要流的构造器参数不是一个物理节点,而是已存在的流,那么这种流就一定是处理流;而所有节点流都是直接以物理IO节点作为构造器参数的。

程序中使用的System.out的类型就是PrintStream。

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

输入/输出流体系

java的输入/输出流体系之所以这么复杂,主要是因为java为了实现更好的设计,它把IO流按功能分成了许多类,而每类中又分别提供了字节流和字符流,字节流和字符流里又分别提供了输入流和输出流两大类。

通常来说,字节流的功能比字符流的功能强大,因为计算机里所有的数据都是二进制的,而字节流可以处理所有的二进制文件——但问题是,如果使用字节流来处理文本文件,则需要使用合适的方式把这些字节转换成字符,这就增加了编程的复杂度。所以通常有一个规则:如果进行输入/输出的内容是文本内容,则应该考虑使用字符流;如果进行输入/输出的内容是二进制内容,则应该考虑使用字节流。

StringReader的构造方法内可以是字符串,而创建StringWriter时,实际上是以一个StringBuffer作为输出节点,因此StringWriter的构造方法内是StringBuffere的初始长度。

转换流

输入/输出流体系中还提供了两个转换流,这两个转换流用于实现将字节流转换成字符流,其中InputStreamReader将字节输入流转换成字符输入流,OutputStreamWriter将字节输出流转换成字符输出流。

Q:怎么没有把字符流转换成字节流的转换流呢?
A:字节流比字符流的使用范围更广,但字符流比字节流操作方便。如果有一个流已经是字符流了,也就是说已经有一个用起来更方便的流,为什么要转换成字节流呢?反之,如果现在有一个字节流,但可以确定这个字节流的内容都是文本内容,那么把它转换成字符流来处理就会更方便一点,所以java只提供了将字节流转换成字符流的转换流。

java使用System.in代表标准输入,即键盘输入,但这个标准输入流是InputStream类的实例,使用不太方便,而且键盘输入内容都是文本内容,所以可以使用InputStreamReader将其转换成字符输入流,普通的Reader读取输入内容时仍然不太方便,可以将普通的Reader再次包装成BufferedReader,利用BufferedReader的readLine()方法可以一次读取一行内容。

BufferedReader流具有缓冲功能,可以一次读取一行文本——以换行符为标志,如果它没有读到换行符,则程序阻塞,等到读取换行符为止。

推回输入流

在输入/输出流体系中,有两个特殊的流与众不同,就是PushbackInputStream和PushbackReader,它们都提供了如下三个方法:

  1. void unread(byte[]/char[] buf):将一个字节/字符数组内容推回到推回缓存区里,从而允许重复读取刚刚读取的内容
  2. void unread(byte[]/char[] buf,int off,int len):将一个字节/字符数组里从off开始,长度为len字节/字符的内容推回到推回缓存区里,从而允许重复读取刚刚读取的内容。
  3. void unread(int b):将一个字节/字符推回到推回缓冲区里,从而允许重复读取刚刚读取的内容。

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

当程序创建一个PushbackInputStream和PushbackReader时需要指定推回缓冲区的大小,默认为1,如果程序中推回到推回缓冲区的内容超出了推回缓冲区的大小,将会引发Pushback buffer overflow的IOException异常。

重定向标准输入/输出

java的标准输入/输出分别通过System.in和System.out来代表,在默认情况下它们分别代表键盘和显示器,当程序通过System.in来获取输入时,实际上是从键盘读取输入;当程序试图通过System.out输出时,程序总是输出到屏幕。

在System类里提供了如下重定向标准输入/输出的方法:

  1. static void setErr(PrintStream err):重定向"标准"错误输出流。
  2. static void setIn(InputStream in):重定向“标准”输入流。
  3. static void setOut(PrintStream out):重定向“标准”输出流。

如果重定性了输入/输出,则会重定向到文件输入/输出,而不是在屏幕上输出。

RandomAccessFile

RandomAccessFile是java输入/输出流体系中功能最丰富的文件内容访问类,它提供了众多的方法来访问文件内容,它既可以读取文件内容,也可以向文件输出数据,并且支持随机访问的方式,程序可以直接跳转到文件的任意地方来读写数据。

由于RandomAccessFile可以自由访问文件的任意位置,所以如果只需要访问文件部分内容,而不是把文件从头读到尾,使用RandomAccessFile将是更好的选择。

RandomAccessFile允许自由定位文件记录指针,RandomAccessFile可以不从开始的地方开始输出,因此RandomAccessFile可以向已存在的文件后追加内容。如果程序需要向已存在的文件后追加内容,则应该使用RandomAccessFile。

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

RandomAccessFile对象包含了一个记录指针,用以标识当前读写处的位置,当程序新创建一个RandomAccessFile对象时,该对象的文件记录指针位于文件头,当读写了n个字节后,文件记录指针将会向后移动n个字节。除此之外,RandomAccessFile可以自由移动该记录指针,既可以向前移动,也可以向后移动。RandomAccessFile包含如下方法来操作文件记录指针:

  1. long getFilePointer():返回文件记录指针的当前位置
  2. void seek(long pos):将文件记录指针定位到pos位置

RandomAccessFile既可以读文件,也可以写文件,因此包含了完全类似于InputStream的三个read()方法和OutputStream的三个write()方法,除此之外,RandomAccessFile还包含了一系列的readXxx()和writeXxx()方法来完成输入、输出。

RandomAccessFile类有两个构造器,这两个构造器基本相同,只是指定文件的形式不同——一个使用String参数来指定文件名,一个使用File参数来指定文件本身。除此之外,创建该对象时还需要一个mode参数,用于指定访问模式,mode参数有四个值:

  1. “r” 以只读方式打开指定文件
  2. “rw” 以读写方式打开,如果文件不存在,则尝试创建
  3. “rws” 以读写方式打开,还要求对文件的内容或元数据的每个更新都同步写入到底层存储设备。
  4. “rwd” 以读写方式打开,还要求对文件内容的每个更新都同步写入到底层存储设备。

虽然RandomAccessFile可以向文件后追加内容,但仍然不能向文件指定位置插入内容,如果直接将文件记录指针移动到中间某位置后开始输出,则新输出的内容会覆盖文件中原有的内容,如果需要向指定位置插入内容,程序需要先把插入点后面的内容读入缓存区,等把需要插入的数据写入文件后,再将缓存区的内容追加到文件后面。

对象序列化

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

序列化的含义和意义

序列化机制允许将实现序列化的java对象转换成字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以备以后重新恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立的存在。

对象的序列化是指将一个java对象写入IO流中,与此对应的shi,对象的反序列化是指从IO流中恢复该java对象。

如果需要让某个对象支持序列化机制,则必须让它的类是可序列化的,该类必须实现下列两个接口

  • Serializable
  • Externalizable

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

所有可能在网络上传输的对象都必须是可序列化的,所有需要保存到磁盘里的对象的类也必须是可序列化的。

使用对象流实现序列化

使用Serializable来实现序列化非常简单,主要让目标类实现Serializable标记接口即可,无须实现任何方法。

将对象转换为字节序列后,字节序列会呈现为类似乱码的形式,此时应该用反序列化才可以将序列转为java对象。

必须指出的是,反序列化读取的仅仅是java对象的数据,而不是java类,因此采用反序列化恢复java对象时,必须提供该java对象所属类的class文件,否则将引发ClassNotFoundException异常。反序列化机制无须通过构造器来初始化java对象。

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

当一个可序列化类有多个父类时,这些父类要么有无参数的构造器,要么也是可序列化的,否则会抛出异常。如果父类是不可序列化的,只是带有无参构造器,则在父类中定义的成员变量值不会序列化到二进制流中

对象引用的序列化

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

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

  • 所有保存到磁盘中的对象都有一个序列号编号
  • 当程序试图序列化一个对象时,程序将先检查该对象是否已经被序列化过,只有该对象从未(在本次虚拟机中)被序列化过,系统才会将该对象转换成字节序列输出。
  • 如果某个对象已经被序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象。

如果多次序列化同一个java对象时,只有第一次序列化时才会把该java对象转换成字节序列并输出,这可能会引起一个潜在的问题——当程序序列化一个可变对象时,只有第一次使用writeObject()方法输出时才会将该对象转换为字节序列并输出,当程序再次调用方法时,程序只是输出前面的序列化编号,即使后面该对象的实例变量值已被改变,改变的实例变量值也不会被输出。

自定义序列化

递归序列化
当对某个对象进行序列化时,系统会自动把该对象的所有实例变量依次进行序列化,如果某个实例变量引用到另一个对象,则被引用的对象也会被序列化;如果被引用的对象的实例变量也引用了其他对象,则被引用的对象也会被序列化,这种情况称为递归序列化

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

transient关键字只能用于修饰实例变量,不可修饰java程序中的其他成分

在序列化和反序列化过程中需要特殊处理的类应该提供如下特殊签名的方法,这些特殊的方法用以实现自定义序列化

  • private void writeObject(java.io.ObjectOutputStream out) throws IOException;
  • private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException;
  • private void readObjectNoData() throws ObjectStreamException;

writeObject()方法负责写入特定类的实例状态,以便相应的readObject()方法可以恢复它。通过重写该方法,程序员可以完全获得对序列化机制的控制,可以自主决定哪些实例变量需要序列化,需要怎样序列化。

readObject()方法负责从流中读取并恢复对象实例变量,通过重写该方法,程序员可以完全获得对反序列化机制的控制,可以自主决定需要反序列化哪些实例变量,以及如何进行反序列化。在通常情况下,readObject()方法与writeObject()方法对应,如果writeObject()方法中对java对象的实例变量进行了一些处理,则应该在readObject()方法中对其实例变量进行相应的反处理,以便正确恢复该对象。

当序列化流不完整时,readObjectNoData()方法可以用来正确地初始化反序列化地对象。例如:接收方使用地反序列化地版本不同于发送方,或者接收方版本扩展地类不是发送方版本扩展地类,或者序列化流被篡改时,系统都会调用readObjectNoData()方法来初始化反序列化的对象。

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

Object writeReplace() throws ObjectStreamException;

此writeReplace()方法将由序列化机制调用,只要该方法存在。因为该方法可以拥有私有的、受保护的和包私有等访问权限,所以其子类有可能获得该方法。

java的序列化机制保证在序列化某个对象之前,先调用该对象的writeReplace()方法,如果该方法返回另一个Java对象,则系统转为序列化另一个对象。

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

Object readResolove() throws ObjectStreamException;

这个方法会紧接着readObject()方法之后被调用,该方法的返回值将会代替原来反序列化的对象,而原来readObject()反序列化的对象将会被立即丢弃。

另一种自定义序列化机制

java还提供了另一种序列化机制,这种序列化方式完全由程序员决定存储和恢复对象数据。要实现该目标,java类必须实现Externalizable接口。该接口有如下方法

  • void readExternal(ObjectInput in):需要序列化的类实现readExternal()方法来实现反序列化
  • void writeExternal(ObjectOutput out):需要序列化的类实现writeExternal()方法来保存对象的状态

实际上,采用实现Externalizable接口方式的序列化与前面介绍的自定义序列化非常相似,只是Extermalizable接口强制自定义序列化。

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

版本

java序列化机制允许为序列化类提供一个private static final的serialVersionUID值,该类变量的值用于标识该JAVA类的序列化版本,也就是说,如果一个类升级后,只要它的serialVersionUID类变量值保持不变,序列化机制也会把它们当成同一个序列化版本。

为了在反序列化时确保序列化版本的兼容性,最好在每个要序列化的类中加入这个类变量,具体数值自己定义。如果不显示定义类变量的值,该类变量的值将由JVM根据类的相关信息计算,而修改后的类的计算结果与修改前的类的计算结果往往不同,从而造成对象的反序列化因为类版本不兼容而失败。

不显示指定serialVersionUID类变量的值的另一个坏处是,不利于程序在不同的JVM之间移植。

NIO

前面介绍的输入流、输出流都是阻塞式的输入、输出。不仅如此,传统的输入流输出流都是通过字节的移动来处理的。也就是说面向流的输入输出系统一次只能处理一个字节,因此面向流的输入/输出系统效率不高。

java新IO概述

新IO使用了不同的方式来处理输入输出,新IO采用了内存映射的方式来处理输入输出,将文件或文件的一段区域映射到内存中,这样就可以向访问内存一样来访问文件了。

Channel(通道)和Buffer(缓冲)是新IO中的两个核心对象,Channel是对传统的输入/输出系统的模拟,在新IO系统中所有的数据都需要通过通道传输,Channel提供了一个map()方法,通过该方法可以直接将“一块数据”映射到内存中,新IO是面向块的处理。

Buffer可以被理解为容器,它的本质是一个数组,发送到Channel中的所有对象都必须先被放到Buffer中,而从Channel中读取的数据也必须先放到Buffer中。

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

使用Buffer

从内部结构上看,Buffer就像一个数组,它可以保存多个类型相同的数据。Buffer是一个抽象类,其最常用的子类是ByteBuffer,它可以在底层字节数组上进行get/set操作,可以通过如下方法创建一个容量位capacity的XxxBuffer对象

  • static XxxBuffer allocate(int capacity):创建一个容量为capacity的XxxBuffer对象。

实际使用较多的是ByteBuffer和CharBuffer,其中ByteBuffer类还有一个子类:MappedByteBuffer,它用于表示Channel将磁盘文件的部分或全部内容映射到内存中后得到的结构,该对象通常由Channel的map()方法返回。

在Buffer中有三个重要的概念:容量(capacity),界限(limit)和位置(position)。

  • 容量:缓存区的容量表示该Buffer的最大数据容量,即最多可以存储多少数据。容量不能为负值,创建后不能改变
  • 界限:第一个不应该被读出或者写入的缓冲区位置索引。也就是说,位于limit后的数据既不可以读也不可以写。
  • 位置:用于指明下一个可以被读出或者写入的缓冲区位置索引(类似于IO流中的记录指针)。当使用Buffer从Channel中读取数据时,position的值恰好等于已经读到了多少数据。当刚刚创建一个Buffer对象时,其position的值为0;如果从Channel中读取了2个数据到该Buffer中,则position为2,指向Buffer中第三个数据(第一个位置索引为0)。

Buffer的主要作用就是装入数据,然后输出数据。程序可以通过put()方法向Buffer中放入一些数据,每放入一些数据,Buffer的position相应地向后移动一些位置。

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

flip()为从Buffer中取出数据做好准备,而clear为再次从Buffer中装入数据做好准备。put()用于向Buffer中放入数据,get()用于向Buffer中取出数据,Buffer既支持对单个数据的访问,也支持对批量数据的访问。

当使用put()和get()来访问Buffer中的数据时,分为绝对和相对两种

  • 相对:从Buffer的当前position处开始读取或写入数据,然后将位置的值按处理元素的个数增加。
  • 绝对:直接根据索引向Buffer中读取或写入数据,使用绝对方式访问Buffer里的数据时,并不会影响位置的值。

通过allocate()方法创建的Buffer对象是普通Buffer,ByteBuffer还提供了一个allocateDirect()方法来创建直接Buffer。直接Buffer的创建成本比普通Buffer的创建成本高,但直接Buffer的读取效率更高。

由于直接Buffer的创建成本很高,所以直接Buffer只适用于长生存期的Buffer,而不适用于短生存期、一次用完就丢弃的Buffer。而且只有ByteBuffer才提供了alloacteDirect()方法,所以只能在ByteBuffer级别上创建直接Buffer。如果希望使用其他类型,则应该将该Buffer转换成其他类型的Buffer。

使用Channel

Channel类似于传统的流对象,但与传统的流对象有两个主要区别

  1. Channel可以直接将指定文件的部分或全部直接映射成Buffer。
  2. 程序不能直接访问Channel中的数据,包括读取、写入都不行,Channel只能与Buffer进行交互。

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

Channel中最常用的三类方法是map()、read()和write(),其中map()方法用于将Channel对应的部分或全部数据映射成ByteBuffer;而write()和read()方法都有一系列重载形式,这些方法用于从Buffer中读取数据或向Buffer中写入数据。

map()方法的方法签名为:MappedByteBuffer map(FileChannel.MapMode mode,long position,long size),第一个参数执行映射时的模式,分别有只读、读写等模式;而第二个第三个参数用于控制将Channel的哪些数据映射成ByteBuffer。

在RandomAccessFile中也包含了一个getChannel()方法,RandomAccessFile返回的FileChannel()是只读的还是读写的,则取决于RandomAccessFile打开文件的模式。

字符集和Charset

通常而言,把明文的字符序列转换成计算机理解的二进制序列称为编码,把二进制序列转换成普通人能看懂的明文字符串称为解码。

计算机底层是没有文本文件、图片文件之分的,它只是忠实地记录每个文件的二进制序列。当需要保存文本文件时,程序必须先把文件中的每个字符翻译成二进制序列,当需要读取文本文件时,程序必须把二进制序列转换为一个个字符。

java默认使用Unicode字符集,但很多操作系统并不使用Unicode字符集,那么当从系统中读取数据到java程序中时,就可能出现乱码的问题。

一旦知道了字符集的别名之后,程序就可以调用Charset的forName()方法来创建对应的Charset对象,forName()方法的参数就是相应字符集的别名。获得了Charset对象之后,就可以通过该对象的newDecoder()、newEncoder()这两个方法分别返回CharsetDecoder和CharsetEncoder对象,代表该Charset的解码器和编码器。

调用CharsetDecoder的decode()方法就可以将ByteBuffer转换成CharBuffer,调用CharsetEncoder的encode()方法就可以将CharBuffer转换成ByteBuffer。

Charset类也提供了如下方法:

  • CharBuffer decode(ByteBuffer bb):将ByteBuffer中的字节序列转换成字符序列的便携方法。
  • ByteBuffer encode(CharBuffer cb):将CharBuffer中的字符序列转换成字节序列的便携方法。
  • ByteBuffer encode(String str):将String中的字符序列转换成字节序列的便携方法。

也就是说,获取了Charset对象后,如果仅仅需要进行简单的编码、解码操作,其实无须创建CharsetEncoder和CharsetDecoder对象,直接调用Charset的encode()和decode()方法进行编码,解码即可。

NIO.2的功能和用法

java7对原有的NIO进行了重大改进,改进主要包括如下两方面的内容

  1. 提供了全面的文件IO和文件系统访问支持
  2. 基于异步Channel的IO

Path、Paths和Files核心API

早期的java只提供了一个File类来访问文件系统,但Files类的功能比较有限,它不能利用特定文件系统的特性,File所提供的方法的性能也不高。NIO。2为了弥补这种不足,引入了一个Path接口,Path接口代表一个平台无关的平台路径,还提供了Files、Paths两个工具类,其中Files包含了大量静态的工具方法来操作文件;Path则包含了两个返回Path的静态工厂方法。

getNameCount()方法返回Path路径所包含的路径名的数量,即表示第几层路径。

东西太多了,写不过来

总结

这章的东西非常非常非常非常多,我只是匆匆的看完了,具体的用法在做题中慢慢熟悉,小组布置的学习任务也完成了,接下来就不断地做题,巩固知识,了解各种方法各种对象的用法。

发布了24 篇原创文章 · 获赞 32 · 访问量 1704

猜你喜欢

转载自blog.csdn.net/weixin_45729946/article/details/103397850