1.== 和 equals() 有什么区别?
简答:== 比较基本类型时比较值,比较对象时比较地址。
equals() 通常比较对象内容,但前提是类重写了 equals,比如 String 就重写了。
基本类型:== 比较值,无equals
引用类型:== 和 equals() 默认都比较地址(但是很多类重写了equals,来比较内容)
java
String a = new String("hello"); //堆内存
String b = new String("hello"); //堆
a==b//false ,比较地址
a.equals(b)//true,String 重写了 equals,比较内容
重写了equals()的类( 按内容或值判断相等**):**
包装类:String、Integer、Long、Double、Float、Boolean、Character、Byte、Short
日期时间类:Date、LocalDate、LocalDateTime、LocalTime、Instant
大数类:BigIntegerBigDecimal
BigDecimal 有一个经典坑:BigDecimal.equals() 不只比较数值,还比较精度。
1.0 和 1.00 数值一样,但小数位数不同, equals() 是 false。
集合类:ArrayList、LinkedList、HashSet、HashMap
枚举类、record 类
未重写equals()的类:
数组、 StringBuilder 和 StringBuffer、普通自定义类
String 的特殊情况:字符串常量池
java
String s1 = "hello"; //字符串常量池
String s2 = "hello"; //字符串常量池
System.out.println(s1 == s2); // true 比较地址
System.out.println(s1.equals(s2)); // true 比较内容
为什么这里 == 也是 true?
因为 "hello" 这种字面量会放到字符串常量池 里。s1 和 s2 都指向常量池里的同一个 "hello" 对象。
字面量:直接写在代码里的值,字符串字面量就是直接用双引号写出来的字符串。(字符串必须用双引号)。
**字符串常量池:**JVM 为了复用字符串字面量而维护的区域,相同的字符串字面量通常会共用池中的同一个对象。
编译期优化:多个字面量连续拼接,基本类型常量参与拼接,final 修饰的编译期常量参与拼接,都是在字符串常量池
java
String s1 = "hk";
String s2 = new String("hk");
System.out.println(s1 == s2); // false 比较地址
System.out.println(s1.equals(s2)); // true 比较内容
JVM 会先去字符串常量池里找有没有 "hk"。如果没有,就在字符串常量池中创建一个 "hk"。
然后让 s1 指向常量池中的 "hk"。s2 指向堆内存中创建的新String对象.
java
String s1 = "hk";
String s2 = new String("hk");
System.out.println(s1 == s2); // false 比较地址
System.out.println(s1 == s2.intern()); // true 比较地址
intern() 的原理是:查询 JVM 字符串常量池中是否存在相同内容的字符串,存在则返回池中引用,不存在则将当前字符串加入池中并返回其引用。
s2.intern() 返回的是字符串常量池中的 "hk" 引用。所以它和 s1 指向同一个对象,地址一样。
总结: == 比较地址,equals() 比较内容, String s1 = "hk" 和 String s2 = new String("hk") 内容一样,但前者通常指向常量池对象,后者指向堆中新对象,因此 s1 == s2 是 false,s1.equals(s2) 是 true。
2.String类相关:
String 为什么不可变?
简答:String 不可变是为了支持常量池共享、线程安全、安全性以及作为 HashMap key 时保持 hash 值稳定。字符串拼接时不是修改原对象,而是创建新对象,所以大量拼接应使用 StringBuilder。
字符串常量池需要安全共享、HashMap 找数据时依赖 key 的 hash 值,如果以String做key,key变化时,hash值变化,就找不到存储位置。多个线程同时读取同一个字符串不会出问题、
String、StringBuilder、StringBuffer有什么区别?
String 不可变,适合少量字符串和固定文本。
StringBuilder 可变,适合单线程下频繁拼接字符串。
StringBuffer 可变,并且方法加了锁,线程安全,但性能通常低于 StringBuilder。
java
StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.append("world");
String result = sb.toString();
总结:
少量字符串直接用 String ,循环大量拼接建议用 StringBuilder。StringBuffer 线程安全,但现在普通业务中较少用。
String 拼接为什么不推荐在循环里用 +?
循环次数多了,会产生大量临时对象,性能差
String类常用方法:
| 排名 | 方法 | 用途 | 示例 |
|---|---|---|---|
| 1 | length() |
获取字符串长度 | "abc".length() → 3 |
| 2 | equals(Object obj) |
比较内容是否相等(区分大小写) | "abc".equals("abc") → true |
| 3 | equalsIgnoreCase(String s) |
比较内容是否相等(忽略大小写) | "abc".equalsIgnoreCase("ABC") → true |
| 4 | substring(int begin, int end) |
截取子串 | "hello".substring(1,4) → "ell" |
| 5 | split(String regex) |
按正则拆分成数组 | "a,b,c".split(",") → ["a","b","c"] |
| 6 | trim() / strip() |
去除首尾空白 | " hi ".trim() → "hi" |
| 7 | toLowerCase() / toUpperCase() |
大小写转换 | "ABC".toLowerCase() → "abc" |
| 8 | contains(CharSequence s) |
判断是否包含子串 | "hello".contains("ell") → true |
| 9 | replace(char old, char new) / replaceAll(String regex, String replacement) |
替换字符或字符串 | "hello".replace('l','w') → "hewwo" |
| 10 | indexOf(String s) |
查找子串首次出现的位置 | "hello".indexOf("l") → 2 |
|---------------------------|-----------|---------------------------|
| charAt(int index) | 获取指定位置的字符 | "abc".charAt(1) → 'b' |
| isEmpty() / isBlank() | 判断是否为空或空白 | "".isEmpty() → true |
3.ArrayList 和 LinkedList
简答:ArrayList 底层是动态数组,查询和遍历快,适合大多数业务列表场景。LinkedList 底层是双向链表,理论上插入删除快,但查找慢。LinkedList 增删快的前提是已经定位到目标节点,否则它仍然需要先遍历查找。
实际开发中,中间位置的增删不一定 LinkedList 更快,大多数业务场景 ArrayList 更常用。
ArrayList 底层是动态数组 ,数据在内存中逻辑上是连续存放的,所以可以通过下标快速定位元素。
LinkedList 底层是双向链表,每个节点除了保存数据,还保存前一个节点和后一个节点的引用。
ArrayList 不是线程安全的。
ArrayList 扩容机制:
无参创建 ArrayList 时,底层数组一开始是空数组,第一次添加元素时扩容到 10。
通常:新容量 = 旧容量的 1.5 倍,涉及小则向下取整。
ArrayList 不会自动把数组变小。
常见理解是:创建一个更大的新数组,然后把旧数组的数据复制过去。
如果你想手动释放多余空间,可以调用:
java
list.trimToSize();
可以把底层数组容量调整为当前 size,但是底层是复制数组。
4. HashMap 底层原理
HashMap的底层原理_hashmap底层实现原理-CSDN博客 感觉这个很好
实现:数组 + 链表/红黑树
-
计算哈希 :调用键的
hashCode()计算哈希值 -
定位桶 :
(n - 1) & hash计算出数组下标 -
处理冲突:如果多个键映射到同一个桶,用链表或红黑树存储
-
扩容:当元素达到阈值(容量 × 负载因子)时,容量变为原来的2倍
默认容量 = 16 ;默认负载因子 = 0.75 ;threshold = 16 × 0.75 = 12
源码中的默认初始容量是 16,默认负载因子是 0.75f,树化阈值是 8,反树化阈值是 6,最小树化容量是 64
当 链表长度超过阈值(默认 8)且数组长度 ≥ 64 时,链表会自动转换为红黑树,将查询时间复杂度从链表的 O(n) 优化至 O(log n),避免了长链表导致的性能瓶颈。
HashMap 对象本身不直接存所有 key-value,它持有一个 table 数组;table 数组每个位置存的是 Node 引用;Node 里才真正保存 hash、key、value、next。
java
map.put("name", "张三");
put过程:
- 计算 key 的 hash 值;
- 根据 hash 值计算数组下标;
- 如果该位置为空,直接放进去;
- 如果该位置已经有元素,说明发生 hash 冲突;
- 冲突后,先比较 key 是否相同;
- 如果 key 相同,覆盖旧 value;
- 如果 key 不同,挂到链表或红黑树中。
get过程:
- 根据 hash 找到数组下标;
- 到对应位置查找;
- 如果只有一个元素,直接比较 key;
- 如果是链表,一个个比较,红黑树,就按树结构查找;
- 找到 key 相等的节点,返回 value。
HashMap 特点:
| 特点 | 说明 |
|---|---|
| 存储键值对 | 每个元素包含一个键(Key)和一个值(Value) |
| 键唯一 | 键不能重复,但值可以重复 |
| 快速存取 | 增删改查的时间复杂度接近 O(1) |
| 允许 null | 允许一个 null 键和多个 null 值 |
| 无序 | 不保证元素的顺序(与插入顺序无关) |
| 线程不安全 | 多线程环境下需要手动同步 |
ConcurrentHashMap = 线程安全的 HashMap,且并发性能优于 Hashtable。
HashMap对比LinkedHashMap和TreeMap和Hashtable
| 特性 | HashMap | LinkedHashMap | TreeMap | Hashtable |
|---|---|---|---|---|
| 底层结构 | 哈希表 | 哈希表 + 双向链表 | 红黑树 | 哈希表 |
| 是否有序 | 无序 | 按插入顺序或访问顺序 | 按键排序 | 无序 |
| 允许 null 键 | 允许(一个) | 允许(一个) | 不允许 | 不允许 |
| 允许 null 值 | 允许 | 允许 | 不允许 | 不允许 |
| 线程安全 | 否 | 否 | 否 | 是 |
| 性能 | 高 | 较高 | 较低 | 较低 |
常用方法
| 方法 | 说明 |
|---|---|
put(K key, V value) |
添加或更新键值对 |
get(Object key) |
根据键获取值,不存在返回 null |
remove(Object key) |
删除指定键的键值对 |
containsKey(Object key) |
判断是否包含某个键 |
containsValue(Object value) |
判断是否包含某个值 |
keySet() |
返回所有键的 Set 集合 |
values() |
返回所有值的 Collection 集合 |
entrySet() |
返回所有键值对的 Set 集合 |
size() |
返回键值对数量 |
isEmpty() |
判断是否为空 |
clear() |
清空所有元素 |
使用注意事项
1. 键的 equals() 和 hashCode() 必须正确重写
如果使用自定义对象作为键,必须同时重写 equals() 和 hashCode(),否则 HashMap 无法正确判断键是否相等。
2. 遍历时不要直接删除
java
// 错误方式
for (String key : map.keySet()) {
if (条件) map.remove(key); // 可能抛出 ConcurrentModificationException
}
// 正确方式:使用迭代器
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()) {
if (条件) it.remove();
}
3. 初始化容量建议
如果已知元素数量,建议指定初始容量,避免频繁扩容:
总结
HashMap 是 Java 中基于哈希表实现的键值对集合,以 O(1) 的时间复杂度提供快速的增删改查能力。它允许一个 null 键和多个 null 值,但不保证顺序,且线程不安全。
5.什么是ConcurrentHashMap?
ConcurrentHashMap 是 Java 中一个线程安全 的哈希表实现,属于 java.util.concurrent 包。它解决了 HashMap 在多线程环境下不安全的问题,同时比传统的 Hashtable 有更好的并发性能。
可以理解为:ConcurrentHashMap = 线程安全的 HashMap,且并发性能优于 Hashtable。
一、为什么需要 ConcurrentHashMap?
| 集合类 | 线程安全 | 并发性能 | 问题 |
|---|---|---|---|
HashMap |
❌ 不安全 | 高 | 多线程下可能死循环、数据丢失 |
Hashtable |
✅ 安全 | 低 | 全局锁,同一时间只有一个线程能操作 |
ConcurrentHashMap |
✅ 安全 | 高 | 分段/细粒度锁,多线程可并发操作 |
示例问题 :HashMap 在多线程同时 put 时,可能造成死循环 (JDK 1.7 头插法)或数据覆盖(JDK 1.8)。
核心原理(JDK 1.8 及以后)
1. 底层结构:数组 + 链表/红黑树
与 HashMap 相同,ConcurrentHashMap 也使用 数组 + 链表 + 红黑树。
2. 线程安全实现:CAS + synchronized
| 操作 | 同步方式 | 说明 |
|---|---|---|
| 初始化数组 | CAS | 保证只有一个线程初始化 |
| 写操作(put) | synchronized |
只锁住当前操作的桶(链表/树的头节点) |
| 读操作(get) | 无锁(volatile) | 读操作不加锁,性能极高 |
| 统计计数 | LongAdder 思想 |
分段计数,避免竞争 |
3. 细粒度锁(锁桶)
ConcurrentHashMap 不是锁整个表 ,而是锁住当前操作的桶 (数组的一个位置)。多个线程操作不同桶时可以并发执行,互不阻塞。
text
ConcurrentHashMap 结构(JDK 1.8)
┌───┬───┬───┬───┬───┐
│ 0 │ 1 │ 2 │ 3 │ 4 │ ... 数组
└─┬─┴─┬─┴─┬─┴─┬─┘
│ │ │ │
null 线程B 线程A null
正在put 正在put
线程C操作索引3的桶 → 与线程A、B不冲突
基本使用示例
java
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentHashMapDemo {
public static void main(String[] args) {
// 创建 ConcurrentHashMap
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 1. put() - 线程安全的添加
map.put("苹果", 5);
map.put("香蕉", 3);
// 2. get() - 读取,无锁
System.out.println(map.get("苹果")); // 5
// 3. putIfAbsent() - 仅当键不存在时才放入
map.putIfAbsent("苹果", 10); // 因为已存在,不会覆盖
System.out.println(map.get("苹果")); // 还是 5
// 4. replace() - 替换
map.replace("苹果", 10); // 只有存在时才替换
// 5. remove() - 删除
map.remove("香蕉");
// 6. 遍历(弱一致性,不抛 ConcurrentModificationException)
for (String key : map.keySet()) {
System.out.println(key + " = " + map.get(key));
}
}
}
常用方法
| 方法 | 说明 | 线程安全特点 |
|---|---|---|
put(K, V) |
添加或更新 | 锁对应桶 |
get(Object) |
获取值 | 无锁(volatile 读) |
remove(Object) |
删除 | 锁对应桶 |
putIfAbsent(K, V) |
不存在时才放入 | 原子操作 |
replace(K, V) |
替换 | 原子操作 |
compute() / computeIfAbsent() |
原子计算 | 函数式更新 |
size() / mappingCount() |
获取元素个数 | 非精确(弱一致性) |
ConcurrentHashMap vs Hashtable vs HashMap
| 特性 | HashMap | Hashtable | ConcurrentHashMap |
|---|---|---|---|
| 线程安全 | ❌ 不安全 | ✅ 安全(全表锁) | ✅ 安全(锁桶) |
| 并发性能 | 高(单线程) | 低(串行) | 高(多线程并发) |
| 允许 null 键 | 允许(一个) | 不允许 | 不允许 |
| 允许 null 值 | 允许 | 不允许 | 不允许 |
| 遍历时修改 | 快速失败 | 快速失败 | 弱一致性(不抛异常) |
| 适用场景 | 单线程 | 遗留代码 | 高并发环境 |
注意 :ConcurrentHashMap 不允许 null 键和 null 值,这是为了避免二义性(无法区分是值为 null 还是键不存在)。
六、使用注意事项
1. 复合操作需要原子性
java
// ❌ 错误:非原子操作
if (!map.containsKey(key)) {
map.put(key, value); // 可能被其他线程插入
}
// ✅ 正确:使用原子方法
map.putIfAbsent(key, value);
2. size() 是弱一致性
size() 返回的是估计值 ,不是精确值,高并发下可能不准确。如果需要精确计数,使用 LongAdder 或加锁。
3. 遍历时的弱一致性
ConcurrentHashMap 的迭代器是弱一致性 的:遍历过程中,其他线程的修改不会抛 ConcurrentModificationException,但迭代器也可能看不到最新的修改。
5. synchronized 、AtomicInteger和 volatile
synchronized:
同一时间,只允许一个线程进入被 synchronized 保护的代码。
java
public class Counter {
private int count = 0;
public synchronized void add() {
count++;
}
}
//其中
public synchronized void add()
//等价于
public void add() {
synchronized (this) {
count++;
}
}
synchronized修饰静态方法:锁当前类
java
public static synchronized void add() {
}
synchronized修饰代码块:锁的是括号里的对象:
synchronized 适合:
- 多个线程修改共享数据;
- 复合操作;
- 临界区保护;
- 保证一段代码完整执行。
java
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void add() {
count.incrementAndGet();
}
}
volatile:
保证一个线程修改变量后,其他线程能立刻看到最新值。
volatile 只适合:
- 状态标记;
- 开关变量;
- 一个线程写,多个线程读;
- 不需要复合操作的场景。
AtomicInteger
是 Java 提供的原子整数类。是基于 CAS 的无锁原子操作。它不需要你手动写 synchronized,也能保证加 1 是原子操作。
java
private AtomicInteger count = new AtomicInteger(0);
public void add() {
count.incrementAndGet();
}
| 对比项 | synchronized | AtomicInteger |
|---|---|---|
| 实现方式 | 加锁 | CAS |
| 适合场景 | 复杂临界区 | 简单数值原子更新 |
| 是否阻塞 | 可能阻塞 | 通常不阻塞,但可能自旋重试 |
| 能保护多行代码吗 | 可以 | 不适合 |
| 典型用途 | 保护一段业务逻辑 | 计数器、状态值更新 |
6.CAS 机制是什么?原理是什么
全称:Compare And Swap
原理:
修改变量之前,先比较当前值是不是我以为的旧值;如果是,就修改;如果不是,就失败或重试。
初始条件:内存中的当前值 V,期望值 A,准备修改的新值 B
规则:如果 V == A,说明没有别人改过,就把 V 改成 B;如果 V != A,说明已经被别人改过,就修改失败
CAS 是由 CPU 底层原子指令支持的。也就是说,"比较"和"交换"这两个动作,在 CPU 层面被保证成一个不可分割的整体。
CAS 为什么叫无锁?
因为它不像 synchronized 一样让其他线程阻塞等待锁。
synchronized 的思路是:
我进去操作时,别人不能进来。
CAS 的思路是:
大家都可以尝试修改,但只有一个人能成功。
失败的人重新再试。
所以 CAS 又叫一种乐观锁思想。
什么是乐观锁?乐观锁的想法是:我先假设没有别人改,提交时再检查有没有冲突。CAS 就是典型乐观锁。
优点:
- 不需要阻塞线程
失败了可以重试,不一定要进入阻塞状态。
- 性能较好
在竞争不激烈的情况下,CAS 通常比加锁更轻量。
- 适合简单变量更新
比如:计数器、状态标记、序号生成、简单统计值
问题:
一:自旋开销
CAS 失败后通常会重试。
如果竞争非常激烈,很多线程一直失败、一直重试,就会浪费 CPU。
这种不断重试叫:
自旋
所以 CAS 适合竞争不太激烈、操作比较短的场景。
二:ABA 问题
CAS 只检查值有没有变化。
假设线程 A 读到值是:A ;然后线程 B 把它改成:B ;又改回:A
线程 A 再来检查,发现还是 A,就以为没人改过。但实际上它已经被改过两次了。
这就是 ABA 问题。
解决方式之一是加版本号。
比如:原来只比较:值 ;现在比较:值 + 版本号
Java 里可以用:AtomicStampedReference 来处理这类问题。
问题三:只能比较单个变量
CAS 很适合更新一个变量:
count + 1
但如果你要同时保证多个变量一致,比如:
余额减少
订单创建
库存扣减
日志写入
这种场景要用:事务、锁、数据库约束、分布式锁