Thursday, April 11, 2013

[JAVA] Practical Java Programming Language Guide-第六章 類別與介面

Practical Java Programming Language Guide-第六章 類別與介面

這個章節有java基本觀念的應該都熟到不行再熟了
我簡單整理一下覺得不動要的就自動跳過了

case 59:運用insterface支持多重繼承
java其實是可以用多重繼承的,只要使用interface就可以達到類似效果
這點在重構(Refacorting)設計模式(Design Patterns)都有相關手法

interface A{...}
class B implements A{}

implements這個關鍵字可以看作extends的一種變體,他會讓B成為A的子類別
而interface則可以看作class一種變體

A a=new B();//這是可以的


case 60:避免interface中的函式衝突

interface A{
int f();
}
interface B{
void f();
}

class C implements A,B{

}

上述例子是編譯不過的
但是可以用回傳一致化或是改名子來解決

interface A{
int f();
}
interface B{
int f();
}

class C implements A,B{

}

如果無法修改原碼的時候,可以用繼承interface或是用adapter這個Desing Patterns解決

case 61:如需提供部分實作(partial implementation)請使用abstract classes
interface強迫必須實作所有他放出來的函式,但是有時候不希望每個介面都實作
就可以使用abstract class

abstract class Foo{
abstract void f1();//有abstract修飾 一定得實作複寫
void f2(){...}//可以不用複寫
}


case 62:區分interface,abstract class和concrete class



一種契約(contract)表示,不代實作
噹想使用單一或多個interface繼承,或為了確定maker interface
public函式和public static final常數
不允許實作
特性

interfaceabstract classconcrete class
描述
一種契約(contract)表示,帶有部分實作一種具象實作,經常是某個interface或abstract class的實作
使用時機
當打算提供部分實作當打算提供完整的具象實作
內容限制
無任何限制無任何限制
實作
允許部分實作要求完全實作








總結

支援多重繼承支援抽象函式允許實作允許創劍實體允許部分實作
interfaceYYaNNN
abstract classNYYNY
concrete classNNYYN

a:interface所有函式都暗自宣告成abstract

case 63:審慎定義和實作immutable classes
有些類別希望生存期間不得修改其值,這種類別一旦件夠好就不再變化
他有以下規範

  1. 將class中所有資料宣告成private

  2. 只提供取值(getter)函式而不提供設值(setter)函式

  3. 宣告class為final

  4. 從獲取器回傳reference to mutable object之前先clone那些物件

  5. 將傳遞給建構式之reference to mutable object先clone一份

  6. 在建構式之中就設定好所有資料

舉個例子

final class PinNumber{//使用final防止繼承
private StringBuffer owner;
orivate int acc;
PinNumber(StringBuffer owe,int ac){
owner=owe;
acc=ac;
//物件所有值在建構時就設定好
}
//只提供取值函式
public StringBuffer getOwner{//getter
return owner
}
....
}


case 64:欲傳遞或接收mutable object(可變物件)之object reference時請實施clone()
這是延續上一個case,為了避免因為getter傳出個物件讓外部有機會修其物件,應該替他實施clone

PinNumber p=new PinNumber(new StringBuffer("Tom"),10000);
StringBuffer owe=p.getOwner();
owe.append(" cat");//不可變被破壞

修改

public StringBuffer getOwner{//getter
return owner.clone();
}

clone又分成shallow cloning(淺層clone 只copy reference,依然共享實體)跟deep cloning(深層clone ,連reference的實體也copy一份)

case 65:使用ingeritance或delegation來定義immutable object
這個case探討三個定義immutable class的方法

  1. immutable inteface(不可變界面)

  2. 公共界面或基礎類別(common interface or base class)

  3. immutable delegation class


immutable inteface


提供一個只能取值的界面

interface Circle{
public double radius();
}
class MyCircle implements Circle{
double radius;
private MyCircle();
MyCircle createCircle(){
return new MyCircle();
}
void setRadius(double r){radius=r;}
}

類似這樣的設計,把建構式藏起來,用一個factor取代

Circle c=MyCircle.createCircle();
c.setRadius(100);

像這樣的程式就會編譯不通過,因為Circle沒有提供設值函式,這樣一來user就不能修改c
但是他是有破綻的

Circle c=MyCircle.createCircle();
((MyCircle)c).setRadius(100);

諸如此類,透過轉型就能破解

common interface or base class


因此有了進階的common interface or base class出現補救
他有三個要素

  1. 一個interface或abstract的父類別,宣告可被子類別(derived class)共用的immutable函式

  2. 一個子類別提供mutable函式實作

  3. 另一個子類別提供immutable函式實作



interface Circle{
public double radius();
}
class IMCircle implements Circle{
double radius;
private IMCircle();
IMCircle createCircle(){
return new IMCircle();
}
}
class MCircle implements Circle{
double radius;
private MCircle();
MCircle createCircle(){
return new MCircle();
}
void setRadius(double r){}
}


immutable delegation class


利用委託的方式讓物建不可變


class Circle{
double radius;
public Circle();
void setRadius(double r){}
double getRadius(){};
}
class ImmutableCircle{
private Circle c;
ImmutableCircle(){
c=new Circle();
}
double getRadius(){
return c.getRadius();
}
}

以下提出總結





技術優點缺點
immutable inteface實作簡單,無效率代價有破綻 可破壞
common interface or base class沒破綻,將immutable和mutable清楚分開需實作更多class跟繼承體
immutable delegation class無破綻,當無法修改源碼時可用效率較低


case 66:實作clone()記得呼叫super.clone()
要支援clone的class必須呼叫java.lang.Object的clone()
呼叫super.clone()可以確保java.lang.Object的clone()會被呼叫
java.lang.Object的clone()會創建一個正確型別的新物件並執行shallow clone
即使要實作deep clone也該呼叫
考慮下面一段程式

class House implements Cloneable
{

@Override
protected Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return new House();
}
}

