16.6.3 使用阻塞队列(BlockingQueue)控制线程通信

Java5提供了一个BlockingQueue接口,虽然BlockingQueue也是Queue的子接口,但BlockingQueue的主要用途并不是作为容器,而是作为线程同步的工具。

BlockingQueue特征

BlockingQueue具有一个特征:

  • 当生产者线程试图向BlockingQueue中放入元素时,如果该队列已满,则该生产者线程被阻塞;
  • 当消费者线程试图从BlockingQueue中取出元素时,如果该队列已空,则该消费者线程被阻塞

程序的两个线程通过交替向BlockingQueue中放入元素、取出元素,即可很好地控制线程的通信。

BlockingQueue阻塞方法

BlockingQueue提供如下两个支持阻塞的方法。

方法 描述
void put(E e) 尝试把E元素放入BlockingQueue中,如果该队列的元素已满,则阻塞该线程。
E take() 尝试从BlockingQueue的头部取出元素,如果该队列的元素已空,则阻塞该线程。

Queue接口方法

BlockingQueue继承了Queue接口,当然也可使用Queue接口中的方法。这些方法归纳起来可分为如下三组。

  1. 在队列尾部插入元素。包括add(E e)offer(E e)put(E e)方法,当该队列已满时,这三个方法分别会抛出异常、返回false阻塞队列
  2. 在队列头部删除并返回删除的元素。包括removepoll()take()方法。当该队列已空时,这三个方法分别会抛出异常、返回false、阻塞队列。
  3. 在队列头部取出但不删除元素。包括element()pek()方法,当队列已空时,这两个方法分别抛出异常、返回false

BlockingQueue方法分类

BlockingQueue包含的方法之间的对应关系如表16.1所示。

功能 抛出异常 不同返回值 阻塞线程 指定超时时长
队尾插入元素 add(e) offer(e) put(e) offer(e,time,unit)
队头删除元素 remove() poll() take() poll(time,unit)
获取、不删除元素 element() peek()

BlockingQueue实现类

BlockingQueue与其实现类之间的类图如图16.11所示。
图片

图16.11中以黑色方框框出的都是Java7新增的阻塞队列。从图16.11可以看到,BlockingQueue包含如下5个实现类。

BlockingQueue实现类 描述
ArrayBlockingQueue 基于数组实现的BlockingQueue队列。
LinkedBlockingQueue 基于链表实现的BlockingQueue队列。
SynchronousQueue 同步队列。对该队列的存、取操作必须交替进行。
PriorityBlockingQueue 它并不是标准的阻塞队列。
与前面介绍的PriorityQueue类似,该队列调用remove()poll()take()等方法取出元素时,并不是取出队列中存在时间最长的元素,而是队列中最小的元素
PriorityBlockingQueue判断元素的大小即可根据元素(实现Comparable接口)的本身大小来自然排序,也可使用Comparator进行定制排序。
DelayQueue 它是一个特殊的BlockingQueue,底层基于PriorityBlockingQueue实现。不过,DelayQueue要求集合元素都实现Delay接口(该接口里只有一个longGetDelay()方法),DelayQueue根据集合元素的getDalay()方法的返回值进行排序

程序示例 ArrayBlockingQueue阻塞队列

下面以ArrayBlockingQueue为例介绍阻塞队列的功能和用法。下面先用一个最简单的程序来测试BlockingQueueput()方法。

1
2
3
4
5
6
7
8
9
10
11
import java.util.concurrent.*;

public class BlockingQueueTest {
public static void main(String[] args) throws Exception {
// 定义一个长度为2的阻塞队列
BlockingQueue<String> bq = new ArrayBlockingQueue<>(2);
bq.put("Java"); // 与bq.add("Java"、bq.offer("Java")相同
bq.put("Java"); // 与bq.add("Java"、bq.offer("Java")相同
bq.put("Java"); // ① 阻塞线程。
}
}

上面程序先定义一个大小为2的BlockingQueue,程序先向该队列中放入两个元素,此时队列还没有满,两个元素都可以放入,因此使用put()add()offer()方法效果完全一样。当程序试图放入第三个元素时,

  • 如果使用put()方法尝试放入元素将会阻塞线程,如上面程序①号代码所示。
  • 此时,如果使用add()方法尝试放入元素将会引发异常;
  • 此时,如果使用offer()方法尝试放入元素则会返回false,元素不会被放入

与此类似的是,在BlockingQueue已空的情况下,程序

  • 使用take()方法尝试取出元素将会阻塞线程;
  • 使用remove()方法尝试取出元素将引发异常;
  • 使用poll()方法尝试取出元素将返回false,元素不会被删除.

程序 使用BlockingQueue实现线程通信

掌握了BlockingQueue阻塞队列的特性之后,下面程序就可以利用BlockingQueue来实现线程通信了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
import java.util.concurrent.*;

// 生产者线程
class Producer extends Thread {
private BlockingQueue<String> bq;

public Producer(BlockingQueue<String> bq) {
this.bq = bq;
}

public void run() {
String[] strArr = new String[] { "Java", "Struts", "Spring" };
for (int i = 0; i < 999999999; i++) {
System.out.println(getName() + "生产者准备生产集合元素!");
try {
Thread.sleep(200);
// 尝试放入元素,如果队列已满,线程被阻塞
bq.put(strArr[i % 3]);
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println(getName() + "生产完成:" + bq);
}
}
}
// 消费者线程
class Consumer extends Thread {
private BlockingQueue<String> bq;

public Consumer(BlockingQueue<String> bq) {
this.bq = bq;
}

public void run() {
while (true) {
System.out.println(getName() + "消费者准备消费集合元素!");
try {
Thread.sleep(200);
// 尝试取出元素,如果队列已空,线程被阻塞
bq.take();
} catch (Exception ex) {
ex.printStackTrace();
}
System.out.println(getName() + "消费完成:" + bq);
}
}
}

public class BlockingQueueTest2 {
public static void main(String[] args) {
// 创建一个容量为1的BlockingQueue
BlockingQueue<String> bq = new ArrayBlockingQueue<>(1);
// 启动3条生产者线程
new Producer(bq).start();
new Producer(bq).start();
new Producer(bq).start();
// 启动一条消费者线程
new Consumer(bq).start();
}
}

上面程序启动了3个生产者线程向BlockingQueue集合放入元素,启动了1个消费者线程从BlockingQueue集合取出元素。
本程序的BlockingQueue集合容量为1,因此3个生产者线程无法连续放入元素,必须等待消费者线程取出一个元素后,3个生产者线程的其中之一才能放入一个元素。运行该程序,会看到如下所示的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Thread-1生产者准备生产集合元素!
Thread-0生产者准备生产集合元素!
Thread-2生产者准备生产集合元素!
Thread-3消费者准备消费集合元素!
Thread-0生产完成:[Java]
Thread-0生产者准备生产集合元素!
Thread-3消费完成:[]
Thread-3消费者准备消费集合元素!
Thread-2生产完成:[Java]
Thread-2生产者准备生产集合元素!
Thread-1生产完成:[Java]
Thread-1生产者准备生产集合元素!
Thread-3消费完成:[Java]
Thread-3消费者准备消费集合元素!
Thread-3消费完成:[]
Thread-3消费者准备消费集合元素!

3个生产者线程都想向BlockingQueue中放入元素,但只要其中一个线程向该队列中放入元素之后,其他生产者线程就必须等待,等待消费者线程取出BlockingQueue队列里的元素。

16.6.2 使用Condition控制线程通信

synchronized同步的 使用同步监视器进行通信

如果程序不使用synchronized关键字来保证同步,而是直接使用Lock对象来保证同步,则系统中不存在隐式的同步监视器,也就不能使用wait()notify()notifyAll()方法进行线程通信了。

Lock对象同步的使用Condition对象通信

当使用Lock对象来保证同步时,Java提供了一个Condition类来保持协调,使用Condition可以让那些已经得到Lock对象却无法继续执行的线程释放Lock对象,Condition对象也可以唤醒其他处于等待的线程

Condition将同步监视器方法(wait()notify()notifyAll())分解成截然不同的对象,以便通过将这些对象与Lock对象组合使用,为每个对象提供多个等待集(wait-set)。
在这种情况下,

