代码语言

知识点思维导图

29 个知识节点

Java(35) - A3 - 附录-JS到Java速查表

写代码时卡在「这个 JS 操作 Java 怎么写」?本篇就是给你随时翻的双向对照表。不讲原理(原理在各专题篇),只回答一个问题:我会的 JS/TS 写法,等价的 Java 是什么,以及哪里会咬人。

一、怎么用本篇

  • 正向查(JS → Java):知道 JS 怎么写,想要 Java 等价写法 → 看每张表左列找右列。
  • 反向查(Java → JS):读别人 Java 代码看不懂某个写法 → 用表格右列反查左列。
  • 表格里凡是标了 ⚠️ 的,都是长得像但行为不一样的坑,看到必须停一下。
  • 想深入某个点,跟着「详见第 X 篇」的索引去对应专题篇。

心智模型一句话:Java ≈ 强制写类型的 TS + 一切皆对象 + 先编译再运行。你已经会 TS,"类型"这关早就过了,真正的坑集中在「基本类型 vs 包装类型」「== 比的是引用」「String 不可变」这几个点上。详见第 03 篇。


二、变量与基本类型

2.1 变量声明

JavaScript / TypeScript Java
let name = "Tom" String name = "Tom";(⚠️ 类型在前,末尾加 ;
let name: string = "Tom" String name = "Tom";(TS 类型在后,Java 在前)
const AGE = 18 final int AGE = 18;finalconst,不可重新赋值)
let a, b; int a, b;(同类型可一行声明多个)
let [a, b] = [1, 2] 无解构,得 int a = 1, b = 2;(⚠️ Java 没有解构赋值)
let x = 1; x = "hi" ❌ 编译报错(⚠️ 类型定死不能换)
var x = 1(JS 老写法) var x = 1;(⚠️ Java 10+ 的 var类型推断,不是弱类型,仍是 int)

⚠️ Java 的 var 不是 JS 的 var:Java 的 var 只是让编译器推断类型(var x = 1 编译后就是 int x),推断出来后类型锁死,不能像 JS 那样再赋别的类型。只能用于局部变量。

2.2 基本类型对照

JS/TS Java 说明
number int 整数,32 位,约 ±21 亿
number long 长整数,64 位,字面量要加 L10000000000L
number double 小数(双精度),3.14 默认是 double
number float 单精度小数,字面量加 f3.14f(少用)
boolean boolean ⚠️ 全小写,字面量 true / false
string String ⚠️ 首字母大写,是对象不是基本类型
string(单字符) char 单字符,⚠️ 用单引号 'a',双引号是 String
null null 一致(但基本类型 int/double 不能为 null)
undefined ⚠️ Java 没有 undefined,未初始化的对象是 null

⚠️ 基本类型 vs 包装类型(Java 独有,重点):int 是基本类型(栈上存值),Integer 是它的包装对象。集合泛型里只能放对象,所以 List<Integer> 不能写 List<int>。详见 1.3 和第 06 篇。

基本类型 包装类型 什么时候用包装类型
int Integer 放进 List/Map 泛型、需要为 null 时
long Long 同上
double Double 同上
boolean Boolean 同上
char Character 同上

2.3 类型转换

// JavaScript
Number("42");      // 字符串转数字
String(42);        // 数字转字符串
parseInt("42px");  // 容错解析 → 42
// Java:每种类型有自己的转换静态方法
int n = Integer.parseInt("42");      // 字符串转 int → 42
String s = String.valueOf(42);       // int 转字符串 → "42"(或直接 42 + "")
double d = Double.parseDouble("3.14"); // 字符串转 double → 3.14
// ⚠️ Integer.parseInt("42px") 会抛 NumberFormatException,没有 JS parseInt 那种容错

⚠️ 自动装箱/拆箱Integer x = 5;(int 自动变 Integer)和 int y = x;(Integer 自动变 int)是编译器帮你做的,叫「装箱/拆箱」。但 Integer x = null; int y = x;NPE(拆箱一个 null)。这是很隐蔽的坑。


三、字符串操作

