1.==equals() 有什么区别?

简答:== 比较基本类型时比较值,比较对象时比较地址。
equals() 通常比较对象内容,但前提是类重写了 equals,比如 String 就重写了。

基本类型:== 比较值,无equals

引用类型:== 和 equals() 默认都比较地址(但是很多类重写了equals,来比较内容)

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.01.00 数值一样,但小数位数不同, equals() 是 false。

集合类:ArrayList、LinkedList、HashSet、HashMap

枚举类、record 类

未重写equals()的类:

数组、 StringBuilderStringBuffer、普通自定义类

String 的特殊情况:字符串常量池

String s1 = "hello";  //字符串常量池
String s2 = "hello";  //字符串常量池
System.out.println(s1 == s2);      // true 比较地址
System.out.println(s1.equals(s2)); // true 比较内容

为什么这里 == 也是 true?

因为 "hello" 这种字面量会放到字符串常量池里。s1s2 都指向常量池里的同一个 "hello" 对象。

字面量:直接写在代码里的值,字符串字面量就是直接用双引号写出来的字符串。(字符串必须用双引号)。

字符串常量池: JVM 为了复用字符串字面量而维护的区域,相同的字符串字面量通常会共用池中的同一个对象。

编译期优化:多个字面量连续拼接,基本类型常量参与拼接,final 修饰的编译期常量参与拼接,都是在字符串常量池

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对象.

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

StringBuilder sb = new StringBuilder();
sb.append("hello");
sb.append("world");
String result = sb.toString();

总结:
少量字符串直接用 String ,循环大量拼接建议用 StringBuilderStringBuffer 线程安全,但现在普通业务中较少用。

String 拼接为什么不推荐在循环里用 +?

循环次数多了,会产生大量临时对象,性能差

String类常用方法:

排名方法用途示例
1length()获取字符串长度"abc".length() → 3
2equals(Object obj)比较内容是否相等(区分大小写)"abc".equals("abc") → true
3equalsIgnoreCase(String s)比较内容是否相等(忽略大小写)"abc".equalsIgnoreCase("ABC") → true
4substring(int begin, int end)截取子串"hello".substring(1,4) → "ell"
5split(String regex)按正则拆分成数组"a,b,c".split(",") → ["a","b","c"]
6trim() / strip()去除首尾空白" hi ".trim() → "hi"
7toLowerCase() / toUpperCase()大小写转换"ABC".toLowerCase() → "abc"
8contains(CharSequence s)判断是否包含子串"hello".contains("ell") → true
9replace(char old, char new) / replaceAll(String regex, String replacement)替换字符或字符串"hello".replace('l','w') → "hewwo"
10indexOf(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 不会自动把数组变小。

常见理解是:创建一个更大的新数组,然后把旧数组的数据复制过去。

如果你想手动释放多余空间,可以调用:

list.trimToSize();

可以把底层数组容量调整为当前 size,但是底层是复制数组。

4. HashMap 底层原理

HashMap的底层原理_hashmap底层实现原理-CSDN博客 感觉这个很好

 实现:数组 + 链表/红黑树

  1. 计算哈希:调用键的 hashCode() 计算哈希值

  2. 定位桶(n - 1) & hash 计算出数组下标

  3. 处理冲突:如果多个键映射到同一个桶,用链表或红黑树存储

  4. 扩容:当元素达到阈值(容量 × 负载因子)时,容量变为原来的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。

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

特性HashMapLinkedHashMapTreeMapHashtable
底层结构哈希表哈希表 + 双向链表红黑树哈希表
是否有序无序按插入顺序或访问顺序按键排序无序
允许 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

特性HashMapHashtableConcurrentHashMap
线程安全❌ 不安全✅ 安全(全表锁)✅ 安全(锁桶)
并发性能高(单线程)低(串行)高(多线程并发)
允许 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 保护的代码。

public class Counter {
    private int count = 0;

    public synchronized void add() {
        count++;
    }
}
//其中
public synchronized void add()
//等价于
public void add() {
    synchronized (this) {
        count++;
    }
}

synchronized修饰静态方法:锁当前类

public static synchronized void add() {
}

synchronized修饰代码块:锁的是括号里的对象:

synchronized 适合:

  • 多个线程修改共享数据;
  • 复合操作;
  • 临界区保护;
  • 保证一段代码完整执行。
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 是原子操作。

private AtomicInteger count = new AtomicInteger(0);

public void add() {
    count.incrementAndGet();
}
对比项synchronizedAtomicInteger
实现方式加锁CAS
适合场景复杂临界区简单数值原子更新
是否阻塞可能阻塞通常不阻塞,但可能自旋重试
能保护多行代码吗可以不适合
典型用途保护一段业务逻辑计数器、状态值更新

6.CAS 机制是什么?原理是什么

全称:Compare And Swap

原理:

修改变量之前,先比较当前值是不是我以为的旧值;如果是,就修改;如果不是,就失败或重试。

初始条件:内存中的当前值 V,期望值 A,准备修改的新值 B

规则:如果 V == A,说明没有别人改过,就把 V 改成 B;如果 V != A,说明已经被别人改过,就修改失败

 CAS 是由 CPU 底层原子指令支持的。也就是说,“比较”和“交换”这两个动作,在 CPU 层面被保证成一个不可分割的整体。

CAS 为什么叫无锁?

因为它不像 synchronized 一样让其他线程阻塞等待锁。

synchronized 的思路是:

我进去操作时,别人不能进来。

CAS 的思路是:

大家都可以尝试修改,但只有一个人能成功。
失败的人重新再试。

所以 CAS 又叫一种乐观锁思想

什么是乐观锁?乐观锁的想法是:我先假设没有别人改,提交时再检查有没有冲突。CAS 就是典型乐观锁。

优点:

1. 不需要阻塞线程

失败了可以重试,不一定要进入阻塞状态。

2. 性能较好

在竞争不激烈的情况下,CAS 通常比加锁更轻量。

3. 适合简单变量更新

比如:计数器、状态标记、序号生成、简单统计值

    问题:

    一:自旋开销

    CAS 失败后通常会重试。

    如果竞争非常激烈,很多线程一直失败、一直重试,就会浪费 CPU。

    这种不断重试叫:

    自旋

    所以 CAS 适合竞争不太激烈、操作比较短的场景。


    二:ABA 问题

    CAS 只检查值有没有变化。

    假设线程 A 读到值是:A   ;然后线程 B 把它改成:B  ;又改回:A

    线程 A 再来检查,发现还是 A,就以为没人改过。但实际上它已经被改过两次了。

    这就是 ABA 问题。

    解决方式之一是加版本号。

    比如:原来只比较: ;现在比较:值 + 版本号

    Java 里可以用:AtomicStampedReference 来处理这类问题。


    问题三:只能比较单个变量

    CAS 很适合更新一个变量:

    count + 1

    但如果你要同时保证多个变量一致,比如:

    余额减少
    订单创建
    库存扣减
    日志写入

    这种场景要用:事务、锁、数据库约束、分布式锁

    更多推荐