  • Lock替代了同步方法或同步代码块,
  • Condition替代了同步监视器的功能

如何创建Condition实例

Condition实例被绑定在一个Lock对象上。要获得特定Lock实例的Condition实例,调用Lock对象的newCondition方法即可

Condition类提供了如下三个方法。

方法 描述
void await() 类似于隐式同步监视器上的wait()方法,导致当前线程等待,直到其他线程调用该Conditionsignal()方法或signalAll()方法来唤醒该线程
void signal() 唤醒在此Lock对象上等待的单个线程。如果所有线程都在该Lock对象上等待,则会选择唤醒其中一个线程。选择是仼意性的。只有当前线程放弃对该Lock对象的锁定后(使用await方法),才可以执行被唤醒的线程。
void signalAll() 唤醒在此Lock对象上等待的所有线程。只有当前线程放弃对该Lock对象的锁定后才可以执行被唤醒的线程。
await方法变体 描述
boolean await(long time, TimeUnit unit) Causes the current thread to wait until it is signalled or interrupted, or the specified waiting time elapses.
long awaitNanos(long nanosTimeout) Causes the current thread to wait until it is signalled or interrupted, or the specified waiting time elapses.
void awaitUninterruptibly() Causes the current thread to wait until it is signalled.
boolean awaitUntil(Date deadline) Causes the current thread to wait until it is signalled or interrupted, or the specified deadline elapses.

程序 使用Lock同步 使用Condition通信

账户类

下面程序中Account使用Lock对象来控制同步,并使用Condition对象来控制线程的协调运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Account {
// 显式定义Lock对象
private final Lock lock = new ReentrantLock();
// 获得指定Lock对象对应的Condition
private final Condition cond = lock.newCondition();
// 封装账户编号、账户余额的两个成员变量
private String accountNo;
private double balance;
// 标识账户中是否已有存款的旗标
private boolean flag = false;

public Account() {
}

// 构造器
public Account(String accountNo, double balance) {
this.accountNo = accountNo;
this.balance = balance;
}

// accountNo的setter和getter方法
public void setAccountNo(String accountNo) {
this.accountNo = accountNo;
}

public String getAccountNo() {
return this.accountNo;
}

// 因此账户余额不允许随便修改,所以只为balance提供getter方法,
public double getBalance() {
return this.balance;
}
// 取钱
public void draw(double drawAmount) {
// 加锁
lock.lock();
try {
// 如果flag为假,表明账户中还没有人存钱进去,取钱方法阻塞
if (!flag) {
cond.await();
} else {
// 执行取钱
System.out.println(Thread.currentThread().getName() + " 取钱:" + drawAmount);
balance -= drawAmount;
System.out.println("账户余额为:" + balance);
// 将标识账户是否已有存款的旗标设为false。
flag = false;
// 唤醒其他线程
cond.signalAll();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
// 使用finally块来释放锁
finally {
lock.unlock();
}
}
// 存钱
public void deposit(double depositAmount) {
lock.lock();
try {
// 如果flag为真,表明账户中已有人存钱进去,则存钱方法阻塞
if (flag) // ①
{
cond.await();
} else {
// 执行存款
System.out.println(Thread.currentThread().getName() + " 存款:" + depositAmount);
balance += depositAmount;
System.out.println("账户余额为:" + balance);
// 将表示账户是否已有存款的旗标设为true
flag = true;
// 唤醒其他线程
cond.signalAll();
}
} catch (InterruptedException ex) {
ex.printStackTrace();
}
// 使用finally块来释放锁
finally {
lock.unlock();
}
}

// 下面两个方法根据accountNo来重写hashCode()和equals()方法
public int hashCode() {
return accountNo.hashCode();
}

public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj != null && obj.getClass() == Account.class) {
Account target = (Account) obj;
return target.getAccountNo().equals(accountNo);
}
return false;
}
}

用该程序与上一节的Account.java进行对比,不难发现这两个程序的逻辑基本相似,只是现在显式地使用Lock对象来充当同步监视器,然后需要使用Condition对象来暂停、唤醒指定线程
该示例程序的其他类与前一个示例程序的其他类完全一样,该程序的运行效果与前一个示例程序的运行效果完全一样.

使用管道流进行线程通信

本书第1版还介绍了一种使用管道流进行线程通信的情形,但实际上由于两个线程属于同一个进程,它们可以非常方便地共享数据,因此很少需要使用管道流进行通信,故此处不再介绍那种烦琐的方式。

15.11 本章小结

本章主要介绍了Java输入输出体系的相关知识。本章介绍了如何使用File来访问本地文件系统,以及JavaIo流的三种分类方式。本章重点讲解了o流的处理模型,以及如何使用⑩o流来读取物理存储节点中的数据,归纳了Java不同Io流的功能,并介绍了几种典型Io流的用法。本章也介绍了RandomAccessFile类的用法,通过RandomAccessFile允许程序自由地移动文件指针,任意访问文件的指定位置。
本章也介绍了Java对象序列化的相关知识,程序通过序列化把Java对象转换成二进制字节流,然后就可以把二进制字节流写入网络或者永久存储器。本章还介绍了Java提供的新IO支持,使用新IO能以更高效的方式来进行输入、输出操作。本章最后介绍了Java7提供的NO.2的文件IO和文件系统访问支持2极大地增强了JavaIo的功能

疯狂java讲义4 第15章 练习