JavaScript Java 说明
`Hi ${name}` "Hi " + nameString.format("Hi %s", name) ⚠️ Java 没有模板字符串,用 + 拼或 format
str.length str.length() ⚠️ 是方法要加括号,不是属性
str.toUpperCase() str.toUpperCase() 一致
str.toLowerCase() str.toLowerCase() 一致
str.trim() str.trim() 一致(去首尾空白)
str.includes("x") str.contains("x") ⚠️ 方法名叫 contains
str.indexOf("x") str.indexOf("x") 一致,找不到返回 -1
str.split(",") str.split(",") ⚠️ 参数是正则split(".") 会全切光要写 split("\\.")
arr.join(",") String.join(",", list) ⚠️ 静态方法,分隔符在前
str.replace(a, b) str.replace(a, b) ⚠️ Java 的 replace 替换全部(和 JS replaceAll 一样)
str.startsWith("x") str.startsWith("x") 一致
str.slice(1, 3) str.substring(1, 3) ⚠️ 方法名叫 substring,含头不含尾
str[0] str.charAt(0) ⚠️ 不能用下标,用 charAt
str1 === str2 str1.equals(str2) ⚠️⚠️ 重灾区:比内容必须用 .equals()== 比的是引用!
// JavaScript:模板字符串
const name = "Tom", age = 18;
const msg = `${name} is ${age}, next year ${age + 1}`;
// Java:字符串拼接用 +,或用 String.format(%s 占位)
String name = "Tom";                                    // name 存名字
int age = 18;                                           // age 存年龄
String msg = name + " is " + age + ", next year " + (age + 1);  // msg 存拼好的字符串
// 或:String.format("%s is %d, next year %d", name, age, age + 1);

⚠️ String 比较必须用 .equals()(Java 头号坑,单开一节讲,见「陷阱对照」):== 比的是「是不是同一个对象」,"a" == "a" 可能碰巧为 true(字符串常量池),但 new String("a") == "a" 就是 false。记死规则:比字符串内容永远用 .equals(),绝不用 ==

⚠️ String 不可变str.toUpperCase() 不会改原字符串,而是返回一个新字符串。想拼接大量字符串用 StringBuilder,别在循环里 +(每次都新建对象,性能差)。


四、数组 / 集合

4.1 先分清:数组 vs List

⚠️ Java 里「数组」和「List」是两个东西(JS 只有一个 Array):

Java 数组 Java List(ArrayList)
长度 固定,创建后不能改 动态,可增删
写法 int[] arr = {1, 2, 3}; List<Integer> list = new ArrayList<>();
取值 arr[0] list.get(0)
长度 arr.length(属性) list.size()(方法)
类比 JS 定长的 Array 你平时用的 Array

日常几乎都用 List(≈ JS 的 Array),数组只在性能敏感或固定长度场景用。详见第 06 篇。

4.2 增删改查(List)

JavaScript Java(List) 说明
arr.push(x) list.add(x) 尾部追加
arr.pop() list.remove(list.size() - 1) ⚠️ 没有直接 pop,按索引删末尾
arr.shift() list.remove(0) 删头部
arr.unshift(x) list.add(0, x) 头部插入
arr.length list.size() ⚠️ List 用 size() 方法
arr[i] list.get(i) ⚠️ 用 get,不能用下标
arr[i] = x list.set(i, x) ⚠️ 用 set 改值
arr.includes(x) list.contains(x) 判断包含
arr.indexOf(x) list.indexOf(x) 找不到返回 -1,一致
arr.splice(i, 1) list.remove(i) 按索引删
[...a, ...b] list.addAll(b) ⚠️ 没有展开语法,用 addAll
arr.reverse() Collections.reverse(list) ⚠️ 用工具类,原地反转

4.3 遍历

// JavaScript
for (const item of arr) { ... }
arr.forEach((item, i) => { ... });
// Java:增强 for(≈ for-of),用冒号
for (String item : list) {   // ≈ for (const item of list)
    ...
}

// 需要索引时用传统 for
for (int i = 0; i < list.size(); i++) {  // i 存当前下标
    String item = list.get(i);           // item 存当前元素
    ...
}

// Java 8+ 也有 forEach(≈ JS forEach,Lambda ≈ 箭头函数)
list.forEach(item -> System.out.println(item));
JS Java
for (const x of arr) for (Type x : list)
arr.forEach(x => ...) list.forEach(x -> ...)
for (const [i, x] of arr.entries()) 传统 for + list.get(i)
for (const k in obj) for (String k : map.keySet())

⚠️ 增强 for 不能拿到索引:JS 的 forEach 回调有第二个参数 i,Java 增强 for 没有。要索引就用传统 for (int i...)


