Java 的 IO 支持通过 java.io 包下的类和接口来支持,在 java.io 包下主要包括输入、输出两种 IO 流,每种输入、输出流又可分为字节流和字符流两大类。其中字节流以字节为单位来处理输入、输出操作,而字符流以字符来处理输入、输出操作。除此之外,Java 的 IO 流使用了一种装饰器设计模式,它将 IO 流分成底层节点流和上层处理流,其中节点流用于和底层物理存储节点直接关联——不同的物理节点获取节点流的方式可能存在一些差异,但程序可以把不同的的物理节点流包装成统一的处理流,从而允许程序用统一的输入、输出代码来读取不同物理存储节点的资源。
一、File 类
File 类是 java.io 包下代表与平台无关的文件和目录,也就是说如果希望在程序中操作文件和目录都可以通过 File 类来完成,值得指出的是不管是文件、还是目录都可以使用 File 来操作,File 能新建、删除和重命名文件和目录,File 不能访问文件内容本身。如果希望访问文件内容本身,则需要使用输入/输出流。
1、访问文件和目录
可以使用 绝对路径 和 相对路径
访问文件名相关方法:
- String getName()
- String getPath()
- File getAbsoluteFile()
- String getAbsolutePath()
- String getParent()
- boolean renameTo(File newName)
文件检测相关方法:
- boolean exists()
- boolean canWrite()
- boolean canRead()
- boolean isFile()
- boolean isAbsolute()
获取常规文件信息
- long lastModified()
- long length()
文件操作的相关方法
- boolean createNewFile()
- boolean delete()
- static File createTempFile(String prefix, String suffix)
- static File createTempFile(String prefix, String suffix, File directory)
- void deleteOnExit()
目录操作的相关方法
- boolean mkdir()
- String[] list()
- File[] listFiles()
- static File[] listRoots()
示例:
package code15_1;
import java.io.File;
public class FileTest {
public static void main(String[] args) throws Exception {
// 以当前路径来创建一个 File 对象
File file = new File(".");
// 直接获取文件名,输出一点
System.out.println(file.getName());
// 获取相对路径的父路径可能出错,下面代码输出 null
System.out.println(file.getParent());
// 获取绝对路径
System.out.println(file.getAbsolutePath());
// 获取上一级路径
System.out.println(file.getAbsoluteFile().getParent());
// 在当前路径下创建一个临时文件
File tempFile = File.createTempFile("aaa", ".txt", file);
// 指定当 JVM 退出时删除该文件
tempFile.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();
for(File root:roots)
{
System.out.println(root);
}
}
}
2、文件过滤器
在 File 的 list 方法中可以接受一个 FilenameFilter 参数,通过该参数可以只列出符合条件的文件。
FilenameFilter 接口里包含了一个 accept(File dir, String name) 方法,该方法将依次对指定 File 的所有子目录、子文件夹进行迭代,如果该方法返回 true,这 list 方法会列出该子目录或者子文件夹。
示例:
package code15_1;
import java.io.File;
import java.io.FilenameFilter;
public class FilenameFilterTest {
/**
* @param args
*/
public static void main(String[] args) {
File file = new File(".");
String[] nameList = file.list(new MyFilenameFilter());
for(String name : nameList)
{
System.out.println(name);
}
}
}
// 实现自己的 FilenameFilter 实现类
class MyFilenameFilter implements FilenameFilter
{
@Override
public boolean accept(File dir, String name) {
// 如果文件名以 .java 结尾,或者文件对应一个路径,返回 true
return name.endsWith(".java") || new File(name).isDirectory();
}
}
二、理解 Java 的 IO 流
1、流的分类
输入流和输出流
- 输入流:只能从中读取数据,而不能向其写出数据
- 输出流:只能向其写出数据,而不能从中读取数据
Java 的输入流主要由 InputStream 和 Reader 作为基类,而输出流则主要由 OutputStream 和 Writer 作为基类。
字节流和字符流
- 字节流操作的最小数据单元是 8 位的字节
- 字符流则主要由 Reader 和 Writer 作为基类
节点流和处理流
- 可以从/向一个特定的 IO 设备(如磁盘、网络)读/写数据的流,称为节点流,节点流常常也被称为低级流(Low Level Stream)
- 处理流则用于对一个已存在的流进行连接或封装,通过封装后流来实现数据读/写功能。处理流也被称为高级流或者包装流
2、流的概念模型
Java 把所有设备里的有序数据抽象成流模型简化了输入/输出的处理,理解了流的概念模型也就了解了 Java IO
Java 的 IO 流共涉及到 40 多个类,这 40 多个类都是从 4 个抽象基类派生出来的:
- InputStream/Reader:所有输入流的基类,前者是字节数入流,后者是字符输入流
- OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流
三、字节流和字符流
1、InputStream 和 Reader
他们是所有输入流的基类, 他们两个都是抽象类.
InputStream 里包含如下三个方法:
- int read(): 从输入流中读取单个字节,返回所读取字节数据(字节数据可直接转换成 int 类型)
- int read(byte[] b):从输入流中读取最多 b.length 个字节的数据,并将其存储在字节数组 b 中,返回实际读取的字节数
- int read(byte[] b, int off, int len):从输入流中读取最多 len 字节的数据,并将其存储在数组 b 中,放入 b 数组中时,并不是从数组起点开始,而是从 off 位置开始,返回实际读取的字节数。
在 Reader 里包含如下三个方法:
- int read(): 从输入流中读取单个字符,返回所读取的字符数据(字符数据可直接转换成 int 类型)
- int read(char[] cbuf): 从输入流中读取最多 cbuf.length 个字符的数据,并将其存储在字符数组 cbuf 中,返回实际读取的字符数
- int read(char[] cbuf, int off, int len): 从输入流中读取最多 len 个字符的数据,并将其存储在字符数组 cbuf 中,放入 cbuf 数组中时,并不是从数组的起点开始,而是从 off 位置开始,返回实际读取的字符数。
read(char[] cbuf) 或 read(byte[] b) 方法返回 -1 时,即表明到了输出流的结束点。
正如前面提到,InputStream 和 Reader 都是抽象类,本身并不能创建实例,但它们分别有一个读取文件的输入流:FileInputStream 和 FileReader,它们都是节点流——会直接和指定文件关联。
示例:
public class FileInputStreamTest
{
public static void main(String[] args) throws IOException
{
// 创建字节数入流
FileInputStream fis = new FileInputStream("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();
}
}
FileReader 示例:
public class FIleReaderTest
{
public static void main(String[] args) throws IOException
{
FileReader fr = null;
try
{
// 创建字符输入流
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 ioe)
{
ioe.printStackTrace();
}
finally
{
// 使用 finally 块来关闭文件输入流
if (fr != null)
{
fr.close();
}
}
}
}
除此之外,InputStream 和 Reader 还支持如下几个方法来移动记录指针:
- void mark(int readAheadLimit): 在记录指针当前位置记录一个标记 (mark)
- boolean markSupported(): 判断此输入流是否支持 mark() 操作, 即是否支持记录标记
- void reset():将此流的记录指针重新定位到上一次记录标记 (mark) 的位置
- long skip(long n): 记录指针向前移动 n 个字节/字符
2、OutputStream 和 Writer
OutputStream 和 Writer 两个流都提供了如下三个方法:
- void write(int c): 将指定的字节/字符输出到输出流中, 其中 c 既可以代表字节,也可以代表字符.
- void write(byte[]/char[] buf): 将字节数组/字符数组中的数据输出到指定的输出流中
- void write(byte[]/char[] buf, int off, int len): 将字节数组/字符数组中从 off 位置开始,长度为 len 的字节/字符输出到输出流中
示例:
public class FileOutputStreamTest
{
public static void main(String[] args) throws IOException
{
FileInputStream fis = null;
FilrOutputStream fos = null;
try
{
// 创建字节数入流
fis = new FileInputStream("FileOutputStreamTest.java");
// 创建字节输出流
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();
}
finally
{
// 使用 finally 块来关闭文件输入流
if (fis != null)
{
fis.close();
}
// 使用 finally 块来关闭文件输出流
if (fos != null)
{
fos.close();
}
}
}
}
如果希望直接输出字符串内容, 则使用 Writer 会有更好的效果, 示例:
public class FileWriterTest
{
public static void main(String[] args) throws IOException
{
FileWriter fw = null;
try
{
// 创建字符输出流
fw = new FileWriter("poem.txt");
fw.write("aaa\r\n");
fw.write("bbb\r\n");
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
finally
{
// 使用 finally 块来关闭文件输出流
if (fw != null)
{
fw.close();
}
}
}
}
四、输入/输出流体系
1、处理流的用法
使用处理流的典型思路是:使用处理流来包装节点流,程序通过处理流来执行输入/输出功能, 让节点流与底层的 I/O 设备、文件交互.
识别处理流非常简单, 只要流的构造器参数不是一个物理节点, 而是一个已经存在的流, 那么这种流就一定是处理流; 而所有节点流都是直接以物理 IO 节点作为构造器参数的.
示例: 使用 PrintStream 处理流来包装 OutputStream, 使用处理流后的输出流在输出时将更加方便.
public class PrintStreamTest
{
public static void main(String[] args) throws IOException
{
PrintStream ps = null;
try
{
// 创建一个接点输出流: FileOutpuptStream
FileOutputStream fos = new FileOutputStream("test.txt");
// 以 PrintStream 来包装 FileOutputStream 输出流
ps = new PrintStream(fos);
// 使用 PrintStream 来执行输出
ps.println("普通字符串");
// 直接使用 PrintStream 输出对象
ps.println(new PrintStreamTest());
}
catch (IOException ioe)
{
ioe.printStackTrace(ps);
}
finally
{
ps.close();
}
}
}
2、输入/输出流体系
Java 的输入输出流体系提供了近 40 个类
分类 | 字节输入流 | 字节输出流 | 字符输入流 | 字符输出流 |
抽象基类 | InputStream | OutputStream | Reader | Writer |
访问文件 | FileInputStream | FileOutputStream | FileReader | FileWriter |
访问数组 | ByteArrayInputStream | ByteArrayOutputStream | CharArrayReader | CharArrayWriter |
访问管道 | PipedInputStream | PipedOutputStream | PipedReader | PipedWriter |
访问字符串 | StringReader | StringWriter | ||
缓冲流 | BufferedInputStream | BufferedOutputStream | BufferedReader | BufferedWriter |
转换流 | InputStreamReader | OutputStreamWriter | ||
对象流 | ObjectInputStream | ObjectOutputStream | ||
抽象基类 | FilterInputStream | FilterOutputStream | FilterReader | FilterWriter |
打印流 | PrintStream | PrintWriter | ||
退回输入流 | PushbackInputStream | PushbackReader | ||
特殊流 | DataInputStream | DataOutputStream |
上面表格中列出了一种以数组为物理节点的节点流, 字节流以字节数组为节点, 字符流以字符数组为节点; 与此类似的是, 字符流还可以使用字符串作为为物理节点, 用于实现从字符串读取内容, 或将内容写入字符串(实际上用 StringBuffer 充当了字符串)的功能.
示例:
public class StringNodeTest
{
public static void main(String[] args) throws Exception
{
String src = "SELECT * FROM USERS WHERE (1 = 1) \r\n"
+ "AND (username = ?) \r\n"
+ "AND (password = ?) \r\n"
+ "AND (address LIKE ?) \r\n";
StringReader sr = new StringReader(src);
char[] buffer = new char[32];
int hasRead = 0;
try
{
// 采用循环读取的访问读取字符串
while ((hasRead = sr.read(buffer)) > 0)
{
System.out.print(new String(buffer, 0, hasRead));
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
finally
{
sr.close();
}
// 创建 StringWriter 时, 实际上以一个 StringBuffer 作为输出接点
// 下面指定的 20 就是 StringBuffer 的初始长度
StringWriter sw = new StringWriter(20);
sw.write("SELECT * FROM EMP \r\n");
sw.write("WHERE (1 = 1) \r\n");
sw.write("AND (add LIKE ?) \r\n");
System.out.println("------------- 下面是 sw 的字符串节点里的内容 -----------");
// 使用 toString 方法返回 StringWriter 的字符串节点的内容
System.out.println(sw.toString());
}
}
3、转换流
输入/输出流体系里还提供了 2 个转换流, 这两个转换流用于实现将字节流转换成字符流
- InputStreamReader 将字节输入流转换成字符输入流
- OutputStreamReader 将字节输出流转换成字符输出流
示例: 下面是以获取键盘输入为例介绍转换流的用法, Java 使用 System.in 代表标准输入, 即键盘输入, 但这个标准输入流是 InputStream 类实例, 使用不方便, 而我们知道键盘输入内容都是文本内容, 所以我们可以使用 InputStreamReader 将其转化成字符输入流, 普通 Reader 读取输入内容时依然不方便, 我们可以将普通 Reader 再次包装成 BufferedReader, 利用 BufferedReader 的 readLine 方法可以一次读取一行的内容.
public class KeyinTest
{
public static void main(String[] args)
{
BufferedReader br = null;
try
{
// 将 System.in 对象转化成 Reader 对象
InputStreamReader reader = new InputStreamReader(System.in);
// 将普通 Reader 包装成 BufferedReader
br = new BufferedReader(reader);
String buffer = null;
while ((buffer = br.readLine()) != null)
{
// 如果循环方式来一行一行的读取
if (buffer.equals("exist"))
{
System.exit(1);
}
// 打印读取的内容
System.out.println("输入内容为:" + buffer);
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
// 关闭输入流
finally
{
try
{
br.close();
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
}
4、推回输入流
在输入/输出流体系中, 有两个特殊的流与众不同, 就是 PushbackInputStream 和 PushbackReader, 它们分别提供了如下三个方法:
- void unread(byte[]/char[] buf): 将一个字节/字符数组内容推回到推回缓冲区里, 从而允许重复读取刚刚读取的内容
- void unread(byte[]/char[] b, int off, int len): 将一个字节/字符数组里从 off 开始, 长度为 len 字节/字符的内容推回到推回缓冲区里,从而允许重复读取刚刚读取的内容
- void unread(int b): 将一个字节/字符推回到推回缓冲区里, 从而允许重复读取刚刚读取的内容
这三个方法实际上和三个 read 方法是一一对应的, 这两个推回输入流都带有一个推回缓冲区,当程序调用这两个推回输入流的 unread 方法时, 系统将会把指定数组的内容推回到该缓冲区里, 而退回输入流每次调用 read 方法时总是先从推回缓冲区读取, 只有当完全读取了推回缓冲区内的内容后, 而且还没有装满 read 所需的数组时才会从原输入流中读取. 当我们创建一个 PushbackInputStream 和 PushbackReader 时, 需要指定推回缓冲区的大小, 默认的推回缓冲区的长度为 1. 如果程序中推回到推回缓冲区的内容超出了推回缓冲区的大小, 程序将引发 Pushback buffer overflow 的 IOException.
示例:
import java.io.FileReader;
import java.io.IOException;
import java.io.PushbackReader;
public class PushbackTest
{
public static void main(String[] args)
{
PushbackReader pr = null;
try
{
// 创建一个 PushbackReader 对象, 指定推回缓冲区的长度为 64
pr = new PushbackReader(new FileReader("src\\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());
// 再次读取指定长度的内容 (就是目标字符串之前的内容)
char[] buf2 = new char[targetIndex];
pr.read(buf2, 0, targetIndex);
// 打印读取的内容
System.out.print(new String(buf2, 0, buf2.length));
System.exit(1);
}
else
{
// 打印上次读取的内容
System.out.print(lastContent);
// 将本次读取的内容设为上次读取的内容
lastContent = content;
}
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
finally
{
try
{
if (pr != null)
{
pr.close();
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
}
五、重定向标准输入/输出
Java 的标准输入/输出分别通过 System.in 和 System.out 来代表, 默认情况下它们分别代表键盘和显示器.
在 System 类里提供了三个重定向标准输入/输出的方法:
- static void setErr(PrintStream err): 重定向 "标准" 错误输出流
- static void setIn(InputStream in): 重定向 "标准" 输入流
- static void setOut(PrintStream out): 重定向 "标准" 输出流
示例: 下面程序通过重定向标准输出流, 将 System.out 的输出重定向向文件输出, 而不是在屏幕上输出
public class RedirectOut
{
public static void main(String[] args)
{
PrintStream ps = null;
try
{
// 一次性创建 PrintStream 输出流
ps = new PrintStream(new FileOutputStream("out.txt"));
// 将标准输出重定向到 ps 输出流
System.setOut(ps);
// 向标准输出输出一个字符串
System.out.println("普通字符串");
// 项标准输出输出一个对象
System.out.println(new RedirectOut());
}
catch(IOException ioe)
{
ioe.printStackTrace();
}
finally
{
if (ps != null)
{
ps.close();
}
}
}
}
示例: 下面程序重定向标准输入, 从而可以将 System.in 重定向到指定文件, 而不是键盘输入.
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Scanner;
public class RedirectIn
{
public static void main(String[] args)
{
FileInputStream fis = null;
try
{
fis = new FileInputStream("src/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 ioe)
{
ioe.printStackTrace();
}
finally
{
if (fis != null)
{
try
{
fis.close();
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
}
}
六、Java 虚拟机读写其他进程的数据
Runtime 对象的 exec 方法可以运行平台上的其他程序, 该方法产生一个 Process 对象, Process 对象代表由该 Java 程序启动的子进程, Process 类提供了如下三个方法, 用于让程序和其子进程进行通信:
- InputStream getErrorStream(): 获取子进程的错误信息
- InputStream getInputStream(): 获取子进程的输入流
- OutputStream getOutputStream(): 获取子进程的输出流
示例:
public class ReadFromProcess
{
public static void main(String[] args)
{
BufferedReader br = null;
try
{
// 运行 javac 命令, 返回运行该命令的子进程
Process p = Runtime.getRuntime().exec("javac");
// 以 p 进程的错误流创建 BufferedReader 对象
// 这个错误流对本程序是输入流, 对 p 进程则是输出流
br = new BufferedReader(new InputStreamReader(p.getErrorStream()));
String buff = null;
while ((buff = br.readLine()) != null)
{
System.out.println(buff);
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
finally
{
try
{
if (br != null)
{
br.close();
}
}
catch (IOException ioe)
{
ioe.printStackTrace();
}
}
}
}
九. Java 新 IO
前面介绍 BufferedReader 时介绍到她的一个特性, 当 BufferedReader 读取输入流中的数据时, 如果没有读到有效数据时, 程序将在此处阻塞该线程的执行 (使用 InputStream 的 read 方法从流中读取数据时, 如果数据源中没有数据, 他也会阻塞线程), 也就是前面介绍的输入, 输出流都是阻塞式的输入, 输出. 不仅如此, 传统的输入, 输出流都是通过字节的移动来处理的(即使我们可因不直接去处理字节流, 但底层的实现还是依赖于字节处理), 也就是说面向流的输入/输出系统一次只能处理一个字节, 因此面向流的输入/输出系统通常效率不高.
从 JDK 1.4 开始, Java 提供了一些列改进的输入/输出处理的新特性, 这些功能通常被称为新 I/O (New I/O), 这些类都被放在 java.nio 包及其子包下, 并且对原 java.io 包中的很多类似类以 NIO 为基础进行了改写, 新增了满足新 IO 的功能.
1. Java 新 IO 概述
新 IO 采用内存映射文件的方式来处理输出/输出, 新 IO 将文件或文件的一段区域映射到内存中, 这样就可以向访问内存一样访问文件了.
Java 中 NIO 相关的包如下:
- java.nio 包: 主要提供了一些和 Buffer 相关的类
- java.nio.channels 包: 主要包括 Channel 和 Selector 相关的类
- java.nio.charset 包: 主要包含和字符集相关的类
- java.nio.channels.spi 包: 主要包含提供 Channel 服务的类
- java.nio.charset.spi 包: 主要包含提供字符集服务的相关类
Channel (通道) 和 Buffer (缓冲) 是新 IO 中两个核心对象, Channel 是对传统输入/输出系统中的模拟, 在新 IO 系统中所有数据都需要通过通道传输; Channel 与传统的 InputStream, OutputStream 最大的区别在于它提供了一个 map 方法, 通过该 map 方法可以直接将 "一块数据" 映射到内存中, 如果说传统的输入/输出系统是面向流的处理, 而新 IO 则是面向块的处理.
Buffer 可以被理解为一个容器, 它的本质是一个数组, 发送到 Channel 中的所有对象都必须先放到 Buffer 中, 而从 Channel 中读取的数据也必须先读到 Buffer 中.
除了 Channel 和 Buffer 之外, 新 IO 还提供了用于将 UNICODE 字符串映射成字节序列以及逆映射操作的 Charset 类, 还提供了用于支持非阻塞式输入/输出 的 Selector 类.
2、使用 Buffer
从结构上看, Buffer 就像一个数组, 他可以保存多个类型相同的数据. Buffer 是一个抽象类, 其最常用的子类是 ByteBuffer, 他可以在底层字节数组上进行 get/set 操作, 除了 ByteBuffer 之外, 对应其他基本数据类型 (boolean 除外) 都有相应的 Buffer 类: ByteBuffer, CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer.
上面这些 Buffer 类, 除了 ByteBuffer 之外, 他们都采用了相同或者类似的方法来管理数据, 只是各自管理的数据类型不同, 这些 Buffer 都没有提供构造器, 通过使用如下方法得到一个 Buffer 对象:
- static XxxBuffer allocate(int capacity): 创建一个容量为 capacity 的 XxxBuffer 对象
其中 ByteBuffer 类还有一个子类: MappedByteBuffer, 它用于表示 Channel 将磁盘文件的部分或全部映射到内存中后得到的结果, 通常 MappedByteBuffer 对象由 Channel 的 map 方法返回.
在 Buffer 中有三个重要的概念: 容量(capacity), 界限(limit) 和位置(position):
- 容量 (capacity):
- 界限 (limit): 第一个不应该被读出或者写入的缓冲区位置索引, 也就是说, 位于 limit 后的数据既不可被读也不可被写
- 位置 (position): 用于指明下一个可以被读出的或者写入的缓冲区位置索引(类似于 IO 流中的记录指针).
除此之外, Buffer 里还可以支持一个可选标记 (mark, 类似传统 IO 流中 mark), 该 mark 允许程序直接将 position 直接定位到该 mark 处, 这些只满足:
0 <= mark <= position <= capacity
Buffer 的主要作用就是装入数据, 然后传输数据, 开始时 Buffer 的 position 为 0, limit 为 capacity, 程序调用
put 不断地向 Buffer 中放入数据(或者从 Channel 中获取一些数据), 每放入一些数据, Buffer 的 position 相应地向后
移动一些位置.
当 Buffer 装入数据结束后, 调用 Buffer 的 flip 方法, 该方法将 limit 设置为 position 所在的位置, 将 position 设置为 0,这样使得从 Buffer 中读数据时总是从 0 开始, 读完刚刚装入的所有数据即结束, 也就是说 Buffer 调用 flip 方法后, Buffer 为输出数据做好了准备. 当 Buffer 输出数据结束后, Buffer 调用 clear 方法, clear 方法不是清空 Buffer 的数据, 它仅仅将 position 置为 0 , 将 limit 置为 capacity, 这样为再次向 Buffer 重装如数据做好准备.
除此之外, Buffer 还包含如下一些常用的方法:
- int capacity()
- boolean hasRemaining(): 判断当前位置(position)和界限(limit)之间的元素是否可供处理
- int limit(): 返回 Buffer 的界限 (limit) 的位置
- Buffer limit(int newLt): 重新设置界限 (limit)的值, 并返回一个具有新的 limit 的缓冲区对象
- Buffer mark(): 设置 Buffer 的 mark 的位置, 他只能在 0 和 位置 (position) 之间做 mark
- int position(): 返回 Buffer 中的当前位置
- Buffer position(int newPs): 设置 Buffer 的新位置, 并返回一个具有改变 position 后的 Buffer 对象
- int remaining(): 返回当前位置和界限(limit)之间的元素个数
- Buffer reset(): 将位置(position)转到 mark 所在的位置
- Buffer rewind(): 将位置 (position) 设置为 0, 取消设置的 mark
除了这些移动 position, limit,mark 的方法之外, Buffer 的所有子类还提供了两类重要的方法: put 和 get 方法, 用于向 Buffer 中放入数据和从 Buffer 中取出数据. 当使用 put 和 get 方法来放入, 取出数据时, Buffer 既支持对单个数据的访问, 也支持对批量数据的访问 (以数组作为参数)
当使用 put 和 get 来访问 Buffer 中的数据时, 分为相对和绝对两种:
- 相对 (Relative): 从 Buffer 的当前位置读取或写入数据, 然后将位置 (position) 的值按处理元素的个数增加
- 绝对 (Absolute): 直接根据索引来向 Buffer 中读取或写入数据, 使用绝对方式来访问 Buffer 里的数据时, 并不会影响位置 position 的值.
示例:
import java.nio.CharBuffer; 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("执行flip() 方法后, 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()); } }