🧭 编程学习站

Java 入门 + 进阶训练

公网版 · 手机/电脑自适应

🐣 纯新手先看这一框(30 秒秒懂)

1

Java 适合做什么?企业后台、安卓 App、大数据(Hadoop/Spark)、微服务、金融系统。一句名言:"一次编写,到处运行"——编译成字节码,靠 JVM 跨平台。

2

为什么要学它?岗位多、生态成熟、强类型能在编译期挡掉一堆低级 bug。想进大厂后端、做安卓,Java 都是硬通货。

3

怎么跑起来?本站「动手实验室」选 Java 点运行即可,不用装环境;想深入再装 JDK,用 javac Hello.java && java Hello 编译运行。

4

新手最常踩的坑?① 类名必须和文件名一致且首字母大写;② == 比的是对象地址不是内容(字符串要用 equals);③ 所有代码都在类里,没有全局函数;④ 整数相除会截断小数。

课程说明从零掌握 Java 语法 + 进阶性能与并发要点

Java 由 Sun 公司在 1995 年发布,设计目标是"跨平台、面向对象、自动内存管理"。它编译为字节码(.class),在 JVM(Java 虚拟机)上运行,因此同一份代码能在 Windows、Linux、macOS 上跑——这就是"一次编写,到处运行"(Write Once, Run Anywhere)。

本课程假设你有最基础的编程概念(变量、循环)。重点放在Java 区别于 C/C++ 的关键差异:所有代码必须包在类里、强类型、垃圾回收(GC)自动管内存、丰富的标准库(集合、并发、IO)。进阶部分会直击面试高频的 JVM 内存模型、锁、GC、Stream 等工业级话题。

环境与版本 用 JDK(建议 17 LTS 或 21 LTS,长期支持版)。编译命令 javac X.java,运行 java X(注意运行时不带 .class 后缀)。本站运行器使用 OpenJDK 22。LTS 版本会长期获得安全更新,生产环境务必用 LTS。
学习路径建议 Java 基础语法 → 面向对象(类/继承/多态/接口) → 集合与泛型 → Lambda/Stream → 并发与 JVM 原理。别一上来死磕 JVM 调优,先把语法和"对象思维"练熟,否则容易半途而废。

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)
为什么有 main 的 static static 表示"属于类而非对象",JVM 启动时还没创建任何对象,所以入口必须是 static,才能直接调用。参数 String[] args 是命令行传进来的参数数组,例如 java Hello a b cargs = ["a","b","c"]
输出三件套 println 输出并换行;print 输出不换行;printf 支持格式化(System.out.printf("%.2f", 3.14159) 输出 3.14)。字符串拼接用 + 最直观,高频拼接用 StringBuilder(见第 6 节)。

2. 变量与数据类型八种基本类型、引用类型、命名

Java 是强类型语言:每个变量必须先声明类型,且类型不可随意变。类型分两类:基本类型(primitive)直接存值(在栈上);引用类型(reference)存的是对象在堆里的地址。

