Java学习笔记:输入、输出

这篇文章是对自己学习的一个总结,学习资料是疯狂Java讲义第三版,李刚编,电子工业出版社出版。


流的分类

Java将不同的输入/输出流(键盘、文件、网络连接等)抽象表达为“流”(stream),通过流的方式允许Java程序使用相同过的方式来访问不同的输入/输出流。stream是从起源到接受的有序数据。

流从不同的角度可以分为以下几类

  • 输入流和输出流

从流的流向来分可以分为输出流和输入流。我们只能从输入流中读取数据,不能从输入流中写入数据;相反,我们只能向输出流中写入数据,不能从里面读出数据。

另外也可以以内存为标准,从内存流出到其它地方的流,比如硬盘,称为输出流;流入内存的流为输入流。

  • 字符流和字节流

从流操作的基本数据单位来看,可以分为字符流和字节流。字节流操作的数据单元是8位二进制;字符流操作的数据单元是16位二进制的字符流。

  • 节点流和处理流

节点流就是直接和物理节点关联的流。比如面向文件的FileInputStream,面向对象的ObjectOutputStream,面向数组的ByteArrayInputStream等都是节点流。但是节点流的功能可能不会很完善,这时候我们可以用处理流来包装节点流,然后直接使用处理流的方法就相当于操作节点流,那处理流出多来的方法和复写节点流的方法就是对原节点流的加强。处理流的这种加强是基于装饰模式实现的。关于装饰模式,有兴趣可以看这篇文章Java设计模式:装饰模式

如何区分节点流和处理流可以看这篇文章 Java学习笔记:如何区分节点流和处理流


流的继承架构

Java中的IO流涉及到40多个类,这些类看起来很繁杂但是他们其实非常有序,并且彼此之间联系很紧密。Java的IO流的40多个类都是以下面的4个抽象类作为基类的,也就是所有的IO类都直接或间接继承下面的4各类。

  • InputStream、Reader:所有的输入流的基类,前者是字节流,后者是字符流。
  • OutputStream、Writer:所有的输入流的基类,前者是字节流,后者是字符流。

对于InputStream和Reader而言,可以将输入设备抽象成一个水管,水管里的个水滴是最小单位的数据。输入流采用隐式的指针来表示当前正准备从哪个水滴开始读取,每当程序从水管中取出一个或多个水滴后,记录指针就自动向后移动,并且InputStream和Reader都有一些方法来控制指针的移动。

下图演示了输入流的输入过程

输入流的过程也是类似,也是有一个指针记录着输出的位置。

从上图我们可以看出处理流的两个好处:以缓冲的方式来提高输入/输出的效率。一次性输入输出一批数据而不是一个一个地处理。

输入输出流让数据的转运变得简单,无论数据是从什么地方传到什么地方,Java的写法都大体相同,只需要根据数据的输入来源和输出来源来稍微做点改变就好。

前面说到输入输出是以内存为参考,输入到内存就是输入流,从内存中输出就是输出流,数据从一个地方转到另一个地方都是这样的过程:来源——>内存——>目的地。

比如来源是键盘,目的地是硬盘的文件,那用InputStream接受键盘的数据到内存,然后FileOutputStream输出到硬盘。其它的也可以想象都是大致的流程。所以输入输出流让数据的传输变得简单。


字符流和字节流

字节流操作的基本单元是字节,8位。字符流操作的基本单元是字符,16位。

  • InputStream和Reader

因为所有的输入流都是以这两个为基类,所以就介绍一下这两个抽象类的方法,这些方法适用于所有的输入流。

InputStream里包含下面三个方法:

  1. int read():从输入流中读取单个字节,返回锁读取的字节数据(可直接转成int类型)。
  2. int read(byte[] b):从输入流中最多读取b.length个字节的数据,并存储在字节数组b中,返回实际读取到的字节数。
  3. int read(byte[] b, int off, int len):从输入流中的off位置开始读取len长度的字节的数据(包括off位置上的元素),并存储在字节数组中,返回实际读取的字节数。

Reader里也是和上面一样的方法,只不过每次读取的数据的单位是字符而不是字节。

下面是从文件中读取数据到内存并输出的示例。先在桌面上创建一个txt文件,第一行是hello world,第二行是你好,世界。

然后运行如下代码

