JavaSE 笔记
java常见语法
一个类 = 一个程序入口,运行哪个类,就只执行那个类里的 main。其他类里的 main 方法,不会被自动触发。
Java中,javac 文件名.java 用于编译源文件生成class字节码文件,java 类名(不带后缀)执行程序,JVM会自动运行目标类中的main方法;仅当前执行类的main方法生效,其他类里的main不会自动运行,且public修饰的公共类需与java文件名同名。
System.out.println() 方法接收一个对象时,会调用该对象的 toString() 方法得到字符串再打印。
如果 User 类没有重写 toString(),就会调用 Object 类的默认 toString(),输出类似
**POJO = Plain Old Java Object,中文叫:普通老式 Java 对象。就是一个纯粹的 Java 类,只用来存数据,没有任何继承、没有任何实现、不继承任何框架类,只有私有属性、get/set 方法、构造方法,不做业务逻辑,专门当数据载体。
如果一个 Java 文件里 有 public 类。文件名必须和这个 public 类完全一样,包括大小写。如果文件里 没有 public 类。文件名可以和任意一个类名相同,没有强制要求。一个 Java 文件 最多只能有一个 public 类
this 指向的是当前对象本身,所以它存在于对象中,不是类里。但 this 的代码是写在类里面的,它代表正在调用方法的那个对象。super 写在子类里面,指向当前对象的父类部分
Java 的"引用" = C++ 的"指针"(但去掉了指针运算) 。Java 里没有 C++ 那种用 & 的"引用"
引用类型变量在声明后必须通过实例化开辟数据空间,才能对变量所指向的对象进行访问
成员变量对于类中的方法、构造方法或者语句块是可见的。一般情况下应该把成员变量设为私有。通过使用访问修饰符可以使成员变量对子类可见。 成员变量具有默认值。数值型变量的默认值是0,布尔型变量的默认值是 false,引用类型变量的默认值是 null。变量的值可以在声明时指定,也可以在构造方法中指定;
成员变量和局部变量区别:
- 声明位置不同成:员变量(属性):在类中定义;局部变量:在方法/代码块内部定义。
- 默认初始值不同:成员变量:未手动赋值自带对应类型默认值;局部变量:无默认值,必须先赋值才能使用。
- 变量同名优先级:同类中局部变量可与成员变量重名,局部优先;如需访问同名成员变量用
this.属性名,this指代当前调用方法的对象。
public 意味着任何地方的其他类都能访问。default 则是同一个包的类可以访问。protected 表示同一个包的类可以访问,其他的包的该类的子类也可以访问。private 表示只有自己类能访问。
父类中声明为 public 的方法在子类中也必须为 public。父类中声明为 protected 的方法在子类中要么声明为 protected,要么声明为 public,不能声明为 private。父类中声明为 private 的方法,不能够被子类继承。
Java运算符的结合顺序(即“先左还是先右”)遵循一个核心原则:绝大多数运算符是左结合的(从左向右算),只有少数几个是右结合的(从右向左算)。
具体来说:赋值类运算符(=、+=等)、单目运算符(++、--、!、~等)、以及条件运算符(?:)是右结合;而其他像算术、关系、逻辑(&&、||)、位运算、括号、方法调用、数组下标等通通是左结合。如果搞混了,括号 () 可以强制指定顺序。
拆箱和装箱是Java为了弥补“基本类型不是对象”这个缺陷而设计的自动机制:
装箱指编译器自动把int、double等基本类型用Integer.valueOf()等方法包装成对应的对象(如int→Integer),拆箱则相反,用Integer.intValue()等把对象变回基本类型。这样做是为了让基本类型能放进ArrayList、充当泛型参数、表达null等“对象专属”场景,同时让程序员写代码时感觉不到类型差异——但你心里要清楚:自动拆装箱本质是编译器替你插入了转换代码,因此涉及大量运算或null值时仍需谨慎。
Python 格式化输出主要有四种方式,最推荐用 f-string(Python 3.6+):在字符串前加 f,用 {变量名} 直接嵌入变量,比如 print(f"年龄{age}岁"),需要控制格式时写 {age:2d}(整数两位)、{pi:.2f}(浮点两位小数)或 {rate:.1%}(百分比)。此外还有 str.format() 方法("{}".format(变量))、老式 % 格式化("%d" % 变量)和 Python 3.8+ 新增的 f"{var=}" 调试写法(输出 var=值)。核心记住:新代码一律用 f-string,简洁直观性能好。
String:字符串常量,字符串长度不可变。Java中String 是immutable(不可变)的。用于存放字符的数组被声明为final的,因此只能赋值一次,不可再更改。 StringBuffer:字符串变量(Synchronized,即线程安全)。如果要频繁对字符串内容进行修改,出于效率考虑最好使用 StringBuffer,如果想转成 String 类型,可以调用 StringBuffer 的 toString() 方法。Java.lang.StringBuffer 线程安全的可变字符序列。在任意时间点上它都包含某种特定的字符序列,但通过某些方法调用可以改变该序列的长度和内容。可将字符串缓冲区安全地用于多个线程。 StringBuilder:字符串变量(非线程安全)。在内部 StringBuilder 对象被当作是一个包含字符序列的变长数组。 Java 中 StringBuffer 和 String 是有一定的区别的,首先,String 是被 final 修饰的,他的长度是不可变的,就算调用 String 的 concat 方法,那也是把字符串拼接起来并重新创建一个对象,把拼接后的 String 的值赋给新创建的对象,而 StringBuffer 的长度是可变的,调用StringBuffer 的 append 方法,来改变 StringBuffer 的长度,并且,相比较于 StringBuffer,String 一旦发生长度变化,是非常耗费内存的!
**基本原则:如果要操作少量的数据用 String ; 单线程操作大量数据用StringBuilder ;多线程操作大量数据,用StringBuffer。 通过 new关键字来生成对象是在堆区进行的,而在堆区进行对象生成的过程是不会去检测该对象是否已经存在的。因此通过 new 来创建对象,创建出的一定是不同的对象,即使字符串的内容是相同的。
Java里的多维数组本质上是“数组的数组”——外层数组的每个元素存放的是内层数组的引用(地址),而不是直接存放数据。因此无论元素类型是字符串、整数还是其他对象,声明时都可以只指定外层维度(如int[][] matrix = new int[2][];),之后再分别给每个内层数组分配不同的大小(如matrix[0]=new int[3]; matrix[1]=new int[5];),从而构造出每行长度的“锯齿数组”。matrix本身存的是内层数组的地址,matrix[0]存的是这一行数组的地址,matrix[0][0]才存到真正的数值。这种设计让多维数组非常灵活,但也要注意:必须先分配外层,再分配内层,否则会因内层数组为null而空指针异常。matrix 本身存的是“内层数组的地址”,这句话的意思是:matrix 是一个引用变量,它指向一个一维数组。这个一维数组里的每个元素,又存着另一个数组的地址。
.of不是 Java 关键字,而是一种命名习惯——很多类的静态工厂方法都叫 of,用来根据参数创建对象
静态方法是属于类本身、而不是属于某个具体对象的方法,用 static 关键字修饰。调用时直接用 类名.方法名() 即可,不需要先创建对象。它的主要特点包括:不能访问实例变量或实例方法(因为还没有对象),只能访问静态变量或调用其他静态方法,常用于工具方法(如 Math.max()、Collections.sort())、工厂方法(如 Pattern.compile()、Integer.valueOf())或单例模式的 getInstance()。静态方法在类加载时就已经存在,与对象无关,所以非常适合实现不依赖对象状态的功能。注意:静态方法不能被重写(但可以被子类隐藏),也不能使用 this 或 super 关键字。
静态方法由static修饰,归属类而非实例,可通过类名直接调用,无需new创建对象;方法内不能使用this、super,只能直接访问静态成员,无法直接调用实例变量与实例方法,多用于封装通用工具方法。
try{
// 程序代码
}catch(异常类型1 异常的变量名1){
// 程序代码
}catch(异常类型2 异常的变量名2){
// 程序代码
}catch(异常类型3 异常的变量名3){
// 程序代码
}
上面的代码段包含了 3 个 catch块。可以在 try 语句后面添加任意数量的 catch 块。 如果保护代码中发生异常,异常被抛给第一个 catch 块。如果抛出异常的数据类型与 ExceptionType1 匹配,它在这里就会被捕获。如果不匹配,它会被传递给第二个 catch 块。如此,直到异常被捕获或者通过所有的 catch 块。
子类的所有构造方法内部, 第一行会(隐式)自动先调用父类的无参构造函数super();如果子类构造方法第一行显式调用了父类构造方法,系统就不再调用无参的super()了。
多态引用时,构造子类对象时的构造方法的调用顺序 1,先调用超类的构造方法,多重超类首先调用最远超类的方法; 2,然后再执行当前子类的构造方法
普通类的抽象方法由子类实现;枚举类的抽象方法由每个枚举常量(即每个枚举对象)各自实现。 原因在于枚举类不能被继承(无法创建子类来提供实现),而枚举常量本身就是具体的对象,不能含有未实现的方法,所以必须每个枚举常量都单独给出方法体。这种设计常用于让不同枚举值拥有相同的行为接口但具体逻辑不同(如计算器的加减乘除),从而避免写大量 if-else 或 switch 语句。
类路径(Classpath)就是告诉 Java 虚拟机(JVM)“去哪里找 .class 文件”的路径列表。 可以有很多个(用分隔符隔开),设置它就是为了让 JVM 能找到你写的和引用的所有类。类路径是JVM搜寻class文件与jar包的目录集合,可配置多个路径,Windows用分号、Linux用冒号分隔,常规配置包含当前目录、项目编译目录和第三方依赖jar路径,仅用于指定类的查找位置,和类定义、程序入口无关。
java常见使用
String file = this.getClass().getClassLoader().getResource("emp.xml").getFile();
这一行代码的作用:从项目的 resources 目录中找到 emp.xml 文件,并返回它在磁盘上的真实路径(字符串)。
this.getClass():先拿到当前对象的类(一个Class对象),获取当前这个 Controller 对象运行时所属的 Class 对象。.getClassLoader():从类上拿到“类加载器”(负责找文件的东西)类加载器负责加载.class文件,也能读取resources目录下的资源文件。.getResource("emp.xml"):用类加载器在resources目录下找emp.xml,返回一个URL对象(代表文件位置).getFile():从URL中取出路径字符串(例如/E:/project/emp.xml) 最终:file变量里存的就是emp.xml在硬盘上的完整路径,方便后面代码去读取和解析这个文件。这是一个固定写法,照用即可。
public class Demo {
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(3, 12, 5, 20, 8, 15);
List<Integer> result = nums.stream()
.filter(n -> n > 10)
.collect(Collectors.toList());
System.out.println(result); // [12, 20, 15]
}
}
这段代码的作用是:从 nums 这个集合里筛选出大于 10 的数字。nums.stream() 表示把集合交给 Stream 去处理,.filter(n -> n > 10) 表示只保留满足条件的元素,n -> n > 10 是 Lambda,意思是“拿到每个数字 n,判断它是否大于 10”,最后 .collect(Collectors.toList()) 把筛选后的结果重新变成一个 List。简单记:Stream 用来处理集合,Lambda 用来写处理规则
Stream 的作用就是:让你用更简洁的方式处理集合里的数据。以前你想从 nums 这个 List 里找出大于 10 的数,要自己写 for 循环、if 判断、add 到新集合;用了 Stream 以后,就可以写成“流水线”的形式:nums.stream().filter(n -> n > 10).collect(...),意思是先把 nums 交给 Stream 处理,然后 filter 负责筛选,只留下满足 n > 10 的元素,最后 collect 把筛选结果重新变成一个 List。它本质上不是新集合,而是 Java 提供的一套集合数据处理工具,常用于筛选、转换、排序、去重、统计等操作。简单记:List 存数据,Stream 处理数据,Lambda 写处理规则。
new String[]{“a”, “b”} = 创建一个 String 一维数组,并直接初始化元素。 String[] 是数组类型,{} 是数组内容。
String[] arr = new String[2];
arr[0] = "a";
arr[1] = "b";
new String[]{“a”,“b”} 是 Java 创建并初始化一维数组的简写语法,叫"匿名数组"或"数组初始化器"。它相当于 new String[2]; arr[0]=“a”; arr[1]=“b”;。前面有 [] 是因为它要声明"这是一个数组",但这不是二维数组(二维数组需要两个 [][])
正则表达式
正则表达式(Regular Expression,简称 Regex)是一种用规则匹配字符串的语法。它不是 Java 独有的东西,很多语言都支持。简单理解:正则就是用一串特殊符号描述“我要找什么样的文本”。例如判断手机号格式、校验邮箱、提取日期、替换敏感词、按复杂规则分割字符串,都可以用正则。
例如:
"\\d+" // 匹配一个或多个数字
"[a-zA-Z]+" // 匹配一个或多个英文字母
"\\w+@\\w+\\.com" // 简单匹配邮箱格式
注意:正则表达式本身是一套规则,Java 只是用字符串来写这套规则。
1. 正则怎么用
Java 中常见用法有两类:一类是直接用 String 的方法,适合简单场景;另一类是用 Pattern 和 Matcher,适合复杂匹配和提取。
方式一:String 直接使用
String s = "12345";
System.out.println(s.matches("\\d+"));
matches() 用来判断整个字符串是否符合正则。这里 "\\d+" 表示一个或多个数字,所以结果是 true。
常见方法:
str.matches(regex); // 判断整个字符串是否匹配
str.replaceAll(regex, newStr); // 按正则替换
str.split(regex); // 按正则切割
示例:
String text = "a1b22c333";
String result = text.replaceAll("\\d+", "#");
System.out.println(result); // a#b#c#
这里 \\d+ 会匹配连续数字,所以 1、22、333 都会被替换成 #。
方式二:Pattern + Matcher
如果要从一段文本中反复查找符合规则的内容,一般用 Pattern 和 Matcher。
String text = "张三:18, 李四:20";
Pattern pattern = Pattern.compile("(\\S+):(\\d+)");
Matcher matcher = pattern.matcher(text);
while (matcher.find()) {
System.out.println(matcher.group(0)); // 整体匹配,如 张三:18
System.out.println(matcher.group(1)); // 第1组:姓名
System.out.println(matcher.group(2)); // 第2组:年龄
}
Pattern 表示编译后的正则规则,Matcher 表示用这个规则去匹配某个字符串。find() 是查找下一个匹配结果,group(0) 是整体匹配内容,group(1)、group(2) 是括号捕获到的内容。
2. 常见正则符号
| 写法 | 含义 | 示例 |
|---|---|---|
abc |
直接匹配 abc | abc 匹配 "abc" |
. |
匹配任意一个字符 | a.c 可匹配 abc、a1c |
\\d |
匹配一个数字 | \\d 匹配 0-9 |
\\D |
匹配一个非数字 | \\D 匹配字母、符号等 |
\\w |
匹配字母、数字、下划线 | \\w+ 匹配 abc_123 |
\\s |
匹配空白字符 | 空格、换行、制表符 |
[abc] |
匹配 a、b、c 中任意一个 | [abc] 匹配 "a" |
[^abc] |
匹配不是 a、b、c 的任意字符 | [^abc] |
[a-z] |
匹配小写字母 | [a-z]+ |
[0-9] |
匹配数字 | 等价于 \\d |
^ |
匹配开头 | ^abc 表示必须以 abc 开头 |
$ |
匹配结尾 | abc$ 表示必须以 abc 结尾 |
3. 量词:控制出现次数
量词用来表示前面的内容出现多少次。
| 写法 | 含义 | 示例 |
|---|---|---|
* |
出现 0 次或多次 | a* 匹配空串、a、aaa |
+ |
出现 1 次或多次 | a+ 匹配 a、aaa |
? |
出现 0 次或 1 次 | a? 匹配空串或 a |
{n} |
正好出现 n 次 | \\d{3} 匹配 3 位数字 |
{n,} |
至少出现 n 次 | \\d{2,} 匹配至少 2 位数字 |
{n,m} |
出现 n 到 m 次 | \\d{2,4} 匹配 2 到 4 位数字 |
例如:
String phone = "[已隐藏手机号]";
System.out.println(phone.matches("1[3-9]\\d{9}"));
解释:1 表示必须以 1 开头,[3-9] 表示第二位是 3 到 9,\\d{9} 表示后面还有 9 位数字。
4. 字符类 [ ] 和分组 ( )
[ ] 表示“从里面选一个字符”。
"[abc]"
它匹配的是一个字符:a、b、c 其中之一,不是匹配 "abc" 整体。
( ) 表示把一部分正则作为一个整体,叫捕获组。
"(abc){3}"
它匹配的是:
abcabcabc
因为 (abc) 把 abc 当成一个整体,{3} 表示这个整体重复 3 次。
5. 捕获组
捕获组就是用括号 ( ) 把正则的一部分包起来,让它变成可以单独取出的内容。匹配成功后,可以用 matcher.group(1)、group(2) 取出每个括号匹配到的内容。编号按左括号从左到右,从 1 开始;group(0) 表示整个匹配结果。
示例:提取日期中的年、月、日。
String date = "2026-06-20";
Pattern pattern = Pattern.compile("(\\d{4})-(\\d{2})-(\\d{2})");
Matcher matcher = pattern.matcher(date);
if (matcher.matches()) {
System.out.println(matcher.group(0)); // 2026-06-20
System.out.println(matcher.group(1)); // 2026
System.out.println(matcher.group(2)); // 06
System.out.println(matcher.group(3)); // 20
}
捕获组常见作用:提取子串、把多个字符作为整体使用量词、在 replaceAll() 中用 $1、$2 引用分组。
String date = "2026-06-20";
String result = date.replaceAll("(\\d{4})-(\\d{2})-(\\d{2})", "$1年$2月$3日");
System.out.println(result); // 2026年06月20日
6. Java 中为什么要写两个反斜杠
正则里的 \d 表示数字,但 Java 字符串里 \ 本身也有转义含义,所以在 Java 代码中要写成 "\\d"。
也就是说:
"\\d+"
Java 字符串真正传给正则引擎的是:
\d+
所以记住:正则语法里写一个反斜杠,Java 字符串里通常要写两个反斜杠。
7. 常用正则例子
"\\d+" // 一个或多个数字
"\\d{6}" // 6 位数字
"[a-zA-Z]+" // 一个或多个英文字母
"\\w+" // 字母、数字、下划线
"1[3-9]\\d{9}" // 简单手机号
"\\S+@\\S+\\.\\S+" // 简单邮箱
"\\d{4}-\\d{2}-\\d{2}" // 日期格式:2026-06-20
8. 一句话总结
正则表达式就是用一套符号规则匹配字符串。普通字符直接匹配自己,\\d、\\w、\\s 匹配特定类型字符,*、+、{n} 控制次数,[ ] 表示选一个字符,( ) 表示分组并可以提取内容。在 Java 中,正则要写在字符串里,所以反斜杠通常要写成 \\。
反射
反射就是:Java 程序在运行时,拿到一个类的结构信息,并且可以动态创建对象、访问属性、调用方法。 普通写法是编译前就确定类和方法,例如 new Emp()、emp.setName();反射写法是运行时通过类名、属性名、方法名去操作,例如根据完整类名 "com.example.Emp" 找到 Emp 类,再创建对象、设置字段、调用方法。
因为 Java 程序运行不是直接跑 .java 文件,而是跑编译后的 .class 字节码。.class 文件里本来就保存了类的结构信息,比如类名、字段、方法、构造方法、访问修饰符等。JVM 加载 .class 文件后,也需要知道这些信息才能创建对象、调用方法、检查类型。反射就是 Java 把 JVM 掌握的一部分“类元信息”开放给程序员使用。
所以反射能做到这些,不是因为它神奇,而是因为:
.class 文件里本来就有类结构信息;JVM 加载类时本来就要解析这些信息;Class 对象把这些信息暴露给 Java 代码;Constructor / Field / Method 让你操作构造方法、字段、方法。
反射最核心的入口是 Class 对象。每个类被 JVM 加载后,都会有一个对应的 Class 对象,它保存这个类的运行时信息,比如类名、构造方法、属性、方法、注解等。常见获取方式有三种:
==这里Emp.class 是 Emp 类的 Class 对象,表示“Emp 这个类本身的信息”,不是 Emp 的实例。==
Class<Emp> c1 = Emp.class; // 已知类名,最常用
Class<?> c2 = new Emp().getClass(); // 已有对象,获取对象所属类
Class<?> c3 = Class.forName("com.example.Emp"); // 只有完整类名字符串,动态加载
反射里主要用到四个类:Class、Constructor、Field、Method。Class 表示类本身,Constructor 表示构造方法,Field 表示成员变量,Method 表示成员方法。多数反射操作就是围绕这四个东西展开。
最常用的操作之一是 反射创建对象。普通创建对象是:
Emp emp = new Emp();
反射创建对象是:
Class<Emp> clazz = Emp.class;
Emp emp = clazz.getDeclaredConstructor().newInstance();
这里 getDeclaredConstructor() 是获取构造方法,newInstance() 是调用构造方法创建对象。==如果以后看到工具类里传 Emp.class,通常就是为了让工具类知道要创建哪种对象。==
第二个常用操作是 反射操作属性。例如 Emp 里有私有属性:
private String name;
可以通过反射赋值:
Emp emp = new Emp();
Field field = Emp.class.getDeclaredField("name");
field.setAccessible(true);
field.set(emp, "张三");
这里最特别的是 setAccessible(true),它可以绕过访问权限检查,所以即使 name 是 private,也能被反射访问。这是反射的典型特性之一:可以突破封装。但也正因为这样,普通业务代码不建议乱用。
第三个常用操作是 反射调用方法。普通调用是:
emp.setName("张三");
反射调用是:
Method method = Emp.class.getDeclaredMethod("setName", String.class);
method.invoke(emp, "张三");
这里 getDeclaredMethod("setName", String.class) 表示找到 setName(String name) 这个方法,invoke(emp, "张三") 表示在 emp 对象上调用这个方法,并传入 "张三"。反射调用方法时,方法名可以来自字符串,所以它比普通调用更动态。
反射里有一组很容易混的 API:getXxx() 和 getDeclaredXxx()。简单记住:带 Declared 的,获取当前类自己声明的成员,包括 private;不带 Declared 的,主要获取 public 成员,并且包含继承来的 public 成员。 所以平时想拿私有属性、私有方法,一般用:
getDeclaredField()
getDeclaredMethod()
getDeclaredConstructor()
然后再配合:
setAccessible(true)
JavaSE 里反射最典型的用途是写“通用工具”。比如一个方法想创建任意类型的对象,而不是写死只能创建 Emp:
public static <T> T createObject(Class<T> clazz) throws Exception {
return clazz.getDeclaredConstructor().newInstance();
}
使用时:
Emp emp = createObject(Emp.class);
User user = createObject(User.class);
Student student = createObject(Student.class);
这就是反射的价值:让代码不再写死某一个类,而是可以根据传进来的 Class 对象动态处理不同类型。 你之前看到的:
XmlParserUtils.parse(file, Emp.class);
这里的 Emp.class 就是在把 Emp 的 Class 对象传给解析工具。解析工具内部拿到 Emp.class 后,就知道要把 XML 数据封装成 Emp 对象,可能会通过反射创建 Emp,再根据 XML 字段反射调用 setter 或直接给字段赋值。
反射还有一个重要搭配是 注解。注解本身只是标记,反射负责读取标记。例如可以通过反射判断某个方法上有没有注解:
Method method = clazz.getDeclaredMethod("list");
boolean exists = method.isAnnotationPresent(MyAnnotation.class);
所以一句话概括:注解负责标记信息,反射负责在运行时读取这些信息并执行对应逻辑。
反射的几个特有点需要记住:第一,反射是运行时机制,很多错误编译期发现不了,比如方法名写错,运行时才报错;第二,反射可以通过 setAccessible(true) 访问 private 成员,能力强但破坏封装;第三,反射比普通调用慢一些,因为多了运行时查找和动态调用;第四,反射主要用于框架、工具类、配置驱动、对象映射、注解解析,普通业务代码能直接调用就直接调用。
最后压缩成一句话:JavaSE 反射主要掌握 Class、Constructor、Field、Method 四个核心对象,以及 newInstance() 创建对象、field.set() 操作属性、method.invoke() 调用方法、setAccessible(true) 访问 private 这几个典型操作即可。
| 概念 | 核心问题 | 好 | 坏 |
|---|---|---|---|
| 内聚 | 一个类/模块内部,事情做得多不多、杂不杂? | 高内聚(只做一件事) | 低内聚(啥都往里塞) |
| 耦合 | 两个类/模块之间,修改一个牵连多少个? | 低耦合(改了没事) | 高耦合(改一处炸一片) |
Java Lambda
Lambda 表达式是 Java 8 引入的简化写法,主要用来简化匿名内部类。它的核心作用是:把一段代码逻辑当成参数传递。 常见场景是线程任务、集合遍历、排序、过滤等。
1. 先理解匿名内部类
匿名内部类就是:没有名字的内部类,通常用来临时实现一个接口或抽象类。
比如 Runnable 是一个接口:
public interface Runnable {
void run();
}
以前要创建线程任务,可以写一个实现类:
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行");
}
}
然后使用:
new Thread(new MyRunnable()).start();
但如果这个实现类只用一次,单独写一个类就很麻烦,所以可以用匿名内部类:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程执行");
}
}).start();
这段代码的意思是:临时创建一个没有名字的 Runnable 实现类对象,并重写 run 方法。 匿名内部类的格式大概是:
new 接口名或父类名() {
重写方法
};
它的缺点是代码太啰嗦,尤其是接口里明明只有一个方法时,还要写 new Runnable()、@Override、方法名、方法体。
2. Lambda 是什么
Lambda 就是用更短的写法实现“只有一个抽象方法的接口”。这种接口叫函数式接口。
比如 Runnable 只有一个抽象方法 run(),所以它可以用 Lambda 简化:
new Thread(() -> {
System.out.println("线程执行");
}).start();
这句等价于上面的匿名内部类。因为 Runnable 里只有一个 run() 方法,所以 Java 知道 () -> { ... } 这段逻辑就是在实现 run()。
Lambda 基本格式是:
(参数列表) -> {
方法体
}
可以简单理解为:
参数 -> 要执行的代码
3. Lambda 使用前提:函数式接口
Lambda 不是随便哪里都能写,它只能用于函数式接口。函数式接口就是:只有一个抽象方法的接口。
@FunctionalInterface
public interface Calculator {
int add(int a, int b);
}
用匿名内部类写:
Calculator c = new Calculator() {
@Override
public int add(int a, int b) {
return a + b;
}
};
用 Lambda 写:
Calculator c = (a, b) -> a + b;
这两种写法本质一样,都是在实现 add 方法。@FunctionalInterface 不是必须的,但建议写,它可以让编译器帮你检查这个接口是不是只有一个抽象方法。
4. 常见简化规则
完整写法:
Calculator c = (int a, int b) -> {
return a + b;
};
可以简化为:
Calculator c = (a, b) -> a + b;
==常见规则记住这几个就够了:参数类型通常可以省略;方法体只有一行时,可以省略 {};如果这一行是返回值,可以同时省略 return;只有一个参数时,可以省略参数的小括号;没有参数时,必须写 ()。==
Runnable r = () -> System.out.println("hello");
Consumer<String> c = s -> System.out.println(s);
Comparator<Integer> cmp = (a, b) -> a - b;
5. 常见使用场景
场景一:线程任务 匿名内部类:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程执行");
}
}).start();
Lambda:
new Thread(() -> System.out.println("线程执行")).start();
场景二:集合遍历 传统写法:
for (String s : list) {
System.out.println(s);
}
Lambda 写法:==forEach 是集合提供的一个遍历方法,意思是:把集合里的每个元素依次拿出来,然后对每个元素执行一次你传进去的操作。==
list.forEach(s -> System.out.println(s));
进一步简化为方法引用:
list.forEach(System.out::println);
场景三:集合排序 匿名内部类:
list.sort(new Comparator<Integer>() {
@Override
public int compare(Integer a, Integer b) {
return a - b;
}
});
Lambda:
list.sort((a, b) -> a - b);
对象按年龄排序:
users.sort((u1, u2) -> u1.getAge() - u2.getAge());
也可以写成:
users.sort(Comparator.comparing(User::getAge));
场景四:过滤数据 Lambda 经常和 Stream 一起用:
List<Integer> result = nums.stream()
.filter(n -> n > 10)
.collect(Collectors.toList());
这里 n -> n > 10 表示过滤条件:只保留大于 10 的元素。
6. 常见函数式接口
Java 内置了一些常用函数式接口,记几个高频的就行。
Runnable:无参数、无返回值。
Runnable r = () -> System.out.println("run");
Consumer<T>:接收一个参数,无返回值,表示“消费一个数据”。
Consumer<String> c = s -> System.out.println(s);
Supplier<T>:无参数,有返回值,表示“提供一个数据”。
Supplier<String> s = () -> "hello";
Function<T, R>:接收一个参数,返回一个结果,表示“转换”。
Function<String, Integer> f = str -> str.length();
Predicate<T>:接收一个参数,返回 boolean,表示“判断条件”。
Predicate<Integer> p = n -> n > 10;
Comparator<T>:接收两个参数,返回比较结果,常用于排序。
Comparator<Integer> c = (a, b) -> a - b;
7. 一句话总结
匿名内部类是临时创建一个没有名字的接口实现类;Lambda 是匿名内部类在函数式接口场景下的简化写法。 你先这样记:Lambda = 更短地实现只有一个抽象方法的接口,常用于线程、遍历、排序、过滤。
Java 高级数据结构笔记
Java 里最常用的数据结构主要在 java.util 包下,核心分两大类:Collection 和 Map。Collection 存单个元素,下面有 List、Set、Queue;Map 存键值对,不属于 Collection,但非常常用。Java 的集合只能直接存对象,不能直接存 int、double 这种基本类型,所以会自动装箱成 Integer、Double,这点和 C++ 的 vector<int> 不一样,性能敏感场景要注意。
import java.util.*;
List<Integer> list = new ArrayList<>();
Set<String> set = new HashSet<>();
Map<String, Integer> map = new HashMap<>();
Queue<Integer> queue = new LinkedList<>();
Deque<Integer> deque = new ArrayDeque<>();
PriorityQueue<Integer> heap = new PriorityQueue<>();
1. Java 集合和 C++ STL 对照
| Java | C++ 大致对应 | 底层数据结构 | 数据定义 & 样例 |
|---|---|---|---|
ArrayList |
vector |
连续数组 | 定义: List<String> list = new ArrayList<>(); <br>数据样子: ["apple", "banana", "cherry"] <br>按下标取 list.get(1) → "banana" |
LinkedList |
list / deque |
双向链表 | 定义: List<String> list = new LinkedList<>(); <br>数据样子: ["head", "node1", "node2", "tail"] <br>两端操作方便:addFirst()、addLast() |
HashSet |
unordered_set |
哈希表 | 定义: Set<Integer> set = new HashSet<>(); <br>数据样子: [3, 7, 1, 9](注意:无序,每次运行顺序可能不同) <br>自动去重:再添加 3 也不会重复 |
TreeSet |
set |
红黑树 | 定义: Set<Integer> set = new TreeSet<>(); <br>数据样子: [1, 3, 7, 9](自动升序排序) <br>字符串则按字典序排列 |
LinkedHashSet |
无直接常用对应 | 哈希表 + 双向链表 | 定义: Set<String> set = new LinkedHashSet<>(); <br>数据样子: ["first", "second", "third"](保持插入顺序) <br>插入时是什么顺序,遍历时就是什么顺序 |
HashMap |
unordered_map |
哈希表 | 定义: Map<String, Integer> map = new HashMap<>(); <br>数据样子: {"apple": 5, "banana": 3, "cherry": 8} <br>取值:map.get("banana") → 3 <br>Key 无序 |
TreeMap |
map |
红黑树 | 定义: Map<String, Integer> map = new TreeMap<>(); <br>数据样子: {"apple": 5, "banana": 3, "cherry": 8} <br>但 Key 按字母排序:apple → banana → cherry |
LinkedHashMap |
有序哈希表 | 哈希表 + 双向链表 | 定义: Map<String, Integer> map = new LinkedHashMap<>(); <br>数据样子: {"first": 1, "second": 2, "third": 3}(保持插入顺序) <br>遍历时按插入顺序输出 |
PriorityQueue |
priority_queue |
数组实现的二叉堆 | 定义: Queue<Integer> pq = new PriorityQueue<>(); <br>插入 [5, 1, 8, 3] 后,队头永远是最小值: <br>pq.poll() → 1,再 poll() → 3,再 poll() → 5 |
ArrayDeque |
deque |
循环数组 | 定义: Deque<String> deque = new ArrayDeque<>(); <br>作为栈(后进先出): push("A") → push("B") → pop() → "B" <br>作为队列(先进先出): offer("A") → offer("B") → poll() → "A" |
ConcurrentHashMap |
并发 unordered_map | 分段锁 + 哈希表 | 定义: Map<String, Integer> map = new ConcurrentHashMap<>(); <br>数据样子跟 HashMap 一样:{"key1": 100, "key2": 200} <br>但多线程环境下安全,多个线程同时读写不会出问题 |
2. List:有序、可重复,最常用是 ArrayList
List 的特点是元素有顺序,可以重复,可以按下标访问。项目里默认优先用 ArrayList,除非你明确需要频繁在链表中间插删,否则不要轻易用 LinkedList。因为现代 CPU 对数组连续内存更友好,ArrayList 实际性能经常比链表更好。
List<String> list = new ArrayList<>();
list.add("a"); // 尾部添加
list.add(0, "b"); // 指定位置插入
list.get(0); // 按下标访问
list.set(0, "c"); // 修改
list.remove(0); // 按下标删除
list.remove("a"); // 按元素删除
list.size(); // 长度
list.isEmpty(); // 是否为空
list.contains("a"); // 是否包含
ArrayList 底层是动态数组,默认容量不等于元素数量,元素多了会扩容。扩容时会创建新数组并复制旧元素,所以如果知道大概数量,项目里可以提前指定容量,减少扩容成本。
List<Integer> list = new ArrayList<>(10000);
ArrayList 常见复杂度:按下标访问 O(1),尾部添加均摊 O(1),中间插入/删除 O(n),查找某个元素 O(n)。面试常问它和 LinkedList 的区别:ArrayList 是数组,查询快;LinkedList 是双向链表,理论上插删快,但定位节点仍然要遍历,所以实际项目多数场景 ArrayList 更常用。
遍历时不要一边增强 for 一边删除,否则会触发 ConcurrentModificationException。正确方式是用 Iterator 删除,或者用 removeIf。
Iterator<String> it = list.iterator();
while (it.hasNext()) {
String s = it.next();
if (s.equals("bad")) {
it.remove();
}
}
list.removeIf(s -> s.equals("bad"));
3. Set:去重集合,重点是 HashSet 和 TreeSet
Set 表示不允许重复元素的集合。最常用的是 HashSet,底层本质是 HashMap,元素作为 key 存进去,value 是一个固定占位对象。判断是否重复依赖 hashCode() 和 equals(),这也是 Java 面试高频点。
Set<String> set = new HashSet<>();
set.add("a");
set.add("b");
set.contains("a");
set.remove("a");
set.size();
for (String s : set) {
System.out.println(s);
}
HashSet 无序,不能保证遍历顺序。如果需要按插入顺序遍历,用 LinkedHashSet;如果需要排序,用 TreeSet。
Set<String> ordered = new LinkedHashSet<>(); // 保持插入顺序
Set<Integer> sorted = new TreeSet<>(); // 自动排序
TreeSet 底层是红黑树,元素必须可比较。要么元素实现 Comparable,要么创建时传入 Comparator。
Set<Integer> set = new TreeSet<>((a, b) -> b - a); // 降序
但是实际写比较器时不建议直接 b - a,可能整数溢出。更稳妥写法:
Set<Integer> set = new TreeSet<>((a, b) -> Integer.compare(b, a));
面试重点:HashSet 判断两个对象是否相同,不是只看地址,而是先看 hashCode(),哈希相同后再用 equals() 精确判断。因此自定义对象放进 HashSet 或作为 HashMap 的 key 时,必须同时重写 equals() 和 hashCode()。
class User {
String id;
String name;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof User)) return false;
User user = (User) o;
return Objects.equals(id, user.id);
}
@Override
public int hashCode() {
return Objects.hash(id);
}
}
不要把会变化的字段作为 HashMap key 或 HashSet 元素的哈希依据。比如对象放进 HashSet 后,你修改了它的 id,它的哈希位置就乱了,之后可能查不到也删不掉。
4. Map:键值对,项目核心数据结构
Map<K,V> 类似 C++ 的 unordered_map / map。项目里最常用的是 HashMap,缓存、计数、索引、分组、映射关系基本都离不开它。
Map<String, Integer> map = new HashMap<>();
map.put("a", 1); // 插入或覆盖
map.get("a"); // 根据 key 获取 value
map.getOrDefault("b", 0); // 不存在时返回默认值
map.containsKey("a"); // 是否存在 key
map.remove("a"); // 删除 key
map.size();
map.isEmpty();
常用遍历方式:
for (Map.Entry<String, Integer> entry : map.entrySet()) {
String key = entry.getKey();
Integer value = entry.getValue();
}
for (String key : map.keySet()) {
Integer value = map.get(key);
}
for (Integer value : map.values()) {
System.out.println(value);
}
项目里更推荐 entrySet() 遍历,因为它一次拿到 key 和 value,避免重复 get()。
HashMap 常用高级 API 很重要,能少写很多 if:
map.putIfAbsent("a", 1); // key 不存在才放入
map.computeIfAbsent("list", k -> new ArrayList<>()); // key 不存在就创建 value
map.merge("count", 1, Integer::sum); // 计数累加
典型计数:
Map<String, Integer> count = new HashMap<>();
for (String word : words) {
count.put(word, count.getOrDefault(word, 0) + 1);
}
更高级写法:
for (String word : words) {
count.merge(word, 1, Integer::sum);
}
典型分组:
Map<String, List<User>> group = new HashMap<>();
for (User user : users) {
group.computeIfAbsent(user.city, k -> new ArrayList<>()).add(user);
}
HashMap 底层是数组 + 链表 + 红黑树。根据 key 的 hashCode() 算出数组下标,如果多个 key 落在同一个位置,就形成链表;链表太长时会树化成红黑树,降低极端冲突下的查询成本。JDK 8 之后,当链表长度超过阈值且数组容量足够时,会转红黑树,这是高频面试点。
HashMap 允许一个 null key 和多个 null value;ConcurrentHashMap 不允许 null key 和 null value,因为并发环境下 get(key) == null 无法区分“key 不存在”还是“value 本来就是 null”。
Map<String, String> map = new HashMap<>();
map.put(null, "x");
map.put("a", null);
5. HashMap、LinkedHashMap、TreeMap 的选择
HashMap 无序,查询快,项目默认首选。LinkedHashMap 在哈希表基础上维护双向链表,可以保持插入顺序,也可以保持访问顺序,常用于实现 LRU 缓存。TreeMap 底层红黑树,key 自动排序,适合范围查询、排序映射。
Map<String, Integer> hashMap = new HashMap<>();
Map<String, Integer> linkedMap = new LinkedHashMap<>();
Map<String, Integer> treeMap = new TreeMap<>();
TreeMap 常用范围 API:
TreeMap<Integer, String> tree = new TreeMap<>();
tree.put(10, "a");
tree.put(20, "b");
tree.put(30, "c");
tree.firstKey(); // 最小 key
tree.lastKey(); // 最大 key
tree.floorKey(25); // <= 25 的最大 key
tree.ceilingKey(25); // >= 25 的最小 key
tree.lowerKey(20); // < 20 的最大 key
tree.higherKey(20); // > 20 的最小 key
LinkedHashMap 实现简单 LRU:
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;
public LRUCache(int capacity) {
super(capacity, 0.75f, true); // true 表示按访问顺序
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}
这段在项目和面试都很常见。accessOrder=true 表示每次 get 或 put 后,该元素会移动到链表尾部,链表头就是最久未使用的数据。
6. Queue、Deque、PriorityQueue:队列、栈、堆
Java 里普通队列推荐用 Queue 接口,双端队列推荐用 Deque。如果只是当栈用,不推荐老的 Stack 类,推荐 ArrayDeque,因为 Stack 继承自 Vector,历史包袱重,性能和设计都不如 ArrayDeque。
Queue<Integer> queue = new LinkedList<>();
queue.offer(1); // 入队,推荐
queue.poll(); // 出队,队空返回 null
queue.peek(); // 查看队头,队空返回 null
add/remove/element 和 offer/poll/peek 类似,但失败时会抛异常。项目里更常用 offer/poll/peek,更安全。
Deque<Integer> deque = new ArrayDeque<>();
deque.offerLast(1); // 队尾入队
deque.pollFirst(); // 队头出队
deque.push(1); // 当栈用,压栈
deque.pop(); // 弹栈
deque.peek(); // 看栈顶
PriorityQueue 是优先队列,底层是堆。Java 默认是小根堆,也就是每次 poll() 拿最小值。C++ 的 priority_queue 默认是大根堆,这点正好相反。
PriorityQueue<Integer> minHeap = new PriorityQueue<>();
minHeap.offer(3);
minHeap.offer(1);
minHeap.offer(2);
System.out.println(minHeap.poll()); // 1
大根堆写法:
PriorityQueue<Integer> maxHeap =
new PriorityQueue<>((a, b) -> Integer.compare(b, a));
对象堆写法:
PriorityQueue<User> pq = new PriorityQueue<>(
Comparator.comparingInt(u -> u.age)
);
多条件排序:
PriorityQueue<User> pq = new PriorityQueue<>(
Comparator.comparingInt((User u) -> u.age)
.thenComparing(u -> u.name)
);
堆常用于 Top K、任务调度、最短路 Dijkstra、合并 K 个有序链表、定时任务。复杂度:插入 O(log n),弹出堆顶 O(log n),查看堆顶 O(1)。
7. 排序、比较器和对象排序
Java 排序主要靠 Comparable 和 Comparator。Comparable 是类自己具备比较能力,Comparator 是外部传入比较规则。项目中更常用 Comparator,灵活,不污染实体类。
List<Integer> nums = Arrays.asList(3, 1, 2);
nums.sort(Integer::compareTo);
降序:
nums.sort((a, b) -> Integer.compare(b, a));
对象排序:
users.sort(Comparator.comparingInt(User::getAge));
多字段排序:
users.sort(
Comparator.comparingInt(User::getAge)
.thenComparing(User::getName)
);
降序:
users.sort(
Comparator.comparingInt(User::getAge).reversed()
);
面试和项目都要注意:比较器必须满足自反性、传递性、一致性。不要随便写 return a - b,可能溢出;也不要写逻辑不稳定的比较器,否则 TreeMap、TreeSet、排序都可能异常。
8. Arrays、Collections:工具类快速查
Arrays 主要操作数组,Collections 主要操作集合。数组是固定长度,集合是动态容器,这点和 C++ 的原生数组、vector 类似。
int[] arr = {3, 1, 2};
Arrays.sort(arr);
Arrays.binarySearch(arr, 2);
Arrays.fill(arr, 0);
System.out.println(Arrays.toString(arr));
对象数组排序:
Integer[] arr = {3, 1, 2};
Arrays.sort(arr, (a, b) -> Integer.compare(b, a));
Collections 常用 API:
List<Integer> list = new ArrayList<>(Arrays.asList(3, 1, 2));
Collections.sort(list);
Collections.reverse(list);
Collections.shuffle(list);
Collections.max(list);
Collections.min(list);
Collections.binarySearch(list, 2);
不可变集合:
List<String> list = List.of("a", "b", "c");
Set<String> set = Set.of("a", "b");
Map<String, Integer> map = Map.of("a", 1, "b", 2);
List.of()、Set.of()、Map.of() 创建的是不可变集合,不能 add/remove/put。项目里常用于常量配置。
9. BitSet:位图,适合大量 boolean 标记
BitSet 可以理解成压缩版 boolean[],用 bit 表示状态,空间效率高。适合大量开关、去重标记、权限位、布隆过滤器底层思想、算法题中的状态压缩。
BitSet bitSet = new BitSet();
bitSet.set(10); // 第 10 位设为 true
bitSet.get(10); // 判断第 10 位
bitSet.clear(10); // 清除第 10 位
bitSet.cardinality(); // true 的数量
集合运算:
BitSet a = new BitSet();
BitSet b = new BitSet();
a.set(1);
a.set(3);
b.set(3);
b.set(4);
a.and(b); // 交集
a.or(b); // 并集
a.xor(b); // 异或
如果只是几万个布尔值,用 boolean[] 也可以;如果是百万、千万级别,BitSet 更省空间。
10. 并发数据结构:项目高级常用
普通的 ArrayList、HashMap、HashSet 都不是线程安全的。多线程同时读写可能出现数据错乱、死循环、覆盖、不可见等问题。项目里高并发场景常用 ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue。
ConcurrentHashMap 是线程安全 HashMap,高并发读写常用。JDK 8 之后主要通过 CAS + synchronized 控制桶级别并发,不是整张表加锁,所以性能比 Hashtable 和 Collections.synchronizedMap() 更好。
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("a", 1);
map.get("a");
map.putIfAbsent("a", 2);
map.computeIfAbsent("list", k -> 1);
map.merge("count", 1, Integer::sum);
高并发计数更推荐 LongAdder:
ConcurrentHashMap<String, LongAdder> counter = new ConcurrentHashMap<>();
counter.computeIfAbsent("click", k -> new LongAdder()).increment();
long count = counter.get("click").sum();
CopyOnWriteArrayList 适合读多写少,比如配置列表、监听器列表。它写入时复制一份新数组,所以写成本高;读不加锁,读性能好。
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
list.add("a");
list.get(0);
BlockingQueue 是阻塞队列,常用于生产者消费者模型、线程池任务队列、异步日志、削峰。队列满时生产者可以阻塞,队列空时消费者可以阻塞。
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
queue.put("task"); // 满了会阻塞
String task = queue.take(); // 空了会阻塞
常见阻塞队列:
| 类 | 特点 |
|---|---|
ArrayBlockingQueue |
数组有界阻塞队列 |
LinkedBlockingQueue |
链表阻塞队列,可有界可无界 |
PriorityBlockingQueue |
支持优先级的阻塞队列 |
DelayQueue |
延迟队列,定时任务常见 |
SynchronousQueue |
不存元素,直接交接任务,线程池常用 |
11. 常见项目套路
计数:用 HashMap 或 ConcurrentHashMap + LongAdder。
Map<String, Integer> count = new HashMap<>();
for (String x : list) {
count.merge(x, 1, Integer::sum);
}
分组:用 Map<K, List<V>>。
Map<String, List<User>> group = new HashMap<>();
for (User user : users) {
group.computeIfAbsent(user.getCity(), k -> new ArrayList<>()).add(user);
}
去重并保持顺序:用 LinkedHashSet。
List<String> unique = new ArrayList<>(new LinkedHashSet<>(list));
缓存:简单缓存用 HashMap,需要淘汰策略用 LinkedHashMap,高并发用 ConcurrentHashMap,复杂项目一般用 Redis / Caffeine。
Map<String, Object> cache = new ConcurrentHashMap<>();
Top K:小根堆维护 K 个最大值。
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int x : nums) {
heap.offer(x);
if (heap.size() > k) {
heap.poll();
}
}
存在性判断:用 HashSet,不要用 List.contains() 扫描。
Set<Long> ids = new HashSet<>(idList);
if (ids.contains(userId)) {
// O(1) 平均复杂度
}
按 key 排序和范围查找:用 TreeMap。
TreeMap<Integer, String> map = new TreeMap<>();
Integer floor = map.floorKey(100);
Integer ceiling = map.ceilingKey(100);
LRU:用 LinkedHashMap,生产项目更推荐成熟库 Caffeine。
Map<String, String> lru = new LRUCache<>(100);
面试高频底层问题
ArrayList 和 LinkedList 区别:ArrayList 底层动态数组,查询快,尾插快,中间插删慢;LinkedList 底层双向链表,理论插删快,但定位慢,内存占用大,缓存不友好。实际项目里 ArrayList 更常用。
HashMap 底层原理:数组 + 链表 + 红黑树。put 时先根据 key 的 hash 定位桶,如果桶为空直接放;如果桶里有节点,就比较 key 是否相同,相同覆盖,不同追加到链表或树中。链表太长并且数组容量达到要求后会树化,查询从 O(n) 优化到 O(log n)。
HashMap 为什么容量通常是 2 的幂:这样可以用 (n - 1) & hash 计算下标,比取模更快,也有利于扩容时重新分布。扩容通常是容量翻倍。
HashMap 什么时候扩容:元素数量超过 capacity * loadFactor 时扩容。默认负载因子是 0.75,是在时间和空间之间折中。
HashMap 和 Hashtable 区别:Hashtable 是老类,方法加 synchronized,线程安全但性能差,不允许 null;HashMap 非线程安全,允许 null;高并发应使用 ConcurrentHashMap。
HashMap 和 ConcurrentHashMap 区别:HashMap 不保证并发安全;ConcurrentHashMap 支持并发访问,JDK 8 后通过 CAS + 局部 synchronized 减少锁粒度。它不允许 null key/value。
HashMap 为什么要重写 equals 和 hashCode:哈希集合先用 hashCode 定位,再用 equals 判断逻辑相等。如果只重写一个,会导致相等对象无法正确查找、去重或覆盖。
TreeMap 和 HashMap 区别:HashMap 平均 O(1),无序;TreeMap 基于红黑树,操作 O(log n),key 有序,支持范围查询。
fail-fast 是什么:遍历集合时,如果集合结构被非迭代器方式修改,可能抛出 ConcurrentModificationException。它不是线程安全保证,只是尽早发现错误。
Java 数据结构选择口诀
默认需要顺序列表:ArrayList。
需要去重:HashSet。
需要去重且保持插入顺序:LinkedHashSet。
需要排序去重:TreeSet。
需要 key-value:HashMap。
需要 key 有序和范围查询:TreeMap。
需要保持插入顺序或 LRU:LinkedHashMap。
需要队列/栈/双端队列:ArrayDeque。
需要堆:PriorityQueue。
需要线程安全 Map:ConcurrentHashMap。
需要生产者消费者:BlockingQueue。
读多写少列表:CopyOnWriteArrayList。
大量 boolean 标记:BitSet。
常用 API 速查
// List
list.add(x);
list.get(i);
list.set(i, x);
list.remove(i);
list.size();
list.isEmpty();
list.contains(x);
// Set
set.add(x);
set.remove(x);
set.contains(x);
set.size();
// Map
map.put(k, v);
map.get(k);
map.getOrDefault(k, defaultValue);
map.containsKey(k);
map.remove(k);
map.putIfAbsent(k, v);
map.computeIfAbsent(k, key -> new ArrayList<>());
map.merge(k, 1, Integer::sum);
// Queue
queue.offer(x);
queue.poll();
queue.peek();
// Deque
deque.offerFirst(x);
deque.offerLast(x);
deque.pollFirst();
deque.pollLast();
deque.push(x);
deque.pop();
// PriorityQueue
pq.offer(x);
pq.poll();
pq.peek();
// Sort
list.sort(Comparator.naturalOrder());
list.sort(Comparator.reverseOrder());
list.sort(Comparator.comparingInt(User::getAge));
Arrays.sort(arr);
Collections.sort(list);
最容易踩的坑
第一,Integer、Long、String 比较内容要用 equals(),不要随便用 ==。== 比较的是引用地址,虽然小整数缓存可能让一部分结果看起来对,但不可靠。
Integer a = 1000;
Integer b = 1000;
System.out.println(a == b); // false
System.out.println(a.equals(b)); // true
第二,自定义对象放入 HashMap / HashSet,必须正确重写 equals() 和 hashCode()。否则去重、查找、覆盖都可能失效。
第三,遍历集合时不要直接修改集合结构。删除元素用 Iterator.remove() 或 removeIf()。
第四,PriorityQueue 不是整体有序,它只保证堆顶元素是当前最小或最大。遍历 PriorityQueue 得到的顺序不是排序结果。
第五,HashMap 不是线程安全的。多线程写共享 Map,直接换 ConcurrentHashMap,不要侥幸。
第六,List.of()、Set.of()、Map.of() 创建的是不可变集合,不能修改。
第七,项目中不要滥用 LinkedList。很多人以为链表插删快,但真实场景中定位节点、对象分散、缓存不友好,常常不如 ArrayList。
整体来说,Java 高级数据结构的核心不是记类名,而是知道:是否有序、是否去重、是否排序、是否线程安全、底层是数组/链表/哈希/树/堆、常用复杂度是什么、项目场景该选哪个。 这几个维度掌握后,面试和项目基本够用。