类别类型位数/范围示例
整数byte8 位,-128~127byte b=1;
short16 位short s=10;
int32 位(最常用)int n=100;
long64 位,字面量加 Llong t=1L;
浮点float32 位,加 Ffloat f=1.2f;
double64 位(默认)double d=1.2;
字符char16 位 Unicodechar c='中';
布尔boolean仅 true/falseboolean 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 变负),大整数用 longBigInteger。④ 不同类型不能直接运算,需强转。
命名约定 类名大驼峰(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);
    }
}
switch 注意 老式 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。
length 是字段不是方法 数组用 a.length(字段);字符串用 s.length()(方法);集合用 list.size()(方法)。三者不同,别混。
多维数组本质 Java 没有真正的多维数组,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 指"调用该方法的那个对象"。当参数名和字段名撞车时用 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(参数),否则编译失败。这保证"父类部分先初始化好"。
用继承要谨慎 优先"组合优于继承"。只有真满足 is-a 才继承;否则用一个字段持有另一个对象更灵活,避免继承层级过深难维护。方法重写时务必写 @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 的价值@Override 让编译器检查你确实重写了父类/接口方法。拼错方法名(少个字母)会立刻报错,而不是悄悄新建一个无关方法——这是个救命注解,务必写。
接口 vs 抽象类 需要"多个不同类型都能被同样对待"用接口;需要共享代码/状态用抽象类。Java 8+ 接口也能有 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 与 return 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+ 的 removeIflist.removeIf(x -> x.isEmpty());
常见实现对照 ListArrayList(数组)、LinkedList(链表);SetHashSet(哈希)、TreeSet(红黑树有序);MapHashMapTreeMapLinkedHashMap(保序)。

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
    }
}
类型擦除 Java 泛型是编译期的,运行时 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() 就够了。
函数式接口 Lambda 只能赋给"只有一个抽象方法的接口"(如 RunnableComparatorFunction)。常用内置接口:Predicate(判断)、Function(转换)、Consumer(消费)、Supplier(供给)。
Stream 惰性 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。这是经典竞态。更简单的替代是 AtomicIntegern.incrementAndGet())。
锁升级 synchronized 并非一上来就重锁:JVM 会按"偏向锁 → 轻量级锁(CAS自旋)→ 重量级锁(操作系统互斥)"逐步升级,只在竞争激烈时才用重锁,平时开销很小(详见干货第 5 条)。
更优方案 高并发计数优先用 java.util.concurrent.atomic.AtomicInteger 或无锁结构;复杂协作用 java.util.concurrent 包(ReentrantLockCountDownLatch 等),比手写 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 ~ 127Integer 被缓存复用,所以 == 恰好为 true;超出范围就是不同对象,== 变 false。永远用 equals 比包装类内容,别被缓存侥幸蒙蔽(详见干货第 3 条)。
自动拆箱空指针 Integernull 时自动拆箱会抛 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();
        }
    }
}
原理:AutoCloseable 只要在 try(...) 括号里声明实现了 AutoCloseable 接口的对象(如流、连接),编译器会在结尾自动调用它的 close()。不用再手写 finally 关资源(详见干货第 9 条)。
路径与异常 IO 操作抛受检异常 IOException,必须处理或声明 throws。生产代码别用 printStackTrace() 草草了事,应走日志框架(SLF4J + Logback)。大文件用缓冲流(BufferedReader/Writer)提升性能。
NIO.2 java.nio.fileFiles/Path 比老 File 好用:Files.readAllLinesFiles.copyFiles.walk 一行搞定常见操作,优先用它。

进阶训练场(含答案)

题 1:写一个带构造/封装的 Student 类

Student 私有字段 namescore;构造器初始化;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());
        }
    }
}

🧪 动手实验室

不用从零敲代码:下面每个程序都已写好,点「▶ 运行」直接看结果;想改就在框里改,再点运行。手机、电脑都一样用。打开本页时会自动跑一次,方便你先看效果。

① 基础输出与计算

② 数组与循环(求和 + 找最大)

③ 字符串处理(反转 + 统计元音)

④ 类与对象(简单银行账户)

⑤ 集合 + 流式处理(偶数平方求和)

