【java】详解I/O流

目录结构:

contents structure [+]

1. File类

Java用File来访问文件、目录,以及对它们的删除、创建等。
接下来列出File类中常用的方法:
1. 访问文件名相关的方法
Sring getName():返回此File对象所表示的文件名或路径名(如果是路径则返回最后一级子路径)
String getPath():返回此File对象所对应的路径名
File getAbsoluteFile():返回此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所对应的文件或目录是否是绝对路径。该方法消除了不同平台的差异,可以直接判断FIle对象是否为绝对路径。在UNIX/Linux/BSD 等系统,如果路径名开头是一条斜线(/),则表明该File对象所对应一个绝对路径。在Window系统上,如果路径开头盘符,则说明它是一个绝对路径。

3.获取常规文件信息
boolean lastModified():返回文件的最后修改时间。
boolean length():返回文件内容的长度。

4.获取文件的常规信息
boolean createNewFile():当此File对象所对应的文件不存在时,该方法将新建一个该File对象所对应的新文件。如果创建成功则返回true,否则返回false。
boolean delete():删除File对象所对应的文件或是目录。
static File createTempFile(String prefix,String suffix) 在默认的临时文件目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和给定后缀做文件名。这是一个静态方法,可以直接通过File类来调用。Prefix参数必须至少是3字节长。建议前缀使用一个短的,有意义的字符串。suffix 可以为null,这种情况下默认使用后缀“.temp”。
static File createTempFile(String prefix,String suffix,File directory) 在directory指定的目录中创建一个临时的空文件,使用给定前缀、系统生成的随机数和给定后缀作为文件名。这是一个静态方法,可以直接通过File类来调用。
void deleteOnExit():注册一个删除勾子,指定当Java虚拟机退出时,删除File对象所对应的文件和目录。

5.目录操作相关的方法
boolean mkdir():试图创建一个File对象所对应的目录,如果创建成功,则返回true;否则返回false,调用方法时,File对象必须对应一个路径。
String[] list() 列出File对象的所有子文件名和路径名。
File[] listFiles() 列出File对象的所有子文件和路径。
static File[] listRoots() 列出系统所有的根目录。

下面使用使用递归打印某个目录下及其子目录下的所有的文件名:

public class TestDirPrint {
    //打印指定目录中的内容,要求子目录中的内容也要打印出来
    public static void dirPrint(File f){
        //1.若f关联的是普通文件,则直接打印文件名即可
        if(f.isFile()){
            System.out.println(f.getName());
        }
        //2.若f关联的是目录文件,则打印目录名的同时使用[]括起来
        if(f.isDirectory()){
            System.out.println("[" + f.getName() + "]");
            //3.获取该目录中的所有内容,分别进行打印
            File[] ff = f.listFiles();
            for(File ft : ff){
                dirPrint(ft);
            }
            
        }    
    }
}

2. I/O流体系

2.1 流的基本介绍

首先介绍流的分类:

按照流的方向来分,可以分为字节流和字符流。
输入流:只能读取数据。
输出流:只能向其中写入数据。
输入流和输出流都是站在程序的角度进行考虑的。

按照流处理的数据单元不同,可以分为字节流和字符流。
字节流和字符流的用法几乎一致,区别是它们操作的数据单元不同。字节流操作的数据单元是一个字节,字符流操作的数据单元是2个字节。

按照流的角色,可以分为节点流和处理流。
节点流是可以直接向特定的IO设备进行读/写操作的流,这种流也被称为低级流。处理流则用于对一个已存在的流,通过封装后的流来实现数据读/写功能,处理流被称为装饰流或高级流。

java的输入/输出流提供了近40个类,下面按照流的功能进行分类:

分类 字节输入流 字节输出流 字符输入流 字符输出流
抽象基类 InputStream OutputStream Reader Writer
访问文件 FileInputStream FileOutputStream FileReader FileWriter
访问数组 ByteArrayInputStream ByteArrayOutputStream CharArrayReader CharArrayWriter
访问管道 PipedInptStream PipedOutoutStream PipedReader PipedWriter
访问字符串     StringReader StringWriter
缓冲流 BufferedInputStream BufferedOutputStream BufferedReader BufferedWriter
转化流     InputStreamReader OutputStreamWriter
对象流 ObjectInputStream ObjectOutputStream    
抽象基类 FilterInputStream FilterOutputStream FilterReader FilterWriter
打印流   PrintStream    
推回输入流 PushbackInputStream   PushbackReader  
特殊流 DataInputStream DataOutputStream    


