Java面试常问题

5 阅读15分钟

1.==和equals的区别

==比较基本数据类型(int、double、char)时,比较的是值是否相等。比较引用数据类型(对象、数组)时,比较的是内存地址是否相同

int a = 10; 
int b = 10;
System.out.println(a == b); 
// true(值相等) String s1 = new String("abc");
String s2 = new String("abc"); 
System.out.println(s1 == s2); // false(两个不同对象,地址不同)

equals()方法

来自Object类,默认实现和==完全一样,也比较地址:

// Object 里的 equals 源码 
public boolean equals(Object obj) { 
return (this == obj); 
}

常被重写,更改比较内容:String、Integer、Double等包装类、集合类(List、Set)等

String s1 = new String("abc");
String s2 = new String("abc"); System.out.println(s1.equals(s2)); // true(内容相同)
场景==equals()
基本类型比较值不能用(不是方法)
对象(默认)比较地址比较地址(同 ==
String / 包装类比较地址比较内容
自定义对象比地址比地址,除非你自己重写

总结

  • 比较基本类型:只用==
  • 比较字符串/对象内容:只用equals
  • ==看是不是同一个东西;equals看内容像不像

2.String、StringBuilder、StringBuffer的区别

可变性

  • String不可变:底层final char[],每次修改都会新建字符串对象,旧对象变成垃圾
  • StringBuilder可变:底层普通数组,直接在原对象上修改,不新建对象。
  • StringBuffer可变:和StringBuilder一样,底层也是可变数组

线程安全

  • String:不可变→天然线程安全
  • StringBuilder:非线程安全,没有同步锁
  • StringBuffer:线程安全,方法带synchronized同步锁

执行效率

StringBuilder>StringBuffer>String

  • String每次拼接都新建对象,最慢
  • StringBuilder无锁,最快
  • StringBuffer有锁,稍慢

使用场景

  • 少量字符串操作、简单复制→用String
  • 单线程下大量拼接、循环拼接→用StringBuilder(最常用)
  • 多线程环境下操作字符串→用StringBuffer

总结

  • String不可变,StringBuilder、StringBuffer可变
  • StringBuilder非线程安全、最快;StringBuffer线程安全、稍慢
  • 日常拼接优先StringBuilder

3.接口和抽象类的区别

本质不同

  • 抽象类

是类,体现is-a(是什么)的关系。对子类进行归类、模板定义

  • 接口

是规范/契约,体现can-do(能做什么)的关系。之定义行为,不关心实现类是谁。

继承/实现限制

  • 抽象类:只能单继承(extends 一个)
  • 接口:可以多实现(implements多个)

成员变量

  • 抽象类:可以有任意变量(实例变量、静态变量、常量)
  • 接口:默认就是public static final常量,不能有普通成员变量

方法

  • 抽象类

可以有:抽象方法、普通方法、构造方法、静态方法

  • 接口

可以有:抽象方法、默认方法(default)、静态方法,不能有构造方法

访问修饰符

  • 抽象类:方法可以用public/protected/default/private
  • 接口:方法默认public,不能有其他修饰符

设计思想

  • 抽象类:为一组相似类提供通用模板
  • 接口:为不同类提供同一行为标准,解耦

总结

  1. 抽象类单继承,接口多实现
  2. 抽象类可有成员变量,接口只能是常量
  3. 抽象类能写构造方法,接口不能
  4. 抽象类是模板,接口是规范
  5. 优先用接口,需要共用代码再用抽象类

4.重载和重写的区别

  • 重载:同一个类里,方法名相同,参数不同
  • 重写:子类里,重写父类同名、同参的方法

重载(Overload)

同一个类中

  • 方法名相同
  • 参数列表必须不同(个数、类型、顺序)
  • 返回值、修饰符无所谓
  • 编译期确定(静态绑定)
public class Test { 
public void add(int a) { } 
public void add(int a, int b) { } // 重载 
public void add(double a) { } // 重载 }

重写(Override)

子类继承父类

  • 方法名、参数列表完全相同
  • 返回值类型要兼容(基本类型必须相同)
  • 访问权限不能更严格
  • 不能抛出更宽泛的异常
  • 运行期确定(动态绑定)
class Father { 
public void say() {} 
} 
class Son extends Father {
@Override 
public void say() {} // 重写 
}
对比点重载 Overload重写 Override
位置同一个类中子类继承父类
方法名相同相同
参数列表必须不同必须完全相同
返回值无关紧要基本类型必须相同
访问权限随便子类不能更严格
绑定时机编译期(静态)运行期(动态)
注解建议加 @Override

总结

  • 重载:同名不同参,同类里相亲
  • 重写:同名又同参,子类改父类

5.final关键字

可用于修饰类、方法、变量、核心作用是不可改变

1.修饰类:最终类,不可继承

  • 含义:这个类不能被继承,不能有子类。
  • 目的:安全性(防止被篡改)、效率(编译器优化)。
  • 典型例子:java.lang.String、java.lang.Math
// 最终类 
public final class MyString { 
// 内容... 
} 
// 编译报错!无法继承最终类 
// public class AdvancedString extends MyString { }

2.修饰方法:最终方法,不可重写

  • 含义:这个方法不能被子类重写(Override)。
  • 目的:锁定逻辑,避免子类破坏父类的契约;同时也能提高一点效率。
  • 典型例子:Thread类的start()方法。
class Father { 
// 最终方法 
public final void sayHello() { System.out.println("Hello"); } 
} 
class Son extends Father {
// 编译报错!无法重写最终方法 
// @Override 
// public void sayHello() { } }

3.修饰变量:最终变量,不可修改

这是最复杂的

A.修饰基本数据类型

  • 含义:数值一旦初始化,就不能再改变(相当于常量)。
  • 注意:必须在声明时构造方法中初始化。
public class Test {
// 声明时初始化 
public final int MAX_NUM = 100; 
public void method() { 
// 编译报错!无法赋值 
// MAX_NUM = 200; 
} 
}

B.修饰引用数据类型

  • 含义:引用(地址)一旦初始化,就不能再指向其他对象
  • 重点:对象内部的内容是可以修改的
public class Test { 
// 最终引用 
public final List<String> list = new ArrayList<>(); 
public void method() { 
// 可以!修改对象内部元素 
list.add("Java"); 
// 编译报错!无法改变引用指向 
// list = new ArrayList<>(); 
} 
}

C.修饰局部变量

  • 含义:只能赋值一次
public void method() { 
final int num = 10; 
/ 编译报错! 
// num = 20;
}

总结

  1. 修饰类:不可继承,保证安全性。
  2. 修饰方法:不可重写,锁定逻辑。
  3. 修饰变量:引用不可变,内容可变(引用类型);值不可变(基本类型)。

6.异常体系:Error和Exception,运行时异常vs编译异常

1.Java异常整体结构

所有异常都继承自Throwable

Throwable 
├─ Error(错误) 
└─ Exception(异常) 
    ├─ 运行时异常 RuntimeException(非受检) 
    └─ 编译时异常(受检异常)

2.Error和Exception的区别

Error(错误)

  • JVM层面的严重问题,程序无法处理,无法恢复
  • 比如:
    • StatckOverFlowError栈溢出
    • OutOfMemoryErrorOOM内存溢出
    • NoClassDefFoundError类找不到
  • 特点:
    • 程序不用捕获,捕获了也没用
    • 一旦出现,JVM一般会直接终止线程
  • Error是JVM崩了,不是代码逻辑问题

Exception(异常)

  • 程序逻辑层面的问题,可以捕获、可以处理
  • 分为两大类:运行时异常和编译时异常

3.运行时异常VS编译时异常

运行时异常(RuntimeException)

  • 继承自RuntimeException
  • 编译不检查,运行时才引爆
  • 常见:
    • NullPointerException空指针
  • IndexOutofBoundsException下标越界
    • IllegalArgumentException参数非法
    • ClassCastException类型转换异常
  • 特点:
    • 代码不写try-catch也能编译通过
    • 一般是代码逻辑写错导致
  • 运行时异常=代码Bug

编译时异常(受检异常Checked Exception)

  • 不是RuntimeException的其他Exception
  • 编译器强制要求处理(try-catch或trows)
  • 常见:
    • IOException文件IO异常
    • SQLException数据库异常
    • FileNotFoundException文件未找到
  • 特点:
    • 不捕获、不抛出,直接编译报错
    • 一般是外部环境问题(文件不存在、网络断了)
  • 编译异常=外部可能出问题,java强制你提前处理

总结

  1. Throwable是顶级父类,分为Error和Exception。
  2. Error是JVM严重错误,程序无法处理,不用捕获。
  3. Exception是程序异常,可以捕获处理。
  4. Exception分两种:
    • 运行时异常(RuntimeException):编译不检查,代码逻辑问题
    • 编译时异常(Checked Exception):编译强制检查,外部环境问题。
  1. 运行时异常不用捕获,编译异常必须捕获或抛出

7.集合

  • ArrayList底层是动态数组
  • LinkedList底层是双向链表

底层结构

  • ArrayList
    • transient Object[] elementData;
    • 连续内存空间,动态扩容
  • LinkedList
    • 双向链表,每个节点存prev、item、next
    • 内存不连续

查询访问(get/set)

  • ArrayList:快,支持随机访问,通过下表直接定位O(1)
  • Linked List:慢,必须从头/尾遍历查找O(n)

增删效率

  • ArrayLIst
    • 尾部增删快O(1)
    • 中间/头部增删慢,需要移动元素O(n)
  • LinkedList
  • -头尾增删极快O(1)
    • 已知节点时增删快
  • -先查找在增删则慢

内存占用

  • ArrayList:内存紧凑,浪费少(只数组扩容时预留空间)
  • LinkedList:每个节点多存prev/next指针,内存开销更大

线程安全

二者都非线程安全

  • CopyOnWriteArrayList
  • 或自己加锁

使用场景

  • 大量查询、遍历、读多改少→ArrayLIst(最常用)
  • 频繁头尾插入/删除、改多读少→LinkedList

总结

  1. ArrayList数组,LinkedList双向链表
  2. ArrayList查找快,中间增删慢
  3. LinkedList增删快,查找慢
  4. 日常开发优先ArrayList

8.HashMap底层原理

底层数据结构

Java8:数组+链表+红黑树

  • 主体是Entry数组(哈希桶数组)
  • 哈希冲突时用链表解决
  • 链表长度>=8且数组长度>=64时,转为红黑树
  • 红黑树节点<=6时,退化为链表

存储过程(put流程)

  1. 对key调用hashCode()计算哈希值
  2. 对哈希值做扰动处理,再与数组长度-1做位运算,得到数组下标
  3. 若改位置为空,直接放入节点
  4. 若已存在元素(哈希冲突):
    • 是链表:遍历比较equals(),相同则覆盖,不同则尾插
    • 是红黑树:按树结构插入
  1. 插入后判断是否需要扩容(负载因子0.75)

核心参数

  • 默认初始容量:16(必须是2的n次方)
  • 负载因子:0.75
  • 扩容阈值:容量X负载因子
  • 扩容击值:达到阈值后,扩容为原来的2倍,重新哈希

为什么容量必须是2的n次方

  • 下表计算hash&(length -1)等价于取模,但位运算更快
  • 保证散列更均匀,减少哈希冲突

哈希冲突怎么解决

  • 链地址法:同一下标位置用链表串起来
  • Java8改为尾插法(避免多线程下链表成环)
  • 过长链表转为红黑树,把查询从O(n)降到O(log n)

为什么要用红黑树

  • 链表太长时,查询效率太低
  • 红黑树时平衡二叉树,查询稳定高效
  • 防止恶意构造哈希冲突导致的拒绝服务攻击

JDK7与JDK8的区别

  • JDK7:数组+ 链表,头插法,多线程可能死循环
  • JDK8:数组+链表+红黑树,尾插法,更安全、效率更高

线程安全问题

  • HaskMap是非线程安全
  • 多线程:
    • ConcurrentHashMap(推荐)
    • 或Collection.synchronizedMap()

总结

HashMap地城市数组+链表+红黑树,通过key哈希值定位下表,冲突用链表解决,链表过长转红黑树,默认容量16,负载因子0.75,达到阈值扩容2倍;非线程安全,JDK8优化了尾插与红黑树

9.HashMap 为什么线程不安全

HashMap在多线程下没有任何同步保护,并发操作会导致数据错乱、覆盖、死循环、数据丢失等问题

数据覆盖(丢失)

  • 多个线程同时执行put(),计算出同一个数组下标
  • 两个线程都判断该位置为null
  • 先后都往这个位置插入数据
  • 后插入的会直接覆盖先插入的,导致数据丢失

扩容时链表死循环

JDK1.7扩容采用头插法,多线程并发扩容时:

  • 链表节点会被重新哈希转移
  • 两个线程交替操作,可能让链表形成环形链表
  • 之后调用get()遍历这个环时,会死循环,CPU瞬间100%
  • 之后的JDK改成了尾插法,不会死循环,但仍然线程不安全

扩容期间数据丢失

  • 线程A开始扩容,重新计算哈希、转移节点
  • 线程B同时put、get
  • 扩容过程中数组结构被修改,导致B读写到错误位置,数据丢失或错乱

根本原因

  • 没有加锁(synchronized/CAS)
  • put、resize扩容都是非原子操作
  • 多线程竞争修改同一数组、同一链表节点,没有任何保护

总结

Hash Map没有任何同步机制,多线程并发put会出现数据覆盖、丢失;JDK1.7以下扩容还可能造成链表死循环,因此它是非线程安全的。多线程环境应该使用ConcurrentHashMap

10.HashSet 底层是什么

HashSet底层就是Hash Map,本质是借Hash Map的key来存数据,value同一存一个空对象。

底层实现

  • HashSet内部有一个Hash Map实例
  • 往Hash Set里add的元素,其实是存在Hash Map的key上
  • Hash Map的value统一放一个静态空对象PRESENT,所有元素共用

为什么这样设计

  • Hash Set要保证元素不重复
  • 而HashMap的key本身就是唯一、不重复的
  • 直接服用HashMap的哈希、去重逻辑,不用自己实现
hashSet.add(e) 
→ 调用 map.put(e, PRESENT) 
→ 如果 key 已存在,put 返回旧值,add 返回 false 
→ 如果 key 不存在,put 返回 nulladd 返回 true

特点

  • 无序
  • 无索引
  • 元素不可重复
  • 允许存在一个null
  • 线程不安全(和HashMap一样)

11.创建线程的三种方式

1.继承Thread类

  • 继承Thread,重写run()
  • 启动用start(),不是run()
class MyThread extends Thread {
@Override 
public void run() { 
// 任务 
    } 
} 
// 使用 
new MyThread().start();

2.实现Runnable接口

  • 实现Runnable,重写run()
  • 无返回值、不抛异常
  • 避免单继承局限
class MyRunnable implements Runnable { 
@Override 
public void run() { 
// 任务
    } 
} 
// 
使用 new Thread(new MyRunnable()).start();

3.实现Callable接口+FutureTask

  • 有返回值
  • 可以抛出异常
  • 配合Future.get()获取结果
class MyCallable implements Callable<String> { 
@Override 
public String call() throws Exception { 
return "结果"; 
} 
} 
// 使用
FutureTask<String> task = new FutureTask<>(new MyCallable()); new Thread(task).start(); 
String result = task.get();

三者区别

  1. Thread:继承,单继承受限
  2. Runnable:无返回值,不能抛异常
  3. Callable:有返回值,能抛异常,功能最强

延申

  • 真正创建线程的只用new Thread().start()
  • Runnable/Callable只是任务,不是线程
  • 启动线程必须用start(),直接调用run()只是普通方法

12.sleep 和 wait 区别

  • sleep是Thread类的静态方法,抱着锁睡觉
  • wait是Object的方法,会释放锁,进入等待池

所属类不同

  • sleep()→Thread类的静态方法
  • wait()→Object类的方法9所有对象都有)

是否释放锁

  • sleep:不释放锁 抱着锁休眠,其他线程依然拿不到锁,只能干等。
  • wait:释放锁 调用会立即释放锁,其他线程可以进入同步块

使用位置

  • sleep:任何地方都能用
  • wait:必须在synchronized同步块/方法中使用,否则抛异常

唤醒方式

  • sleep:时间到自动唤醒
  • wait:
    • 时间到自动唤醒
    • 或被notify()/notifyAll()唤醒

用途场景

  • sleep:单纯暂停、延时、定时
  • wait:线程间通信、等待条件、生产者消费者模式

总结

  • sleep属于Thread,wait属于Object
  • sleep不释放锁,wiat释放锁
  • sleep不需要在同步块中,wait必须在同步块中
  • sleep时间到自动行,wait可被notify唤醒

13.synchronized 作用

synchronized是Java中的内置锁(隐式锁、悲观锁),用来保证多线程下的原子性、可见性、有序性、解决线程安全问题

三大作用

  1. 原子性 同一时刻只有一个线程执行代码,避免指令交错,保证操作不可分割
  2. 可见性 解锁时会将修改刷新到主内存,加锁时会重新读取,保证线程间看到最新值
  3. 有序性 禁止锁内代码排序,避免多线程下指令重排导致问题

三种使用位置

  1. 修饰实列方法 锁当前对象(this)
  2. 修饰静态方法 锁当前类的Class对象(全局锁)
  3. 修饰代码块 锁括号里的对象(灵活指定锁)

锁升级机制

synchronized会自动升级,小路很高:偏向锁→轻量级锁(自旋锁)→重量级锁(OS互斥锁)

特点

  • 可重入锁(同一线程可重复获取锁,不会锁死自己)
  • 非公平锁(线程抢占,不按等待顺序)
  • 异常自动释放锁(不会死锁)

总结

synchronized用于保证多线程安全,实现原子性、可见性、有序性;可锁对象、锁类、锁代码块;JDK1.6以后通过锁升级大幅度优化性能

14.volatile 作用

volatile是Java轻量级同步机制,核心作有两个:

  1. 保证可见性
  2. 禁止指令重排序

注意:volatile不保证原子性!

1.保证可见性

  • 一个线程修改了volatile变量,会立即刷新到主内存
  • 其他线程会立即从主内存读取最新值,而不是用自己工作内的缓存
  • 解决多线程下变量不可见问题

2.禁止指令重排序(有序性)

  • 编译器/CPU可能会优化指令顺序
  • volatile会加内存屏障,禁止指令重排
  • 典型场景:DCL单例模式必须加volatile,防止半初始化对象

不保证原子性

  • volatile只保证单次读/写原子
  • 像i++这种读-改-写三步操作,volatile无法保证原子性
  • 要保证原子性必须用:
    • synchronized
    • Lock
    • AtomicInteger等原子类

使用场景

  • 状态标量:volatile boolean flag=true;
  • 单例双重校验锁(DCL)
  • 纯负值、纯读取,且不需要符合操作的场景

总结

volatile保证可见性和有序性,禁止指令重排,但不保证原子性。

适合做状态标记、单例模式禁止重排,不适合技术、i++等符合操作。