每个框都能用下拉切换语言、「↺ 重置」回到初始代码。想写完整程序或体验其他语言,去顶部「练习场」(支持 Python/Java/Go/JS/TS/Rust/C#/Ruby 等)。注意:本站运行器文件名固定为 Main.java,所以 public 类必须叫 Main

实战干货(12 条工业级技巧)面试与上线都绕不开

干货 1 · JVM 内存模型 JVM 运行时数据区大致分:堆(Heap)放所有对象实例,是 GC 主战场,线程共享;栈(Stack)每线程私有,存局部变量、方法参数、操作数栈,方法结束即弹栈;方法区(元空间 Metaspace,Java 8+)存类信息、常量、静态变量;PC 寄存器记录当前线程执行到哪条指令。-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
干货 2 · 字符串常量池与 IntegerCache 字符串字面量("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 返回池里引用)
干货 3 · 自动装箱拆箱性能坑 在循环里大量 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;
干货 4 · 泛型类型擦除与桥接方法 Java 泛型在编译后擦除为上限类型(无界擦成 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); }  // 桥接方法
}
干货 5 · synchronized 锁升级 现代 JVM 的 synchronized 是自适应、可升级的:无竞争时偏向锁(记录线程 ID,近乎无开销);有轻度竞争升级为轻量级锁(CAS 自旋);竞争激烈才膨胀为重量级锁(挂起线程、进操作系统队列)。注意:Java 15 起偏向锁默认禁用-XX:-UseBiasedLocking),因为维护偏向锁的撤销成本在现代多核上往往得不偿失。别再神话"偏向锁"了。
附:锁使用建议 竞争不激烈时 synchronized 已足够;需要超时/可中断/公平锁时用 ReentrantLock;读多写少考虑 ReadWriteLock。别一上来就用最重的锁。
干货 6 · volatile 可见性与禁止重排序 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;
    }
}
干货 7 · Stream 短路与并行流陷阱 Stream 的 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());
干货 8 · GC 选型 G1 是 Java 9+ 的默认收集器,兼顾吞吐与停顿,适合大多数服务;ZGC(JDK 11+,生产可用 15+)和 Shenandoah 主打超低停顿(<10ms),适合对延迟极敏感的金融/交易系统,代价是更高 CPU 开销。别盲目追新:先压测,多数业务 G1 就够。调优口诀——先定延迟目标再选收集器,别一上来就调几百个 JVM 参数。
常用 GC 参数 -XX:+UseG1GC(默认)、-XX:+UseZGC-Xmx4g 设堆上限、-XX:MaxGCPauseMillis=200 设目标停顿。监控用 GC 日志 + Prometheus/Grafana。
干货 9 · try-with-resources 原理 任何实现 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 都会自动关闭
干货 10 · Optional 正确用法 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(); }
干货 11 · JIT 热点方法与分层编译 JVM 不全靠解释执行:C1 编译器快速产出较优机器码,C2 编译器热点方法(频繁执行,由计数器识别)做激进优化(内联、逃逸分析、去虚化)。这叫分层编译(tiered compilation),默认开启。启示:Java 程序"越跑越快",微基准测试务必充分预热(warm-up),否则测的是解释期性能,毫无意义。
相关工具 -XX:+PrintCompilation 看哪些方法被 JIT 编译;jstack/jmap/jcmd 排查;JMH 才是正经微基准框架,别用 System.currentTimeMillis() 自测性能。
干货 12 · 模块系统 JPMS Java 9 引入模块系统(JPMS),用 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. 八种基本类型与默认值

类型位数默认值包装类范围
byte80Byte-128 ~ 127
short160Short-32768 ~ 32767
int320Integer约 ±21 亿
long640LLong约 ±9×10¹⁸
float320.0fFloat约 6~7 位有效数字
double640.0dDouble约 15 位有效数字
char16'\u0000'Character0 ~ 65535(Unicode)
boolean1(JVM 实现相关)falseBoolean仅 true / false
要点局部变量没有默认值,不初始化就使用会编译报错;只有类的成员变量(字段)才有上表默认值。这是新手常踩的"变量未初始化"错误来源。

2. == 还是 equals?决策表

