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,不能有其他修饰符
设计思想
- 抽象类:为一组相似类提供通用模板
- 接口:为不同类提供同一行为标准,解耦
总结
- 抽象类单继承,接口多实现
- 抽象类可有成员变量,接口只能是常量
- 抽象类能写构造方法,接口不能
- 抽象类是模板,接口是规范
- 优先用接口,需要共用代码再用抽象类
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;
}
总结
- 修饰类:不可继承,保证安全性。
- 修饰方法:不可重写,锁定逻辑。
- 修饰变量:引用不可变,内容可变(引用类型);值不可变(基本类型)。
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强制你提前处理
总结
- Throwable是顶级父类,分为Error和Exception。
- Error是JVM严重错误,程序无法处理,不用捕获。
- Exception是程序异常,可以捕获处理。
- Exception分两种:
-
- 运行时异常(RuntimeException):编译不检查,代码逻辑问题
-
- 编译时异常(Checked Exception):编译强制检查,外部环境问题。
- 运行时异常不用捕获,编译异常必须捕获或抛出
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
总结
- ArrayList数组,LinkedList双向链表
- ArrayList查找快,中间增删慢
- LinkedList增删快,查找慢
- 日常开发优先ArrayList
8.HashMap底层原理
底层数据结构
Java8:数组+链表+红黑树
- 主体是Entry数组(哈希桶数组)
- 哈希冲突时用链表解决
- 链表长度>=8且数组长度>=64时,转为红黑树
- 红黑树节点<=6时,退化为链表
存储过程(put流程)
- 对key调用hashCode()计算哈希值
- 对哈希值做扰动处理,再与数组长度-1做位运算,得到数组下标
- 若改位置为空,直接放入节点
- 若已存在元素(哈希冲突):
-
- 是链表:遍历比较equals(),相同则覆盖,不同则尾插
-
- 是红黑树:按树结构插入
- 插入后判断是否需要扩容(负载因子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 返回 null,add 返回 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();
三者区别
- Thread:继承,单继承受限
- Runnable:无返回值,不能抛异常
- 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中的内置锁(隐式锁、悲观锁),用来保证多线程下的原子性、可见性、有序性、解决线程安全问题
三大作用
- 原子性 同一时刻只有一个线程执行代码,避免指令交错,保证操作不可分割
- 可见性 解锁时会将修改刷新到主内存,加锁时会重新读取,保证线程间看到最新值
- 有序性 禁止锁内代码排序,避免多线程下指令重排导致问题
三种使用位置
- 修饰实列方法 锁当前对象(this)
- 修饰静态方法 锁当前类的Class对象(全局锁)
- 修饰代码块 锁括号里的对象(灵活指定锁)
锁升级机制
synchronized会自动升级,小路很高:偏向锁→轻量级锁(自旋锁)→重量级锁(OS互斥锁)
特点
- 可重入锁(同一线程可重复获取锁,不会锁死自己)
- 非公平锁(线程抢占,不按等待顺序)
- 异常自动释放锁(不会死锁)
总结
synchronized用于保证多线程安全,实现原子性、可见性、有序性;可锁对象、锁类、锁代码块;JDK1.6以后通过锁升级大幅度优化性能
14.volatile 作用
volatile是Java轻量级同步机制,核心作有两个:
- 保证可见性
- 禁止指令重排序
注意:volatile不保证原子性!
1.保证可见性
- 一个线程修改了volatile变量,会立即刷新到主内存
- 其他线程会立即从主内存读取最新值,而不是用自己工作内的缓存
- 解决多线程下变量不可见问题
2.禁止指令重排序(有序性)
- 编译器/CPU可能会优化指令顺序
- volatile会加内存屏障,禁止指令重排
- 典型场景:DCL单例模式必须加volatile,防止半初始化对象
不保证原子性
- volatile只保证单次读/写原子
- 像i++这种读-改-写三步操作,volatile无法保证原子性
- 要保证原子性必须用:
-
- synchronized
-
- Lock
-
- AtomicInteger等原子类
使用场景
- 状态标量:volatile boolean flag=true;
- 单例双重校验锁(DCL)
- 纯负值、纯读取,且不需要符合操作的场景
总结
volatile保证可见性和有序性,禁止指令重排,但不保证原子性。
适合做状态标记、单例模式禁止重排,不适合技术、i++等符合操作。