2009年7月23日 星期四

What is “this”?

What is “this”?這是許多物件導向程式語言初學者的疑問。
在許多物件導向語言中都存在著「this」關鍵字,舉凡Java、C++、C#都有這樣一個謎一般的關鍵字,在VB.Net與Ruby中則是以「self」之名存在著。某些非物件導向的程式語言如Javascript,甚至也提供「this」來模擬物件導向的特性。
究竟「this」是天使的化身,還是地獄的使者?是什麼樣的力量促使他在物件導向的世界裡屹立不搖?其實,它沒那麼偉大,充其量只是個「第一人稱代名詞」。
在解釋它存在的必要之前,我們必須先為一個人生難題找出解答:如何在不說「我」的情況下,用合理的文法表達「教授把我當了」?如果你說「教授把XXX當了」,這樣的句子很奇怪,像是說另一個和你同名同姓還被你爆料的可憐同學。如果你說「教授當了」,請問教授到底當了誰?如果你說「被教授當了」,聽起來合理,但這其實是把「我」省略的慣用句型,文法上不合理,因為缺乏主詞。
不知道有沒有人可以找出答案,如果有請留言告訴我。無論如何,代名詞不是我的重點,我只是想證實第一人稱代名詞的必要性:若沒有第一人稱代名詞,我們很難用語言表達自己的存在,也很難做第一人稱敘述。
在物件導向的世界裡也是一樣的。由於一個類別可以產生許多實體,就必須用一個代名詞來讓個別實體表達「我」,以區分和其他實體的不同。一個類別的實體就是用「this」表達「我」。

例如以下程式碼:
class Student{
private int score;
Student(int s){
score = s; //A
}
public void say(){
if( this.score > 59) // B
System.out.println("教授讓我過了!");
else
System.out.println("教授把我當了!");
}
public static void main(String[] arg){
Student 好學生 = new Student(80);
Student 混學生 = new Student(40);
好學生.say();
混學生.say();
}
}

在main中,產生兩個Student類別的實體,兩個實體say的結果不一樣,因為this.score的值不一樣。這樣就可以看出,每個new出來的實體都有自己的this,代表各自的實體。

※B的this是可以省略的,因為是在同一個類別,Java編譯器會在編譯時自動加入this。如A的score也可以改為this.score。

其實看下圖就一目了然:



main方法的Stack Frame中,「好學生」與「混學生」參照分別參考到不同的Student實體,兩個Student實體的this參照也分別參考到自己從屬的實體。如此一來,每個實體就可以用this表達「我」。例如,我的score就是this.score。
那麼你可能會有個疑惑:難道「this」存在的意義只是為了表達像this.score這種可以省略的語法嗎?「this」真正的好處在於,實體可以將自己傳遞給其他實體。

為了解釋這點,我們定義一個Professor類別,並在Student類別裡加入getScore方法,如下:
class Professor{
public void grade(Student student){
student.score = (int)(Math.random()*101);
}
}
class Student{
...
Professor 叫獸 = new Professor();
public int getScore(){
叫獸.grade(this); // C
return score;
}

public static void main(String[] arg){
Student 羔羊 = new Student(0);
羔羊.getScore();
羔羊.say();
}
}

我們先產生一個Student的實體「羔羊」,並呼叫getScore方法。呼叫時,羔羊的實體就會傳入叫獸.grade()方法,並設定score。C的部分,就是Student實體將自身傳入叫獸.grade()方法的部分。其實這行程式碼,翻成中文就是「叫獸給我打分數。」「this」就是「我」!

2009年7月8日 星期三

執行緒同步化之時間怪獸

