一、基础概念与环境安装
1.1 编译型 vs 解释型
我们写前端,浏览器直接解释 JS 运行;但是 Java 多了一道编译关卡:
.java 源码 --(javac 编译)--> .class 字节码 --(JVM 执行)--> 运行
.java文件 = Java 源码,类似我们的.vue/.ts文件(人类可读)。.class文件 = JDK 把.java编译后的产物,可由 JDK 或 JRE 运行(与平台无关的中间码)。
1.2 JDK / JRE / JVM
| 名词 | 全称 | 能编译? | 能运行? | 类比前端 |
| JDK | Java Development Kit | ✅ | ✅ | 装了 vite 依赖 + node 环境 的开发机 |
| JRE | Java Runtime Environment | ❌ | ✅ | 只装了 node 的运行环境 |
| JVM | Java Virtual Machine | --- | 真正执行字节码 | 浏览器里的 JS 引擎(JVM 和 JS 引擎内部都含有 JIT) |
**WORA(Write Once, Run Anywhere)**:字节码不绑定操作系统/CPU,任何装了 JVM 的机器都能跑。JVM 内部做「解释执行 + JIT 即时编译」,兼顾跨平台与性能。
1.3 安装
注:**IDEA 会在我们使用的时候自动弹出 JDK 缺失的弹窗,我们点击后就可以在 IDEA 内部自动下载,这里的 JDK 安装仅供此方法不生效的同学(下图展示了详细的安装步骤)**。
- JDK 装 17 (老项目用 8/11,新项目用 17/21)(可选)。
- 下载:https://www.oracle.com/java/technologies/javase/jdk17-archive-downloads.html
- Windows 下
.msi会自动配好环境变量;macOS 下.dmg即可。
- **IDE 用 IntelliJ IDEA Community(社区版)**,免费够用,没有必要使用破解版。
- 验证:
java -version看到17.x即正确。