public class Test {
    public static void main(String[] args) throws IOException {
        FileInputStream fis = new FileInputStream("/Users/username/Desktop/test");
        byte[] buffer = new byte[1024];
        int length = 0;
        while((length = fis.read(buffer)) > 0){
            System.out.println(new String(buffer, 0, length));
        }
        fis.close();
    }
}

输出结果如下所示

这里要注意,如果将缓冲区buffer设为较小的大小,那在输入中文的时候可能导致乱码。比如buffer的大小为3的话,输出结果如下

因为本次txt文件是utf-8编码格式,在这个格式下一个汉字或者中文符号是3字节(这里好像不太对),一个英文字符是1字节·······所以缓冲区要设置得足够大,不然涉及到中文的时候会出现乱码,关于乱码的解决,目前来说只要涉及到中文,用FileReader来输入输出即使buffer的长度是1也不会乱码,至于为什么······可以看(还没写,有时间再补)。

另外File文件流不是内存的资源,必须显示关闭,所以最后有一个fis.close()的代码。

前面说到输入输出是系统有一个指针标记已经输入或输出到哪个数据单元了,现在讲一下InputStream和Reader如何移动指针。

  • void mark(int readLimit):和reset()一起使用。这个函数表示在当前指针的位置做一个记号,然后指针继续移动一段距离后,如果执行reset()函数,那指针就会返回到最新一次标记的位置。不过某些输入流不支持(会抛出异常,如下图),比如FileInputStream和FileReader,所以使用之前用markSupported()判断一下。

  • boolean markSupported():判断该输入流是否支持mark()操作,支持返回true,不支持返回false。
  • void reset():返回最新标记的位置,有可能跑太远而回不去。
  • long skip(long n):指针向后移动n个字节/字符。
  • OutputStream和Writer

OutputStream和Writer都有下面的这三个方法

  • void write(int c):将指定字节 /字符输出到输出流中,其中c既可以代表字节,也可以代表字符。
  • void write(byte[]/char[] buffer):将字节数组或字符数组输出到输出流中。
  • void write(byte[] buffer, int off, int length):将指定字节数组或字符数组从off的位置(包括off)输出length个字节/字符到输出流中。

同时因为Writer是字符型输出流,所以我们可以用字符串来代替字符数组,也就是说Writer有多余的两个write()的重载形式write(String string)和write(String string, int off, int length)。

IO流是物理资源,使用完成之后一定要关闭输出流。


输入输出流体系

  • 处理流

对文件进行输出时,节点流无论是FileOutputStream还是FileWriter都不能直接输出float型的数据,如果想直接输出float数据,就要使用处理流包装节点流。

处理流的实现使用装饰模式实现的,是对字符字节流的一个加强,加强的一个点就是在节点流的基础上能直接float。关于装饰模式,可以看这篇文章Java设计模式:装饰模式

下面是使用PrintStream处理流包装FileOutputStream后直接在文件夹中输出float类型数据

public class Test {
    public static void main(String[] args){
        try(FileOutputStream fos = new FileOutputStream("/Users/users/Desktop/test");
            PrintStream ps = new PrintStream(fos))
        {
            Float x = 123423.12344F;
            ps.println(x);       
        }
        catch (IOException e){
            e.printStackTrace();
        }
    }
}

从上面可以看成,我们将FileOutputStream当做PrintStream构造函数的参数,也就是用PrintStream包装FileOutputStream,对FileOutputStream的功能进行了增强。

FileOutputStream原本是不能直接将字符串输出到文件中的,经过PrintStream包装之后,我们直接使用PrintStream中的方法就能直接输出字符串到文件中。PrintStream中还有很多方法来包装节点流,让我们使用起来更方便,具体可以看文档。

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

  • 输入输出流体系

访问不同的节点需要有不同的节点流,下面是对这个进行总结

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

通常来说,字节流的功能比字符流更强大,因为计算机中所有的数据都是二进制的,字节流可以处理所有的二进制文件。但是涉及到文本数据时,也就是记事本可以显示的数据,因为各种不同的字符集,使用字节流处理文本数据可能会出现乱码。所以,涉及到文本数据时,应该使用字符流;涉及到二进制文件时,应该使用字节流。

其中管道流是用于实现进程之间的通信功能的。

缓冲流是增加了缓冲功能,提高输入输出的效率。缓冲流中的数据必须使用flush()之后才能将缓冲区中的内容写入实际的物理节点。

