物件導向筆記 - 基礎

以前寫的筆記,好讓團隊正常的使用 Java.

因為觀念大家都差不多懂了,所以重點是學習物件導向的哲學,了解「這些語法解決了什麼工程問題?」

所謂工程問題,目前可以理解為「大型程式開發與維護」遇到的問題。 這種問題主要有兩大類:

  • 程式碼的維護。怎樣的程式架構難以維護、難以除錯、難以 Code Reuse?客戶需求改變時,程式會需要完全重寫嗎?
  • 人員的分工。怎麼讓多人工作時彼此的程式可以銜接?誰可以改誰的 Code?權責分明是放諸四海皆準的守則。

以下的筆記以 Java 語法為準

一、類別與物件

不需要多說,類別是抽象概念(或型別)的定義,物件是依照概念而建立的具象實體(變數)。

二、屬性與方法

屬性 Field

這裡就不廢話了

方法 Method

這裡為特殊的方法 – 建構子與 finalize – 做補充

建構子

  • 可以是私有的
  • 物件的生命始於建構子,止於 GC

finalize

  • finalize 會在 GC 時被呼叫,並不同於 C++ 的解構子,因為我們不需要自己釋放記憶體
  • 我們不應該主動呼叫 finalize(那是 GC 的事,我們也不應該臆測 GC 的行為,權責分明

三、封裝 Encapsulation

  • 抽象化的第一步
  • 隱藏所有實作訊息,不只是寫 getter 與 setter

公有與私有

大家應該都用習慣了XD 這些語法正是為封裝而生。

public class Foo {
	public int i;
	private int j;
	int k; // package scope
}

在 Eclipse 中快速封裝:

對一個 field 右鍵 > Refactor > Encapsulation

這樣解決了什麼工程問題

沒封裝好的例子:

class Square {
	public double getEdgeLengthFromArea(double area){
		/* 請各位開發者輸入非負的數,否則會噴 */
		return Math.sqrt(area);
	}
}

發現了嗎?他不應該要求所有上層開發者來看他的 code 與註解。當 code 越來越大,上層開發者會有很多地雷要閃。

修正:

class Square {
	private double area;
	public Square(double area) throws IllegalArgumentException {
		if (area >= 0) this.area = area;
		else throw new IllegalArgumentException("Area should be non-negative");
	}
	public double getEdgeLengthFromArea(double area){
		return Math.sqrt(area);
	}
}

四、靜態屬性與方法 Static Fields and Methods

  • 靜態屬性是關於類別本身的。所有 new 出來的物件、所有的 Thread 、都共用這一個變數。
  • 非靜態屬性是物件的。每個 new 出來的物件都有自己個別的屬性
  • 靜態方法不能存取非靜態屬性(他也不知道要從哪個 Object 取)

舉例

例 1:這個屬性是誰的?

public class AESCipher{
	public static int BLOCKLEN = 16; // 跟 cipher 的抽象定義有關,不會因個別的 cipher 而變
	private byte[] key; // 每個 cipher 要用的 key 都不一樣
	public AESCipher(byte[] key){...};
	public byte[] encrypt(byte[] data){...}
}
...
AESCipher cipher = new AESCipher(key);
  • print(cipher.BLOCKLEN)? // 不建議
  • print(AESCipher.BLOCKLEN) // 建議

例 2:常見的例子,無關物件的函數

public class Math {
	public static double sin(double x){...}
	public static double log(double x){...}
}

例 3:有關初始化的語法

動態的初始 Assignment 在建構子 靜態的初始 Assignment 在 ClassLoader

public class foo{
	public static int i;
	static {
		i = 10;
	}
}

等同於以下的 compiler sugar

public class foo{
	public static int i = 10;
}

例 4:常犯的錯誤

原文出處:http://www.oreilly.com.tw/column_sleepless2.php?id=part24

public class Singleton {
  private static Singleton obj = new Singleton();
  public static int counter1;
  public static int counter2 = 0;
  private Singleton() {
    counter1++;
    counter2++;
   }
  public static Singleton getInstance() {
    return obj;
  }
}

public class MyMain {
  public static void main(String[] args) {
   Singleton obj = Singleton.getInstance();
   System.out.println("obj.counter1=="+obj.counter1);
   System.out.println("obj.counter2=="+obj.counter2);
  }
}

沒有初始化的 counter1 初始值應該也是零,但執行結果卻會與 counter2 不同

obj.counter1==1
obj.counter2==0

請從**初始化乃是 compiler sugar **的角度去想

五、繼承 Inherit

extends關鍵字

使用時機

Subset (必要條件)

ObjectInputStream is an Inputstream AESCipher is a Cipher DESCipher is a Cipher

Code Reuse

不應為了 code reuse 而盲目使用繼承。要注意必要條件

Extend Class Functionality

public class ProfileSaver{
	public void save(Profile pf, File f){
		...
	}
}
public class BetterProfileSaver extends ProfileSaver{
	@Override
	public void save(Profile pf, File file){
		super.save(pf, file);                          // reuse
		print("Success");
	}
	public void saveEncryptly(Profile pf, File file){  // extend
		save(AES.encrypt(pf), file);
	}
}

Protected 指示字

  • 只有自己與子類別可以存取。
  • 子類別是父類別的擴充,可能需要父類別的權限,所以設計此語法。

Override Annotation

  • 覆寫父類別方法時可以使用
  • 不加也可以,但是當拼錯覆類別方法名稱的時候加了很有用
  • 關於 Annotation 會在 Java 章節介紹

誤用繼承

例一:子類別改變父類別的預期行為

誇張一點的例子:

public class ArrayList {
	public void sort(){
		/* sorting */
	}
}
public class MyList extends ArrayList {
	@Override
	public void sort(){
		deleteAllElements();
	}
}

總之,不要混淆使用你 code 的人

例二:抽象意義上不符合,不應該繼承

public class UserAuthenticationData {
	protected byte[] ID;
	protected byte[] PrivateKey;
}
public class UserData extends UserAuthenticationData {
	protected byte[] phoneNum;
	protected char bloodType;
	protected int age;
	public String toString(){
		return "" + ID + PK + phoneNum + bloodType + age;
	}
}
...
UserData user = new UserData()

呼叫會變這樣

user.toString() // 的確是可以印出來
AuthenticatedChannel c = new AuthenticatedChannel(user); // 這樣變成意義不明
// 原本是
new AuthenticatedChannel(UserAuthenticationData u)

要保持繼承中 is a 的概念

例三:背叛父類別

或是為了揭露父類別特地去繼承

public class GuessNumber {
	protected static int answer;
	public boolean guess(int quest){
		return quest == answer;
	}
}
public class revealGuessNumber extends GuessNumber{
	public int getAnswer(){ return answer; }
}

Java 提供物件導向的語法,但語法並不是重點,遵守規則才是。

六、抽象類別 Abstract Class

  • 定義類別的行為,卻沒有完整的實作,用來當作別人的父類別
  • 子類別除了必須實作抽象方法之外,還必須在建構子呼叫抽象類別的建構子

使用時機

  • 用來分開定義與實作
  • 例如ListSet等容器只有定義呼叫方法,實際上再用ArrayListHashSet等技術來實作
  • 可以視為分工的界線
  • 仍然蘊含有 is a 關係,這是和介面最大的不同

例:ProfileDatabase

public abstract class ProfileDatabase {
	public ProfileDatabase(){
		if(!existsTable()) createTable();
	}
	public abstract bool existsTable();
	public abstract void createTable();
	public abstract void insertProfile(Profile pf);
	public abstract void queryProfile(String ID);
}

public class HbaseProfileDatabase extends ProfileDatabase {
	public HbaseProfileDatabase(){
		super(); // 這裡是要呼叫,不是覆寫
	}
	@Override
	public bool existsTable(){...}
	@Override
	public abstract void createTable(){...}
	@Override
	public abstract void insertProfile(Profile pf){...}
	@Override
	public abstract void queryProfile(String ID){...}
}

七、介面 Interface

關於語法

  • 介面中的屬性只能是staticfinal的,就算沒加這些指示字也一樣

使用時機

  • 宣告行為:實作的人要有這個函數可以呼叫
    • Runnable: should implement public void run()
    • Comparable: should implement public int compareTo(). Think of Sorting

例:實作 Comparable 介面

Collections :: sort(List<? extends Comparable>)

public Profile implements Comparable<Profile> {
	private int ID;
	private int PW;
	public int compareTo(Profile pf){
		if     (pf.ID > this.ID) return 1;
		else if(pf.ID < this.ID) return -1;
		else return 0;
	}
}

List<Profile> pflist = ...;
Collections.sort(pflist);

  • 也可作為分工的界線
  • 與抽象類別相像。如果一定要由某類特定的類別來實作,則使用抽象類別比較好

Java 官方建議的使用時機

以下情況建議使用抽象類別

  • 幾個緊密關聯的類別之間,想要使用同一些函數(就是關於繼承的 Code Reuse)
  • 這些子類別的屬性或方法都雷同,只是實作不同而已 ,所以如果想要宣告非static或是非final的屬性則需要使用抽象類別

以下情況建議使用介面

  • 想要讓幾個無關的類別,都提供你想要的函數
  • 你預期某些特定類別要提供這個方法,但還不知道要由誰來寫。
  • 模擬多型