最近开始面试了,410面试了一家公司 针对自己薄弱的面试题库,深入了解下,也应付下面试。在这里先祝愿大家在现有公司好好沉淀,定位好自己的目标,在自己的领域上发光发热,在自己想要的领域上(技术管理、项目管理、业务管理等)越走越远!希望各位面试都能稳过,待遇都是杠杠的!
基础概念
java的特点是?
- 面向对象
- 平台无关性(一次编译,到处运行)
- 支持多线程
- 自动内存管理(垃圾回收)
- 安全性
- 丰富的类库
Q:为何说JAVA是跨平台的?而他如何做到一次编译 到处运行的?
A:通过标准化的字节码和平台特定的JVM,将平台相关性从编译阶段转移到运行阶段,从而实现“一次编译,到处运行”。
- 编译生成字节码,java源代(.java文件)通过javac编译器编译成平台无关的字节码(.class文件)
- 字节码是JVM的指令集,不是针对特定硬件或者操作系统的机器码,而是一种中间代码。
- java解释执行字节码,不同的JVM负责相关的字节码翻译成当前本地机器指令并执行;jvm是平台相关的(需要针对不同操作系统单独安装),但字节码和平台无关。
- 关键技术支持:
- 字节码规范:JVM定义统一的字节码格式(由oracle的jvm规范标准化)
- JVM适配各平台:不同厂商为不同系统实现JVM,确保字节码能正确的翻译为本地指令。
- 类加载机制:jvm动态加载.class文件,按需解析和执行
- 实际限制:
- JVM版本兼容性:字节码需与目标JVM版本兼容(高版本的字节码不能在低版本的JVM运行)
- 平台相关代码:通过native方法(如JNI)调用本地库(.dll/.so)扔需针对不同平台编译
- 性能开销:JVM解释执行或者JIT编译可能比直接运行机器码稍慢,但已经大幅度提升了。
2.JDK\JRE\JVM的区别是什么?
- JDK(java development kit):开发工具包,包含JRE+编译器(javac)等开发工具
- JRE(Java runtime environment):运行环境,包含JVM+核心类库
- JVM(java virtual machine):执行java字节码的虚拟机
3.java是解释型语言还是编译型语言
两者结合,java代码先编译为字节码(.class文件),然后由JVM解释执行或者JIT编译执行。
4.java的基础类型有哪些
- 整形:byte(1),sort(2) ,int(4),long(8)
- 浮点型:float(4),double(8)
- 字符型:char(2)
- 布尔型:boolean(1)
Q:使用BigDecimal对大数据处理留下的坑
- 构架方法问题:double构造精度丢失
- double本身精度问题,使用时先转成string
- 等值比较问题
- equals()与compareTo()差异
- equals比较涉及到精度问题,比较尽量使用compareTo
- equals()与compareTo()差异
- 触发运算问题、精度问题
- 未指定舍入模式,除法会出现ArithmeticExcetion
- 舍入模式:RoundingMode.HALF_UP 四舍五入,RoundingMode.DOWN 直接截断,RoundingModeCEILING 向正无穷舍入
- 大量运算性能开销大
- BigDecimal的运算比基本类型慢很多
- 对于简单的运算,考虑先用基本类型运算后,在转换成BigDecimal
- 重用BigDecimal对象(不可变对象每次运算都创建新对象)
- 不可变性带来的问题
- 忽略返回值,计算时需要将返回值获取
- 科学计数法问题
- 意外科学计数法显示,可以用toPlainString处理
Q:为何BigDecimal更适合精确运算
- 精确的数值表示
- 完全控制运算精度
- 可配置的舍入模式
- 大数据处理能力好
- 金融计算必须特性
- 避免累积误差
- 小结
-
金融计算必须使用BigDecimal
-
构造时使用String参数:
new BigDecimal("0.1")
而非new BigDecimal(0.1)
-
设置明确的精度和舍入模式:特别是除法运算
-
考虑定义工具类封装常用操作
-
对于简单计算:可先用基本类型计算,最后转为BigDecimal
-
5.基础类型和包装类型的区别
- 包装类型是类,可以为null,有方法和属性
- 基本类型直接存储值,效率更高
- 自动装箱/拆箱机制
Q:自动装箱/拆箱机制是什么?
A:装箱和拆箱主要是简化基本数据类型和其对应的包装类之间的转换
- 自动装箱(AutoBoxing):将基本数据类型紫装转换成对应的包装类对象
- 自动拆箱(Unboxing):将包装类对象自动转换为对应的基本数据类型
- 实现原理:自动装箱和拆箱是编译器提供的语法糖,编译时会替换为对应的包装/拆箱方法调用
- 装箱:调用Integer.valueOf()、Double.valueOf()等方法
- 拆箱:调用intValue、doubleValue()等方法
- 注意事项:
- 性能考虑:自动装箱/拆箱会创建临时对象,在循环中大量使用可能会影响性能
- 空指针异常:拆箱null值会抛出NullPointerException
- 缓存机制:包装类对部分值有缓存(如integer缓存)
6.==和equals()的区别
- ==比较基本类型的值或对象的引用地址
- equals()比较对象的内容(默认比较地址可重写)
7.面向对象的三大特性是什么?
- 封装:隐藏实现细节,提供公共访问方式
- 继承:子类继承父类特征的行为
- 多态:同一操作作用于不同对象产生的不同兴义
8.重载(overload)和重写(override)的区别
- 重载:同一类中,方法名相同但参数不同(类型、数量、顺序)
- 重写:子类重新定义父类的方法,方法签名必须相同
9.接口和抽象类的区别?
- 接口:完全抽象,java8前只有抽象方法,java8后接口可以有默认方法和静态方法,支持多实现
- 抽象类:可以有抽象方法和具体方法,单继承
10.java集合框架主要接口有哪些?
- Collection:List\Set\Queue
- Map:Hashmap\Treemap
11.ArrayList和LinkedList区别?
- ArrayList:基于动态数组,随机访问快,插入删除慢
- LinkedList:基于双向链表,插入删除快,随机访问慢
Q:Arrays.asList数组转换列表的命案?
- 固定大小列表,不可增加/删除元素
- 因为返回的Arrays内部的ArrayList(非java.util.ArrayList)
- 这个内部类没有实现add()和remove()方法
解决方案
// 方法1:使用 new ArrayList 包装
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));// 方法2:Java 8+ Stream
List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());
- 数组与列表的共享引用,数组被修改后,list对应的元素值也会被修改
- Arrays.asList()返回的列表直接“包装”原始数组
- 两者共享同一数据引用
解决方案
// 创建新列表(深拷贝)
List<String> list = new ArrayList<>(Arrays.asList(arr.clone()));
- 基本类型数组问题,非包装类的基础类型,会被视为一个整体,即长度永远为1
- 基本类型数组会被当做单个对象处理
- Arrays.asList()不支持基本类型的自动装箱
解决方案
// 方法1:使用包装类型数组
Integer[] integerArray = {1, 2, 3};
List<Integer> list = Arrays.asList(integerArray);// 方法2:Java 8 Stream
List<Integer> list = Arrays.stream(intArray).boxed().collect(Collectors.toList());
- 不支持序列化
- 尝试序列化可能失败,因为内部ArrayList未实现serializable
解决方案
// 使用可序列化的ArrayList
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));
- 视图与原始数据同步
- 当原始数据放入一个对象变量a,然后在通过asList转换后变量b,只要变量a中的元素变换了,b对应的位置也会改变
解决方案
// 如果需要独立副本
List<String> list = new ArrayList<>(Arrays.asList(arr));
- 结论:
- 需要修改列表是:使用new ArrayList<>(Arrays.asList())
- 处理基本类型数组时:先用Arrays.stream.boxed()转换
- 需要独立副本时:确保穿件新列表而非直接使用asList结果
- 需要序列化:避免直接使用asList返回的列表
Q:ArrayList中的其他问题?
- ArrayList的subList强转ArrayList导致异常calssCastException
- subList返回的是ArrayList的内部类SubList并不是ArrayList,而是ArrayList的一个视图,对于Sublist子列表的所有操作最终会反映到原列表上。
- 解决方案就是重新new一个新的arrayList出来装返回的list
- ArrayList中的subList切片造成OOM
- 使用subList切片,因为原List被强引用,得不到回收,造成OOM
- 解决方案
- 在subList方法返回SubList,重新使用new ArrayList,来构建一个独立的ArrayList
- 利用Java8的Stream中的skip和limit来达到切片的目的
- 切断与原始List的关系
- ArrayList如果不正确增删操作会导致ConcurrentModificationException
Q:copyOnWriteArrayList工作原理?
A:当我们对某个类上面的全局变量List进行读写操作的时候,单线程不会有什么影响,但是多线程当A线程在迭代,B线程在写操作,会导致java.util.ConcurrentModificationException的异常,而CopyOnWriteArrayList就解决了这个问题。
- 原理:在写操作(add、remove等)时,不直接对原数据进行修改,而是先将原数据复制一份,然后在心腹之的数据上执行写操作,最后将原数据的引用指向新数据。这样做的好处是读操作(get、iterator等)可以不枷锁,因为读取的数据始终是不变的。
- 优点
- 线程安全:copyOnWriteArrayList是线程安全的,由于写操作对原数据进行复制,因此写操作不会影响读操作,读操作不加锁,降低了并发冲突的概率。
- 不会抛出ConcurrentModificationException异常。由于读操作遍历的是不变的数组副本,因此不会抛出ConcurrentModificationException异常
- 缺点
- 写操作性能较低:由于每次写操作都要复制一份数据,因此写操作性能较低
- CopyOnWriteArrayList迭代器是只读的,不支持增删改,以为CopyOnWriteArrayList迭代器中没有remove()和add()方法,没有支持增删而是直接抛出了异常
- 内存占用增加,由于每一次写操作都需要创建一个新的数据副本,因此内存占用会增加,特别是当集合有大量数据时,内存占用较高
- CopyOnWriteArrayList每次修改都会重新创建一个大对象,并且原来的大对象也需要回收,这都是会触发GC,如果超过老年代的大小则容易触发FullGC 引发应用程序长时间停顿。
- 数据一致性问题,由于读操作遍历的是不变的数组副本,因此在对数组执行写操作期间,读操作可能读取到旧的数组数据,这就涉及到数据一致性问题
- 写操作性能较低:由于每次写操作都要复制一份数据,因此写操作性能较低
- 使用场景
- 读多写少。
- 集合不大。
- 实时性要求不高
12.HashMap的工作原理
- 数组+链表/红黑树结果
- 通过hashCode计算位置,解决冲突使用链表法
- 当链表长度>8且数组长度>64时转为红黑树
Q:如何优雅的删除HashMap元素
- 已知key删除
// 删除值为2的元素
map.values().removeIf(value -> value == 2);// 删除key以"b"开头的元素
map.keySet().removeIf(key -> key.startsWith("b"));
- 迭代器删除
//使用Iterator(传统方式
Iterator<Map.Entry<String, Integer>> it = map.entrySet().iterator();
while (it.hasNext()) {Map.Entry<String, Integer> entry = it.next();if (entry.getValue() == 1) {it.remove(); // 安全删除当前元素}
}//使用removeIf(Java 8+更简洁)
map.entrySet().removeIf(entry -> entry.getValue() == 1);
- 函数式编程删除
//使用Stream过滤(创建新Map)
Map<String, Integer> filteredMap = map.entrySet().stream().filter(entry -> !entry.getKey().equals("a")).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));//使用computeIfPresent(条件计算后删除)
map.computeIfPresent("a", (k, v) -> v == 1 ? null : v);
// 当返回null时,该键值对会被删除
- 批量删除
// 删除多个key
Set<String> keysToRemove = Set.of("a", "b");
keysToRemove.forEach(map::remove);
// 保留特定元素(反向删除)
Set<String> keysToKeep = Set.of("c", "d");
map.keySet().retainAll(keysToKeep); // 只保留指定key的元素
- 并发安全删除
// ConcurrentHashMap删除
ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>();
concurrentMap.remove("a"); // 线程安全删除// 原子条件删除
concurrentMap.remove("a", 1); // 只有当key"a"对应value为1时才删除
// 同步块删除
synchronized(map) {if (map.containsKey("a")) {map.remove("a");}
}
- 优雅的删除空值
// Java 8+ 最简洁方式
map.values().removeIf(Objects::isNull);// 传统方式
Iterator<Integer> it = map.values().iterator();
while (it.hasNext()) {if (it.next() == null) {it.remove();}
}
方法 | 适用场景 | 线程安全 | 备注 |
---|---|---|---|
remove(key) | 简单单元素删除 | 不安全 | 最常用基础方法 |
removeIf | 条件删除多个元素 | 不安全 | Java8+ 简洁语法 |
Iterator.remove() | 迭代时删除 | 不安全 | 传统安全删除方式 |
Stream filter | 创建新Map不修改原Map | 不安全 | 函数式风格,有性能开销 |
ConcurrentHashMap | 多线程环境 | 安全 | 最佳并发性能 |
Q:红黑树原理?
A:jdk8中,HashMap为了解决哈希冲突导致的性能退化问题,当链表长度超过阈值(默认为8)且数组长度>=64,会将链表转换成红黑树。
- 优化目的
- 链表查找时间复杂度O(n)
- 红黑树查找时间复杂度O(log n)
- 显著提升高冲突情况下的查询效率
- 红黑树基本特征
- 红黑树是一种自平衡的二叉查找树,
- 节点颜色:每个节点非红即黑
- 根节点:根节点必须是黑色的
- 叶子节点:所有叶子节点(NIL节点)都是黑色
- 红色限制:红色节点的子节点必须是黑色(不能有连续红色节点)
- 黑高平衡:从任意节点到器每个叶子节点所有的路径包含相同数量的黑色节点
- 核心操作原理:
- 插入操作:
- 按照二叉查找树规则插入新节点(初始为红色)
- 通过旋转和变色维持红黑树特性
- 左旋:以某个节点为支点向右选择
- 右旋:以某个节点为支点向左选择
- 变色:改变节点颜色
- 插入后的修复情况:
- 叔节点为红色:重新着色
- 叔节点为黑色且新节点是右孩子:左旋父节点
- 叔节点为黑色且新节点是左孩子:右旋祖父节点并重新着色
- 删除操作
- 执行标准的BST删除
- 如果删除的是黑色节点,需要进行平和修复
- 兄弟节点为红色:转换情况
- 兄弟节点为黑色且兄弟的子节点都是黑色:重新着色
- 兄弟节点为黑色且至少一个红子节点:旋转并重新着色
- 树化与反树化条件
- 树化条件(链表->红黑树)
- 链表长度>=TREEIFY_THRESHOLD(8)
- 且数组长度>= MIN_TREEIFY_CAPACITY(64)
- 树化条件(链表->红黑树)
- 反树化条件(红黑树->链表)
- 树节点<=UNTREEIFY_THRESHOLD(6)
- 红黑树的优势体现:同样是8节点
- 查找操作时链表最多8次比较,插入O(1),删除O(n)
- 查找操作时红黑树最多3次比较,插入O(log n),删除O(log n)
- 红黑树树化是先扩容在树化
- 设计考量
- 为何阈值设为8
- 根据泊松分布,哈希冲突达到8的概率极低(约0.000000006)
- 正常使用情况下几乎不会转换为红黑树
- 为什么会退化阈值为6
- 避免频繁的树化和反树化
- 为什么需要数组长度>=64才树化
- 优先通过扩容减少冲突,而不是立刻转为树结构
- 为何阈值设为8
- 设计考量
- 总结:
- HashMap的红黑树机制是其高性能的重要保障
- 在极端哈希冲突情况下自动优化数据结构
- 保证O(log n)的查询效率
- 平衡了空间和时间复杂度
- 智能的在树和链表之间转换
- 插入操作:
13.java异常解构体系是怎么样的
- Throwable:
- Error:系统错误,不应捕捉
- Exception:
- RuntimeException:非受检异常
- 其他Exception:受检异常
14.throw和throws的区别
- throw:在方法内部抛出异常对象
- throws:声明方法可能抛出的异常类型
15.创建线程的几种方式
- 集成Thread类
- 由于java是单继承,这种方式会占用继承的位置
- 耦合度高
- 实现Runnale接口
- 可以实现多接口,灵活度高
- 适合多个线程共享相同代码的情况
- 实现Callable接口(可返回结果)
- 可以返回执行结果
- 可以抛出异常
- 通常配合ExecutorService使用
- 实现线程池
- 有效管理线程生命周期
- 减少线程创建和销毁的开销
- 提供多种线程池配置选项
- completableFuture
- 异步编程,使用并行流
创建方式 | 返回值 | 异常处理 | 适用场景 | 推荐指数 |
---|---|---|---|---|
继承Thread | 无 | 无 | 简单测试 | ★★☆☆☆ |
实现Runnable | 无 | 无 | 通用场景 | ★★★★★ |
实现Callable | 有 | 支持 | 需要返回结果的场景 | ★★★★☆ |
线程池 | 可选 | 支持 | 生产环境、高并发场景 | ★★★★★ |
CompletableFuture | 有 | 支持 | 异步编程、组合操作 | ★★★★☆ |
Q:为何不推荐用Executors创建线程池?
A:虽然Executors工具类提供了快速创建线程吃的静态工厂方法,但在生产环境中直接使用这些方法可能存在风险。
- 主要的问题与风险
- FixedThreadPool和SingleThreadPool的队列风险
- 问题:使用无界队列(LinkedBlockingQueue)
- 风险:任务堆积可能导致OOM,eg:当任务提交速度 > 处理速度时,队列无限增长
- CachedThreadPool的线程数风险
- 问题:线程数无上限(Integer.MAX_VALUE)
- 风险:
- 大量创建线程可能导致系统资源耗尽
- 极端情况下会引发OOM或者系统崩溃
- SheduledThreadPool的类似问题
- 问题:同样适用无界队列(DelayedWorkQueue)
- 风险:与FixedThreadPool类似的内存问题
- FixedThreadPool和SingleThreadPool的队列风险
- 推荐做法:
- 手动创建ThreadPoolExector
- 参数配置要点:
- corePoolSize:根据CPU核数(通常Runtime.getRuntim().availableProcessors())
- maximumPoolSize:根据任务特性设置(IO密集型设置高,CPU密集型不宜过高避免上下文切换)
- workQueue:必须适用有界队列(ArrayBlockingQueue)
- keepAliveTime:合理设置线程回收时间
- 拒绝策略:
- AbortPolicy(默认):抛出RejectedExecutionException
- CallerRunsPolicy;由提交任务的线程直接执行
- DiscardPolicy:静默丢弃任务
- DiscardOldestPolicy:丢弃队列中最旧的任务
- 参数配置要点:
- 使用第三方线程池工具
- spring的ThreadPoolTaskExecutor
- Guava的ThreadPoolBuilder
- 手动创建ThreadPoolExector
16.sleep()和wait()的区别
- sleep()是Thread方法,不释放锁
- wait()是Object的方法,释放锁,需在同步块中使用。
17.synchronized和ReentrantLock区别
- synchronized是关键字,JVM实现;ReentrantLock是类
- ReenTrantLock功能更丰富:可以中断,支持公平锁、多条件变量等
18.BIO/NIO/AIO区别
- BIO:同步阻塞IO,一个连接只能处理一个线程
- NIO:同步非阻塞IO,多路复用
- AIO:异步非阻塞IO,基于回调,IO处理完成后通知系统回调
19.java NIO的核心组件有哪些
- Channel数据通道
- Buffer:数据缓冲区
- Selector:多路复用器
20.Java8的主要特性
- Lambda表达式
- Stream API
- 方法引用
- 默认方法
- Optional类
- 新的日期时间API
21.Lambda表达式的使用场景
- 函数式接口(只有一个抽象方法的接口)
- 替代匿名内部类
- 集合操作
22.Stream API的特点是什么?
- 函数式风格处理数据
- 惰性求值
- 可并行处理
- 不修改原数据
23.列举几种常见的设计模式以及应用场景
- 单例模式:全局唯一实例(如配置类)
- 工厂模式:对象创建解耦
- 观察者模式:事件处理系统
- 装饰器模式:IO流设计
24.java如何实现线程安全的单例模式
可以使用双重检锁定来确定,定义静态实例对象,获取实例对象时,判断实例对象是否为空,若为空通过synchronized锁住该对象,然后再次判断对象是否为空,若为空则创建,否则直接返回
// 双重检查锁定
public class Singleton {private volatile static Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}
25.java内存模型(JMM)是什么
- 定义线程如何与内存交互
- 主内存与工作内存的概念
- happens-before原则保证可见性
26.volatile关键字的作用
- 保证可见性:修改立即对该线程可见
- 禁止指令冲排序
- 不保证原子性
27.final关键字和finally的区别
- final:
- 修饰类:不可以集成
- 修饰方法:不能重写
- 修饰变量:基本类型值,引用类型引用不可变
- finally:
- 一般和try catch一起使用,保证try catch无论走哪个方法都可以进finally进行处理。
28.如何优雅的避免空指针
- 使用if else判断,避免出现空指针
- 使用工具类美化一下if判断如StringUtils.isEmpty/OjbectUtils.isEmpty/Collections.emptyXxx
- 使用optional解决层次多的问题,当配和orElse时,会优先执行orElse方法,然后执行逻辑代码
- 使用断言处理接口入参,检查假设和前置条件是否满足,检查空值情况,提前捕获空指针
- 使用@Nullable/@NotNull注解,标识变量或者方法参数和返回值是否为null,在编译器或者开发工具提示风险
29.String能存储多少个字符
- String的length方法返回的是int,理论上长度不会超过int的最大值
- 编译器源码限制了字符串长度大于等于65535就编译不通过
- JVM规范对常量池有所限制。量池中的每一种数据项都有自己的类型
- 运行时限制,在运行时限制主要体现在构造函数上
Q:JDK9为何要讲String的底层实现由char[]改成byte[]
A:为了节省字符串暂用的内存,减少GC的次数
- byte[]更节省空间,从char[]到byte[]中文是两个字节,纯应为是一个字节,在此之前中文是两个字节,英文也是两个字节
29.如何在有限内存下读取超限的数据并统计数字重复此书,获取最大的重复数
- 分块读取使用Map来进行统计
- 这种适合处理数据均匀分布,不存在大量唯一值数据,如果文件高度稀疏,大量的唯一值,会导致Map爆内存出现OOM;
- 外部排序法
- 将文件分块读取,在将读取的数据hash到不同的文件中,然后在完成对应的统计。
- MMAP映射不进行分片,逐个处理,但这种可能会出现统计Map爆内存OOM。
- 概率统计法(近似算法)
- 使用布隆过滤器等概率数据结构,适合允许近似结果的场景
- 数据库辅助法
- 将数据分批导入数据库,用SQL统计
- 内存优化技巧
- 使用原始类型集合:Trove库的TintIntHashMap
- 压缩存储:对数子进行差值编码等压缩
- 位图法:如果数据范围有限,可用BitSet统计
- 内存优化技巧
- 将数据分批导入数据库,用SQL统计