Java 基礎與 JVM 生態:給 JS/TS 開發者的入門
JVM 怎麼運作、跟 Node/Go 有什麼不一樣、基本語法怎麼寫——Java 學習系列第一篇,從已經會的 JS/TS 開發經驗出發,不用從程式設計基礎重學一次。
系列第一篇。這篇打基礎:JVM 怎麼運作、跟 Node/Go 的差異在哪、基本語法怎麼寫。物件導向、Collections、例外處理寫在下一篇〈Java 物件導向核心與 Collections、例外處理〉,再下一篇是實際動手用 Spring Boot 寫一個 API 的〈Spring Boot 入門:給 Next.js/Go 開發者的第一個 REST API〉。
一、先搞懂 JVM 在幹嘛
平常寫 Node,node app.js 直接把 JS 原始碼丟給 V8 引擎執行(內部其實還是有 JIT 編譯,但對開發者來說是「一步到位」的體感)。寫 Go,go build 編譯成一個平台專屬的原生執行檔,./app 直接跑,不需要另外裝執行環境。
Java 是第三種模式,分兩個階段:
YourCode.java --javac--> YourCode.class(bytecode) --java--> JVM 執行
原始碼 編譯器 位元碼(不是機器碼) JVM 直譯/JIT 編譯成機器碼
關鍵差異:.class 檔裡的 bytecode 不是任何 CPU 看得懂的機器碼,是 JVM 自己定義的一種中介格式。這個 bytecode 拿到 Windows、Mac、Linux 上的 JVM 都能跑——這正是 Java 當年那句口號「Write Once, Run Anywhere」的實際機制:你只要確保目標機器上有裝對應的 JVM,不需要像 Go 那樣針對每個平台分別編譯出不同的執行檔。
三個常見縮寫分清楚:
| 縮寫 | 全名 | 是什麼 |
|---|---|---|
| JVM | Java Virtual Machine | 真正執行 bytecode 的虛擬機器,負責記憶體管理(GC)、執行期優化(JIT) |
| JRE | Java Runtime Environment | JVM + 標準函式庫,只能「跑」Java 程式,不能編譯 |
| JDK | Java Development Kit | JRE + 編譯器(javac)、除錯工具等開發用工具,寫程式要裝這個 |
日常開發只要記得:裝 JDK(它已經包含 JRE 跟 JVM),javac 負責編譯,java 負責執行。
二、靜態型別:比 TypeScript 更硬的那種
TypeScript 的型別系統是編譯期的:tsc 編譯完之後,型別資訊整個消失,瀏覽器或 Node 執行的是純 JS,執行期完全沒有型別檢查這回事——這也是為什麼你可以用 // @ts-ignore 硬闖,或是執行期塞一個型別不對的值進去也不會馬上出事。
Java 的型別檢查發生在編譯期(javac 階段,型別不對直接編譯失敗),但型別資訊在執行期並沒有完全消失——每個物件在執行期都帶著自己的 class 資訊,可以用 instanceof 或 getClass() 在執行期查詢一個物件實際是什麼型別。這點常常被誤解成「Java 跟 TS 一樣是編譯期擦除」,其實不是:
Object obj = "hello";
System.out.println(obj instanceof String); // true,執行期真的能查
System.out.println(obj.getClass().getName()); // java.lang.String
泛型(Generics)是唯一比較接近「型別擦除」的部分——List<String> 編譯完之後,執行期的 bytecode 看到的其實是 raw 的 List,泛型參數 <String> 主要是編譯期用來幫你擋型別錯誤,這個之後在 Collections 那篇會再細講。
三、基本型別 vs 參考型別
這是 JS 開發者第一次寫 Java 最容易卡住的地方。JS 裡「原始型別」(number/string/boolean)跟「物件」在使用上其實感覺差不多,都能直接 .toString()。Java 把兩者分得很開:
int age = 25; // 基本型別(primitive),直接存值,不是物件
Integer boxedAge = 25; // 參考型別,是物件,裡面包了一個 int
基本型別(int, double, boolean, char...) | 參考型別(Integer, String, 自訂 class...) | |
|---|---|---|
| 存的是什麼 | 值本身 | 指向物件的參考(類似指標) |
| 預設值 | 0 / false / ' ' | null |
能不能是 null | 不行 | 可以 |
能不能放進 List<T> 這類泛型容器 | 不行,要用對應的包裝類別(Integer) | 可以 |
int 跟 Integer 之間的轉換(autoboxing/unboxing)大部分時候 Java 會自動幫你做,但知道背後在做什麼很重要——例如 Integer 是物件,== 比的是參考位址不是值,這是新手很容易踩的雷:
Integer a = 200;
Integer b = 200;
System.out.println(a == b); // false!比的是物件參考
System.out.println(a.equals(b)); // true,比較值才要用 equals
(小整數 -128 到 127 之間因為 JVM 有做快取,== 剛好會是 true,這也是這個陷阱常常被輕描淡寫過去的原因——換成 200 這種範圍外的數字才會現出原形。)
四、方法、流程控制:語法對照一輪
Java 每個檔案要對應一個 public class,main 方法是程式進入點,寫法比 JS 囉唆一點,但邏輯結構完全對得上你已經會的東西:
public class Hello {
public static void main(String[] args) {
String name = "Tommy";
int score = 85;
if (score >= 60) {
System.out.println(name + " passed");
} else {
System.out.println(name + " failed");
}
for (int i = 0; i < 3; i++) {
System.out.println("round " + i);
}
// Java 14+ 的 switch expression,寫法比傳統 switch 精簡,
// 跟 JS 的 switch 比,少了 fallthrough 忘記寫 break 的坑
String grade = switch (score / 10) {
case 10, 9 -> "A";
case 8 -> "B";
case 7 -> "C";
default -> "F";
};
System.out.println(grade);
}
}
跟 TS 對照,幾個明顯不同:
| TypeScript | Java |
|---|---|
let x: number = 1 | int x = 1;(型別寫在變數名前面,不是後面) |
function foo(x: number): string | String foo(int x) { ... }(回傳型別寫在方法名前面) |
console.log(x) | System.out.println(x) |
| 檔案就是模組,想寫幾個函式就寫幾個 | 每個 .java 檔案基本對應一個 public class,方法一定要包在 class 裡,沒有「裸函式」這回事 |
=== 比對值(基本型別)或參考(物件) | 基本型別 == 比值;物件 == 一律比參考,比值要用 .equals() |
五、Maven / Gradle:對照 package.json
Node 專案靠 package.json 記依賴、npm install 抓套件。Java 生態常見兩套對應的建置工具:Maven(設定檔是 XML,pom.xml)跟 Gradle(設定檔是 Groovy/Kotlin DSL,build.gradle)。功能上做的事跟 npm 差不多:宣告依賴、抓套件、跑編譯/測試/打包。
Maven 的 pom.xml 長這樣:
<project>
<groupId>com.tommy</groupId>
<artifactId>hello-app</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.3.0</version>
</dependency>
</dependencies>
</project>
對照關係大概是:groupId + artifactId + version 這組三元組相當於 package.json 裡的 name + version,<dependencies> 相當於 package.json 的 dependencies。mvn install 對應 npm install,mvn package 打包成一個 .jar(相當於整包編譯產物,概念上有點像 Go 編譯出的單一執行檔,只是 .jar 還是要靠 JVM 執行,不是原生機器碼)。
下一篇會進到 Java 真正的核心——物件導向設計,以及每天寫 Java 都會用到的 Collections、泛型、例外處理。這幾樣才是「寫起來像 Java」的關鍵,這篇的基礎打完,後面會順很多。