# 14.3 堵塞
一个线程可以有四种状态:
(1) 新(New):线程对象已经创建,但尚未启动,所以不可运行。
(2) 可运行(Runnable):意味着一旦时间分片机制有空闲的CPU周期提供给一个线程,那个线程便可立即开始运行。因此,线程可能在、也可能不在运行当中,但一旦条件许可,没有什么能阻止它的运行——它既没有“死”掉,也未被“堵塞”。
(3) 死(Dead):从自己的`run()`方法中返回后,一个线程便已“死”掉。亦可调用`stop()`令其死掉,但会产生一个异常——属于`Error`的一个子类(也就是说,我们通常不捕获它)。记住一个异常的“抛”出应当是一个特殊事件,而不是正常程序运行的一部分。所以不建议你使用`stop()`(在Java 1.2则是坚决反对)。另外还有一个`destroy()`方法(它永远不会实现),应该尽可能地避免调用它,因为它非常武断,根本不会解除对象的锁定。
(4) 堵塞(Blocked):线程可以运行,但有某种东西阻碍了它。若线程处于堵塞状态,调度机制可以简单地跳过它,不给它分配任何CPU时间。除非线程再次进入“可运行”状态,否则不会采取任何操作。
## 14.3.1 为何会堵塞
堵塞状态是前述四种状态中最有趣的,值得我们作进一步的探讨。线程被堵塞可能是由下述五方面的原因造成的:
(1) 调用`sleep(毫秒数)`,使线程进入“睡眠”状态。在规定的时间内,这个线程是不会运行的。
(2) 用`suspend()`暂停了线程的执行。除非线程收到`resume()`消息,否则不会返回“可运行”状态。
(3) 用`wait()`暂停了线程的执行。除非线程收到`nofify()`或者`notifyAll()`消息,否则不会变成“可运行”(是的,这看起来同原因2非常相象,但有一个明显的区别是我们马上要揭示的)。
(4) 线程正在等候一些IO(输入输出)操作完成。
(5) 线程试图调用另一个对象的“同步”方法,但那个对象处于锁定状态,暂时无法使用。
亦可调用`yield()`(`Thread`类的一个方法)自动放弃CPU,以便其他线程能够运行。然而,假如调度机制觉得我们的线程已拥有足够的时间,并跳转到另一个线程,就会发生同样的事情。也就是说,没有什么能防止调度机制重新启动我们的线程。线程被堵塞后,便有一些原因造成它不能继续运行。
下面这个例子展示了进入堵塞状态的全部五种途径。它们全都存在于名为`Blocking.java`的一个文件中,但在这儿采用散落的片断进行解释(大家可注意到片断前后的`Continued`以及`Continuing`标志。利用第17章介绍的工具,可将这些片断连结到一起)。首先让我们看看基本的框架:
```
//: Blocking.java
// Demonstrates the various ways a thread
// can be blocked.
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.io.*;
//////////// The basic framework ///////////
class Blockable extends Thread {
private Peeker peeker;
protected TextField state = new TextField(40);
protected int i;
public Blockable(Container c) {
c.add(state);
peeker = new Peeker(this, c);
}
public synchronized int read() { return i; }
protected synchronized void update() {
state.setText(getClass().getName()
+ " state: i = " + i);
}
public void stopPeeker() {
// peeker.stop(); Deprecated in Java 1.2
peeker.terminate(); // The preferred approach
}
}
class Peeker extends Thread {
private Blockable b;
private int session;
private TextField status = new TextField(40);
private boolean stop = false;
public Peeker(Blockable b, Container c) {
c.add(status);
this.b = b;
start();
}
public void terminate() { stop = true; }
public void run() {
while (!stop) {
status.setText(b.getClass().getName()
+ " Peeker " + (++session)
+ "; value = " + b.read());
try {
sleep(100);
} catch (InterruptedException e){}
}
}
} ///:Continued
```
`Blockable`类打算成为本例所有类的一个基类。一个`Blockable`对象包含了一个名为`state`的`TextField`(文本字段),用于显示出对象有关的信息。用于显示这些信息的方法叫作`update()`。我们发现它用`getClass.getName()`来产生类名,而不是仅仅把它打印出来;这是由于`update(0)`不知道自己为其调用的那个类的准确名字,因为那个类是从`Blockable`派生出来的。
在`Blockable`中,变动指示符是一个`int i`;派生类的`run()`方法会为其自增。
针对每个`Bloackable`对象,都会启动`Peeker`类的一个线程。`Peeker`的任务是调用`read()`方法,检查与自己关联的`Blockable`对象,看看i是否发生了变化,最后用它的`status`文本字段报告检查结果。注意`read()`和`update()`都是同步的,要求对象的锁定能自由解除,这一点非常重要。
(1) 睡眠
这个程序的第一项测试是用`sleep()`作出的:
```
///:Continuing
///////////// Blocking via sleep() ///////////
class Sleeper1 extends Blockable {
public Sleeper1(Container c) { super(c); }
public synchronized void run() {
while(true) {
i++;
update();
try {
sleep(1000);
} catch (InterruptedException e){}
}
}
}
class Sleeper2 extends Blockable {
public Sleeper2(Container c) { super(c); }
public void run() {
while(true) {
change();
try {
sleep(1000);
} catch (InterruptedException e){}
}
}
public synchronized void change() {
i++;
update();
}
} ///:Continued
```
在`Sleeper1`中,整个`run()`方法都是同步的。我们可看到与这个对象关联在一起的`Peeker`可以正常运行,直到我们启动线程为止,随后`Peeker`便会完全停止。这正是“堵塞”的一种形式:因为`Sleeper1.run()`是同步的,而且一旦线程启动,它就肯定在`run()`内部,方法永远不会放弃对象锁定,造成`Peeker`线程的堵塞。
`Sleeper2`通过设置不同步的运行,提供了一种解决方案。只有`change()`方法才是同步的,所以尽管`run()`位于`sleep()`内部,`Peeker`仍然能访问自己需要的同步方法——`read()`。在这里,我们可看到在启动了`Sleeper2`线程以后,`Peeker`会持续运行下去。
(2) 暂停和恢复
这个例子接下来的一部分引入了“挂起”或者“暂停”(`Suspend`)的概述。`Thread`类提供了一个名为`suspend()`的方法,可临时中止线程;以及一个名为`resume()`的方法,用于从暂停处开始恢复线程的执行。显然,我们可以推断出`resume()`是由暂停线程外部的某个线程调用的。在这种情况下,需要用到一个名为`Resumer`(恢复器)的独立类。演示暂停/恢复过程的每个类都有一个相关的恢复器。如下所示:
```
///:Continuing
/////////// Blocking via suspend() ///////////
class SuspendResume extends Blockable {
public SuspendResume(Container c) {
super(c);
new Resumer(this);
}
}
class SuspendResume1 extends SuspendResume {
public SuspendResume1(Container c) { super(c);}
public synchronized void run() {
while(true) {
i++;
update();
suspend(); // Deprecated in Java 1.2
}
}
}
class SuspendResume2 extends SuspendResume {
public SuspendResume2(Container c) { super(c);}
public void run() {
while(true) {
change();
suspend(); // Deprecated in Java 1.2
}
}
public synchronized void change() {
i++;
update();
}
}
class Resumer extends Thread {
private SuspendResume sr;
public Resumer(SuspendResume sr) {
this.sr = sr;
start();
}
public void run() {
while(true) {
try {
sleep(1000);
} catch (InterruptedException e){}
sr.resume(); // Deprecated in Java 1.2
}
}
} ///:Continued
```
`SuspendResume1`也提供了一个同步的`run()`方法。同样地,当我们启动这个线程以后,就会发现与它关联的`Peeker`进入“堵塞”状态,等候对象锁被释放,但那永远不会发生。和往常一样,这个问题在`SuspendResume2`里得到了解决,它并不同步整个`run()`方法,而是采用了一个单独的同步`change()`方法。
对于Java 1.2,大家应注意`suspend()`和`resume()`已获得强烈反对,因为`suspend()`包含了对象锁,所以极易出现“死锁”现象。换言之,很容易就会看到许多被锁住的对象在傻乎乎地等待对方。这会造成整个应用程序的“凝固”。尽管在一些老程序中还能看到它们的踪迹,但在你写自己的程序时,无论如何都应避免。本章稍后就会讲述正确的方案是什么。
(3) 等待和通知
通过前两个例子的实践,我们知道无论`sleep()`还是`suspend()`都不会在自己被调用的时候解除锁定。需要用到对象锁时,请务必注意这个问题。在另一方面,`wait()`方法在被调用时却会解除锁定,这意味着可在执行`wait()`期间调用线程对象中的其他同步方法。但在接着的两个类中,我们看到`run()`方法都是“同步”的。在`wait()`期间,`Peeker`仍然拥有对同步方法的完全访问权限。这是由于`wait()`在挂起内部调用的方法时,会解除对象的锁定。
我们也可以看到`wait()`的两种形式。第一种形式采用一个以毫秒为单位的参数,它具有与`sleep()`中相同的含义:暂停这一段规定时间。区别在于在`wait()`中,对象锁已被解除,而且能够自由地退出`wait()`,因为一个`notify()`可强行使时间流逝。
第二种形式不采用任何参数,这意味着`wait()`会持续执行,直到`notify()`介入为止。而且在一段时间以后,不会自行中止。
`wait()`和`notify()`比较特别的一个地方是这两个方法都属于基类`Object`的一部分,不象`sleep()`,`suspend()`以及`resume()`那样属于`Thread`的一部分。尽管这表面看有点儿奇怪——居然让专门进行线程处理的东西成为通用基类的一部分——但仔细想想又会释然,因为它们操纵的对象锁也属于每个对象的一部分。因此,我们可将一个`wait()`置入任何同步方法内部,无论在那个类里是否准备进行涉及线程的处理。事实上,我们能调用`wait()`的唯一地方是在一个同步的方法或代码块内部。若在一个不同步的方法内调用`wait()`或者`notify()`,尽管程序仍然会编译,但在运行它的时候,就会得到一个`IllegalMonitorStateException`(非法监视器状态异常),而且会出现多少有点莫名其妙的一条消息:`current thread not owner`(当前线程不是所有人”。注意`sleep()`,`suspend()`以及`resume()`都能在不同步的方法内调用,因为它们不需要对锁定进行操作。
只能为自己的锁定调用`wait()`和`notify()`。同样地,仍然可以编译那些试图使用错误锁定的代码,但和往常一样会产生同样的`IllegalMonitorStateException`异常。我们没办法用其他人的对象锁来愚弄系统,但可要求另一个对象执行相应的操作,对它自己的锁进行操作。所以一种做法是创建一个同步方法,令其为自己的对象调用`notify()`。但在`Notifier`中,我们会看到一个同步方法内部的`notify()`:
```
synchronized(wn2) {
wn2.notify();
}
```
其中,`wn2`是类型为`WaitNotify2`的对象。尽管并不属于`WaitNotify2`的一部分,这个方法仍然获得了`wn2`对象的锁定。在这个时候,它为`wn2`调用`notify()`是合法的,不会得到`IllegalMonitorStateException`异常。
```
///:Continuing
/////////// Blocking via wait() ///////////
class WaitNotify1 extends Blockable {
public WaitNotify1(Container c) { super(c); }
public synchronized void run() {
while(true) {
i++;
update();
try {
wait(1000);
} catch (InterruptedException e){}
}
}
}
class WaitNotify2 extends Blockable {
public WaitNotify2(Container c) {
super(c);
new Notifier(this);
}
public synchronized void run() {
while(true) {
i++;
update();
try {
wait();
} catch (InterruptedException e){}
}
}
}
class Notifier extends Thread {
private WaitNotify2 wn2;
public Notifier(WaitNotify2 wn2) {
this.wn2 = wn2;
start();
}
public void run() {
while(true) {
try {
sleep(2000);
} catch (InterruptedException e){}
synchronized(wn2) {
wn2.notify();
}
}
}
} ///:Continued
```
若必须等候其他某些条件(从线程外部加以控制)发生变化,同时又不想在线程内一直傻乎乎地等下去,一般就需要用到`wait()`。`wait()`允许我们将线程置入“睡眠”状态,同时又“积极”地等待条件发生改变。而且只有在一个`notify()`或`notifyAll()`发生变化的时候,线程才会被唤醒,并检查条件是否有变。因此,我们认为它提供了在线程间进行同步的一种手段。
(4) IO堵塞
若一个数据流必须等候一些IO活动,便会自动进入“堵塞”状态。在本例下面列出的部分中,有两个类协同通用的`Reader`以及`Writer`对象工作(使用Java 1.1的流)。但在测试模型中,会设置一个管道化的数据流,使两个线程相互间能安全地传递数据(这正是使用管道流的目的)。
`Sender`将数据置入`Writer`,并“睡眠”随机长短的时间。然而,`Receiver`本身并没有包括`sleep()`,`suspend()`或者`wait()`方法。但在执行`read()`的时候,如果没有数据存在,它会自动进入“堵塞”状态。如下所示:
```
///:Continuing
class Sender extends Blockable { // send
private Writer out;
public Sender(Container c, Writer out) {
super(c);
this.out = out;
}
public void run() {
while(true) {
for(char c = 'A'; c <= 'z'; c++) {
try {
i++;
out.write(c);
state.setText("Sender sent: "
+ (char)c);
sleep((int)(3000 * Math.random()));
} catch (InterruptedException e){}
catch (IOException e) {}
}
}
}
}
class Receiver extends Blockable {
private Reader in;
public Receiver(Container c, Reader in) {
super(c);
this.in = in;
}
public void run() {
try {
while(true) {
i++; // Show peeker it's alive
// Blocks until characters are there:
state.setText("Receiver read: "
+ (char)in.read());
}
} catch(IOException e) { e.printStackTrace();}
}
} ///:Continued
```
这两个类也将信息送入自己的`state`字段,并修改`i`值,使`Peeker`知道线程仍在运行。
(5) 测试
令人惊讶的是,主要的程序片(Applet)类非常简单,这是大多数工作都已置入`Blockable`框架的缘故。大概地说,我们创建了一个由`Blockable`对象构成的数组。而且由于每个对象都是一个线程,所以在按下`"start"`按钮后,它们会采取自己的行动。还有另一个按钮和`actionPerformed()`从句,用于中止所有`Peeker`对象。由于Java 1.2“反对”使用`Thread`的`stop()`方法,所以可考虑采用这种折衷形式的中止方式。
为了在`Sender`和`Receiver`之间建立一个连接,我们创建了一个`PipedWriter`和一个`PipedReader`。注意`PipedReader in`必须通过一个构造器参数同`PipedWriterout`连接起来。在那以后,我们在`out`内放进去的所有东西都可从`in`中提取出来——似乎那些东西是通过一个“管道”传输过去的。随后将`in`和`out`对象分别传递给`Receiver`和`Sender`构造器;后者将它们当作任意类型的`Reader`和`Writer`看待(也就是说,它们被“上溯”转换了)。
`Blockable`引用`b`的数组在定义之初并未得到初始化,因为管道化的数据流是不可在定义前设置好的(对`try`块的需要将成为障碍):
```
///:Continuing
/////////// Testing Everything ///////////
public class Blocking extends Applet {
private Button
start = new Button("Start"),
stopPeekers = new Button("Stop Peekers");
private boolean started = false;
private Blockable[] b;
private PipedWriter out;
private PipedReader in;
public void init() {
out = new PipedWriter();
try {
in = new PipedReader(out);
} catch(IOException e) {}
b = new Blockable[] {
new Sleeper1(this),
new Sleeper2(this),
new SuspendResume1(this),
new SuspendResume2(this),
new WaitNotify1(this),
new WaitNotify2(this),
new Sender(this, out),
new Receiver(this, in)
};
start.addActionListener(new StartL());
add(start);
stopPeekers.addActionListener(
new StopPeekersL());
add(stopPeekers);
}
class StartL implements ActionListener {
public void actionPerformed(ActionEvent e) {
if(!started) {
started = true;
for(int i = 0; i < b.length; i++)
b[i].start();
}
}
}
class StopPeekersL implements ActionListener {
public void actionPerformed(ActionEvent e) {
// Demonstration of the preferred
// alternative to Thread.stop():
for(int i = 0; i < b.length; i++)
b[i].stopPeeker();
}
}
public static void main(String[] args) {
Blocking applet = new Blocking();
Frame aFrame = new Frame("Blocking");
aFrame.addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
aFrame.add(applet, BorderLayout.CENTER);
aFrame.setSize(350,550);
applet.init();
applet.start();
aFrame.setVisible(true);
}
} ///:~
```
在`init()`中,注意循环会遍历整个数组,并为页添加`state`和`peeker.status`文本字段。
首次创建好`Blockable`线程以后,每个这样的线程都会自动创建并启动自己的`Peeker`。所以我们会看到各个`Peeker`都在`Blockable`线程启动之前运行起来。这一点非常重要,因为在`Blockable`线程启动的时候,部分`Peeker`会被堵塞,并停止运行。弄懂这一点,将有助于我们加深对“堵塞”这一概念的认识。
## 14.3.2 死锁
由于线程可能进入堵塞状态,而且由于对象可能拥有“同步”方法——除非同步锁定被解除,否则线程不能访问那个对象——所以一个线程完全可能等候另一个对象,而另一个对象又在等候下一个对象,以此类推。这个“等候”链最可怕的情形就是进入封闭状态——最后那个对象等候的是第一个对象!此时,所有线程都会陷入无休止的相互等待状态,大家都动弹不得。我们将这种情况称为“死锁”。尽管这种情况并非经常出现,但一旦碰到,程序的调试将变得异常艰难。
就语言本身来说,尚未直接提供防止死锁的帮助措施,需要我们通过谨慎的设计来避免。如果有谁需要调试一个死锁的程序,他是没有任何窍门可用的。
(1) Java 1.2对`stop()`,`suspend()`,`resume()`以及`destroy()`的反对
为减少出现死锁的可能,Java 1.2作出的一项贡献是“反对”使用`Thread`的`stop()`,`suspend()`,`resume()`以及`destroy()`方法。
之所以反对使用`stop()`,是因为它不安全。它会解除由线程获取的所有锁定,而且如果对象处于一种不连贯状态(“被析构”),那么其他线程能在那种状态下检查和修改它们。结果便造成了一种微妙的局面,我们很难检查出真正的问题所在。所以应尽量避免使用`stop()`,应该采用`Blocking.java`那样的方法,用一个标志告诉线程什么时候通过退出自己的`run()`方法来中止自己的执行。
如果一个线程被堵塞,比如在它等候输入的时候,那么一般都不能象在`Blocking.java`中那样轮询一个标志。但在这些情况下,我们仍然不该使用`stop()`,而应换用由`Thread`提供的`interrupt()`方法,以便中止并退出堵塞的代码。
```
//: Interrupt.java
// The alternative approach to using stop()
// when a thread is blocked
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
class Blocked extends Thread {
public synchronized void run() {
try {
wait(); // Blocks
} catch(InterruptedException e) {
System.out.println("InterruptedException");
}
System.out.println("Exiting run()");
}
}
public class Interrupt extends Applet {
private Button
interrupt = new Button("Interrupt");
private Blocked blocked = new Blocked();
public void init() {
add(interrupt);
interrupt.addActionListener(
new ActionListener() {
public
void actionPerformed(ActionEvent e) {
System.out.println("Button pressed");
if(blocked == null) return;
Thread remove = blocked;
blocked = null; // to release it
remove.interrupt();
}
});
blocked.start();
}
public static void main(String[] args) {
Interrupt applet = new Interrupt();
Frame aFrame = new Frame("Interrupt");
aFrame.addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
aFrame.add(applet, BorderLayout.CENTER);
aFrame.setSize(200,100);
applet.init();
applet.start();
aFrame.setVisible(true);
}
} ///:~
```
`Blocked.run()`内部的`wait()`会产生堵塞的线程。当我们按下按钮以后,`blocked`(堵塞)的引用就会设为`null`,使垃圾收集器能够将其清除,然后调用对象的`interrupt()`方法。如果是首次按下按钮,我们会看到线程正常退出。但在没有可供“杀死”的线程以后,看到的便只是按钮被按下而已。
`suspend()`和`resume()`方法天生容易发生死锁。调用`suspend()`的时候,目标线程会停下来,但却仍然持有在这之前获得的锁定。此时,其他任何线程都不能访问锁定的资源,除非被“挂起”的线程恢复运行。对任何线程来说,如果它们想恢复目标线程,同时又试图使用任何一个锁定的资源,就会造成令人难堪的死锁。所以我们不应该使用`suspend()`和`resume()`,而应在自己的`Thread`类中置入一个标志,指出线程应该活动还是挂起。若标志指出线程应该挂起,便用`wait()`命其进入等待状态。若标志指出线程应当恢复,则用一个`notify()`重新启动线程。我们可以修改前面的`Counter2.java`来实际体验一番。尽管两个版本的效果是差不多的,但大家会注意到代码的组织结构发生了很大的变化——为所有“听众”都使用了匿名的内部类,而且`Thread`是一个内部类。这使得程序的编写稍微方便一些,因为它取消了`Counter2.java`中一些额外的记录工作。
```
//: Suspend.java
// The alternative approach to using suspend()
// and resume(), which have been deprecated
// in Java 1.2.
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
public class Suspend extends Applet {
private TextField t = new TextField(10);
private Button
suspend = new Button("Suspend"),
resume = new Button("Resume");
class Suspendable extends Thread {
private int count = 0;
private boolean suspended = false;
public Suspendable() { start(); }
public void fauxSuspend() {
suspended = true;
}
public synchronized void fauxResume() {
suspended = false;
notify();
}
public void run() {
while (true) {
try {
sleep(100);
synchronized(this) {
while(suspended)
wait();
}
} catch (InterruptedException e){}
t.setText(Integer.toString(count++));
}
}
}
private Suspendable ss = new Suspendable();
public void init() {
add(t);
suspend.addActionListener(
new ActionListener() {
public
void actionPerformed(ActionEvent e) {
ss.fauxSuspend();
}
});
add(suspend);
resume.addActionListener(
new ActionListener() {
public
void actionPerformed(ActionEvent e) {
ss.fauxResume();
}
});
add(resume);
}
public static void main(String[] args) {
Suspend applet = new Suspend();
Frame aFrame = new Frame("Suspend");
aFrame.addWindowListener(
new WindowAdapter() {
public void windowClosing(WindowEvent e){
System.exit(0);
}
});
aFrame.add(applet, BorderLayout.CENTER);
aFrame.setSize(300,100);
applet.init();
applet.start();
aFrame.setVisible(true);
}
} ///:~
```
`Suspendable`中的`suspended`(已挂起)标志用于开关“挂起”或者“暂停”状态。为挂起一个线程,只需调用`fauxSuspend(`)将标志设为`true`(真)即可。对标志状态的侦测是在`run()`内进行的。就象本章早些时候提到的那样,`wait()`必须设为“同步”(`synchronized`),使其能够使用对象锁。在`fauxResume()`中,`suspended`标志被设为`false`(假),并调用`notify()`——由于这会在一个“同步”从句中唤醒`wait()`,所以`fauxResume()`方法也必须同步,使其能在调用`notify()`之前取得对象锁(这样一来,对象锁可由要唤醍的那个`wait()`使用)。如果遵照本程序展示的样式,可以避免使用`wait()`和`notify()`。
`Thread`的`destroy()`方法根本没有实现;它类似一个根本不能恢复的`suspend()`,所以会发生与`suspend()`一样的死锁问题。然而,这一方法没有得到明确的“反对”,也许会在Java以后的版本(1.2版以后)实现,用于一些可以承受死锁危险的特殊场合。
大家可能会奇怪当初为什么要实现这些现在又被“反对”的方法。之所以会出现这种情况,大概是由于Sun公司主要让技术人员来决定对语言的改动,而不是那些市场销售人员。通常,技术人员比搞销售的更能理解语言的实质。当初犯下了错误以后,也能较为理智地正视它们。这意味着Java能够继续进步,即便这使Java程序员多少感到有些不便。就我自己来说,宁愿面对这些不便之处,也不愿看到语言停滞不前。
- 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 推荐读物