对象流主要是用于实现对象的序列化。

事实上还有AudioInputStream,ZipInputStream等特殊功能的流,但它们不再java.io包之下,这里不进行介绍。

  • 转换流

上面还提供了转换流,那是用于将字节流转换成字符流。Java中建立转换流的目的是为了方便。流可分为字节流和字符流,字节流功能比字符流更强大,字符流能处理的,字节流一定也能处理。而字符流被创建出来的目的就是为了方便处理文本数据,不导致乱码。所以转换流就是当我们发现字节流中存在文本数据后,为了方便处理,我们使用转换流将字节流转换成字符流。所以转换流只有从字节流转换到字符流,没有字符流转换到字节流。

  • 推回输入流

推回输入流是处理流。这个流能让输入流在读数据的过程中随时插入一段数据,要求输入流先读完这段数据才能继续读取原来的内容。所以如果我们又记录之前读过的数据,只要用unread插入读过的数据,那我们就能继续读取之前读过的内容,所以这个流叫推回输入流。

PushbackInputStream和PushbackReader两个推回输入流,两者的区别就不用说了。

推回输入流提供了unread方法,这个方法有三种重载形式。

  • void unread(byte[]/char[] buf):将buf数组中的内容推回到缓冲区中。之后如果继续执行read方法的话,必须先读取缓冲区中的内容后才能继续读取到流中的信息。
  • void unread(byte[]/char[] buf, int off, int len):同上。
  • void unread(int b):同上。

推回输入流有一个自己的缓冲区,这个缓冲区的大小要在创建流的时候指明。

PushbackReader pr = new PushbackReader(new FileReader("/Users/user/Desktop/test"), 64)

需要注意的是,当推回输入流的缓冲区大小不够,也就是unread()中推回的数据大小大于缓冲区的大小的时候,会报Pushback overflow的IOException异常。

  • 重定向标准输入输出

Java中的标准输入输出System.in和System.out默认情况下分别代表键盘和屏幕。程序通过System.in获取输入时,实际是从键盘获取输入;而使用System.out进行输出时,会将结果输出到屏幕。

重定向标准输入输出就是将标准输入输出原本的来源或去向都换了。比如System.out经过重定向包装后,可以变成输出到文件而不是屏幕。

System类里提供了下面三个重定向标准输入输出流。

  • static void setErr(PrintStream err):重定向“标准”错误输出流。
  • static void setIn(InputStream in):重定向“标准”输入流。
  • static void setOut(PringStream out):重定向“标准”输出流。

比如下面的代码就是重定向标注输出流,调取System.out.print()等方法时,数据是输出到文件而不是屏幕。

try (PrintStream ps = new PrintStream(new FileOutputStream("/Users/user/Desktop/test"))) {
    System.setOut(ps);
    System.out.println("普通字符串");
} catch (IOException e) {
    e.printStackTrace();
}

这个System.out.println()实际执行的仍然FileOutputStream的write方法,所以输出后回清楚test文件里原本的内容。


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

在Java中,使用Runtime对象的exec()方法可以运行平台上的其它程序。这个方法会产生一个Process对象,Process对象代表的事Java程序启动的子进程。


RandomAccessFile

  • RandomAccess简介

RandomAccessFile是输入输出流体系中功能最丰富的文件内容访问类,它提供了许多方法来访问文件内容,而且它既可以读取文件也可以向文件输出。而且RandomAccessFile就像它的名字一样,支持“随机访问”。随机这个词会让人误解,这是个翻译的问题,Random不只是有随机的意思,还有任意的意思,这里翻译成任意比较符合上下文。也就是说,这个类可以访问文件的任意位置。所以,如果只是访问文件的某一部分,而不是整个文件,就应该使用这个类而不是FileInputStream这一类的类。

RandomAccessFile也有记录指针,RandomAccessFile包含下面两个方法来操作文件记录指针。

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

前面说到,FileOutStream这类文件流不支持指针,所以FileOutStream不支持任意读取、输出文件的某一部分;而RandomAccessFile支持指针,所以RandomAccessFile有任意读取输出的属性。

因为RandomAccessFile既可以读文件也可以写,所以它既包含了完全类似于InputStream的三个读方法,也包含了OutputStream的三个写方法。

不过RandomAccessFile也有缺点,就是它只能读写文件,不能读写其它的IO节点。

  • RandomAccessFile的构造器

