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 男人();