Java-IO

标签(空格分隔): Java


掌握三点:熟悉I/O类库结构、常用I/O类的功能、掌握如何利用装饰模式进行I/O类的组装

1. I/O系统概览

Java 的 I/O 大概可以分成以下几类:

  • 磁盘操作:File
  • 字节操作:InputStream 和 OutputStream
  • 字符操作:Reader 和 Writer
  • 对象操作:Serializable
  • 新的输入/输出:NIO
  • 网络操作:Socket

2. 磁盘操作:File类

File类可以用于表示文件和目录的信息,但是它不表示文件的内容。 Java中通常的 File 并不代表一个真实存在的文件对象,当你通过指定一个路径描述符时,它就会返回一个代表这个路径相关联的一个虚拟对象,这个可能是一个真实存在的文件或者是一个包含多个文件的目录。

File类常用方法:

方法 描述
String getName() 获取文件的名称
boolean canRead() 判断文件是否是可读的
boolean canWrite() 品判断文件是否可被写入
boolean exits() 判断文件长度是否存在
int length() 获取文件的长度(以字节为单位)
String getAbsolutePath() 获取文件的绝对路径
String getParent() 获取文件的父路径
boolean isFile() 判断此抽象路径名表示的文件是否为普通文件
boolean isDirectory() 判断此抽象路径名表示的是否是一个目录
boolean isHidden 判断文件是否是隐藏文件
long lastModified() 获取文件最后修改时间
Boolean canExecute() 测试应用程序是否可以执行此抽象路径名表示的文件。
boolean createNewFile() 当且仅当具有该名称的文件尚不存在时,原子地创建一个由该抽象路径名命名的新的空文件。
boolean delete() 删除由此抽象路径名表示的文件或目录。
File[] listFiles() 返回一个抽象路径名数组,表示由该抽象路径名表示的目录中的文件。
String[] list() 返回一个字符串数组,命名由此抽象路径名表示的目录中的文件和目录。
boolean mkdirs() 创建由此抽象路径名命名的目录,包括任何必需但不存在的父目录。可创建多层文件包
boolean mkdir() 创建由此抽象路径名命名的目录。只能创建一层文件包
boolean reNameTo(File dest) 重命名由此抽象路径名表示的文件。
boolean setReadOnly() 标记由此抽象路径名命名的文件或目录,以便只允许读取操作。
boolean setWritable(boolean writable) 一种方便的方法来设置所有者对此抽象路径名的写入权限。

File类用法讲解

递归地列出一个目录下所有文件:

public static void listAllFiles(File dir) {
    if (dir == null || !dir.exists()) {
        return;
    }
    if (dir.isFile()) {
        System.out.println(dir.getName());
        return;
    }
    for (File file : dir.listFiles()) {
        listAllFiles(file);
    }
}

过滤指定文件目录并返回需要文件列表:

    public static File[] local(File dir, final String regex) {
        return dir.listFiles(new FilenameFilter() {
            Pattern pattern = Pattern.compile(regex);
            @Override
            public boolean accept(File arg0, String arg1) {
                return pattern.matcher(arg1).matches();
            }
        });
    }

3. 流

它代表任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象。
流是数据和数据处理过程的统称。流操作关心三部分内容:数据源、目标以及过程。

这些数据源包括:

  • 字节数组
  • String对象
  • 文件
  • “管道”,工作方式与实际管道相似,即,从一段输入,从另一端输出。
  • 一个由其他种类的流组成的序列,以便我们可以将它们收集合并到一个流内。
  • 其他数据源,如Internet连接等。

而且还需要多种不同的方式与它们通信:

  • 顺序
  • 随机存取
  • 缓冲
  • 按字节
  • 按字符
  • 按行

4. 字节操作:InputStream 和 OutputStream

IO-1.png-2395kB

4.1 装饰者模式

我们常常通过层叠多个对象来提供所期望的功能,这就是装饰者模式,FilterOutputStream和FilterInputStream就是用来提供装饰类接口以控制特定输入流(InputStream)和输出流(OutStream)的两个类。