RandoAccessFile有两个构造器,这两个构造器基本相同,只是指定文件的形式不同而已——一个通过String来指定文件名,另一个通过

File类来指定操作的文件。

这两个构造函数指定了要操作的文件后,还需要一个参数mode来指定访问模式。这个mode有下面四个值。

  1. "r":以只读方式打开指定文件。如果试图通过该RandomAccessFile对文件执行写方法,那将抛出IOException异常。
  2. "rw":以可读可写的方式来操作文件。若文件不存在则创建一个新文件。
  3. "rws":和"rw"模式相同,不同点是,该模式要求对文件的内容或元数据都每个更新都同步写入底层存储设备。可想而知,这个模式的需要消耗很多资源,但是足够稳定,至少不会因为各种意外而丢失未保存的修改。
  4. "rwd":以可读可写方式操作文,和"rw"相同,不同的是,该模式要求对文件的内容都每个更新都同步写入底层存储设备。

对象序列化

对象序列化的目的是将对象保存在磁盘中,或者允许在网络中知己恩传输对象。对象序列化机制允许吧内存中的Java对喜丧转换成与平台无关的二进制流存储在磁盘上或者将这二进制流通过网路传输到另一个网络节点。其它程序一旦获得了这种二进制流(无论是从网络还是磁盘),都可以称这种二进制流恢复成原来的Java对象。恢复二进制恢复成对象的过程就是反序列化。

序列化机制使对象可以脱离程序的运行而独立存在。

如果某个对象要支持序列化机制,那这个对象必须实现下面两个接口之一。

  • Serializable
  • Externlizable

Java的很多类已经实现了Serializable,该接口是一个标记接口,实现该接口不需要实现任何方法,这个接口只是告诉人们这个对象可序列化。Externalizable接口暂时先不管,以后会详细介绍。

所有会在网络上传输的对象的类都应该是可序列化的,否则程序会出现异常。

  • 使用对象流实现序列化

我们可以实验对象的序列化和反序列化过程。我们先定义一个Person类,该类是可序列化的。

class Person implements Serializable{
    public String name;
    public Integer age;
}

主函数中使用对象流将该对象的实例输出到磁盘的txt文件上,因为对象流是处理流,所以它必须建立在其他节点流之上。

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
Person person = new Person();
person.name = "孙悟空";
person.age = 500;
oos.writeObject(person);

现在就有一个object.txt文件。这里需要注意的是,因为ObjectOutputStream是OutputStream的子类,所以对象流的操作的单位是字节而不是字符,所以刚刚得到的object.txt文件直接查看的话会打不开或者出现乱码。

现在开始从object.txt中恢复person实例。

ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt"));
Person p = (Person) ois.readObject();

这样得到的p就是刚才的被序列化的实例化对象。这里注意,Person类中并没有构造器,所以反序列化并不是通过构造器实现的

序列化与反序列化时,属于父类的成员变量会如何处理

当一个可序列化对象有父类时,他的所有父类(无论是直接的还是间接的)要么具有无参的构造器,并在构造器中对成员变量赋值;要么也是序列化对象。如果都不是,那即使对父类的成员变量赋值,系统也不会将父类的成员变量序列化。

比如使上面的Person类继承类A,类A定义如下

class A{
    int a;
    int b;
}

class Person extends A implements Serializable{···}

创建Person类的实例后对父类的成员变量赋值

person.name = "孙悟空";
person.age = 500;
person.a = 3;
person.b = 4;

然后序列化和反序列化过程不变,反序列化得到的对象p的具体信息如下所示

可以看出,父类的成员变量并没有被序列化。如果使类A实现Serializable接口,那么反序列化后a和b就是有值的。

但是如果类A不可序列化,但是有无参的构造函数,如下所示

class A{
    int a;
    int b;
    A() {
        a = 1; 
        b = 2;
    }
}

对person序列化时,无论person对父类的成员变量如何赋值,序列化只会对父类构造器中的赋值进行序列化。比如下面person序列化之前先对a和b赋值。

//person序列化之前先对a,b赋值
person.a = 3;
person.b = 4;

反序列化后得到的p对象的a和b却和父类构造器中的赋值一致

如果A可序列化,那person对A成员变量的改变是可以覆盖构造参数中的赋值

  • 对象引用的序列化

