Home
探索 Uedu
學生控制台
註冊會員/登入
研究知情同意中心
教師控制台
課程設定
支援與訊息
Uptime 數據

UeduGPTs

--

Jupyters

4

UG26 CISOSE26
臺北 AQI 46 · 臺中 AQI 28 · 臺南 AQI 24 · 高雄 AQI 33

AI 回覆桌面通知

AI 助教回覆完成時顯示桌面通知

聊天訊息通知

同學在討論區發送訊息時通知

聲音通知

每當有新通知時播放提示音

Java 入門與 JVM

Java 入門與 JVM:一次編寫,到處執行

從 javac 與 java 兩道指令出發,看懂位元碼、JVM 與靜態強型別,理解 Java 為何介於 C++ 與 Python 之間、又為何被企業愛用三十年。

把同一份程式丟進筆電、伺服器、手機,全都跑得起來

想像一個情境:你寫了一支計算成績平均的小工具,組員用 Windows、助教用 Mac、學校伺服器跑 Linux。如果是 C++,你得在三種平台各自重新編譯一次,還要祈禱沒踩到平台差異;如果是 Python,得確保每台機器都裝對版本的直譯器和套件。而 Java 給了一個讓企業愛了三十年的承諾:Write Once, Run Anywhere(一次編寫,到處執行)——你只編譯一次,產出的成品丟到任何裝有 Java 的機器都能跑。

這篇文章不打算把 Python 篇的內容換個語法重講一遍。如果你已經讀過本專區的 Python 篇與計算機概論概念篇,這裡會聚焦在 Java 真正不一樣的地方:那顆叫做 JVM 的虛擬機器、嚴格的靜態強型別、以類別(class)為核心的設計,以及為什麼銀行、電信、電商後台清一色用 Java。我們會一路寫程式、編譯、執行,親手感受這些差異。

Java 入門與 JVM概念示意圖

從一份原始碼到能跑的程式:多了一道「編譯」

Python 是直譯式語言:你寫好 .py,直接 python hello.py,直譯器一行一行邊讀邊跑。Java 不一樣,它在「寫程式」和「執行」之間,多插了一道編譯(compile)步驟。

先看最小的一支 Java 程式。請開一個檔案,檔名一定要是 Hello.java(稍後會解釋為什麼):

public class Hello {
    public static void main(String[] args) {
        System.out.println("你好,Java!");
    }
}

接著在終端機做兩件事:

javac Hello.java     # 編譯:產出 Hello.class
java Hello           # 執行:跑 Hello.class

執行後會看到:

你好,Java!

注意這裡出現了兩個指令,這正是 Java 與 Python 在工具鏈上的根本差異:

  • javac(Java Compiler,Java 編譯器):把人讀的原始碼 Hello.java 翻譯成一個 Hello.class 檔。
  • java(Java Launcher,啟動器):載入 .class 並實際執行。

那個 Hello.class 不是給人看的,它裝的是位元碼(bytecode)。如果你好奇它長什麼樣,可以用 javap -c Hello 反組譯看看,會出現一堆像 getstaticldcinvokevirtual 的指令。這就是 Java 的中間產物。

位元碼與 JVM:「一次編寫,到處執行」的真相

這裡是 Java 最關鍵的設計,也是它和 C++ 分道揚鑣的地方。

C++ 的編譯器會把原始碼直接翻成特定 CPU 與作業系統的機器碼。在 Windows x86 編出來的執行檔,搬到 Mac 的 ARM 晶片上完全跑不動,因為機器碼是綁死平台的。

Java 多繞了一層。javac 不直接產生機器碼,而是產生位元碼——一種介於原始碼與機器碼之間、不綁任何特定 CPU 的「中間語言」。真正負責把位元碼變成當下這台機器能跑的動作的,是 JVM(Java Virtual Machine,Java 虛擬機器)

   Hello.java
       │  javac(編譯,只做一次)
       ▼
   Hello.class(位元碼,跨平台)
       │  java(啟動 JVM)
       ▼
 ┌─────────────────────────────┐
 │  JVM(每個平台各有一套)       │
 │  Windows JVM / Mac JVM / ... │
 └─────────────────────────────┘
       │
       ▼
   各平台的機器碼 → CPU 執行

關鍵在於:位元碼只有一份,JVM 有很多種。Oracle、各家廠商針對 Windows、macOS、Linux、x86、ARM 各自實作了對應的 JVM。同一個 Hello.class,丟到任何裝有 JVM 的機器,由那台機器的 JVM 負責「翻譯落地」。所以「一次編寫,到處執行」這句話完整來說是:一次編譯成位元碼,靠各平台的 JVM 到處執行