IO-2.png-26.6kB

  • InputStream 是抽象组件;
  • FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
  • FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。

实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。

FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);

4.2 装饰类

1)DataInputStream与DataOutputStream

DataInputStream是用来装饰其它输入流,它允许应用程序以与机器无关方式从底层输入流中读取基本Java 数据类型。应用程序可以使用DataOutputStream(数据输出流)将各种基本数据类型以及String对象格式化写入由DataInputStream(数据输入流)读取的数据。

    //文件 格式化的字节输入流
    public static String fileByteInputStream(String filename) throws IOException {
        DataInputStream dataInputStream = new DataInputStream(
                new BufferedInputStream(new FileInputStream(filename)));
        StringBuffer sb = new StringBuffer();
        while (dataInputStream.available() != 0) {
            sb.append((char)dataInputStream.readByte());
        }
        dataInputStream.close();
        return sb.toString();
    }

    //文件 字节输出流(存储与恢复)
    public static void fileByteOutputStream(String filename) throws IOException{
        DataOutputStream out = new DataOutputStream(
                new BufferedOutputStream(new FileOutputStream(filename)));
        out.writeInt(989);
        out.writeDouble(1.3123);
        out.writeUTF("卧槽niubi");
        out.writeInt(123);
        out.close();
        DataInputStream in = new DataInputStream(
                new BufferedInputStream(new FileInputStream(filename)));
        System.out.println(in.readInt());
        System.out.println(in.readDouble());
        System.out.println(in.readUTF());
        System.out.println(in.readInt());
        in.close();
    }

2)PrintStream

用于格式化输出,其中DataOutputStream处理数据地存储,PrintStream处理显示。
两个重要方法:print和prinln,后者多个换行符。

4.3 典型使用方法

1)实现文件复制

public static void copyFile(String src, String dist) throws IOException {
    FileInputStream in = new FileInputStream(src);  // 有个重载构造方法,第二个参数为append参数,默认值为false,即默认覆盖
    FileOutputStream out = new FileOutputStream(dist);

    byte[] buffer = new byte[20 * 1024];  // 20KB
    int cnt;

    // read() 最多读取 buffer.length 个字节
    // 返回的是实际读取的个数
    // 返回 -1 的时候表示读到 eof,即文件尾
    while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
        out.write(buffer, 0, cnt);
    }

    in.close();
    out.close();
}

5. 字符操作:Reader和Writer

Reader和Writer的提供兼容Unicode与面向字符的I/O功能:

IO-3.png-1681kB

5.1 编码与解码

编码就是把字符转换为字节,而解码是把字节重新组合成字符。如果编码和解码过程使用不同的编码方式那么就出现了乱码。

  • GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
  • UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;

String的编码方式:

String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。

String str1 = "中文";
byte[] bytes = str1.getBytes("UTF-8");
String str2 = new String(bytes, "UTF-8");
System.out.println(str2);

在调用无参数 getBytes() 方法时,默认编码为系统编码(file.encoding)。
Java采用双字节编码,可以使用一个 char 存储中文和英文。

深入分析 Java 中的中文编码问题

5.2 常用子类

1)InputStreamReader和OutputStreamWriter

适配器模式可以把字节流转换为字符流,其使用的编码如未指定则为系统编码

2)BufferedReader和BufferedWriter

这两个并不是装饰类,在字符流类库中FilterWriter只是个占位符,前面也不是它的子类。

    // 文件 缓冲字符输入流
    public static String fileBufferedCharReader(String fileName) throws IOException{
        BufferedReader in = new BufferedReader(new FileReader(fileName));
        String s;
        StringBuffer sb = new StringBuffer();
        while ((s = in.readLine()) != null) {
            sb.append(s + "\n"); //readLine已经去除换行
        }
        in.close();
        return sb.toString();
    }

3)PrintWriter

既能接受Writer对象又能接受OutputStream对象的构造器,格式化接口和PrintStream相同。