比较对象比内容用比同一对象用说明
基本类型(int/double…)==基本类型没有 equals,直接比值
Stringequals==(仅常量池巧合)内容比较一律 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.ofJava 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 个坑每条都值得你记在便签上

  1. 类名与文件名不一致:public 类名必须和 .java 文件名完全一致(含大小写),否则编译失败。
  2. == 比字符串:引用类型 == 比地址;内容比较一律 equals
  3. 忘记 break 导致 switch 穿透:用表达式形式 case x -> ... 避免。
  4. 整数相除截断10/3 是 3,不是 3.333;要浮点就转一个操作数。
  5. 数组越界:下标 0 ~ length-1,越界抛 ArrayIndexOutOfBoundsException
  6. 混淆 length / length() / size():数组用 .length,字符串 .length(),集合 .size()
  7. 自动拆箱 null 空指针Integer 为 null 时拆箱直接 NPE,取集合值先判空。
  8. 依赖 Integer 缓存-128~127== 比较包装类为 false,用 equals
  9. 子类构造器忘写 super():父类无无参构造时必须显式调用 super(参数),且放第一行。
  10. 漏写 @Override:拼错方法名时编译器不会提醒,写了能立刻报错。
  11. 抛了受检异常不处理IOException 等必须 try-catch 或 throws
  12. catch 里空着吞异常catch(Exception e){} 会让 bug 消失得无影无踪。
  13. 遍历集合时直接 remove:用迭代器 it.remove()removeIf
  14. 在并行流里改共享集合:结果不确定,用 collect 收拢。
  15. 以为 volatile 保证原子性:它只保可见性,n++ 仍需锁或 AtomicInteger
  16. 认为 String 可变s += "x" 生成新对象;高频拼接用 StringBuilder
  17. 手动关资源漏写 finally:用 try-with-resources 自动关闭。
  18. ==Double 特殊值NaN 不等于任何值(包括自己),用 Double.isNaN()
  19. 不理解泛型擦除:运行时拿不到 T 的具体类型,不能 new T[]
  20. 微基准不预热就下结论:Java 有 JIT,没预热的测量毫无意义,用 JMH。
一句话Java 是"编译期严格 + 运行期托管"的语言:把类型、异常、资源在写代码时就管规矩,JVM 会替你兜底内存与并发的大部分麻烦。

综合小项目 · 学生成绩管理系统把前面学的串起来,写一个能跑的小程序

下面这个项目把"类、集合、泛型、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实体类的字段常设为 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));
    }
}
Optional 兜底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 与 reversed()Comparator 的 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());
        }
    }
}
项目小结一个真实项目就是"实体类 + 集合存储 + 业务逻辑(Stream 计算)+ 持久化(IO)"的组合。把这个骨架记住,你就能套到订单、商品、用户等各种业务上。

面试高频 12 问边学边想,胜过死记

Q1 · String 为什么不可变? String 内部是 final char[](Java 9+ 是 byte 数组),且类与字段都 final,无法修改。好处:可安全进常量池复用、可做 HashMap 的 key(hash 不变)、线程安全。代价是每次"改"都产生新对象。
Q2 · final、finally、finalize 区别? final 修饰类不可继承、方法不可重写、变量不可改;finally 是 try 里总执行的块(用于关资源);finalize 是对象被 GC 前调用的废弃方法,别用。
Q3 · 重载 overload 与重写 override? 重载是同一类里"同名不同参数"(编译期绑定);重写是子类覆盖父类方法(运行期多态)。重写要满足"两同两小一大":方法名参数同、返回类型/异常更小或相等、访问修饰符更大或相等。
Q4 · 抽象类与接口怎么选? 需要共享代码/状态、且是 is-a 关系用抽象类;只需要约定"能做什么"、且一个类要实现多种能力用接口。Java 8+ 接口也能有 default 方法,但抽象类仍可持有非静态字段。
Q5 · HashMap 底层原理? JDK 8 中 HashMap = 数组 + 链表 + 红黑树。key 的 hash 决定桶位置;冲突时挂链表;当链表长度 ≥ 8 且数组容量 ≥ 64 时转红黑树,查询从 O(n) 降到 O(log n)。key 最好是不可变且重写过 hashCode/equals 的对象。
Q6 · 为什么重写 equals 必须重写 hashCode? HashMap/HashSet 先用 hashCode 找桶,再用 equals 比对。若两个对象 equals 为 true 但 hashCode 不同,会被放进不同桶,导致"逻辑相等却查不到 / 重复存入",破坏集合语义。
Q7 · 什么是浅拷贝与深拷贝? 浅拷贝只复制引用(两个对象指向同一份内部对象);深拷贝连内部对象也一并复制。实现 Cloneable 默认是浅拷贝;要深拷贝需手动递归复制,或改用不可变对象 + 复制构造器。
Q8 · 线程的几种创建方式? ① 继承 Thread 重写 run;② 实现 Runnable(更灵活,推荐);③ 实现 Callable(有返回值、可抛异常,配合 FutureTask);④ 用线程池 Executors。实际项目几乎都用线程池,不直接 new Thread。
Q9 · synchronized 和 ReentrantLock 区别? synchronized 是 JVM 内置关键字,自动释放、可锁升级、用起来简单;ReentrantLock 是 API 层面的锁,支持可中断、超时获取、公平锁、多条件变量,但必须手动 unlock(常放 finally)。
Q10 · 什么是死锁,如何避免? 两个线程各持有一把锁、又互相等待对方那把锁,就死锁。避免:① 统一加锁顺序;② 用 tryLock 超时放弃;③ 缩小锁粒度;④ 用高层并发工具(如 ConcurrentHashMap)替代手写锁。
Q11 · 什么时候用单例?怎么写线程安全? 全局只需一个实例(如配置中心、连接池)时用单例。线程安全写法:① 饿汉式(类加载即创建,简单安全);② 双重检查 + volatile(懒加载,见干货第 6 条);③ 枚举单例(最简洁且防反射破坏)。
Q12 · JVM 调优从哪入手? 先定目标(吞吐 or 低延迟),再用 -Xmx 设堆、选 G1/ZGC、开 GC 日志,用工具(GC 日志 + Prometheus)看停顿与回收频率,最后才微调参数。绝大多数性能问题其实是代码/SQL 问题,别忙着调 JVM。