1.4 第一个程序
TypeScript
// HelloWorld.java
public class HelloWorld {
public static void main(String[] args) {
System.out.println("你好,后端世界");
}
}
Python
javac HelloWorld.java # 生成 HelloWorld.class
java HelloWorld # JVM 运行,输出一句话
这里运行有两种方法:
- 第一种方法就是上述你们看到的,通过命令行工具 javac 进行编译,java 进行程序运行。(讲解命令行运行的主要原因是想让大家感受到,平时 AI 帮我们运行 Java 代码或者启动 Java 项目,其实都是通过各种各样的命令行命令来运行的)
- 第二种方法就是通过可视化的界面,我们鼠标直接点击这个按钮就可以直接运行该文件(IDEA 内部经过了编译和运行的步骤)。
跑完后去目录里看一眼 HelloWorld.class------那就是 javac 的编译产物。
二、八大基本数据类型
2.1 为什么类型这么多?
TS 里 let n: number 底层永远是 64 位双精度浮点,不论我们存 1 还是 10 亿。
Java 把数字按占用内存大小拆成多种------后端常处理海量数据,每个字段省几个字节,总量天壤之别。
这里埋一个彩蛋 🥚,后续会有相关面试题专门考察这里的坑。
| 类型 | 字节 | 位数 | 取值范围 | 类成员默认值 | 说明 |
| byte | 1 | 8 | -128 ~ 127 | 0 | 最小整型,省内存 |
| short | 2 | 16 | -32768 ~ 32767 | 0 | 短整数,极少用 |
| int | 4 | 32 | -2³¹ ~ 2³¹-1 | 0 | 日常最常用整数 |
| long | 8 | 64 | -2⁶³ ~ 2⁶³-1 | 0L | 超大数字,字面量末位加 L |
| float | 4 | 32 | 约 ±3.4E38(精度 6~7 位) | 0.0f | 单精度,末位加 F |
| double | 8 | 64 | 约 ±1.8E308(精度 15~16 位) | 0.0d | 默认浮点类型 |
| char | 2 | 16 | 0 ~ 65535(Unicode) | \u0000 | 单字符,可存中文 |
| boolean | 1 位 | --- | true / false | FALSE | 不能与数字互转 |
2.2 必须记住的语法细节
- 整数默认是
int;写long字面量末位加L(如10000000000L)。 - 浮点默认是
double;写float末位加F。 ""是字符串 ,''是**单字符(char)**------和 JS 里''/""混用完全不同。- **String、数组、类的实例都不是基本类型,而是「引用类型」**:变量里存的是地址,不是值本身(这点后面讲堆/栈会展开)。
Java
int a = 10;
long b = 100L; // 必须加 L
double c = 3.14; // 默认 double
float d = 3.14F; // 必须加 F
char e = '中'; // 单引号
boolean f = true;
String s = "hello"; // 引用类型,不是基本类型
三、包(package)与导入(import)
包 = 前端用来划分模块的目录 ,区别是每个 Java 类都必须用 package 声明自己属于哪个包,且有个统一规范:基础包名用公司域名倒写。
Java
package com.example.demo; // 域名倒写:com.example
import java.util.List; // 引入指定类
import java.util.*; // 通配符,引入该包下所有类
和前端
import目的一样------引入别的模块。语法差异:import 包名.类名;。
四、类、对象与实体类
4.1 类与实例(面向对象的第一直觉)
- 类 :和 TS 中的类一个意思,提供属性 和方法。
- 实例 :通过
new关键字构建出来的对象,叫实例。
5.1 实体类(和表结构一一对应)
后端最常用的一类类(突然叠词 😂):实体类(Entity)和数据库表结构一一对应。一笔订单、一个用户,就是一张表对应一个类。
5.2 构造方法
用来初始化实例 ,在 new 时自动调用。分无参构造、有参构造。
5.3 getter / setter(封装的核心)
Java 约定:**类属性默认 private(私有)**,再通过 getter/setter 暴露读写。
从这里就可以看出强类型语言和我们 JS 这种弱类型语言的明显差异。
5.4 equals / hashCode / toString
这三个方法都在 Object 基类里,所以每个 Java 对象都自带:
toString():把对象转成字符串(默认打印出来是一串看不懂的地址,所以要重写)。equals(Object o):判断两个对象「逻辑上是否相等」。hashCode():对象的哈希值,默认按存储地址算。
两个对象 equals 为 true,hashCode 必须相等 ------所以重写
equals时必须同时重写hashCode。
TypeScript
package com.demo;
public class User {
private Long id;
private String name;
public User() {} // 无参构造
public User(Long id, String name) { // 有参构造
this.id = id; this.name = name;
}
// getter / setter
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
@Override
public boolean equals(Object o) { // @Override 有这个东西就是重写
if (this == o) return true;
if (!(o instanceof User)) return false;
User u = (User) o;
return id != null && id.equals(u.id); // 按业务主键判等
}
@Override
public int hashCode() {
return id != null ? id.hashCode() : 0; // 与 equals 保持一致
}
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "'}";
}
}
六、继承与 final
- 继承 :
A extends B,表示 A 继承 B。B 里的protected和public属性和方法会被 A 继承下来。 - 不可继承 :
final class不能被extends。
Java
public class Animal {
protected String name;
public void eat() { System.out.println(name + " 在吃东西"); }
}
public class Cat extends Animal { // Cat 继承了 name 和 eat()
public void meow() { System.out.println("喵"); }
}
public final class Constants { // 加了 final,谁都不能继承它
public static final int MAX = 100;
}
七、抽象类、重写、重载、注解
7.1 抽象类(abstract)
用 abstract 标记类或方法。抽象方法强制子类必须实现 ,否则子类也得标成 abstract。
7.2 重写(Override)vs 重载(Overload)
| 重写 Override | 重载 Overload | |
| 场景 | 子类覆盖父类方法 | 同一类里同名不同参 |
| 要求 | 方法签名相同 | 只认参数列表不同 |
Java
abstract class Shape {
abstract double area(); // 抽象方法,子类必须实现
}
class Circle extends Shape {
double r;
@Override
double area() { return Math.PI * r * r; } // 重写
}
class Calculator {
int add(int a, int b) { return a + b; } // 重载 1
double add(double a, double b) { return a + b; } // 重载 2(参数不同)
// void add(int a, int b) { } // ❌ 编译错误:只改返回类型不算重载
}
7.3 注解(Annotation)
标记在类或方法上,对类/方法做描述或处理:
@Override:声明这是重写方法(编译器会帮我们校验父类是否真有该方法)。@Deprecated:标记方法/类已过期,不建议使用。
TS 语言对照:这里的注解类似于我们使用 NestJS 中的装饰器。
八、枚举与接口
8.1 枚举(enum)
「变异的类」,用来定义一组固定的复合型常量,比满地 public static final 更优雅、类型更安全。
Java
enum OrderStatus {
PENDING, PAID, CANCELLED, REFUNDED
}
OrderStatus s = OrderStatus.PAID;
8.2 接口(interface)
用来定义规范/契约 ,不关心实现。implements 一个接口,就必须实现里面的方法。
Java
interface Payable {
void pay(); // 抽象方法,默认 public abstract
}
class Order implements Payable {
@Override
public void pay() { System.out.println("支付成功"); }
}
思考灌输,超前学习:接口是后端「解耦」的核心武器。Controller 依赖 Service 接口而非具体实现,将来换实现(比如把 MySQL 换成 ES)上层代码一行都不用改。这是 Spring 的依赖倒置(DIP)。
九、常见数据结构
| 结构 | 特点 | 类比 |
| 数组 Array | 查询快(下标直取),插入慢(要挪位) | 一排带编号的储物柜 |
| 链表 Linked List | 查询慢(挨个走),插入快(改指针) | 一节节火车,加车厢只动接头 |
| 栈 Stack | 后进先出 LIFO | 炮弹,后塞的先打 |
| 队列 Queue | 先进先出 FIFO | 子弹夹,先进先出 |
| 树 Tree | 有父子关系 | 文件夹/文件、后台系统的菜单 |
十、集合(Collection / Map)与泛型
**Java 的集合就是对上面那些数据结构的「封装 + 工程化」**,我们不用自己手写链表,直接用现成的。
我们终于学到了真正的集合,而不是像在 JS 中那样强行用数组模拟各种复杂数据结构。
10.1 两大体系
Java
Collection(存单个元素)
├── List 有序可重复 → ArrayList(数组实现,查快)、LinkedList(链表,插快)
└── Set 无序不重复 → HashSet(哈希实现)
Map(存键值对 key-value)
└── HashMap(最常用)
10.2 泛型(<>)
泛型限制集合里元素的类型,编译期就能拦住「往装 String 的 list 里塞 Integer」这种错。
TypeScript
List<String> names = new ArrayList<>(); // 只能放 String
names.add("Tom");
// names.add(123); 编译报错,类型不安全被拦下
Map<String, Integer> score = new HashMap<>();
score.put("Tom", 100);
// 泛型也能用在类上
class Box<T> {
private T value;
public void set(T v) { this.value = v; }
public T get() { return value; }
}
Box<Integer> intBox = new Box<>();
intBox.set(10);
前端对照:泛型 ≈ TypeScript 的
<T>。我们写Array<string>、Promise<T>就是一回事。
十一、字符串拼接与日期
11.1 字符串拼接的坑(性能)
Java 里用 + 拼字符串会产生临时字符串对象,大量拼接时开销爆炸。消除方式:
StringBuilder:非线程安全,拼接与线程无关时用它(绝大多数场景)。StringBuffer:线程安全,拼接涉及多线程时用它。
Java
// ❌ 一万次循环会产生上万个临时 String
String s = "";
for (int i = 0; i < 10000; i++) s += i;
// ✅ 用 StringBuilder,只产生一个对象
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 10000; i++) sb.append(i);
String result = sb.toString();
11.2 日期
Date:日期对象,接收一个日期数据时用它接。Calendar:类似前端的dayjs,用来做日期计算。
注:
Date/Calendar是老 API ,设计有缺陷。工作中我们更常用的是 Java 8 引入的java.time包:LocalDate(日期)、LocalDateTime(日期时间)、DateTimeFormatter(格式化)。
Java
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
LocalDate today = LocalDate.now();
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");
String s = today.format(fmt);
十二、异常处理
Java 稳定性高,很大程度来自完善的异常机制。
12.1 异常体系
TypeScript
Throwable
├── Error (如 OutOfMemoryError,严重错误,不该捕获)
└── Exception
├── RuntimeException (运行时的异常,不会受到Java编译器的检查,可不处理,如空指针)
└── 其他 Exception 如 IOException(受到Java编译器的检查,必须 try-catch 或 throws 声明)
12.2 基本写法
Java
try {
int x = 1 / 0;
} catch (ArithmeticException e) {
e.printStackTrace(); // 生产环境请打日志,别直接 print
} finally {
System.out.println("无论成败都会执行"); // 常用于关资源
}
12.3 自定义异常
两点就够:① 继承 Exception(受检)或 RuntimeException(非受检);② 构造方法里 super(message)。
Java
// 业务异常,继承 RuntimeException 不用强制 try-catch
public class BizException extends RuntimeException {
// 自定义错误码
private Integer code;
public BizException(Integer code, String message) {
super(message); // 把错误信息传给父类
this.code = code;
}
public void setCode(Integer code) {
this.code = code;
}
public Integer getCode() {
return code;
}
}
// 使用:throw new BizException("余额不足");
真实后端几乎都定义一套业务异常(如
BizException/AuthException),配合全局异常处理器(@ControllerAdvice)统一返回错误码和提示,前端拿到的是结构化 JSON,而不是一堆堆栈。
十三、线程入门
线程 = 一条独立的执行路径。一个程序(进程)里可以跑多个线程「同时」干活。
JS 是单线程语言,而 Java 是多线程语言,这里需要额外的精力去理解使用。
方式一:继承 Thread,重写 run 方法
TypeScript
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行:" + Thread.currentThread().getName());
}
}
// 启动
new MyThread().start(); // 注意调 start(),不是 run()
方式二:实现 Runnable(可以避免单继承限制)
TypeScript
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Runnable 执行");
}
}
// 启动
new Thread(new MyRunnable()).start();
我们写 JS 用
Promise/async-await处理异步,那是「单线程事件循环」。Java 是真·多线程并行,能利用多核 CPU------这是后端高并发的根本,但多线程也带来了「并发安全问题」(下一节)。
十四、并发问题与解决方案
14.1 问题:竞态条件
多个线程同时改同一个共享变量,执行顺序不确定,结果就错。
Java
class Counter {
private int count = 0;
public void inc() { count++; } // 看似一行,实际是 读-改-写 三步,在多线程会丢更新
}
14.2 解决方案(从浅到深)
| 方案 | 说明 | 适用 |
| synchronized | JVM 内置锁,同一时刻只一个线程进临界区 | 简单同步 |
| ReentrantLock | 显示锁,功能更丰富(可超时、可公平) | 复杂控制 |
| AtomicInteger 等 | 基于 CAS 无锁,性能更高 | 计数器类简单场景 |
| volatile | 保证可见性(一改全见),不保证原子性 | 状态标志位 |
Java
class SafeCounter {
private int count = 0;
public synchronized void inc() { count++; } // 加锁即可修复
}
真实后端不会手搓 synchronized 。我们会用:① 线程池
ExecutorService管理线程(而不是 new Thread);② 并发集合ConcurrentHashMap;③ 各种锁和原子类。并发是后端最难的深水区之一,这一节先建立「共享变量 + 多线程 = 要加锁」的直觉即可。
这里解决竞态条件的锁和前端解决竞态条件的状态锁类似,本质都是对业务原子化。
十五、IO 流与反射
15.1 流(Stream)
从方向分输入流 / 输出流 ,从数据分字节流 / 字符流:
| 维度 | 含义 |
| 输入流 | 以程序为准,把外部(如文件)读进程序 |
| 输出流 | 以程序为准,把程序数据写到外部(如生成新文件) |
| 字节流 | 最小存储单位,音频/视频/图片等二进制都用它 |
| 字符流 | 按「字符」处理,是对字节流的封装,纯文本(txt)用它 |
Java
// 读文本文件(字符流 + 缓冲,自动关资源 try-with-resources)
try (BufferedReader br = new BufferedReader(new FileReader("a.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
}
// 写文本文件
try (BufferedWriter bw = new BufferedWriter(new FileWriter("b.txt"))) {
bw.write("你好");
}
15.2 反射(Reflection)
Java 提供的一种在运行时动态创建实例、调用方法的机制。
Java
// 1. 根据类名拿到 Class 对象
Class<?> clazz = Class.forName("com.demo.User");
// 2. 动态创建实例(调无参构造)
Object obj = clazz.getDeclaredConstructor().newInstance();
// 3. 拿到方法并调用
Method m = clazz.getMethod("setName", String.class);
m.invoke(obj, "Tom");
System.out.println(obj);
反射是 Spring 的地基。Spring 启动时干的事就是------扫描我们打了
@Component/@Service注解的类,用反射new出实例,塞进自己的容器(IOC 容器)统一管理。所以我们现在理解的「注解 + 反射 + 容器」,就是后面学 Spring 要捅破的那层窗户纸。