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 入門與 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:一次編寫,到處執行〉的內容回答。可以問我「解釋某段」「舉個例子」「出題考我」,或反白文中段落後點下方「解釋選取段落」。