我们已经知道字节流的数据单元是一个字节,字符流的数据单元是两个字节。通常情况下,如果访问的是文本内容,那么应该使用字符流,如果访问的是二进制内容,那么应该使用字节流。
使用字节流访问文本内容,如果一次访问不完,那么很有可能出现乱码的情况。
例如有GBK格式编码的文本文件test.txt,内容如下:
“do not give up.不要放弃。”
这段文本中“do not give up.”一共占据15个字节。“不要放弃。”一共占了10个字节。
如果使用如下的代码来访问:

        String path="C:\\Users\\dell\\Desktop\\test.txt";
        FileInputStream fis=new FileInputStream(new File(path));
        byte[] bytes=new byte[1024];//一次读取1024个字节
        int offset=0;
        String result="";
        while((offset=fis.read(bytes,0,bytes.length))!=-1){
            result+=new String(bytes,0,offset);
        }
        System.out.println(result);

由于上面的文本没有1024个字节,所以可以一次性读完,并不会出现乱码情况。
而如果改为一次读取3个字节,那么就会出现乱码。我们甚至还可以推断出,那些字符会出现乱码,由于读取数据的单位为3个字节,所以前面的“do not give up.”会分为5次读完。后面的汉字“不要放弃。”每个汉字占两个字节,为了方便理解,我们把这10个字节用一下这些字符来表示“12 34 56 78 9A”,第一次读取了三个字节123,12可以被正常解析为“不”,后面有一个字节不能被正常解析。第二此又读取三个字节456,45不能被正常解析,6也不能被正常解析。第三次又读取三个字节789,78可以被正常解析,9不能。最后读取最后一个字节A,A也不能被正常解析。所以最终汉字中只有“不”、“弃”可以被正常解析。

2.2 访问文件

在前面介绍过访问文件主要涉及到FileInputStreaam、FileOutputStream、FileReader、FileWriter,如果是二进制文件,那么使用字节流。如果是文本文件,那么使用字符流。
接下使用FileInputStream、FileOutputStream实现对文件的复制:

【java】详解I/O流【java】详解I/O流
public class TestFileCopy {

