{ 代數與組合 }
{ 計算機科學 }
{ 高中教材 }
物件導向筆記 - 基礎
物件導向筆記 - 基礎
以前寫的筆記,好讓團隊正常的使用 Java.
因為觀念大家都差不多懂了,所以重點是學習物件導向的哲學,了解「這些語法解決了什麼工程問題?」
所謂工程問題,目前可以理解為「大型程式開發與維護」遇到的問題。 這種問題主要有兩大類:
- 程式碼的維護。怎樣的程式架構難以維護、難以除錯、難以 Code Reuse?客戶需求改變時,程式會需要完全重寫嗎?
- 人員的分工。怎麼讓多人工作時彼此的程式可以銜接?誰可以改誰的 Code?權責分明是放諸四海皆準的守則。
以下的筆記以 Java 語法為準
- 一、類別與物件
- 二、屬性與方法
- 三、封裝 Encapsulation
- 四、靜態屬性與方法 Static Fields and Methods
- 五、繼承 Inherit
- 六、抽象類別 Abstract Class
- 七、介面 Interface
一、類別與物件
不需要多說,類別是抽象概念(或型別)的定義,物件是依照概念而建立的具象實體(變數)。
二、屬性與方法
屬性 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 anInputstream
AESCipher
is aCipher
DESCipher
is aCipher
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
- 定義類別的行為,卻沒有完整的實作,用來當作別人的父類別
- 子類別除了必須實作抽象方法之外,還必須在建構子呼叫抽象類別的建構子
使用時機
- 用來分開定義與實作
- 例如
List
、Set
等容器只有定義呼叫方法,實際上再用ArrayList
與HashSet
等技術來實作- 可以視為分工的界線
- 仍然蘊含有 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
關於語法
- 介面中的屬性只能是
static
且final
的,就算沒加這些指示字也一樣
使用時機
- 宣告行為:實作的人要有這個函數可以呼叫
Runnable
: should implementpublic void run()
Comparable
: should implementpublic 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
的屬性則需要使用抽象類別
以下情況建議使用介面
- 想要讓幾個無關的類別,都提供你想要的函數
- 你預期某些特定類別要提供這個方法,但還不知道要由誰來寫。
- 模擬多型