五、高阶函数:map / filter / reduce → Stream

这是前端最能无缝迁移的地方之一。Java 8 的 Stream 就是链式的 map/filter/reduce,写法和 JS 数组方法几乎一一对应,只是中间要 .stream() 开头、.collect() 收尾。详见第 06 篇。

// JavaScript:链式方法
const doubled = nums.map(x => x * 2);
const evens = nums.filter(x => x % 2 === 0);
const result = nums.filter(x => x > 0).map(x => x * 2);
// Java:.stream() 开头,.collect(Collectors.toList()) 收尾
import java.util.stream.Collectors;

// doubled 存每项翻倍后的新列表
List<Integer> doubled = nums.stream()
    .map(x -> x * 2)                    // ≈ JS 的 map
    .collect(Collectors.toList());      // ⚠️ 要收集成 List

// evens 存所有偶数
List<Integer> evens = nums.stream()
    .filter(x -> x % 2 == 0)            // ≈ JS 的 filter
    .collect(Collectors.toList());

// 过滤 + 映射链式(和 JS 一样能连起来)
List<Integer> result = nums.stream()
    .filter(x -> x > 0)
    .map(x -> x * 2)
    .collect(Collectors.toList());
JS Java Stream
arr.map(x => x * 2) stream().map(x -> x * 2).collect(toList())
arr.filter(x => x > 0) stream().filter(x -> x > 0).collect(toList())
arr.reduce((a, b) => a + b, 0) stream().reduce(0, (a, b) -> a + b)stream().mapToInt(x -> x).sum()
arr.find(x => ...) stream().filter(...).findFirst().orElse(null)
arr.some(x => ...) stream().anyMatch(x -> ...)
arr.every(x => ...) stream().allMatch(x -> ...)
arr.sort((a,b) => a-b) stream().sorted().collect(toList())

⚠️ Stream 用完就废:一个 Stream 只能消费一次,.collect() 之后就不能再用了(不像 JS 数组能反复遍历)。想再遍历得重新 .stream()

⚠️ 别忘了 .collect():JS 的 map/filter 直接返回数组,Java Stream 的中间操作是「懒」的,不 .collect()(或 .forEach())就什么都不会发生。


六、对象 / 字典(Map)

⚠️ 先分清:Java 里 JS 的「对象」拆成了两个概念——装数据的键值对用 Map有固定字段的实体用 class(见第九节)。JS 的 {key: value} 当字典用时对应 Java 的 Map。

JavaScript Java(Map) 说明
const obj = {} Map<String, Object> map = new HashMap<>(); ⚠️ 要声明键值类型
obj.key / obj["key"] map.get("key") ⚠️ 用 get,不能用点号也不能用下标
obj.key = v map.put("key", v) ⚠️ 用 put,新增/覆盖
obj.key ?? "default" map.getOrDefault("key", "default") 取默认值,更安全
delete obj.key map.remove("key") 删除
"key" in obj map.containsKey("key") 判断键存在
Object.keys(obj) map.keySet() 所有键
Object.values(obj) map.values() 所有值
Object.entries(obj) map.entrySet() 键值对集合
{...a, ...b} map.putAll(b) ⚠️ 没有展开语法,用 putAll
obj?.a?.b 无直接等价 ⚠️ 没有可选链,逐层判空或用 Optional
// JavaScript:安全取值
const port = config.port ?? 8080;
// Java:map.getOrDefault(键, 默认值),键不存在时返回默认值,不报错
int port = config.getOrDefault("port", 8080);   // port 存端口号,没配就用 8080

⚠️ 遍历 Map 和 JS 不太一样:

// 遍历键值对(最常用)
for (Map.Entry<String, Integer> entry : map.entrySet()) {
    String key = entry.getKey();      // key 存当前键
    Integer value = entry.getValue(); // value 存当前值
}

// 只遍历键
for (String key : map.keySet()) { ... }

// Java 8+ 的 forEach(≈ JS 写法,最简洁)
map.forEach((key, value) -> System.out.println(key + "=" + value));

七、substring / 截取(Java 没有切片)

⚠️ Java 没有 Python 那种 arr[1:3] 切片语法,字符串用 substring,List 用 subList