上面提到的类中的成员变量的类型都是基本类型,基本类型都是支持序列化的。但如果成员是引用,那引用的类的类型可能是可序列化也可能是不可序列化,下面就对这种情况进行讨论。

当对一个对象序列化时,势必要对其所有的成员变量序列化,那其中也包括它的引用。但如果它的引用是不可序列化的,那么就会序列化失败,那么着整个类都是不可序列化的,无论这个类有没有实现Serializable接口或者Externalizable接口。

所以一个类要想真正支持序列化,那么它的所有成员变量都必须可序列化

同时引用型的序列化还有一个点值得讨论。就是一个流重复序列化同一个对象,那这个流中是会存在这个对象所有信息的多个副本,还是其他情况?

Teacher类有两个成员,一个是String型,一个是上文中的Person类的引用。

class Teacher implements Serializable{
    public String name;
    public Person person;
    Teacher(String name, Person person){
        this.name = name;
        this.person = person;
    }
}

然后我们创建两个Teacher类对象,这两个对象都有指向同一个Person类对象的引用。

Person person = new Person();
person.name = "孙悟空";
person.age = 500;
Teacher teacher1 = new Teacher("菩提祖师", person);
Teacher teacher2 = new Teacher("唐僧", person);

然后对着三个对象序列化

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
oos.writeObject(person);
oos.writeObject(teacher1);
oos.writeObject(teacher2);

程序先序列化person,那没问题,person的整个对象的信息都放在oos流之中。

然后程序开始序列化teacher1,现在teacher1中有指向person的引用,那么程序又将person的整个对象信息放入oos流之中。

然后程序又开始序列化teacher2,同样的person对象所有信息又被存入oos流之中。

当然,上面的操作并不是系统真正的操作,因为真按上面的说法做,oos流之中就有三份person对象信息。以后反序列化时,就会得到三个person对象。序列化之前的对象和序列化之后得到的对象都不一样了,所以上面的做法是不可能的。

系统对于引用型并不总是序列化整个引用对象的信息。序列化的算法逻辑如下所示。

  1. 所有保存到磁盘的对象都有一个序列化编号。
  2. 当程序试图序列化一个对象时,先查看该对象是否已被序列化过,只有在该对象没有被序列化过的情况,系统才会将该对象的所有信息转化成字节序列。
  3. 如果某个对象已经序列化过,程序将只是直接输出一个序列化编号,而不是再次重新序列化该对象。

所以上面的例子中,那三个类序列化后磁盘文件的存储示意图如左图所示,但如果序列化的顺序改成

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"));
oos.writeObject(teacher1);
oos.writeObject(person);
oos.writeObject(teacher2);

那序列化的存储结构就如右图所示

                                                                      

这种处理就能保证,以后反序列化时,只会读出一个person对象。

同时上面的做法也会引起一些问题。因为系统不会一次性将三个对象都序列化,所以三次序列化的间隙中如果引用对象的一些值改变了,系统时察觉不出来的,系统仍然只存储一个序列化编号,而不会存储改变后的值。

  • 自定义序列化

所谓自定义序列化就是序列化时选择性忽略掉某些成员变量,不去序列化这些成员变量,这样自然也不用在意这些成员变量是否支持序列化。做法也很简单,就砸类的定义中,给成员变量加上transient关键字修饰。比如下面的

class Person implements Serializable{
    public String name;
    public transient Integer age;
}

这样就不会序列化age了。


NIO

前面介绍的输入流输出流都是阻塞式的输入输出,比如InputStream的read方法从流中读取数据时,如果数据园中没有数据,那这个流会阻塞对应线程。

