Java 變數、型別與輸入輸出
從靜態強型別到自動裝箱:搞懂 Java 為什麼這樣設計,以及它與 Python 刻意分道揚鑣之處
從一支跑在 JVM 上的計算機開始
想像你要寫一支小程式:請使用者輸入合購教科書的書價、運費與分攤人數,算出每人該付多少。在 Python 裡,你大概打開直譯器就能即興敲完。但在 Java 裡,事情從一開始就不太一樣——你得先建立一個 class,把程式碼放進 main 方法,宣告每個變數時還得明白寫出它的「型別」。這些看似囉嗦的儀式,其實正是 Java 的性格所在:它是一門靜態強型別(statically strongly-typed)、物件導向優先(OOP-first)、跑在 JVM(Java Virtual Machine)上的語言。
如果你已經讀過本專區的 Python 篇,這一篇的目標不是把 Python 的內容換個語法重講一次,而是帶你看清 Java「為什麼要這樣設計」,以及它與你熟悉的語言在哪些地方刻意分道揚鑣。建議你準備好 JDK(Java Development Kit),把每段程式存成 .java 檔,親手 javac 編譯、java 執行一次——你會對「編譯期」這三個字產生具體的身體感。

第一個 Java 程式:儀式背後的意義
先看一支最小但完整的 Java 程式:
public class Hello {
public static void main(String[] args) {
System.out.println("你好,Java");
}
}
把它存成 Hello.java(檔名必須與 public class 名稱一致,這是 Java 的硬性規定),然後:
javac Hello.java // 編譯,產生 Hello.class(位元組碼 bytecode)
java Hello // 由 JVM 執行 Hello.class
// 輸出:你好,Java
這裡藏著 Java 與 Python 最根本的差異。Python 是直譯執行,原始碼直接餵給直譯器;Java 則是先編譯成位元組碼(bytecode),再交由 JVM 執行。位元組碼不是某顆 CPU 的機器碼,而是 JVM 這部「虛擬機器」看得懂的中介指令。正因如此,同一份 .class 檔可以在 Windows、Linux、macOS 上不改一行就跑——這就是 Java 當年那句口號「Write Once, Run Anywhere」的技術基礎。
main 方法是程式進入點,String[] args 是命令列參數。System.out.println 則是把字串印到標準輸出。注意這裡的一切都包在 class Hello 裡:Java 沒有「游離在外」的程式碼,每行邏輯都得隸屬於某個類別,這正是「OOP-first」的體現。相較於 Python 允許你寫一個只有 print("hi") 一行的腳本,Java 要求你先搭好物件導向的骨架。
宣告變數:型別寫在名字前面
在 Java 裡建立變數,必須先講清楚它是什麼型別:
int bookPrice = 720; // 整數
int shipping = 60;
int people = 3;
int total = bookPrice + shipping;
int perPerson = total / people; // 整數除法
System.out.println(total); // 輸出:780
System.out.println(perPerson); // 輸出:260
int bookPrice = 720; 這行做了三件事:宣告一個名為 bookPrice 的變數、規定它的型別是 int、把 720 賦給它。和 Python 不同,你不能之後把字串塞進 bookPrice:
int bookPrice = 720;
bookPrice = "免費"; // 編譯錯誤!incompatible types: String cannot be converted to int
這就是「靜態強型別」的核心:型別在編譯期就被檢查並固定下來。「靜態(static)」指的是型別在編譯時確定,不是執行到那行才知道;「強型別(strong)」指的是 Java 不會偷偷幫你把字串硬轉成數字。相較於 Python 那種「變數只是貼在值上的標籤、隨時可換」的動態型別,Java 用一點前期的囉嗦,換取編譯期就能攔下一大票錯誤的安全感——許多在 Python 裡要跑到那一行才爆的型別錯誤,在 Java 裡你連編譯都過不了。
Java 變數命名慣例是 camelCase(小駝峰),例如 perPerson、studentCount;類別名則用 PascalCase(大駝峰),例如 BankAccount;常數用全大寫加底線:
final double TAX_RATE = 0.05; // final 表示這個值不可再改,等同其他語言的常數
加上 final 後若再試圖改它,一樣是編譯期就被擋下。
八種基本型別:直接是值,不是物件
Java 的型別世界分成兩大陣營。第一陣營是基本型別(primitive types),總共八種,它們直接儲存值本身,不是物件:
// 整數家族
byte b = 100; // 8 位元,範圍 -128 ~ 127
short s = 30000; // 16 位元
int i = 2_000_000_000; // 32 位元,最常用(底線只是讓數字好讀)
long big = 9_000_000_000L; // 64 位元,字面值要加 L
// 浮點數家族
float f = 3.14f; // 32 位元,字面值要加 f
double d = 3.141592653589; // 64 位元,浮點數預設用這個
// 其他
char c = 'A'; // 16 位元 Unicode 字元,用單引號
boolean flag = true; // 只有 true / false
幾個容易踩的點:long 的字面值要加 L、float 要加 f,否則編譯器會把 9000000000 當成 int(超出範圍)或把 3.14 當成 double(塞不進 float)。char 用單引號且只能裝一個字元,這跟字串的雙引號是兩回事。boolean 只認 true / false,不能像某些語言那樣把整數 0 當成假——這又是強型別的展現。
相較於 Python 的 int 可以無限大、不會溢位,Java 的 int 是固定 32 位元,會溢位:
int max = 2_147_483_647; // int 的上限
System.out.println(max + 1); // 輸出:-2147483648(溢位繞回最小值!)
這不是 bug,而是固定寬度整數的數學現實。需要更大範圍時改用 long,需要任意精度時改用 java.math.BigInteger。
包裝類別與自動裝箱:當基本型別需要「變成物件」
八種基本型別每一種都有對應的包裝類別(wrapper class),它們是真正的物件:
| 基本型別 | 包裝類別 |
|---|---|
int |
Integer |
double |
Double |
char |
Character |
boolean |
Boolean |
long |
Long |
為什麼需要包裝類別?因為 Java 的許多容器(如 ArrayList、HashMap)只能裝物件,不能直接裝基本型別。當你想把一串整數放進 List,就得用 Integer 而非 int:
import java.util.ArrayList;
import java.util.List;
List<Integer> scores = new ArrayList<>();
scores.add(90); // 這裡發生「自動裝箱」:int 90 自動變成 Integer
scores.add(85);
int first = scores.get(0); // 「自動拆箱」:Integer 自動變回 int
System.out.println(first); // 輸出:90
scores.add(90) 表面上塞進去的是 int,但 Java 編譯器會自動幫你把它裝箱(autoboxing)成 Integer 物件;取出時又自動拆箱(unboxing)回 int。這層糖衣讓程式碼讀起來很順,但它在背後悄悄做了不少事——這正是後面「深入探討」要拆解的陷阱來源。
包裝類別還提供實用的工具方法與常數:
int maxInt = Integer.MAX_VALUE; // 2147483647
int parsed = Integer.parseInt("256"); // 把字串轉成 int,很常用!
double pi = Double.parseDouble("3.14");
System.out.println(parsed + 1); // 輸出:257
Integer.parseInt 在處理使用者輸入時幾乎天天用到,請記住它。
String:不是基本型別,而且「不可變」
String 在 Java 裡是物件,不是基本型別——但它常用到享有特殊待遇(可以用 "..." 字面值與 + 串接):
String name = "Uedu";
String greeting = "你好," + name + "!"; // + 可以串接字串
System.out.println(greeting); // 輸出:你好,Uedu!
System.out.println(name.length()); // 輸出:4
System.out.println(name.toUpperCase()); // 輸出:UEDU
System.out.println(name.charAt(0)); // 輸出:U
String 有一個關鍵性質:不可變(immutable)。一旦建立,內容就無法更改,所有看似「修改」的方法其實都回傳一個新的 String:
String s = "abc";
s.toUpperCase(); // 回傳新字串 "ABC",但沒接住
System.out.println(s); // 輸出:abc(原字串完全沒變!)
s = s.toUpperCase(); // 要把回傳值接回去才有效
System.out.println(s); // 輸出:ABC
還有一個初學者最常踩的雷:比較字串內容絕不能用 ==。== 比的是「是不是同一個物件」(記憶體位址),equals() 才比內容:
String a = new String("hello");
String b = new String("hello");
System.out.println(a == b); // 輸出:false(兩個不同物件)
System.out.println(a.equals(b)); // 輸出:true(內容相同)
口訣:比較字串內容永遠用 .equals()。這跟 Python 用 == 比字串內容的習慣正好相反,請特別留意。
讀取輸入:Scanner
要讀使用者從鍵盤輸入的資料,最常見的入門做法是 Scanner:
import java.util.Scanner;
public class Echo {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("請輸入你的名字:");
String name = sc.nextLine(); // 讀一整行字串
System.out.print("請輸入你的年齡:");
int age = sc.nextInt(); // 讀一個整數
System.out.println(name + ",明年你就 " + (age + 1) + " 歲了");
sc.close();
}
}
Scanner 提供 nextLine()(讀整行)、next()(讀一個詞)、nextInt()、nextDouble() 等方法,會自動幫你把文字轉成對應型別——這省去了手動 Integer.parseInt 的麻煩。注意 import java.util.Scanner;:Java 把標準函式庫切成許多套件(package),用到哪個類別就要 import 哪個,這跟 Python 的 import 精神相通,但 Java 的標準庫更龐大、組織更分明,這也是它「企業生態」厚實的一面。
輸出:System.out 的三種寫法
輸出最常用 System.out,有三種寫法各有用途:
System.out.print("不換行"); // 印完不換行
System.out.println("換行"); // 印完自動換行
// printf:格式化輸出,控制小數位數、對齊
double price = 786.6666;
System.out.printf("每人付 %.2f 元%n", price);
// 輸出:每人付 786.67 元
printf 的 %.2f 表示「浮點數保留兩位小數」,%n 是跨平台的換行符。當你要漂亮地對齊報表或控制小數位數時,printf 比字串串接乾淨許多。
動手寫一段:教科書分帳計算機
把這篇學到的東西串起來,寫一支完整可執行的小程式:
import java.util.Scanner;
public class SplitBill {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.print("書價:");
int bookPrice = sc.nextInt();
System.out.print("運費:");
int shipping = sc.nextInt();
System.out.print("分攤人數:");
int people = sc.nextInt();
int total = bookPrice + shipping;
double perPerson = (double) total / people; // 強制轉型避免整數除法
System.out.println("總金額:" + total + " 元");
System.out.printf("每人應付:%.2f 元%n", perPerson);
sc.close();
}
}
假設輸入 720、60、3,輸出會是:
書價:720
運費:60
分攤人數:3
總金額:780 元
每人應付:260.00 元
若輸入 720、40、3,total 是 760,每人 253.33——這裡 (double) total 這個強制轉型(cast)很關鍵。如果寫成 total / people,兩個都是 int,Java 會做整數除法直接捨去小數,得到 253。先把 total 轉成 double,整個算式才會用浮點數運算。這個「整數除法陷阱」是初學者最常犯的計算錯誤之一。
常見錯誤
整理幾個 Java 新手最容易踩的雷:
- 用
==比較字串內容。==比物件身分,內容比較一律用.equals()。這是面試與作業裡最高頻的 bug。 - 整數除法吃掉小數。
5 / 2在 Java 裡是2不是2.5。需要小數結果時,至少有一邊要是double(用字面值5.0或強制轉型(double))。 long忘了加L、float忘了加f。long x = 9000000000;會編譯錯誤,因為右邊先被當成超出範圍的int。- 檔名與 public class 名稱不一致。
public class Hello必須存成Hello.java,大小寫也要完全相同,否則javac直接拒編。 - 拿可能是
null的包裝類別做運算。例如把Integer變數設為null後又拿去+ 1,會在拆箱時丟出NullPointerException——下一段會細談。
重點回顧
- Java 是靜態強型別:型別在編譯期固定且嚴格檢查,用前期的明確性換取編譯期的安全。
- 八種基本型別(
byte/short/int/long/float/double/char/boolean)直接存值;每種都有對應的包裝類別物件。 String是物件、不可變,內容比較用.equals()。- 輸入用
Scanner,輸出用System.out.print/println/printf,都要記得相應的import。 - Java 程式一律包在
class裡,先編譯成位元組碼再由 JVM 執行,這是它跨平台與 OOP-first 的根基。
深入探討(研究所視角)
基本型別 vs 參考型別:記憶體與效能的真實差異
要理解 Java 為什麼同時存在 int 和 Integer,得從記憶體模型看起。JVM 把記憶體大致分成兩塊:堆疊(stack)與堆積(heap)。
基本型別的區域變數(如方法裡的 int x = 5)直接存在堆疊上,那塊空間就是值本身——int 佔 4 個位元組,沒有額外開銷,讀寫是 $O(1)$ 且極快。
參考型別(所有物件,包含 Integer、String、陣列)則不同。變數本身在堆疊上存的只是一個參考(reference,可理解為指向堆積的位址),真正的物件資料躺在堆積裡。所以一個 Integer 物件除了那 4 位元組的數值,還要背負物件標頭(object header,64 位元 JVM 上通常 12–16 位元組)與對齊填補,外加堆疊上那根參考——把一個 int 包成 Integer,記憶體成本可能膨脹三、四倍以上。
效能上的連鎖反應有三層。其一,間接定址:讀 Integer 的值要先讀參考、再跳到堆積取數,比直接讀 int 多一跳。其二,快取局部性(cache locality):int[] 是一塊連續記憶體,CPU 快取友善;Integer[] 存的是一串散落在堆積各處的參考,逐一追蹤時頻繁 cache miss。其三,垃圾回收(GC)壓力:每個 Integer 都是 GC 要追蹤、回收的物件,大量裝箱會製造海量短命物件,加重 GC 負擔。這也是為什麼處理大量數值的高效能程式碼,會刻意全程用基本型別陣列,而不是 List<Integer>。
順帶一提,這正是 Java「自動記憶體回收」這項設計的代價與便利的一體兩面:你不必像 C/C++ 那樣手動 free,JVM 的 GC 會幫你回收不再被參考的堆積物件;但代價是你失去了對記憶體佈局的細緻控制,且 GC 本身要耗 CPU。理解 stack/heap 之分,才能在便利與效能之間做出有意識的取捨。
自動裝箱的陷阱
自動裝箱讓 List<Integer> 寫起來很順,但它在三個地方會咬人。
陷阱一:Integer 快取造成 == 行為不一致。 Java 為了省記憶體,會把 −128 到 127 之間的 Integer 預先快取成共用物件。於是:
Integer a = 100, b = 100;
System.out.println(a == b); // 輸出:true(落在快取範圍,是同一物件)
Integer c = 200, d = 200;
System.out.println(c == d); // 輸出:false(超出快取,各自 new 一個物件)
同一段邏輯,只因數值大小不同就給出相反結果——這是教科書級的坑。對 Integer 比值請一律用 .equals() 或先拆箱成 int 比,永遠不要用 == 比兩個包裝物件的值。
陷阱二:拆箱時的 NullPointerException。 包裝類別可以是 null,基本型別不行。當一個 null 的 Integer 被自動拆箱,就會炸:
Integer score = null; // 例如從某查詢回傳,查無資料
int x = score + 1; // 自動拆箱 score.intValue(),對 null 呼叫方法 → NullPointerException
危險在於這顆地雷外觀無辜——score + 1 看起來只是個加法,型別卻在背後悄悄拆箱。處理可能為 null 的包裝值時,務必先判空。
陷阱三:迴圈裡的隱形裝箱開銷。 下面這段把累加器宣告成 Long(包裝類別):
Long sum = 0L;
for (long i = 0; i < 1_000_000; i++) {
sum += i; // 每一輪:拆箱 sum → 相加 → 把結果裝箱成新 Long 物件
}
每次 sum += i 都會拆箱、運算、再裝箱出一個新的 Long 物件,一百萬輪就製造一百萬個垃圾物件,速度可能比用 long 慢上一個數量級。修法很單純:累加器宣告成基本型別 long sum = 0L; 即可。
這三個陷阱的共同根源,是自動裝箱把「基本型別」與「物件」之間的轉換藏進了語法糖。糖衣本身是好設計,讓 Java 在保留高效能基本型別的同時,也能無縫接上以物件為中心的泛型容器與整個 OOP 生態。但作為寫程式的人,你必須隨時清楚:眼前這個變數,究竟是堆疊上的一塊值,還是堆積裡的一個物件?看穿這層糖衣,正是從會用 Java 邁向懂 Java 的分水嶺。