  1. 定义一个工具类,该类要求用户运行该程序时输入一个路径。该工具类会将该路径下(及其子目录下)的所有文件列出来。
  2. 定义一个工具类,该类要求用户运行该程序时输入一个路径。该工具类会将该路径下的文件、文件夹的数量统计出来。
  3. 定义一个工具类,该工具类可实现copy功能(不允许使用Files类)。如果被copy的对象是文件,程序将指定文件复制到指定目录下;如果被copy的对象是目录,程序应该将该目录及其目录下的所有文件复制到指定目录下。
  4. 编写仿Windows记事本的小程序。
  5. 编写一个命令行工具,这个命令行工具就像window提供的cmd命令一样,可以执行各种常见的命令,如dirmdcopymove等。
  6. 完善第12章的仿Editplus的编辑器,提供文件的打开、保存等功能

15.10.4 访问文件属性

早期的Java提供的File类可以访问一些简单的文件属性,比如文件大小、修改时间、文件是否隐藏、是文件还是目录等。如果程序需要获取或修改更多的文件属性,则必须利用运行所在平台的特定代码来实现,这是一件非常困难的事情。

文件属性工具类

Java7NIO.2java.nio.file.attribute包下提供了大量的工具类,通过这些工具类,开发者可以非常简单地读取、修改文件属性。这些工具类主要分为如下两类

描述
XxxAttributeView 代表某种文件属性的“视图”
XxxAttributes 代表某种文件属性的“集合”,程序一般通过XxxAttributeView对象来获取XxxAttributes

FileAttributeView

在这些工具类中,FileAttributeView是其他XxxAttributeView的父接口,下面简单介绍一下这些XxxAttributeView

AclFileAttributeView

通过AclFileAttributeView,开发者可以为特定文件设置ACL(Access Control List)及文件所有者属性。它的getAcl()方法返回List<AclEntry>对象,该返回值代表了该文件的权限集。通过setAcl(List)方法可以修改该文件的ACL

方法 描述
List<AclEntry> getAcl() Reads the access control list.
String name() Returns the name of the attribute view.
void setAcl(List<AclEntry> acl) Updates (replace) the access control list.

BasicFileAttributeView

它可以获取或修改文件的基本属性,包括文件的最后修改时间最后访问时间创建时间大小是否为目录是否为符号链接等。它的readAttributes()方法返回一个BasicFileAttributes对象,对文件夹基本属性的修改是通过BasicFileAttributes对象完成的。

BasicFileAttributeView的readAttributes方法 描述
BasicFileAttributes readAttributes() Reads the basic file attributes as a bulk operation.

BasicFileAttributes

BasicFileAttributes接口方法 描述
FileTime creationTime() Returns the creation time.
Object fileKey() Returns an object that uniquely identifies the given file, or null if a file key is not available.
boolean isDirectory() Tells whether the file is a directory.
boolean isOther() Tells whether the file is something other than a regular file, directory, or symbolic link.
boolean isRegularFile() Tells whether the file is a regular file with opaque content.
boolean isSymbolicLink() Tells whether the file is a symbolic link.
FileTime lastAccessTime() Returns the time of last access.
FileTime lastModifiedTime() Returns the time of last modification.
long size() Returns the size of the file (in bytes).

DosFileAttributeView

它主要用于获取或修改文件**DOS相关属性**,比如文件是否只读是否隐藏是否为系统文件是否是存档文件等。它的readAttributes方法返回一个DosFileAttributes对象,对这些属性的修改其实是由DosFileAttributes对象来完成的|

DosFileAttributes

DosFileAttributes接口方法 描述
boolean isArchive() Returns the value of the archive attribute.
boolean isHidden() Returns the value of the hidden attribute.
boolean isReadOnly() Returns the value of the read-only attribute.
boolean isSystem() Returns the value of the system attribute.

FileOwnerAttributeView

它主要用于获取或修改文件的所有者。它的getOwner()方法返回一个UserPrincipal对象来代表文件所有者;也可调用setOwner(UserPrincipal owner)方法来改变文件的所有者

FileOwnerAttributeView方法 描述
UserPrincipal getOwner() Read the file owner.
void setOwner(UserPrincipal owner) Updates the file owner.

PosixFileAttributeView

它主要用于获取或修改POSX(Portable Operating System Interface of INIX)属性,它的readAttributes()方法返回一个PosixFileAttributes对象,该对象可用于获取或修改文件的所有者组所有者访问权限信息(就是UNIXchmod命令负责干的事情)。这个view只在UNNXLinux等系统上有用

PosixFileAttributeView方法 描述
PosixFileAttributes readAttributes() Reads the basic file attributes as a bulk operation.

PosixFileAttributes

PosixFileAttributes接口方法 描述
GroupPrincipal group() Returns the group owner of the file.
UserPrincipal owner() Returns the owner of the file.
Set<PosixFilePermission> permissions() Returns the permissions of the file.

UserDefinedFileAttributeView

它可以让开发者为文件设置一些自定义属性。

程序 读取 修改文件属性

下面程序示范了如何读取、修改文件的属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributeView;
import java.nio.file.attribute.BasicFileAttributes;
import java.nio.file.attribute.DosFileAttributeView;
import java.nio.file.attribute.FileOwnerAttributeView;
import java.nio.file.attribute.UserDefinedFileAttributeView;
import java.nio.file.attribute.UserPrincipal;
import java.util.Date;
import java.util.List;

public class AttributeViewTest {
public static void main(String[] args) throws Exception {
// 获取将要操作的文件
Path testPath = Paths.get("AttributeViewTest.java");
// 获取访问基本属性的BasicFileAttributeView
BasicFileAttributeView basicView =
Files.getFileAttributeView(testPath, BasicFileAttributeView.class);
// 获取访问基本属性的BasicFileAttributes
BasicFileAttributes basicAttribs = basicView.readAttributes();
// 访问文件的基本属性
System.out.println("创建时间:" + new Date(basicAttribs.creationTime().toMillis()));
System.out.println("最后访问时间:" + new Date(basicAttribs.lastAccessTime().toMillis()));
System.out.println("最后修改时间:" + new Date(basicAttribs.lastModifiedTime().toMillis()));
System.out.println("文件大小:" + basicAttribs.size());

// 获取访问文件属主信息的FileOwnerAttributeView
FileOwnerAttributeView ownerView =
Files.getFileAttributeView(testPath, FileOwnerAttributeView.class);
// 获取该文件所属的用户
System.out.println(ownerView.getOwner());

// 获取系统中guest对应的用户
UserPrincipal user = FileSystems.getDefault().getUserPrincipalLookupService().lookupPrincipalByName("guest");
// 修改用户
ownerView.setOwner(user);

// 获取访问自定义属性的FileOwnerAttributeView
UserDefinedFileAttributeView userView = Files.getFileAttributeView(testPath,
UserDefinedFileAttributeView.class);
List<String> attrNames = userView.list();
// 遍历所有的自定义属性
for (String name : attrNames) {
ByteBuffer buf = ByteBuffer.allocate(userView.size(name));
userView.read(name, buf);
buf.flip();
String value = Charset.defaultCharset().decode(buf).toString();
System.out.println(name + "--->" + value);
}
// 添加一个自定义属性
userView.write("发行者", Charset.defaultCharset().encode("疯狂Java联盟"));

// 获取访问DOS属性的DosFileAttributeView
DosFileAttributeView dosView = Files.getFileAttributeView(testPath, DosFileAttributeView.class);
// 将文件设置隐藏、只读
dosView.setHidden(true);
dosView.setReadOnly(true);
}
}

上面程序中的分别访问了4种不同类型的文件属性,关于读取、修改文件属性的说明,程序中的代码已有详细说明,因此不再过多地解释。第二次运行该程序(记住第一次运行后AttributeViewTestJava文件变成隐藏、只读文件,因此第二次运行之前一定要先取消只读属性)

15.10.3 使用WatchService监控文件变化

在以前的Java版本中,如果程序需要监控文件的变化,则可以考虑启动一条后台线程,这条后线程每隔一段时间去“遍历”一次指定目录的文件,如果发现此次遍历结果与上次遍历结果不同,则认为文件发生了变化。但这种方式不仅十分烦琐,而且性能也不好。

Path类监听文件系统的方法

NIO.2Path类提供了如下一个方法来监听文件系统的变化。

Path类的register方法 描述
default WatchKey register(WatchService watcher, WatchEvent.Kind<?>... events) watcher监听该path代表的目录下的文件变化。events参数指定要监听哪些类型的事件。
WatchKey register(WatchService watcher, WatchEvent.Kind<?>[] events, WatchEvent.Modifier... modifiers) Registers the file located by this path with a watch service.

在这个方法中WatchService代表一个文件系统监听服务,它负责监听path代表的目录下的文件变化。

WatchService

一旦使用register方法完成注册之后,接下来就可调用WatchService的如下三个方法来获取被监听目录的文件变化事件

WatchService方法 描述
WatchKey poll() 获取下一个LatchKey,如果没有LatchKey发生就立即返回null
WatchKey poll(long timeout, TimeUnit unit) 尝试等待timeout时间去获取下一个LatchKey
WatchKey take() 获取下一个LatchKey,如果没有LatchKey发生就一直等待。
  • 如果程序需要一直监控,则应该选择使用take()方法;
  • 如果程序只需要监控指定时间,则可考虑使用poll()方法。

程序WatchService监控文件变化

下面程序示范了使用WatchService来监控D盘根路径下文件的变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.nio.file.FileSystems;
import java.nio.file.Paths;
import java.nio.file.StandardWatchEventKinds;
import java.nio.file.WatchEvent;
import java.nio.file.WatchKey;
import java.nio.file.WatchService;

public class WatchServiceTest
{
public static void main(String[] args)
throws Exception
{
// 获取文件系统的WatchService对象
WatchService watchService = FileSystems.getDefault()
.newWatchService();
// 为D:盘根路径注册监听
Paths.get("D:/").register(watchService
, StandardWatchEventKinds.ENTRY_CREATE
, StandardWatchEventKinds.ENTRY_MODIFY
, StandardWatchEventKinds.ENTRY_DELETE);
while(true)
{
// 获取下一个文件改动事件
WatchKey key = watchService.take(); //①
for (WatchEvent<?> event : key.pollEvents())
{
System.out.println(event.context() +" 文件发生了 "
+ event.kind()+ "事件!");
}
// 重设WatchKey
boolean valid = key.reset();
// 如果重设失败,退出监听
if (!valid)
{
break;
}
}
}
}

上面程序使用了一个死循环重复获取D盘根路径下文件的变化,程序在①号代码处试图获取下一个LatchKey,如果没有发生就等待。因此D盘根路径下每次文件的变化都会被该程序监听到
运行该程序,然后在D:盘下新建一个文件,再删除该文件,将看到如下的输出。

1
2
3
4
新建文本文档.txt 文件发生了 ENTRY_CREATE事件!
新建文本文档.txt 文件发生了 ENTRY_DELETE事件!
test.txt 文件发生了 ENTRY_CREATE事件!
test.txt 文件发生了 ENTRY_DELETE事件!

通过使用WatchService可以非常优雅地监控指定目录下文件的变化,至于文件发生变化后,程序应该进行哪些处理,这就取决于程序的业务需要了

15.10.2 使用FileVisitor遍历文件和目录

在以前的Java版本中,如果程序要遍历指定目录下的所有文件和子目录,则只能使用递归进行遍历,但这种方式不仅复杂,而且灵活性也不高。
有了Files工具类的帮助,现在可以用更优雅的方式来遍历文件和子目录。

walkFileTree方法

Files类提供了如下两个方法来遍历文件和子目录

Files类的walkFileTree方法 描述
static Path walkFileTree(Path start, FileVisitor<? super Path> visitor) 遍历start路径下的所有文件和子目录。
static Path walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor) 与上一个方法的功能类似。该方法最多遍历maxDepth深度的文件。