而且传统的输入输出流都是通过字节的移动来处理数据(及时不直接处理字节流,底层的实现还是依赖于字节处理,也就是说,面向流的输入输出系统一次只能处理一个字节,所以面向流的输入输出系统效率通常不高。

从Java1.4开始,Java提供了很多用于处理输入输出的类,这些类被放在java.nio包下,全程是new IO。

  • NIO概述

NIO的速度比传统的IO要快很多,这是因为IO是一个字节一个字节地处理,而NIO是采用内存映射文件的方式来处理输入输出。NIO将文件或文件的一部分区域映射到内存中(借鉴了操作系统中虚拟内存的原理),这样就可以像访问内存一样访问文件。所以NIO比传统IO要快很多(这里我不是很懂,把文件映射到内存中不还是要把文件从磁盘弄到内存中来,弄到内存中不还是和传统IO一样花时间吗,怎么就快了?

Channel和Buffer是新IO中两个核心对象,Channel是对传统的输入输出系统的模拟,在新IO中所有的数据,不管是输入输出都要通过Channel传输。Channel和传统的InputStream,OutputStream最大的区别就是它提供了一个map方法,通过该map方法可以直接将”一块数据“映射到内存中。如果说传统的IO是面向流的处理,那新IO是面向块的处理。

Buffer可以理解成是一个容器,它本质上是一个数组,发送到Channel的所有数据都必须先放在Buffer中,而从Channel读取的数据也必须先放在Buffer中。这样的话,读取或输出数据时,可以直接从Channel中一次性获取到Buffer那么大的数据映射到Buffer中,这就是面向块的处理。

  • 使用Buffer

Buffer是一个抽象类,使用它只能使用它的子类,它的子类有各种基本类型的Buffer(boolean除外),比如ByteBuffer,CharBuffer,ShortBuffer,IntBuffer等,这些代表着存储对应类型的Buffer,其中最常用的是ByteBuffer和CharBuffer。

以上各种类型的Buffer的初始化基本都是通过静态函数

static xxxBuffer allocate(int capacity)来实现,意为创建一个容量为capacity的容器。

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

  1. 容量:容量表示缓冲区能容纳的最大数据量,它不能为负值,并且只能在初始化时指定,之后不能再修改。
  2. 界限: 界限指定以后,缓冲区界限之后的数据(包含界限)都不能被读出或写入。
  3. 位置:用于指明下一个可以被读出或者写入的缓冲区位置索引,类似于IO中的记录指针,该指针是随着缓冲区数据的写入和读出而改变的。比如初始化时位置为0,写一个数据位置就变成1,再写两个数据位置就变成3。

另外,Buffer也支持mark来标记一个位置,类似IO中的mark。Buffer可以直接将position定位到该mark处。所以,这些值应该满足以下关系。0\leqslant mark\leqslant position\leq limit\leq capacity

Buffer有两个很常用的方法,用来读取数据然后被Channel取走数据(或者数据被内存取走)。这两个方法分别是flip和clear。

当Buffer装入数据完成后,调用flip()方法,limit被放置在position所在的地方,然后position=0。这个方法很形象,flip有弹,碰撞的意思,整个过程就像是position和limit的完全弹性碰撞。如下图所示

另一个就是clear()函数,我们从Buffer中读取函数结束后,就可以执行clear()函数,它会让position=0,limit=capacity,这就可以继续装入数据了。要注意这个方法不清空数据,只是改变指针的位置。

Buffer还有其他的方法,但是这里就不多介绍了。

最后说一下Buffer的初始化。使用静态方法allocate()方法创建的Buffer对象是普通Buffer,ByteBuffer(只有ByteBuffer才提供这个方法)还提供一个allocateDirect()方法来创建直接Buffer。直接Buffer创建的代价比普通方法高,但是读取效率更快。所以直接Buffer适合创建长生存期的Buffer。

直接Buffer和普通Buffer在用法上没什么区别,这里不赘述。

  • 使用Channel

前面提到NIO会将文件的部分或全部直接映射到内存的Buffer上,这个映射就是通过Channel完成的。

Channel是个很有原则的数据结构,它只和Buffer进行数据的交互,其他程序是无法和Channel直接交互的。如果其他程序想要
Channel中的数据,或者想把数据放到Channel中,那就必须通过Buffer。

Channel有以下实现类,DatagramChannel、FileChannel、Pipe.SinkChannel、Pipe.SourceChannel、SocketChannel等,名字上可以看出,这些实现类是按功能分类的。Pipe.SinkChannel和Pipe.SourceChannel是用于支持线程之间通信的;ServerSockketChannel和SocketChannel是用于支持TCP网络通信的Channel。

下面讲讲Channel的创建

所有的Channel都没有构造器(这里我是到文档中随便看了几个Channel都没有构造器,文中是说不应该有构造器),只能通过传统节点比如InputStream、OutputStream等节点流的getChanenl()方法构造。不同节点流会得到对应的Channel实现类。比如FileInputStream会得到只有读能力的FileChannel,FileOutputStream会得到只有写能力的FileChanel。

之后来看看Channel最常用的三类方法 

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

了解到Channel的基本知识以后,可以看看下面Channel和Buffer协同合作的实例,就能更清楚NIO如何使用。

我们使用Channel将一个文件的内容复制并覆盖另一个文件。

File f = new File("object.txt");
//读channel,将磁盘数据读到chanenl,然后可用map方法将数据移到Buffer上
FileChannel inChannel = new FileInputStream(f).getChannel();
//写channel,将Buffer上的数据写到目标磁盘
FileChannel outChannel = new FileOutputStream("a.txt").getChannel();
//将数据映射到buffer,buffer的长度和文件的长度一致
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
Charset charset = Charset.forName("GBK");
//目前buffer的position=0,limit=capacity,所以是写入所有数据,如果改变position或limit,那就是输出部分数据了
outChannel.write(buffer);
outChannel.close();
inChannel.close();

RandomAccessFile也支持getChannel。RandomAccessFile得到的Channel也和RandomAccessFile一样,具有读写文件任意部分内容的能力。其实一般的Channel,比如上面例子中用来读的FileChannel,在读的时候也可以指定position和map来读取文件任意部分的内容。但是一般的用来写得Channel就没法从文件的任意位置开始写,他们只能将想要写的内容完全覆盖目标文件的内容,对文件原本的信息是具有完全破坏性的。

RandomAccessFile得到的channel可以通过position(Long newPosition)来指定从文件的newPosition之后新增数据(会删除newPosition之后的数据)。

File f = new File("object.txt");
File a = new File("a.txt");
//读channel,将磁盘数据读到chanenl,然后可用map方法将数据移到Buffer上
FileChannel inChannel = new FileInputStream(f).getChannel();
//写channel,将Buffer上的数据写到目标磁盘
FileChannel outChannel = new RandomAccessFile(a, "rw").getChannel();
//将数据映射到buffer,buffer的长度和文件的长度一致
MappedByteBuffer buffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
//在文件最后添加数据。FileOutputStream的channel也有这个函数,但是只会改变position的值,不会有其他任何变化
outChannel.position(a.length());
outChannel.write(buffer);
outChannel.close();
inChannel.close();

字符集和Charset

计算机中的所有数据都是二进制形式存储,文件,图片,视频等只是外在的不同表现形式。图片音乐等文件先不说,对于文本文件而言,之所以可以看到一个个字符而不是只有0和1,就是因为系统将底层的二进制序列转换成字符的缘故。这个过程涉及到编码(Encode)和解码(Decode)两个概念。

不同的字符集有不同的编码和解码方式,Java默认使用Unicode字符集,其他不使用Unicode的操作系统,读取Java文件时就会出现乱码问题。

Java为此提供了Charset来处理字节序列和字符序列之间的转换关系。该类包含了用于创建解码器和编码器的方法。

我们可以使用Charset.forName()方法来获取特定的字符集对象

Charset cn = Charset.forName("GBK");

然后获取该字符集对象的指定编码器和解码器

CharsetEncoder enEncoder = cn.newEncoder();
CharsetEncoder enDecoder = cn.newDecoder();

之后就可以使用编码器的encode方法和decode方法来进行编码和解码。


Java 7中的NIO2

  • Path、Paths和Files核心API

Files和Paths这两个类很符合Java一贯的命名风格,一看就知道是用来对路径和文件进行操作的类。

Paths只有两个方法,都是get()的不同重载形式,用于获取Path对象,Path就是描述路径的。

Path中有很多强大的功能对路径进行处理,这些方法都比较简单,需要用到的时候再看文档就好,这里就不啰嗦了。只要记得有Path这么一个类来操作路径就好。

Files也是用着很多强大的功能来处理文件,比如下面的几个方法

一看方法名就知道这些方法是用来赋值文件的。知道了这些以后,复制文件就可以不用写麻烦的IO,直接使用Files的方法就好。

  • 使用FileVisitor遍历文件和目录

以前的Java想要遍历指定目录下的所有文件和子目录的话,就只能使用递归的方式。这种方式不仅复杂,灵活性也很差。

但现在不一样了,Files提供了两个方法来遍历文件。

  1. walkFileTree(Path start, FileVisitor<? super Path> visitor>:遍历start路径下的所有文件和子目录。
  2. walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor):和上一个方法功能类似,但是它最多遍历到maxDepth深度的文件。

猜你喜欢

转载自blog.csdn.net/sinat_38393872/article/details/102724588