代码风格与工程规范写好 Java 的"软实力"

规范点推荐做法反面例子
命名类大驼峰、方法/变量小驼峰、常量全大写下划线class studentint 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<>()
用工具兜底团队里用 CheckstyleSpotBugs、IDE 的格式化器统一风格,把规范交给 CI,别靠人肉 review 揪空格。
声明变量用接口类型List<String> list = new ArrayList<>(); 比声明成 ArrayList 好——未来想换成 LinkedList 只改右边,调用方代码不动。这是"面向接口编程"的基本功。

学完之后怎么走一张路线图,别迷茫

  1. 夯实语法:把本课程的 10 节入门 + 6 节进阶练熟,每个例子自己敲一遍、改一遍。
  2. 面向对象进阶:深入理解封装/继承/多态、接口设计、设计模式(单例、工厂、策略、观察者先学这四个)。
  3. 标准库:集合、并发(java.util.concurrent)、IO/NIO、时间 API(java.time,别用老的 Date)。
  4. JVM 原理:内存模型、GC、类加载、JIT——对应本课程 12 条干货,是进阶与面试分水岭。
  5. 工程化:Maven/Gradle 构建、单元测试(JUnit)、日志(SLF4J)、Git。
  6. 框架:Spring Boot 写 Web 服务,MyBatis/JPA 操作数据库,这是找工作的最常用组合。
  7. 并发与性能:线程池、锁、无锁结构、性能调优,往资深走必须啃。
别踩的弯路不要一上来就背 Spring 全家桶却不懂 Java 本身;不要只看视频不动手;不要跳过 JVM 原理。"语法会了"只是起点,能写出正确、健壮、可维护的代码才算入门。
给纯新手的最后一句话Java 入门不难,难在"坚持写"。每天写 30 行、改 30 行,一个月后你会惊讶于自己的进步。本站「练习场」和上面的「动手实验室」随时等你。

扩展专题一 · 日期时间 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);
    }
}
别混用旧 APIjava.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());
    }
}
枚举的好处类型安全(只能传枚举值,不能传任意 int)、可遍历、可附带数据、可用在 switch 里。状态/类型/选项类字段优先用 enum。

注解 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"(转进制)
parse 会抛异常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 开始进入了"每半年一版"的快节奏。下面这些是你一定会天天用到的现代语法,看到旧代码没有它们别奇怪(那是老项目)。