// JavaScript
str.slice(1, 3);       // 字符串 索引 1~2
arr.slice(1, 3);       // 数组 索引 1~2
arr.slice(-2);         // 最后两个
// Java:字符串和 List 各有方法,都是含头不含尾
str.substring(1, 3);            // 字符串索引 1~2
str.substring(2);               // 从索引 2 到末尾
list.subList(1, 3);             // List 索引 1~2(⚠️ 返回的是视图,改它会影响原 list)

// Java 没有负索引,"最后两个" 要自己算
list.subList(list.size() - 2, list.size());
JS Java
str.slice(1, 3) str.substring(1, 3)
str.slice(2) str.substring(2)
arr.slice(1, 3) list.subList(1, 3)
arr[arr.length - 1] list.get(list.size() - 1)

⚠️ 没有负索引:JS 的 arr.slice(-2)arr[arr.length-1] 很常用,Java 全得用 size() - n 手动算,别习惯性写负数下标(会 IndexOutOfBoundsException)。


八、函数 / 方法

8.1 声明对照

// JavaScript
function add(a, b = 0) { return a + b; }
const add = (a, b) => a + b;          // 箭头函数
function sum(...nums) { ... }          // 剩余参数
// Java:方法必须写在类里,必须声明返回类型和参数类型
int add(int a, int b) {   // 返回类型 int 在最前,参数要带类型
    return a + b;
}

// Lambda ≈ 箭头函数(Java 8+),但要有个「函数式接口」类型来接
// 如 java.util.function.BiFunction
BiFunction<Integer, Integer, Integer> add = (a, b) -> a + b;

// 可变参数用 类型...(≈ JS 的 ...nums,收集成数组)
int sum(int... nums) {    // nums 是 int[]
    int total = 0;        // total 存累加和
    for (int n : nums) total += n;
    return total;
}
JS Java 说明
function f(a, b) 返回类型 f(类型 a, 类型 b) ⚠️ 必须写返回类型和参数类型
(a, b) => a + b (a, b) -> a + b Lambda,⚠️ 箭头是 -> 不是 =>
function f(...args) f(类型... args) 可变参数,收集成数组
f(...arr) 无直接等价 ⚠️ 没有展开传参,数组直接当可变参数传

⚠️ Java 没有默认参数function add(a, b = 0) 在 Java 里做不到,得用方法重载(写多个同名方法,参数不同)来模拟:

// 方法重载:同名方法,参数列表不同,编译器按调用时的参数选
int add(int a, int b) { return a + b; }   // 两个参数版本
int add(int a) { return add(a, 0); }       // 一个参数版本,内部调上面的(模拟默认值 b=0)

8.2 方法重载(Java 特有,≈ TS 函数重载但真存在多个实现)

// 同一个方法名,靠「参数类型/个数不同」区分,编译期决定调哪个
void print(int x) { ... }        // 打印整数
void print(String x) { ... }     // 打印字符串
void print(int x, int y) { ... } // 打印两个整数

⚠️ 重载只看参数,不看返回类型。两个方法只有返回类型不同、参数一样,编译报错。


九、同名不同义的关键字 / 运算符(⚠️ 重灾区)

JS Java ⚠️ 区别
=== == ⚠️ Java 的 ==对象比引用、对基本类型比值;没有 ===
==(带隐式转换) Java == 不做隐式转换,且比对象引用
值内容相等(对象) .equals() ⚠️⚠️ 比 String/对象内容必须.equals(),不是 ==
! ! 一致
&& && 一致
|| || 一致
a ? b : c a ? b : c ⚠️ 三元一致!(和 Python 不同)
null / undefined null 只有 null
typeof x x.getClass() / x instanceof C 运行时类型
x instanceof C x instanceof C 一致
this this ⚠️ 一致!(不像 Python 要写 self)
x ?? y 用三元 x != null ? x : yOptional
x?.y 逐层判空或 Optional
// JavaScript
const label = age >= 18 ? "成年" : "未成年";
if (str1 === str2) { ... }
// Java:三元和 JS 一样,但字符串比较要用 equals
String label = age >= 18 ? "成年" : "未成年";  // label 存年龄段文案(三元和 JS 完全一致)
if (str1.equals(str2)) {  // ⚠️⚠️ 比内容用 equals,不是 ==
    ...
}

⚠️ == 是 Java 头号坑== 比对象时比的是「内存地址是不是同一个」,不是内容。字符串、Integer、自定义对象比内容都要用 .equals()。只有基本类型(int/double/boolean/char)才用 == 比值。详见「陷阱对照」第一节。