    public static void main(String[] args) {
        
        try{
            //1.建立FileInputStream类的对象与源文件建立关联
            FileInputStream fis
                = new FileInputStream("D:/java09/day16/javaseday16-IO流常用的类-06.wmv");
            //2.建立FileOutputStream类的对象与目标文件建立关联
            FileOutputStream fos = new FileOutputStream("c:/javaseday16-IO流常用的类-06.wmv");
            //3.不断地读取源文件中的内容并写入到目标文件中
            /* 可以实现文件的拷贝,但是文件比较大时效率很低
            int res = 0;
            while((res = fis.read()) != -1){
                fos.write(res);
            }
            */
            //第二种方案,根据源文件的大小准备对应的缓冲区(数组),可能导致内存溢出
            //第三种方案,无论文件的大小是多少,每次都准备一个1024整数倍的数组
            byte[] data = new byte[1024 * 8];
            int res = 0;
            while((res = fis.read(data)) != -1){
                fos.write(data, 0, res);
            }
            System.out.println("拷贝文件结束!");
            //4.关闭文件输入流对象
            fis.close();
            //5.关闭文件输出流对象
            fos.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
TestFileCopy.java

2.3 转化流

转化流主要涉及到InputStreamReader和OutputStreamWriter,都是将字节流转化为字符流。
接下来结合输入转化流InputStreamReader和打印流PrintStream进行演示:

【java】详解I/O流【java】详解I/O流
public class TestBufferedReaderPrintStream {

    public static void main(String[] args) {
        
        try{
            //1.创建BufferedReader类型的对象与键盘输入(System.in)进行关联
            BufferedReader br = new BufferedReader(
                    new InputStreamReader(System.in));
           //有时候在这里读取网页中的中文的时候,会出现乱码,可以用如下的解决
           //  BufferedReader br = new BufferedReader(
           //       new InputStreamReader(new URL("http://www.baidu.com").openStream(),"utf-8"));
            //2.创建PrintStream类型的对象与c:/a.txt文件进行关联
            PrintStream ps = new PrintStream(new FileOutputStream("d:/a.txt"));
            //3.不断地提示用户输入并读取一行本文,并且写入到d:/a.txt中
            int flag = 1;
            while(true){
                System.out.println("请输入要发送的内容:");
                //读取用户输入的一行文本
                String str = br.readLine();
                //4.当用户输入的是"bye"时,则结束循环
                if("bye".equalsIgnoreCase(str)) break;
                /*
                   //将发送消息的时间写入到文件中
                   Date d1 = new Date();
                   SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                   ps.println(sdf.format(d1));//将时间转化为字符串,写入到文件中
                   //也可以将一个字符串转化为时间对象
                  Date d2=new Date();
                  String str2="2017-05-31 17:51:04";
                  d2=sdf.parse(str2);//将字符串解析为对应的Date对象
                */
                //将str写入到文件中
                ps.println(str);
                flag++;
            }
            //5.关闭相关流对象
            ps.flush();
            ps.close();
            br.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
TestBufferedReaderPrintStream.java

2.4 DataInputStream 和 DataOutputStream

这两流比较特殊,他们可以对基本数据类型进行操作。
DataInputStream测试:

【java】详解I/O流【java】详解I/O流
public class TestDataOutputStream {

    public static void main(String[] args) {
        
        try{
            //1.创建DataOutputStream的对象与参数指定的文件进行关联
            DataOutputStream dos = new DataOutputStream(
                    new FileOutputStream("c:/a.txt"));
            //2.将整数数据66写入文件
            dos.writeInt(88);
            //3.关闭输出流对象
            dos.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
TestDataOutputStream.java

DataOutputStream测试:

【java】详解I/O流【java】详解I/O流
public class TestDataInputStream {

    public static void main(String[] args) {
        
        try{
            //1.创建DataInputStream类的对象与参数指定的文件关联
            DataInputStream dis = new DataInputStream(
                    new FileInputStream("c:/a.txt"));
            //2.读取文件中的一个int类型数据并打印出来
            int res = dis.readInt();
            System.out.println("res = " + res); // 88
            //3.关闭流对象
            dis.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
TestDataInputStream.java

2.5 对象流

对象流主要涉及到类是ObjectOutputStream和ObjectInputStream,ObjectOutputStream是用于将一个对象整体写入到输入流中,ObjectInputStream是用于从流中读取一个对象。
ObjectOutputStream测试:

【java】详解I/O流【java】详解I/O流
public class TestObjectOutputStream {

    public static void main(String[] args) {
        
        try{
            //1.创建ObjectOutputStream类的对象与指定的文件关联
            ObjectOutputStream oos = new ObjectOutputStream(
                    new FileOutputStream("c:/user.dat"));
            //2.准备User类型的对象并进行初始化
            User user = new User("Mark", "123456", "[email protected]");
            //3.将User类型的对象整体写入到文件中
            oos.writeObject(user);
            System.out.println("写入对象成功!");
            //4.关闭输出流对象
            oos.flush();
            oos.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
TestObjectOutputStream.java

ObjectInputStream测试

【java】详解I/O流【java】详解I/O流
public class TestObjectInputStream {

    public static void main(String[] args) {
        
        try{
            //1.创建ObjectInputStream类型的对象与指定的文件关联
            ObjectInputStream ois = new ObjectInputStream(
                    new FileInputStream("c:/user.dat"));
            //2.读取文件中的一个对象并打印出来
            Object obj = ois.readObject();
            if(obj instanceof User){
                User user = (User)obj;
                System.out.println(user);
            }
            //3.关闭流对象
            ois.close();
        }catch(Exception e){
            e.printStackTrace();
        }
    }
}
TestObjectInputStream.java

需要注意被writeObject和readObject的对象应该序列化,因为流不能传输对象,所以只能将其状态保存在一组字节进行传输,详情可以查看Java对象序列化。

2.6 推回输入流

在java体系中有两个特殊的流,就是PushbackInputStream和PushbackReader,他们都提供了如下三个方法:
void unread(byte[]/char[] buf):将一个字节/字符数组的内容推回到缓存区里,从而允许重复读取刚刚读取的内容。
void unread(byte[]/char[] buf,int off,int len):将一个字节/字符数组里从off开始,长度为len字节/字符推回到缓冲区里,从而允许重复读取刚刚读取的内容。
void unread(int b): 将一个字节/字符推回到缓存区里,从而允许重复读取刚刚读取的内容。

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

【java】详解I/O流

在创建PushbackInputStream和PushbackReader时指定了缓冲区的大小,如果没指定那么默认的缓冲区大小是1,如果程序中推回到缓冲区的内容超过了推回缓冲区的大小,那么将会引发Pushback buffer overflow的IOException异常。

下面的案例是打印“throws”前的32个字节的数据:

【java】详解I/O流【java】详解I/O流
public class PushBackStreamTest {
    public static void main(String[] args) throws IOException {
        try {
            // 创建PushbackReader对象,指定推回缓冲区的大小为64
            PushbackReader pr = new PushbackReader(new FileReader("PushBackStreamTest.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("throws")) > 0) {
                    // 将本次内容和上次内容一起推回缓冲区
                    pr.unread((lastContent + content).toCharArray());
                    // 重新定义一个长度为targetIndex的Char数组
                    if (targetIndex > 32) {
                        buf = new char[targetIndex];
                    }
                    // 再次读取指定长度的内容,也就是throws之前的内容
                    pr.read(buf, 0, targetIndex);
                    // 打印
                    System.out.println(new String(buf, 0, targetIndex));
                    break;
                } else {
                    lastContent = content;
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
PushBackStreamTest.java

3.重定向标准输入/输出

Java的标准输入/输出是分别通过System.in和System.out来代表的,在默认情况下,它们默认代表键盘和显示器。当程序通过System.in来获取时,实际上是从键盘输入;当程序从System.out输出时,实际上是输出到屏幕。java提供可以调用系统脚本命令的实现。

System类提供了如下三个标准重定向输入/输出的方法

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

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

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

下面的程序是重定向标准的输出流,将System.out的输出重定向到文件:

【java】详解I/O流【java】详解I/O流
import java.io.File;
import java.io.IOException;
import java.io.PrintStream;

public class RedirectOut {
    public static void main(String[] args) {
        PrintStream ps=null;
        try{
            //创建PrintStream输出流
            ps=new PrintStream(new File("out.txt"));
            //将标准数据流重定向到ps流
            System.setOut(ps);
            //向标准输出流输出一个字符串
            System.out.println("hello world");
            //向标准输出流输出一个对象
            System.out.println(new RedirectOut());
        }catch(IOException e){
            e.printStackTrace();
        }finally{
            ps.close();
        }
    }
}
RedirectOut.java

下面的程序是重定向标准的输入流,将System.in的输入重定向到文件:

【java】详解I/O流【java】详解I/O流
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Scanner;

public class RedirectIn {
    public static void main(String[] args) throws IOException {
        FileInputStream fis=null;
        try{
            fis=new FileInputStream(new File("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 e)
        {
            
        }finally{
            fis.close();
        }
    }
}
RedirectIn.java

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

使用Runtime对象的exec()方法,可以运行平台上的其他程序,该方法产生一个Process对象,Process对象代表由该Java程序启动的子进程。通过该方法,java提供可以调用系统脚本命令的实现。

exec()方法提供了几个重载版本,如下:

public Process exec(String command)----- 在单独的进程中执行指定的字符串命令。
public Process exec(String [] cmdArray)--- 在单独的进程中执行指定命令和变量
public Process exec(String command, String [] envp)---- 在指定环境的独立进程中执行指定命令和变量
public Process exec(String [] cmdArray, String [] envp)---- 在指定环境的独立进程中执行指定的命令和变量
public Process exec(String command,String[] envp,File dir)---- 在有指定环境和工作目录的独立进程中执行指定的字符串命令
public Process exec(String[] cmdarray,String[] envp,File dir)---- 在指定环境和工作目录的独立进程中执行指定的命令和变量

Process类提供了如下三个进行通信的方法,用于让程序和其他子进程通信:

InputStream getErrorStream():获取子进程的错误流
InputStream getInputStream():获取子进程的输入流
OutputStream getOutputStream():获取子进程的输出流

例如:

        1.RunTime.getRuntime().exec(String  command);
          //在windows下相当于直接调用   /开始/搜索程序和文件  的指令,比如                              
          Runtime.getRuntime().exec("notepad.exe");  //打开windows下记事本。

        2.public Process exec(String [] cmdArray);
          Runtime.getRuntime().exec(new String[]{"/bin/sh","-c",command);//Linux下
          Runtime.getRuntime().exec(new String[]{ "cmd", "/c",command});//Windows下

exec方法的返回值是一个Process类型的数据,通过这个返回值,通过这个返回值就可以获取到命令的执行信息。

Process的其余几种方法:

1.destroy():杀掉子进程
2.exitValue():返回子进程的出口值,值 0 表示正常终止
3.getErrorStream():获取子进程的错误流
4.getInputStream():获取子进程的输入流
5.getOutputStream():获取子进程的输出流
6.waitFor():导致当前线程等待,如有必要,一直要等到由该 Process 对象表示的进程已经终止。如果已终止该子进程,此方法立即返回。如果没有终止该子进程,调用的线程将被阻塞,直到退出子进程,根据惯例,0 表示正常终止

需要注意的是,在java中调用runtime线程执行脚本是非常消耗资源的,所以不要频繁调用。

需要说一说Process的waitFor方法,该方法的作用是等待子线程的结束。

Process p = Runtime.getRuntime().exec("notepad.exe");
p.waitFor();         
System.out.println("--------------------------------------------我被执行了");//在手动关闭记事本软件后,才会被打印

需要注意,调用Runtime.getRuntime().exec()后,如果不及时捕捉进程的输出,会导致JAVA挂住,看似被调用进程没退出。所以,解决办法是,启动进程后,再启动两个JAVA线程及时的把被调用进程的输出截获。

【java】详解I/O流【java】详解I/O流
class StreamGobbler extends Thread {

    InputStream is;
    String type;

    public StreamGobbler(InputStream is, String type) {
        this.is = is;
        this.type = type;
    }

    public void run() {
        try {
            InputStreamReader isr = new InputStreamReader(is);
            BufferedReader br = new BufferedReader(isr);
            String line = null;
            while ((line = br.readLine()) != null) {
                if (type.equals("Error")) {
                    System.out.println("Error   :" + line);
                } else {
                    System.out.println("Debug:" + line);
                }
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }
}
StreamGobbler.java

调用代码:

        try {
            Process proc = Runtime.getRuntime().exec("cmd /k start dir");
            StreamGobbler errorGobbler = new StreamGobbler(
                    proc.getErrorStream(), "Error");
            StreamGobbler outputGobbler = new StreamGobbler(
                    proc.getInputStream(), "Output");
            errorGobbler.start();
            outputGobbler.start();
            proc.waitFor();
        } catch (Exception e) {
            e.printStackTrace();
        }

在实际的项目中,我们除了像上面那样调用,还会执行脚本文件。

    public static void main(String[] args) {
        executeCommand("dir");
    }
    // 执行一个命令并返回相应的信息
    public static void executeCommand(String command) {
        try {
            // 在项目根目录下将命令生成一份bat文件, 再执行该bat文件
            File batFile = new File("dump.bat");
            if (!batFile.exists())
                batFile.createNewFile();
            // 将命令写入文件中
            FileWriter writer = new FileWriter(batFile);
            writer.write(command);
            writer.close();
            // 执行该bat文件
            Process process = Runtime.getRuntime().exec(
                    "cmd /c " + batFile.getAbsolutePath());
            process.waitFor();
            // 将bat文件删除
            batFile.delete();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

5 NIO

传统的I/O都是通过流的方式来访问数据的,输入流和输出流都是阻塞式的,比如InputStream,当调用数据的read方法时,若数据源没有数据,那么程序将会阻塞。传统的输入/输出流都是通过字节的移动来处理的(即使不直接处理字节流,但底层的实现还是依赖字节处理),也就是说,面向流的系统一次只能处理一个字节,因此面向流的输入/输出效率通常不高。

从JDK1.4开始,Java提供了一些列用于改进的输入/输出处理的新功能,这些功能被称为新IO(New IO,简称NIO),这些类都放在java.nio包及子包下面。

5.1 NIO简介

NIO和传统IO有相同的目的,都是进行输入/输出,但是新IO使用不同的方式来处理输入/输出,新IO采用内存映射文件的方式来处理输入/输出,新IO将文件或文件的一段区域映射到内存中,这样就可以像访问内存的方式来访问文件了,通过这种方式比传统的IO速度要快许多。

Channel(通道)和Buffer(缓冲)是新IO中的两个核心对象,Channel是对传统的输入/输出系统的模拟,在新IO系统中所有的数据都需要通过通道传输;Channel与传统的InputStream和OutputStream的最大区别就是提供了一个map()方法,通过该map()方法可以直接将“一块数据”映射到内存中。如果说传统的输入/输出系统是面向流的处理,则新IO是面向块的处理。
Buffer可以理解为容器,发送到Channel中的所有对象都必须先放到Buffer中,从Channel中读取到的数据也必须先放到Buffer中。

5.2 Buffer类

Buffer就像一个数组,可以保存多个相同类型的数据。Byte是一个抽象类,其最常用的的之类是ByteBuffer类,它可以在底层数组上执行get/set操作,除此之外,其他基本数据类型都有相应的Buffer类:CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer。
在Buffer中有三个比较重要的概念分别为:容量(capacity)、界限(limit)和位置(position)。
a.容量(capacity):缓冲区的容量表示该Buffer的最大数据容量。该值不可为负,创建后不能修改。
b.界限(limit):第一个不应该被读取或写入缓冲区的位置索引。位于Limit之后的数据,既不可被读,也不可被写。该值不能超过Capacity,或是小于0。
c.位置(position):用于指明下一个可以被读取数据的位置索引。
除此之外,还提供一个可选标记mark,Buffer运行直接将position定位到mark处。
他们之间的关系如下:

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

下面是读了一些数据的Buffer图:
【java】详解I/O流
当调用flip()方法,该方法将limit设置为position所在位置,并将position重置为0,这样就是使Buffer的读写指针又移到了开始位置。也就是说,Buffer调用flip()方法之后,Buffer为输出数据做好准备。当调用Buffer的clear()方法时,它将position设置为0,将limit设置为capacity,这样再次为向Buffer重装数据做好准备。
示例:

public class Test {
    public static void main(String[] args) {
        CharBuffer buff=CharBuffer.allocate(8);
        System.out.println("capacity:"+buff.capacity());//8
        System.out.println("limit:"+buff.limit());//8
        System.out.println("position:"+buff.position());//0
        buff.put('a');
        buff.put('b');
        buff.put('c');
        System.out.println("capacity:"+buff.capacity());//8
        System.out.println("limit:"+buff.limit());//8        
        System.out.println("position:"+buff.position());//3
        //调用flip()方法
        buff.flip();
        System.out.println("capacity:"+buff.capacity());//8
        System.out.println("limit:"+buff.limit());//3        
        System.out.println("position:"+buff.position());//0
        
        System.out.println("第一个元素:"+buff.get());//a
        System.out.println("position:"+buff.position());//1
        //调用clear()后
        buff.clear();
        System.out.println("capacity:"+buff.capacity());//8
        System.out.println("limit:"+buff.limit());//8        
        System.out.println("position:"+buff.position());//0        
        
        //取出第二个元素
        System.out.println("执行clear()方法后,缓冲区并没有被清除,第三个数据:"+buff.get(1));//b
    }
}

上面介绍了Buffer类的使用,下面介绍Channel类。

5.3 Channel类

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

java为Channel接口提供了DatagramChannel、FileChannel、Pipe.SinkChannel、Pipe.SourceChannel、SelectableChannel、ServerSocketChannel、SocketChannel等实现类,顾名思义,可以非常方便的理解这些Channel类。
所有的Channel都不应该通过构造器直接创建,而是通过传统的InputStream、OutputStream方法的getChannel来返回Channel,不同的节点流获得Channel也不一样。
下面的所有案例都是以FileChannel为介绍。


打印一个文件中的所有内容:

public class FileChannelTest {
    public static void main(String[] args) {
        File fileIn=new File("D:/test1.txt");
        FileInputStream fis=null;//文件输入流
        FileChannel fisChannel=null;//文件输入通道
        try{
            fis=new FileInputStream(fileIn);//创建文件输入流
            fisChannel=fis.getChannel();//通过FileInputStream获得FileChannel对象
            //将fisChannel里的所有的数据映射到buffer中
            MappedByteBuffer buffer=fisChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileIn.length());
            //使用GBK的字符集来创建解码器
            Charset charset=Charset.forName("GBK");
            CharBuffer cBuffer= charset.decode(buffer);
            System.out.println(cBuffer);//打印
        }catch(Exception e)
        {
            e.printStackTrace();
        }finally{
            try{
                fisChannel.close();
                fis.close();    
            }catch(Exception e)
            {
                e.printStackTrace();
            }
        }
    }
}

上面的程序是一次性将文件的所有内容都加载到内存中,如果担心文件过大引起性能下降,那么可以分部分来获取。
例如:

      File fileIn=new File("D:/test1.txt");
        FileInputStream fis=null;//文件输入流
        FileChannel fisChannel=null;//文件输入通道
        try{
            fis=new FileInputStream(fileIn);//创建文件输入流
            fisChannel=fis.getChannel();//通过FileInputStream获得FileChannel对象
            //设置每次取数据的大小
            ByteBuffer bbf=ByteBuffer.allocate(8);
            Charset charset=Charset.forName("GBK");
            while(fisChannel.read(bbf)>0)
            {
                //锁定Buffer的空白区
                bbf.flip();
                CharBuffer cBuffer= charset.decode(bbf);
                System.out.println(cBuffer);
                //初始化Buffer,为下次取数据做好准备
                bbf.clear();
            }
        }catch(Exception e)
        {
            e.printStackTrace();
        }finally{
            try{
                fisChannel.close();
                fis.close();    
            }catch(Exception e)
            {
                e.printStackTrace();
            }
        }

下面使用FileChanel实现文件的复制功能:

【java】详解I/O流【java】详解I/O流
public class FileChannelCopyTest {
    public static void main(String[] args) {
        File fileIn=new File("D:/test1.txt");
        File fileOut=new File("D:/test2.txt");
        FileInputStream fis=null;//文件输入流
        FileOutputStream fos=null;//文件输出流
        FileChannel fisChannel=null;//文件输入通道
        FileChannel fosChannel=null;//文件输出通道
        try{
            fis=new FileInputStream(fileIn);//创建文件输入流
            fisChannel=fis.getChannel();//通过FileInputStream获得FileChannel对象、
            
            fos=new FileOutputStream(fileOut);
            fosChannel=fos.getChannel();//通过FileOutputStream获得FileChannel对象.
            
            MappedByteBuffer buffer=fisChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileIn.length());
            fosChannel.write(buffer,0);
        }catch(Exception e)
        {
            e.printStackTrace();
        }finally{
            try{
                fisChannel.close();
                fis.close();    
            }catch(Exception e)
            {
                e.printStackTrace();
            }
        }
    }
}
FileChannelCopyTest.java

5.4 文件锁

文件锁在操作系统中是很平常的事情,如果多个运行的程序需要并发地同时修改同一个文件时,程序之间需要某种机制来通信,使用文件锁可以有效地阻止多个进程并发修改同一个文件。
在NIO中,java提供了FileLock来支持文件锁功能,在FileChannel中提供的lock()和tryLock()方法可以获得文件锁FileLock对象。


lock和tryLock方法的区别:
a.当lock试图锁定某个文件时,如果无法获得文件锁,程序将会一直阻塞;
b.tryLock是尝试锁定文件,它将直接返回而不是阻塞,如果获得文件锁,则返回文件锁,否则返回null。


lock和tryLock除了定义了无参方法,还定义了如下格式的方法:
lock(long position,long size,boolean shared):对文件从position开始,长度为size的内容加锁。
tryLock(long position,long size,boolean shared):非阻塞方式加锁,参数的作用与上一个方法类似。
当shared为true时,表明该锁是一个共享锁,它允许多个进程来读取文件,阻止其他进程获得对该文件的排他锁,shared为true只能使用在一个可读的Channel上。
当shared为false时,表明该锁是一个排他锁,它将锁住对该文件的写操作,shared为false只能使用在一个可写的Channel上。


FileLock还提供了一个isShared()方法来判断它获得的锁是否是共享锁。


下面是一个案例:

public class FileLockTest {

    public static void main(String[] args) {
        try{
            File file=new File("D:/test1.txt");
            FileChannel fc=new FileInputStream(file).getChannel();
            FileLock flock= fc.tryLock();
            Thread.sleep(10000);//锁定10秒钟
            flock.release();
        }catch(Exception e)
        {
            e.printStackTrace();
        }
    }
}