版本特性一句话
8Lambda / Stream / Optional函数式与流式处理基石
8接口 default 方法接口能带默认实现,向后兼容
8新的日期时间 APIjava.time(见专题一)
9模块系统 JPMSmodule-info.java(见干货 12)
9集合工厂 List.of一行建不可变集合
10var 局部变量推断var x = new ArrayList<>();
11字符串 repeat/lines小工具方法
14switch 表达式case x -> ... 可返回值
15文本块 Text Blocks"""...""" 多行字符串
16record一行定义不可变数据类
17密封类 sealed限制谁能继承
17模式匹配 instanceofif (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());
}
项目用哪个版本新项目直接上 Java 17 LTS 或 21 LTS,上面的现代特性几乎都能用。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 行
别被框架行吓到栈里大部分是 JDK/框架内部行,先找你自己包名的那一行,那里才是你该改的地方。

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);
断点调试学会用 IDE(IntelliJ IDEA / Eclipse)打断点、单步执行、看变量。比满屏 println 高效十倍,是职业程序员的标配技能。

设计模式入门(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));
模式是工具不是教条别为了用模式而用模式。小项目直接写清楚就好;当发现"这里总在改、这里总在加 if 分支"时,再考虑引入对应模式。

单元测试入门(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 与其他语言对比选对工具,别盲目站队

维度JavaC / C++PythonJavaScriptGo
类型静态强类型静态弱/强动态强动态弱静态强
内存GC 自动手动GC 自动GC 自动GC 自动
运行JVM 字节码原生机器码解释/字节码引擎(V8)原生机器码
性能高(JIT)极高较低
并发线程/并发库强线程/裸锁GIL 受限事件循环goroutine 轻量
典型场景后端/安卓/大数据系统/游戏/嵌入式脚本/AI/数据前端/全栈云原生/微服务
上手难度
怎么选
  • 企业后端、安卓、大数据:Java 生态最成熟,岗位最多。
  • 极致性能、贴近硬件、游戏引擎:C/C++。
  • 数据分析、AI、运维脚本:Python 开发最快。
  • 网页、全栈、前端:JavaScript/TypeScript 绕不开。
  • 高并发云服务、命令行工具:Go 语法简单、部署轻。
语言不是壁垒学通一门(尤其是 Java 这种概念完整的),再学别的会很快——变量、循环、函数、对象、并发的思想是通用的。本站还有 C/C++/Python/Go/JS/TS/Rust/C#/Ruby 课程,学有余力可以横向对比,理解更深。
别陷入"语言之争"没有最好的语言,只有最适合场景的语言。面试考察的是计算机基础与解决问题的能力,语言只是表达工具。把 Java 学扎实,你已经具备转任何语言的地基。

术语中英对照(随时查)读文档/看视频不卡壳

中文英文一句话
class对象的蓝图
对象 / 实例object / instance按类创建出来的具体个体
封装encapsulation隐藏实现,暴露接口
继承inheritance子类复用父类
多态polymorphism同一接口,不同实现
抽象类abstract class不能实例化,可含抽象方法
接口interface只约定能力
构造器constructornew 时初始化对象
方法重载overload同名不同参数
方法重写override子类覆盖父类
泛型generics类型参数化
自动装箱autoboxing基本类型↔包装类自动转换
异常exception运行时错误对象
垃圾回收garbage collection, GC自动回收无用对象
虚拟机JVM运行字节码的引擎
字节码bytecode编译后的 .class 中间码
不可变immutable创建后不能改(如 String)
线程thread轻量执行单元
同步 / 锁synchronized / lock保护共享资源
反射reflection运行时探查类结构
注解annotation给代码的"标签"
流式处理stream声明式集合操作
Lambda 表达式lambda expression匿名函数
模块moduleJPMS 强封装单元
读法建议英文文档/报错信息里这些词出现频率极高。混个脸熟后,报错栈、Stack Overflow、官方文档都能读得更顺。初学不必背,遇到回来查即可。

21 天练习清单照着练,比看十遍都管用

天数主题今日任务
1-2环境与语法装 JDK / 跑通 Hello World;变量、类型、运算符各写 10 行
3-4流程控制写猜数字、九九乘法表、素数判断
5-6数组与字符串数组求和/最大、字符串反转/统计元音
7-9类与对象写 Student/Account/Bank 三个小类
10-11继承与多态动物继承树 + Shape 接口多态
12异常写带校验的除法/文件读取,正确捕获
13-14集合词频统计、去重、按值排序
15泛型写泛型栈/泛型最大值方法
16-17Lambda/Stream用流重写之前的集合练习
18多线程两个线程计数,对比加锁 vs AtomicInteger
19IO读写文件 + try-with-resources
20综合项目完成"学生成绩管理系统"专题
21复盘重做自测题 + 面试题,查漏补缺
关键心法每天写代码的时间 > 看教程的时间。卡住就回到对应章节的例子,改一改、跑一跑,比一直往下看有效得多。本站「动手实验室」和「练习场」随时可用。
你已经读到这儿了说明你是认真想学会 Java 的。把上面 21 天走完,你会从"看得懂"变成"写得出"。剩下的只是时间积累——开始写第一行吧。

常见问题 FAQ新手问得最多的几个

Q:学 Java 要先学 C 吗? A:不需要。Java 比 C 更友好(自动内存、无指针算术),零基础可直接学 Java。C 的内容在这站也有单独课程,想理解底层再回头看。
Q:JDK、JRE、JVM 什么关系? A:JVM 是运行引擎;JRE = JVM + 运行所需类库(只跑程序用);JDK = JRE + 编译器 javac 等开发工具(写程序用)。开发者装 JDK 即可。
Q:Java 和 JavaScript 是什么关系? A:基本没关系,只是名字都带 Java 做营销。一个是静态强类型、跑在 JVM 的编译型语言;一个是动态弱类型、跑在浏览器的脚本语言。语法表面有点像,但思想差异很大(本站另有 JS 课程)。
Q:报错 "找不到或无法加载主类" 怎么办? A:通常是运行命令写错(多写了 .class 后缀,应为 java Hello),或类名与文件名/包结构不一致,或不在正确目录下。回到第 1 节核对。
Q:本站运行器报编译错误,本地能跑? A:运行器用 OpenJDK 22 且要求 public 类名为 Main(文件名固定 Main.java)。若你本地类名不同,把 public 类改名为 Main 即可在本站运行。
最后一个提醒编程是"做"出来的,不是"看"出来的。遇到任何报错误,先把错误信息完整读一遍——十次有八次答案就在那几行里。

写 Java 前的小自查清单交代码前扫一眼

  • public 类名是否和文件名完全一致(含大小写)?
  • 比较字符串内容是否用了 equals 而不是 ==
  • 整数相除需要小数时,是否转了浮点?
  • 数组/集合下标是否在合法范围内?
  • 子类构造器第一行是否调了父类构造器?
  • 受检异常(IO 等)是否处理了或声明了 throws
  • 资源(流/连接)是否用了 try-with-resources?
  • 共享变量在并发下是否有锁或原子类保护?
  • 集合遍历时是否避免了直接 remove?
  • 字段是否尽量 private + getter/setter(封装)?
习惯成自然这 10 条覆盖了 90% 的新手错误。每次写完对照一遍,一个月后你根本不会犯这些错了。

延伸资源学完本课程去哪深造

  • dev.java · Oracle 官方的现代 Java 学习入口,示例新、讲得透。
  • Java 官方文档 · 类库 API 与语言规范的权威来源。
  • Java Language Specification · 想抠语言细节(如泛型擦除、switch 语义)看这里。
  • LeetCode · 用 Java 刷算法题,巩固语法与数据结构。
  • Spring 官网 · 下一步学 Spring Boot 写真实 Web 服务。
  • 本站练习场 · 随时把想法敲成代码跑一跑,比看文档印象深。
学习节奏每天 30 分钟 + 写代码,比周末突击 5 小时效果好。把"看懂→改写→独立写"三步循环起来,Java 很快就能上手。

自测题(点击展开答案)

  • 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-resourcestry (流) {...}),编译器自动调用 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,样板代码几乎为零。
本 Java 课程遵循 Java 17/21 LTS 常用子集,手机与电脑端自适应,配套本站运行器(OpenJDK 22)练习效果最佳。 公网参考:Oracle Java Docs · dev.java · Java Language Specification (JLS)。
🛠 在线练习