java基础面试题目

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

集合类:ArrayList、LinkedList、HashSet、HashMap

枚举类、record 类

未重写equals()的类:

数组、 StringBuilderStringBuffer、普通自定义类

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

java 复制代码
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 修饰的编译期常量参与拼接,都是在字符串常量池

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 ,循环大量拼接建议用 StringBuilderStringBuffer 线程安全,但现在普通业务中较少用。

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博客 感觉这个很好

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

  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。

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 就是典型乐观锁。

优点:

  1. 不需要阻塞线程

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

  1. 性能较好

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

  1. 适合简单变量更新

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

问题:

一:自旋开销

CAS 失败后通常会重试。

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

这种不断重试叫:

复制代码
自旋

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


二:ABA 问题

CAS 只检查值有没有变化。

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

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

这就是 ABA 问题。

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

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

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


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

CAS 很适合更新一个变量:

复制代码
count + 1

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

复制代码
余额减少
订单创建
库存扣减
日志写入

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

相关推荐
小江的记录本3 小时前
【Java并发编程】锁机制:volatile:JMM内存模型、可见性/禁止指令重排、内存屏障、单例模式中的应用(附《思维导图》+《面试高频考点清单》)
java·后端·python·mysql·单例模式·面试·职场和发展
暗不需求4 小时前
玩转 React Hooks:从基础到实战,逐行解析带你彻底掌握
前端·react.js·面试
_日拱一卒4 小时前
LeetCode:105从前序与中序遍历序列构造二叉树
算法·leetcode·职场和发展
天真小巫4 小时前
六年之约-2026.5.22
职场和发展
AI人工智能+电脑小能手5 小时前
【大白话说Java面试题 第68题】【JVM篇】第28题:对于 JDK 自带的监控和性能分析工具用过哪些?一般你怎么用的?
java·开发语言·jvm·面试
huaCodeA5 小时前
Android面试-Kotlin作用域函数
android·面试·kotlin
programhelp_5 小时前
Roblox Coding OA 面经分享|题量不小,但整体更偏工程思维
人工智能·算法·面试
JAVA社区5 小时前
Java进阶全套教程(一)—— 数据框架Mybatis详解
java·开发语言·面试·职场和发展·mybatis
王璐WL5 小时前
【C++进阶】多态,坑很多,面试常考!!!
c++·面试