摘要:Java之所以得到很多程序员的亲睐,除了她的严谨的面向对象特性外,还有一个不容轻视的因素,那就是她强大的类库。一门语言如果没有库,功能将会大打折扣,在JDK5.0版本中,其核心库也有了诸多的改进,本文将就其新特性进行简介。
1. 访问环境变量和调用子进程
1.1 访问环境变量
虽然Java从一开始推出的时候,就一再强调她的跨平台特性,“一次编译,到处运行”。所以能够访问平台专有信息的System.getenv()方法从一开始进入java的lang包时,就遭到了大多数人的反对。虽然1.0版本中抛弃了其中的一些内容,但是在tiger版本中我们又可以使用这个方法了,请注意该方法的名称全部是小写。
使用的方法很简单:如清单1
public class EnvTest {
public static void main(String args[]) {
System.out.println(System.getenv(args[0]));
}
}
只要给定变量的名称,就可以得到它的当前值。
Tiger提供了两个版本的
getenv(),第二个版本返回系统当前设置中所有的环境变量对应的“名值”对。清单2说明该方法的使用:
import java.util.Map;
public class EnvDump {
public static void main(String args[]) {
for (Map.Entry entry: System.getenv().entrySet()) {
System.out.println(entry.getKey() + “/” + entry.getValue());
}
}
}
1.2 访问子进程
J2SE平台的前期版本提供了Runtime类的exec()方法用来创建子进程的运行,在Tiger版本中,这个方法依然有效,但是为了更方便的定制子进程,Tiger提供了ProcessBuilder类,它使依据改变了的进程变量来创建子进程更加便利。ProcessBuilder提供了directory(File)方法来改变进程的工作目录,用enviroment()方法在进程空间中添加和删除环境变量。清单3说明了Processor的简单用法,它使用
ipconfig
命令获得 Internet 配置信息。该方法适用于多数平台,否则可以将
ipconfig
改写成所用平台上的工作命令。启动进程构造程序之后,需要获得其
InputStream
,以读入所创建进程的结果。
import java.io.*;
public class ProcessTest {
public static void main(String args[]) throws IOException {
Process p = new ProcessBuilder("ipconfig").start();
InputStream is = p.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
}
运行结果如清单
4:
Windows 2000 IP Configuration
Ethernet adapter
本地连接
:
Connection-specific DNS Suffix . :
IP Address. . . . . . . . . . . . : 10.97.69.166
Subnet Mask . . . . . . . . . . . : 255.255.255.128
Default Gateway . . . . . . . . . : 10.97.69.129
ProcessBuilder
类不仅能生成新的进程,而且还能获得其结果。在调用其
start()
方法之前,还可以调整进程所执行的上下文。如果不喜欢环境变量,可以使用
environment
获得当前设置,并调用
clear()
清除映射。如果需要添加环境变量,可以调用
environment
获得当前设置,然后通过
put(name, value)
添加新的变量。如果希望使用新的工作目录,可以调用
directory()
并提供新的工作目录作为
File
对象。就是这么简单。
使用表示将运行的命令及其参数的数目可变的字符串参数来创建
ProcessBuilder
,一旦使用新的环境变量和工作目录配置
ProcessBuilder
,就可以调用
start()
来执行命令。
2 并发集合
Java中的集合框架一直是令人津津乐道的,在Tiger中,集合框架新添加了Queue接口以及这个接口的并发和非并发实现,以及并发Map的实现和专用于读操作大大超过写操作的情况下的并发List和Set实现。
2.1 Queue接口
虽然在
List的两端可以添加删除元素达到模拟queue的性能,但是Queue的提出提供了支持添加、删除和检查集合的更为方便的方法:如清单
5所示
public boolean offer(Object element)
public Object remove()
public Object poll()
public Object element()
public Object peek()
一些队列有大小长度的限制,因此如果想在一个已满的队列中加入一个新项,多出的项就会被拒绝。这时新的
offer
方法就可以起作用了。它不是对调用
add()
方法抛出一个
unchecked 异常,而只是得到由
offer()
返回的
false。
remove()
和
poll()
方法都是从队列中删除第一个元素(
head)。
remove()
的行为与
Collection
接口的版本相似,但是新的
poll()
方法在用空集合调用时不是抛出异常,只是返回
null。因此新的方法更适合容易出现异常条件的情况。后两个方法
element()
和
peek()
用于在队列的头部查询元素。与
remove()
方法类似,在队列为空时,
element()
抛出一个异常,而
peek()
返回
null。
2.1.1 基本队列实现
1、 在
Tiger中,java.util.LinkedList已经被改造为实现了java.util.List和java.util.Queue两个接口。清单6显示的是LinkedList的简单使用:
Queue queue = new LinkedList();
queue.offer("One");
queue.offer("Two");
queue.offer("Three");
queue.offer("Four");
System.out.println("Head of queue is: " + queue.poll());
输出的结果应该是One
2、 util包新增加的AbstractQueue类,其工作方式类似于AbstractList和AbstractSet类,在需要创建自己所需的对垒时,可以直接继承该类,必须实现offer(),poll(),peek()三个方法,读者可以在其中提供优化的实现。读者也可以不必创建自己的子类,而是使用几个tiger提供的实现,其中两个是不阻塞队列:PriorityQueue和ConcurrentLinkedQueue。
PriorityQueue类实际上是维护了一个有序的队列,加入到该Queue中的元素按照它们的自然顺序来进行排序,排序的依据是元素对java.util.Comparable的实现或者传递给该Queue构造函数的Comparator参数。如果将清单6的具体实现改为PriorityQueue,则输入的结果应该为Four,因为按字母顺序排列,Four是排在了第一个。ConcurrentLinkedQueue 是基于链接节点的、线程安全的队列。并发访问不需要同步。因为它在队列的尾部添加元素并从头部删除它们,所以只要不需要知道队列的大小,ConcurrentLinkedQueue 对公共集合的共享访问就可以工作得很好。收集关于队列大小的信息会很慢,需要遍历队列。
3、
2.1.2 阻塞队列实现
Tiger提供的 java.util.concurrent 包在 集合框架 中加入了 BlockingQueue 接口和五个阻塞队列类。简单的讲,
阻塞队列的意思就是当队列无空间时,添加元素的线程执行操作阻塞,直到有空间;或者是,当队列为空无元素可删时,执行删除的线程阻塞,知道有元素可删。BlockingQueue 接口的 Javadoc 给出了阻塞队列的基本用法,如清单 7 所示。生产者中的 put() 操作会在没有空间可用时阻塞,而消费者的 take() 操作会在队列中没有任何东西时阻塞。
class Producer implements Runnable {
private final BlockingQueue queue;
Producer(BlockingQueue q) { queue = q; }
public void run() {
try {
while(true) { queue.put(produce()); }
} catch (InterruptedException ex) { ... handle ...}
}
Object produce() { ... }
}
class Consumer implements Runnable {
private final BlockingQueue queue;
Consumer(BlockingQueue q) { queue = q; }
public void run() {
try {
while(true) { consume(queue.take()); }
} catch (InterruptedException ex) { ... handle ...}
}
void consume(Object x) { ... }
}
class Setup {
void main() {
BlockingQueue q = new SomeQueueImplementation();
Producer p = new Producer(q);
Consumer c1 = new Consumer(q);
Consumer c2 = new Consumer(q);
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
}
另外五个阻塞队列提供的情况各有不同:
- ArrayBlockingQueue:一个由数组支持的有界队列。
- LinkedBlockingQueue:一个由链接节点支持的可选有界队列。
- PriorityBlockingQueue:一个由优先级堆支持的无界优先级队列。
- DelayQueue:一个由优先级堆支持的、基于时间的调度队列。
- SynchronousQueue:一个利用 BlockingQueue 接口的简单聚集(rendezvous)机制
前两个类 ArrayBlockingQueue 和 LinkedBlockingQueue 几乎相同,只是在底层存储实现方面有所不同,LinkedBlockingQueue 并不总是有容量界限。无大小界限的 LinkedBlockingQueue 类在添加元素时永远不会有阻塞队列的等待(至少在其中有 Integer.MAX_VALUE 元素之前不会,这个时候采用的容量限制即是Integer.MAX_VALUE)。
PriorityBlockingQueue 是具有无界限容量的队列,它利用所包含元素的 Comparable 排序顺序来以逻辑顺序维护元素。可以将它看作 TreeSet 的可能替代物。例如,在队列中加入字符串 One、Two、Three 和 Four 会导致 Four 被第一个取出来。对于没有天然顺序的元素,可以为构造函数提供一个 Comparator 。不过对 PriorityBlockingQueue使用时需要注意,从 iterator() 返回的 Iterator 实例并不一定按照优先级顺序返回元素。如果必须以优先级顺序遍历所有元素,那么让它们都通过 toArray() 方法并自己对它们排序,像 Arrays.sort(pq.toArray())。
新的 DelayQueue 实现可能是其中最有意思(也是最复杂)的一个。加入到队列中的元素必须实现新的 Delayed 接口(只有一个方法 —— long getDelay(java.util.concurrent.TimeUnit unit))。因为队列的大小没有界限,使得添加可以立即返回,但是在延迟时间过去之前,不能从队列中取出元素。如果多个元素完成了延迟,那么最早失效/失效时间最长的元素将第一个取出。实际上没有听上去这样复杂。清单8演示了这种新的阻塞队列集合的使用:
import java.util.*;
import java.util.concurrent.*;
public class Delay {
/**
* Delayed implementation that actually delays
*/
static class NanoDelay implements Delayed {
long trigger;
NanoDelay(long i) {
trigger = System.nanoTime() + i;
}
public int compareTo(Object y) {
long i = trigger;
long j = ((NanoDelay)y).trigger;
if (i < j) return -1;
if (i > j) return 1;
return 0;
}
public boolean equals(Object other) {
return ((NanoDelay)other).trigger == trigger;
}
public boolean equals(NanoDelay other) {
return other.trigger == trigger;
}
public long getDelay(TimeUnit unit) {
long n = trigger - System.nanoTime();
return unit.convert(n, TimeUnit.NANOSECONDS);
}
public long getTriggerTime() {
return trigger;
}
public String toString() {
return String.valueOf(trigger);
}
}
public static void main(String args[]) throws InterruptedException {
Random random = new Random();
DelayQueue queue = new DelayQueue();
for (int i=0; i < 5; i++) {
queue.add(new NanoDelay(random.nextInt(1000)));
}
long last = 0;
for (int i=0; i < 5; i++) {
NanoDelay delay = (NanoDelay)(queue.take());
long tt = delay.getTriggerTime();
System.out.println("Trigger time: " + tt);
if (i != 0) {
System.out.println("Delta: " + (tt - last));
}
last = tt;
}
}
}
这个例子首先是一个内部类 NanoDelay,它实质上将暂停给定的任意纳秒(nanosecond)数,这里利用了 System 的新 nanoTime() 方法。然后 main() 方法只是将 NanoDelay 对象放到队列中并再次将它们取出来。如果希望队列项做一些其他事情,就需要在 Delayed 对象的实现中加入方法,并在从队列中取出后调用这个新方法。(请随意扩展 NanoDelay 以试验加入其他方法做一些有趣的事情。)显示从队列中取出元素的两次调用之间的时间差。如果时间差是负数,可以视为一个错误,因为永远不会在延迟时间结束后,在一个更早的触发时间从队列中取得项。
SynchronousQueue 类是最简单的。它没有内部容量。它就像线程之间的手递手机制。在队列中加入一个元素的生产者会等待另一个线程的消费者。当这个消费者出现时,这个元素就直接在消费者和生产者之间传递,永远不会加入到阻塞队列中
2.2 ConcurrentMap实现
新的 java.util.concurrent.
ConcurrentMap 接口和 ConcurrentHashMap 实现只能在键不存在时将元素加入到 map 中,只有在键存在并映射到特定值时才能从 map 中删除一个元素。
ConcurrentMap中有一个新的 putIfAbsent() 方法用于在 map 中进行添加。这个方法以要添加到
ConcurrentMap 实现中的键和值为参数,就像普通的 put() 方法,但是只有在 map 不包含这个键时,才能将键加入到 map 中。如果 map 已经包含这个键,那么这个键的现有值就会返回。这个操作等于于清单9的代码:
if (!map.containsKey(key))
return map.put(key, value);
else
return map.get(key);
像 putIfAbsent() 方法一样,重载后的 remove() 方法有两个参数 —— 键和值。在调用时,只有当键映射到指定的值时才从 map 中删除这个键。如果不匹配,那么就不删除这个键,并返回 false。如果值匹配键的当前映射内容,那么就删除这个键。清单 10 显示了这种操作的等价源代码:
if (map.get(key).equals(value)) {
map.remove(key);
return true;
} else {
return false;
}
2.3 CopyOnWriteArrayList 和 CopyOnWriteArraySet
copy-on-write 模式是这样声明的,为了维护对象的一致性快照,要依靠不可变性(immutability)来消除在协调读取不同的但是相关的属性时需要的同步。对于集合,这意味着如果有大量的读(即 get()) 和迭代,不必同步操作以照顾偶尔的写(即 add())调用。对于新的 CopyOnWriteArrayList 和 CopyOnWriteArraySet 类,所有可变的(mutable)操作都首先取得后台数组的副本,对副本进行更改,然后替换副本。这种做法保证了在遍历自身更改的集合时,永远不会抛出 ConcurrentModificationException。遍历集合会用原来的集合完成,而在以后的操作中使用更新后的集合。
这些新的集合,CopyOnWriteArrayList 和 CopyOnWriteArraySet,最适合于读操作通常大大超过写操作的情况。一个最常提到的例子是使用监听器列表。已经说过,Swing 组件还没有改为使用新的集合。相反,它们继续使用 javax.swing.event.EventListenerList 来维护它们的监听器列表。
如清单11所示,集合的使用与它们的非 copy-on-write 替代物完全一样。只是创建集合并在其中加入或者删除元素。即使对象加入到了集合中,原来的 Iterator 也可以进行,继续遍历原来集合中的项。
import java.util.*;
import java.util.concurrent.*;
public class CopyOnWrite {
public static void main(String args[]) {
List list1 = new CopyOnWriteArrayList(Arrays.asList(args));
List list2 = new ArrayList(Arrays.asList(args));
Iterator itor1 = list1.iterator();
Iterator itor2 = list2.iterator();
list1.add("New");
list2.add("New");
try {
printAll(itor1);
} catch (ConcurrentModificationException e) {
System.err.println("Shouldn't get here");
}
try {
printAll(itor2);
} catch (ConcurrentModificationException e) {
System.err.println("Will get here.");
}
}
private static void printAll(Iterator itor) {
while (itor.hasNext()) {
System.out.println(itor.next());
}
}
}
这个示例程序用命令行参数创建 CopyOnWriteArrayList 和 ArrayList 这两个实例。在得到每一个实例的 Iterator 后,分别在其中加入一个元素。当 ArrayList 迭代因一个 ConcurrentModificationException 问题而立即停止时,CopyOnWriteArrayList 迭代可以继续,不会抛出异常,因为原来的集合是在得到 iterator 之后改变的。如果这种行为(比如通知原来一组事件监听器中的所有元素)是您需要的,那么最好使用 copy-on-write 集合。如果不使用的话,就还用原来的,并保证在出现异常时对它进行处理。
3 Formatter
对于那些从一开始就使用 Java 编程而从没有接触过 C 的人,或者,对那些对 C 没有足够了解的人,格式化字符串是一些古怪的文本串,它们指定一组变量的输出特性。不是用加号将字符串连接在一起(如 firstName + " " + lastName),而是提供一个字符串描述输出,并提供参数以在方法调用结束时,替换字符串中的占位符:String s = String.format("%1$s %2$s", firstName, lastName)。
首先,让我们分析新的 java.util.Formatter 类。您可能不会经常直接使用这个类,但是它提供了所要进行的格式化的内部机制。在这个类的 Javadoc 中,会看到一个描述所支持的格式化选项的表。这些选项的范围从以类似 %7.4f 这样的格式指定浮点数的精度和位数,到格式化时间的 %tT,到格式化第三个参数 %3$s。
用 Formatter 格式化输出分为两步:创建一个 Appendable 对象以存储输出,用 format() 方法将带格式的内容放到这个对象中。下面列出了 Appendable 接口的实现器:
- BufferedWriter
- CharArrayWriter
- CharBuffer
- FileWriter
- FilterWriter
- LogStream
- OutputStreamWriter
- PipedWriter
- PrintStream
- PrintWriter
- StringBuffer
- StringBuilder
- StringWriter
- Writer
在使用 Formatter 类时,可以将实现了这个接口的对象传递给构造函数 Formatter 以把它作为目标。大多数这种类看起来很相似,除了 StringBuilder 类。StringBuilder 与 StringBuffer 类几乎相同,只有一个大的区别:它不是线程安全的。如果知道要在单线程中构建字符串,就使用 StringBuilder。如果构建过程会跨越多个线程,则使用 StringBuffer。清单12显示了通常如何开始使用 Formatter:
StringBuilder sb = new StringBuilder();
Formatter formatter = new Formatter(sb, Locale.US);
创建了Formatter 类后,用格式化字符串和参数调用其 format() 方法。如果需要使用与传递给出构造函数的不同的 Locale 作为格式化输出的一部分,还可以向 format() 方法传递一个 Locale 对象。清单 13 显示了两种不同的 format():
public Formatter format(String format, Object... args)
public Formatter format(Locale l, String format, Object... args)
如果希望得到精度为 10 位数字的 Pi 值,清单 14 中的代码会将这个值放到 StringBuilder 中并打印输出。打印 formatter 对象将显示 Appendable 对象的内容。
import java.util.Locale;
import java.util.Formatter;
public class Build {
public static void main(String args[]) {
StringBuilder sb = new StringBuilder();
Formatter formatter = new Formatter(sb, Locale.US);
formatter.format("PI = %12.10f", Math.PI);
System.out.println(formatter);
}
}
3.1 PrintStream支持
PrintStream 类中定义了常见的、分别用于写入标准输出和标准错误的 System.out 和 System.err 对象。Tiger 引入了两个新的构造函数(用于直接写入文件)和六个方法以提供对格式化的支持(三对)。第一对是另一版本的 append() 方法。这一对方法实现了新的 java.lang.Appendable 接口。一般不会直接调用这些方法。直接调用的是 format() 和 printf(),其中 printf() 版本只是 format() 版本的方便的包装器,如清单15所示:
public PrintStream format(String format, Object... args)
public PrintStream format(Locale l, String format, Object... args)
要记住新的变量参数支持,它是由 ... 指派的
清单16演示了用 PrintStream 的 format() 方法打印今天的日期:
import java.util.Calendar;
public class Now {
public static void main(String args[]) {
System.out.format("Today is %1$tB %1$te, %1$tY.",Calendar.getInstance());