4)StringReader

源为一个字符串,从内存中读入。

    // 内存 字符输入流
    public static String memoryCharReader(String fileName) throws IOException {
        StringReader rd = new StringReader(fileBufferedCharReader(fileName));
        int c;
        StringBuffer sb = new StringBuffer();
        while((c = rd.read()) != -1) {
            sb.append((char)c);
        }
        rd.close();
        return sb.toString();
    }

5.3 典型使用方法

1)复制文件

    // 文件 字符输出流 (复制文件)
    public static void fileCharWriter(String sourceFileName, String targetFileName) throws IOException {
        BufferedReader in = new BufferedReader(new FileReader(sourceFileName));
        File targetFile = new File(targetFileName);  //目标不存在则自动创建(覆盖)
        PrintWriter out = new PrintWriter(new FileWriter(targetFile)); 
        // PrintWriter out = new PrintWriter(targetFile); ——另外一种构造形式,更简洁
        String s;
        while ((s = in.readLine()) != null) {
            out.println(s);   // readLine不读取换行
        }
        out.close();
        in.close();
        // 输出已复制的文件
        System.out.println(InputStreamReaderTest.fileBufferedCharReader(targetFileName));
    }

2)实现逐行输出文本文件的内容

public static void readFileContent(String filePath) throws IOException {

    FileReader fileReader = new FileReader(filePath);
    BufferedReader bufferedReader = new BufferedReader(fileReader);

    String line;
    while ((line = bufferedReader.readLine()) != null) {
        System.out.println(line);
    }

    // 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
    // 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法
    // 因此只要一个 close() 调用即可
    bufferedReader.close();
}
/**
 * 封装了文件的操作工具类
 *
 */
public class TextFile {