昨天終於把專題Server端的Database Connection Pool改好了。原本以為是Thread之間的Race condition,造成資料庫記錄取得之前,連線就被回收連線的Thread給close掉。後來才發現是自己沒把 java.sql.Connection的autoCommit設為false,造成Connection在還沒有被commit的狀態下就被放回 Connection Pool裡,時間一到就帶著沒送出的資料一起被回收。問題根本不在Thread的同步,而是我幾乎忘記有autoCommit這東西。
不過,問題改好之後,發現更大的問題:速度超慢。更離譜的是,使用單執行緒的方式執行,竟然還比有快取的多執行緒池快10倍以上。昨天晚上看著 Profiler的分析到凌晨4點,仍然找不出問題在哪,只看到執行緒wait的頻率很高。白天上課一直恍神,完全無法理解老師在上什麼,不過可能也因為恍神太多,讓我在走回宿舍的路上精神飽滿,聽著Claudio Abbado指揮Lucerne Festival Orchestra演出的Mahler交響曲第二號,靈機一動想到了一個可能性:「會不會有人拿著互斥鎖,做一些很耗時間的工作?」回到宿舍看code,果如所料!

試比較下列這兩段程式碼,你比較喜歡哪一種寫法?

Code 1.
synchronized(this){
if(queue.isEmpty()){
if(connectionCount.get()< maxConnectionCount){
conn = new
DBConnection(this,getRawConnection());

connectionCount.incrementAndGet();
}
}
else{...}
}
Code 2.
synchronized(this){
if(queue.isEmpty()){
if(connectionCount.get()< maxConnectionCount){
newConn = true;
connectionCount.incrementAndGet();
}
}
else{...}
}

if(newConn)
conn = new DBConnection(this,getRawConnection());
一般而言,多數人都會選Code 1.,因為兩段程式碼功能一樣,Code1卻顯得比較簡潔直觀。然而,Code1就是我一開始寫的,很花時間的程式碼。

Code2和Code1最大的差別在於Code 2把 conn = new DBConnection(this,getRawConnection()); 從同步區塊取出。這麼做會讓效能增加數倍的原因是getRawConnection()操作太花時間。如果放在同步區塊,代表一次只有1個執行緒能夠呼叫這個方法。這個方法一次得花10秒鐘的時間去和遠端資料庫連繫,如果一次只有1個執行緒呼叫,則20個執行緒至少得花200秒。Code2的getRawConnection()呼叫因為是在同步區塊之外,能夠讓多個執行緒同時取用,就沒有這種問題。
這次的教訓告訴我們,可以放在同步區塊的程式碼越少越好。至少,非必要的話,不要把怪獸放進去。

2009年7月7日 星期二

Digital Sun

今天終於把事情忙完,心血來潮就把上次的Digital Chaos拿來修改,想說改成動態的形式。後來想到不錯的點子,讓圖的半徑交互遞增遞減,動態畫出一張類似太陽的圖形,姑且就稱它為Digital Sun吧。


這是截圖,所以沒有動畫。在Processing上執行會是圓心到圓周的文字交替增加,形成中心越來越密集的太陽。

後來又加上一個細微旋轉,使圖形變成颶風:



程式碼:
int lineSize = 400;
int wide = 5;
int hei = 8;
int fontSize = 16;
int shadow_shift = 2; //文字殘影偏移量
int amount = 50;
int radius = amount;
float angel = 0; // 一行的角度偏移量,設成0.3會變颶風。
PFont font;
boolean sw = true;
void setup(){
size(800,800);
font = createFont("Geogreia",fontSize);
textFont(font);
background(0);
}
void draw(){
if(radius == -amount)
sw = true;
if(radius == amount)
sw = false;
radius += sw? 2:-2;
translate(width/2,height/2);
for(int i = 1 ; i <= 10*abs(radius); i+=5){
int rand = (int)random(2);
int num = (int)random(2);
int shift = (int)random(8) * (i/lineSize >=1 ? -1:1);
int x = i%lineSize+i/100*wide + shift;
int y = (i/lineSize)*hei+ shift;
float trans = 160+95*(shift/7);
fill(255,trans);
rotate((rand == 0? -shift:shift));
rotate(angel);
text(String.valueOf(num), x , y);
if(shadow_shift != 0){
fill(255,trans*0.5);
text(String.valueOf(num), x+shadow_shift,y+shadow_shift);
}
textFont(font,fontSize+(rand == 0 ? -shift:shift));
}
translate(width/-2,height/-2);
}
後來仔細想想,也許它該叫「眾妙之門」。

