文章目录

String、StringBuffer、StringBuilder的区别

背景

它们都是操作字符串的

是什么

String StringBuffer StringBuilder
可变性 不可变 可变 可变
线程安全 安全(因为不可变) 安全(方法用 synchronized 修饰) 不安全
适用环境 字符串不常变化 多线程下字符串拼接 单线程下字符串拼接

为什么

为什么 String 不可变?

字符数组 final,每次修改都会新建对象

为什么 StringBuffer 是线程安全的?

它的 append()insert() 等方法都用 synchronized 修饰,保证多线程下操作同一对象时数据不混乱。

为什么 StringBuilder 线程不安全却更好?

StringBuffer / StringBuilder:内部是一个可扩展的字符数组,修改时直接操作该数组,避免频繁创建新对象。

接口和抽象类的区别

背景

在面向对象设计中,我们需要两种层次的抽象:

  • 定义规范(协议):只声明“能做什么”,不关心“怎么做”。

  • 提供共同部分的实现(模板):抽取子类的公共代码,同时强制子类完成某些细节。

    Java 用接口实现前者,用抽象类实现后者。

是什么

抽象类 接口
定义 定义的是什么, 是动物, 还是交通工具 定义的是能力和行为, 比如可飞行, 可奔跑
本质 本质是一个类, 可以有成员变量, 构造方法, 具体方法和抽象方法 极简的抽象类, 通常只包含抽象方法, 接口只声明方法, 不包含实例变量
继承 抽象类只支持单继承, 一个类只能继承一个抽象类 接口支持多继承, 一个类可以实现多个接口
使用场景 当要多个相关类提供基类, 用抽象类 当需要用跨不同的类的行为时, 用接口

Java常见的异常类有哪些

背景

Java的异常都是继承 Throwable

是什么

运行时异常 编译时的异常
不强制处理 强制处理(catch,throws)
代码逻辑错误(空指针, 数组越界等), 就是程序员的bug 外部问题(IO, 数据库等), 这部分异常编译器要求必须处置

为什么

设计理念

运行时异常的原因是代码逻辑错误, 所以应该修复bug, 而不是到处catch

受检异常, 是外部问题, 程序员无法避免, 所以编译器要求强制考虑这种情况, 要么处理(catch), 要么上报(throws)

说一说Java面向对象三大特性