文件访问器FileVisitor

上面两个方法都需要FileVisitor参数,FileVisitor代表一个文件访问器,walkFileTree()方法会自动遍历start路径下的所有文件和子目录,遍历文件和子目录都会“触发”FileVisitor中相应的方法。

FileVisitor接口方法

FileVisitor接口中定义了如下4个方法。

FileVisitor方法 描述
FileVisitResult postVisitDirectory(T dir, IOException exc) 访问子目录之后触发该方法
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) 访问子目录之前触发该方法
FileVisitResult visitFile(T file, BasicFileAttributes attrs) 访问file文件时触发该方法
FileVisitResult visitFileFailed(T file, IOException exc) 访问file文件失败时触发该方法

FileVisitResult枚举

上面4个方法都返回一个FileVisitResult对象,FileVisitResult是一个枚举类,代表了访问之后的后续行为

FileVisitResult枚举值 描述
CONTINUE 代表“继续访问”的后续行为
SKIP_SIBLINGS 代表“继续访问”的后续行为,但不访问该文件或目录的兄弟文件或目录
SKIP_SUBTREE 代表“继续访问”的后续行为,但不访问该文件或目录的子目录树。
TERMINATE 代表“中止访问”的后续行为

SimpleFileVisitor实现类

实际编程时没必要为FileVisitor的4个方法都提供实现,可以通过继承FileVisitor的实现类SimpleFileVisitor来实现自己的“文件访问器”,这样就根据需要、选择性地重写指定方法了。

程序 使用FileVisitor遍历目录 查找指定文件

如下程序示范了使用FileVisitor来遍历文件和子目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.io.*;
import java.nio.file.*;
import java.nio.file.attribute.*;

public class FileVisitorTest {
public static void main(String[] args) throws Exception {
// 遍历 G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\15\ 目录下的所有文件和子目录
Files.walkFileTree(Paths.get("G:", "Desktop", "随书源码", "疯狂Java讲义(第4版)光盘", "codes", "15"),
new SimpleFileVisitor<Path>() {
// 访问文件时候触发该方法
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
throws IOException {
System.out.println("正在访问--文件:" + file);
// 找到了FileInputStreamTest.java文件
if (file.endsWith("FileInputStreamTest.java")) {
System.out.println("--已经找到目标文件--");
// 停止
return FileVisitResult.TERMINATE;
}
// 继续
return FileVisitResult.CONTINUE;
}

// 开始访问目录时触发该方法
@Override
public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs)
throws IOException {
System.out.println("正在访问 路径:" + dir);
// 继续
return FileVisitResult.CONTINUE;
}
});
}
}

上面程序中使用了Files工具类的walkFileTree()方法来遍历G:\Desktop\随书源码\疯狂Java讲义(第4版)光盘\codes\15\目录下的所有文件和子目录,如果找到的文件以“FileInputStreamTest.java”结尾,则程序停止遍历:这就实现了对指定目录进行搜索,直到找到指定文件为止

15.10 Java7的NIO.2

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

  • 提供了全面的文件IO和文件系统访问支持。
  • 基于异步ChannelIO

第一个改进表现为Java7新增的java.nio.file包及各个子包;
第二个改进表现为Java7java.nio.channels包下增加了多个以Asynchronous开头的Channel接口和类。Java7把这种改进称为NIO.2,本章先详细介绍NIO的第二个改进。

15.1.0.1 Path Paths和Files核心API

早期的Java只提供了一个File类来访问文件系统,但File类的功能比较有限,它不能利用特定文件系统的特性,File所提供的方法的性能也不高。而且,其大多数方法在出错时仅返回失败,并不会提供异常信息

Path接口

NIO.2为了弥补这种不足,引入了一个Path接口,Path接口代表一个平台无关的平台路径

Path接口方法

方法 描述
Path getRoot() Returns the root component of this path as a Path object, or null if this path does not have a root component.
Path toAbsolutePath() Returns a Path object representing the absolute path of this path.
Path getName(int index) Returns a name element of this path as a Path object.
int getNameCount() 返回Path路径所包含的路径名的数量,例如g:\publishl\codes调用该方法就会返回3。

工具类

除此之外,NIO.2还提供了FilesPaths两个工具类,其中

  • Files包含了大量静态的工具方法来操作文件;
  • Paths则包含了两个返回Path的静态工厂方法。

工具类命名规则

FilesPaths两个工具类非常符合Java一贯的命名风格,比如前面介绍的操作数组的工具类为Arrays,操作集合的工具类为Collections,这种一致的命名风格可以让读者快速了解这些工具类的用途。

Paths工具类方法

Paths接口方法 描述
static Path get(String first, String... more) Converts a path string, or a sequence of strings that when joined form a path string, to a Path.
static Path get(URI uri) Converts the given URI to a Path object.

第一个get方法会将给定的多个字符串连缀成路径,比如Paths.get("g:","publish","codes")就返回g:\publish\codes路径。

程序 Path接口Paths工具类示例

下面程序简单示范了Path接口的功能和用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.nio.file.Path;
import java.nio.file.Paths;