對照三種語言的定位,差異就清楚了:

C++ Python Java
執行方式 編譯成機器碼 直譯器逐行跑 編譯成位元碼,JVM 執行
跨平台 每平台重新編譯 靠直譯器抹平 位元碼一份,JVM 抹平
速度 最快(貼近硬體) 較慢 接近 C++(靠 JIT,下面會談)
型別檢查 編譯期 執行期 編譯期(強型別)

可以說 Java 站在 C++ 與 Python 中間:它要 C++ 等級的執行效率與型別安全,又要 Python 等級的跨平台便利。代價是多了一道編譯,以及需要先裝 JVM。

class 與 main:一切都得包在類別裡

回頭看那支 Hello,逐行拆解,會看到 Java 與 Python 在「程式長相」上的第二個大差異。

public class Hello {              // 一切都得放進某個 class
    public static void main(String[] args) {   // 程式的進入點
        System.out.println("你好,Java!");
    }
}
  • public class Hello:Java 是 OOP-first(物件導向優先)的語言。在 Python 你可以寫一個沒有任何 class 的腳本;但在 Java,所有程式碼都必須住在某個 class 裡,連最簡單的 Hello World 也不例外。
  • 檔名規定:一個 .java 檔裡的 public 類別,名稱必須與檔名完全一致。public class Hello 就一定要存成 Hello.java,連大小寫都不能錯。這是初學者第一個常踩的雷。
  • public static void main(String[] args):這是程式的進入點(entry point)。當你執行 java Hello,JVM 會去找這個固定簽章的方法開始跑。這串字每個字都有意義:public(外界可呼叫)、static(不需先建立物件就能呼叫)、void(不回傳值)、main(固定名稱)、String[] args(接收命令列參數)。簽章寫錯一個字,程式就找不到進入點。
  • System.out.println(...):印出一行字。相當於 Python 的 print(),但你看得出它是「呼叫 Systemout 物件的 println 方法」——非常物件導向的長相。

相較於 Python 三行就能跑的腳本,Java 的「儀式感」確實重。但這份結構性,正是它在大型團隊協作時的優勢來源。

靜態強型別:在還沒執行前就抓出錯誤

Java 與 Python 第三個根本差異是型別系統。Python 是動態型別,變數的型別等到執行時才確定;Java 是靜態強型別(statically strongly typed),每個變數宣告時就要寫清楚型別,編譯器會在執行前替你檢查。

public class Types {
    public static void main(String[] args) {
        int age = 20;            // 整數,必須宣告型別
        double score = 87.5;     // 浮點數
        String name = "小明";     // 字串
        boolean passed = true;   // 布林值

        // age = "二十";   // ← 這行若解除註解,編譯期就直接報錯!

        System.out.println(name + " 今年 " + age + " 歲,分數 " + score);
    }
}

輸出:

小明 今年 20 歲,分數 87.5

注意被註解掉的 age = "二十";:在 Python 裡你可以隨時把字串塞進原本放整數的變數,直到執行到那行炸掉才知道錯了。但在 Java,這種型別不合的指派根本通不過 javac——程式連產出 .class 的機會都沒有。

這就是靜態型別的價值:很多錯誤被攔在編譯期,而不是上線後才在使用者面前爆炸。對一個十人團隊維護的數十萬行系統來說,這道「編譯期防線」省下的除錯時間是巨大的。代價是你得多打很多字、彈性比 Python 低。這是一種刻意的取捨。

動手寫一段:成績分級小工具

把前面學到的東西串起來,寫一支完整可執行的程式:讀入一組分數,算平均,並判斷等第。請存成 Grades.java

public class Grades {
    public static void main(String[] args) {
        int[] scores = {88, 72, 95, 60, 45};   // 陣列,型別固定為 int

        int sum = 0;
        for (int s : scores) {     // for-each:走訪每個元素
            sum += s;
        }
        double average = (double) sum / scores.length;   // 強制轉型避免整數除法

        System.out.println("共有 " + scores.length + " 筆成績");
        System.out.println("平均:" + average);

        for (int s : scores) {
            System.out.println(s + " 分 → " + grade(s));
        }
    }

    // 自訂方法:把分數轉成等第
    static String grade(int score) {
        if (score >= 90) return "A";
        else if (score >= 80) return "B";
        else if (score >= 70) return "C";
        else if (score >= 60) return "D";
        else return "F";
    }
}

執行 javac Grades.javajava Grades,預期輸出:

// 輸出:
// 共有 5 筆成績
// 平均:72.0
// 88 分 → B
// 72 分 → C
// 95 分 → A
// 60 分 → D
// 45 分 → F

這支程式裡藏了一個很多人會踩的雷:(double) sum / scores.length。如果寫成 sum / scores.length,兩個都是 int,Java 會做整數除法,$360 / 5 = 72$ 看起來剛好沒事,但若是 $361 / 5$ 就會得到 72 而非 72.2——小數直接被無條件捨去。先用 (double) 把其中一個轉成浮點數,整個運算才會走浮點除法。這類「型別決定運算行為」的細節,正是靜態型別語言要特別留意的地方。

自動記憶體回收:不用像 C++ 那樣手動還記憶體

Java 還繼承了一個讓它比 C++ 好寫的特性:自動記憶體回收(Garbage Collection,GC)

在 C++,你 new 出來的物件得自己記得 delete,忘了就記憶體洩漏,重複刪除或用到已釋放的記憶體則直接當掉。Java 把這件苦差事交給 JVM:你只管 new 出物件,當沒有任何變數再指向它時,垃圾回收器會自動把記憶體收回,你完全不必手動釋放。

public class Memory {
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {
            String temp = "暫時物件 " + i;   // 每圈都 new 一個新字串
            System.out.println(temp);
        }
        // 迴圈結束後,那些 temp 物件沒人引用了,
        // JVM 的 GC 會在適當時機自動回收,不需要你手動處理。
    }
}

這一點上 Java 和 Python 站在同一邊(Python 也有自動記憶體管理),都比 C++ 友善。差別在於 Java 的 GC 是高度工程化的,有多種演算法可選,能在企業級的大流量服務下穩定運作——這也是下一節的伏筆。

為什麼企業愛用 Java

把上面的特性收攏起來,就能理解為什麼銀行核心系統、電信帳務、電商後台、Android App 大量採用 Java:

  1. 跨平台部署省心:開發機、測試機、正式機平台不同也不必擔心,位元碼一份到處跑,JVM 抹平差異。
  2. 強型別 + 編譯期檢查:大型團隊、長壽命專案最怕「改一處壞十處」。靜態型別讓 IDE 能精準重構、提早抓錯,維護成本大幅下降。
  3. 穩定與向後相容:二十年前的 Java 程式,很多到今天仍能跑。企業系統動輒維護十幾年,這種穩定性是金錢買不到的安全感。
  4. 成熟的生態系:Spring(後端框架)、Maven/Gradle(建置工具)、龐大的函式庫與工具鏈,幾乎任何企業需求都有現成方案。
  5. 效能足夠且可預測:靠 JIT(下節詳述),執行效率接近 C++,又有成熟的 GC 與監控工具,能撐住高併發。
  6. 人才庫龐大:Java 教學資源、會 Java 的工程師都非常多,企業招募與交接相對容易。

簡言之,Java 不見得是寫起來最快、最炫的語言,但它是最適合「很多人、寫很久、不能出事」的軟體的語言。這正是企業情境的核心需求。

常見錯誤(初學者最容易踩的雷)

  • 檔名與 public 類別名不一致public class Hello 就必須存成 Hello.java,大小寫也要一致,否則 javac 直接報錯。這是新手最常見的第一個坑。
  • 忘了先 javacjava:Java 不像 Python 可以直接執行原始碼。你必須先 javac Foo.java 產出 .class,才能 java Foo。而且 java 後面接的是類別名 Foo,不是檔名 Foo.class
  • 整數除法吃掉小數int / int 的結果仍是 int。算平均、比例時記得至少把一邊轉成 double
  • main 簽章打錯public static void main(String[] args) 少一個字、型別寫錯,JVM 就找不到進入點,會丟出找不到 main 方法的錯誤。
  • 以為 Java 可以沒有 class:所有程式碼都得包在某個 class 裡,這跟 Python 能寫純腳本不同。

重點回顧

  • Java 在「寫程式」與「執行」之間多了編譯成位元碼這一步:javac 編譯、java 執行。
  • 位元碼跨平台、JVM 各平台一套,這才是「一次編寫,到處執行」的完整含義。
  • Java 是 OOP-first、靜態強型別、自動記憶體回收,定位介於 C++(快、貼硬體)與 Python(彈性、好寫)之間。
  • 企業愛用的根本原因:跨平台、編譯期防線、長期穩定、生態成熟——適合「多人、長期、高可靠」的系統。

深入探討(研究所視角)

到目前為止我們把 JVM 當黑盒子。研究所層級該打開它,看看那層「虛擬機器」內部到底發生什麼事。

JVM 架構:類別載入、執行引擎與記憶體區域