    public static String read(String filename) {
        StringBuffer sb = new StringBuffer();
        try {
            BufferedReader in = new BufferedReader(new FileReader(filename));
            try {
                String s;
                while ((s = in.readLine()) != null) {
                    sb.append(s + "\n");
                }
            } finally {
                in.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return sb.toString();
    }
    public static void write(String filename, String text) {
        try {
            File file = new File(filename);
            PrintWriter out = new PrintWriter(new FileWriter(file));
            try {
                out.print(text);
            } finally {
                out.close();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public static void write(String filename, List<String> list) {
        try {
            File file = new File(filename);
            PrintWriter out = new PrintWriter(new FileWriter(file));
            try {
                for (String s : list)
                    out.println(s);
            } finally {
                out.close();
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
    public static void main(String[] args) throws UnsupportedEncodingException { 
        List<String> list = Arrays.asList(read("E:\\test3.txt").split("\n"));
        for (String string :list) {
            System.out.println(string);   //输出按照eclipse的编码设置,如果编码格式和写入文件的编码不一致时,自然elcipse解码显示也会乱码
        }
        write("E:\\test4.txt", list);
        write("E:\\test5.txt", "工具类write");

    }
}

6. 独立的类:RandomAccessFile

RandomAccessFile适用于由大小已知的记录组成的文件。

  • seek方法将记录从一处转移到另外一处,然后读取和修改记录;
  • getFilePointer用于查找当前所处文件位置;
  • length判断文件最大尺寸;
  • 构造器的参数用于指示是随机读(r)、还是既读又写(rw),并不支持只写文件。
public class RandomAccessFileTest {
    public static void display() throws IOException{
        RandomAccessFile rFile = new RandomAccessFile("E:\\test2.txt", "r");
        for (int i = 0; i < 6; ++i) {
            System.out.println(rFile.readDouble());
        }
        System.out.println(rFile.readUTF());
        rFile.close();
    }
    public static void main(String[] args) throws IOException{
        RandomAccessFile rf = new RandomAccessFile("E:\\test2.txt", "rw");    
        for (int i = 0; i < 6; ++i) {
            rf.writeDouble(i * 1.024);
        }
        rf.writeUTF("结束");
        rf.close();
        display();
        // 插入指定位置数据
        rf = new RandomAccessFile("E:\\test2.txt", "rw");
        rf.seek(5 * 8); //第五个字节位置
        rf.writeDouble(99.99);
        rf.close();
        display();
    }
}

7. 标准I/O

按照标准I/O模型,Java提供了Sytem.in、System.out、System.error

1)System.in

这是一个没有包装过的InputStream,故在使用其之前必须进行包装:

public class SystemIOTest {

    public static void main(String[] args) throws IOException{
        System.out.println(System.getProperty("file.encoding")); // 源代码与控制台字符编码(eclipse),但编译后类文件又转换为Java内部编码unicode
        BufferedReader in = new BufferedReader(
                new InputStreamReader(System.in));
        String s;
        while ((s = in.readLine()) != null && s.length() != 0) {
            System.out.println(s);
        }
    }
}

2)System.out

这是一个PrintStream类型,而PrintWriter有个重载构造器接受第一个参数就为PrintStream类型,第二个参数为开启自动清空功能(true)。

8. 磁盘I/O工作机制

从磁盘读取文件:

IO-4.png-22.6kB

当传入一个文件路径,将会根据这个路径创建一个 File 对象来标识这个文件,然后将会根据这个 File 对象创建真正读取文件的操作对象,这时将会真正创建一个关联真实存在的磁盘文件的文件描述符 FileDescriptor,通过这个对象可以直接控制这个磁盘文件。由于我们需要读取的是字符格式,所以需要 StreamDecoder 类将 byte 解码为 char 格式,至于如何从磁盘驱动器上读取一段数据,由操作系统帮我们完成。

9. 对象操作:Serializable

9.1 序列化与反序列化

序列化就是将一个对象转换成字节序列,方便存储和传输,实现轻量级持久化;反序列化就是将字节序列转换为对象,转换过程就是重新构造对象的过程,即会调用构造器等。

  • 显式序列化:ObjectOutputStream.writeObject()
  • 显式反序列化:ObjectInputStream.readObject()

1)无法序列化静态变量

不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。

2)一个子类实现了 Serializable 接口,它的父类都没有实现 Serializable 接口,序列化该子类对象,然后反序列化后输出父类定义的某变量的数值,该变量数值与序列化时的数值不同。

需要让父类也实现Serializable接口,如果不实现的话,就必须要有默认的父类无参构造器,而有默认无参构造器也不代表能正确。
由于没有实现接口所以父类不会被序列化,自然也不会被反序列。在反序列化时,构造实例对象,调用子类的无参构造器,而无参构造器必须调用父类的无参构造器,所以父类的变量都是父类无参构造器提供的,没有提供则默认内存值为0(int 型的默认是 0,string 型的默认是 null),所以不能说明反序列化后父类的继承下来的变量和序列化前一致。

9.2 Serializable

序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常,和cloneable类似。

public static void main(String[] args) throws IOException, ClassNotFoundException {

    A a1 = new A(123, "abc");
    String objectFile = "file/a1";

    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
    objectOutputStream.writeObject(a1);
    objectOutputStream.close();

    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
    A a2 = (A) objectInputStream.readObject();
    objectInputStream.close();
    System.out.println(a2);
}

private static class A implements Serializable {

    private int x;
    private String y;

    A(int x, String y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "x = " + x + "  " + "y = " + y;
    }
}

9.3 transient

transient 关键字可以使一些属性不会被序列化。

ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化,所以需要通过自定义序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据,在要序列化的对象类中重写writeObject和readObject,这两个方法会在ObjectInputStream与ObjectOutputStream中被通过反射调用。

方法writeObject处理对象的序列化,如果声明该方法,它将会被ObjectOutputStream调用而不是默认的序列化进程。ObjectOutputStream使用了反射来寻找是否声明了这两个方法,因为ObjectOutputStream使用getPrivateMethod,所以这些方法不得不被声明为priate以至于供ObjectOutputStream来使用。

private transient Object[] elementData;
class SessionDTO implements Serializable {  
    private static final long serialVersionUID = 1L;  
    private transient int data; // Stores session data  

    //Session activation time (creation, deserialization)  
    private transient long activationTime;   

    public SessionDTO(int data) {  
        this.data = data;  
        this.activationTime = System.currentTimeMillis();  
    }  

    private void writeObject(ObjectOutputStream oos) throws IOException {  
        oos.defaultWriteObject();  
        oos.writeInt(data);  
        System.out.println("session serialized");  
    }  

    private void readObject(ObjectInputStream ois) throws IOException,  
            ClassNotFoundException {  
        ois.defaultReadObject();  
        data = ois.readInt();  
        activationTime = System.currentTimeMillis();  
        System.out.println("session deserialized");  
    }  

    public int getData() {  
        return data;  
    }  

    public long getActivationTime() {  
        return activationTime;  
    }  
}

9.4 Externalizable

这个接口继承了Serializable接口,同时增添了两个方法,writeExternal和readExternal,这两个方法会在序列化与反序列化还原过程中自动调用,以便来控制序列化(对指定属性序列化或不序列化)。和Serializable接口不同的是,Serializable会自动序列化所有内容(除非用transient修饰),而Externalizable则要显式地序列化,所以它可以控制序列化过程。

class B1 implements Externalizable {
    private String s;
    private int i;
    public B1() {
        System.out.println("B1 Constructor");
    }
    public B1(String s, int i) {
        System.out.printf("B1 Constructor(String %s, int %d)", s, i);
        this.s = s;
        this.i = i;
    }
    public String toString() {
        return s + i;
    }
    public void readExternal(ObjectInput arg0) throws IOException, ClassNotFoundException {
        System.out.println("B1 readExternal:");
        this.s = (String)arg0.readObject();
        this.i = arg0.readInt();
    }
    public void writeExternal(ObjectOutput arg0) throws IOException {
        System.out.println("B1 writeExternal:");
        arg0.writeObject(this.s);
        arg0.writeInt(this.i);
    }
}
class B2 implements Externalizable {
    B2() {  // 当前包内,不是公有
        System.out.println("B2 Constructor");
    }
    public void readExternal(ObjectInput arg0) throws IOException, ClassNotFoundException {
        System.out.println("B2 readExternal");
    }
    public void writeExternal(ObjectOutput arg0) throws IOException {
        System.out.println("B2 writeExternal");
    }
}

public class ExternalizableTest {

    public static void main(String[] args) throws IOException, ClassNotFoundException{
        System.out.println("Constructor:");
        B1 b1 = new B1();
        B2 b2 = new B2();
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("E:\\b.out"));
        System.out.println("writeObject:");
        out.writeObject(b1);
        out.writeObject(b2);
        out.close();
        ObjectInputStream in = new ObjectInputStream(new FileInputStream("E:\\b.out"));
        System.out.println("readObject:");
        b1 = (B1)in.readObject();
        //b2 = (B2)in.readObject(); 错误,由于B2的构造函数不是公有,实现Externalizable接口在反序列化的时候将会执行所有构造函数
        in.close();

        // 完整保存和恢复一个序列化对象
        System.out.println();
        System.out.println("B1 Completed:");
        System.out.println("B1 Constructor:");
        B1 b12 = new B1("I'm", 97);
        out = new ObjectOutputStream(new FileOutputStream("E:\\b1.out"));
        System.out.println("writeObject:");
        out.writeObject(b12);
        out.close();
        in = new ObjectInputStream(new FileInputStream("E:\\b1.out"));
        System.out.println("readObject:");
        b1 = (B1)in.readObject();
        System.out.println(b1);
    }

}

IBM: Java序列化的高级认识


10. NIO

10.1 流与块的区别

面向流 的 I/O 系统一次一个字节地处理数据。一个输入流产生一个字节的数据,一个输出流消费一个字节的数据。为流式数据创建过滤器非常容易。链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。
一个 面向块 的 I/O 系统以块的形式处理数据。每一个操作都在一步中产生或者消费一个数据块。按块处理数据比按(流式的)字节处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。

10.2 通道和缓冲区

1)缓冲区

Buffer 是一个对象,它包含一些要写入或者刚读出的数据。 在 NIO 中加入 Buffer 对象,体现了新库与原 I/O 的一个重要区别。在面向流的 I/O 中,您将数据直接写入或者将数据直接读到 Stream 对象中。

在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的。在写入数据时,它是写入到缓冲区中的。任何时候访问 NIO 中的数据,您都是将它放到缓冲区中。

缓冲区实质上是一个数组,它有多种类型,通常为字节数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer
  • DoubleBuffer

每一个 Buffer 类都是 Buffer 接口的一个实例,每一个 Buffer 类都有完全一样的操作,只是它们所处理的数据类型不一样。

2)通道

Channel是一个对象,可以通过它读取和写入数据,通道就像是IO中的流。通道是双向的,而流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类), 而通道可以用于读、写或者同时用于读写。

