Learning Objectives
- 說明類別是藍圖、物件是用 new 在堆積上建立的實體,變數存的是指向物件的參照
- 寫建構子替新物件設定初始值,用 this 分辨欄位與同名的參數
- 把欄位設成 private、透過方法存取,在方法裡檢查資料是否合理(封裝)
- 說明兩個變數指向同一個物件時,透過其中一個修改,另一個也看得到;== 比較的是參照
- 分辨 static 欄位(整個類別共用一份)與一般欄位(每個物件各有一份)
類別與物件
寫程式時常常要處理「一群有相同結構的資料」:每位學生都有姓名和分數、每本書都有書名和價格。 Java 用類別(class)描述這種結構,再依照類別建立一個一個的物件(object):
class Student { // 類別:描述學生「有什麼資料、能做什麼事」
String name; // 欄位(field):每個物件各有一份
int score;
void show() { // 方法(method):物件能做的事
System.out.println(name + ":" + score + " 分");
}
}
Student amy = new Student(); // 依照類別建立一個物件
amy.name = "Amy"; // 用「變數.欄位」存取物件的資料
amy.show(); // 用「變數.方法()」呼叫物件的方法
類別像是設計圖,物件是依照設計圖蓋出來的房子:一張設計圖可以蓋很多棟,每一棟都有自己的門牌和住戶。
本課的逐步執行動畫分成兩欄:左邊是變數(每次呼叫方法都有自己的一個框架),右邊是物件。
new 建立的物件放在堆積(heap)這塊記憶體裡;變數本身不裝物件,只存一個指向物件的參照(reference),動畫用箭頭表示。
用 new 建立物件
第 13 行執行完,右邊出現一個 Student 物件,amy 的圓點用箭頭指向它。物件剛建立時,欄位是預設值:參照型別(例如 String)是 null、數字是 0、boolean 是 false。呼叫 amy.show() 時多出一個框架,裡面的 this 指向 amy 那個物件;呼叫 bob.show() 時,this 就換成指向 bob 的物件。
建構子與 this
一個一個設定欄位很麻煩,也容易忘記。建構子(constructor)是 new 的時候自動執行的特殊方法,
負責替新物件設定初始值:
class Book {
String title;
int price;
Book(String title, int price) { // 建構子:名稱和類別相同,沒有回傳型別
this.title = title; // this.title 是欄位,title 是參數
this.price = price;
}
}
Book a = new Book("Java 入門", 450);
- 建構子的名稱必須和類別完全相同,而且不寫回傳型別(連
void都不寫)。寫了void就變成一般方法,new的時候不會執行 - 參數和欄位同名時,單寫
title指的是參數;要指欄位必須寫this.title。this代表「正在建立(或正在呼叫方法)的這個物件」 - 同一個類別可以有好幾個參數不同的建構子(多載)。在建構子的第一行寫
this(…),可以呼叫另一個建構子,避免重複的程式碼 - 類別裡完全沒有寫建構子時,Java 會自動提供一個不接受參數的預設建構子;只要你寫了任何一個建構子,預設建構子就不再自動提供
建構子、this(…) 與方法
注意第 23 行 new Book("演算法"):先進入只有一個參數的建構子,它的第一行 this(title, 300) 再呼叫兩個參數的建構子,所以右邊會疊出兩層框架,兩層的 this 都指向同一個新物件。
想一想:物件、預設值與建構子
下面的程式會印出什麼?
class Cat {
String name;
int age;
}
public class Main {
public static void main(String[] args) {
Cat c = new Cat();
c.name = "Mimi";
System.out.println(c.name + " " + c.age);
}
}
下面的程式會印出什麼?
class Box {
int size;
Box(int size) {
size = size;
}
}
public class Main {
public static void main(String[] args) {
Box b = new Box(5);
System.out.println(b.size);
}
}
類別裡只寫了 Dog(String name) { … } 這一個建構子,執行 new Dog() 會怎樣?
封裝:private 欄位與存取方法
如果任何程式都能直接改 acc.balance = -5000;,物件裡的資料就可能變得不合理。封裝(encapsulation)的做法是:
- 欄位加上
private,類別外面的程式不能直接讀寫 - 提供
public的方法讓外面使用,方法裡先檢查資料是否合理,再修改欄位
class BankAccount {
private int balance;
void deposit(int amount) {
if (amount <= 0) return; // 不合理的金額直接拒絕
balance += amount;
}
int getBalance() { return balance; } // getter:讓外面讀取,但不能直接改
}
在類別外面寫 acc.balance 會編譯錯誤:balance has private access in BankAccount。
只讀取欄位的方法習慣叫 getXxx(),修改欄位的叫 setXxx(…)(getter/setter)。不是每個欄位都需要 setter:餘額只能透過存款、提款改變,就不應該提供 setBalance。
| 修飾字 | 誰可以使用 |
|---|---|
public |
任何類別 |
protected |
同一個套件,以及子類別(第 09 課) |
| 不寫 | 同一個套件(package)裡的類別 |
private |
只有同一個類別裡面 |
封裝的銀行帳戶
main 沒有辦法直接碰 balance,只能呼叫 deposit、withdraw、getBalance。看第 35 行存入 -50 時,方法在第 11 行檢查之後就 return,欄位沒有被改;第 36 行領 1500 超過餘額,withdraw 回傳 false。
變數存的是參照
int、double 這類基本型別的變數直接存值;類別、陣列、String 這類參照型別的變數,存的是指向物件的參照。這會影響三件事:
1. 指派只複製參照。 Point b = a; 不會複製物件,而是讓 b 也指向 a 那個物件。之後透過 b 改欄位,用 a 讀到的也是改過的值。
要真的得到一份副本,必須自己 new 一個新物件,把欄位一一複製過去。
2. == 比較的是「是不是同一個物件」。 兩個物件的欄位完全一樣,只要是分別 new 出來的,== 就是 false。
要比較內容,類別要提供 equals 方法(第 09 課)。字串也一樣:比較文字內容要用 s1.equals(s2),不要用 ==。
3. 把物件傳給方法,傳的是參照的複本。 方法拿到的參照和呼叫端的指向同一個物件,所以方法裡改欄位,呼叫端看得到。 (Java 永遠是傳值:傳進去的是「參照這個值」的複本。方法裡讓參數改指向別的物件,不會影響呼叫端的變數。)
null 表示「沒有指向任何物件」。對 null 呼叫方法或存取欄位會拋出 NullPointerException。
當一個物件不再被任何變數指到,程式就再也用不到它,Java 的垃圾回收(garbage collection)會在適當的時候自動釋放它的記憶體。
兩個變數指向同一個物件
第 19 行之後,a 和 b 的箭頭指向同一個 Point。第 29、30 行把兩個變數都設成 null 之後,第一個 Point 再也沒有變數指到,從右邊消失:這就是垃圾回收會處理的物件。
static:屬於類別的成員
一般欄位是每個物件各有一份;加上 static 的欄位則屬於類別本身,整個程式只有一份,所有物件共用。
適合用來記錄「跟整個類別有關」的資料,例如一共建立了幾個物件、共用的常數。
class Ticket {
static int count = 0; // 所有 Ticket 共用
int number; // 每張票各自的號碼
Ticket() {
count++;
number = count;
}
}
System.out.println(Ticket.count); // 用「類別名稱.欄位」存取
static 方法同樣屬於類別,用 類別名稱.方法() 呼叫,例如 Math.sqrt(2)、Integer.parseInt("5")。
static 方法執行時沒有 this,所以不能直接使用一般欄位或一般方法。這就是為什麼在 main 裡直接呼叫自己寫的非 static 方法,
編譯器會報錯 non-static method … cannot be referenced from a static context:要先 new 出物件,再用物件呼叫。
常數通常寫成 static final,名稱全部大寫,例如 static final double TAX_RATE = 0.05;。
共用的計數器
第一次用到 Ticket 類別時,動畫左邊會多出「靜態欄位」框,裡面是整個類別共用的 Ticket.count。每建立一張票,count 就加 1;每個物件自己的 number 則各不相同。
想一想:參照、== 與 static
下面的程式會印出什麼?
class Counter {
int n;
}
public class Main {
static void add(Counter c) { c.n++; }
static void add(int x) { x++; }
public static void main(String[] args) {
Counter c = new Counter();
int x = 0;
add(c);
add(x);
System.out.println(c.n + " " + x);
}
}
下面的程式會印出什麼?
class P {
int v;
P(int v) { this.v = v; }
}
public class Main {
public static void main(String[] args) {
P a = new P(3);
P b = new P(3);
P c = a;
System.out.println((a == b) + " " + (a == c));
}
}
下面的程式會印出什麼?
class Player {
static int total = 0;
int score;
Player(int s) { score = s; total += s; }
}
public class Main {
public static void main(String[] args) {
Player p1 = new Player(10);
Player p2 = new Player(25);
System.out.println(p1.score + " " + Player.total);
}
}
動手寫:長方形類別
完成 Rectangle 類別:建構子把寬、高存進欄位,area() 回傳面積,perimeter() 回傳周長。main 已經寫好,執行後的輸出要和預期相同。
重點回顧
- 類別描述資料(欄位)與行為(方法);
new依照類別在堆積上建立物件,變數存的是指向物件的參照 - 物件的欄位有預設值(數字 0、
boolean為false、參照為null);方法裡的區域變數沒有預設值 - 建構子的名稱和類別相同、沒有回傳型別,
new時自動執行;參數和欄位同名時用this.欄位指欄位,this(…)呼叫另一個建構子 - 封裝:欄位設成
private,透過方法存取,在方法裡檢查資料 - 指派物件變數只複製參照;
==比較是不是同一個物件;把物件傳給方法,方法裡改欄位,呼叫端看得到 static成員屬於類別、整個程式只有一份;static 方法沒有this,不能直接使用一般欄位與方法