# 14.2 共享有限的资源
可将单线程程序想象成一种孤立的实体,它能遍历我们的问题空间,而且一次只能做一件事情。由于只有一个实体,所以永远不必担心会有两个实体同时试图使用相同的资源,就象两个人同时都想停到一个车位,同时都想通过一扇门,甚至同时发话。
进入多线程环境后,它们则再也不是孤立的。可能会有两个甚至更多的线程试图同时同一个有限的资源。必须对这种潜在资源冲突进行预防,否则就可能发生两个线程同时访问一个银行帐号,打印到同一台计算机,以及对同一个值进行调整等等。
## 14.2.1 资源访问的错误方法
现在考虑换成另一种方式来使用本章频繁见到的计数器。在下面的例子中,每个线程都包含了两个计数器,它们在`run()`里自增以及显示。除此以外,我们使用了`Watcher`类的另一个线程。它的作用是监视计数器,检查它们是否保持相等。这表面是一项无意义的行动,因为如果查看代码,就会发现计数器肯定是相同的。但实际情况却不一定如此。下面是程序的第一个版本:
```
//: Sharing1.java
// Problems with resource sharing while threading
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
class TwoCounter extends Thread {
private boolean started = false;
private TextField
t1 = new TextField(5),
t2 = new TextField(5);
private Label l =
new Label("count1 == count2");
private int count1 = 0, count2 = 0;
// Add the display components as a panel
// to the given container:
public TwoCounter(Container c) {
Panel p = new Panel();
p.add(t1);
p.add(t2);
p.add(l);
c.add(p);
}
public void start() {
if(!started) {
started = true;
super.start();
}
}
public void run() {
while (true) {
t1.setText(Integer.toString(count1++));
t2.setText(Integer.toString(count2++));
try {
sleep(500);
} catch (InterruptedException e){}
}
}
public void synchTest() {
Sharing1.incrementAccess();
if(count1 != count2)
l.setText("Unsynched");
}
}
class Watcher extends Thread {
private Sharing1 p;
public Watcher(Sharing1 p) {
this.p = p;
start();
}
public void run() {
while(true) {
for(int i = 0; i < p.s.length; i++)
p.s[i].synchTest();
try {
sleep(500);
} catch (InterruptedException e){}
}
}
}
public class Sharing1 extends Applet {
TwoCounter[] s;
private static int accessCount = 0;
private static TextField aCount =
new TextField("0", 10);
public static void incrementAccess() {
accessCount++;
aCount.setText(Integer.toString(accessCount));
}
private Button
start = new Button("Start"),
observer = new Button("Observe");
private boolean isApplet = true;
private int numCounters = 0;
private int numObservers = 0;
public void init() {
if(isApplet) {
numCounters =
Integer.parseInt(getParameter("size"));
numObservers =
Integer.parseInt(
getParameter("observers"));
}
s = new TwoCounter[numCounters];
for(int i = 0; i < s.length; i++)
s[i] = new TwoCounter(this);
Panel p = new Panel();
start.addActionListener(new StartL());
p.add(start);
observer.addActionListener(new ObserverL());
p.add(observer);
p.add(new Label("Access Count"));
p.add(aCount);
add(p);
}
class StartL implements ActionListener {
public void actionPerformed(ActionEvent e) {
for(int i = 0; i < s.length; i++)
s[i].start();
}
}
class ObserverL implements ActionListener {
public void actionPerformed(ActionEvent e) {
for(int i = 0; i < numObservers; i++)
new Watcher(Sharing1.this);
}
}
public static void main(String[] args) {
Sharing1 applet = new Sharing1();
// This isn't an applet, so set the flag and
// produce the parameter values from args:
applet.isApplet = false;
applet.numCounters =
(args.length == 0 ? 5 :
Integer.parseInt(args[0]));
applet.numObservers =
(args.length < 2 ? 5 :
Integer.parseInt(args[1]));
Frame aFrame = new Frame("Sharing1");
aFrame.addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e){
System.exit(0);
}
});
aFrame.add(applet, BorderLayout.CENTER);
aFrame.setSize(350, applet.numCounters *100);
applet.init();
applet.start();
aFrame.setVisible(true);
}
} ///:~
```
和往常一样,每个计数器都包含了自己的显示组件:两个文本字段以及一个标签。根据它们的初始值,可知道计数是相同的。这些组件在`TwoCounter`构造器加入`Container`。由于这个线程是通过用户的一个“按下按钮”操作启动的,所以`start()`可能被多次调用。但对一个线程来说,对`Thread.start()`的多次调用是非法的(会产生异常)。在`started`标记和重载的`start()`方法中,大家可看到针对这一情况采取的防范措施。
在`run()`中,`count1`和`count2`的自增与显示方式表面上似乎能保持它们完全一致。随后会调用`sleep()`;若没有这个调用,程序便会出错,因为那会造成CPU难于交换任务。
`synchTest()`方法采取的似乎是没有意义的行动,它检查`count1`是否等于`count2`;如果不等,就把标签设为`"Unsynched"`(不同步)。但是首先,它调用的是类`Sharing1`的一个静态成员,以便自增和显示一个访问计数器,指出这种检查已成功进行了多少次(这样做的理由会在本例的其他版本中变得非常明显)。
`Watcher`类是一个线程,它的作用是为处于活动状态的所有`TwoCounter`对象都调用`synchTest()`。其间,它会对`Sharing1`对象中容纳的数组进行遍历。可将`Watcher`想象成它掠过`TwoCounter`对象的肩膀不断地“偷看”。
`Sharing1`包含了`TwoCounter`对象的一个数组,它通过`init()`进行初始化,并在我们按下`"start"`按钮后作为线程启动。以后若按下`"Observe"`(观察)按钮,就会创建一个或者多个观察器,并对毫不设防的`TwoCounter`进行调查。
注意为了让它作为一个程序片在浏览器中运行,Web页需要包含下面这几行:
```
<applet code=Sharing1 width=650 height=500>
<param name=size value="20">
<param name=observers value="1">
</applet>
```
可自行改变宽度、高度以及参数,根据自己的意愿进行试验。若改变了`size`和`observers`,程序的行为也会发生变化。我们也注意到,通过从命令行接受参数(或者使用默认值),它被设计成作为一个独立的应用程序运行。
下面才是最让人“不可思议”的。在`TwoCounter.run()`中,无限循环只是不断地重复相邻的行:
```
t1.setText(Integer.toString(count1++));
t2.setText(Integer.toString(count2++));
```
(和“睡眠”一样,不过在这里并不重要)。但在程序运行的时候,你会发现`count1`和`count2`被“观察”(用`Watcher`观察)的次数是不相等的!这是由线程的本质造成的——它们可在任何时候挂起(暂停)。所以在上述两行的执行时刻之间,有时会出现执行暂停现象。同时,`Watcher`线程也正好跟随着进来,并正好在这个时候进行比较,造成计数器出现不相等的情况。
本例揭示了使用线程时一个非常基本的问题。我们跟无从知道一个线程什么时候运行。想象自己坐在一张桌子前面,桌上放有一把叉子,准备叉起自己的最后一块食物。当叉子要碰到食物时,食物却突然消失了(因为这个线程已被挂起,同时另一个线程进来“偷”走了食物)。这便是我们要解决的问题。
有的时候,我们并不介意一个资源在尝试使用它的时候是否正被访问(食物在另一些盘子里)。但为了让多线程机制能够正常运转,需要采取一些措施来防止两个线程访问相同的资源——至少在关键的时期。
为防止出现这样的冲突,只需在线程使用一个资源时为其加锁即可。访问资源的第一个线程会其加上锁以后,其他线程便不能再使用那个资源,除非被解锁。如果车子的前座是有限的资源,高喊“这是我的!”的孩子会主张把它锁起来。
## 14.2.2 Java如何共享资源
对一种特殊的资源——对象中的内存——Java提供了内建的机制来防止它们的冲突。由于我们通常将数据元素设为从属于`private`(私有)类,然后只通过方法访问那些内存,所以只需将一个特定的方法设为`synchronized`(同步的),便可有效地防止冲突。在任何时刻,只可有一个线程调用特定对象的一个`synchronized`方法(尽管那个线程可以调用多个对象的同步方法)。下面列出简单的s`ynchronized`方法:
```
synchronized void f() { /* ... */ }
synchronized void g() { /* ... */ }
```
每个对象都包含了一把锁(也叫作“监视器”),它自动成为对象的一部分(不必为此写任何特殊的代码)。调用任何`synchronized`方法时,对象就会被锁定,不可再调用那个对象的其他任何`synchronized`方法,除非第一个方法完成了自己的工作,并解除锁定。在上面的例子中,如果为一个对象调用`f()`,便不能再为同样的对象调用`g()`,除非`f()`完成并解除锁定。因此,一个特定对象的所有`synchronized`方法都共享着一把锁,而且这把锁能防止多个方法对通用内存同时进行写操作(比如同时有多个线程)。
每个类也有自己的一把锁(作为类的`Class`对象的一部分),所以`synchronized static`方法可在一个类的范围内被相互间锁定起来,防止与`static`数据的接触。
注意如果想保护其他某些资源不被多个线程同时访问,可以强制通过`synchronized`方访问那些资源。
(1) 计数器的同步
装备了这个新关键字后,我们能够采取的方案就更灵活了:可以只为`TwoCounter`中的方法简单地使用`synchronized`关键字。下面这个例子是对前例的改版,其中加入了新的关键字:
```
//: Sharing2.java
// Using the synchronized keyword to prevent
// multiple access to a particular resource.
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
class TwoCounter2 extends Thread {
private boolean started = false;
private TextField
t1 = new TextField(5),
t2 = new TextField(5);
private Label l =
new Label("count1 == count2");
private int count1 = 0, count2 = 0;
public TwoCounter2(Container c) {
Panel p = new Panel();
p.add(t1);
p.add(t2);
p.add(l);
c.add(p);
}
public void start() {
if(!started) {
started = true;
super.start();
}
}
public synchronized void run() {
while (true) {
t1.setText(Integer.toString(count1++));
t2.setText(Integer.toString(count2++));
try {
sleep(500);
} catch (InterruptedException e){}
}
}
public synchronized void synchTest() {
Sharing2.incrementAccess();
if(count1 != count2)
l.setText("Unsynched");
}
}
class Watcher2 extends Thread {
private Sharing2 p;
public Watcher2(Sharing2 p) {
this.p = p;
start();
}
public void run() {
while(true) {
for(int i = 0; i < p.s.length; i++)
p.s[i].synchTest();
try {
sleep(500);
} catch (InterruptedException e){}
}
}
}
public class Sharing2 extends Applet {
TwoCounter2[] s;
private static int accessCount = 0;
private static TextField aCount =
new TextField("0", 10);
public static void incrementAccess() {
accessCount++;
aCount.setText(Integer.toString(accessCount));
}
private Button
start = new Button("Start"),
observer = new Button("Observe");
private boolean isApplet = true;
private int numCounters = 0;
private int numObservers = 0;
public void init() {
if(isApplet) {
numCounters =
Integer.parseInt(getParameter("size"));
numObservers =
Integer.parseInt(
getParameter("observers"));
}
s = new TwoCounter2[numCounters];
for(int i = 0; i < s.length; i++)
s[i] = new TwoCounter2(this);
Panel p = new Panel();
start.addActionListener(new StartL());
p.add(start);
observer.addActionListener(new ObserverL());
p.add(observer);
p.add(new Label("Access Count"));
p.add(aCount);
add(p);
}
class StartL implements ActionListener {
public void actionPerformed(ActionEvent e) {
for(int i = 0; i < s.length; i++)
s[i].start();
}
}
class ObserverL implements ActionListener {
public void actionPerformed(ActionEvent e) {
for(int i = 0; i < numObservers; i++)
new Watcher2(Sharing2.this);
}
}
public static void main(String[] args) {
Sharing2 applet = new Sharing2();
// This isn't an applet, so set the flag and
// produce the parameter values from args:
applet.isApplet = false;
applet.numCounters =
(args.length == 0 ? 5 :
Integer.parseInt(args[0]));
applet.numObservers =
(args.length < 2 ? 5 :
Integer.parseInt(args[1]));
Frame aFrame = new Frame("Sharing2");
aFrame.addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e){
System.exit(0);
}
});
aFrame.add(applet, BorderLayout.CENTER);
aFrame.setSize(350, applet.numCounters *100);
applet.init();
applet.start();
aFrame.setVisible(true);
}
} ///:~
```
我们注意到无论`run()`还是`synchTest()`都是“同步的”。如果只同步其中的一个方法,那么另一个就可以自由忽视对象的锁定,并可无碍地调用。所以必须记住一个重要的规则:对于访问某个关键共享资源的所有方法,都必须把它们设为`synchronized`,否则就不能正常地工作。
现在又遇到了一个新问题。`Watcher2`永远都不能看到正在进行的事情,因为整个`run()`方法已设为“同步”。而且由于肯定要为每个对象运行`run()`,所以锁永远不能打开,而`synchTest()`永远不会得到调用。之所以能看到这一结果,是因为`accessCount`根本没有变化。
为解决这个问题,我们能采取的一个办法是只将`run()`中的一部分代码隔离出来。想用这个办法隔离出来的那部分代码叫作“关键区域”,而且要用不同的方式来使用`synchronized`关键字,以设置一个关键区域。Java通过“同步块”提供对关键区域的支持;这一次,我们用`synchronized`关键字指出对象的锁用于对其中封闭的代码进行同步。如下所示:
```
synchronized(syncObject) {
// This code can be accessed by only
// one thread at a time, assuming all
// threads respect syncObject's lock
}
```
在能进入同步块之前,必须在`synchObject`上取得锁。如果已有其他线程取得了这把锁,块便不能进入,必须等候那把锁被释放。
可从整个`run()`中删除`synchronized`关键字,换成用一个同步块包围两个关键行,从而完成对`Sharing2`例子的修改。但什么对象应作为锁来使用呢?那个对象已由`synchTest()`标记出来了——也就是当前对象(`this`)!所以修改过的`run()`方法象下面这个样子:
```
public void run() {
while (true) {
synchronized(this) {
t1.setText(Integer.toString(count1++));
t2.setText(Integer.toString(count2++));
}
try {
sleep(500);
} catch (InterruptedException e){}
}
}
```
这是必须对`Sharing2.java`作出的唯一修改,我们会看到尽管两个计数器永远不会脱离同步(取决于允许`Watcher`什么时候检查它们),但在`run()`执行期间,仍然向`Watcher`提供了足够的访问权限。
当然,所有同步都取决于程序员是否勤奋:要访问共享资源的每一部分代码都必须封装到一个适当的同步块里。
(2) 同步的效率
由于要为同样的数据编写两个方法,所以无论如何都不会给人留下效率很高的印象。看来似乎更好的一种做法是将所有方法都设为自动同步,并完全消除`synchronized`关键字(当然,含有`synchronized run()`的例子显示出这样做是很不通的)。但它也揭示出获取一把锁并非一种“廉价”方案——为一次方法调用付出的代价(进入和退出方法,不执行方法主体)至少要累加到四倍,而且根据我们的具体现方案,这一代价还有可能变得更高。所以假如已知一个方法不会造成冲突,最明智的做法便是撤消其中的`synchronized`关键字。
## 14.2.3 回顾Java Beans
我们现在已理解了同步,接着可换从另一个角度来考察Java Beans。无论什么时候创建了一个Bean,就必须假定它要在一个多线程的环境中运行。这意味着:
(1) 只要可行,Bean的所有公共方法都应同步。当然,这也带来了“同步”在运行期间的开销。若特别在意这个问题,在关键区域中不会造成问题的方法就可保留为“不同步”,但注意这通常都不是十分容易判断。有资格的方法倾向于规模很小(如下例的`getCircleSize()`)以及/或者“微小”。也就是说,这个方法调用在如此少的代码片里执行,以至于在执行期间对象不能改变。如果将这种方法设为“不同步”,可能对程序的执行速度不会有明显的影响。可能也将一个Bean的所有`public`方法都设为`synchronized`,并只有在保证特别必要、而且会造成一个差异的情况下,才将`synchronized`关键字删去。
(2) 如果将一个多转换事件送给一系列对那个事件感兴趣的“听众”,必须假在列表中移动的时候可以添加或者删除。
第一点很容易处理,但第二点需要考虑更多的东西。让我们以前一章提供的`BangBean.java`为例。在那个例子中,我们忽略了`synchronized`关键字(那时还没有引入呢),并将转换设为单转换,从而回避了多线程的问题。在下面这个修改过的版本中,我们使其能在多线程环境中工作,并为事件采用了多转换技术:
```
//: BangBean2.java
// You should write your Beans this way so they
// can run in a multithreaded environment.
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import java.io.*;
public class BangBean2 extends Canvas
implements Serializable {
private int xm, ym;
private int cSize = 20; // Circle size
private String text = "Bang!";
private int fontSize = 48;
private Color tColor = Color.red;
private Vector actionListeners = new Vector();
public BangBean2() {
addMouseListener(new ML());
addMouseMotionListener(new MM());
}
public synchronized int getCircleSize() {
return cSize;
}
public synchronized void
setCircleSize(int newSize) {
cSize = newSize;
}
public synchronized String getBangText() {
return text;
}
public synchronized void
setBangText(String newText) {
text = newText;
}
public synchronized int getFontSize() {
return fontSize;
}
public synchronized void
setFontSize(int newSize) {
fontSize = newSize;
}
public synchronized Color getTextColor() {
return tColor;
}
public synchronized void
setTextColor(Color newColor) {
tColor = newColor;
}
public void paint(Graphics g) {
g.setColor(Color.black);
g.drawOval(xm - cSize/2, ym - cSize/2,
cSize, cSize);
}
// This is a multicast listener, which is
// more typically used than the unicast
// approach taken in BangBean.java:
public synchronized void addActionListener (
ActionListener l) {
actionListeners.addElement(l);
}
public synchronized void removeActionListener(
ActionListener l) {
actionListeners.removeElement(l);
}
// Notice this isn't synchronized:
public void notifyListeners() {
ActionEvent a =
new ActionEvent(BangBean2.this,
ActionEvent.ACTION_PERFORMED, null);
Vector lv = null;
// Make a copy of the vector in case someone
// adds a listener while we're
// calling listeners:
synchronized(this) {
lv = (Vector)actionListeners.clone();
}
// Call all the listener methods:
for(int i = 0; i < lv.size(); i++) {
ActionListener al =
(ActionListener)lv.elementAt(i);
al.actionPerformed(a);
}
}
class ML extends MouseAdapter {
public void mousePressed(MouseEvent e) {
Graphics g = getGraphics();
g.setColor(tColor);
g.setFont(
new Font(
"TimesRoman", Font.BOLD, fontSize));
int width =
g.getFontMetrics().stringWidth(text);
g.drawString(text,
(getSize().width - width) /2,
getSize().height/2);
g.dispose();
notifyListeners();
}
}
class MM extends MouseMotionAdapter {
public void mouseMoved(MouseEvent e) {
xm = e.getX();
ym = e.getY();
repaint();
}
}
// Testing the BangBean2:
public static void main(String[] args) {
BangBean2 bb = new BangBean2();
bb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e){
System.out.println("ActionEvent" + e);
}
});
bb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e){
System.out.println("BangBean2 action");
}
});
bb.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e){
System.out.println("More action");
}
});
Frame aFrame = new Frame("BangBean2 Test");
aFrame.addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
aFrame.add(bb, BorderLayout.CENTER);
aFrame.setSize(300,300);
aFrame.setVisible(true);
}
} ///:~
```
很容易就可以为方法添加`synchronized`。但注意在`addActionListener()`和`removeActionListener()`中,现在添加了`ActionListener`,并从一个`Vector`中移去,所以能够根据自己愿望使用任意多个。
我们注意到,`notifyListeners()`方法并未设为“同步”。可从多个线程中发出对这个方法的调用。另外,在对`notifyListeners()`调用的中途,也可能发出对`addActionListener()`和`removeActionListener()`的调用。这显然会造成问题,因为它否定了`Vector actionListeners`。为缓解这个问题,我们在一个`synchronized`从句中“克隆”了`Vector`,并对克隆进行了否定。这样便可在不影响`notifyListeners()`的前提下,对`Vector`进行操纵。
`paint()`方法也没有设为“同步”。与单纯地添加自己的方法相比,决定是否对重载的方法进行同步要困难得多。在这个例子中,无论`paint()`是否“同步”,它似乎都能正常地工作。但必须考虑的问题包括:
(1) 方法会在对象内部修改“关键”变量的状态吗?为判断一个变量是否“关键”,必须知道它是否会被程序中的其他线程读取或设置(就目前的情况看,读取或设置几乎肯定是通过“同步”方法进行的,所以可以只对它们进行检查)。对`paint()`的情况来说,不会发生任何修改。
(2) 方法要以这些“关键”变量的状态为基础吗?如果一个“同步”方法修改了一个变量,而我们的方法要用到这个变量,那么一般都愿意把自己的方法也设为“同步”。基于这一前提,大家可观察到`cSize`由“同步”方法进行了修改,所以`paint()`应当是“同步”的。但在这里,我们可以问:“假如`cSize`在`paint()`执行期间发生了变化,会发生的最糟糕的事情是什么呢?”如果发现情况不算太坏,而且仅仅是暂时的效果,那么最好保持`paint()`的“不同步”状态,以避免同步方法调用带来的额外开销。
(3) 要留意的第三条线索是`paint()`基类版本是否“同步”,在这里它不是同步的。这并不是一个非常严格的参数,仅仅是一条“线索”。比如在目前的情况下,通过同步方法(好`cSize`)改变的一个字段已组合到`paint()`公式里,而且可能已改变了情况。但请注意,`synchronized`不能继承——也就是说,假如一个方法在基类中是“同步”的,那么在派生类重载版本中,它不会自动进入“同步”状态。
`TestBangBean2`中的测试代码已在前一章的基础上进行了修改,已在其中加入了额外的“听众”,从而演示了`BangBean2`的多转换能力。
- Java 编程思想
- 写在前面的话
- 引言
- 第1章 对象入门
- 1.1 抽象的进步
- 1.2 对象的接口
- 1.3 实现方案的隐藏
- 1.4 方案的重复使用
- 1.5 继承:重新使用接口
- 1.6 多态对象的互换使用
- 1.7 对象的创建和存在时间
- 1.8 异常控制:解决错误
- 1.9 多线程
- 1.10 永久性
- 1.11 Java和因特网
- 1.12 分析和设计
- 1.13 Java还是C++
- 第2章 一切都是对象
- 2.1 用引用操纵对象
- 2.2 所有对象都必须创建
- 2.3 绝对不要清除对象
- 2.4 新建数据类型:类
- 2.5 方法、参数和返回值
- 2.6 构建Java程序
- 2.7 我们的第一个Java程序
- 2.8 注释和嵌入文档
- 2.9 编码样式
- 2.10 总结
- 2.11 练习
- 第3章 控制程序流程
- 3.1 使用Java运算符
- 3.2 执行控制
- 3.3 总结
- 3.4 练习
- 第4章 初始化和清除
- 4.1 用构造器自动初始化
- 4.2 方法重载
- 4.3 清除:收尾和垃圾收集
- 4.4 成员初始化
- 4.5 数组初始化
- 4.6 总结
- 4.7 练习
- 第5章 隐藏实现过程
- 5.1 包:库单元
- 5.2 Java访问指示符
- 5.3 接口与实现
- 5.4 类访问
- 5.5 总结
- 5.6 练习
- 第6章 类复用
- 6.1 组合的语法
- 6.2 继承的语法
- 6.3 组合与继承的结合
- 6.4 到底选择组合还是继承
- 6.5 protected
- 6.6 累积开发
- 6.7 向上转换
- 6.8 final关键字
- 6.9 初始化和类装载
- 6.10 总结
- 6.11 练习
- 第7章 多态性
- 7.1 向上转换
- 7.2 深入理解
- 7.3 覆盖与重载
- 7.4 抽象类和方法
- 7.5 接口
- 7.6 内部类
- 7.7 构造器和多态性
- 7.8 通过继承进行设计
- 7.9 总结
- 7.10 练习
- 第8章 对象的容纳
- 8.1 数组
- 8.2 集合
- 8.3 枚举器(迭代器)
- 8.4 集合的类型
- 8.5 排序
- 8.6 通用集合库
- 8.7 新集合
- 8.8 总结
- 8.9 练习
- 第9章 异常差错控制
- 9.1 基本异常
- 9.2 异常的捕获
- 9.3 标准Java异常
- 9.4 创建自己的异常
- 9.5 异常的限制
- 9.6 用finally清除
- 9.7 构造器
- 9.8 异常匹配
- 9.9 总结
- 9.10 练习
- 第10章 Java IO系统
- 10.1 输入和输出
- 10.2 增添属性和有用的接口
- 10.3 本身的缺陷:RandomAccessFile
- 10.4 File类
- 10.5 IO流的典型应用
- 10.6 StreamTokenizer
- 10.7 Java 1.1的IO流
- 10.8 压缩
- 10.9 对象序列化
- 10.10 总结
- 10.11 练习
- 第11章 运行期类型识别
- 11.1 对RTTI的需要
- 11.2 RTTI语法
- 11.3 反射:运行期类信息
- 11.4 总结
- 11.5 练习
- 第12章 传递和返回对象
- 12.1 传递引用
- 12.2 制作本地副本
- 12.3 克隆的控制
- 12.4 只读类
- 12.5 总结
- 12.6 练习
- 第13章 创建窗口和程序片
- 13.1 为何要用AWT?
- 13.2 基本程序片
- 13.3 制作按钮
- 13.4 捕获事件
- 13.5 文本字段
- 13.6 文本区域
- 13.7 标签
- 13.8 复选框
- 13.9 单选钮
- 13.10 下拉列表
- 13.11 列表框
- 13.12 布局的控制
- 13.13 action的替代品
- 13.14 程序片的局限
- 13.15 视窗化应用
- 13.16 新型AWT
- 13.17 Java 1.1用户接口API
- 13.18 可视编程和Beans
- 13.19 Swing入门
- 13.20 总结
- 13.21 练习
- 第14章 多线程
- 14.1 反应灵敏的用户界面
- 14.2 共享有限的资源
- 14.3 堵塞
- 14.4 优先级
- 14.5 回顾runnable
- 14.6 总结
- 14.7 练习
- 第15章 网络编程
- 15.1 机器的标识
- 15.2 套接字
- 15.3 服务多个客户
- 15.4 数据报
- 15.5 一个Web应用
- 15.6 Java与CGI的沟通
- 15.7 用JDBC连接数据库
- 15.8 远程方法
- 15.9 总结
- 15.10 练习
- 第16章 设计模式
- 16.1 模式的概念
- 16.2 观察器模式
- 16.3 模拟垃圾回收站
- 16.4 改进设计
- 16.5 抽象的应用
- 16.6 多重分发
- 16.7 访问器模式
- 16.8 RTTI真的有害吗
- 16.9 总结
- 16.10 练习
- 第17章 项目
- 17.1 文字处理
- 17.2 方法查找工具
- 17.3 复杂性理论
- 17.4 总结
- 17.5 练习
- 附录A 使用非JAVA代码
- 附录B 对比C++和Java
- 附录C Java编程规则
- 附录D 性能
- 附录E 关于垃圾收集的一些话
- 附录F 推荐读物