十、类与面向对象

Java 是「一切皆对象」,类比 JS 反而更啰嗦但更严格。JS 一个 class 就搞定的,Java 常拆成字段 + 构造方法 + getter/setter。

// JavaScript
class User {
  constructor(name) { this.name = name; }
  greet() { return `Hi ${this.name}`; }
}
const u = new User("Tom");
// Java:构造方法名 = 类名,字段要声明类型和访问修饰符
public class User {
    private String name;              // 字段:私有,存用户名

    public User(String name) {        // 构造方法,⚠️ 方法名 = 类名,没有 constructor 关键字
        this.name = name;             // this 和 JS 一样,指当前实例
    }

    public String greet() {           // 普通方法,要声明返回类型
        return "Hi " + this.name;
    }

    // getter/setter:Java 惯例,private 字段靠它们对外读写
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
}

User u = new User("Tom");             // ⚠️ 有 new 关键字(和 JS 一样,不像 Python)
JS Java 说明
constructor() {} public 类名() {} ⚠️ 构造方法名 = 类名
this this 一致,且自动可用(不用像 Python 写 self)
new User() new User() ⚠️ 一致,都用 new
class B extends A class B extends A 继承,一致
super() super() 调父类构造,一致
static method() static 返回类型 method() 静态方法,一致
get x() getX()(普通方法约定) ⚠️ Java 无 getter 语法糖,靠命名约定
toString() @Override public String toString() 转字符串,要加 @Override
(无接口) interface / implements ⚠️ Java 有独立的接口概念,比 TS interface 强

⚠️ 字段私有 + getter/setter 是 Java 强约定:JS 里习惯直接 obj.name,Java 里字段一般 private,外部通过 getName()/setName() 访问。实际项目常用 Lombok 的 @Data 注解自动生成,你会在 demo 代码里大量见到(详见第 08 篇注解、第 18 篇)。

⚠️ 一个文件一个 public 类:Java 强制 public class User 必须放在 User.java 里,文件名和类名一致。详见第 03 篇。


十一、异步与并发(心智差异最大)

⚠️ 这里和 JS 差异最大:JS 是单线程 + 事件循环,Java 是真多线程。JS 的 async/await 心智不能直接搬。

// JavaScript:async/await 单线程事件循环
async function fetchData() {
  const data = await fetch(url);
  return await data.json();
}
await Promise.all([a(), b()]);
// Java:CompletableFuture ≈ Promise,但底层是真的多线程
import java.util.concurrent.CompletableFuture;

// supplyAsync ≈ new Promise,丢到线程池异步执行
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
    return httpClient.get(url);   // 在别的线程里跑
});

// thenApply ≈ .then(),链式处理结果
future.thenApply(data -> parse(data));

// join() ≈ await(阻塞等结果)
String result = future.join();

// allOf ≈ Promise.all(等多个都完成)
CompletableFuture.allOf(a(), b()).join();
JS Java 说明
new Promise(...) CompletableFuture.supplyAsync(...) 异步任务
.then(fn) .thenApply(fn) 链式处理
await x x.join() / x.get() ⚠️ 是阻塞当前线程,不是让出
Promise.all([...]) CompletableFuture.allOf(...) 等多个
(单线程,无此概念) new Thread(...).start() ⚠️ 真开线程
(单线程,无此概念) synchronized / Lock ⚠️ 多线程要加锁防竞争

⚠️ JS 不用担心的「线程安全」,Java 要操心:JS 单线程,永远不会两段代码同时改一个变量。Java 多线程下,多个线程同时改一个 HashMap 会出错,得用 ConcurrentHashMap 或加 synchronized 锁。这是前端转 Java 最容易忽视的心智差异。日常 Spring 开发中大部分并发被框架管了,但要知道有这回事。


十二、注解 ≈ TS/Angular 的 @Decorator

如果你写过 Angular 或 TS 的 @Component / @Injectable,那 Java 注解长得几乎一样——@ 加在类/方法/字段上面那行。这是 Spring 的基础,到处都是。详见第 08 篇、第 13 篇。

// TypeScript / Angular:你已经见过的 @ 语法
@Component({ selector: "app" })
class AppComponent {}
// Java / Spring:@注解 加在类/方法上,框架靠反射读取并处理
@RestController                    // 标记这是个 REST 控制器(≈ 注册路由的类)
public class UserController {