class MyHouse extends House{

}
...
public static void main(String[] args) {
// TODO Auto-generated method stub
MyHouse t1=new MyHouse();
try {
MyHouse t2=(MyHouse)t1.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

他沒有呼叫Object的clone
會出現如下錯誤訊息
Exception in thread "main" java.lang.ClassCastException: House cannot be cast to MyHouse
因為此時clone回傳的是House物件而非MyHouse

但是我只需要將程式碼小小修改

class House implements Cloneable
{

@Override
protected Object clone() throws CloneNotSupportedException {
// TODO Auto-generated method stub
return super.clone();
}
}
class MyHouse extends House{

}
...
public static void main(String[] args) {
// TODO Auto-generated method stub
MyHouse t1=new MyHouse();
try {
MyHouse t2=(MyHouse)t1.clone();
} catch (CloneNotSupportedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}

就不會有這問題了

case 67:別只依賴finalize()清理non-memory(記憶體之外)的資源
簡單來說像是socket或File等東西都應該手動去清除他,JVM不會替我們回收

class MySocket{
Socket ss;
....
void close(){//如果class有物件是會暫用non-memory資源,就該提供清除的函式
ss.close();
}

}

....
MySocket ss=new MySocket();
...
ss.close();


case 68:在建構室內呼叫non-final函式的時候要小心
當使用函式去初始化資料的時候,要小心多型帶來的例外

class Base{
private int val;
Base(){
val=lookup();
}
int lookup(){return 5;}
int values(){return val;}

}
class Dev extends Base{
private int num=10;
int lookup(){return num;}

}
...
Dev d=new Dev();
System.out.println(d.values());
//印出結果:0

為什麼是0呢,原來Dev呼叫的是int lookup(){return num;}而非int lookup(){return 5;}
但此時Dev的instance都還沒初始化完成,num此時是0而不是10

[JAVA] Practical Java Programming Language Guide-第五章 多緒

Practical Java Programming Language Guide-第五章 多緒

章節開頭就來一句

如果只追求容易和速度,工作就失卻了恆久的穩固或毫釐無誤所帶來的美感
For easy and speed in doing a thing do not give the work lasting solidity or exactness of beauty.

這提醒了我們,在追求效率的同時,不該忽略其他面的事物,比如說捨棄設計而帶來的設計債(Desing Dept)
還記得基峯出過一套遊戲程式設計精華系列,裡面就有強調:不要寫死任何程式

case 46:面對instance函式,synchronized鎖定的是物件(Object)而非函式(Method)或程式碼(Code)
有關同步行程方面知識有念過恐龍本(作業系統)的應該都非常清楚了,我就簡單帶過
synchronized可以替我們實現鎖(lock)的機制,互斥鎖確保了程式的正確性
書中提了一段程式碼

class Test{
public synchronized void method1(){}//修飾method

public void method2(){
synchronized(this){}修飾object reference
}
public void method3(Object obj){
synchronized(obj){}修飾object reference
}
}

method1跟method2都是鎖定同一個物件,都是對this進行同步控制,在這邊的lock就是this
而method3則是把參數obj當作lock,要注意一點,lock是指reference的memory本身而非變數
這在另一本『Effective Java Programming Language Guide』有提出相關議題


case 47:弄清楚synchronized static函式與synchronized instance函式之間的差異
這邊探討synchronized在static跟non-static函式上有哪些不同



class Turn{
public synchronized static void f1(){
while(true)
{
System.out.println("f1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void f2(){
synchronized (Turn.class) {
while(true)
{
System.out.println("f2");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}
}

在這邊f1跟f2是爭取同一個lock,都是想要取得Turn這個class
主程式

Thread t1=new Thread(new Runnable(){

@Override
public void run() {
Turn.f1();

}

});
Thread t2=new Thread(new Runnable(){

@Override
public void run() {
Turn t=new Turn();
t.f2();

}

});
t1.start();
t2.start();

印出的結果是

f1
f1
f1
...

但是如果將其中一個程式改成聽取instance的lock如下


class Turn{
public synchronized static void f1(){
while(true)
{
System.out.println("f1");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public void f2(){
synchronized (this) {
while(true)
{
System.out.println("f2");
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}

}
}

結果就會變成

f1
f2
f2
f1
f2
f1
f1
f2
...

在第一個case裡面t2監聽的跟t1一樣都是Turn這個Class
但是到了第二個case,t2變成監聽Turn t=new Turn()這個lock
在類別與實體的同步控制,必須要格外小心
修正方法

class Turn{
byte[] lock=new lock[0];
public static void f1(Turn t)
{
synchronized (t.lock){}
}
public void f2()
{
synchronized (lock){}
}
}

讓要同步控制的函式去監聽同一個物件,置於為何用一個0byte的陣列
是因為這樣最節省空間並且經濟實惠
創造一個byte為0的array並不需要呼叫建構式(比方說用Object lock=new Object();)
而且byte arrays在JVM比int arrays有更緊湊的表述

case 48:以private的accessor取代/public/protected
當我們用synchronized進行同步控制的時候,必須保護好lock,必面被惡意攻擊造成程式碼漏洞


class Foo{
byte[] lock=new lock[0];
void f(){
synchronized(lock){...}
}

}

類似上面的程式碼,如果用戶端不安好心
使用以下攻擊手法

Foo foo=new Foo();
foo.f();//沒事
foo.lock=null;
foo.f();//丟出NullPointException例外

諸如此類,對於lock應該用privae修飾好好保護


class Foo{
private byte[] lock=new lock[0];
void f(){
synchronized(lock){...}
}

}


case 49:避免無謂的同步控制
這個case指出只有在真正需要的時候使用同步控制
一方面提升效能一方面避免死(deadlock)

case 50:取用共享變數時使用synchronized或volatile
Java允許每個執行緒都有變數的私有專屬複本(private working copy),但是有時候會希望多個執行緒之間共用變數,為了保持交易的不可分割(atomic)必須使用synchronized或volatile
比較synchronized和volatile




技術優點缺點
synchronized存取lock時進行私有專屬複本與主記憶體正本一致化消除了並行(con-currency)的可能性
volatile允許並行(con-currency)每次取用變數就進行一致化

這邊所謂並行,是只兩個執行緒可否同時執行,因為使用synchronized,另一個執行緒就非得等到變數被釋放後才能執行,但是volatile沒有這方面的困擾,他會維護變數的一致性而又步去鎖定他
舉個例子

class VolTest extends Thread{
static int t=0;

@Override
public void run() {
while(true)
{
t++;
System.out.println(t);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}

主程式

VolTest f1=new VolTest();
VolTest f2=new VolTest();
f1.start();
f2.start();
...
//印出結果
1
2
3
3
4
4
5
6
8
7//直接跳過8
9
...

變數的值不一致,但是我只有小小修改一下
使用synchronized

class VolTest extends Thread{
static Integer t=0;

@Override
public void run() {
while(true)
{
synchronized(t){
t++;
System.out.println(t);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
...
//印出結果
1
2
3
4
5
6
7
8
...


變數是一致了,但是也造成了執行緒執行時候的阻塞

使用volatile

class VolTest extends Thread{
volatile static int t=0;

@Override
public void run() {
while(true)
{
t++;
System.out.println(t);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
...
//印出結果
1
2
3
4
5
5
6
7
8
8
9
9
10
11
...

變數一致了,而且也沒有被鎖定住,執行緒沒有阻塞
這邊沒有說誰比較好,規則是死的人是活的synchronized可以讓行程序列化而不並行
但是他只能鎖定在Heap上的記憶體也就是一個物件(上面例子必須用Integer)
而volatile雖然沒辦法讓行程序列化,但是他可以用於原生變數(上面例子可使用int)

case 51:在單一操作中(single operation)鎖定所有用到的物件
簡單來說,就是希望只要在執行緒用到的物件,就該把他鎖起來

int sum(int[] a1,int [] a2)
{
synchronized(a1){
synchronized(a2){//行程中會用到a1跟a2相加,兩個都該鎖起來
//do something
}
}
}


case 52:以固定而且全域性順序取得多個lock避免死結(deadlock)
學過OS應該都知道,當A等B而B也在等A的時候,就會造成死結
而這個case就是說要避免這種可能
舉個例子

int sum(int[] a1,int [] a2)
{
synchronized(a1){
synchronized(a2){//行程中會用到a1跟a2相加,兩個都該鎖起來
//do something
}
}
}

如果主程式這樣寫

sum(a1,a2);//thread1
...
sum(a2,a1);//thread2

這類程式碼,因為thread1是先取得a1在去取a2,而thread2正好相反先去取得a2才去取a1
假設thread1先取得a1,但是執行權被thread2拿去而取得a2,但是a1被thread1拿走了他就阻塞
而執行回到thread1,他想取a2但是a2被thread2拿走了,此時就造成了死結(deadlock)
thread1擁有a1等thread2的a2,而thread2擁有a2等thread1的a1 誰也動不了
一個簡單的解決方法就是把lock物件化

ArrayLock{
static int num=0;
int order;
int[] arr;
ArrayLock(int[] a){
arr=a;
synchronized(ArrayLock.class){//num共用,必須保護
num++;
order=num;
}
}
}

用類似這種方法替lock排序
這樣程式碼只要改程如下就可以避免死結

int sum(ArrayLock a1,ArrayLock a2)
{
ArrayLock first,second;

//先將lock排序
if(a1.order>a2.order)
{ first=a1;second=a2;}
else
{
first=a2;second=a1;
}

//如此一來鎖定就不成問題
synchronized(first){
synchronized(second){
//do something
}
}
}


case 53:優先使用notifyAll()而非notofy()
notofy()會喚醒現在在等lock的執行緒,但是一次只會喚醒一個,而不確定是哪一個
如果是雙執行緒還可以用,但是多執行緒還是得用notifyAll()

case 54:針對notifyAll和wait使用旋鎖(spin lock)
case 55:使用notifyAll和wait取代輪詢迴圈(polling loops)
上面兩個case請參照
Spin lock跟polling loop 

case 56:不要對locked object(上鎖物件)之object reference重新賦值
簡單來說,就是不要做下面的動作

...
synchronized(lock){
lock=new Lock();//不應該
}

這會造成lock無限產生而無法達到同步化的效果,因為lock的對象是instance記憶體
從新參照會讓lock變成孤兒

case 57:不要呼叫stop()或suspend()
這兩個是不安全的函式,目前被java列為不建議使用函式
主要是因為怕stop之後仍有某些東西被等待釋放的執行緒把持
比如有個執行緒正在將資料寫入共享記憶體緩衝區(shared memory buffer),如果此時該執行緒被stop,無法得知他是否已經write完成,那個緩衝區可能將不在處於有效狀態

case 58:透過執行緒之間的協作來中止執行緒
這個case算是替上一個case提出解法
間單來說就是用一個變數控制執行緒的存活


class Work extends Thread{
private volatile boolean stop=false;//使用volatile確保run可以看件stop的變化
...
public void stopWork(){//一個公用函式,提供外界手動停止
stop=true;
}

public void run(){

if(!stop){
//do work
}
}
}

如上,因為stop是由另一個執行緒控制,必須使用前面的機制volatile或是synchronized確保run可以即時正確的知道stop現在的狀態

[JAVA] Practical Java Programming Language Guide-第四章 效率

Practical Java Programming Language Guide-第四章 效率

昨天回宿舍後我開始在想,我打這些文章有什麼意義,只是點出大概不如直接去看書
而我只需要推薦書就好,將裡面的內容打出來是否多此一舉
後來我想過了,既然要做就貫徹到底,這些文章不只是為了分享,更是為了驗證我倒底對看過的書理解了沒,這是個隨手筆記,加深我對書中的內容的印象才是我打出這些文章的目的



這個章節個人認為是這本書裏面最有趣的一章,他提到很多對performanace如何改進的手法,提供當我們要最佳化程式的時候一種思維,我們重構程式其中一個目的,不就是為了讓最佳化更有彈性嗎?


書中提到,要最佳化有幾個手段,像是最佳化用編譯器最佳化bytecode或是最佳化JVM,但是最常用的仍應該是手動最佳化自己的程式碼,程式效率往往被不佳的設計給破壞掉

java要驗證效率最簡單的方法就是去抓時間差,以前我用C#測試演算法的時候也常用這套手法
不過java的System.currentTimeMillis()精準度不是很高,印象中只有40毫秒
C#雖然也差不多但是他可以抓kernel的timegettime等函式來將精準度提升到毫秒甚至微秒
如果要寫FPS遊戲計算FPS的話務必用kernel提供的函式才能正確計算

long begin=System.currentTimeMillis();
//do something
System.out.println(System.currentTimeMillis()-begin);


case 28:先把焦點放在設計、資料結構和演算法身上
這個case一開始用一句話開頭

大約97%的時間裡,我們應該忘掉小幅效率這東西,過早最佳化是一切錯誤根源
We should forget about small efficiencies,about 97% of the time.
Premature optimization is the root of all evil.

主要是說不要為了那一點點不可靠的效率而放棄了良好的設計,這將使程式碼難以閱讀及最佳化
典型情況80~90%的程式執行時間是花在10~20%的程式碼身上(80-20法則)
這點在許多重構或是XP的書也都有提及
高效能程式碼與三個原則息息相關

  1. 良好的設計

  2. 明智的選擇資料結構(data structures)

  3. 明智的選擇演算法(algorithm)


case 29:不要依賴編譯期(Compile-time)最佳化技術
這個case提到,編譯器最佳化的效果有限,比起依賴他不如手動最佳化
舉個例子

void f(){
int a=10;
int b=20;
int arr[20000];
for(int i=0;i<20000 br="" i=""> {
arr[i]=a+b;//效率低
}
}
////////////////
void f(){
int a=10;
int b=20;
int c=a+b;
int arr[20000];
for(int i=0;i<20000 br="" i=""> {
arr[i]=c;//改良
}
}

諸如此類,一點點改變就能提高程式碼效率
java要最佳化bytecode的方法是利用 -o這個參數
javac -o XXX.java
但是書上有提到他帶來的效果極低,而且對大多數的bytecode這選項也無法做到實質的提升,所以也有人提倡捨棄他
最後提到了幾個最佳化程式的選擇

  1. 手工最佳化程式碼

  2. 使用第三方最佳化編譯器(thrid-party optimizing compiler)將原始碼最佳化

  3. 依賴JIT或是Hotspot等執行期最佳化策略


case 30:理解執行期(runtime)程式碼最佳化技術
這篇提到使用JIT來提升執行效率的相關議題,優秀的JIT可以提升效能,但是要啟動JIT也要付出相對的代價

case 31:要進行字串接合,StringBuffer優於String
這個幾乎每本java的書都會提到

String buf="Hello";
buf+=" Tom";

效能比不上

StringBuffer buf=new StringBuffer("Hello");
buf.append(" Tom");

因為String是不可變的,他每次都會產生一個新的字串來當接合的結果,而非擴展原物件
String在做字串接合時,會先new一個StringBuffer 再用他的append,再用toString()回傳接合結果,短短一行程式碼就做了三個步驟,這效率非常恐怖

case 32:將物件的創建成本降到最小
簡單來說,慎用new產生物件
物件創造幾個步驟


  1. 從Heap中配置記憶體,用以存放全部的instance變數及物件的superclass的實作專屬資料(implementation-specific data),包括指向"class and method data"的指標


  2. 將物件的instance變數初始化其相應的預設值


  3. 叫用most derived class(最深層延伸類別)的建構式。建構式第一個要做的就是呼叫superclass的建構式,此過程反覆直到呼叫到java.lanh.Object的建構式為止


  4. 在建構式本體執行之前,所有的instance變數初始值設定跟初始化區段(initialization block)先換得執行,之後才執行建構式本體。

舉個例子


...
long begin=System.currentTimeMillis();
for(int i=0;i<20000000 br="" i="">{
//什麼都不做的for迴圈跑兩億次
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:141毫秒
...
long begin=System.currentTimeMillis();
for(int i=0;i<20000000 br="" i="">{
int j=0;
j+=i;
//產生一個local變數跟一個簡單的運算
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:156毫秒

...
class Base{//這是一個什麼都沒有的類別
}
long begin=System.currentTimeMillis();
for(int i=0;i<20000000 br="" i="">{
new Base();//創建一個空類別的for迴圈跑兩億次
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:1516毫秒

這之間的差距非常明顯,而這只是創建一個什麼都沒有的空類別就有如此差距

書中提到以下特徵會增加物件的創建成本

  1. 建構式中有大量程式碼

  2. 類別有數量眾多或是體積龐大的member物件,他的初始化是建構式的一部份

  3. 太深的繼承階層(inheritance hierarchy)


case 33:慎防未用上的物件
小心不要創造無謂甚至會擱置太久的物件,會增加不必要的成本

case 34:將同步化(synchrinization)降至最低
多執行緒的同步化會對效能有很大的傷害,應該盡量避免
因為lock的或取還是釋放都是一個高成本的動作
簡單舉個例子

class Foo{
void run(){};//一個空函式
}
...
Foo f=new Foo();
long begin=System.currentTimeMillis();
for(int i=0;i<20000000 br="" i="">{
f.run();
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:203毫秒
...
class Foo{
synchronized void run(){};//有synchronized修飾的空函式
}
...
Foo f=new Foo();
long begin=System.currentTimeMillis();
for(int i=0;i<20000000 br="" i="">{
f.run();
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:10594毫秒
...

看到沒有!竟然有如此的差距,比建構式還恐怖,多緒的時候要很小心諸如此類陷阱
為了提升效率而使用多緒執行反而降低了執行效率

之後書中有提到一些容器的效能
array比ArrayList快,ArrayList又比Vector快上兩倍,對於容器的選擇應該謹慎

case 35:盡可能使用stack變數
嗯...這個case是說JVM是一種stack-based的虛擬機器,因此取用stack data的時候效率最佳
如果想從constant pool存取實體變數還是static變數的時候效率會沒那麼好
直接來實地測是一下

int var=0;//stack 變數
long begin=System.currentTimeMillis();
for(int i=0;i<20000000 br="" i="">{
var++;
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:204毫秒
...
class VData{
int _var;//instance變數
}
....
long begin=System.currentTimeMillis();
VData var=new VData();
for(int i=0;i<20000000 br="" i="">{
var._var++;
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:641毫秒
...
static int v=0;//靜態變數
long begin=System.currentTimeMillis();
VData var=new VData();
for(int i=0;i<20000000 br="" i="">{
v++;
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:609毫秒


case 36:使用static、final和private修飾讓函式inline
也就是說當函式有static或final、private等修飾的時候,他會變成inline候補
這樣可以提升效率,不過他只是成為候補,如果函式內容太複雜仍然不會inline
這個在重構的Extract Method的時候很好用
實地測試一下

class Foo{
void run(){};//一個空函式
}
...
Foo f=new Foo();
long begin=System.currentTimeMillis();
for(int i=0;i<20000000 br="" i="">{
f.run();
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:203毫秒
...
class Foo{
static void run(){};//加上static
}
...
Foo f=new Foo();
long begin=System.currentTimeMillis();
for(int i=0;i<20000000 br="" i="">{
f.run();
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:141毫秒

就像這樣,程式碼效能獲得提升

case 37:instance變數初使化一次就好
由於建構式是一個高成本的動作,不要讓他做多於的指派
跟C++不同,java再創建物件的時候就會給變數預設值像是,0,false,null等等
除非要透過建構式給予新值,否則不應該在給予重複的預設值
書中舉了兩個簡單的例子

class Foo{
private int count;
private boolean done;
Foo(){
//多此一舉
count=0;
done=false;
}

}


class Foo{
//多此一舉
private int count=0;
private boolean done=false;
}

諸如此類,對於同個變數賦予相同的初始值,因為java自動會給予
不過這點是有盲點的,在標準化編程裡面,有人提倡程式碼行為應該一致
因而不贊成這種論點

class Foo{
private int count;
private ArrayList list;
Foo(){
list=new ArrayList();
}
}

如上面例子,既然list在Constructor裡面作初始化動作,為何cout不做
諸如此類的程式碼行為不一致,也有人提倡不該為了讓那一點幾乎看不見的效率破壞了程式碼的可讀性

case 38:使用基本型別(primitive types)使程式碼更快更小
用前面提過的一個例子

int i=5;
Integer j=new Integer(10);


基本型別小巧快速,是很多人的最愛
接下來實地探測一下

static int usePrimitive(int income) //Primitive
{
int i=5;
i+=income;
return i;
}
static int useObject(Integer income) //Object
{
int i=5;
i+=income.intValue();
return i;
}
...
long begin=System.currentTimeMillis();
for(int i=0;i<200000000 br="" i="">{
usePrimitive(1);
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:156毫秒
...
long begin=System.currentTimeMillis();
for(int i=0;i<200000000 br="" i="">{
useObject(1);
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:422毫秒

但是同上一個case,這之中會有讓程式碼難以重構的可能性
太過依賴primitive也不好,在重構(Refactoring)中提到,這犯了基本型別偏執(Primitive Obsession)這個壞味道

case 39:不要使用Enumeration或Iterator來尋訪Vector
Vector提供幾種尋訪的方法

  1. Iterator

  2. ListIterator

  3. Enumeration

  4. get()函式

書中提到Enumeration比兩個Iterator快了約12%,get()函式則比其他三個快了29%~34%
這邊就不實地測試了

case 40:使用System.arraycopy()來複製arrays
System.arraycopy()是以原生函式(native method)實作,他的效率會比一般for迴圈指定更快
測試內容

使用for迴圈

int [] arr1=new int[10];
int [] arr2=new int[10];
long begin=System.currentTimeMillis();
for(int i=0;i<200000000 br="" i="">{
for(int j=0;j {
arr2[j]=arr1[j];//size為10空陣列的copy,使用for迴圈
}
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:9672毫秒
...
int [] arr1=new int[20];
int [] arr2=new int[20];
long begin=System.currentTimeMillis();
for(int i=0;i<200000000 br="" i="">{
for(int j=0;j {
arr2[j]=arr1[j];//size為20空陣列的copy,使用for迴圈
}
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:17344毫秒
...
int [] arr1=new int[50];
int [] arr2=new int[50];
long begin=System.currentTimeMillis();
for(int i=0;i<200000000 br="" i="">{
for(int j=0;j {
arr2[j]=arr1[j];//size為50空陣列的copy,使用for迴圈
}
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:43203毫秒

使用System.arraycopy();

int [] arr1=new int[10];
int [] arr2=new int[10];
long begin=System.currentTimeMillis();
for(int i=0;i<200000000 br="" i="">{
//size為10空陣列的copy,使用System.arraycopy();
System.arraycopy(arr1, 0, arr2, 0, arr1.length);
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:11188毫秒
...
int [] arr1=new int[20];
int [] arr2=new int[20];
long begin=System.currentTimeMillis();
for(int i=0;i<200000000 br="" i="">{
//size為20空陣列的copy,使用System.arraycopy();
System.arraycopy(arr1, 0, arr2, 0, arr1.length);
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:12453毫秒
...
int [] arr1=new int[50];
int [] arr2=new int[50];
long begin=System.currentTimeMillis();
for(int i=0;i<200000000 br="" i="">{
//size為50空陣列的copy,使用System.arraycopy();
System.arraycopy(arr1, 0, arr2, 0, arr1.length);
}
System.out.println(System.currentTimeMillis()-begin);
//印出結果:15546毫秒

令人訝異的結果,在size為10的時候,使用for迴圈竟然比System.arraycopy()還快
但是隨著資料的增長,System.arraycopy的效率卻遠遠超越for迴圈

這或許意味著在少量的copy時候,可以使用for迴圈,但是到了大量資料copy的時候,for迴圈就變得相當沒有優勢並且非常的耗時間。

筆者我對System.arraycopy相當有印象,以前我寫過一個排版的函式,他必須做字串接合,那段時間研究了很多作法,還去看了StringBuffer跟StringUtil的source code,他裡面就是用System.arraycopy對char 陣列作多次copy來完成字串接合
我把當時寫的原始碼分享給大家看,這是參考各家網路版本跟StringUtil等source code所挑出速度最快的版本

//將字串最大長度固定的函式 不足以空白補齊
private String getLayoutString(String temp,int spaceNum)
{
if(temp==null)
{
return null;
}
int pads=spaceNum-temp.length();
if(pads<=0)
{ //: 字串長度大於指定長度直接回傳
return temp;
}
else
{
return padRe(temp,spaceNum);//spaceNum是要填充的字元
}
}

private char[] buffer = new char[0];
public String pad(String str, char ch, int size){
if(buffer.length < size) buffer = new char[size];
if(size <= str.length()) return str;

System.arraycopy(str.toCharArray(), 0, buffer, 0, str.length());
java.util.Arrays.fill(buffer, str.length(), size, ch);
return new String(buffer);
}


case 41:優先使用array,然後再考慮Vector和ArrayList
主要是在說上面提到的array>ArrayList>Vector
其中Vector效率會差的的原因他的get()函式是synchronized,synchronized會讓函式效率低下

case 42:盡可能reuse物件
當物件非常龐大且非常耗效能的話,創造之後究竟可能共用
像是寫一個遊戲,對Image或是Sound的存取是非常耗時間的一件事,通常會有個pool,創造之後就放進pool讓大家共用

case 43:使用緩式評估(延遲求值,lazy evaluation)
當某件事非常龐大的時候,就讓他晚點執行,或是需要他的時候才執行
這個方法最典型的就是四人當的提出的Design Pattern裡面的Proxy
現在的Web Service的使用大概都是依照這個Pattern去實作出來的
把遠端的呼叫放到真正要取用的時候才去開啟
我簡單模擬一下(我自己想的例子,真實情況應該會更複雜)

class Service{
Socket request;
....
public void Request(String url)
{
if(request==null)request=new Socket(url);
....
}

}

類似這樣,平常就讓他保持null的狀態,真正需要的時候才去產生他

case 44:以手工坊是將程式碼最佳化
上面有提過,編譯器提供的最佳化效果有限,他提出了幾個簡單最佳化的方法
我就直接用程式碼舉例子了

  1. 剔除空白函式(Empty method removal)



    class Test{
    void foo();
    }

    如上,儘管foo()函式空空如也,但是main仍回呼叫他們,將他移除替.class瘦身

  2. 剔除無用程式碼(Dead code removal)



    for(int i=0;i if(i<0 br="" return=""> //do something
    }

    可修改為

    for(int i=0;i //do something
    }

    將永遠不會執行的程式碼刪除,書中提到替上面的for迴圈瘦身後執行效率可提高4%

  3. 削減強度(Strength reduction)


    以效率更高的操作取代昂貴的動作
    由於以前我對這方面都沒去注意,做個記號警惕一下
    舉個例子

    for(int i=0;i{
    a=a+b;
    }

    改成

    for(int i=0;i{
    a+=b;
    }

    書中提到新版本會比就版本快10%,而且可以替bytecode瘦身
    有很多operaror都是不同的動作同樣的效果,但是會有不同的效率

  4. 合併常數(Constant folding)


    這個也蠻重要的,對於一些不變的變數加上final修飾,可以使他最佳化

    static int a=30;
    static int b=60;
    int c=a+b;

    可以改成

    static final int a=30;
    static final int b=60;
    int c=a+b;

    加上final修飾之後就可以讓bytecode得到最佳化

  5. 刪減相同的子運算式(Common subexpression elimination)


    簡單來講就是刪除同樣的運算

    arr[i+1].foo(1);
    arr[i+1].foo(2);
    arr[i+1].foo(3);
    arr[i+1].foo(4);

    可以改成

    int k=i+1;
    arr[k].foo(1);
    arr[k].foo(2);
    arr[k].foo(3);
    arr[k].foo(4);

    不過在Refatoring裡面有提到可以用Query取代暫存變數,因為不用為了這一點雞毛蒜皮的效率犧牲程式碼的可讀性

  6. 展開迴圈(Loop unrolling)


    迴圈小且固定的時候,可以展開以提升效率

    for(int i=0;i<3 arr="" br="" i="">

    可以改成

    arr[0]=10;
    arr[1]=10;
    arr[2]=10;

  7. 簡化代數(Algebraic simplification)


    運算上的簡化

    int x=(f*a)+(f*b)+(f*c);

    改進

    int x=f*(a+b+c);

  8. 移動迴圈內的不變式(Loop invariant code motion)



    int a=10;
    int b=20;
    for(int i=0;i<3 br="" i="">x=a+b;

    可以改成

    int a=10;
    int b=20;
    int c=a+b;
    for(int i=0;i<3 br="" i="">x=c;

    不過同樣的,以重構的想法是盡量去減少暫存變數,不須要為了那一點點效率犧牲了重構的可能性,暫存變數太多會讓重構變的困難


case 45:編譯為原生碼(Compile to native code)
書中提到有些程式設計師相信有必要的話,可以犧牲java的可移植性,直接編譯程原生碼(native binary code)來換取更高的效率。

[JAVA] Practical Java Programming Language Guide-第三章 異常處裡

Practical Java Programming Language Guide-第三章 異常處裡

這個章節介紹了java Exception的一些概念,java很好心的提供了一個完善捕捉例外的方法
不過例外處裡不是萬靈丹,已有人提唱不要依賴他,也有許多相關的重構手法

case 16:認識『異常控制流』
這段沒有特別提到什麼,只是簡單介紹try-catch的流程
真要說的話,就是try可以不接catch直接用finally這段吧

try{
System.out.println(1);
}finally{
System.out.println(2);
}


case 17:絕對不可以輕忽異常
裡面提到異常發生時,我們不該忽略他,以及異常發生時我們可以做些什麼

  1. 捕捉並處裡,防止異常進一步散播(propagate)

  2. 捕捉並再次拋出,這麼一來他會向上傳給呼叫者

  3. 捕捉拋出新異常給呼叫端/li>
  4. 不捕捉這個異常,放任他繼續往上傳,直到main或是遇到try-catch


case 18:千萬不要遮掩異常
這段有提到一個很重要的概念,遮掩異常
考慮下面一段程式

f() throws Exception{
try{
throw new Exception("Try");
}
catch(Exception e){
throw new Exception("catch");
}
finally{
throw new Exception("finally");
}
...
try{
f();
}catch(Exception e)
{
System.out.println(e.getMessage());
}

他會印出finally,為什麼呢?丟出異常後不是後面的程式都不會執行了嗎
來觀察整個流程,當try丟出一個例外後被catch捕捉,如果此時沒有finally,最後收到的就是catch的例外,try的例外被隱藏了,這就是遮掩異常。
而我們有做finally,不論try或catch做了什麼只要進入try-catch區塊finally就一定會執行
所以catch執行完又做了finally,這個範例最後丟出的例外是
new Exception("finally");
在看下面一段程式

f() throws Exception{
try{
return;//或是System.exit(0);
}
catch(Exception e){
throw new Exception("catch");
}
finally{
throw new Exception("finally");
}
...
try{
f();
}catch(Exception e)
{
System.out.println(e.getMessage());
}

他依然會印出finally,為什麼?不是return了,上面有提到就算執行System.exit(0)也一樣
如果今天只有catch沒有finally,那他什麼也不會印出,但是有了finally,不管怎樣也會執行那個區塊,除非停電還是電腦關機一類外力強迫電腦無法執行

因為這是我以前都沒注意到的事情,所以我覺得值得做個重點
『直要進入try-catch區塊,只要有finally無論如何都會執行』
如果要避免遮掩異常,可以用一個List記錄這一路丟出了哪些例外然後再丟出新例外

class ListException extends Exception{
ArrayList list=new ArrayList();
void add(Exception e)
{
list.add(e);
}
}
...
f() throws Exception{
ListException _liste=new ListException();
try{
_liste.add(new Exception("try"));
}
catch(Exception e){
_liste.add(new Exception("catch"));
}
finally{
_liste.add(new Exception("finally"));
throw _liste;//丟出一個例外串列
}


case 19:明察throws子句的缺點
這個case簡單來說就是向上丟出例外,無法保證上面的呼叫者一定會處理他
通常例外有兩個解決方式,自己處理掉或是拋出給呼叫者處裡

case 20:細緻而全面理解throws子句
這個章節提醒要擲出所有的例外,不要有遺漏

void f() throws Exception1{//不好

if(...)throw new Exception1();
if(...)throw new Exception2();
if(...)throw new Exception3();
}
//////////////////////////
void f() throws Exception1,Exception2,Exception3{//應該丟出所有可能的例外

if(...)throw new Exception1();
if(...)throw new Exception2();
if(...)throw new Exception3();
}


case 21:使用finally避免資源洩漏(resource leaks)
簡單來說,就是如果有開啟什麼resource,像是socket還是file,必須在finally把他關起來
因為不能保證他會在try把他關裡來

Socket s=new Socket();
try{
...
s.close();//不保證跑到這行
}
catch(Exception e){
s.close();//程式碼可讀性降低
}
finally{
s.close();//最好是放這裡
}

case 22:不要從try區段中返回
字面意思就是這樣

case 23:將try-catch區段置於迴圈之外
主要是因為把try-catch放到迴圈裡面會降低效能

void f1(){
for(.....)
{
try{
}catch(Exception e){
}
}
}

void f2(){
try{
for(.....)
{
}
}catch(Exception e){
}
}

以上面為例,f2的效能會比f1還高,書中用bytecode作解釋,在此不贅言

case 24:不要將exception用於流程控制
簡單來說就是不要用try-catch去取代if-else

case 25:不要每逢出錯就使用exception
這段大意是說不要再不該用exception的場合使用他,比方說跳出迴圈

try{
while(true){
if(i==0)throw new Exception("");//多此一舉
}
}catch(){...}


case 26:在建構式中拋出異常
期望用拋出異常的方式告知使用者這個建構式失敗了,他期望怎麼處裡
典型範例:FileInputStream等IO類別的建構式

case 27:拋出異常之前先將物件回復成有效狀態
這個case的概念我覺得很不錯,很多人在處裡例外都只是印出錯誤訊息後就不管了
而沒試者去讓修復他讓他可以繼續執行
這讓我想起大學教授舉的一個例子『假設今天你寫的程式是給那些插肺管的病人用,而今天你程式當掉了,重開要兩分鐘,你有辦法閉氣兩分鐘嗎?一個健康的人都沒辦法,何況是那些病人』

上面的例子是極端了點,但是我覺得這很重要,捕捉到例外就應該處裡,就算無法處裡也該詳細記錄哪個地方出問題。
還記得我大學寫的遊戲,全螢幕縮下來時候會拋出一個例外,如果不處裡那個例外我的遊戲就會死當,當初這問題困擾我很久但是也讓我學到了例外處理的重要性

[JAVA] Practical Java Programming Language Guide-第二章 物件與相等性

Practical Java Programming Language Guide-第二章 物件與相等性

這章節介紹了object與primitive types、equality等相關的觀念

case 8:區別reference type和primitive types
Java有兩大型別reference和primitive,primitive的效率會比較高,而reference可以有比較大的彈性,而每個primitive(基本型別)都有對應的wrapper object,像是int就有個Integer支援
reference在創建(new)的時候成本非常高,如果追求高效能的話則應該選用primitive
因為primitive的創建只在stack上,不用再去heap要記憶體(請參考http://hatsukiakio.blogspot.com/2009/04/c-static.html)
舉個例子

int i=5;
Integer j=new Integer(10);


i只用了Stack上的4個byte的記憶體,但是Integer除了stack上的4 byte(32 bit機器上指標的大小),在heap又用了一塊記憶體存真正的值加上對method的mapping,這之間效率比很明顯
但是不是說Integer不好,在很多情況下他也很方便
附帶一提,如果要寫swap函式的話不能用int,要用Integer理由請參考第一章第一個case

case 9:區分==和equals
equal()這函式在java舉足輕重,甚至有人主張所有類別都該複寫這個函式
因為java內建的==只能判別基本型別的相等,對於reference如果不複寫的話,他只會檢查兩變數是否是同一個instance,如果對物件邏輯上的相等有需求,都應該override這個函式

Integer a=new Integer(10);
Integer b=new Integer(10);
System.out.print(a==b);//false;


case 10:不要依賴equals()的預設實作
理由同case9 預設object的equal只是在使用==來做比較,只能比出兩個是否為同一實體

case 11:實作equals()時必須保持深思熟慮
這個case點出你應該好好設計equal

case 12:實作equals()時優先考慮使用getClass()
這邊是考慮『相同的class所產生的物件才可能相等』,不過這必須牽扯到superclass跟subclass是否也該考慮


class Base{

public boolean equals(Object obj)
{
if(obj==this)return true;
if(obj!=null&&getClass()!=obj.getClass())return false;
//otherwise
}
}

書上建議任何equal都可以用此方式當title,先檢查是不是自己,在檢察是不是null最後檢查是不是同個class

case 13:呼叫super.equals()以喚起base class的相關行為
這邊主要說,父類別的比較,交給父類別去做,不要自己做,避免產生不必要麻煩
但是要注意如果base class是Object就不要呼叫super.equals(),因為他是檢察是否為同一個實體

case 14:在equals()函式中謹慎使用instanceof
這邊牽扯到base class跟subclass之間的關連,子類別是父類別的一種,用instanceof他會通過
但是用getClass(),子類別跟父類別是不同類別所以不會通過


case 15:實作equals()遵循某些規則

  1. 如果class的兩物件及使佔用不同記憶體,也可被視為邏輯上相等,就該複寫equals();

  2. 先檢查是否等於this

  3. 比較兩物件的相關屬性判斷是否相等

  4. 如果java.lang,Object以外的父類別實作了equals(),就該呼叫super.equals()


在equals()中對getClass還是instanceof做取捨時該考慮的問題

  1. 如果只允許同一class才能相等才用getClass()

  2. 如果不得不考慮父類別與子類別的相等性,才應該使用instanceof,這其中複雜性很高

  3. 如果使用instanceof,而且父類別跟子類別都有實作equals(),比較能避免不對稱性

當然!這參考就好,真正的equal還是要視自己需求而定

[JAVA] Practical Java Programming Language Guide-第一章 一般技術

第一章‧一般技術


裡面介紹了java通用的一般知識

case 1:引數以by value而非by reference的方式傳遞
某些人誤認Java沒有指標,其實該說java所有物件都是指標,他傳參數全部都以call by value的方式在傳遞,而由於物件以指標的形式被傳入,所以函式內的變數是可以修改傳入的實體
舉個簡單的例子

class Message{
String msg;
public Message(){msg="default"}
}
...
static void f(Message m){m.msg="function"}
...
Message _m=new Message();
f(_m);
System.out.println(_m.msg);

他會印出function,因為函式的m與傳入的_m是指到同一記憶體

case 2:對不變的Data和Object reference使用final
簡單來說就是用final這個修飾子保護變數,但是有一點要注意的
裡面提到儘管用final保護Object reference也只能保護他實體不變而非實體不被改變
用上面的Message類別舉個例子

final Message im=new Message();
im.msg="change";
System.out.println(im.msg);//印出change

im=new Message();//編譯失敗,final object不能重新指向新實體

要改讓object內容不可改,必須用一些編程的技巧讓他成為immutable

case 3:預設情況下,所有non-static的函式都可以被覆寫
簡單來說,只有被加上的final的函式是不能被override的

case 4:在arrays和Vectors之間慎重選擇
一張表格說明一切























支援基本型別支援物件自動改變大小速度快
array
Y
Y
N
Y
vector
N
Y
Y
N

簡單來說,想要速度快就該用array,想要size可變則選擇vector的相關家族

case 5:多型(polymorphis)優於instanceof
這個case是說比起用inststanceof判斷是哪個類別來做哪些事,不如用多型的技巧更為優雅
這在重構(Refactoring)裡面也有許多相似的技巧


class Parent...
class A extends Parent..

class B extends Parent..

void f(Parent p)
{
if(p instanceof A)//do something
if(p instanceof B)//do something
}//這不是好作法

class Parent{
void doSomething(){...}
}
//假設A,B已經實作doSomething
void f(Parent p)
{
p.doSomething();
}
...
A childe=new A();
f(child);
//使用多型的技巧自動決定函式的行為

這邊題外話一下,在C++的時候只有reference跟pointer才能做到多型
但是在java每個object都可以看作pointer,所以他可以更輕易達到多型

case 6:必要時才使用instanceof
這邊主要是說在逼不得已的時候才使用instanceof,像是使用了Vector,物件全部被轉型成Object(現在的java類似的容器類別大概都有提供樣版,大概也沒機會用到了)

Vector v=new Vector();
v.add(new A());
Object o=v.get(0);//vector的get會傳回Object
if(o instanceof A)//do something

類似如上的狀況,除非物件已經被轉型成我們無法掌握或是無法使用多型的情況才使用instanceof,而上面也提到,java現在的版本大概都有提供樣版可以使用,這情況更少見了

case 7:一旦不需要object reference就將他設為null
這個case是說用不到的object就將他設為null,因為JVM的GC必須所有的instance都不再有reference才會去回收那塊記憶體,如果用不到而又不回收就會造成memory leak
也就是系統無法回收卻又無法使用的記憶體區塊會越來越多造成系統當機