「無,為天地之始;有,為萬物之母。故常無,欲以觀其妙;常有,欲以觀其徼。此兩者,同出而異名,同謂之玄,玄之又玄,眾妙之門。」-《道德經》

2009年7月1日 星期三

物件繼承的物件觀點-轉型篇

這篇文章是物件繼承的物件觀點一文的續集,將用物件的功能觀點圖來說明物件的強制轉型何時會成功。
首先,我們先釐清一些有關於物件轉型的概念。
在Java中,所有子類別的物件不需要強制轉型就能夠隱含轉換為父類別型態。因此以下程式碼皆能夠通過編譯,也能正常執行。

女人 欣郁 = new 女人();
人類 路人甲 = 欣郁;

人類 阿呆 = new 男人();



2009年6月30日 星期二

物件繼承的物件觀點

「欣郁是個人。」
「欣郁生了孩子。」
欣郁生了個孩子,表示欣郁是女人,會生孩子很正常。即使這兩句話並沒有提到欣郁是個女人,正常人也可以藉由常理推敲出最有可能的結論。但是,如果不加入過去的經驗,純粹從以上兩句話來推敲,則會得到「人會生孩子」這種以偏概全的結論。正常人會從常識中選擇合理的解釋,但對沒有常識的程式語言編譯器而言,這種結論是上述兩句話的正解。這就是自然語言與程式語言之間的鴻溝:有些東西你認為是常識,編譯器卻不懂你的意思。
這篇文章要談的是,你如何正確地將上述兩句話依照物件繼承的類別觀點一文中的類別圖規格轉換成Java語言。
如果我們直接將上述兩句話逐句翻譯成Java程式碼:

「欣郁是個人。」 → 人類 欣郁 = new 人類();
「欣郁生了孩子。」 →
人類 孩子 = 欣郁.生產();

這兩行程式碼大有問題!
首先,我們並沒有說清楚欣郁究竟是男人還是女人。如果欣郁是男人,那麼欣郁.生產()就不合理。因此我們必須把程式碼改為:

人類 欣郁 = new 女人();
人類 孩子 = 欣郁.生產();

這段程式碼看起來較合理,起碼我們已經說明了欣郁是個女人的事實。然而,這樣的程式碼仍然無法編譯成功!
大家可能認為現在欣郁「事實上」是個女人,因此欣郁.生產()理應合法,那為什麼仍然無法編譯通過?理由是,當你把「欣郁」變數宣告成「人類」型態時,我們就把欣郁的「女人」的特性忽略掉了。這就好像我們說「人類是一種動物,所以人類會覓食」時,是把人類一般化成動物,而忽略掉人類獨有的特性(例如交談)。反之,我們不會說「人類是一種動物,所以人類會交談」因為這句話代表「動物會交談,而人類是一種動物,所以人類會交談」這樣的邏輯當然是錯誤的。同樣的道理,「女人會生產」並不代表人類都會生產。現在如果把欣郁一般化成人類看待,則我們就不能去看她獨有的「生產」功能,只能看到人類共有的「交談」功能。人類的定義裡並沒有生產的功能,而欣郁是人類,欣郁.生產()當然不合理。要讓這段程式碼能夠成功編譯,我們必須把欣郁當成女人看待,因此修改成:

女人 欣郁 = new 女人();
人類 孩子 = 欣郁.生產();

這樣的程式碼就合法了。
要理解上面的解釋或許很費腦筋,畢竟不是每個人的邏輯都相當清楚,幸好我們可以使用物件繼承的類別觀點一文中的功能觀點來清楚解釋每一個步驟。

new 人類();

可以用下圖表示:

※表示物件的功能觀點圖為了與表示類別的功能觀點圖作區分,使用較具立體感的顏色來表達「照類別規格產生的實體物件」的效果。
※本文將不會去區別參照與指標,因「指標」講起來比較傳神。
人類 欣郁 = new 人類();

則可以表達為「欣郁指標」指向「人類物件」:

現在就可以清楚看到欣郁.生產()是不合法的,因為圖中的人類物件根本沒有「生產」功能。