public class PathTest {
public static void main(String[] args) throws Exception {
// 以当前路径来创建Path对象
Path path = Paths.get(".");
System.out.println("path里包含的路径数量:" + path.getNameCount());
System.out.println("path的根路径:" + path.getRoot());
// 获取path对应的绝对路径。
Path absolutePath = path.toAbsolutePath();
System.out.println(absolutePath);
// 获取绝对路径的根路径
System.out.println("absolutePath的根路径:" + absolutePath.getRoot());
// 获取绝对路径所包含的路径数量
System.out.println("absolutePath里包含的路径数量:" + absolutePath.getNameCount());
System.out.println(absolutePath.getName(3));
// 以多个String来构建Path对象
Path path2 = Paths.get("g:", "publish", "codes");
System.out.println(path2);
}
}

Files工具类

Files类是一个高度封装的工具类,它提供了大量的工具方法来完成文件复制读取文件内容写入文件内容等功能,这些原本需要程序员通过IO操作才能完成的功能,现在Files类只要一个工具方法即可。读者应该熟练掌握Files工具类的用法,它所包含的工具方法可以大大地简化文件IO

流操作

Java8进一步增强了Files工具类的功能,允许开发者使用Stream API来操作文件目录和文件内容。

copy方法

方法 描述
static long copy(InputStream in, Path target, CopyOption... options) Copies all bytes from an input stream to a file.
static long copy(Path source, OutputStream out) Copies all bytes from a file to an output stream.
static Path copy(Path source, Path target, CopyOption... options) Copy a file to a target file.

判断方法

是否可读可写可执行

方法 描述
static boolean isReadable(Path path) Tests whether a file is readable.
static boolean isWritable(Path path) Tests whether a file is writable.
static boolean isExecutable(Path path) Tests whether a file is executable.

是否隐藏 是否符号链接

方法 描述
static boolean isHidden(Path path) Tells whether or not a file is considered hidden.
static boolean isSymbolicLink(Path path) Tests whether a file is a symbolic link.

是否文件 是否目录 是否同一个文件

方法 描述
static boolean isDirectory(Path path, LinkOption... options) Tests whether a file is a directory.
static boolean isRegularFile(Path path, LinkOption... options) Tests whether a file is a regular file with opaque content.
static boolean isSameFile(Path path, Path path2) Tests if two paths locate the same file.

是否存在

方法 描述
static boolean exists(Path path, LinkOption... options) Tests whether a file exists.
static boolean notExists(Path path, LinkOption... options) Tests whether the file located by this path does not exist.

read方法

得到字节数组

方法 描述
static byte[] readAllBytes(Path path) Reads all the bytes from a file.

得到字符串

方法 描述
static String readString(Path path) Reads all content from a file into a string, decoding from bytes to characters using the UTF-8 charset.
static String readString(Path path, Charset cs) Reads all characters from a file into a string, decoding from bytes to characters using the specified charset.

得到所有行

方法 描述
static List<String> readAllLines(Path path) Read all lines from a file.
static List<String> readAllLines(Path path, Charset cs) Read all lines from a file.

得到其他

方法 描述
static <A extends BasicFileAttributes> A readAttributes(Path path, Class<A> type, LinkOption... options) Reads a file’s attributes as a bulk operation.
static Map<String,​Object> readAttributes(Path path, String attributes, LinkOption... options) Reads a set of file attributes as a bulk operation.
static Path readSymbolicLink(Path link) Reads the target of a symbolic link (optional operation).

create方法

创建目录

方法 描述
static Path createDirectories(Path dir, FileAttribute<?>... attrs) Creates a directory by creating all nonexistent parent directories first.
static Path createDirectory(Path dir, FileAttribute<?>... attrs) Creates a new directory.

创建文件

方法 描述
static Path createFile(Path path, FileAttribute<?>... attrs) Creates a new and empty file, failing if the file already exists.

创建链接

方法 描述
static Path createLink(Path link, Path existing) Creates a new link (directory entry) for an existing file (optional operation).
static Path createSymbolicLink(Path link, Path target, FileAttribute<?>... attrs) Creates a symbolic link to a target (optional operation).

创建临时目录

方法 描述
static Path createTempDirectory(String prefix, FileAttribute<?>... attrs) Creates a new directory in the default temporary-file directory, using the given prefix to generate its name.
static Path createTempDirectory(Path dir, String prefix, FileAttribute<?>... attrs) Creates a new directory in the specified directory, using the given prefix to generate its name.

创建临时文件

方法 描述
static Path createTempFile(String prefix, String suffix, FileAttribute<?>... attrs) Creates an empty file in the default temporary-file directory, using the given prefix and suffix to generate its name.
static Path createTempFile(Path dir, String prefix, String suffix, FileAttribute<?>... attrs) Creates a new empty file in the specified directory, using the given prefix and suffix strings to generate its name.

delete方法

方法 描述
static void delete(Path path) Deletes a file.
static boolean deleteIfExists(Path path) Deletes a file if it exists.

set方法

方法 描述
static Path setAttribute(Path path, String attribute, Object value, LinkOption... options) Sets the value of a file attribute.
static Path setLastModifiedTime(Path path, FileTime time) Updates a file’s last modified time attribute.
static Path setOwner(Path path, UserPrincipal owner) Updates the file owner.
static Path setPosixFilePermissions(Path path, Set<PosixFilePermission> perms) Sets a file’s POSIX permissions.

get方法

方法 描述
static Object getAttribute(Path path, String attribute, LinkOption... options) Reads the value of a file attribute.
static FileTime getLastModifiedTime(Path path, LinkOption... options) Returns a file’s last modified time.
static UserPrincipal getOwner(Path path, LinkOption... options) Returns the owner of a file.
static Set<PosixFilePermission> getPosixFilePermissions(Path path, LinkOption... options) Returns a file’s POSIX file permissions.
static <V extends FileAttributeView> V getFileAttributeView(Path path, Class<V> type, LinkOption... options) Returns a file attribute view of a given type.
static FileStore getFileStore(Path path) Returns the FileStore representing the file store where a file is located.

new方法

newBufferedReader

方法 描述
static BufferedReader newBufferedReader(Path path) Opens a file for reading, returning a BufferedReader to read text from the file in an efficient manner.
static BufferedReader newBufferedReader(Path path, Charset cs) Opens a file for reading, returning a BufferedReader that may be used to read text from the file in an efficient manner.

newBufferedWriter

方法 描述
static BufferedWriter newBufferedWriter(Path path, Charset cs, OpenOption... options) Opens or creates a file for writing, returning a BufferedWriter that may be used to write text to the file in an efficient manner.
static BufferedWriter newBufferedWriter(Path path, OpenOption... options) Opens or creates a file for writing, returning a BufferedWriter to write text to the file in an efficient manner.

newByteChannel

方法 描述
static SeekableByteChannel newByteChannel(Path path, OpenOption... options) Opens or creates a file, returning a seekable byte channel to access the file.
static SeekableByteChannel newByteChannel(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs) Opens or creates a file, returning a seekable byte channel to access the file.

newDirectoryStream

方法 描述
static DirectoryStream<Path> newDirectoryStream(Path dir) Opens a directory, returning a DirectoryStream to iterate over all entries in the directory.
static DirectoryStream<Path> newDirectoryStream(Path dir, String glob) Opens a directory, returning a DirectoryStream to iterate over the entries in the directory.
static DirectoryStream<Path> newDirectoryStream(Path dir, DirectoryStream.Filter<? super Path> filter) Opens a directory, returning a DirectoryStream to iterate over the entries in the directory.

newInputStream