    @GetMapping("/users/{id}")     // 路由注解,≈ Express 的 app.get
    public User getUser(@PathVariable Long id) {  // @PathVariable 注入路径参数
        return userService.findById(id);
    }
}
TS/Angular Java/Spring 说明
@Component @Component / @Service 标记为组件/服务
@Injectable + 构造注入 @Autowired / 构造注入 依赖注入,详见第 09 篇
@Input() @RequestParam / @PathVariable 接收入参
装饰器工厂 注解 + 反射处理 机制不同但用法像

⚠️ 注解本身不做事@ 只是「贴个标签」,真正干活的是框架用反射读取这些标签后的处理逻辑(详见第 08 篇)。这点和 TS 装饰器一致。


十三、异常处理

// JavaScript
try {
  risky();
} catch (e) {
  console.error(e);
} finally {
  cleanup();
}
throw new Error("boom");
// Java:catch 要声明异常类型,throw 用 new
try {
    risky();
} catch (IllegalArgumentException e) {  // ⚠️ 可以按异常类型分别捕获
    System.out.println(e.getMessage());
} catch (Exception e) {                 // 兜底捕获所有异常
    e.printStackTrace();
} finally {
    cleanup();
}

throw new RuntimeException("boom");     // ⚠️ throw 要 new 一个异常对象
JS Java 说明
try {} try {} 一致
catch (e) catch (Exception e) ⚠️ 要声明异常类型
finally {} finally {} 一致
throw new Error(x) throw new RuntimeException(x) ⚠️ 要 new,且要指定异常类
try (资源) {} try-with-resources,自动关资源,详见第 07 篇
无区分 受检异常 vs 非受检异常 ⚠️ Java 独有,见下

⚠️ 受检异常(Checked Exception)是 Java 独有的坑:某些异常(如 IOException)编译器强制你要么 try-catch,要么在方法签名上 throws 声明,否则编译不过。JS 完全没有这个概念。RuntimeException 及其子类是「非受检」的,不强制处理。详见第 07 篇。


十四、包与导入

