7
0
0

Java 基础笔记:语法、常用类与面向对象

2026-10-23
2026-10-23

📝 笔记摘要:Java 基础,从语法到面向对象

Java 语法不难磨人的是「规矩」。什么必须大写、什么不能改、什么能被继承、什么只能实现、哪些异常必须处理——这些约定编译器不提醒的时候没人拦你,等线上炸了才知道。

这篇按入门到进阶的顺序把 32 个知识点捋一遍,每个点都是四步:它是什么、语法长什么样、代码怎么写、容易踩哪。照着敲一遍比读十遍快,纯看很容易觉得都会,写出来才知道哪句没记住。

上篇 语法与常用类
  1. 开发环境配置 2. 第一个程序与基本语法 3. 注释
  4. 对象和类 5. 基本数据类型 6. 变量类型
  7. 变量命名规则 8. 修饰符 9. 运算符
  10. 循环结构 11. 条件语句 12. switch case
  13. Number & Math 类 14. Character 类 15. String 类
  16. StringBuffer 与 StringBuilder 17. 数组 18. 日期时间
  19. 正则表达式 20. 方法 21. 构造方法
  22. 文件与 IO 23. Scanner 类 24. 异常处理

下篇 面向对象
  25. 封装 26. 继承 27. 多态
  28. 抽象类 29. 接口 30. 重写与重载
  31. 包 32. 反射

1. 开发环境配置

JDK 装完能用了才谈得上写代码。这节定三件事:装哪个版本、环境变量怎么配、怎么确认装对了。

JDK 不是一样东西,是三样的合集:javac 把源码编译成字节码,java 加载字节码交给虚拟机执行,JVM 是真正跑字节码的那个虚拟机。只装 JRE 的话,java 命令有、javac 没有,编译这一步直接断掉。

JDK、JRE、JVM 三层包含关系,以及 javac 编译和 java 运行两条命令的分工

# 装完 JDK 配两个环境变量
setx JAVA_HOME "C:\Program Files\Java\jdk-21"
setx PATH "%PATH%;%JAVA_HOME%\bin"

# 验证,两个版本号对得上就算通了
java -version
javac -version

JAVA_HOME 指的是 JDK 根目录,不是 bin 目录
  PATH 里加的是 %JAVA_HOME%\bin,指错层级 Maven、Gradle 会报找不到 JAVA_HOME
版本按用途挑
  长期支持版是 8 / 11 / 17 / 21 / 25,最新的 LTS 是 25,2025 年 9 月发布
  非 LTS 版半年一版,2026 年出了 26 和 27,生产环境别用,官方只给 LTS 长期维护
  新项目从 21 起步,老系统要兼容才用 8
报「javac 不是内部或外部命令」是装错了,不是环境变量错了
  这说明装的是 JRE 或者 PATH 那一行写错了,重装 JDK 比反复调 PATH 快
版本管理工具可以先不装
  IDEA 自带 JDK 管理,命令行党再上 jenv 或 sdkman 也来得及

2. 第一个程序与基本语法

写完一个 .java 文件,javac 编译出 .class,java 再把 class 跑起来。两步是分开的,IDE 只是把这两步包起来了。

// HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}
# 编译,生成 HelloWorld.class
javac -encoding UTF-8 HelloWorld.java

# 运行,注意这里写的是类名,不带 .class
java HelloWorld

源文件名必须和 public 类名完全一致
  不一致直接编译失败:class HelloWorld is public, should be declared in a file named HelloWorld.java
  类名不带 public 时文件名能随意,但同名是好习惯
大小写敏感
  Hello 和 hello 是两个东西,类名、变量名、方法名都算
main 是唯一入口,签名不能改
  写成 public static void main(String[] args),参数名可以换,签名改了报 Main method not found
中文乱码就在编译那一步
  javac -encoding UTF-8,漏了这个参数,源码里的中文会变成问号
  IDE 里改编码只改文件不解决,终端编译还是得带参数

3. 注释

Java 有三种注释,作用完全不同:单行注释、多行注释、文档注释。前两种给看代码的人,第三种给生成 API 文档的工具。

// 单行注释,解释一行在干什么

/*
   多行注释
   可以写好几行
*/

/**
 * 文档注释
 *
 * @param name 传入的名字
 * @return 处理后的结果
 */