方法 描述
static InputStream newInputStream(Path path, OpenOption... options) Opens a file, returning an input stream to read from the file.
static OutputStream newOutputStream(Path path, OpenOption... options) Opens or creates a file, returning an output stream that may be used to write bytes to the file.

write方法

write

方法 描述
static Path write(Path path, byte[] bytes, OpenOption... options) Writes bytes to a file.
static Path write(Path path, Iterable<? extends CharSequence> lines, Charset cs, OpenOption... options) Write lines of text to a file.
static Path write(Path path, Iterable<? extends CharSequence> lines, OpenOption... options) Write lines of text to a file.

writeString

方法 描述
static Path writeString(Path path, CharSequence csq, Charset cs, OpenOption... options) Write a CharSequence to a file.
static Path writeString(Path path, CharSequence csq, OpenOption... options) Write a CharSequence to a file.

walk方法

walk

方法 描述
static Stream<Path> walk(Path start, int maxDepth, FileVisitOption... options) Return a Stream that is lazily populated with Path by walking the file tree rooted at a given starting file.
static Stream<Path> walk(Path start, FileVisitOption... options) Return a Stream that is lazily populated with Path by walking the file tree rooted at a given starting file.

walkFileTree

方法 描述
static Path walkFileTree(Path start, FileVisitor<? super Path> visitor) Walks a file tree.
static Path walkFileTree(Path start, Set<FileVisitOption> options, int maxDepth, FileVisitor<? super Path> visitor) Walks a file tree.

返回流的方法

walk

方法 描述
static Stream<Path> walk(Path start, int maxDepth, FileVisitOption... options) Return a Stream that is lazily populated with Path by walking the file tree rooted at a given starting file.
static Stream<Path> walk(Path start, FileVisitOption... options) Return a Stream that is lazily populated with Path by walking the file tree rooted at a given starting file.

find

方法 描述
static Stream<Path> find(Path start, int maxDepth, BiPredicate<Path,​BasicFileAttributes> matcher, FileVisitOption... options) Return a Stream that is lazily populated with Path by searching for files in a file tree rooted at a given starting file.

lines

方法 描述
static Stream<String> lines(Path path) Read all lines from a file as a Stream.
static Stream<String> lines(Path path, Charset cs) Read all lines from a file as a Stream.

list方法

方法 描述
static Stream<Path> list(Path dir) Return a lazily populated Stream, the elements of which are the entries in the directory.

其他方法

方法 描述
static Path move(Path source, Path target, CopyOption... options) Move or rename a file to a target file.
static String probeContentType(Path path) Probes the content type of a file.
static long size(Path path) Returns the size of a file (in bytes).

程序 Files工具类示例

Files是一个操作文件的工具类,它提供了大量便捷的工具方法,下面程序简单示范了Files类的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.nio.file.*;
import java.nio.charset.*;
import java.io.*;
import java.util.*;

public class FilesTest {
public static void main(String[] args) throws Exception {
// 复制文件
Files.copy(Paths.get("FilesTest.java"), new FileOutputStream("a.txt"));
// 判断FilesTest.java文件是否为隐藏文件
System.out.println("FilesTest.java是否为隐藏文件:"
+ Files.isHidden(Paths.get("FilesTest.java")));
// 一次性读取FilesTest.java文件的所有行
List<String> lines = Files.readAllLines(Paths.get("FilesTest.java"), Charset.forName("gbk"));
System.out.println(lines);
// 判断指定文件的大小
System.out.println("FilesTest.java的大小为:" + Files.size(Paths.get("FilesTest.java")));
List<String> poem = new ArrayList<>();
poem.add("水晶潭底银鱼跃");
poem.add("清徐风中碧竿横");
// 直接将多个字符串内容写入指定文件中
Files.write(Paths.get("pome.txt"), poem, Charset.forName("gbk"));
// 使用Java 8新增的Stream API列出当前目录下所有文件和子目录
Files.list(Paths.get(".")).forEach(path -> System.out.println(path));
// 使用Java 8新增的Stream API读取文件内容
Files.lines(Paths.get("FilesTest.java"), Charset.forName("gbk"))
.forEach(line -> System.out.println(line));
FileStore cStore = Files.getFileStore(Paths.get("C:"));
// 判断C盘的总空间,可用空间
System.out.println("C:共有空间:" + cStore.getTotalSpace());
System.out.println("C:可用空间:" + cStore.getUsableSpace());
}
}

上面程序简单示范了Files工具类的用法。

上面示例程序中①号代码使用Stream API列出了指定路径下的所有文件和目录;②号代码则使用了Stream API读取文件内容。

15.9.5 文件锁

文件锁在操作系统中是很平常的事情,如果多个运行的程序需要并发修改同一个文件时,程序之间需要某种机制来进行通信,使用文件锁可以有效地阻止多个进程并发修改同一个文件,所以现在的大部分操作系统都提供了文件锁的功能。
文件锁控制文件的全部或部分字节的访问,但文件锁在不同的操作系统中差别较大,所以早期的JDK版本并未提供文件锁的支持。JDK1.4NIO开始,Java开始提供文件锁的支持

FileLock

NIO中,Java提供了FileLock来支持文件锁定功能。

如何获取FileLock

无参的lock和tryLock方法

FileChannel中提供的lock()tryLock()两个方法可以获得文件锁FileLock对象,从而锁定文件。

FileChannel获取FileLock的方法 描述
FileLock lock() 获取FileChannel对应的排它锁
FileLock tryLock() 获取FileChannel对应的排它锁

lock方法和tryLock方法的区别

lock()tryLock()方法存在区别:

  • lock()试图锁定某个文件时,如果无法得到文件锁,程序将一直阻塞;
  • tryLock()是尝试锁定文件,它将直接返回而不是阻塞,如果获得了文件锁,该方法则返回该文件锁,否则将返回null

部分锁定

如果FileChannel只想锁定文件的部分内容,而不是锁定全部内容,则可以使用如下的lock()tryLock()方法。

方法 描述
abstract FileLock lock(long position, long size, boolean shared) Acquires a lock on the given region of this channel’s file.
abstract FileLock tryLock(long position, long size, boolean shared) Attempts to acquire a lock on the given region of this channel’s file.

共享锁

带参数的locktryLock方法的参数sharedtrue时,表明该锁是一个共享锁,它将**允许多个进程来读取该文件,但阻止其他进程获得对该文件的排他锁**。(共享锁:别人可读不可写)

排他锁

带参数的locktryLock方法的参数sharedfalse时,表明该锁是一个排他锁,它将锁住对该文件的读写。(排它锁:别人不可读,也不可写)

直接使用lock()tryLock()方法获取的文件锁是排他锁

判断是否是共享锁

程序可以通过调用FileLockisShared()方法来判断它获得的锁是否为共享锁。

FileLockisShared方法 描述
boolean isShared() Tells whether this lock is shared.

释放文件锁

处理完文件后通过FileLockrelease()方法释放文件锁。

FileLockrelease方法 描述
abstract void release() Releases this lock.

程序 使用FileLock

下面程序示范了使用FileLock锁定文件的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.io.*;
import java.nio.channels.*;

public class FileLockTest {
public static void main(String[] args) throws Exception {

try (
// 使用FileOutputStream获取FileChannel
FileChannel channel = new FileOutputStream("a.txt").getChannel()) {
// 使用非阻塞式方式对指定文件加锁
FileLock lock = channel.tryLock();
// 程序暂停10s
Thread.sleep(10000);
// 释放锁
lock.release();
}
}
}

上面程序中的代码:

1
FileLock lock = channel.tryLock();

