🐣 纯新手先看这一框(30 秒秒懂)
Java 适合做什么?企业后台、安卓 App、大数据(Hadoop/Spark)、微服务、金融系统。一句名言:"一次编写,到处运行"——编译成字节码,靠 JVM 跨平台。
为什么要学它?岗位多、生态成熟、强类型能在编译期挡掉一堆低级 bug。想进大厂后端、做安卓,Java 都是硬通货。
怎么跑起来?本站「动手实验室」选 Java 点运行即可,不用装环境;想深入再装 JDK,用 javac Hello.java && java Hello 编译运行。
新手最常踩的坑?① 类名必须和文件名一致且首字母大写;② == 比的是对象地址不是内容(字符串要用 equals);③ 所有代码都在类里,没有全局函数;④ 整数相除会截断小数。
课程说明从零掌握 Java 语法 + 进阶性能与并发要点
Java 由 Sun 公司在 1995 年发布,设计目标是"跨平台、面向对象、自动内存管理"。它编译为字节码(.class),在 JVM(Java 虚拟机)上运行,因此同一份代码能在 Windows、Linux、macOS 上跑——这就是"一次编写,到处运行"(Write Once, Run Anywhere)。
本课程假设你有最基础的编程概念(变量、循环)。重点放在Java 区别于 C/C++ 的关键差异:所有代码必须包在类里、强类型、垃圾回收(GC)自动管内存、丰富的标准库(集合、并发、IO)。进阶部分会直击面试高频的 JVM 内存模型、锁、GC、Stream 等工业级话题。
javac X.java,运行 java X(注意运行时不带 .class 后缀)。本站运行器使用 OpenJDK 22。LTS 版本会长期获得安全更新,生产环境务必用 LTS。
1. Hello World 与程序结构类、main 方法、编译运行
Java 是纯粹的面向对象语言:所有代码都必须写在类(class)里,没有全局函数、没有全局变量。程序入口是固定签名 public static void main(String[] args)。JVM 启动后找到这个 main 开始执行。
public class Hello {
public static void main(String[] args) {
System.out.println("你好,Java!"); // 打印并换行
System.out.print("2 * 3 = ");
System.out.println(2 * 3); // 6
}
}
public 类的名字必须和文件名完全一致(含大小写)。上面类叫 Hello,文件就必须叫 Hello.java,否则 javac 直接报错。非 public 类可以不同名,但强烈建议一律同名,省心。
javac Hello.java # 编译:生成 Hello.class(字节码)
java Hello # 运行(不要写 java Hello.class)
static 表示"属于类而非对象",JVM 启动时还没创建任何对象,所以入口必须是 static,才能直接调用。参数 String[] args 是命令行传进来的参数数组,例如 java Hello a b c 时 args = ["a","b","c"]。
println 输出并换行;print 输出不换行;printf 支持格式化(System.out.printf("%.2f", 3.14159) 输出 3.14)。字符串拼接用 + 最直观,高频拼接用 StringBuilder(见第 6 节)。
2. 变量与数据类型八种基本类型、引用类型、命名
Java 是强类型语言:每个变量必须先声明类型,且类型不可随意变。类型分两类:基本类型(primitive)直接存值(在栈上);引用类型(reference)存的是对象在堆里的地址。
| 类别 | 类型 | 位数/范围 | 示例 |
|---|---|---|---|
| 整数 | byte | 8 位,-128~127 | byte b=1; |
short | 16 位 | short s=10; | |
int | 32 位(最常用) | int n=100; | |
long | 64 位,字面量加 L | long t=1L; | |
| 浮点 | float | 32 位,加 F | float f=1.2f; |
double | 64 位(默认) | double d=1.2; | |
| 字符 | char | 16 位 Unicode | char c='中'; |
| 布尔 | boolean | 仅 true/false | boolean ok=true; |
public class Types {
public static void main(String[] args) {
int age = 18;
double price = 9.9; // 小数默认 double
char ch = 'A';
boolean flag = true;
String name = "小明"; // String 是引用类型(类)
long big = 1_000_000L; // 下划线增强可读性,必须加 L
System.out.println(age + " " + name);
System.out.println(flag); // true
}
}
boolean 只有 true/false,不能用 1/0 代替(和 C 不同)。② 小数字面量是 double,给 float 赋值必须加 F,否则编译错。③ int 溢出不会报错只会回绕(如 Integer.MAX_VALUE + 1 变负),大整数用 long 或 BigInteger。④ 不同类型不能直接运算,需强转。
HelloWorld),变量/方法小驼峰(userName),常量全大写加下划线(MAX_SIZE)。这是 Java 圈默认规范,面试加分。变量名用有意义的英文,别用 a/b/c 堆砌。
3. 运算符与表达式算术/关系/逻辑/位运算
运算符和 C/JS 类似,但要注意 Java 没有指针算术,整数相除会截断,关系运算结果永远是 boolean。
public class Op {
public static void main(String[] args) {
int a = 10, b = 3;
System.out.println(a / b); // 3(整数相除截断小数!)
System.out.println(a % b); // 1(取余)
System.out.println(a == b); // false(关系运算返回 boolean)
// 短路与:左边为 false 右边不执行
boolean r = (b != 0) && (a / b > 2);
System.out.println(r); // true
// 短路或:左边为 true 右边不执行
boolean r2 = (b == 0) || (a / b > 2);
System.out.println(r2); // true
// 自增
int i = 0;
System.out.println(i++); // 0(先用后加)
System.out.println(++i); // 2(先加后用)
}
}
10 / 3 结果是 3 不是 3.333。想要小数,至少一个操作数是浮点:10.0 / 3 或 (double) a / b。除法前想清楚要整数还是小数。
&& 和 || 会短路:左边已能决定结果就不再算右边。常用它写"防 null":if (obj != null && obj.ok()) 能避免空指针。注意别和 &/|(不短路、两侧都算)混淆。
& | ^ ~ << >> >>> 是位运算。常见技巧:n & 1 判奇偶,n << 1 乘 2。日常业务少碰,但读源码(如 HashMap 容量计算)必遇。
4. 流程控制if / for / while / switch
分支与循环和主流语言一致。Java 14+ 支持 switch 表达式(带 -> 和返回值),更简洁安全。
public class Flow {
public static void main(String[] args) {
int n = 7;
if (n % 2 == 0) {
System.out.println("偶数");
} else {
System.out.println("奇数");
}
// 传统 for
int sum = 0;
for (int i = 1; i <= 100; i++) sum += i;
System.out.println("1..100 和=" + sum);
// while 与 do-while
int k = 0;
while (k < 3) { System.out.print(k++ + " "); }
System.out.println();
// switch 表达式(Java 14+)
String level = switch (n % 3) {
case 0 -> "整除3";
case 1 -> "余1";
default -> "其他";
};
System.out.println(level);
}
}
case 忘了写 break 会"穿透"执行下一分支(常见 bug)。用表达式形式 case x -> ... 不会穿透,更安全,新代码优先用它。switch 的 case 只能匹配常量,不能写范围。
for,遍历容器用增强 for(for (x : list)),不确定次数用 while。避免手动维护索引出错。需要提前退出用 break,跳到下一轮用 continue。
5. 数组与多维数组定长、索引、Arrays 工具
数组长度固定,下标从 0 开始,越界会抛 ArrayIndexOutOfBoundsException。多数场景其实更该用 ArrayList(见进阶第 11 节)。
public class Arr {
public static void main(String[] args) {
int[] a = {5, 2, 9, 1}; // 声明并初始化
System.out.println(a.length); // 4(注意是 .length 不是 ())
System.out.println(a[0]); // 5
// 二维数组(每行长度可不同,是"数组的数组")
int[][] m = {{1, 2}, {3, 4, 5}};
System.out.println(m[1][2]); // 5
System.out.println(m.length); // 2(两行)
// 排序、打印、填充用工具类
java.util.Arrays.sort(a);
System.out.println(java.util.Arrays.toString(a)); // [1, 2, 5, 9]
int[] z = new int[3];
java.util.Arrays.fill(z, 7);
System.out.println(java.util.Arrays.toString(z)); // [7, 7, 7]
}
}
0 ~ length-1,访问 a[a.length] 直接抛异常(不像 C 那样静默越界)。这是好事——把 bug 暴露出来而不是脏数据。遍历用 for (int i=0; i<a.length; i++) 或增强 for。
a.length(字段);字符串用 s.length()(方法);集合用 list.size()(方法)。三者不同,别混。
int[][] 是"元素为 int[] 的数组"。所以每行长度可不同(不规则数组),也更灵活。
6. 字符串 String不可变、拼接、常用方法、== vs equals
String 是引用类型,且不可变(immutable):每次"修改"其实是生成新对象。字符串字面量会进字符串常量池复用(详见干货第 2 条)。
public class Str {
public static void main(String[] args) {
String s = "hello";
s = s + " world"; // 生成新对象,原 "hello" 不变
System.out.println(s); // hello world
System.out.println(s.length()); // 11
System.out.println(s.substring(0, 5)); // hello
System.out.println(s.contains("world")); // true
System.out.println(s.startsWith("he")); // true
System.out.println(s.equals("hello world")); // true(比内容)
System.out.println(s.indexOf("world")); // 6
String upper = s.toUpperCase();
System.out.println(upper); // HELLO WORLD(原 s 仍是小写)
}
}
== 比较的是"是否同一个对象",不是内容!比字符串内容必须用 equals。字面量 "a"=="a" 为 true 只是因为常量池复用了同一对象,千万别依赖这个巧合。
String a = new String("hi");
String b = new String("hi");
System.out.println(a == b); // false(两个不同对象)
System.out.println(a.equals(b)); // true(内容相同)
// 高频拼接用 StringBuilder,避免产生大量中间对象
StringBuilder sb = new StringBuilder();
sb.append("hello").append(" ").append("world");
System.out.println(sb.toString()); // hello world
+ 拼字符串会不断生成新 String 对象,低频无所谓,高频(循环/大文本)请用 StringBuilder(单线程)或 StringBuffer(线程安全,稍慢)。
7. 类与对象字段、方法、构造器、this、getter/setter
类是"蓝图",对象是按蓝图造出的实例。用 new 创建对象,构造器在 new 时自动调用做初始化。
public class Student {
private String name; // 私有字段(封装)
private int age;
public Student(String name, int age) { // 构造器(无返回类型,名同类名)
this.name = name; // this 指"当前对象",区分同名字段与参数
this.age = age;
}
// getter / setter:受控访问私有字段
public String getName() { return name; }
public void setAge(int age) {
if (age >= 0) this.age = age; // 可在 setter 里加校验
}
public void sayHi() {
System.out.println("我是 " + name + ",今年 " + age);
}
public static void main(String[] args) {
Student s = new Student("小明", 18); // new 触发构造器
s.sayHi(); // 我是 小明,今年 18
}
}
private,通过 public 的 getter/setter 访问。别把字段设成 public 让人直接改——否则哪天要加校验(如年龄不能为负)都无从下手。setter 是加业务规则的天然位置。
this 指"调用该方法的那个对象"。当参数名和字段名撞车时用 this.name 区分,读起来清楚。构造器里 this(...) 还能调用本类其他构造器(必须第一行)。
8. 继承extends、super、构造链、方法重写
用 extends 表达"是一个"关系,子类复用并扩展父类。Java 只支持单继承(一个类只能有一个直接父类),但可实现多个接口。
class Animal {
protected String name; // 子类可见
public Animal(String name) { this.name = name; }
public void eat() { System.out.println(name + " 在吃"); }
}
class Dog extends Animal {
public Dog(String name) {
super(name); // 必须第一行调用父类构造器
}
@Override
public void eat() { // 重写父类方法
super.eat(); // 可先调父类版本
System.out.println(name + " 吃狗粮");
}
public void bark() { System.out.println(name + " 汪汪"); }
}
public class Test {
public static void main(String[] args) {
Dog d = new Dog("旺财");
d.eat(); // 调重写的版本
d.bark();
}
}
super(...) 或隐式无参 super())。若父类没有无参构造,子类必须显式写 super(参数),否则编译失败。这保证"父类部分先初始化好"。
@Override。
9. 多态与接口向上转型、@Override、interface、default 方法
多态让你用父类/接口类型操作不同子类对象。接口(interface)只规定"能做什么",是实现多继承能力的关键。
interface Shape { // 接口:抽象方法(Java 8+ 可带 default)
double area();
default void desc() { System.out.println("我是一个图形"); }
}
class Circle implements Shape {
private double r;
public Circle(double r) { this.r = r; }
@Override
public double area() { return 3.14159 * r * r; }
}
class Rect implements Shape {
private double w, h;
public Rect(double w, double h) { this.w = w; this.h = h; }
@Override
public double area() { return w * h; }
}
public class Test {
public static void main(String[] args) {
Shape s = new Circle(2.0); // 向上转型:用接口类型持有
System.out.println(s.area()); // 12.56...(实际调 Circle 的)
Shape[] shapes = {new Circle(1), new Rect(3, 4)};
for (Shape sh : shapes) sh.desc(); // 接口 default 方法
}
}
@Override 让编译器检查你确实重写了父类/接口方法。拼错方法名(少个字母)会立刻报错,而不是悄悄新建一个无关方法——这是个救命注解,务必写。
default 方法,差异变小,但抽象类仍可持有字段。一个类可 implements 多个接口,但只能 extends 一个类。
10. 异常处理try-catch-finally、throws、自定义异常
Java 用异常机制把"出错"和"正常逻辑"分离。受检异常(如 IOException)必须处理或声明抛出,强迫你面对错误。
public class Exc {
static int divide(int a, int b) throws ArithmeticException {
if (b == 0) throw new ArithmeticException("除零");
return a / b;
}
public static void main(String[] args) {
try {
System.out.println(divide(10, 0));
} catch (ArithmeticException e) {
System.err.println("出错:" + e.getMessage()); // 出错:除零
} finally {
System.out.println("无论成败都会执行");
}
// 多个 catch 从具体到宽泛
try {
int[] x = {1};
System.out.println(x[9]);
} catch (ArrayIndexOutOfBoundsException e) {
System.out.println("越界");
} catch (Exception e) {
System.out.println("其他异常");
}
// 自定义异常
try {
throw new MyException("我的错");
} catch (MyException e) {
System.out.println(e.getMessage());
}
}
}
class MyException extends Exception {
public MyException(String m) { super(m); }
}
finally 在方法返回前一定执行,常用于释放资源。但手动关资源容易漏,Java 7+ 的 try-with-resources(进阶第 16 节)更安全,优先用它。
catch (Exception e) {} 空着什么都不做是大忌——错误被悄悄吞掉,事后排查要命。至少要打日志或带上上下文重新抛出(throw e;)。
RuntimeException 及其子类是非受检异常(空指针、越界、类型转换),可不声明;其余 Exception 是受检异常,必须 try-catch 或 throws。错误(Error,如 OOM)通常不该捕获。
11. 集合框架List / Set / Map、ArrayList、HashMap
数组定长不好用,日常首选集合。三大接口:List(有序可重复)、Set(去重)、Map(键值对)。都在 java.util 包。
import java.util.*;
public class Coll {
public static void main(String[] args) {
// List:有序、可重复
List<String> list = new ArrayList<>();
list.add("a"); list.add("b"); list.add("a");
System.out.println(list.get(0)); // a
list.remove("a"); // 删除第一个 "a"
// Set:自动去重
Set<Integer> set = new HashSet<>(Arrays.asList(1, 2, 2, 3));
System.out.println(set.size()); // 3(重复被去掉)
// Map:键值对
Map<String, Integer> map = new HashMap<>();
map.put("alice", 90);
map.put("bob", 80);
System.out.println(map.get("alice")); // 90
for (var e : map.entrySet()) {
System.out.println(e.getKey() + " -> " + e.getValue());
}
}
}
ArrayList;频繁头插用 LinkedList;需要排序/去重且有序用 TreeSet/TreeMap;高频查找用 HashMap(平均 O(1))。new ArrayList<>() 的菱形写法让编译器推断泛型。
for 边遍历边 remove 会漏元素或抛异常。正确做法:用迭代器的 it.remove(),或 Java 8+ 的 removeIf:list.removeIf(x -> x.isEmpty());。
List→ArrayList(数组)、LinkedList(链表);Set→HashSet(哈希)、TreeSet(红黑树有序);Map→HashMap、TreeMap、LinkedHashMap(保序)。
12. 泛型泛型类/方法、通配符、类型擦除
泛型让集合"只能装某一种类型",编译期就挡掉类型错配,省去手动强转。
public class Box<T> { // 泛型类,T 是类型参数
private T v;
public void set(T v) { this.v = v; }
public T get() { return v; }
// 泛型方法
public static <E> E pick(E a, E b) { return a; }
public static void main(String[] args) {
Box<String> b = new Box<>();
b.set("hi");
String s = b.get(); // 无需强转,编译期保证是 String
System.out.println(s);
System.out.println(pick(1, 2)); // 1
}
}
List<String> 和 List<Integer> 都是同一个原始类型 List(字节码被擦除)。所以 instanceof List<String> 不合法,也不能 new T[]。这是和 C++ 模板的本质区别(详见干货第 4 条)。
? extends T 生产者(只读)、? super T 消费者(可写),即 PECS 原则(Producer Extends, Consumer Super)。List<?> 表示"某种未知类型的列表"。例如 void copy(List<? super Integer> dst, List<? extends Integer> src)。
13. Lambda 与 Stream函数式接口、链式流、并行流
Java 8 引入 Lambda(匿名函数)和 Stream(流式处理),让集合操作像写 SQL 一样声明式。
import java.util.*;
import java.util.stream.*;
public class Streams {
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(1, 2, 3, 4, 5, 6);
// 过滤偶数并求平方和
int sum = nums.stream()
.filter(n -> n % 2 == 0) // 偶数
.mapToInt(n -> n * n) // 平方
.sum(); // 4 + 16 + 36 = 56
System.out.println(sum);
// 收集成新列表
List<Integer> evens = nums.stream()
.filter(n -> n % 2 == 0)
.collect(Collectors.toList());
System.out.println(evens); // [2, 4, 6]
// Lambda 简化 Runnable
new Thread(() -> System.out.println("hello thread")).start();
}
}
.parallelStream() 会用公共 ForkJoinPool 并行执行。若在流里修改共享的外部变量(尤其是非线程安全的集合),会得到错误/不确定的结果(详见干货第 7 条)。绝大多数场景用顺序 stream() 就够了。
Runnable、Comparator、Function)。常用内置接口:Predicate(判断)、Function(转换)、Consumer(消费)、Supplier(供给)。
filter/map 等中间操作是惰性的,只有遇到 sum/collect/forEach 等终止操作才真正执行。且 Stream 只能消费一次,重用会抛 IllegalStateException。
14. 多线程基础Thread / Runnable、synchronized、可见性
Java 内置多线程支持。共享变量不加保护,会出"竞态条件"和"可见性"问题。
public class Count {
static int n = 0;
static final Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Runnable task = () -> {
for (int i = 0; i < 1000; i++) {
synchronized (lock) { n++; } // 加锁,保证原子性
}
};
Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
t1.start(); t2.start();
t1.join(); t2.join(); // 等两个线程结束
System.out.println(n); // 期望 2000
}
}
synchronized 时,n++ 实际是"读-改-写"三步,两个线程可能同时读到旧值,最终 n 小于 2000。这是经典竞态。更简单的替代是 AtomicInteger(n.incrementAndGet())。
synchronized 并非一上来就重锁:JVM 会按"偏向锁 → 轻量级锁(CAS自旋)→ 重量级锁(操作系统互斥)"逐步升级,只在竞争激烈时才用重锁,平时开销很小(详见干货第 5 条)。
java.util.concurrent.atomic.AtomicInteger 或无锁结构;复杂协作用 java.util.concurrent 包(ReentrantLock、CountDownLatch 等),比手写 synchronized 灵活。
15. 包装类与自动装箱Integer / 缓存、== vs equals、空指针
基本类型(int)没有方法,对应的包装类(Integer)是对象,让基本类型能放进集合。自动装箱/拆箱让你在两者间无缝切换。
import java.util.*;
public class Wrap {
public static void main(String[] args) {
Integer a = 127, b = 127;
System.out.println(a == b); // true(命中缓存区)
Integer c = 128, d = 128;
System.out.println(c == d); // false(超出缓存,新对象)
// 正确比内容用 equals
System.out.println(c.equals(d)); // true
List<Integer> list = new ArrayList<>();
list.add(5); // 自动装箱 int -> Integer
int x = list.get(0); // 自动拆箱 Integer -> int
System.out.println(x);
}
}
-128 ~ 127 的 Integer 被缓存复用,所以 == 恰好为 true;超出范围就是不同对象,== 变 false。永远用 equals 比包装类内容,别被缓存侥幸蒙蔽(详见干货第 3 条)。
Integer 为 null 时自动拆箱会抛 NullPointerException。集合取出来的值先判空再拆箱,尤其是从数据库/接口拿到的字段。写 if (x != null && x > 0) 而不是直接 x > 0。
16. IO 与 try-with-resourcesFile 读写、自动关闭、AutoCloseable
读写文件、网络都要用流。传统 try-catch-finally 关资源易漏,Java 7 的 try-with-resources 自动关闭,更安全。
import java.nio.file.*;
import java.io.*;
public class Io {
public static void main(String[] args) {
// 写文件(一行搞定)
try {
Files.writeString(Path.of("a.txt"), "你好Java");
} catch (IOException e) {
e.printStackTrace();
}
// 读文件并自动关闭流
try (BufferedReader br = Files.newBufferedReader(Path.of("a.txt"))) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) { // 离开 try 块自动关 br
e.printStackTrace();
}
}
}
try(...) 括号里声明实现了 AutoCloseable 接口的对象(如流、连接),编译器会在结尾自动调用它的 close()。不用再手写 finally 关资源(详见干货第 9 条)。
IOException,必须处理或声明 throws。生产代码别用 printStackTrace() 草草了事,应走日志框架(SLF4J + Logback)。大文件用缓冲流(BufferedReader/Writer)提升性能。
java.nio.file 的 Files/Path 比老 File 好用:Files.readAllLines、Files.copy、Files.walk 一行搞定常见操作,优先用它。
进阶训练场(含答案)
题 1:写一个带构造/封装的 Student 类
类 Student 私有字段 name、score;构造器初始化;getScore() 返回;setScore 限制 0~100;show() 打印。
public class Student {
private String name;
private int score;
public Student(String name, int score) { this.name = name; this.score = score; }
public int getScore() { return score; }
public void setScore(int s) { if (s >= 0 && s <= 100) score = s; }
public void show() { System.out.println(name + " : " + score); }
}
题 2:用数组求最大值与平均值
给定 int[],输出最大值与平均值(保留 2 位小数)。
public class Stats {
public static void main(String[] args) {
int[] a = {3, 9, 2, 7, 5};
int max = a[0]; double sum = 0;
for (int x : a) { if (x > max) max = x; sum += x; }
System.out.printf("max=%d avg=%.2f%n", max, sum / a.length);
}
}
题 3:用 HashMap 统计单词次数
给定字符串数组,统计每个单词出现次数并输出。
import java.util.*;
public class WordCount {
public static void main(String[] args) {
String[] words = {"a","b","a","c","b","a"};
Map<String,Integer> cnt = new HashMap<>();
for (String w : words) cnt.put(w, cnt.getOrDefault(w, 0) + 1);
System.out.println(cnt); // {a=3, b=2, c=1}
}
}
题 4:泛型方法实现通用最大值
写泛型静态方法 maxOf,支持 List<Integer>/List<String> 等 Comparable 列表。
import java.util.*;
public class MaxOf {
static <T extends Comparable<T>> T maxOf(List<T> list) {
T m = list.get(0);
for (T x : list) if (x.compareTo(m) > 0) m = x;
return m;
}
public static void main(String[] args) {
System.out.println(maxOf(Arrays.asList(3, 9, 1))); // 9
System.out.println(maxOf(Arrays.asList("b","a","c"))); // c
}
}
题 5:接口 + 多态画面积
Shape 接口有 area(),实现 Circle(r) 与 Square(side),用 List 遍历打印面积。
import java.util.*;
interface Shape { double area(); }
class Circle implements Shape {
double r; Circle(double r){this.r=r;}
public double area(){ return 3.14159*r*r; }
}
class Square implements Shape {
double s; Square(double s){this.s=s;}
public double area(){ return s*s; }
}
public class Test {
public static void main(String[] args) {
List<Shape> list = Arrays.asList(new Circle(2), new Square(3));
for (Shape sh : list) System.out.println(sh.area());
}
}
题 6:Stream 求偶数平方和
用 Stream 对 List<Integer> 求偶数平方和。
import java.util.*;
public class EvenSq {
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(1,2,3,4,5,6);
int r = nums.stream().filter(n->n%2==0).mapToInt(n->n*n).sum();
System.out.println(r); // 56
}
}
题 7:线程安全计数器(AtomicInteger)
用 AtomicInteger 让两个线程各加 1000 次,结果稳定 2000。
import java.util.concurrent.atomic.*;
public class AtomicCount {
static AtomicInteger n = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Runnable t = () -> { for (int i=0;i<1000;i++) n.incrementAndGet(); };
Thread a = new Thread(t), b = new Thread(t);
a.start(); b.start(); a.join(); b.join();
System.out.println(n.get()); // 2000
}
}
题 8:try-with-resources 读文件行数
用 NIO 读取文件,统计行数(用 try-with-resources 保证关闭)。
import java.nio.file.*;
public class LineCount {
public static void main(String[] args) {
try (var lines = Files.lines(Path.of("a.txt"))) {
System.out.println("行数=" + lines.count());
} catch (java.io.IOException e) {
System.out.println("读取出错:" + e.getMessage());
}
}
}
🧪 动手实验室
不用从零敲代码:下面每个程序都已写好,点「▶ 运行」直接看结果;想改就在框里改,再点运行。手机、电脑都一样用。打开本页时会自动跑一次,方便你先看效果。
① 基础输出与计算
② 数组与循环(求和 + 找最大)
③ 字符串处理(反转 + 统计元音)
④ 类与对象(简单银行账户)
⑤ 集合 + 流式处理(偶数平方求和)
Main.java,所以 public 类必须叫 Main。实战干货(12 条工业级技巧)面试与上线都绕不开
-Xmx 调堆上限,-Xss 调栈大小。理解谁共享谁私有,是定位内存溢出(堆 OOM / 栈溢出 StackOverflowError)的前提。
// 栈溢出示例:无限递归
void rec() { rec(); } // StackOverflowError
// 堆溢出示例:疯狂创建对象
List<byte[]> list = new ArrayList<>();
while (true) list.add(new byte[1024 * 1024]); // OutOfMemoryError: Java heap space
"abc")会进字符串常量池复用,避免重复创建;new String("abc") 则强制在堆里新建对象。可调用 s.intern() 把字符串手动放入常量池并返回池里引用。同理 Integer 对 -128~127 有缓存(IntegerCache),该范围内 valueOf 返回同一对象——这正是第 15 节 == 行为差异的根源。
String a = "abc";
String b = "abc";
System.out.println(a == b); // true(常量池同一对象)
String c = new String("abc");
System.out.println(a == c); // false(堆里新对象)
System.out.println(a == c.intern()); // true(intern 返回池里引用)
int <-> Integer 自动装箱会产生海量临时对象,加重 GC 压力;且包装类用 == 比的是引用不是值(见第 15 节)。高性能路径(如解析大数组)尽量用基本类型数组 int[] 而非 List<Integer>;比较内容一律 equals。
// 反例:循环里反复装箱,产生大量 Integer 对象
Integer sum = 0;
for (int i = 0; i < 1_000_000; i++) sum += i; // 每次都 new Integer
// 正例:用基本类型累加
int sum2 = 0;
for (int i = 0; i < 1_000_000; i++) sum2 += i;
Object),所以运行时拿不到具体类型,也不能 new T()。为实现多态,编译器会生成桥接方法(bridge method):子类重写父类泛型方法时,JVM 额外合成一个擦除后的签名方法来保证正确分发。写反射或反序列化时遇到"多出来"的方法签名,多半是桥接方法。
class Node<T> {
public void set(T t) {}
}
class IntNode extends Node<Integer> {
@Override public void set(Integer t) {} // 你写的
// 编译器还会合成:public void set(Object t){ set((Integer)t); } // 桥接方法
}
synchronized 是自适应、可升级的:无竞争时偏向锁(记录线程 ID,近乎无开销);有轻度竞争升级为轻量级锁(CAS 自旋);竞争激烈才膨胀为重量级锁(挂起线程、进操作系统队列)。注意:Java 15 起偏向锁默认禁用(-XX:-UseBiasedLocking),因为维护偏向锁的撤销成本在现代多核上往往得不偿失。别再神话"偏向锁"了。
synchronized 已足够;需要超时/可中断/公平锁时用 ReentrantLock;读多写少考虑 ReadWriteLock。别一上来就用最重的锁。
volatile 保证变量在多线程间的可见性(一个线程改了,别的线程立刻看到)并禁止指令重排序(防止半初始化对象等陷阱)。但它不保证原子性——volatile int n; n++; 依然非线程安全。需要原子计数请用 AtomicInteger,需要复合操作仍要锁。
// 双重检查单例:volatile 防止指令重排序导致拿到"半初始化"对象
class Singleton {
private static volatile Singleton instance;
static Singleton get() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) instance = new Singleton();
}
}
return instance;
}
}
findFirst/anyMatch/limit 等是短路操作,遇到满足条件的元素就停,能省大量计算。但 parallelStream() 用公共线程池,若在流中修改共享可变状态(尤其是对非线程安全集合增删),结果不确定且难复现。经验法则:并行流只用于纯函数(无副作用)的大量数据计算,否则老老实实用顺序流 + 显式同步。
// 危险:并行流里改共享的非线程安全集合
List<Integer> out = new ArrayList<>();
IntStream.range(0, 1000).parallel().forEach(i -> out.add(i)); // 可能丢数据/报错
// 正确:用 collect 收拢结果
List<Integer> ok = IntStream.range(0,1000).parallel()
.boxed().collect(Collectors.toList());
-XX:+UseG1GC(默认)、-XX:+UseZGC、-Xmx4g 设堆上限、-XX:MaxGCPauseMillis=200 设目标停顿。监控用 GC 日志 + Prometheus/Grafana。
AutoCloseable 接口(有 void close() throws Exception)的资源,都能写进 try (资源) { ... },编译器自动在 finally 里调用 close(),且若 try 和 close 都抛异常,close 的异常会被"抑制"(suppressed)并可用 getSuppressed() 取到。比手写 finally 更安全、更简洁,IO/数据库连接务必用它。
// 多个资源可一行声明,按声明逆序关闭
try (FileInputStream in = new FileInputStream("a.txt");
FileOutputStream out = new FileOutputStream("b.txt")) {
// ...
} // in、out 都会自动关闭
Optional<T> 用来显式表达"可能为空",强制调用方处理缺失。正确姿势:用 map/filter/orElse/orElseGet 链式取值,而不是 if (opt.isPresent()) opt.get()(那等于把 Optional 当 if 用,毫无收益)。不要序列化 Optional(它不是为序列化设计,字段用它会坑框架),也不要用它做类字段/方法参数,它更适合作为方法返回值。
Optional<User> u = findUser(id);
// 正确:链式,避免 isPresent/get
String name = u.map(User::getName).orElse("匿名");
// 错误:把它当 if 用,等于没用 Optional
if (u.isPresent()) { String n = u.get().getName(); }
-XX:+PrintCompilation 看哪些方法被 JIT 编译;jstack/jmap/jcmd 排查;JMH 才是正经微基准框架,别用 System.currentTimeMillis() 自测性能。
module-info.java 声明模块依赖(requires)与导出(exports),从语言层面强封装——没导出的包即使 public 也访问不到。好处是更小的运行时镜像(jlink 裁剪)、更强的封装;代价是老库迁移要补模块声明。新项目可顺势用,老项目不必强求,但遇到 IllegalAccessError 多半和模块可见性有关。
// module-info.java 示例
module com.example.app {
requires java.sql;
exports com.example.app.api; // 只导出 api 包,其余包对外不可见
}
附录一 · 速查表面试前过一遍,记不住就回来查
1. 八种基本类型与默认值
| 类型 | 位数 | 默认值 | 包装类 | 范围 |
|---|---|---|---|---|
byte | 8 | 0 | Byte | -128 ~ 127 |
short | 16 | 0 | Short | -32768 ~ 32767 |
int | 32 | 0 | Integer | 约 ±21 亿 |
long | 64 | 0L | Long | 约 ±9×10¹⁸ |
float | 32 | 0.0f | Float | 约 6~7 位有效数字 |
double | 64 | 0.0d | Double | 约 15 位有效数字 |
char | 16 | '\u0000' | Character | 0 ~ 65535(Unicode) |
boolean | 1(JVM 实现相关) | false | Boolean | 仅 true / false |
2. == 还是 equals?决策表
| 比较对象 | 比内容用 | 比同一对象用 | 说明 |
|---|---|---|---|
| 基本类型(int/double…) | == | — | 基本类型没有 equals,直接比值 |
| String | equals | ==(仅常量池巧合) | 内容比较一律 equals |
| Integer 等包装类 | equals | == | 注意 -128~127 缓存干扰 |
| 自定义类 | 重写 equals | == | 不重写 equals 等同 == |
3. 集合选型速查
| 需求 | 首选 | 次选 / 说明 |
|---|---|---|
| 有序可重复列表 | ArrayList | 头插频繁用 LinkedList |
| 去重 | HashSet | 要保序用 LinkedHashSet |
| 去重且排序 | TreeSet | 红黑树,O(log n) |
| 键值查找 | HashMap | 保序用 LinkedHashMap |
| 键值有序 | TreeMap | 按 key 排序 |
| 线程安全集合 | ConcurrentHashMap | 别用 Collections.synchronizedMap 除非简单场景 |
| 不可变集合 | List.of / Map.of | Java 9+ 一行创建 |
4. 垃圾收集器对比
| 收集器 | 目标 | 典型停顿 | 适用 |
|---|---|---|---|
| Serial | 单线程、简单 | 较长 | 客户端/小内存 |
| Parallel(吞吐优先) | 高吞吐 | 中 | 批处理 |
| G1(默认) | 平衡 | 中低 | 大多数服务(JDK 9+ 默认) |
| ZGC | 超低延迟 | <10ms | 对延迟极敏感(JDK 15+ 生产) |
| Shenandoah | 超低延迟 | <10ms | 同 ZGC,Red Hat 主导 |
5. 异常分类
| 类别 | 是否受检 | 典型代表 | 处理要求 |
|---|---|---|---|
RuntimeException 及其子类 | 非受检 | NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException | 可不声明,靠代码避免 |
其他 Exception | 受检 | IOException、SQLException | 必须 try-catch 或 throws |
Error | 非受检 | OutOfMemoryError、StackOverflowError | 一般不捕获,属 JVM 级故障 |
6. 访问修饰符可见性
| 修饰符 | 同类 | 同包 | 子类(不同包) | 全局 |
|---|---|---|---|---|
private | ✓ | ✗ | ✗ | ✗ |
| 默认(包内) | ✓ | ✓ | ✗ | ✗ |
protected | ✓ | ✓ | ✓ | ✗ |
public | ✓ | ✓ | ✓ | ✓ |
private 就别用更大范围;字段几乎总是 private,靠 getter/setter 暴露,既安全又灵活。7. 常用 JVM 参数
| 参数 | 作用 |
|---|---|
-Xms / -Xmx | 堆初始 / 最大大小,如 -Xmx4g |
-Xss | 每个线程栈大小 |
-XX:+UseG1GC | 使用 G1 收集器(默认) |
-XX:+UseZGC | 使用 ZGC |
-XX:MaxGCPauseMillis | 期望最大 GC 停顿(目标值) |
-verbose:gc | 打印 GC 日志 |
附录二 · 新手常见 20 个坑每条都值得你记在便签上
- 类名与文件名不一致:public 类名必须和
.java文件名完全一致(含大小写),否则编译失败。 - 用
==比字符串:引用类型==比地址;内容比较一律equals。 - 忘记
break导致 switch 穿透:用表达式形式case x -> ...避免。 - 整数相除截断:
10/3是 3,不是 3.333;要浮点就转一个操作数。 - 数组越界:下标
0 ~ length-1,越界抛ArrayIndexOutOfBoundsException。 - 混淆
length/length()/size():数组用.length,字符串.length(),集合.size()。 - 自动拆箱 null 空指针:
Integer为 null 时拆箱直接 NPE,取集合值先判空。 - 依赖 Integer 缓存:
-128~127外==比较包装类为 false,用equals。 - 子类构造器忘写
super():父类无无参构造时必须显式调用super(参数),且放第一行。 - 漏写
@Override:拼错方法名时编译器不会提醒,写了能立刻报错。 - 抛了受检异常不处理:
IOException等必须 try-catch 或throws。 - catch 里空着吞异常:
catch(Exception e){}会让 bug 消失得无影无踪。 - 遍历集合时直接 remove:用迭代器
it.remove()或removeIf。 - 在并行流里改共享集合:结果不确定,用
collect收拢。 - 以为
volatile保证原子性:它只保可见性,n++仍需锁或AtomicInteger。 - 认为
String可变:s += "x"生成新对象;高频拼接用StringBuilder。 - 手动关资源漏写 finally:用
try-with-resources自动关闭。 - 用
==比Double特殊值:NaN不等于任何值(包括自己),用Double.isNaN()。 - 不理解泛型擦除:运行时拿不到
T的具体类型,不能new T[]。 - 微基准不预热就下结论:Java 有 JIT,没预热的测量毫无意义,用 JMH。
综合小项目 · 学生成绩管理系统把前面学的串起来,写一个能跑的小程序
下面这个项目把"类、集合、泛型、Stream、异常处理、IO"全部串起来:录入若干学生成绩,统计平均分、找出最高分、按分数排序并导出。我们分四步做,每一步都能独立理解。
第 1 步:定义 Student 实体类
封装字段、提供构造器与 getter,并重写 toString 方便打印。注意分数用 double,姓名为 String。
class Student {
private final String name;
private final double score;
public Student(String name, double score) {
if (score < 0 || score > 100) {
throw new IllegalArgumentException("分数必须在 0~100:" + score);
}
this.name = name;
this.score = score;
}
public String getName() { return name; }
public double getScore() { return score; }
@Override
public String toString() {
return name + " : " + score;
}
}
final,构造时赋值后就不可变,线程安全、好推理。配合构造器里的校验,保证对象永远是合法状态。第 2 步:用 List 管理学生并统计
用 ArrayList<Student> 存学生,借助 Stream 求平均分与最高分。
import java.util.*;
public class GradeApp {
public static void main(String[] args) {
List<Student> students = new ArrayList<>();
students.add(new Student("小明", 88));
students.add(new Student("小红", 95));
students.add(new Student("小刚", 72));
// 平均分
double avg = students.stream()
.mapToDouble(Student::getScore)
.average()
.orElse(0);
System.out.printf("平均分=%.2f%n", avg);
// 最高分(Optional 兜底)
students.stream()
.max(Comparator.comparingDouble(Student::getScore))
.ifPresent(top -> System.out.println("最高分:" + top));
}
}
average() 和 max() 返回 Optional,因为空集合没有结果。用 orElse(0) 或 ifPresent 处理"可能为空",别直接 get()。第 3 步:按分数降序排序
排序用 Comparator,注意是"降序"(从高到低)。
List<Student> sorted = new ArrayList<>(students);
sorted.sort(Comparator.comparingDouble(Student::getScore).reversed());
sorted.forEach(System.out::println);
// 输出:小红 : 95 小明 : 88 小刚 : 72
reversed() 返回反转后的新比较器;先 comparingDouble 再 .reversed() 即降序。直接用 (a,b)->b.getScore()-a.getScore() 也行,但小心浮点减法溢出,推荐用 Comparator。第 4 步:导出到文件(try-with-resources)
把结果写进 report.txt,用 NIO 的 Files.write,并整体包在 try 里处理 IO 异常。
import java.nio.file.*;
public class Exporter {
public static void export(List<Student> list, String path) {
try {
List<String> lines = new ArrayList<>();
lines.add("姓名,分数");
for (Student s : list) {
lines.add(s.getName() + "," + s.getScore());
}
Files.write(Path.of(path), lines); // 自动创建/覆盖文件
System.out.println("已导出到 " + path);
} catch (java.io.IOException e) {
System.err.println("导出失败:" + e.getMessage());
}
}
}
面试高频 12 问边学边想,胜过死记
String 内部是 final char[](Java 9+ 是 byte 数组),且类与字段都 final,无法修改。好处:可安全进常量池复用、可做 HashMap 的 key(hash 不变)、线程安全。代价是每次"改"都产生新对象。
final 修饰类不可继承、方法不可重写、变量不可改;finally 是 try 里总执行的块(用于关资源);finalize 是对象被 GC 前调用的废弃方法,别用。
hashCode/equals 的对象。
hashCode 找桶,再用 equals 比对。若两个对象 equals 为 true 但 hashCode 不同,会被放进不同桶,导致"逻辑相等却查不到 / 重复存入",破坏集合语义。
Cloneable 默认是浅拷贝;要深拷贝需手动递归复制,或改用不可变对象 + 复制构造器。
Thread 重写 run;② 实现 Runnable(更灵活,推荐);③ 实现 Callable(有返回值、可抛异常,配合 FutureTask);④ 用线程池 Executors。实际项目几乎都用线程池,不直接 new Thread。
synchronized 是 JVM 内置关键字,自动释放、可锁升级、用起来简单;ReentrantLock 是 API 层面的锁,支持可中断、超时获取、公平锁、多条件变量,但必须手动 unlock(常放 finally)。
tryLock 超时放弃;③ 缩小锁粒度;④ 用高层并发工具(如 ConcurrentHashMap)替代手写锁。
volatile(懒加载,见干货第 6 条);③ 枚举单例(最简洁且防反射破坏)。
-Xmx 设堆、选 G1/ZGC、开 GC 日志,用工具(GC 日志 + Prometheus)看停顿与回收频率,最后才微调参数。绝大多数性能问题其实是代码/SQL 问题,别忙着调 JVM。
代码风格与工程规范写好 Java 的"软实力"
| 规范点 | 推荐做法 | 反面例子 |
|---|---|---|
| 命名 | 类大驼峰、方法/变量小驼峰、常量全大写下划线 | class student、int X |
| 花括号 | 左花括号不换行(K&R 风格,Java 主流) | 左花括号独占一行 |
| 魔法值 | 提取为 static final 常量 | 到处写 if (x == 3) |
| 异常 | 具体捕获、记录上下文、别吞 | catch(Exception e){} |
| 空值 | 用 Objects.requireNonNull 早失败 | 放任 null 传到深层再 NPE |
| 资源 | 一律 try-with-resources | 手写 finally 关流 |
| 集合 | 用 List.of/Map.of 建不可变、用接口类型声明 | ArrayList<String> x = new ArrayList<>() |
List<String> list = new ArrayList<>(); 比声明成 ArrayList 好——未来想换成 LinkedList 只改右边,调用方代码不动。这是"面向接口编程"的基本功。学完之后怎么走一张路线图,别迷茫
- 夯实语法:把本课程的 10 节入门 + 6 节进阶练熟,每个例子自己敲一遍、改一遍。
- 面向对象进阶:深入理解封装/继承/多态、接口设计、设计模式(单例、工厂、策略、观察者先学这四个)。
- 标准库:集合、并发(
java.util.concurrent)、IO/NIO、时间 API(java.time,别用老的Date)。 - JVM 原理:内存模型、GC、类加载、JIT——对应本课程 12 条干货,是进阶与面试分水岭。
- 工程化:Maven/Gradle 构建、单元测试(JUnit)、日志(SLF4J)、Git。
- 框架:Spring Boot 写 Web 服务,MyBatis/JPA 操作数据库,这是找工作的最常用组合。
- 并发与性能:线程池、锁、无锁结构、性能调优,往资深走必须啃。
扩展专题一 · 日期时间 API(java.time)别再用老的 Date/Calendar
Java 8 引入 java.time 包,彻底解决了老 Date/Calendar 线程不安全、API 反人类的老问题。核心类都是不可变且线程安全的。
import java.time.*;
import java.time.format.*;
public class TimeDemo {
public static void main(String[] args) {
LocalDate d = LocalDate.now(); // 年月日
LocalTime t = LocalTime.now(); // 时分秒
LocalDateTime dt = LocalDateTime.now(); // 两者合一
System.out.println(d + " " + t);
// 构造与加减
LocalDate birth = LocalDate.of(2000, 1, 1);
LocalDate later = birth.plusDays(10).plusMonths(2);
System.out.println(later); // 2000-03-12
// 计算间隔
Period p = Period.between(birth, LocalDate.now());
System.out.println("相差 " + p.getYears() + " 年 " + p.getMonths() + " 月");
Duration dur = Duration.between(LocalTime.of(8,0), LocalTime.of(9,30));
System.out.println("时长 " + dur.toMinutes() + " 分钟");
// 解析与格式化
DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy/MM/dd");
System.out.println(dt.format(fmt));
LocalDate parsed = LocalDate.parse("2026/08/12", fmt);
System.out.println(parsed);
}
}
java.util.Date 月份从 0 开始、非线程安全、可变,是无数 bug 源头。新项目一律用 java.time;和旧库对接时用 Date.from(instant) / date.toInstant() 转换。ZonedDateTime / OffsetDateTime。存数据库推荐用 Instant(UTC 时间戳)或 LocalDateTime(无时区),展示时再按用户时区格式化。扩展专题二 · 枚举与注解让代码更安全、更自解释
枚举 enum:比常量更好用
枚举是一组固定常量的类型,可带字段和方法,比满地 public static final int 安全得多。
enum Status {
PENDING(0, "待处理"),
RUNNING(1, "进行中"),
DONE(2, "完成");
private final int code;
private final String label;
Status(int code, String label) { this.code = code; this.label = label; }
public int code() { return code; }
public String label() { return label; }
}
public class EnumDemo {
public static void main(String[] args) {
Status s = Status.RUNNING;
System.out.println(s.label()); // 进行中
System.out.println(s.code()); // 1
for (Status v : Status.values()) { // 遍历所有枚举
System.out.println(v);
}
Status byName = Status.valueOf("DONE"); // 按名取
System.out.println(byName.label());
}
}
注解 annotation:给编译器/框架看的"标签"
注解不改变逻辑,但能交给编译器或框架做检查/配置。常见内置注解:
| 注解 | 作用 |
|---|---|
@Override | 声明重写,编译器校验 |
@Deprecated | 标记过时,调用处会告警 |
@SuppressWarnings | 抑制特定编译警告 |
@FunctionalInterface | 约束接口只有一个抽象方法(可用 Lambda) |
@SafeVarargs | 声明泛型可变参方法无堆污染 |
@Target(能用在哪)、@Retention(保留到哪阶段:源码/类/运行时)、@Documented。Spring 的 @Controller、JUnit 的 @Test 都是自定义注解。扩展专题三 · 标准库常用 API 速查小例子一抓一大把
1. 字符串常用
String s = " Hello Java ";
System.out.println(s.trim()); // "Hello Java"(去首尾空格)
System.out.println(s.strip()); // 同 trim,但认 Unicode 空白
System.out.println("a,b,c".split(",").length); // 3(拆分)
System.out.println(String.join("-", "a","b","c")); // a-b-c(拼接)
System.out.println("Java".repeat(2)); // JavaJava(Java 11+)
2. 数学与随机数
System.out.println(Math.max(3, 7)); // 7
System.out.println(Math.round(3.6)); // 4(四舍五入)
System.out.println(Math.abs(-5)); // 5
System.out.println(Math.pow(2, 10)); // 1024.0
System.out.println(Math.sqrt(16)); // 4.0
// 推荐用 ThreadLocalRandom(并发安全、更快)
int r = java.util.concurrent.ThreadLocalRandom.current().nextInt(1, 101); // 1~100
3. 集合实用操作
import java.util.*;
List<Integer> list = Arrays.asList(3, 1, 2);
Collections.sort(list); // 原地升序:[1,2,3]
Collections.reverse(list); // 反转
Collections.shuffle(list); // 随机打乱
System.out.println(Collections.max(list)); // 最大值
System.out.println(Collections.frequency(list, 1)); // 某元素出现次数
// 不可变集合(Java 9+)
List<String> fixed = List.of("a", "b");
Map<String,Integer> m = Map.of("x", 1, "y", 2);
4. 文件与路径
import java.nio.file.*;
import java.io.*;
// 读全部行
List<String> lines = Files.readAllLines(Path.of("a.txt"));
// 写字符串
Files.writeString(Path.of("b.txt"), "内容");
// 判断是否存在
boolean exists = Files.exists(Path.of("a.txt"));
// 创建/删除
Files.createDirectories(Path.of("dir/sub"));
Files.deleteIfExists(Path.of("b.txt"));
5. 字符串与数字互转
int n = Integer.parseInt("123"); // String -> int
double d = Double.parseDouble("3.14"); // String -> double
String s = String.valueOf(456); // 任意 -> String
String hex = Integer.toHexString(255); // "ff"(转进制)
Integer.parseInt("abc") 抛 NumberFormatException(受检?不,它是 RuntimeException)。解析外部输入务必 try-catch,否则一个脏数据让程序崩。常见算法模板(背下来就够用)刷题/面试常客
1. 二分查找
static int binarySearch(int[] a, int target) {
int lo = 0, hi = a.length - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2; // 防溢出写法
if (a[mid] == target) return mid;
else if (a[mid] < target) lo = mid + 1;
else hi = mid - 1;
}
return -1;
}
2. 反转数组
static void reverse(int[] a) {
for (int i = 0, j = a.length - 1; i < j; i++, j--) {
int t = a[i]; a[i] = a[j]; a[j] = t;
}
}
3. 判断回文
static boolean isPalindrome(String s) {
int i = 0, j = s.length() - 1;
while (i < j) {
if (s.charAt(i++) != s.charAt(j--)) return false;
}
return true;
}
4. 最大子数组和(Kadane)
static int maxSubArray(int[] a) {
int best = a[0], cur = a[0];
for (int i = 1; i < a.length; i++) {
cur = Math.max(a[i], cur + a[i]); // 要么接上,要么从头开始
best = Math.max(best, cur);
}
return best;
}
扩展专题四 · Java 8+ 新特性清单现代 Java 长这样
Java 从 8 开始进入了"每半年一版"的快节奏。下面这些是你一定会天天用到的现代语法,看到旧代码没有它们别奇怪(那是老项目)。
| 版本 | 特性 | 一句话 |
|---|---|---|
| 8 | Lambda / Stream / Optional | 函数式与流式处理基石 |
| 8 | 接口 default 方法 | 接口能带默认实现,向后兼容 |
| 8 | 新的日期时间 API | java.time(见专题一) |
| 9 | 模块系统 JPMS | module-info.java(见干货 12) |
| 9 | 集合工厂 List.of | 一行建不可变集合 |
| 10 | var 局部变量推断 | var x = new ArrayList<>(); |
| 11 | 字符串 repeat/lines | 小工具方法 |
| 14 | switch 表达式 | case x -> ... 可返回值 |
| 15 | 文本块 Text Blocks | """...""" 多行字符串 |
| 16 | record | 一行定义不可变数据类 |
| 17 | 密封类 sealed | 限制谁能继承 |
| 17 | 模式匹配 instanceof | if (o instanceof String s) |
// record:自动生成字段、构造器、getter、equals、hashCode、toString
record Point(int x, int y) {}
Point p = new Point(1, 2);
System.out.println(p.x() + "," + p.y()); // 1,2
// 文本块:多行字符串不用拼 \n
String json = """
{
"name": "小明",
"age": 18
}
""";
System.out.println(json);
// 模式匹配 instanceof
Object o = "hi";
if (o instanceof String s) { // 命中后 s 直接用,无需强转
System.out.println(s.length());
}
record、文本块、模式匹配能大幅减少样板代码,写起来很爽。调试与常见异常排查会写更要会"救火"
1. 怎么读异常栈
异常 throw 时会打印一段"栈轨迹"。从上往下看:最上面是异常类型和原因(如 NullPointerException: Cannot read field "x"),接着是你的代码行号——第一行带你自己的包名的行,通常就是出问题的地方。
Exception in thread "main" java.lang.NullPointerException: Cannot invoke "String.length()" because "s" is null
at com.demo.App.main(App.java:12) <-- 看这里,App.java 第 12 行
2. 常见异常速查
| 异常 | 常见原因 | 排查 |
|---|---|---|
NullPointerException | 调用了 null 对象的方法 | 用 Objects.requireNonNull 早失败,或 Optional |
ArrayIndexOutOfBoundsException | 数组下标越界 | 检查循环边界、确认 length |
ClassCastException | 强制类型转换失败 | 用 instanceof 先判断 |
NumberFormatException | 字符串转数字失败 | 解析前校验格式,try-catch |
ConcurrentModificationException | 遍历时改集合 | 用迭代器 remove / 并发集合 |
OutOfMemoryError | 内存不够 / 泄漏 | 看堆转储,调 -Xmx |
StackOverflowError | 无限递归 | 检查递归终止条件 |
3. 打印调试与日志
新手爱用 System.out.println 调试,临时可以,但生产请用日志框架。关键点:打印"足够定位"的信息(变量值 + 上下文),而不是只打 "here"。
// 临时调试(提交前删掉)
System.out.println("debug: user=" + user + " step=" + step);
// 正经日志(SLF4J 风格,生产用)
log.info("处理用户 {} 在第 {} 步", userId, step);
设计模式入门(4 个最常用)写出"像样的"代码
1. 单例(Singleton)
全局只有一个实例。用枚举实现最简洁且天然防反射破坏。
enum Config { // 枚举单例:线程安全、防反射
INSTANCE;
public void load() { System.out.println("加载配置"); }
}
// 使用:Config.INSTANCE.load();
2. 工厂(Factory)
把"创建对象"的逻辑集中,调用方不关心具体类。
interface Shape { void draw(); }
class Circle implements Shape { public void draw(){ System.out.println("画圆"); } }
class Square implements Shape { public void draw(){ System.out.println("画方"); } }
class ShapeFactory {
static Shape create(String type) {
return switch (type) {
case "circle" -> new Circle();
case "square" -> new Square();
default -> throw new IllegalArgumentException(type);
};
}
}
// 使用:Shape s = ShapeFactory.create("circle");
3. 策略(Strategy)
把"可互换的算法"抽成接口,运行时选择。
interface Discount { double apply(double price); }
class NoDiscount implements Discount { public double apply(double p){ return p; } }
class HalfDiscount implements Discount { public double apply(double p){ return p/2; } }
class Checkout {
Discount strategy;
Checkout(Discount d){ this.strategy = d; }
double pay(double price){ return strategy.apply(price); }
}
// 使用:new Checkout(new HalfDiscount()).pay(100); // 50
4. 观察者(Observer)
一个对象变化,自动通知多个订阅者。Java 内置 Observer/Observable(已过时),现代用 PropertyChangeSupport 或自己实现。
import java.util.*;
interface Listener { void onEvent(String msg); }
class Publisher {
private final List<Listener> ls = new ArrayList<>();
void subscribe(Listener l){ ls.add(l); }
void publish(String msg){ ls.forEach(l -> l.onEvent(msg)); }
}
// 使用:p.subscribe(m -> System.out.println("收到:" + m));
单元测试入门(JUnit 5)让代码可信
写好功能代码只是上半场,能自动验证它"对"才是专业。JUnit 是 Java 最主流的测试框架。
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalcTest {
@Test
void add_正数相加_返回和() {
assertEquals(5, 2 + 3); // 期望 5
assertTrue(10 > 0);
assertThrows(ArithmeticException.class,
() -> { int x = 1 / 0; }); // 期望抛异常
}
}
常用断言
| 断言 | 含义 |
|---|---|
assertEquals(a, b) | 相等 |
assertTrue / assertFalse | 布尔 |
assertNull / assertNotNull | 空判断 |
assertThrows | 期望抛指定异常 |
assertAll | 一组断言全过才算过 |
@Test 开始,养成习惯。mvn test / gradle test,或在 IDE 里右键方法"Run Test"。测试代码放 src/test/java,和源码分离。Java 与其他语言对比选对工具,别盲目站队
| 维度 | Java | C / C++ | Python | JavaScript | Go |
|---|---|---|---|---|---|
| 类型 | 静态强类型 | 静态弱/强 | 动态强 | 动态弱 | 静态强 |
| 内存 | GC 自动 | 手动 | GC 自动 | GC 自动 | GC 自动 |
| 运行 | JVM 字节码 | 原生机器码 | 解释/字节码 | 引擎(V8) | 原生机器码 |
| 性能 | 高(JIT) | 极高 | 较低 | 中 | 高 |
| 并发 | 线程/并发库强 | 线程/裸锁 | GIL 受限 | 事件循环 | goroutine 轻量 |
| 典型场景 | 后端/安卓/大数据 | 系统/游戏/嵌入式 | 脚本/AI/数据 | 前端/全栈 | 云原生/微服务 |
| 上手难度 | 中 | 高 | 低 | 低 | 低 |
- 写企业后端、安卓、大数据:Java 生态最成熟,岗位最多。
- 要极致性能、贴近硬件、游戏引擎:C/C++。
- 做数据分析、AI、运维脚本:Python 开发最快。
- 做网页、全栈、前端:JavaScript/TypeScript 绕不开。
- 写高并发云服务、命令行工具:Go 语法简单、部署轻。
术语中英对照(随时查)读文档/看视频不卡壳
| 中文 | 英文 | 一句话 |
|---|---|---|
| 类 | class | 对象的蓝图 |
| 对象 / 实例 | object / instance | 按类创建出来的具体个体 |
| 封装 | encapsulation | 隐藏实现,暴露接口 |
| 继承 | inheritance | 子类复用父类 |
| 多态 | polymorphism | 同一接口,不同实现 |
| 抽象类 | abstract class | 不能实例化,可含抽象方法 |
| 接口 | interface | 只约定能力 |
| 构造器 | constructor | new 时初始化对象 |
| 方法重载 | overload | 同名不同参数 |
| 方法重写 | override | 子类覆盖父类 |
| 泛型 | generics | 类型参数化 |
| 自动装箱 | autoboxing | 基本类型↔包装类自动转换 |
| 异常 | exception | 运行时错误对象 |
| 垃圾回收 | garbage collection, GC | 自动回收无用对象 |
| 虚拟机 | JVM | 运行字节码的引擎 |
| 字节码 | bytecode | 编译后的 .class 中间码 |
| 不可变 | immutable | 创建后不能改(如 String) |
| 线程 | thread | 轻量执行单元 |
| 同步 / 锁 | synchronized / lock | 保护共享资源 |
| 反射 | reflection | 运行时探查类结构 |
| 注解 | annotation | 给代码的"标签" |
| 流式处理 | stream | 声明式集合操作 |
| Lambda 表达式 | lambda expression | 匿名函数 |
| 模块 | module | JPMS 强封装单元 |
21 天练习清单照着练,比看十遍都管用
| 天数 | 主题 | 今日任务 |
|---|---|---|
| 1-2 | 环境与语法 | 装 JDK / 跑通 Hello World;变量、类型、运算符各写 10 行 |
| 3-4 | 流程控制 | 写猜数字、九九乘法表、素数判断 |
| 5-6 | 数组与字符串 | 数组求和/最大、字符串反转/统计元音 |
| 7-9 | 类与对象 | 写 Student/Account/Bank 三个小类 |
| 10-11 | 继承与多态 | 动物继承树 + Shape 接口多态 |
| 12 | 异常 | 写带校验的除法/文件读取,正确捕获 |
| 13-14 | 集合 | 词频统计、去重、按值排序 |
| 15 | 泛型 | 写泛型栈/泛型最大值方法 |
| 16-17 | Lambda/Stream | 用流重写之前的集合练习 |
| 18 | 多线程 | 两个线程计数,对比加锁 vs AtomicInteger |
| 19 | IO | 读写文件 + try-with-resources |
| 20 | 综合项目 | 完成"学生成绩管理系统"专题 |
| 21 | 复盘 | 重做自测题 + 面试题,查漏补缺 |
常见问题 FAQ新手问得最多的几个
.class 后缀,应为 java Hello),或类名与文件名/包结构不一致,或不在正确目录下。回到第 1 节核对。
Main(文件名固定 Main.java)。若你本地类名不同,把 public 类改名为 Main 即可在本站运行。
写 Java 前的小自查清单交代码前扫一眼
- public 类名是否和文件名完全一致(含大小写)?
- 比较字符串内容是否用了
equals而不是==? - 整数相除需要小数时,是否转了浮点?
- 数组/集合下标是否在合法范围内?
- 子类构造器第一行是否调了父类构造器?
- 受检异常(IO 等)是否处理了或声明了
throws? - 资源(流/连接)是否用了 try-with-resources?
- 共享变量在并发下是否有锁或原子类保护?
- 集合遍历时是否避免了直接 remove?
- 字段是否尽量 private + getter/setter(封装)?
延伸资源学完本课程去哪深造
- dev.java · Oracle 官方的现代 Java 学习入口,示例新、讲得透。
- Java 官方文档 · 类库 API 与语言规范的权威来源。
- Java Language Specification · 想抠语言细节(如泛型擦除、switch 语义)看这里。
- LeetCode · 用 Java 刷算法题,巩固语法与数据结构。
- Spring 官网 · 下一步学 Spring Boot 写真实 Web 服务。
- 本站练习场 · 随时把想法敲成代码跑一跑,比看文档印象深。
自测题(点击展开答案)
- Q1:比较两个 String 的内容是否相同,应该用?A:
equals()。==比的是对象引用(地址),只有字面量因常量池巧合才可能相等,绝不能依赖。 - Q2:Java 中整数相除
10 / 3结果是?A:3(截断小数)。想得到 3.333 要让至少一个操作数为浮点,如10.0 / 3。 - Q3:子类构造器里对父类构造器的调用必须放在?A:第一行(
super(...)或隐式无参super())。父类没无参构造时必须显式传参。 - Q4:
Integer a=128, b=128; a==b的结果与原因?A:false。超出-128~127缓存范围,valueOf会 new 新对象,==比地址不等。内容相等应用equals。 - Q5:想安全自动关闭文件流,应优先用?A:
try-with-resources(try (流) {...}),编译器自动调用close(),比手写 finally 更可靠。 - Q6:
volatile能保证原子性(如n++线程安全)吗?A:不能。它只保证可见性与禁止重排序,不保证复合操作的原子性;计数要用AtomicInteger或加锁。 - Q7:Java 泛型在运行时能看到具体类型吗(如
List<String>)?A:不能。泛型会被擦除,运行时只有原始类型List。所以instanceof List<String>非法,也不能new T()。 - Q8:并行流
parallelStream()里修改共享的ArrayList安全吗?A:不安全。共享可变状态在并行流里会丢数据或报错。正确做法是让流保持无副作用,用collect收拢结果。 - Q9:Java 8 引入的两个最常用函数式特性是?A:Lambda 表达式与 Stream API(外加 Optional)。它们让集合操作声明式、简洁,是现代化 Java 的分水岭。
- Q10:重写
equals时为什么通常也要重写hashCode?A:HashMap/HashSet 先按hashCode找桶再equals比对。若内容相等但 hash 不同,会被放进不同桶,导致查不到/重复存,破坏集合语义。 - Q11:
final修饰变量、方法、类分别意味着?A:变量不可再赋值;方法不可被子类重写;类不可被继承。常用于定义常量、工具类、不可变设计。 - Q12:现代 Java(17 LTS)里,哪种写法能一行定义不可变数据类?A:
record(如record Point(int x,int y){}),编译器自动生成字段、构造器、getter、equals/hashCode/toString,样板代码几乎为零。