所有数据都通过 Buffer 对象来处理。将数据写入包含一个或者多个字节的缓冲区;同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

10.3 NIO读写

1)获取通道并创建缓冲区

FileInputStream fin = new FileInputStream( "readandshow.txt" ); 
FileChannel fc = fin.getChannel(); // 从 FileInputStream 获取通道
ByteBuffer buffer = ByteBuffer.allocate( 1024 ); // 创建缓冲区

2)写入文件

for (int i=0; i<message.length; ++i) {
     buffer.put( message[i] );
}
buffer.flip();  // 重设缓冲区(见后)
fc.write( buffer );

flip会重设缓冲区内部参数;在这里同样不需要告诉通道要写入多数据,缓冲区的内部统计机制会跟踪它包含多少数据以及还有多少数据要写入。

3)读入文件

buffer.clear();
fc.read(buffer);

clear会重设缓冲区内部参数;这里同样也不需要计算读入多少文件。

4)读写结合

buffer.clear();
int r = fcin.read( buffer );

if (r==-1) {
     break;
}

buffer.flip();
fcout.write( buffer );

10.4 缓冲区内部实现

1)状态变量

position

在写入缓冲区时,position变量跟踪已经写了多少数据,更准确地说,它指定了下一个字节将放到数组的哪一个元素中。
在读出缓冲区时,position 值跟踪从缓冲区中获取了多少数据,更准确地说,它指定下一个字节来自数组的哪一个元素。