用于对指定文件加锁,接着程序调用Thread.sleep(10000)暂停了10秒后才释放文件锁,因此在这10秒之内,其他程序无法对a.txt文件进行修改
文件锁虽然可以用于控制并发访问,但对于高并发访问的情形,还是推荐使用数据库来保存程序信息,而不是使用文件

文件锁需要注点的地方

关于文件锁还需要指出如下几点。

  • 在某些平台上,文件锁仅仅是建议性的,并不是强制性的。这意味着即使一个程序不能获得文件锁,它也可以对该文件进行读写
  • 在某些平台上,不能同步地锁定一个文件并把它映射到内存中
  • 文件锁是由Java虚拟机所持有的,如果两个Java程序使用同一个Java虚拟机运行,则它们不能对同一个文件进行加锁。
  • 在某些平台上关闭FileChannel时,会释放Java虚拟机在该文件上的所有锁,因此应该避免对同一个被锁定的文件打开多个FileChannel

15.9.4 字符集和Charset

前面已经提到:计算机里的文件、数据、图片文件只是一种表面现象,所有文件在底层都是二进制文件,即全部都是字节码。
图片、音乐文件暂时先不说,对于文本文件而言,之所以可以看到一个个的字符,是因为系统将底层的二进制序列转换成字符的缘故
在这个过程中涉及两个概念:编码Encode)和解码(Decode).

编码解码

编码

明文的字符序列转换成计算机理解的二进制序列称为编码

解码

二进制序列转换成普通人能看懂的明文字符串序列称为解码

编码解码示意图

如图15.21所示
这里有一张图片

Encode和Decode术语起源

EncodeDecode两个专业术语来自于早期的电报、情报等。

  • 把明文的消息转换成普通人看不懂的电码(或密码)的过程就是Encode,
  • 将电码(或密码)翻译成明文的消息则被称为Decode

后来计算机也采用了这两个概念,其作用已经发生了变化

计算机底层是没有文本文件、图片文件之分的,它只是忠实地记录每个文件的二进制序列而已。

  • 当需要保存文本文件时,程序必须先把文件中的每个字符翻译成二进制序列;
  • 当需要读取文本文件时,程序必须把二进制序列转换为一个个的字符。

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

Charset类方法

JDK1.4提供了Charset来处理字节序列字符序列(字符串)之间的转换关系,该类包含了用于创建解码器和编码器的方法,还提供了获取Charset所支持字符集的方法,Charset类是不可变的

获取JDK支持的全部字符集

Charset类提供了一个availableCharsets()静态方法来获取当前JDK所支持的所有字符集。

方法 描述
static SortedMap<String,​Charset> availableCharsets() Constructs a sorted map from canonical charset names to charset objects.

获取本地系统的文件编码格式

可以使用System类的getProperty()方法来访问本地系统的文件编码格式,文件编码格式的属性名为file.encoding

程序 获取JDK支持的全部字符集

所以程序可以使用如下程序来获取该JDK所支持的全部字符集。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.nio.charset.*;
import java.util.*;

public class CharsetTest {
public static void main(String[] args) {
// 获取Java支持的全部字符集
SortedMap<String, Charset> map = Charset.availableCharsets();
for (String alias : map.keySet()) {
// 输出字符集的别名和对应的Charset对象
System.out.println(alias + "----->" + map.get(alias));
}
// 输出本地系统的文件编码格式
System.out.println(System.getProperty("file.encoding"));
}
}

上面程序中的获取了当前Java所支持的全部字符集,并使用遍历方式打印了所有字符集的别名(字符集的字符串名称)和Charset对象。
从上面程序可以看出,每个字符集都有一个字符串名称,也被称为字符串别名

中国常用字符集的字符串别名

对于中国的程序员而言,下面几个字符串别名是常用的。

字符集 描述
GBK 简体中文字符集。
BIG5 繁体中文字符集。
ISO-8859-1 ISO拉丁字母表No.1,也叫做ISO-LATIN-1
UTF-8 8UCS转换格式。
UTF-16BE 16UCS转换格式,Big-endian(最低地址存放高位字节)字节顺序。
UTF-16LE 16UCS转换格式,Little-endian(最高地址存放低位字节)字节顺序。
UTF-16 16UCS转换格式,字节顺序由可选的字节顺序标记来标识。

创建Charset对象 forName方法

一旦知道了字符集的别名之后,程序就可以调用CharsetforName()方法:

方法 描述
static Charset forName(String charsetName) Returns a charset object for the named charset.

来创建对应的Charset对象,forName方法的参数就是相应字符集的别名。
例如如下代码

1
2
Charset gbk = Charset.forName("GBK");
Charset iso88591 = Charset.forName("ISO-8859-1");

StandardCharsets类获取Charset对象

Java7新增了一个StandardCharsets类,该类里包含了ISO_8859_1UTF_8UTF_16等类变量,这些类变量代表了最常用的字符集对应的Charset对象:

StandardCharsets的类变量 描述
static Charset ISO_8859_1 ISO Latin Alphabet No. 1, a.k.a.
static Charset US_ASCII Seven-bit ASCII, a.k.a.
static Charset UTF_16 Sixteen-bit UCS Transformation Format,
byte order identified by an optional byte-order mark
static Charset UTF_16BE Sixteen-bit UCS Transformation Format, big-endian byte order
static Charset UTF_16LE Sixteen-bit UCS Transformation Format, little-endian byte order
static Charset UTF_8 Eight-bit UCS Transformation Format

获取解码器 编码器

获得了Charset对象之后,就可以通过该对象的newDecoder()newEncoder()这两个方法分别返回该Charset的解码器CharsetDecoder和编码器CharsetEncoder对象。

方法 描述
abstract CharsetDecoder newDecoder() Constructs a new decoder for this charset.
abstract CharsetEncoder newEncoder() Constructs a new encoder for this charset.

字符序列 转 字节序列(编码)

调用编码器CharsetEncoderencode()方法就可以将CharBufferString(字符序列)转换成ByteBuffer(字节序列)。

字节序列 转 字符序列(解码)

调用解码器CharsetDecoderdecode()方法就可以将ByteBuffer(字节序列)转换成CharBuffer(字符序列),

程序 ByteBuffer转CharBuffer CharBuffer转ByteBuffer

如下程序使用了CharsetEncoderCharsetDecoder完成了ByteBufferCharBuffer之间的转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.nio.*;
import java.nio.charset.*;

public class CharsetTransform {
public static void main(String[] args) throws Exception {
// 创建简体中文对应的Charset
Charset cn = Charset.forName("GBK");
// 获取cn对象对应的编码器和解码器
CharsetEncoder cnEncoder = cn.newEncoder();
CharsetDecoder cnDecoder = cn.newDecoder();
// 创建一个CharBuffer对象
CharBuffer cbuff = CharBuffer.allocate(8);
// 写入Buffer
cbuff.put('孙');
cbuff.put('悟');
cbuff.put('空');
// 准备读取Buffer
cbuff.flip();
// 解码 将CharBuffer中的字符序列转换成字节序列
ByteBuffer bbuff = cnEncoder.encode(cbuff);
// 循环访问ByteBuffer中的每个字节
for (int i = 0; i < bbuff.capacity(); i++) {
System.out.print(bbuff.get(i) + " ");
}
// 编码 将ByteBuffer的数据解码成字符序列
System.out.println("\n" + cnDecoder.decode(bbuff));
}
}

上面程序中的分别实现了将CharBuffer转换成ByteBuffer,将ByteBuffer转换成CharBuffer的功能。

Charset中编码和解码的快捷方法

实际上,Charset类也提供了如下三个方法。