JavaScript Java 说明
import { User } from "./user" import com.imber.model.User; ⚠️ 用点号包路径,不是文件路径
import * as m from "./m" import com.imber.model.*; 导入整个包(用 *
export class User {} public class User {} ⚠️ 靠 public 修饰符控制可见,没有 export
文件路径找模块 package 声明 + 目录一一对应 ⚠️ 包名必须和目录结构完全一致
package.json pom.xml(Maven) 依赖清单,详见第 01 篇、A2 常用命令
npm install x pom.xml 加依赖 + mvn install 装包,详见 A2
node_modules/ ~/.m2/repository/(本地仓库) ⚠️ Maven 依赖全局共享,不在项目里
// User.java
package com.imber.model;   // ⚠️ 包声明必须和目录一致:com/imber/model/User.java

public class User { ... }   // public 让别的包能导入它(≈ export)

// Main.java
package com.imber;

import com.imber.model.User;  // 用点号路径导入

public class Main {
    public static void main(String[] args) {
        User user = new User("Tom");
    }
}

⚠️ 同包不用 import:同一个 package 里的类互相引用不需要 import(≈ JS 同目录也得 import,这点 Java 更省事)。只有跨包才 import。


十五、Web 后端:Spring Boot ≈ 带强类型的 Express

写过 Express 路由的话,Spring Boot 是「注解版 + 强类型 + 自动装配」的 Express。详见第 09~13 篇。

// Express
app.get("/users/:id", (req, data) => {
  data.json({ id: req.params.id });
});
// Spring Boot:路由用注解,参数靠注解 + 类型自动解析
@RestController                          // 标记为 REST 控制器
public class UserController {

    @GetMapping("/users/{id}")           // ⚠️ 路径参数用 {id},不是 :id
    public User getUser(@PathVariable Long id) {  // @PathVariable 自动注入 + 转类型
        return userService.findById(id); // ⚠️ 直接 return 对象,自动转 JSON
    }
}
Express Spring Boot 说明
app.get("/p/:id", fn) @GetMapping("/p/{id}") ⚠️ 路径参数花括号
app.post(...) @PostMapping(...) POST 路由
req.params.id @PathVariable Long id 路径参数,自动注入+转类型
req.query.q @RequestParam String q query 参数
req.body @RequestBody User user ⚠️ 请求体自动转成对象(≈ JSON 反序列化)
data.json(obj) return obj ⚠️ 直接 return,自动转 JSON
express.Router() @RequestMapping("/前缀") 类级注解 路由分组
手写校验 @Valid + 校验注解 参数校验,像 zod 但基于注解

⚠️ 不用手动 data.json():Spring 的 @RestController 会自动把返回的对象序列化成 JSON。直接 return user; 前端就收到 JSON 了。详见第 12 篇、第 19 篇。


十六、数据库访问:MyBatis ≈ 手写 SQL 的 ORM

前端一般不直接碰数据库,这块没有对应物,要建新锚点。Java 后端最常用 MyBatis:你写 SQL(或用注解),它帮你把结果自动映射成 Java 对象。详见第 14、15 篇。

// MyBatis Mapper 接口:定义方法,SQL 写在 XML 或注解里
@Mapper
public interface UserMapper {
    // 这个方法对应一条 SQL,返回结果自动映射成 User 对象
    User findById(Long id);
}
<!-- UserMapper.xml:SQL 写在这里,#{id} 是参数占位(防 SQL 注入) -->
<select id="findById" resultType="com.imber.model.User">
    SELECT * FROM user WHERE id = #{id}
</select>

锚点:Mapper 接口 ≈ 你调的一组「数据访问函数」,XML/注解里的 SQL ≈ 函数体,#{id} ≈ 参数化查询(≈ 前端 ORM 的占位符,防注入)。调用方 userMapper.findById(1L) 就像调普通方法。详见第 15 篇。

⚠️ #{}${} 千万别搞混#{} 是参数化占位(安全,防 SQL 注入),${} 是字符串直接拼接(有注入风险)。默认永远用 #{}。这是安全红线。


十七、命令行 / 工程速查(node 生态对照)

Node 生态 Java 生态 说明
node app.js java -jar app.jar 运行(需先编译打包)
node app.js(直接跑源码) javac X.java && java X ⚠️ Java 要先编译再运行,详见 37-Java疑问记录.md
npm init mvn archetype:generate / IDEA 建项目 初始化项目
npm install x pom.xml<dependency> 加依赖
npm install mvn install 按 pom.xml 装依赖
package.json pom.xml(Maven) 依赖清单,详见 A2
node_modules/ ~/.m2/repository/ ⚠️ 全局共享,不在项目内
npm run build mvn package 打包成 jar
npm run dev IDEA 里跑 / mvn spring-boot:run 启动开发服务
console.log(x) System.out.println(x) 打印
JSON.stringify(o) objectMapper.writeValueAsString(o) 对象转 JSON(用 Jackson 库)
JSON.parse(s) objectMapper.readValue(s, User.class) JSON 转对象

⚠️ 依赖不在项目目录里:不像 node_modules 每个项目一份,Maven 把所有依赖下载到用户目录 ~/.m2/repository/ 全局共享。项目里只有 pom.xml 声明「我要用哪些」。详见 A2 常用命令。


十八、命名风格对照

场景 JS/TS 习惯 Java 习惯
变量/方法 camelCase camelCase(一致!)
类名 PascalCase PascalCase(一致)
常量 UPPER_CASE UPPER_CASE(一致,且常配 static final
私有字段 #field / _field field(靠 private 修饰符,不改名)
文件名 userService.ts UserService.java(⚠️ 必须和 public 类名一致,PascalCase)
包名 无强约定 全小写 + 点号:com.imber.model

✅ 好消息:Java 和 JS 的变量/方法命名习惯几乎一致(都是 camelCase),比 Python 的 snake_case 更亲切。主要区别是文件名必须 PascalCase 且和类名严格一致。


十九、总结

  • 怎么用本篇:表格里凡是标了 ⚠️ 的,都是长得像但行为不一样的坑,看到必须停一下。
  • 变量与基本类型:| let x = 1; x = "hi" | ❌ 编译报错(⚠️ 类型定死不能换) |
  • 字符串操作:| str.length | str.length() | ⚠️ 是方法要加括号,不是属性 |
  • 数组 / 集合:⚠️ Java 里「数组」和「List」是两个东西(JS 只有一个 Array):
  • 高阶函数:map / filter / reduce → Stream:这是前端最能无缝迁移的地方之一。
  • 对象 / 字典(Map):| obj.key / obj["key"] | map.get("key") | ⚠️ 用 get,不能用点号也不能用下标 |