limit

在写入缓冲区时,limit 变量表明还有多少空间可以放入数据。
在读出缓冲区时,limit 变量表明还有多少数据需要取出。

position 总是小于或者等于 limit。

capacity

缓冲区的 capacity 表明可以储存在缓冲区中的最大数据容量。实际上,它指定了底层数组的大小 ― 或者至少是指定了准许我们使用的底层数组的容量。

limit 决不能大于 capacity。

2)flip()和clear()

flip()

在数据写到输出通道之前,必须调用 flip() 方法。这个方法做两件非常重要的事:

  • 它将 limit 设置为当前 position。
  • 它将 position 设置为 0。

flip前:
NIO-1.png-3.8kB
flip后:
NIO-2.png-3.9kB

position 被设置为 0,这意味着我们得到的下一个字节是第一个字节,即读出缓冲区从0开始;limit 已被设置为原来的 position,这意味着它包括以前读到的所有字节。

clear()

在数据读入缓冲区之前,必须调用 clear() 方法。Clear 做两种非常重要的事情:

  • 它将 limit 设置为与 capacity 相同。
  • 它设置 position 为 0。

clear前:
NIO-3.png-3.8kB
clear后:
NIO-4.png-3.5kB

2)访问方法

get()

byte get();
ByteBuffer get( byte dst[] );
ByteBuffer get( byte dst[], int offset, int length );
byte get( int index );

put()