方法 描述
CharBuffer decode(ByteBuffer bb) ByteBuffer中的字节序列转换成字符序列的便捷方法。
ByteBuffer encode(String str) CharBuffer中的字符序列转换成字节序列的便捷方法。
ByteBuffer encode(CharBuffer cb) String中的字符序列转换成字节序列的便捷方法。

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

String类 字符串 转 字节序列(编码)

String类里也提供了一个getBytes(String charset)方法,该方法返回**byte数组**,该方法也是使用指定的字符集将字符串转换成字节序列

15.9.3 使用Channel

Channel与流对象的区别

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

  1. Channel可以直接将指定文件的部分或全部直接映射成Buffer
  2. 程序不能直接访问Channel中的数据,包括读取、写入都不行,Channel只能与Buffer进行交互。也就是说:
    • 如果要从Channel中取得数据,必须先用BufferChannel中取出一些数据,然后让程序从Buffer中取出这些数据;
    • 如果要将程序中的数据写入Channel,一样先让程序将数据放入Buffer中,程序再将Buffer里的数据写入Channe

Channel分类

JavaChannel接口提供了DatagramChannelFileChannelPipe.SinkChannelPipe.SourceChannelSelectableChannelServerSocketChannelSocketChannel等实现类,本节主要介绍FileChannel的用法。
根据这些Channel的名字不难发现,新IO里的Channel是按功能来划分的,例如

  • Pipe.SinkChannelPipe.SourceChannel是用于支持线程之间通信的管道Channel;
  • ServerSocketChannelSocketChannel是用于支持**TCP网络通信**的Channel;
  • DatagramChannel则是用于支持**UDP网络通信**的Channel

本书将会在第17章介绍网络通信编程的详细内容,如果需要掌握ServerSocketChannel,SocketChannelChannel的用法,可以参考本书第17章。

如何创建Channel

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

  • FileInputStreamFileOutputStreamgetChannel()方法返回的是FileChannel,
  • PipedInputStreamPipedOutputStreamgetChannel()方法返回的是Pipe.SinkChannelPipe.SourceChannel

Channel常用方法

Channel中最常用的三类方法是map()read()write(),其中

  • map()方法用于将Channel对应的部分或全部数据映射成ByteBuffer;
  • readwrite方法都有一系列重载形式,这些方法用于从Buffer中读取数据或向Buffer中写入数据。

FileChannel的map方法

map()方法的方法签名为:

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

程序 FileChannel示例

下面程序示范了直接将FileChannel的全部数据映射成ByteBuffer的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

public class FileChannelTest {
public static void main(String[] args) {
File f = new File("FileChannelTest.java");
try (
// 创建FileInputStream,以该文件输入流创建FileChannel
FileChannel inChannel = new FileInputStream(f).getChannel();
// 以文件输出流创建FileBuffer,用以控制输出
FileChannel outChannel = new FileOutputStream("a.txt").getChannel()) {
// 将FileChannel里的全部数据映射成ByteBuffer
MappedByteBuffer buffer =
inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length()); // ①
// 使用GBK的字符集来创建解码器
// Charset charset = Charset.forName("GBK");
Charset charset = Charset.forName("utf-8");
// 直接将buffer里的数据全部输出到outChannel中
outChannel.write(buffer); // ②

// 再次调用buffer的clear()方法,复原limit、position的位置,准备再次读取Buffer
buffer.clear();
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 使用解码器将ByteBuffer转换成CharBuffer
CharBuffer charBuffer = decoder.decode(buffer);
// CharBuffer的toString方法可以获取对应的字符串
System.out.println(charBuffer);
} catch (IOException ex) {
ex.printStackTrace();
}
}
}

上面程序中分别使用FileInputStreamFileOutputStream来获取FileChannel,虽然FileChannel既可以读取也可以写入,

  • 但**FileInputStream获取的FileChannel只能读**,
  • 而**FileOutputStream获取的FileChannel只能写**。

程序中①号代码:

1
2
MappedByteBuffer buffer = 
inChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length()); // ①

直接将指定Channel中的全部数据映射成ByteBuffer.
然后程序中②号代码:

1
outChannel.write(buffer); // ②

直接将整个ByteBuffer的全部数据写入一个输出FileChannel中,这就完成了文件的复制.
程序后面部分为了能将FileChannelTest.java文件里的内容打印出来,使用了Charset类和CharsetDecoder类将ByteBuffer转换成CharBuffer关于CharsetCharsetDecoder下一节将会有更详细的介绍。

程序 RandomAccessFile获取的Channel

不仅InputStreamOutputStream包含了getChannel()方法,在RandomAccessFile中也包含了一个getChannel()方法,RandomAccessFile返回的FileChannel是只读的还是读写的,则取决于RandomAccessFile打开文件的模式
例如,下面程序将会对a.txt文件的内容进行复制,追加在该文件后面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.*;
import java.nio.*;
import java.nio.channels.*;

public class RandomFileChannelTest {
public static void main(String[] args) throws IOException {
File f = new File("a.txt");
try (
// 创建一个RandomAccessFile对象
RandomAccessFile raf = new RandomAccessFile(f, "rw");
// 获取RandomAccessFile对应的Channel
FileChannel randomChannel = raf.getChannel()) {
// 将Channel中所有数据映射成ByteBuffer
ByteBuffer buffer = randomChannel.map(FileChannel.MapMode.READ_ONLY, 0, f.length());
// 把Channel的记录指针移动到最后
// 代码1
randomChannel.position(f.length());
// 将buffer中所有数据输出
randomChannel.write(buffer);
}
}
}

上面程序中的代码1:

1
randomChannel.position(f.length());

可以将Channel的记录指针移动到该Channel的最后,从而可以让程序将指定ByteBuffer的数据追加到该Channel的后面。每次运行上面程序,都会把a.txt文件的内容复制份,并将全部内容追加到该文件的后面。

像传统IO那样使用Channel和Buffer读写文件

如果读者习惯了传统IO的“用竹筒多次重复取水”的过程,或者担心Channel对应的文件过大,使用map()方法一次将所有的文件内容映射到内存中引起性能下降,也可以使用ChannelBuffer传统的“用竹筒多次重复取水”的方式。
如下程序所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

public class ReadFile {
public static void main(String[] args) throws IOException {
try (
// 创建文件输入流
FileInputStream fis = new FileInputStream("ReadFile.java");
// 创建一个FileChannel
FileChannel fcin = fis.getChannel()) {
// 定义一个ByteBuffer对象,用于重复取水
ByteBuffer bbuff = ByteBuffer.allocate(256);
// 将FileChannel中数据读取到ByteBuffer中
while (fcin.read(bbuff) != -1) {
// 准备读取Buffer中的数据
bbuff.flip();

// 创建Charset对象
// Charset charset = Charset.forName("GBK");
Charset charset = Charset.forName("utf-8");
// 创建解码器(CharsetDecoder)对象
CharsetDecoder decoder = charset.newDecoder();
// 将ByteBuffer的内容转码
CharBuffer cbuff = decoder.decode(bbuff);
System.out.print(cbuff);

// 将Buffer初始化,为下一次读取数据做准备
bbuff.clear();
}
}
}
}

上面代码虽然使用FileChannelBuffer来读取文件,但处理方式和使用InputStreambyte[]来读取文件的方式几乎一样,都是采用“用竹筒多次重复取水”的方式。但因为Buffer提供了fip()clear()两个方法,所以程序处理起来比较方便

类似传统方式使用NIO步骤

  • 读取数据到Buffer
  • 每次读取数据后调用fip()方法将没有数据的区域“封印”起来,避免程序从Buffer中取出null值;
  • Buffer中取出数据
  • 数据取出后立即调用clear()方法将Bufferposition设0,为下次读取数据做准备。