那麼,如果是這行程式碼,該怎麼解釋呢?

人類 欣郁 = new 女人();

如下圖:



※在此不管JVM內是否這麼實作,先從邏輯的角度剖析。

new 女人()產生的雖是女人物件,人類型態的欣郁指標卻是指向女人物件中屬於人類的部分,欣郁.生產()也就不合法,因為指標所指向的物件仍然不具有「生產」功能。
根據上述畫圖的原則,很自然可以推出下面這行程式碼代表的意義:

女人 欣郁 = new 女人();



由於欣郁指標現在是女人型態,指向女人物件的整個區塊,欣郁.生產()就變得合法。
同樣的道理也能套用在物件的轉型上,將在下篇文章說明。

2009年6月28日 星期日

物件繼承的類別觀點

從物件導向的術語來看,物件繼承是一種 「is a」的關係,但我個人比較偏好解釋成「is a kind of」,也就是「是一種」的關係,例如,男人是一種人類;女人是一種人類;人類是一種動物。這樣的形容比較精確,否則很容易和物件混淆。例如我們說,「欣郁是個人」,並不是說欣郁是人的子類別,而是說欣郁是人的一個物件,因此有必要釐清語意上的模糊。
將「男人是一種人類」、「女人是一種人類」、「人類是一種動物」的關係與特性以UML類別圖描述,則如下:


從類別圖的表示中,我們可以很清楚看見類別的階層關係。

一般而言,我們會很直觀的把上述類別圖轉換為如下的定義域圖,將每一個類別的定義視為一個集合。
但是我得告訴你一個壞消息:從這種角度思考,對物件導向概念的釐清沒什麼幫助。原因是:你最多只能從這張圖看出男人與女人處於類別階層的何種位階。物件導向著重的是類別所提供的操作,因此你最好將你的角度顛倒過來,像下圖一樣改為從功能性的角度去思考每個類別提供什麼樣的操作。


從這張圖你可以看見,男人與女人的功能裡包含著男人或女人特有的功能,加上人類及動物的功能。這樣你就不會期望男人與女人在交談上必須有一樣的表現,因為雖然交談是「人類」類別提供的功能,現在男人與女人都有自己的「人類」功能區塊,雖然都有「人類」的功能,卻可以提供不同的實作。這種Overriding(覆載)的概念,從功能性的角度才看得出來。

Dependency Injection

Dependency Injection(依賴注入,簡稱DI)是物件導向技術中常被用來降低模組間耦合度的做法,Martin Fowler首先在他的Inversion of Control Containers and the Dependency Injection pattern一文中使用了這個詞,並定義了三種型式的DI,分別是type 1:Interface Injection、type 2:Setter Injection及type 3:Constructor Injection,後來又由picocontainer定義了更多種類的DI。那些DI,其實我也不知道詳細情形為何,也懶得知道。無論如何,DI的用途就是「將模組之間的相依性從程式實作中抽離」。這樣說或許很抽象,但我們生活中其實充滿DI的影子。例如,每個MP3 Player一定需要電池才能運作,有些MP3的電池是內建的,無法更換;有些使用3號或4號鹼性電池,替換很方便。使用內建電池的MP3 Player,由於電池相依於MP3 Player的內部實作,如果之後電池壞掉,就只能買一台新的。而使用標準電池的MP3 Player,即使電池壞掉,只要到7-11買一副新的電池,馬上就能夠使用。同樣的道理也適用於軟體設計:萬一某天發現軟體中的一個類別有bug,究竟是要整個軟體都改過,還是只需要改有bug的類別?這樣我們就能歸納出一個結論:類別和MP3Player的電池一樣,最好能夠想換就換。廢話不多說,我們直接寫個Java MP3Player程式來看看。
class MP3Player{
private NormalBattery battery = new NormalBattery();
public void play(){
battery.usePower();
}
public int getBatteryPower(){
return battery.getPower();
}
public static void main(String... arg){
MP3Player player = new MP3Player();
/** 使用至沒電為止 */
while(player.getBatteryPower()>0){
player.play();
}
}
}
class NormalBattery{
private int power = 100; //預設電力100
public int getPower(){
return power;
}
public void usePower(){
power--; //每使用一次就遞減
}
}
Program A.