ByteBuffer put( byte b );
ByteBuffer put( byte src[] );
ByteBuffer put( byte src[], int offset, int length );
ByteBuffer put( ByteBuffer src );
ByteBuffer put( int index, byte b );

10.5 缓冲区其它

1)缓冲区分配和包装

ByteBuffer buffer = ByteBuffer.allocate( 1024 ); // 分配大小并包装为缓冲区

byte array[] = new byte[1024]; 
ByteBuffer buffer = ByteBuffer.wrap( array ); // 通过现有数组直接包装缓冲区

2)缓冲区分片

slice()方法根据现有的缓冲区创建一种子缓冲区。也就是说,它创建一个新的缓冲区,新缓冲区与原来的缓冲区的一部分共享数据。

可以编写自己的函数处理整个缓冲区,而且如果想要将这个过程应用于子缓冲区上,您只需取主缓冲区的一个片,并将它传递给您的函数。这比编写自己的函数来取额外的参数以指定要对缓冲区的哪一部分进行操作更容易。

10.6 异步I/O

NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,但是可以通过设计模式,即Reactor模式来实现I/O多路复用,已经被越来越多地应用到大型应用服务器,成为解决高并发与大量连接、I/O处理问题的有效方式。

1)同步阻塞I/O与同步非阻塞I/O

同步阻塞I/O是每连接每线程的模型,之所以使用多线程,主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的,当一个连接在处理I/O的时候,系统是阻塞的,如果是单线程的话必然就挂死在那里。
现在的多线程一般都使用线程池,可以让线程的创建和回收成本相对较低。在活动连接数不是特别高(小于单机1000)的情况下,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。不过,这个模型最本质的问题在于,严重依赖于线程。

同步非阻塞I/O是一种没有阻塞地读写数据的方法,实现了IO多路复用中的Reactor模型。一个线程Thread使用一个选择器Selector通过轮询的方式去监听多个通道Channel 上的事件,从而让一个线程就可以处理多个事件。当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找到 IO 事件已经到达的 Channel 执行。

2)创建选择器

Selector selector = Selector.open();

3)将通道注册特定事件到选择器上

ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);

通道必须配置为非阻塞模式,否则通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件。

注册的具体事件,主要有以下几类:

  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE

4)监听事件

int num = selector.select();

使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。

5)获取到达的事件

Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) {
        // ...
    } else if (key.isReadable()) {
        // ...
    }
    keyIterator.remove();
}

6)事件死循环

因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。

while (true) {
    int num = selector.select();
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = keys.iterator();
    while (keyIterator.hasNext()) {
        SelectionKey key = keyIterator.next();
        if (key.isAcceptable()) {
            // ...
        } else if (key.isReadable()) {
            // ...
        }
        keyIterator.remove();
    }
}

10.7 内存映射文件I/O

内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。尽管创建内存映射文件相当简单,但是向它写入可能是危险的。仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。

内存映射文件 I/O 是通过使文件中的数据神奇般地出现为内存数组的内容来完成的。文件中实际读取或者写入的部分才会送入(或者 映射 )到内存中。

现代操作系统一般根据需要将文件的部分映射为内存的部分,从而实现文件系统。Java内存映射机制不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。

将一个 FileChannel (它的全部或者部分)映射到内存中。为此我们将使用 FileChannel.map() 方法。下面代码行将文件的前 1024 个字节映射到内存中:

MappedByteBuffer mbb = fc.map( FileChannel.MapMode.READ_WRITE,
     0, 1024 );

map()方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行行映射。


11. 网络操作:Socket

  • Sockets:使用 TCP 协议实现网络通信;
    • ServerSocket:服务端实例
    • Socekt:客户端实例
    • 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
  • Datagram:使用 UDP 协议实现网络通信。
    • DatagramSocket:通信类
    • DatagramPacket:数据包类

12. 参考资料

  • Eckel B. Java 编程思想 [M]. 机械工业出版社, 2007.
  • Cay S. Horstmann.JAVA核心技术(卷1)[M]. 机械工业出版社, 2008.
  • CyC的CS-Notes:Java篇