文档注释只认 /*** 开头
  写成 /* 开头就退化成普通注释,@param 这些标签不会被识别
注释写「为什么」,不写「做了什么」
  // 累加总分 这种等于没写,代码本身就说清了
  值得写的是 // 这里的分转成厘是历史原因,接口协议改不了
注释掉的代码要删
  留着是给三个月后的自己挖坑,版本管理里有历史,查得回来

4. 对象和类

类是模板,对象是按这个模板造出来的实例。一条狗是对象,「狗」这个类是模板;类里装着属性(状态)和方法(行为)。

public class Dog {
    String name;          // 属性
    int age;

    public void bark() {  // 方法
        System.out.println(name + " 在叫");
    }

    public static void main(String[] args) {
        Dog d = new Dog();  // 造一个对象
        d.name = "旺财";
        d.age = 3;
        d.bark();
    }
}

成员变量有默认值,局部变量没有
  int 是 0、boolean 是 false、char 是 \u0000、对象类型是 null
  局部变量必须先赋值再使用,否则编译报错,这是 Java 帮你在编译期拦下的坑
一个类只能有一个 public 类
  文件名跟这个 public 类同名,而且大小写要一致
static 方法里不能直接用 this
  因为 static 属于类,不属于某个对象,想访问成员得先 new 一个出来

5. 基本数据类型

Java 有 8 种基本类型,4 种整型、2 种浮点型、1 个字符、1 个布尔。剩下的都是对象类型(包装类)。

整数型
  byte  8 位  -128 ~ 127
  short  16 位  -32768 ~ 32767
  int   32 位  约 ±21 亿,日常够用
  long  64 位  约 922 亿亿,时间戳、订单号用这个
浮点型
  float  32 位  单精度,精度约 7 位十进制
  double  64 位  双精度,精度约 15 位,小数默认是它
其他两个
  char   16 位  一个字符,用单引号,能存一个汉字
  boolean  1 位  只有 true 和 false,不能拿 0 / 1 代替

import java.math.BigDecimal;

public class TypeDemo {
    public static void main(String[] args) {
        byte b = 127;
        short s = 32767;
        int i = 2147483647;
        long l = 9223372036854775807L;   // 不加 L 按 int 解析,直接编译失败
        float f = 3.14F;                // 不加 F 按 double 解析,再转回 float 会报错
        double d = 3.14;
        char c = 'A';
        boolean flag = true;

        System.out.println("0.1 + 0.2 = " + (0.1 + 0.2));
        System.out.println("和 0.3 相等吗:" + (0.1 + 0.2 == 0.3));

        BigDecimal money = new BigDecimal("0.1").add(new BigDecimal("0.2"));
        System.out.println("BigDecimal 求和 = " + money);
    }
}

浮点数存的是二进制近似值,不是十进制精确值
  所以 0.1 + 0.2 不等于 0.3,这是 IEEE 754 的规则,不是 bug
金额永远别用 double
  用 BigDecimal,而且必须用字符串构造
  new BigDecimal("0.1") 对,new BigDecimal(0.1) 错——后者把那个近似值原样存进去了
byte 溢出不会报错,会绕回负数
  byte n = (byte) 127; n++; 得到 -128,做计数和位运算时很容易踩
char 只能存一个字符
  'AB' 编译不过,多个字符要用 String

6. 变量类型

按位置分三种:方法里的是局部变量,类里非 static 的是成员变量(实例变量),类里带 static 的是类变量。

一行赋值语句在栈内存与堆内存中的分布,以及局部变量、成员变量、类变量各归何处

public class VariableDemo {
    int instanceVar = 10;        // 成员变量,每个对象一份
    static int staticVar = 20;   // 类变量,整个类一份

    public void method() {
        int localVar = 30;       // 局部变量,方法结束就没了
        System.out.println(instanceVar + staticVar + localVar);
    }

    public static void main(String[] args) {
        VariableDemo d = new VariableDemo();
        d.method();
        System.out.println(VariableDemo.staticVar);  // 类变量用类名访问
    }
}

成员变量跟着对象走,类变量跟着类走
  类变量被所有对象共享,改一个全体都变,常拿来放常量或者计数器
局部变量有作用域,出了那个大括号就没了
  成员变量没有作用域,全类可见
成员变量的默认值是 JVM 帮你初始化的
  局部变量不给默认值,这是 Java 和 C 的区别,也是 Java 更安全的地方

7. 变量命名规则

硬规则不多,但违反了一条就编译不过。

只能由字母、数字、$、下划线组成
  首字符不能是数字,123abc 是非法的
  不能用关键字,class、int、new 这些都别拿来当变量名
  大小写敏感,age 和 Age 是两个变量
Java 允许 $ 开头,别的语言不一定允许
  合法但不推荐,$ 在正则和 shell 里都有特殊含义
  全角空格、中文标点不能出现在标识符里

硬规则之外是命名习惯,团队协作时比编译器更值得较真。

int age = 18;                    // 变量、方法:小驼峰
int maxSize = 100;
String userName = "mavis";        // 多个单词,第二个起首字母大写
int MAX_RETRY_COUNT = 3;          // 常量:全大写,下划线分隔
class OrderService { }             // 类:大驼峰,每个单词首字母大写

// int 1age = 18;                 // 编译错误,数字开头
// int class = 18;                // 编译错误,关键字

别用拼音,别用单字母
  shuzi、nm 这种过三个月自己都看不懂
  循环变量用 i、j、index 是惯例,别的地方就别省了
布尔变量用 is / has / can 开头
  isEmpty、hasPermission,读代码时能省一半猜测
长短名的取舍看生命周期
  用得越久的名字越要写全,临时变量短一点没关系

8. 修饰符

修饰符分两类:控制可见性的访问控制修饰符,和控制行为的非访问控制修饰符。

访问控制
  public  任何地方都能访问
  protected 同包 + 子类能访问
  默认(不写) 同包能访问,跨包就不行了
  private  只有本类能访问
非访问控制
  final  不能被继承、方法不能被重写、变量只能赋值一次
  abstract 抽象类抽象方法,必须由子类实现
  static  属于类不属于对象
  synchronized 同步,多线程下加锁用
  transient 序列化时忽略这个字段
  volatile  保证多线程可见性,不保证原子性
  native  方法体由其他语言实现

public class ModifierDemo {
    public static final String VERSION = "1.0";  // 常量惯例:全大写
    private int count = 0;                      // 私有,外部改不了

    public void add() {
        count++;
    }

    public int getCount() {
        return count;
    }
}

能用 private 就别用 protected
  protected 把实现细节暴露给了子类,改起来到处是牵一发动全身
字段一律 private,靠方法暴露
  需要读就写 getter,需要改就写 setter,顺便能在里面加校验
常量用 static final
  static 保证全类一份,final 保证不会被改
default(不写)不是关键词
  Java 里真要写 default 关键字,是用在接口里的默认方法上,含义完全不同

9. 运算符

算术、关系、逻辑、位、赋值、条件、类型转换,七类。位运算平时用得少,但处理标志位、掩码的时候很顺手。

public class OperatorDemo {
    public static void main(String[] args) {
        int a = 10, b = 3;

        System.out.println(a + b);          // 13
        System.out.println(a / b);          // 3,整数除整数取整,不是 3.33
        System.out.println(a % b);          // 1,取余
        System.out.println(a >> 1);       // 5,右移一位等于除 2
        System.out.println(a << 1);       // 20,左移一位等于乘 2
        System.out.println(a > b && b > 0);   // true,&& 短路
        System.out.println(a > b || b > 5);         // true,|| 短路

        int c = 10;
        c += 5;                             // 复合赋值
        System.out.println(c);

        int max = a > b ? a : b;          // 条件运算符
        System.out.println(max);

        long big = a;                       // int 自动转 long
        int small = (int) big;              // long 转 int 要显式强转
        System.out.println(small);
    }
}

整数除法是截断,不是四舍五入
  10 / 3 是 3,10.0 / 3 才是 3.333
  算钱、算百分比记得有一方是浮点
== 比的是引用,不是内容
  两个 Integer 装同一个值,引用可能不同,内容才相等
短路是 Java 免掉空指针的神器
  obj != null && obj.size() > 0 顺序不能反,反了就 NPE
强制转换会静默丢精度
  (int) 3.99 是 3,不是 4

10. 循环结构

三种循环:for、while、do-while。区别在于判断时机——for 和 while 是先判断再执行,do-while 是先执行再判断,所以至少跑一次。

public class LoopDemo {
    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            System.out.println("for 第 " + i + " 次");
        }

        int j = 1;
        while (j <= 3) {
            System.out.println("while 第 " + j + " 次");
            j++;
        }

        int k = 1;
        do {
            System.out.println("do-while 第 " + k + " 次");
            k++;
        } while (k <= 3);

        for (String s : new String[]{"a", "b"}) {   // 增强 for
            System.out.println(s);
        }
    }
}

break 跳出整个循环,continue 跳到下一轮
  continue 后面那半轮代码不执行,但循环本身还在跑
do-while 末尾的分号别漏
  漏了编译报错,是新手第一大坑
增强 for 遍历集合时不能增删元素
  会抛 ConcurrentModificationException,要改就换普通 for 或者 Iterator
死循环的两种写法
  while (true) 和 for (;;),前者更常见

11. 条件语句

if / else if / else 的核心是只走一条分支,从上往下第一个满足的就执行,后面的全跳过。

public class IfDemo {
    public static void main(String[] args) {
        int score = 85;

        if (score >= 90) {
            System.out.println("优秀");
        } else if (score >= 60) {
            System.out.println("及格");
        } else {
            System.out.println("不及格");
        }
    }
}

条件里是布尔值,不是 0 和 1
  Java 不允许 if (count),必须写 if (count > 0)
  这是 Java 比 C 严格的地方,少了一整类 bug
条件表达式可以很复杂,但别写
  多个条件用 && 和 || 拆开,必要时提成一个有名字的布尔变量
大括号别省
  IDE 能补全,但复制粘贴时容易只带走一行

12. switch case

switch 适合「一个值对应一堆选项」的场景,比如状态码、星期、菜单编号。老写法靠 break 防穿透,Java 14 起的箭头写法不穿透。

switch 冒号写法的穿透路径与箭头写法的独立分支对比图

public class SwitchDemo {
    public static void main(String[] args) {
        int day = 3;

        // 老写法:漏了 break 就会往下穿透
        switch (day) {
            case 1:
            case 2:
                System.out.println("工作日");
                break;
            case 6:
            case 7:
                System.out.println("周末");
                break;
            default:
                System.out.println("其他");
        }

        // 箭头写法(Java 14+):不穿透,能当表达式用
        String name = switch (day) {
            case 6, 7 -> "周末";
            default -> "工作日";
        };
        System.out.println(name);

        // 箭头写法里想写多行,用 yield 产值
        int level = switch (day) {
            case 6, 7 -> {
                System.out.println("休息日");
                yield 1;
            }
            default -> {
                yield 0;
            }
        };
        System.out.println(level);
    }
}

穿透不一定是 bug
  case 1: 下面不写 break 直接落进 case 2:,是合并分支的常用写法
  但漏写 break 去做多分支,是最难查的一类错
箭头写法不能和冒号写法混用
  同一个 switch 里要么全冒号要么全箭头,混着写编译报错
switch 表达式要穷尽所有情况
  枚举类型全覆盖可以不带 default,其他类型不带 default 编译不过
Java 21 起 switch 支持模式匹配
  能按类型分派,case Integer i -> ... 这种写法

13. Number & Math 类

Number 是所有数字包装类的父类,Integer、Long、Double 这些都继承自它。Math 是一堆静态数学方法,不用 new。

public class NumberDemo {
    public static void main(String[] args) {
        int a = Integer.parseInt("123");        // 字符串转数字
        double b = Double.parseDouble("3.14");

        Integer boxed = 42;                      // 自动装箱
        int unboxed = boxed;                     // 自动拆箱

        System.out.println(a + b);
        System.out.println(Integer.MAX_VALUE);
        System.out.println(Integer.toBinaryString(10));   // 1010
        System.out.println(Math.abs(-5));
        System.out.println(Math.max(3, 7));
        System.out.println(Math.pow(2, 10));     // 1024.0
        System.out.println(Math.sqrt(16));       // 4.0
        System.out.println(Math.random());
    }
}

parseInt 遇到非数字直接抛异常
  NumberFormatException,不会像 Integer.parseInt 那样给你个 0
  从外部拿的字符串转数字,一定要包在 try 里
自动装箱有缓存
  Integer.valueOf 对 -128 到 127 之间的值返回缓存对象,所以这个区间里 == 和 equals 结果一样,超出就不一样了
Math.random() 返回的是 [0, 1) 的 double
  不是 int,要整数得自己乘完再转,而且转的时候用 (int) 是截断不是四舍五入
包装类和基本类型别混着比大小
  包装类之间比较用 equals,比大小要先拆箱

14. Character 类

Character 把 char 那些常用判断包成了静态方法,写字符校验的时候不用自己查 ASCII 表。

public class CharacterDemo {
    public static void main(String[] args) {
        char c = 'A';

        System.out.println(Character.isDigit('5'));        // true
        System.out.println(Character.isLetter(c));         // true
        System.out.println(Character.isUpperCase(c));      // true
        System.out.println(Character.isLowerCase(c));      // false
        System.out.println(Character.isWhitespace(' '));    // true
        System.out.println(Character.isAlphabetic('汉'));    // true,中文也算字母
        System.out.println(Character.toLowerCase(c));      // a
        System.out.println(Character.toUpperCase(c));      // A
        System.out.println(Character.getNumericValue('7')); // 7
    }
}

isLetter 对汉字返回 true
  因为它判断的是 Unicode 字母,不是 ASCII 范围
  只认英文字母的话用 isAlphabetic 自己配范围判断
Character 是个类,char 是关键字
  两者不一样,方法调用方式也不同,混着写容易把方法调到了包装类上
取 ASCII 码直接强转
  (int) 'A' 得 65,不需要额外方法

15. String 类

String 是 Java 里最常用的类,它的核心特性是不可变——创建之后内容永远改不了。所谓「修改」其实是造了一个新的字符串。

字符串常量池与堆中对象的区别,以及 equals 与 == 比较结果的对照

public class StringDemo {
    public static void main(String[] args) {
        String s = "Hello, Java";

        System.out.println(s.length());             // 11
        System.out.println(s.charAt(0));            // H
        System.out.println(s.substring(7));         // Java
        System.out.println(s.replace("Java", "Go")); // Hello, Go
        System.out.println(s.indexOf("Java"));       // 7
        System.out.println(s.toUpperCase());
        System.out.println(s.contains("Java"));      // true
        System.out.println(s.split(", ")[0]);        // Hello

        System.out.println(String.join("-", "a", "b", "c"));   // a-b-c
        System.out.println(String.format("%s 今年 %d 岁", "小明", 18));

        String a1 = "abc";
        String b1 = new String("abc");
        System.out.println(a1 == b1);       // false,引用不同
        System.out.println(a1.equals(b1));  // true,内容相同
    }
}

比内容用 equals,别用 ==
  == 比的是引用地址,两个内容一样的字符串用 == 比可能相等也可能不相等,看是不是同一个常量
  字符串常量池里放的是字面量,new String("abc") 一定在堆上另开一份
字符串拼接在循环里就是性能事故
  循环里用 += 拼字符串,每次都造新对象,正确做法是 StringBuilder
substring 会越界
  "abc".substring(3) 直接抛异常,边界要自己判
split 的结果可能为空数组
  输入为空串时 "".split(",") 返回长度 1 的数组,里面是空串,遍历时容易多处理一次

16. StringBuffer 与 StringBuilder

两个都是可变的字符串,区别只有线程安全:StringBuffer 的方法带 synchronized,多线程下安全但慢;StringBuilder 不加锁,单线程下快。

public class BufferDemo {
    public static void main(String[] args) {
        StringBuilder sb = new StringBuilder("hello");
        sb.append(" world").insert(0, ">> ").reverse();
        System.out.println(sb);            // >> dlrow olleh

        StringBuffer buf = new StringBuffer();
        buf.append("a").append("b");
        System.out.println(buf);           // ab
    }
}

绝大多数场景用 StringBuilder
  StringBuffer 是给「多线程共享一个字符串缓冲区」准备的,正常业务里很少遇到
这些方法返回的是自己
  所以能一路链式调用,不用每步都赋给变量
toString 才是真正拿字符串的地方
  sb.toString(),字符串拼接场景最后都要转一次
Java 字符串连接底层也在用 StringBuilder
  但编译器只在能确定是常量拼接时这么优化,循环里的拼接仍然是逐次新建

17. 数组

数组长度创建后不能改,元素类型固定,元素可以是基本类型也可以是对象。数组本身是对象,length 是它的属性。

import java.util.Arrays;

public class ArrayDemo {
    public static void main(String[] args) {
        int[] nums = {1, 2, 3};             // 声明并初始化
        int[] nums2 = new int[5];           // 默认全是 0
        String[] names = new String[]{"a", "b"};

        int[][] grid = new int[2][3];       // 二维数组
        grid[0][1] = 5;

        System.out.println(nums.length);                        // 3
        System.out.println(Arrays.toString(nums));              // [1, 2, 3]
        System.out.println(Arrays.stream(nums).sum());          // 6
        System.out.println(Arrays.stream(nums).max().getAsInt()); // 3

        String[] sorted = {"c", "a", "b"};
        Arrays.sort(sorted);
        System.out.println(Arrays.toString(sorted));             // [a, b, c]
        System.out.println(Arrays.toString(nums2));             // [0, 0, 0, 0, 0]
    }
}

数组越界是运行时异常
  ArrayIndexOutOfBoundsException,编译期查不出来,所以别指望编译器兜底
arr.length 和 str.length() 不一样
  数组是属性,字符串是方法,写反了直接编译报错
数组的默认值有,但数组变量本身没有
  int[] a = new int[5] 里 5 个 0;int[] b 直接用会 NPE
二维数组的行可以不等长
  int[][] g = new int[2][]; 之后每行还得单独 new
打印数组别直接 println
  出来的是 [I@1b6d3586 这种地址,要用 Arrays.toString

18. 日期时间

老 API 是 java.util.Date 加 SimpleDateFormat,后者不是线程安全的,多线程下格式化会串。新代码一律用 java.time,它从 Java 8 起就是线程安全的。

import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

public class DateDemo {
    public static void main(String[] args) {
        LocalDate today = LocalDate.now();          // 只有日期
        LocalDateTime now = LocalDateTime.now();    // 日期加时间

        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        System.out.println(fmt.format(now));

        System.out.println(today.plusDays(30));     // 30 天后
        System.out.println(today.isAfter(LocalDate.of(2020, 1, 1)));

        LocalDate start = LocalDate.of(2026, 1, 1);
        long days = Duration.between(start.atStartOfDay(), today.atStartOfDay()).toDays();
        System.out.println("今年过了 " + days + " 天");
    }
}

SimpleDateFormat 不是线程安全的
  把它做成静态字段,多线程下会得到乱七八糟的时间,要静态就 ThreadLocal 包一层
  新代码直接用 DateTimeFormatter,它是线程安全的,可以放心做常量
日期时间分好几种类型
  LocalDate 日期、LocalDateTime 本地日期时间、Instant 时间戳瞬间、ZonedDateTime 带时区
时区是坑
  服务器时间、数据库时间、用户时间三套,跨时区业务一律存 UTC 或时间戳
月份从 0 开始,日期从 1 开始
  LocalDate.of(2026, 1, 1) 的 1 是一月,但 Calendar 里是 0,老代码迁移时这里必错

19. 正则表达式

正则用来做字符串的模式匹配。Java 里用 Pattern 编译模式、Matcher 去匹配,两者配合使用。

import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class RegexDemo {
    public static void main(String[] args) {
        String text = "订单号 A20260723,金额 129.50 元";

        Pattern p = Pattern.compile("A(\\d{8})");
        Matcher m = p.matcher(text);
        if (m.find()) {
            System.out.println("订单号:" + m.group(1));   // 20260723
        }

        System.out.println(text.replaceAll("\\d+", "*"));  // 把所有数字换掉
        System.out.println("A20260723".matches("A\\d{8}")); // true,整串匹配
    }
}

find 和 matches 不是一个东西
  find() 是「找一段匹配」,matches() 是「整串必须匹配」
  校验手机号用 matches,用 find 会因为前面有别的字符而误判通过
group(0) 是整个匹配,group(1) 是第一组括号
  想拿到括号里的内容,正则里必须有括号
Pattern 可以预编译复用
  把 Pattern 提成静态常量,别在循环里反复 Pattern.compile
Java 字符串里的反斜杠要写两个
  \d 在 Java 字符串里是非法转义,得写成 \\d

20. 方法

方法是类里的行为单元。一个方法有名字、参数、返回值,返回值类型写 void 就是不返回。

public class MethodDemo {

    // 有返回值
    public static int add(int a, int b) {
        return a + b;
    }

    // 可变参数,个数不定
    public static int sum(int... nums) {
        int total = 0;
        for (int n : nums) {
            total += n;
        }
        return total;
    }

    // 不返回,直接打印
    public static void log(String msg) {
        System.out.println("[LOG] " + msg);
    }

    public static void main(String[] args) {
        System.out.println(add(1, 2));        // 3
        System.out.println(sum(1, 2, 3, 4)); // 10
        System.out.println(sum());           // 0,可变参数可以不传
        log("方法跑起来了");
    }
}

一个方法只干一件事
  一个方法同时做三件事,就得拆成三个,调用处才读得懂
可变参数必须是最后一个参数
  而且它本身是个数组,一个方法只能有一个可变参数
方法名 + 参数列表相同就是重载
  返回类型不同不算重载,编译报错
参数是值传递
  传对象进去,改的是对象内容,把对象的引用重新赋值回去是做不到的
返回值尽量不要返回数组本身
  调用方一改就改到内部数据,要返回就返回副本

21. 构造方法

构造方法没有返回值类型,名字必须和类名一模一样。new 的时候它负责把对象初始化好。

类加载与对象创建两个阶段的初始化顺序,含继承时的父先子后排列

public class User {
    private String name;
    private int age;

    static {
        System.out.println("静态代码块:类加载时执行,整个类只跑一次");
    }

    {
        System.out.println("实例代码块:每次 new 都执行");
    }

    public User(String name) {
        this.name = name;
    }

    public User(String name, int age) {
        this(name);          // 调用另一个构造方法,必须是第一句
        this.age = age;
    }
}

初始化顺序:静态块 → 实例块 → 构造方法
  静态块只跑一次,实例块每次 new 都跑,这个顺序在继承里还会再叠一层
不写构造方法,编译器会自动补一个空的
  但你一旦自己写了任何一个,编译器就不再补了,new User() 会直接编译失败
this(...) 必须是构造方法的第一句
  因为调用它之前 this 还没初始化好
构造方法里能抛异常
  对象创建到一半失败,Java 保证不会返回一个半成品对象

22. 文件与 IO

老 API 是 File 配 FileInputStream,新 API 是 java.nio.file 下的 Path 和 Files。新代码直接用后者,写起来短一大截。

import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;

public class FileDemo {
    public static void main(String[] args) throws Exception {
        Path path = Paths.get("notes.txt");

        Files.write(path, List.of("第一行", "第二行"), StandardCharsets.UTF_8);

        List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
        System.out.println(lines);

        // 流式读,大文件用这个
        try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
            String line;
            while ((line = reader.readLine()) != null) {
                System.out.println(line);
            }
        }
    }
}

try-with-resources 是必须的
  括号里声明的流,用完自动关闭,就算中途抛异常也关
  手写 finally 里 close 的话,close 自己抛异常会把原异常盖掉
字符集永远显式指定
  Windows 上默认 GBK,Linux 上默认 UTF-8,不写就是跨平台乱码
读大文件别用 readAllLines
  它会把整个文件读进内存,流式一行一行读才是正解
Paths.get 处理分隔符
  Windows 写 C:\\data\\a.txt,Linux 上要写 /data/a.txt,用 Paths.get 拼路径能跨平台
  服务端接收上传文件的完整流程见 Spring Boot 文件上传

23. Scanner 类

Scanner 从控制台、文件、字符串里读数据。控制台输入最常用,但它有两个坑几乎是每个人都踩过。

import java.util.Scanner;

public class ScannerDemo {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);

        System.out.print("请输入姓名:");
        String name = sc.nextLine();      // 读整行,含空格

        System.out.print("请输入年龄:");
        int age = sc.nextInt();           // 只读一个数字

        sc.nextLine();                    // 把上一行剩下的换行符吃掉

        System.out.println(name + " " + age);

        sc.close();                       // 关掉 System.in,后面就不能再读了
    }
}

next 和 nextLine 混用会漏内容
  nextInt() 读走数字但不读走行尾的换行符
  下一句 nextLine() 读到的是空字符串,多一个 sc.nextLine() 把它吃掉
next 系列遇到空格就停
  输入姓名用 nextLine(),next() 只能拿到第一个词
Scanner 比 BufferedReader 慢
  只是控制台交互用它,读文件还是走 BufferedReader
关掉 Scanner 等于关掉 System.in
  关了之后整个进程不能再读标准输入,单元测试里踩过这个坑

24. 异常处理

Java 的异常分两种:受检异常(checked)编译期强制处理,非受检异常(RuntimeException)可以不处理但通常应该处理。

Throwable 异常体系继承树,标出受检异常与非受检异常的分界

public class ExceptionDemo {
    public static void main(String[] args) {
        try {
            int[] arr = {1, 2, 3};
            System.out.println(arr[5]);
        } catch (ArrayIndexOutOfBoundsException e) {
            System.out.println("数组越界:" + e.getMessage());
        } finally {
            System.out.println("不管有没有异常都会执行");
        }

        try {
            System.out.println(Integer.parseInt("abc"));
        } catch (NumberFormatException e) {
            System.out.println("不是合法数字");
        }
    }
}
// 自定义异常一般继承 RuntimeException(不受检,不用在每个调用点声明)
class BizException extends RuntimeException {
    public BizException(String message) {
        super(message);
    }
}

// 受检异常用 throws 声明,调用方必须 catch 或者继续 throws
static int readAge() throws java.io.IOException {
    return Integer.parseInt("18");
}

catch 要具体,别直接 catch (Exception e)
  全都抓住等于没抓住,而且会顺手把 NullPointerException 这种代码 bug 一起吞掉
异常要处理,不要只打印然后继续跑
  catch 之后什么都不做,等于把故障藏起来
finally 用来释放资源
  但释放资源优先用 try-with-resources,它比 finally 可靠
自定义异常继承 RuntimeException
  这样调用方不用被迫写一堆无用的 catch
throw 抛一个,throws 声明会抛
  这两个词位置不同:throws 写在方法签名上,throw 写在方法体里

25. 封装

封装是把数据藏在类里面,不让人直接改,只能通过你设计好的方法进出。目的是让内部怎么存不影响外部怎么用。

public class Account {
    private double balance;   // 私有,外部拿不到也改不了

    public void deposit(double amount) {
        if (amount <= 0) {
            throw new IllegalArgumentException("金额必须大于 0");
        }
        balance += amount;
    }

    public void withdraw(double amount) {
        if (amount > balance) {
            throw new IllegalStateException("余额不足");
        }
        balance -= amount;
    }

    public double getBalance() {
        return balance;
    }
}

字段一律 private
  对外只暴露方法,内部字段改名、换存储结构都不影响调用方
校验写进方法里,而不是靠调用方自觉
  上面那个 deposit 里金额为负会直接抛异常,比注释里写「请传正数」靠谱得多
getter 不一定都要写
  只读的属性给个 getter 就够,setter 能不给就不给,少一个改入口少一个风险
别在 toString 里暴露敏感字段
  密码、密钥这类字段要么不打印,要么打码

26. 继承

继承让子类复用父类的属性和方法,也可以加自己的东西。Java 的类只能单继承,接口可以多实现,这是后面多态的基础。

class Animal {
    protected String name;

    public void eat() {
        System.out.println(name + " 在吃东西");
    }
}

class Dog extends Animal {
    public Dog(String name) {
        this.name = name;
    }

    public void bark() {
        System.out.println(name + " 在叫");
    }
}

// 用起来
Dog d = new Dog("旺财");
d.eat();      // 继承来的
d.bark();     // 自己加的

子类调不到父类的 private 成员
  private 只在本类可见,子类想用得改成 protected,或者父类提供方法
构造方法不继承
  子类的构造方法第一句默认调 super(),父类没无参构造就编译不过
Java 单继承,接口多实现
  需要「多继承」的效果就走接口,这比 C++ 的多继承干净得多
final 类不能被继承
  final 方法不能被重写,这两条是给框架和库留的扩展锁
继承表达「是一个」,组合表达「有一个」
  能用组合就别用继承,继承把父子绑死,改父类子类全得跟着改

27. 多态

多态是「同一个接口,不同的实现」。父类引用指向子类对象,调用的方法在运行时按对象的实际类型决定。

多态的两段绑定:编译期按引用类型静态绑定,运行期按实际类型动态分派

class Animal {
    public void speak() {
        System.out.println("动物叫");
    }
}

class Dog extends Animal {
    @Override
    public void speak() {
        System.out.println("汪汪");
    }
}

class Cat extends Animal {
    @Override
    public void speak() {
        System.out.println("喵喵");
    }
}

public class PolyDemo {
    public static void main(String[] args) {
        Animal a = new Dog();     // 父类引用指向子类对象
        a.speak();                // 输出 汪汪

        // a.bark();              // 编译不过,a 的类型是 Animal,里面没有 bark

        Animal[] animals = {new Dog(), new Cat()};
        for (Animal an : animals) {
            an.speak();           // 运行时按实际类型分派
        }
    }
}

编译看引用类型,运行看实际类型
  这就是「编译期静态绑定 + 运行期动态绑定」,多态只发生在运行期动态绑定的那一层
引用类型决定你能调哪些方法
  实际类型决定跑出来是什么效果
多态的三个前提
  有继承(或实现)、有重写、父类引用指向子类对象,少一条都不成立
用父类类型统一处理,是框架能扩展的根本原因
  Spring 的 Bean、MyBatis 的 Mapper 全是这套机制,MyBatis 基础那篇里接口加 XML 的组合就是典型例子

28. 抽象类

抽象类不能实例化,但可以有构造函数、成员变量、普通方法。它适合「一类东西有共同的骨架,但关键步骤各不相同」。

abstract class Shape {
    protected String name;

    public Shape(String name) {
        this.name = name;
    }

    // 抽象方法:只有声明没有实现,子类必须实现
    abstract double area();

    // 普通方法:所有子类共用的逻辑,直接写实现
    public void print() {
        System.out.println(name + " 的面积:" + area());
    }
}

class Circle extends Shape {
    private double r;

    public Circle(double r) {
        super("圆");
        this.r = r;
    }

    @Override
    double area() {
        return Math.PI * r * r;
    }
}

抽象类可以有构造方法
  用来初始化子类共用的字段,但不能 new 出抽象类本身
抽象方法不能是 private、static、final
  这三个和「子类必须重写」是矛盾的,编译器会拦
抽象类里的普通方法子类可以重写,也可以不重写
  只有抽象方法是强制的
有抽象方法的类必须声明成抽象类
  反过来,抽象类里可以一个抽象方法都没有

29. 接口

接口是纯粹的契约,只描述「能做什么」,不关心「怎么做」。Java 8 之后接口可以有 default 方法和静态方法。

interface Flyable {
    // 抽象方法,隐式 public abstract
    void fly();

    // default 方法,有默认实现,实现类可以不重写
    default void land() {
        System.out.println("开始降落");
    }

    // static 方法,属于接口本身,不能被实现类继承
    static boolean isFlyable(Object o) {
        return o instanceof Flyable;
    }
}

class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("扑棱棱飞走了");
    }
}

接口里的字段隐式是 public static final
  写不写修饰符都一样,所以接口常量天然是常量
接口不能有实例字段
  状态放接口里不合适,要状态就用抽象类
接口不能有构造方法
  它不是类,不能 new
抽象类和接口怎么选
  有状态、有共同实现、需要单继承 → 抽象类
  纯行为契约、要多实现 → 接口
Java 8 之后接口和抽象类的边界模糊了
  default 方法让接口也能带实现,但依然不能有状态
接口可以继承接口,一个类可以实现多个接口
  但一个类只能继承一个类,这个限制还在

30. 重写与重载

两个词长得像,意思完全相反:重载是同一个类里方法名相同参数不同,重写是子类重新实现父类的方法。

class Animal {
    public void speak() {
        System.out.println("动物叫");
    }

    public int sum(int a, int b) {
        return a + b;
    }
}

class Dog extends Animal {
    // 重写:签名完全一致,子类重新实现
    @Override
    public void speak() {
        System.out.println("汪汪");
    }

    // 重载:同一个类里方法名相同,参数个数不同
    public int sum(int a, int b, int c) {
        return a + b + c;
    }
}

判断标准
  在不在一个类里、在不在父子关系中——重载看类,重写看继承
重写要求方法签名完全一致
  返回类型要么一样,要么是协变的(父类返回 Object、子类返回 String 可以)
重写不能缩小可见性
  父类 public 的方法,子类重写成 protected 会编译报错
static 方法只能隐藏不能重写
  private 方法根本不参与继承,不存在重写
加上 @Override 注解
  方法名写错、参数写错,编译器立刻报错,没有这注解就是「新增了一个重载」,编译能过但逻辑全错
final 方法禁止重写
  父类标了 final,子类再写同名方法就是编译错误

31. 包

包用来组织类,避免类名冲突,也限定访问权限。包名必须和目录结构一致。

// 文件路径:com/example/demo/Main.java
package com.example.demo;

import java.util.ArrayList;
import java.util.List;

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("a");
        System.out.println(list);
    }
}

包名和目录必须一一对应
  包声明是 com.example.demo,文件就得放在 com/example/demo/ 下
同一个包里互相用不需要 import
  java.lang 包自动导入,String、System 这些不用写 import
默认权限是「包级可见」
  同包能用,跨包就得 public,这跟文件系统目录权限是两回事
包名全小写,用域名反写
  com.example.demo 这种,避免全网冲突
分包按职责分
  controller / service / dao / model 这种分层,或者按 feature 分,别把一百个类扔一个包里

32. 反射

反射让程序在运行时去检查类、创建对象、调用方法。日常业务用得不多,框架和工具库靠它干大量底层活。

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;

public class ReflectDemo {
    public static void main(String[] args) throws Exception {
        Class<?> clazz = Class.forName("java.lang.String");

        Method[] methods = clazz.getDeclaredMethods();
        System.out.println("方法数量:" + methods.length);

        Constructor<?>[] ctors = clazz.getDeclaredConstructors();
        System.out.println("构造方法数量:" + ctors.length);

        // 调用实例方法
        Method length = clazz.getDeclaredMethod("length");
        length.setAccessible(true);           // 绕过访问检查
        Object result = length.invoke("hello");
        System.out.println(result);

        // 反射创建对象
        Constructor<?> ctor = clazz.getDeclaredConstructor();
        ctor.setAccessible(true);
        Object obj = ctor.newInstance();
        System.out.println(obj.getClass().getName());
    }
}

反射的代价是性能
  反射调用比直接调用慢一个数量级,热点路径上别用
setAccessible 能绕过 private
  Java 9 之后模块系统会拦一部分,不是万能钥匙
newInstance 已经废弃
  用 getDeclaredConstructor().newInstance(),老的那个在部分版本里已经抛异常
反射拿到的异常在 InvocationTargetException 里
  真正的异常是它的 cause,直接 getMessage() 拿到的是包装信息
它的真正用武之地
  Spring 的依赖注入、MyBatis 的 Mapper 代理、JUnit 的断言,全靠反射,日常业务里手写反射的场合不多

💡 补充几个容易忽略的:
编码从项目第一天就定死
  源码 UTF-8、编译加 -encoding UTF-8、读写文件显式指定字符集,三处对齐才不会出现「我这儿好好的」
异常不要吞,也不要只打日志
  该恢复就恢复,该往上抛就往上抛,实在处理不了至少留下足够排查的现场
别在循环里 new 正则、SimpleDateFormat
  提成静态常量,这是最常见也最容易忽略的性能坑
集合初始化指定容量
  知道大概要放多少就写 new ArrayList<>(100),省掉一连串扩容拷贝
官方文档和教程的定位不一样
  官方文档按主题排,适合查细节;菜鸟教程的 Java 目录按学习顺序排,适合当路线图
  两边对着看,学的顺序和查的效率都能兼顾
学框架之前先把集合和泛型吃透
  Spring、MyBatis 里到处是泛型集合,看不懂的话源码根本读不动

🔥 一句话总结:JDK 装 21 或 25 长期支持版,JAVA_HOME 指根目录不指 bin;源文件名必须和 public 类名完全一致,javac 编译 java 运行,中文乱码就加 -encoding UTF-8;8 种基本类型加包装类,金额一律 BigDecimal 且必须用字符串构造,浮点数天生不等于 0.3;成员变量有默认值、局部变量没有;字符串不可变,比内容用 equals,循环拼接换 StringBuilder;日期时间用 java.time,SimpleDateFormat 不是线程安全的;读文件用 try-with-resources,字符集永远显式指定;catch 要具体,处理不了就往上抛;面向对象四件套是封装藏数据、继承复用代码、多态看实际类型、抽象和接口定契约,重写是子类换实现、重载是同类换参数,加上 @Override 让编译器帮你查错;反射是框架的地基,自己写之前先想清楚性能账。

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或者给予支持!