封装 继承 多态
背景(核心目的) 保护数据 复用代码 灵活拓展
是什么 将对象的**属性(数据)行为(方法)**捆绑在一起,并隐藏内部实现细节,只对外暴露必要的访问接口(如 getter/setter 子类通过 extends 复用父类的非私有属性和方法,并可以扩展或重写父类行为 父类引用可以指向子类对象,调用方法时会执行子类的实现。
总结 把该藏的藏起来,该露的露出来 继承父类的能力 一个接口,多种实现

说一说你对Java多态的理解

多态是指同一个行为具有多种表现形式,即父类引用可以指向子类对象,调用方法时会执行子类的实现。

简单理解:一个接口(或父类),多种实现

// 多态的体现:父类引用指向子类对象
        Animal a1 = new Dog();  // 编译时类型是Animal,运行时类型是Dog
        Animal a2 = new Cat();  // 编译时类型是Animal,运行时类型是Cat
        
        a1.speak(); // 输出"汪汪汪"(实际调用Dog的方法)
        a2.speak(); // 输出"喵喵喵"(实际调用Cat的方法)

Java重写和重载的区别

方法重载就是编译时多态

在编译时就确定调用哪个方法, 根据参数类型,个数, 顺序决定

class Calculator {
    // 方法重载:方法名相同,参数不同
    public int add(int a, int b) {
        return a + b;
    }
    
    public double add(double a, double b) {
        return a + b;
    }
    
    public int add(int a, int b, int c) {
        return a + b + c;
    }
}

编译时就知道调用哪个方法(看参数类型和数量)

方法重写就是运行时多态

运行时才确定调用哪个方法, 根据对象的实际类型决定

Animal animal;  // 编译时类型是Animal
// 运行时才确定调用哪个类的speak()
animal = new Dog();
animal.speak();  // 输出"汪汪汪"

animal = new Cat();
animal.speak();  // 输出"喵喵喵"

final关键字有什么作用

背景
在编程中,有些数据, 类, 方法一旦确定就不希望被修改。Java 提供 final 关键字来实现这些“不可变”约束。

是什么

修饰目标 作用 示例
不能被继承 final class String {}
方法 不能被重写(override) public final void method() {}
变量 值不能被修改(常量) final int MAX_SIZE = 100;

为什么
修饰类:保证类的核心逻辑不被子类篡改,如 String 类,保证字符串的安全性和不可变性
修饰方法:防止子类重写关键方法,确保行为一致
修饰变量:固定值,提升代码可读性,防止误修改,配合 static 可定义全局常量

== 和equals的区别

背景
“相等”有两个维度:是否是同一个对象(内存地址相同),还是逻辑上内容相同。

是什么

== equals()
比较的是内存地址 比较的是内容
基本类型:比较数值是否相等
引用类型:比较两个引用是否指向同一个对象
是 Object 类中的方法,默认行为等同于 ==(比较地址)
需要子类重写才有意义,例如 String、Integer 等已重写,用于比较内容是否相等
使用场景 判断两个引用是否指向同一个对象时 当需要判断两个对象逻辑上相等时

为什么
为什么重写 equals() 必须重写 hashCode():因为 HashMap、HashSet 等集合先通过 hashCode() 定位存储位置,再用 equals() 判断是否相等。如果两者不一致,会导致集合中元素重复或无法找到

Java的集合类有哪些,那些是线程安全的,那些是线程不安全的?

一、背景

Java 集合框架在设计时,为了性能考虑,大部分实现都是线程不安全的。但实际开发中既有单线程也有多线程场景,因此需要区分哪些是线程安全的,哪些不是。

二、是什么

线程安全的集合(位于 java.util.concurrent 包):

  • ListVector(古老,全表锁)、CopyOnWriteArrayList(读多写少场景)

  • SetCopyOnWriteArraySetConcurrentSkipListSet

  • MapHashtable(古老)、ConcurrentHashMap(推荐)

  • QueueConcurrentLinkedQueueArrayBlockingQueueLinkedBlockingQueue

    线程不安全的集合(位于 java.util 包):

  • ListArrayListLinkedList

  • SetHashSetLinkedHashSetTreeSet

  • MapHashMapLinkedHashMapTreeMap

三、为什么

为什么大部分集合线程不安全?

  • 性能考虑:同步会带来性能开销,单线程场景不需要
  • 设计选择:把选择权交给开发者,按需选择同步方案

ArrayList 和 Array 有什么区别?

一、ArrayList 和 Array 的区别

底层

  • Array:一块连续的内存空间,每个元素占固定大小,通过偏移量直接访问
  • ArrayList:基于数组实现, 有自动扩容机制

是什么

对比项 Array ArrayList
特点 固定,不可变 动态,可自动扩容
使用 只有length属性 提供 add(), remove() 等丰富方法
场景 性能更高,适合长度固定、性能敏感场景 使用更灵活,适合需要动态增删的场景

ArrayList 和 LinkedList 的区别是什么?

背景

两者都是 List 的实现,但底层数据结构不同,性能表现各异。

是什么

对比项 ArrayList LinkedList
底层结构 动态数组 双向链表
随机访问 O(1),快 O(n),慢
中间插入/删除 O(n),慢(需移动元素) O(1),快(只改指针)
内存占用 连续内存,较省 每个节点多存前后指针,较耗
使用场景 查询多时 中间的增删多
  • 为什么实际开发 ArrayList 用得多:查询场景远多于增删场景,且数组对 CPU 缓存友好,综合性能更好

    使用建议

  • 查询多 → ArrayList

  • 中间增删多 → LinkedList

  • 大多数情况直接选 ArrayList

ArrayList 扩容机制

背景

ArrayList 底层基于数组实现,但数组长度固定。为了支持动态添加元素,需要自动扩容机制。

是什么

1. 初始容量

  • JDK 1.8 后:无参构造初始容量为 0(懒加载)
  • 第一次 add() 时扩容到 10
  • 也可通过构造器指定初始容量

2. 扩容规则

int newCapacity = oldCapacity + (oldCapacity >> 1);  // 1.5 倍
  • 每次扩容为原容量的 1.5 倍
  • 10 → 15 → 22 → 33 → 49 …

3. 扩容过程

  • 计算新容量(1.5 倍)
  • 创建新数组
  • Arrays.copyOf() 复制原数组元素
  • 原数组被 GC 回收

为什么

1. 为什么是 1.5 倍?

  • 不是 2 倍:避免内存浪费
  • 不是 1.1 倍:避免频繁扩容影响性能
  • 在时间和空间之间取得平衡

2. 为什么采用懒加载(初始容量 0)?

  • 节省内存:很多 ArrayList 实际使用元素很少
  • 避免提前分配不必要的内存

3. 性能建议

  • 能预知元素数量时,指定初始容量
  • 减少扩容次数,提升性能
// 不推荐:可能多次扩容
ArrayList<String> list = new ArrayList<>();

// 推荐:指定容量,避免扩容
ArrayList<String> list = new ArrayList<>(10000);

一句话总结

ArrayList 在容量不足时,会创建一个 1.5 倍大小的新数组并复制元素;能预知数据量时建议指定初始容量以减少扩容开销。

介绍一下HashMap

一、背景

需要存储键值对并快速查找,数组和链表都无法同时满足,HashMap 结合两者优点,提供 O(1) 的平均查询速度。

二、是什么

底层结构

数组 + 链表 + 红黑树

方法

put, 就是插入键值对, 平均时间复杂度:O(1)

扩容机制, 当元素个数大于阈值时, 就会出发扩容, 新容量 = 旧容量 × 2, 所有元素重新分配

get, 根据键获取值, 平均时间复杂度:O(1)

注意事项

  1. 线程不安全
  2. 键对象必须重写 hashCode()equals()

三、为什么

1 为什么引入红黑树?

  • 解决链表过长导致查询退化为 O(n) 的问题
  • 红黑树查询 O(log n),性能更好
  • 阈值 8 基于泊松分布,概率极低

2. 重写 equals 为什么要重写 hashCode?

  • HashMap 先用 hashCode 定位,再用 equals 判断
  • 只重写 equals 会导致相同对象 hash 不同,存到不同位置,逻辑错误

HashMap的底层结构

数组每个元素是一个桶, 桶中有三种情况,

没有元素(null), 有元素(链表或者红黑树)

  • 链表长度 > 8 且数组长度 ≥ 64 时,链表转红黑树
  • 红黑树节点 < 6 时,退化为链表

Put的流程

  1. 计算 hash:(key.hashCode()) ^ (h >>> 16)
  2. 计算索引:(n - 1) & hash
  3. 该位置为空 → 直接插入
  4. 不为空 → 判断 key 是否相等,相等则覆盖
  5. 是红黑树 → 红黑树插入
  6. 是链表 → 遍历链表,尾插法插入,插入后判断是否转红黑树
  7. 元素个数 > 阈值 → 扩容

Get的流程

  1. 计算 hash 和索引
  2. 找到位置后,先判断第一个节点
  3. 不匹配则按链表或红黑树查找

扩容的流程

  • 触发:元素个数 > 阈值
  • 新容量 = 旧容量 × 2
  • JDK 1.8 优化:元素要么在原位置,要么在原位置 + 旧容量
  • 无需重新计算 hash,效率更高

1. 为什么容量是 2 的幂?

HashMap 容量为 2 的幂,是为了用位运算 (n-1) & hash 替代取模提升性能,同时扩容时只需位运算判断新增位,无需重新计算 hash,大幅提升扩容效率。

  • 扩容时容量翻倍,元素的新位置只需判断 (hash & oldCap) == 0
    • 为 0 → 位置不变
    • 不为 0 → 原位置 + 旧容量
  • 不需要重新计算 hash 值,只需一次位运算,效率极高

2. 为什么加载因子是 0.75?

  • 时间和空间的平衡
  • 太大(1.0):冲突多,查询慢
  • 太小(0.5):频繁扩容,浪费空间

解决Hash冲突的方法有哪些?HashMap 是如何解决 hash 冲突的

解决哈希冲突的方法主要有以下两种:

  1. 链地址法:在数组的每个位置维护一个链表。当发生冲突时,新的元素会被添加到链表的尾部。
  2. 开放寻址法:当发生冲突时,根据某种探测算法在哈希表中寻找下一个空闲位置来存储元素。

Java中的 HashMap使用链地址法解决hash冲突。

为什么?

  1. 对负载因子更宽容
  • 开放寻址法:负载因子 > 0.7 时性能暴跌(探测越来越长)
  • 链地址法:负载因子可以到 0.75 甚至更高,性能下降平缓
  1. 删除操作简单
  • 开放寻址法:删除要留“墓碑标记”,否则查找会断,久了堆积垃圾
  • 链地址法:直接从链表删除,干净利落
  1. 不怕冲突聚集
  • 开放寻址法:容易产生“扎堆”现象(一个冲突带出一串冲突)
  • 链地址法:冲突的节点各自挂在链表里,互不干扰

HashMap 为什么是线程不安全的? 如何实现线程安全

背景
设计初衷是为了追求单线程下的极致性能,因此没有加同步机制。

是什么
HashMap 的所有方法都没有加锁,也没有使用 CAS 等并发控制手段。

为什么
实现线程安全推荐使用 ConcurrentHashMap,它在 JDK 1.8 中采用 CAS + synchronized 锁桶的方式

ConcurrentHashMap 如何保证线程安全

线程安全主要通过 CAS 和 synchronized 来保证

具体来说有三点

  1. 当往空桶里放数据时,用 CAS 无锁插入,效率高

  2. 当桶不为空(发生哈希冲突)时,用 synchronized 锁住这个桶的头节点,然后操作链表或红黑树

  3. 读操作完全不加锁,因为 Node 的 value 和 next 是 volatile 的

不同的桶可以同时读写,只有操作同一个桶的写线程才需要竞争锁,所以性能非常好。

HashMap和ConcurrentHashMap的区别

HashSet 和 HashMap 的区别

HashMap 和 HashTable 的区别

Java 创建线程有哪几种方式?

1. 继承 Thread 类

// 最简单:继承 + 重写run
Thread t = new Thread() {
    public void run() {
        System.out.println("线程执行");
    }
};
t.start();

2. 实现 Runnable 接口

// 最简单:Runnable + Thread
Thread t = new Thread(new Runnable() {
    public void run() {
        System.out.println("线程执行");
    }
});
t.start();

3. Lambda 表达式

// 最简单:Lambda一行搞定
Thread t = new Thread(() -> System.out.println("线程执行"));
t.start();

线程start和run的区别

start() 会创建新线程并执行,run() 只是普通方法调用,不会创建新线程。

你知道Java中有哪些锁吗

说一说你对 synchronized 的理解

是什么

synchronized 是 Java 内置的锁关键字,保证同一时刻只有一个线程能执行被它修饰的代码。

怎么用

可以锁方法,也可以锁代码块。锁实例方法锁的是 this,锁静态方法锁的是 Class 对象。

特点

它是可重入的,同一个线程可以多次拿到同一个锁,不会死锁。

底层原理我了解不深,JDK 1.6 之后有锁升级优化

synchronized和lock的区别是什么

synchronized和ReentrantLock的区别是什么

volatile 关键字的作用有那些?

  1. 保证可见性, 一个线程修改了变量,其他线程立刻能看到最新值,不会从本地缓存读旧值。
  2. 禁止指令重排序, 防止编译器优化, 保证代码按顺序执行

volatile 与synchronized 的对比

写一下单例模式

1. 饿汉式(最简单、最安全)

/**
 * 饿汉式单例
 * 优点:线程安全,实现简单
 * 缺点:可能造成资源浪费(不管用不用都创建)
 */
class Singleton {
    private static final Singleton instance = new Singleton();
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        return instance;
    }
}

2. 懒汉式 + 双重检查锁(最经典)

/**
 * 懒汉式 + 双重检查锁(DCL)
 * 优点:延迟加载,线程安全,性能好
 * 关键点:volatile 禁止指令重排序
 */
class Singleton {
    // volatile 防止指令重排序,保证线程安全
    private static volatile Singleton instance = null;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        // 第一次检查:避免不必要的加锁
        if (instance == null) {
            // 加锁保证线程安全
            synchronized (Singleton.class) {
                // 第二次检查:防止重复创建
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

JDK8有哪些新特性

为什么要有线程池?

说一说线程池有哪些常用参数

BIO、NIO、AIO 的区别

Java 内存区域有哪些部分

介绍一下什么是强引用、软引用、弱引用、虚引用

有哪些垃圾回收算法

有哪些垃圾回收器

类加载机制介绍一下

介绍一下双亲委派机制

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