Home
Explore Uedu
Student Console
Register as Member/Login
(2) In future presentations of the research findings, in addition to the course project website and public presentations, your real name and personal information will not appear in this research report. If you are interested in the research results, we can provide you with an executive summary after the study is completed.
問卷中心
Teacher Console
Course Setup
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

0

Local AI

--

CISOSE26 本地 AI UG26
政治大學 AQI 18 29°C

AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

變數、型別與輸入輸出

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 程式:儀式背後的意義

先看一支最小但完整的 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(小駝峰),例如 perPersonstudentCount;類別名則用 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 的字面值要加 Lfloat 要加 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 的許多容器(如 ArrayListHashMap)只能裝物件,不能直接裝基本型別。當你想把一串整數放進 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 新手最容易踩的雷:

  1. == 比較字串內容== 比物件身分,內容比較一律用 .equals()。這是面試與作業裡最高頻的 bug。
  2. 整數除法吃掉小數5 / 2 在 Java 裡是 2 不是 2.5。需要小數結果時,至少有一邊要是 double(用字面值 5.0 或強制轉型 (double))。
  3. long 忘了加 Lfloat 忘了加 flong x = 9000000000; 會編譯錯誤,因為右邊先被當成超出範圍的 int
  4. 檔名與 public class 名稱不一致public class Hello 必須存成 Hello.java,大小寫也要完全相同,否則 javac 直接拒編。
  5. 拿可能是 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 為什麼同時存在 intInteger,得從記憶體模型看起。JVM 把記憶體大致分成兩塊:堆疊(stack)堆積(heap)

基本型別的區域變數(如方法裡的 int x = 5)直接存在堆疊上,那塊空間就是值本身——int 佔 4 個位元組,沒有額外開銷,讀寫是 $O(1)$ 且極快。

參考型別(所有物件,包含 IntegerString、陣列)則不同。變數本身在堆疊上存的只是一個參考(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,基本型別不行。當一個 nullInteger 被自動拆箱,就會炸:

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 的分水嶺。

AI 共讀助教正在陪你讀:Java 變數、型別與輸入輸出
嗨!我是這篇文章的共讀助教,只根據〈Java 變數、型別與輸入輸出〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。