Java面试题总结
目录一、Java基础面试题1、JDK和JRE有什么区别?2、== 和equals的区别是什么?3、介绍下hashCode与equals4、final 在 java 中有什么作用?5、String、StringBuffer、StringBuilder的区别6、基本数据类型有哪些?7、重载和重写的区别8、用过的Java 容器都有哪些?9、Collection 和 Collections 有什么区别?10、List和Set的区别11、ArrayList和LinkedList区别12、简述下HashMap的底层原理13、HashMap的put、get方法操作流程14、HashMap的扩容机制原理15、HashMap和HashTable有什么区别?16、ConcurrentHashMap原理,jdk7和jdk8的区别17、ConcurrentHashMap的扩容机制18、java 中 IO 流分为几种?19、深拷贝和浅拷贝区别是什么?20、Java中的异常体系21、throw 和 throws 的区别?22、try-catch-finally 中,如果在 catch 中 return 了,finally 还会执行吗?23、接口和抽象类有什么区别?24、泛型中extends和super的区别二、Java并发面试题1、并发编程三要素2、sleep和wait的区别是什么?3、Java如何开启线程,如何保证线程安全?4、线程之间如何通信?5、Sychronized的偏向锁、轻量级锁、重量级锁6、Synchronized 和 Volatile 有什么区别?DCL(DoubleCheck Lock)单例为什么要加Volatile?7、volatile关键字是如何保证可见性,有序性的?8、ReentrantLock中 tryLock() 和 lock() 方法的区别9、ReentrantLock中,公平锁和非公平锁的底层实现 10、Sychronized和ReentrantLock的区别11、谈谈对AQS的理解,AQS如何实现可重入锁?12、介绍下线程的生命周期,线程有几种状态?13、如何查看线程死锁?14、线程死锁如何避免?15、ThreadLocal的底层原理16、ThreadLocal内存泄漏问题17、线程池的核心参数有哪些?18、线程池的工作原理是什么?19、线程池核心线程数如何设置?20、介绍下线程池中阻塞队列的作用,为什么是先添加队列而不是先创建最大线程数?21、线程池中线程复用原理是什么?三、Spring/ Spring Boot1、Spring Boot自动装配原理1.1 什么是自动装配1.2 说说自动装配的核心注解:@EnableAutoConfiguration1.3 自动装配原理1.4 源码分析1.5 实际应用示例:1.6 自定义与调试1.7 总结2、web开发中,用户登录或访问API接口的认证方式有哪些?3、你们项目中用什么进行接口认证3、接口如何进行鉴权4、事务是如何实现的5、@transactional实现原理6、AOP实现原理7、Spring Boot的配置文件加载顺序8、CSRF如何防范9、跨域如何解决一、Java基础面试题1、JDK和JRE有什么区别?JDK(Java Development Kit),Java开发工具包JRE(Java Runtime Environment),Java运行环境JDK中包含JRE,JDK中有一个名为jre的目录,里面包含两个文件夹bin和lib,bin就是JVM,lib就是JVM工作所需要的类库。2、== 和equals的区别是什么?对于基本类型,==比较的是栈中的值;对于引用类型,==比较的是堆中内存对象的地址;equals不能用于基本类型的比较;如果没有重写equals,equals就相当于==;如果重写了equals方法,equals比较的是对象的内容。3、介绍下hashCode与equalshashCode介绍:hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode()定义在JDK的Object.java类中,Java中的任何类都包含有hashCode()函数。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了哈希码,可以快速找到所需要的对象。为什么要有hashCode:以“HashSet如何检查重复”为例子来说明为什么要有hashCode:对象加入HashSet时,HashSet会先计算对象的hashcode值来判断对象加入的位置,看该位置是否有值,如果没有、HashSet会假设对象没有重复出现。但是如果发现有值,这时会调用equals()方法来检查两个对象是否真的相同。如果相同,HashSet就不会让其加入操作成功。如果不同,就会重新散列到其他位置。这样就大大减少了equals的次数,相应就大大提高了执行速度。4、final 在 java 中有什么作用?修饰变量:表示变量一旦被赋值就不可以更改修饰方法:表示方法不可被子类重写,但可以重载修饰类:表示类不可被继承,比如常用的String类就是最终类。1. 修饰变量:1.1修饰成员变量:如果final修饰的是类变量,则在声明该类变量时就需要赋值,或者在静态初始化块中指定初始值;如果final修饰的是成员变量,则在声明该变量、非静态初始化块或者构造器中执行初始值1.2修饰局部变量:系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此使用final修饰局部变量时,即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对final变量赋初值(仅一次)1.3 修饰基本类型数据和引用类型数据:如果修饰基本数据类型变量,则其数值一旦初始化便不能再更改;如果修饰引用类型变量,比如对象、数组,则该对象、数组本身的值可以修改,但指向该对象或数组的地址的引用不能修改。为什么局部内部类和匿名内部类只能访问局部final变量?如下类编译之后会生成两个class文件,FinalDemo.class,FinalDemo1.classpublic class FinalDemo { final static int num = 0; // 类变量在声明时就需要赋值,或者在静态代码块中赋值 /** static { num = 0; }*/ final int a = 0; // 成员变量在在声明的时候就需要赋值,或者代码块中赋值,或者构造器赋值 /** { a = 0; }*/ public static void main(String[] args){ final int count; // 局部变量只声明,没有初始化,不会报错 count = 1; // 在使用之前必须赋值 // count = 2; // 不允许第二次赋值,会报错 final int[] arr = {1, 2, 3}; arr[2] = 6; // 合法 // arr = null; // 非法,对arr引用不能重新赋值 final User user = new User(); user.setName("Allen"); // 合法 // user = null; // 非法 } // 局部final变量a, b public void test(final int bb){ final int aa = 10; // 局部变量a, b都加了 final // 匿名内部类 new Thread(){ public void run(){ System.out.println(aa); System.out.println(bb); } }.start(); } } class OutClass { private int age = 22; public void initTest(final int cc) { class InClass { // 局部内部类 public void inPrint() { System.out.println(cc); System.out.println(age); } } new InClass().inPrint(); } }内部类和外部类处于同一级别,内部类不会因为定义在方法中就随着方法的执行完毕而被销毁。这就产生一个问题:当外部类的方法结束时局部变量就会被销毁,但内部类对象可能还存在(只有没再引用它时,才会消亡)。这就出现一个矛盾:内部类对象访问了一个不存在的变量。为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样当局部变量死亡后,内部类仍可以访问它,实际访问的是局部变量的”copy”。这样就好像延长了局部变量的生命周期。将局部变量复制为内部类的成员变量时,必须保证这两个变量是一样的,也就是如果我们在内部类中修改了成员变量,方法中的局部变量也得跟着改变,怎么解决问题呢?将局部变量设置为final,对它初始化后,就不让再去修改这个变量,这就保证了内部类的成员变量和方法的局部变量的一致性,使得局部变量与内部类内建立的拷贝保持一致。5、String、StringBuffer、StringBuilder的区别String是final修饰的,不可变,每次操作都会产生新的String对象StringBuffer和StringBuilder都是在原对象上操作StringBuffer是线程安全的,StringBuilder线程不安全的StringBuffer方法都是synchronized修饰的性能:StringBuilder StringBuffer String场景:经常需要改变字符串内容时使用后面两个,优先使用StringBuilder,多线程使用共享变量时使用StringBuffer6、基本数据类型有哪些?byte、short、int、long、double、float、boolean、char。7、重载和重写的区别重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。8、用过的Java 容器都有哪些?(1) 实现Collection接口的集合 ① set:HashSet、TreeSet ② list:ArrayList、LinkedList、Vector(2) 实现Map接口的集合 HashMap、HashTable、TreeMap9、Collection 和 Collections 有什么区别?Collection(接口)位于java.util.Collection包是 Java 集合框架的根接口主要子接口:List(有序、可重复)Set(无序、不可重复Queue(队列)Collections(工具类)位于java.util.Collections包是一个工具类,提供静态方法操作集不能被实例化(构造器私有)包含各种有关集合操作的静态方法(对集合的搜索、排序、线程安全化等)10、List和Set的区别List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出所有元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素Set:无序,不可重复,最多允许有一个Null元素对象,取元素时只能用Iterator接口取得所有元素,在逐一遍历各个元素11、ArrayList和LinkedList区别ArrayList:基于动态数组,连续内存存储,适合下标访问(随机访问),扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚至超过linkedList(需要创建大量的node对象)LinkedList:基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询:需要逐一遍历遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i)取得某一元素时都需要对list重新进行遍历,性能消耗极大。另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexlOf对list进行了遍历,当结果为空时会遍历整个列表。12、简述下HashMap的底层原理HashMap是Java集合框架中的一个重要部分,它基于哈希表实现,使用键值对(key-value pairs)来存储对象。1.核心存储结构(JDK1.8+)数组(哈希桶/table):用于存储链表(或红黑树)的头部引用。每个数组元素称为一个“桶”(bucket)。链表(链表节点/Node):每个桶可以包含一个或多个键值对。当发生哈希冲突时,即两个不同的键通过哈希函数计算出的索引相同,这些键值对会被存储在同一个桶的链表中。红黑树(树节点/TreeNode):链表的升级结构,从Java8开始,如果链表中的元素数量超过一个阈值(默认为8)且数组长度 ≥ 64 时,链表会自动转换为红黑树,以提升性能。2. 哈希函数HashMap使用哈希函数来确定键值对在数组中的位置。Java中的hashCode()方法由对象实现,用于生成对象的哈希码。HashMap通过以下方式处理哈希码:int hash = key.hashCode(); int index = (n - 1) hash;这里,n是数组的长度,是位与操作符。这种操作确保了哈希码能够均匀分布在数组的索引上,从而减少哈希冲突。3. 扩容机制当HashMap中的元素数量超过其容量(capacity)与负载因子(loadfactor)的乘积时,触发扩容操作(resize()),将数组长度扩大一倍,并将原数组中的元素重新计算哈希值后放入新的数组位置。扩容操作的开销较大,因此在设计时需要注意避免频繁扩容。默认的负载因子是0.75。当数组中元素的数量达到数组容量的75%时,会进行扩容操作。4. 关键特性Key 唯一:Key 基于哈希值 + equals 方法判断唯一性,若 Key 重复,新 Value 会覆盖旧 Value;Value 可重复:多个 Key 可以对应相同的 Value;无序性:插入顺序与遍历顺序不一致(因为节点位置由哈希值决定,扩容时还会迁移节点);非线程安全:多线程环境下,并发执行 put/resize 可能导致链表环化(死循环)、数据丢失等问题,解决方法:多线程环境下操作同一个HashMap时,使用Collections.synchronizedMap(new HashMap())方法或使用ConcurrentHashMap)。13、HashMap的put、get方法操作流程 put方法操作流程:1. 计算 key 的哈希值 hash(key)2. 计算桶索引: (n-1) hash3. 如果桶为空:直接创建新节点放入4. 如果桶不为空: a. 检查第一个节点 key 是否相等(equals) b. 如果是树节点:调用红黑树的插入方法 c. 如果是链表:遍历查找 - 找到相同 key:更新 value - 没找到:添加到链表末尾5. 判断是否需要树化(链表长度 ≥ 8)6. 判断是否需要扩容 get方法操作流程:1. 计算 key 的哈希值2. 计算桶索引3. 检查桶中第一个节点 - 直接匹配:返回 - 链表:遍历查找 - 红黑树:调用树的查找方法4. 没找到返回 null14、HashMap的扩容机制原理1.7版本 1. 先生成新数组 2.遍历老数组中的每个位置上的链表上的每个元素 3.取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标 4.将元素添加到新数组中去 5.所有元素转移完之后,将新数组赋值给HashMap对象的table属性1.8版本 1.先生成新数组 2.遍历老数组中的每个位置上的链表或红黑树