以上是代表MP3 Player與電池的類別。注意在MP3Player類別裡,battery欄位的初始值已經指名了要使用的電池是NormalBattery。這意味著如果哪天我們發現NormalBattery類別已經無法滿足我們的需求,想替換的話就必須連MP3Player類別一起更改。現在這麼看可能覺得只是小事,但試想若有10個類別同時使用到NormalBattery,你得花多少時間在這種猴子也能做的瑣碎工作上?
為了不讓身為人類的你退化成猴子,我們還是將上面的程式修改一下,並加入一個Battery介面。
interface Battery{
int getPower();
void usePower();
}
class MP3Player{
private Battery battery;
public MP3Player(Battery battery){
this.battery = battery;
}
public static void main(String... arg){
/** 建構 player時將NormalBattery傳入當參數 */
MP3Player player = new MP3Player(new NormalBattery());
....
}
}
class NormalBattery implements Battery{
....
}
Program B.

這Program B.裡,我們先將電池所共有的功能抽象成一個介面,並讓NormalBattery去實作它。在MP3Player類別裡,則讓battery欄位的值在建構時才由傳入的參數決定。如此一來,使用何種Battery介面的實作的決定權,便從MP3Player類別的實作者手中,轉移到MP3Player使用者的手上。就好比使用者可以隨意更換MP3 Player的電池,而不是取決於MP3 Player的製造商。
但是萬一MP3 Player用到一半,突然想把電池拆掉,又或者,一開始就不想要裝電池,該怎麼辦?如果是用建構子傳入參數的方式,因為沒有提供修改battery的方法,也強制MP3Player的使用者在建構MP3Player時就必須把battery的實體傳入。這種方式在某些情況下顯然不適用,因此我們改採另一種方法:不使用建構子傳參數,而是增加Setter方法。
class MP3Player{
private Battery battery;
public MP3Player(){}
public void setBattery(Battery battery){
this.battery = battery;
}
...
public static void main(String... arg){
MP3Player player = new MP3Player();
/** 設定電池 */
player.setBattery(new NormalBattery());
....
}
}
class NormalBattery implements Battery{
....
}
Program C.

Program C.中把改為不在建構子傳參數,而是去將參數傳入新增的setBattery()方法。這種作法在建構子參數太多的時候相當有用,特別是那些可以省略的參數。

介紹到這裡,看似頗為人滿意,終於可以快樂大結局了。但是,在這個能源耗竭的時代,我們必須思考著如果有一天,MP3 Player的價錢會跟電池差不多,只更換電池就顯得沒有意義,連MP3 Player也必須要可以替換才行。為了因應這種變態的要求,MP3 Player廠商終於決定不再販賣MP3 Player,宣布轉型為MP3 Player出租商,改成以服務的方式收取費用。
為了因應石油不足對產業結構產生的衝擊,物件導向技術也有一套作法,讓你不需要去理會程式碼寫了什麼或怎麼使用,只要改一改訂單,MP3 Player和電池就送到府上供你使用。這種做法必須仰賴介面,將各個實作類別抽象化,因此必須新增 Player介面。
class NormalBattery implements Battery{....}
interface Battery{
....
}
interface Player{
void setBattery(Battery batery);
void play();
int getBatteryPower();
}
class MP3Player implements Player{
private Battery battery;
public MP3Player(){}
public void setBattery(Battery battery){....}
public void play(){....}
public int getBatteryPower(){....}
}
class NormalBattery implements Battery{....}
import java.io.*;
import java.util.*;
class Main{
public static void main(String... arg)throws Exception{
Properties props = new Properties();
/** 從config.txt檔案中讀出屬性 */
props.load(new FileInputStream("config.txt"));
/** 動態載入類別 */
Class playerClass =
Class.forName(props.getProperty("player"));
Class batteryClass =
Class.forName(props.getProperty("battery"));
Player player = (Player)playerClass.newInstance();
Battery battery = (Battery)batteryClass.newInstance();
/** 設定電池 */
player.setBattery(battery);
/** 使用至沒電為止 */
while(player.getBatteryPower()>0){
player.play();
}
}
}

