# 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 推薦讀物