JVM 不是單純的「翻譯機」,它是一個完整的執行環境,大致由三部分組成:

  1. 類別載入子系統(Class Loader Subsystem):負責在執行期把 .class 載入記憶體,並完成載入(loading)→ 連結(linking)→ 初始化(initialization)三階段。連結階段又細分為驗證(verification)、準備(preparation)、解析(resolution)。值得注意的是 Java 採用雙親委派模型(parent delegation model):類別載入請求會先往上委派給父載入器,避免核心類別(如 java.lang.String)被惡意覆寫。

  2. 執行期資料區(Runtime Data Areas):JVM 把記憶體切成幾塊。其中堆積(Heap)存放所有物件實例,是 GC 的主要作用範圍,通常再分為新生代(Young Generation)與老年代(Old Generation),以配合分代回收(generational GC)假設「多數物件很快就死」。JVM 堆疊(Stack)則是每個執行緒一份,存放方法呼叫的區域變數與運算過程。理解這個劃分,才能解釋 StackOverflowError(堆疊爆掉,常因無窮遞迴)與 OutOfMemoryError(堆積爆掉)的本質差異。

  3. 執行引擎(Execution Engine):真正執行位元碼的核心,內含直譯器、JIT 編譯器與垃圾回收器。

位元碼驗證:在執行前確保安全

前面提到連結階段有個驗證(bytecode verification)步驟,這是 JVM 安全模型的基石,也是 C++ 沒有的環節。

位元碼可能來自不可信來源(例如網路下載),如果直接執行,惡意或損壞的位元碼可能讓堆疊溢位、跳到非法位址、把整數當指標用,破壞 JVM 的記憶體安全。驗證器會在類別載入時,對位元碼做一連串靜態檢查:型別是否一致、運算元堆疊不會上溢或下溢、跳躍目標合法、區域變數使用前已初始化等等。實作上常以資料流分析(data-flow analysis)推導每個指令處運算元堆疊與區域變數的型別狀態,確認在所有控制流路徑上都自洽。通過驗證的位元碼才被允許執行——這讓「執行不可信程式碼」(如早年的 Applet、今日的沙箱環境)在型別安全上有了保障。

JIT 即時編譯:為什麼 Java 能跑得幾乎和 C++ 一樣快

如果 JVM 只是逐條直譯位元碼,效能會明顯輸給 C++。Java 之所以能逼近原生效能,靠的是 JIT(Just-In-Time,即時編譯)

JVM 採用混合執行策略。程式剛啟動時,執行引擎先用直譯器逐條跑位元碼,啟動快但執行慢。同時 JVM 會剖析(profiling)程式的執行行為,統計哪些方法、哪些迴圈被反覆執行——這些就是所謂的熱點(hotspot)(HotSpot 也正是主流 JVM 的名字)。當某段程式碼夠熱,JIT 就把它的位元碼即時編譯成當下這台機器的原生機器碼並快取起來,之後再呼叫就直接跑原生碼,速度大幅提升。

JIT 還能做出靜態編譯器難以企及的剖析導向最佳化(profile-guided optimization),因為它握有真實的執行期資訊:

  • 方法內聯(inlining):把頻繁呼叫的小方法直接展開到呼叫處,省去呼叫開銷。
  • 去虛擬化(devirtualization):根據執行期觀察,把多型呼叫推測性地特化為直接呼叫。
  • 逃逸分析(escape analysis):若分析出某物件不會逃出方法範圍,可改在堆疊配置甚至完全消除配置,減輕 GC 壓力。

這帶來一個 Java 特有的現象:程式剛啟動時較慢(仍在直譯與剖析),跑一陣子「熱起來」後才達到巔峰效能。這也是為什麼 Java 服務常被詬病「冷啟動慢」,以及近年 GraalVM 的 AOT(Ahead-of-Time,提前編譯)原生映像會成為微服務領域的熱門解法——它用犧牲部分峰值最佳化的代價,換取近乎瞬間的啟動。

把這三件事疊起來看,JVM 的設計哲學就完整了:位元碼驗證守住安全、類別載入與記憶體區域管理生命週期、JIT 加上 GC 在執行期動態壓榨效能。Java 不是用「貼近硬體」去換速度(那是 C++ 的路),而是用一層精密的虛擬機器,在跨平台、安全、效能與可維護性之間取得了那個讓企業買單三十年的平衡點。

AI 共讀助教正在陪你讀:Java 入門與 JVM:一次編寫,到處執行
嗨!我是這篇文章的共讀助教,只根據〈Java 入門與 JVM:一次編寫,到處執行〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。