config.txt→
player:MP3Player
battery:NormalBattery
Program D.

由於Program D.要凸顯不需要知道MP3Player內部實作的特性,因此把main方法移到Main類別裡。現在整個主程式完全看不到MP3Player和NormalBattery的蹤影,但程式還是會呼叫MP3Player的setBattery()方法,並將NormalBattery的實體傳入。程式依賴於config.txt的設定,如果想要修改Player或Batter的實作,只需要將實作類別編譯好,接著修改config.txt的內容就可以了。這種方法很明顯要比前面的方法還要有彈性的多,然而如果實作是一些現成的框架提供的介面,在抽離框架時就必須大量修改,代價挺高。由於這種方法有很高的侵入性,大多框架還是使用前面兩種作法。

一開始曾經提到,Martin Fowler曾經定義出三種DI的型態。 Program B.是屬於type 3,Constructor Injection; Program C.是type 2, Setter Injection; Program D.為type 1, Interface Injection。定義得那麼複雜,其實一點也不難。

2009年6月27日 星期六

Digital Chaos

原本Logo右邊那個數碼圖是打算用Fireworks來畫的,直到開始畫才覺得這樣太浪費時間,又不見得好看,就想到用Processing來做,結果效果出乎意料的好。

原始碼:
int lineSize = 200;
int wide = 4;
int hei = 8;
int fontSize = 16;
int shadow_shift = 0;
void setup(){
size(800,800);
PFont font = createFont("Geogreia",fontSize);
textFont(font);
background(0);
translate(width/2,height/2);
for(int i = 1 ; i <= 5000; i+=5){
int rand = (int)random(2);
int num = (int)random(2);
int shift = (int)random(8) * (i/lineSize >=1 ? -1:1);
int x = i%lineSize+i/100*wide + shift;
int y = (i/lineSize)*hei+ shift;
float trans = 160+95*(shift/7);
fill(255,trans);
rotate((rand == 0? -shift:shift));
text(String.valueOf(num), x , y);
fill(255,trans*0.5);
text(String.valueOf(num), x+shadow_shift,y+shadow_shift);
rotate((rand != 0? -shift:shift));
textFont(font,fontSize+(rand == 0 ? -shift:shift));
}
}

言式法則

一直以來,我都想要開一個網誌來存放我那些用完即刪除的小程式,以及那些突然出現的關於程式設計的好點子。這個願望在教授冷酷無情的摧殘與自己無窮無盡的怠惰的雙重夾攻之下,每次都胎死腹中,不了了之。今天在500cc的咖啡因刺激之下,我終於下定決心按下建立網誌的選項。
總而言之,這網誌產生了,而這裡將會是我存放一些程式設計心得以及小作品的地方,可能偶爾會出現一些評論。如果你對文章不甚同意或有疑問,你第一件可以做的事情就是留言,然而喜歡保持網誌整潔的我,不能忍受網誌被留言霸佔,因此你的留言將不會出現在網誌上,系統會直接寄送到我的信箱,到時我會看到。如果你完全不想留言,你可以選擇馬上離開,這是乾脆又容易,同時也最被鼓勵使用的選項。
至於網誌的名字為什麼叫做言式法則?主要原因是作者個人對名字的喜好,次要原因則是這網誌裡的所有文章都是試驗性質的。如果你把言式兩個字合併起來看,就成為了「試」。沒錯,這個網誌就是秉持著試試看的精神創建的,因此若你發現這裡的方法對你無效,或者根本就錯了,請不要大驚小怪,就當和我一起試試看那些鬼東西,然後把問題回報給我知道,不然就乾脆離開,當作沒這回事。
這網誌裡的所有文章都不會標示「版權所有,翻印必究」的噁心標示,但這並不表示這裡的 一字一句都可以任意擷取,而是著作權根本是常識,如果要轉載或引用請一定要註明出處。其餘法律規章請參考Wikipedia之合理使用條文

很好,它終於誕生了。