捲動以翻頁
Computer Science A · AP Computer Science A
值、參考,以及使用它們的方法
變數是記憶體中一個有名字的盒子。基本型別變數存的是值;參考變數存的是物件的位置。
int score = 42; int copy = score; // copy 有自己的 42 copy = 7; // score 仍然是 42 String name = "Ada"; // 一個參考 String other = name; // 同一個參考
圖中逐行執行這些程式碼。copy = score把值 42 複製到第二個盒子裡,所以當copy變成7時,score不受影響。name裡完全沒有字元:它的盒子裡是一個(紅色的)箭頭,指向記憶體另一處建立的 String 物件。other = name複製的是箭頭,不是物件:兩個箭頭,一個物件。
這個差別貫穿整個 Java,也貫穿本章:變數存什麼、方法收到什麼又回傳什麼、new建立了什麼,以及為什麼 String 一旦建立就不會改變。
本章是 AP Computer Science A 第 1 單元使用物件與方法,占選擇題分數的 15–25%:編譯與錯誤(1.1)、型別、運算式與指派(1.2–1.6)、函式庫、文件與方法(1.7–1.11)、物件及其建立(1.12–1.14)以及 String(1.15)。考試的 42 道選擇題附有 Java Quick Reference,其中列出所有可能用到的函式庫方法;第一道自由作答題會呼叫 String 的方法。
以及三種錯誤
演算法是完成一項工作或解決一個問題的逐步流程。它可以用文字寫出、畫成圖表,或寫成程式。
順序把步驟排好順序,並一次執行一個:除非有東西改變流程,Java 方法會由上而下執行它的敘述。
程式碼可以用任何文字編輯器撰寫,但多數程式設計師使用整合式開發環境(IDE),裡面有撰寫、編譯和執行的工具。編譯器會檢查原始碼中的某些錯誤並加以翻譯;Java 的編譯器產生位元組碼(bytecode),再由 Java 虛擬機器(JVM)執行。編譯器找到的錯誤必須先修正,程式才能執行。
圖中把三個程式送進流程,每個各有一種錯誤。
int total = 7 + 3 // 缺少 ; System.out.println(total); 語法錯誤違反了語言的規則。編譯器會找到它(error: ';' expected),什麼都不會執行。
int count = 0; System.out.println(10 / count);
執行階段錯誤發生在程式執行時,通常會讓程式異常結束。這個是例外:編譯器沒有偵測到的錯誤,會中斷正常的執行流程(ArithmeticException: / by zero)。
double avg = (7 + 8) / 2; System.out.println(avg); // prints 7.0 邏輯錯誤可以編譯也能執行,但行為不正確:7 and 8的平均應該是7.5。只有用特定資料測試,並與預期的結果比較,才能找出來。(為什麼會印出7.0:見 1.3 頁。)
int、double、boolean 與參考
資料型別是一組值,以及可對這些值進行的運算。每個型別不是基本型別,就是參考型別。
本課程使用三種基本型別,用於數字和布林值:
| type | values | examples |
|---|---|---|
int | integers | 7, -40, 0 |
double | 實數 | 2.5, 7.0, -0.01 |
boolean | true 或 false | true |
Java 還有五種其他基本型別(long、short、byte、float、char);它們不在課程與考試範圍內。凡不是基本型別的,例如 String,都是物件,它的型別是參考型別。
變數是存放值的儲存位置,這個值在程式執行時可以改變。每個變數都有名稱和型別,基本型別的變數存放該型別的值。
int n = 7; double d = 2.5; boolean ok = true; double w = 7; // stores 7.0 int m = 2.5; // 無法編譯
int 放得進 double:double w = 7;存入7.0。double 放不進 int,所以int m = 2.5;是編譯錯誤:incompatible types: possible lossy conversion from double to int。
依值可能是什麼來選型別:學生人數是 int,價格或平均數是 double,燈是否亮著是 boolean。
print、println 與跳脫序列
System.out.print 和 System.out.println 把資訊顯示在螢幕上。println 之後會把游標移到新的一行;print 則讓游標留在原處。
常值是固定值在程式碼中的表示方式:7、2.5、true。字串常值是用雙引號括起來的一串字元:"CSA"。
有些字元不能直接打在引號裡。跳脫序列以反斜線開頭,有特殊意義。本課程用到三個:
| 跳脫序列 | means |
|---|---|
\" | 一個雙引號 |
\\ | 一個反斜線 |
\n | 換行字元 |
System.out.print("AP "); System.out.println("CSA"); System.out.println("say \"hi\""); System.out.print("a\\b\nc"); 主控台接著會顯示(圖中逐字打出):
AP CSA say "hi" a\b c 第一個 print 讓游標停在 AP 之後,所以 CSA 接在同一行。最後一行中,\\ 印出一個反斜線,\n 在字串中間開始新的一行。
int 除法、餘數與優先順序
算術運算式由數值、變數和運算子 +、-、*、/、% 組合而成。兩個 int 值運算得到 int;至少有一個 double 則得到 double。
System.out.println(17 / 5); // 3 System.out.println(17 % 5); // 2 System.out.println(17.0 / 5); // 3.4
兩個 int 相除只保留商的整數部分:17個積木分成每組5個,可分成3整組。餘數運算子 % 給出剩下的部分:2。如果除法中有 double,結果就是真正的商,3.4。(本課程使用 % 時,只限 a ≥ 0 且 b > 0。)
優先順序:*、/ 和 % 先於 + 和 - 運算;優先順序相同的運算子由左到右;括號會改變分組。編譯器在編譯時就是這樣把數值和運算子分組的。
| expression | value |
|---|---|
2 + 3 * 4 | 14 |
(2 + 3) * 4 | 20 |
10 - 4 - 3 | 3 |
7 + 9 % 4 | 8 |
1 / 2 * 4.0 | 0.0 |
1.0 / 2 * 4 | 2.0 |
在 1 / 2 * 4.0 中,1 / 2 是 int 除法,在 double 出現之前就已經是0了。
用 int 的零去除 int,在該行執行時會丟出 ArithmeticException。(double 除以零,以及它產生的特殊 double 值,不在課程範圍內。)
右邊的值,存到左邊
指派運算子 = 先把右邊的運算式算成單一個值,再存入左邊的變數。
變數在運算式中使用之前,必須先給它一個值,而且這個值的型別必須相容。第一次指派稱為初始化。之後的每次指派都會取代原來的值:舊值就不見了。
int a = 3; // a 3 int b = 8; // a 3, b 8 int temp = a; // a 3, b 8, temp 3 a = b; // a 8, b 8, temp 3 b = temp; // a 8, b 3, temp 3
所以交換兩個變數需要第三個變數。沒有 temp,a = b 會在 b 拿到 a 的3之前就把它毀掉。圖中把每份複本從一個盒子搬到另一個盒子:來源仍保有它的值。
程式執行時,運算式會被算成一個值,而這個值有型別:a + b 是 int,a + 0.5 是 double。像 count = count + 1 這樣的敘述不是等式;它讀取 count,加 1,再把結果存回去。
參考變數可以被指派一個新物件或 null。常值 null 表示這個參考沒有關聯到任何物件。
運算式裡面的指派,例如 a = b = 4;,不在課程範圍內。
輸入可以是觸覺、聲音、視覺或文字。Scanner 類別是讀取鍵盤輸入文字的一種方式;考試不會考任何特定形式的使用者輸入。
截斷、擴大與四捨五入
型別轉換運算子 (int) 和 (double) 把值從一種基本型別轉成另一種。
把 double 轉成 int 會截斷:小數點後的數字被切掉,所以不論正負,值都往零的方向移動。
| expression | value |
|---|---|
(int) 3.99 | 3 |
(int) -3.99 | -3 |
7 / 2 | 3 |
(double) 7 / 2 | 3.5 |
(double) (7 / 2) | 3.0 |
7 / 2.0 | 3.5 |
1 + 2.5 | 3.5 |
轉換運算只作用於緊接在它後面的值。(double) 7 / 2 先把 7 轉成 double,再以 double 相除;(double) (7 / 2) 先做 int 除法,再轉換其結果3.0。
有些程式碼會自動把 int 擴大成 double:在 1 + 2.5 中,以及把 int 存入 double 變數時(1.2 頁)。
要把 double 四捨五入到最接近的整數,先讓它朝遠離零的方向偏移半個單位,再截斷:(int) (x + 0.5) 用於非負的 x,(int) (x - 0.5) 用於負的 x。
| x | (int) x | rounded |
|---|---|---|
2.7 | 2 | 3 |
-2.7 | -2 | -3 |
3.2 | 3 | 3 |
-1.4 | -1 | -1 |
圖中對這些值各做一次轉換:紅色朝零截斷;藍色先偏移半個單位,再截斷。
整數溢位與捨入誤差
int 存放在固定大小的記憶體中,共 4 個位元組,所以有範圍:從 Integer.MIN_VALUE 到 Integer.MAX_VALUE(含兩端)。
| constant | value |
|---|---|
Integer.MAX_VALUE | 2147483647 |
Integer.MIN_VALUE | -2147483648 |
int n = Integer.MAX_VALUE - 2; // n 是 2147483645 n = n + 1; // n 是 2147483646 n = n + 1; // n 是 2147483647 n = n + 1; // n 是 -2147483648 n = n + 1; // n 是 -2147483647
如果某個運算式算出的 int 超出範圍,就會發生整數溢位。結果是範圍內的某個 int,但不是預期的值:MAX_VALUE 再多 1 就是 MIN_VALUE。圖中把這些 int 畫成一個環(未按比例),接縫在最上方。不會出現錯誤訊息;程式帶著錯誤的數字繼續執行。
double 的限制不同。double 也存放在固定大小的記憶體中,所以比它能容納的更精確的結果,會被捨入成它能表示的最接近的值:這是捨入誤差。
System.out.println(0.1 + 0.2); // 0.30000000000000004 0.1和0.2都沒有精確的二進位形式,這些小誤差在總和中就顯現出來。要避免捨入誤差,盡可能使用 int 值:用「分」而不是「元」來計算金額。(可避免誤差的特殊十進位型別不在課程範圍內。)
+=、−=、*=、/=、%=、++ 與 −−
複合指派運算子先對變數的值與右邊的值做算術運算,再把結果指派給該變數。
score += 5 的作用與 score = score + 5 相同。-=、*=、/= 和 %= 也是如此;int 的 /= 是 int 除法。
後置遞增運算子 ++ 把數值變數加 1 並存回去;後置遞減運算子 -- 則減 1。
int score = 10; // score 是 10 score += 5; // score 是 15 score -= 3; // score 是 12 score *= 2; // score 是 24 score /= 5; // score 是 4 score %= 3; // score 是 1 score++; // score 是 2 score--; // score 是 1
圖中逐行執行並把每個值記在一捲帶子上。在紙上也這樣追蹤:一行,一個新值。
前置形式(++x)以及放在其他運算式裡的遞增(arr[x++])不在課程範圍內。
使用不是你寫的類別
函式庫是類別的集合。應用程式介面(API)規格告訴程式設計師如何使用它們。
類別定義一種參考型別。函式庫的類別被分組成套件:String 和 Math 在 java.lang,Scanner 和 ArrayList 在 java.util。不用看程式碼,就可以使用現有的類別來建立物件;讓它們可以使用的,是它們的文件。
類別的屬性是與它相關的資料,存放在變數中:例如 String 的字元。它的行為是它的實例能做的事,或能對它做的事,由方法定義:length()、substring。
註解向原作者和其他人說明程式碼。編譯器會忽略它們,它們也從不執行。Java 有三種:
// a comment to the end of the line /* a block of comment, 跨越多行 */ /** a Javadoc comment, used to make API documentation */
圖中從架上取下 String,並打開 substring 的文件。
前置條件必須在方法執行前為真,方法才能如預期運作。方法不需要檢查它:呼叫 substring 時若 to 超過結尾,就違反了前置條件,結果是例外,而不是一個客氣的答案。
後置條件在方法執行後必須一直為真。它描述結果:回傳了什麼,或之後物件屬性的值。
參數、引數、回傳值
方法是有名字的一段程式碼,只在被呼叫時才執行。區塊是大括號中的任何程式碼。
程序抽象化:你可以在不知道方法如何寫成的情況下,只靠知道它做什麼來使用它。它的標頭告訴你需要什麼:
public static double pow(double base, double exponent)
參數是在標頭中宣告的變數(base、exponent),在方法內使用。方法簽章是方法的名稱,加上依序排列的參數型別清單:pow(double, double)。沒有參數的方法,清單是空的:length()。
引數是呼叫方法時傳入的值。引數在個數和順序上必須與參數相符。它們以傳值方式傳遞:每個參數一開始都是其引數的複本。圖中,2和10成為複本2.0和10.0。
double r = Math.pow(2, 10); System.out.println(r); // 1024.0 非 void 方法會回傳其回傳型別的值;要使用它,就存入變數或放進運算式中。void 方法不回傳任何東西,所以不能成為運算式的一部分:int x = System.out.println(5); 無法編譯。
當多個方法同名但簽章不同時,這些方法就被多載:Math.abs(int) 和 Math.abs(double)。由引數決定執行哪一個。
呼叫會打斷敘述的順序:先執行方法的敘述,當最後一個敘述(或 return)執行完,控制權回到呼叫處的緊接之後。
靜態方法與 Math.random()
類別方法屬於類別,不屬於任何一個物件。它們的標頭有關鍵字 static,呼叫時使用類別名稱和點運算子:Math.sqrt(2.0)。在定義它們的類別內部,可以省略類別名稱。
Math 類別在 java.lang 中,所以不用 import 就能使用,而且它只有類別方法。Java Quick Reference 列出五個:
| method | returns |
|---|---|
static int abs(int x) | int 的絕對值 |
static double abs(double x) | double 的絕對值 |
static double pow(double base, double exponent) | base 的 exponent 次方 |
static double sqrt(double x) | 非負的平方根 |
static double random() | 一個 ≥ 0.0 且 < 1.0 的 double |
Math.random() 的範圍包含 0.0(包含端點),不包含 1.0(不包含端點)。用算術和型別轉換可以把它移到你要的範圍:
int roll = (int) (Math.random() * 6) + 1;
圖中一步一步做。* 6 把 [0.0, 1.0) 拉伸成 [0.0, 6.0)。(int) 把每一段截斷成 0、1、…、5:共 6 等份,所以有 6 個機率相等的值。+ 1 把它們移到 1、…、6。一般而言,(int) (Math.random() * count) + low 會得到 count 個 int 其中之一,這些 int 從 low 開始。
它擲了 30 次,結果落在 5 on 1, 5 on 2, 6 on 3, 4 on 4, 6 on 5, 4 on 6:機率相等不代表在短時間內次數也相等。
藍圖,以及它做出來的東西
類別是物件屬性與行為的正式實作,也就是藍圖。物件是類別的一個特定實例,這些屬性有它自己的值。
圖中使用一個假設的類別 Book,它有兩個屬性(書名和頁數)和兩個行為(getTitle() 和 getPages())。由它印製出來的每本 Book 都有自己的書名和頁數:"Maps", 120; "Birds", 340; "Stars", 96。
相關的類別可以把共同的屬性和行為放在一個類別中來共用,這個類別稱為父類別。擴充它的類別稱為子類別,不用再寫一次就能取用這些屬性和行為:這是繼承關係。(設計與撰寫繼承不在課程範圍內。)
Java 中的每個類別都是 Object 類別的子類別,所以每個物件都有 toString() 和 equals 這類方法。
參考型別的變數存放物件參考,可以把它想成物件的記憶體位址:就是圖中的箭頭,而不是物件。
Book b; // b 可以存放指向 Book 的參考 String s; // s 可以存放指向 String 的參考
new、建構子與 null
物件用關鍵字 new,後面接上對類別其中一個建構子的呼叫來建立。
建構子與其類別同名。它的簽章是這個名稱加上依序排列的參數型別清單;它的標頭列出參數的型別和名稱。假設的 Book 的文件列出:
Book(String title, int pages) Book(String title) String getTitle() int getPages()
兩個簽章不同的建構子:這些建構子被多載,由引數決定呼叫哪一個。new Book("Maps") 呼叫第二個。
Book b = new Book("Maps", 120); Book c = null; 圖中依序執行第一行:new 為 Book 配置記憶體;建構子執行,它的參數設定屬性的初始值;然後指向新物件的參考被存入 b。和方法呼叫一樣,建構子呼叫會打斷敘述的順序,當它的最後一個敘述執行完,控制權回到呼叫處的緊接之後。
建構子的引數在個數和順序上必須與參數型別相容,並以傳值方式傳遞:new Book(120, "Maps") 無法編譯,因為沒有任何建構子先接收 int 再接收 String。
參考型別的變數存放物件參考,如果沒有物件,則是 null:c 不指向任何東西。
點運算子與 null
實例方法是對類別的某個物件呼叫的,寫法是物件的名稱、點運算子,再加方法:b.getPages()。
int p = b.getPages(); // p 是 120 int q = c.getPages(); // NullPointerException
這次呼叫順著 b 裡的參考找到它的物件,在那個物件上執行 getPages(),回傳的 int(120)存入 p。另一個 Book 物件會回答它自己的頁數。
c 是 null:沒有物件可以讓箭頭指過去。對 null 參考呼叫方法,在該行執行時會丟出 NullPointerException。它可以編譯:編譯器只檢查 Book 有沒有 getPages()。
比較 1.10 頁的類別方法:Math.sqrt(x) 是對類別呼叫的;s.length() 是對某一個 String 物件呼叫的,只回答那個字串的結果。
不可變;串接會產生新的物件
String 物件代表一串字元。它由字串常值 "cat" 建立,或用建構子 new String("cat") 建立。String 在 java.lang 中,預設就能使用。
String 物件是不可變的:一旦建立,它的字元就永遠不會改變。對 String 呼叫的方法不會改變它;它們回傳新的值。
String s = "cat"; // s 是 "cat" s = s + "s"; // s 是 "cats" String t = s + 1 + 2; // t 是 "cats12" String v = 1 + 2 + s; // v 是 "3cats"
+ 和 += 會串接 String,產生一個新的 String 物件。圖中,s = s + "s" 建出 "cats",並讓 s 指向它;"cat" 那個物件仍然正好是 "cat"。
基本型別與 String 串接時,會被轉成 String。+ 由左到右運算,所以 String 第一次出現的位置很重要:在 s + 1 + 2 中,每個數字都以文字接上去;在 1 + 2 + s 中,先以 int 計算 1 + 2。
任何物件都可以與 String 串接:Java 會呼叫該物件的 toString 方法,每個類別都從 Object 繼承了它,它回傳代表該物件的字串。子類別常會覆寫它(一個 public 方法,簽章與父類別的相同,行為是自己的);撰寫這樣的覆寫不在課程範圍內。
索引、length、substring、indexOf
String 的字元有索引,從 0 到長度減 1。使用超出這個範圍的索引會丟出 StringIndexOutOfBoundsException。
Java Quick Reference 列出這些 String 方法(以及建構子;split 隨文字檔一起介紹,見主題 4.6):
| method | returns |
|---|---|
int length() | 字元的個數 |
String substring(int from, int to) | 從索引 from 到索引 to - 1 的字元 |
String substring(int from) | substring(from, length()) |
int indexOf(String str) | str 第一次出現處的索引;找不到則為 −1 |
boolean equals(Object other) | next page |
int compareTo(String other) | next page |
String s = "BANANAS"; s.length(); // 7 s.substring(1, 4); // "ANA" s.substring(4); // "NAS" s.indexOf("NA"); // 2 s.indexOf("X"); // -1 s.substring(3, 4); // "A"
to 是第一個被排除的索引,所以 substring(from, to) 有 to - from 個字元。indexOf 只找第一次出現的位置:"NA" 在索引 4 也有開頭。
只含 index 處那一個字元的字串是 substring(index, index + 1)。
equals、compareTo,而不是 ==
equals 問兩個 String 是否有相同的字元序列。compareTo 問哪一個排在前面。
String a = "pear"; String b = new String("pear"); System.out.println(a == b); // false System.out.println(a.equals(b)); // true System.out.println(a.compareTo("peach") > 0); // true a 和 b 指向兩個不同的物件,裡面的字元相同。a.equals(b) 逐一比較字元:true。a == b 比較的是參考,也就是它們是否指向同一個物件:false。比較 String 要用 equals。(用 == 比較參考會在主題 2.6 再出現。)
a.compareTo(other) 在 a 排在 other 之前時回傳小於 0 的值,相等時回傳 0,a 排在之後時回傳大於 0 的值。String 依字母順序排列:第一個不同的字元決定結果。"pear" 和 "peach" 到索引 3 之前都相同,在那裡 'r' 排在 'c' 之後,所以結果是正的。只依賴它的正負號:Quick Reference 只保證這一點。
本課程只用 equals 來比較 String 與另一個 String。