2025最新java高频面试题(八股文)

基础/集合
1.ArrayList/LinkedList有什么区别?

1、数据结构: 在数据结构上,ArrayList 和 LinkedList 都是 “线性表”,都继承于 Java 的 List 接口。另外 LinkedList 还实现了 Java 的 Deque 接口,是基于链表的栈或队列,与之对应的是 ArrayDeque 基于数组的栈或队列;

2、线程安全: ArrayList 和 LinkedList 都不考虑线程同步,不保证线程安全;

3、底层实现: 在底层实现上,ArrayList 是基于动态数组的,而 LinkedList 是基于双向链表的。

  遍历速度: 数组是一块连续内存空间,基于局部性原理能够更好地命中 CPU 缓存行,而链表是离散的内存空间对缓存行不友好;

  访问速度: 数组是一块连续内存空间,支持 O(1) 时间复杂度随机访问,而链表需要 O(n) 时间复杂度查找元素;

  添加和删除: 如果是在数组的末尾操作只需要 O(1) 时间复杂度,但在数组中间操作需要搬运元素,所以需要 O(n)时间复杂度,而链表的删除操作本身只是修改引用指向,只需要 O(1) 时间复杂度(如果考虑查询被删除节点的时间,复杂度分析上依然是 O(n),在工程分析上还是比数组快);

  内存消耗: ArrayList 在数组的尾部增加了闲置位置,而 LinkedList 在节点上增加了前驱和后继指针

2.HashMap的底层实现-详解(jdk7&jdk8)

数据结构

jdk1.7 :数组 + 链表(单向)

jdk1.8 :数组+链表(单向)+红黑树

概念:

数组:一段连续的节点组成的内存区域,在内存中连续存储

链表:一段非连续的节点组成的存储结构,分为单向链表(最后一个节点的next 指向null)和双向链表(两个指针,pre 指向上一个节点,next 指向下一个节点,第一个节点的pre 指向null,最后一个节点的next 指向null)。

单向循环链表:最后一个节点next 指向head

双向循环链表:最后一个节点next 指向head,第一个节点pre 指向最后一个节点,linkedList 采用的就是这种

而hashMap 采用单向链表就是为了避免循环引用

jdk 1.7 中的数据存储结构:

 
  1. static class Entry<K,V> implements Map.Entry<K,V> {

  2. final K key;

  3. V value;

  4. Entry<K,V> next;

  5. final int hash;

  6. }

put 操作

1.若数组不存在,会初始化一个默认容量(1<<4=16) 的一个数组

 
  1. * The default initial capacity - MUST be a power of two.

  2. */

  3. static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

此处没有直接写16(10000),用的是位移,是为了增加效率
1.1. 由于数据长度为16,所以向数组中放入数据时,数组的下标index 应该为0-15。

  • 取模:用key.hashCode%16 取模得到,但是取模运算比较耗时
  • 位运算(& 与):所以采用位运算与 (n - 1) & hash(直接对内存数据进行运算,不需要转成十进制,1&1 =1)
 
  1. if ((p = tab[i = (n - 1) & hash]) == null)

  2. tab[i] = newNode(hash, key, value, null);

为了让key更均匀的分布,对hash 做异或 ^,在高低16位

 
  1. hash = key.hashCode^key.hashCode >>>16

 
  1. static final int hash(Object key) {

  2. int h;

  3. return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);

  4. }

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案【点击此处即可】免费获取

初始容量和扩容为什么一定是2的整数次幂(面试点)

和与的计算有关,因为与操作是对应位置都为1 时,计算结果才为1,当hashmap 的容量为2的n 次幂时,n-1 的二进制才是1111***111,这样和添加的元素hash 与操作时才能更充分的散列,减少hash 碰撞,使添加的元素更均匀的分配的hashmap上。

2.如果存在,查找key 如果有值,替换原来的,并返回老的值

 
  1. if (e != null) { // existing mapping for key

  2. V oldValue = e.value;

  3. if (!onlyIfAbsent || oldValue == null)

  4. e.value = value;

  5. afterNodeAccess(e);

  6. return oldValue;

  7. }

如果没有,将新插入的节点放到尾部

 
  1. if ((e = p.next) == null) {

  2. p.next = newNode(hash, key, value, null);

  3. if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

  4. treeifyBin(tab, hash);

  5. break;

  6. }

头插法(1.7)和尾插法(1.8)的区别(面试点)
  • 头插法:从链表头部插入,多线程会产生闭环,插入慢
  • 尾插法:从链表尾部插入,不会产生闭环,插入快。

2.1 instanceof 方法会判断节点的类型是否为tree

 
  1. else if (p instanceof TreeNode)

  2. e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

  1. 链表转红黑树:因为树节点所占空间是普通节点的两倍,所以只有当节点足够多(长度大于8,泊松分布)的时候,才会使用树节点。也就是说,节点少的时候,尽管时间复杂度上,红黑树比链表好一点,但是红黑树所占空间比较大,综合考虑,认为只能在节点太多的时候,红黑树占空间大这一劣势不太明显的时候,才会舍弃链表,使用红黑树
 
  1. * Because TreeNodes are about twice the size of regular nodes, we

  2. * use them only when bins contain enough nodes to warrant use

  3. * (see TREEIFY_THRESHOLD). And when they become too small (due to

  4. * removal or resizing) they are converted back to plain bins.

 
  1. * The bin count threshold for using a tree rather than list for a

  2. * bin. Bins are converted to trees when adding an element to a

  3. * bin with at least this many nodes. The value must be greater

  4. * than 2 and should be at least 8 to mesh with assumptions in

  5. * tree removal about conversion back to plain bins upon

  6. * shrinkage.

  7. */

  8. static final int TREEIFY_THRESHOLD = 8;

3.2 转红黑树条件2 数组的长度大于64

   static final int MIN_TREEIFY_CAPACITY = 64;
 
  1. if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)

  2. resize();

  3. else if ((e = tab[index = (n - 1) & hash]) != null) {

  4. TreeNode<K,V> hd = null, tl = null;

  5. do {

  6. TreeNode<K,V> p = replacementTreeNode(e, null);

  7. if (tl == null)

  8. hd = p;

  9. else {

  10. p.prev = tl;

  11. tl.next = p;

  12. }

  13. tl = p;

  14. } while ((e = e.next) != null);

  15. if ((tab[index] = hd) != null)

  16. hd.treeify(tab);

  17. }

3.3 当长度小于6时,在转为链表

 
  1. /**

  2. * The bin count threshold for untreeifying a (split) bin during a

  3. * resize operation. Should be less than TREEIFY_THRESHOLD, and at

  4. * most 6 to mesh with shrinkage detection under removal.

  5. */

  6. static final int UNTREEIFY_THRESHOLD = 6;

扩容 resize

当数组的长度大于 默认值16* 加载因子0.75(泊松分布)时会扩容 ,最大值为2的30次幂

 
  1. if (++size > threshold)

  2. resize();

扩容完成后,新数组的长度为老数组的2倍,新数组的下标为老数组的下标+计算下标

hashmap 容量的设置(面试点)

当我们明确知道HashMap中元素的个数的时候,把默认容量设置成expectedSize / 0.75F + 1.0F 是一个在性能上相对好的选择,但是,同时也会牺牲些内存。

3.== 和 equals 比较有什么区别?

   而对于 "==" 操作符来说

  (1). 基本数据类型:比较的是两个基本数据类型的值是否相等,比如两个整型值是否相等。

  (2). 引用数据类型:比较的是两个引用对象的引用地址是否一样,比如说新建了两个对象,比较的是两个对象的引用地址是否一样。

  对于 "equals" 来说

  (1).没有重写equals方法

  Object中的equals()方法用于判断this和obj本身的值是否相等,即用来判断调用equals方法的对象和形参obj所引用的对象是否是同一对象。所谓同一对象就是指两个对象是否指向了内存中的同一块存储单元,如果this和obj指向的是同一块内存单元,则返回true;反之,则返回false。如果没有指向同一内存单元,即便是内容完全相等,也会返回false。所以 Object类中equals()方法与"=="操作符其实是等效的,都是用于比较两个对象的引用地址是否相等。

  (2)重写了equals方法

  String类中equals()方法用于比较两个字符串是否相同。如果两个字符串的地址相同,则直接返回true;如果两个字符串的地址不同,则进一步判断字符串的内容是否相同,如果字符串内容完全相同,也会返回true。

4.Java 到底是值传递还是引用传递?

    引用传递(pass by reference)是指在调用方法时将实际参数的地址直接传递到方法中,那么在方法中对参数所进行的修改,将影响到实际参数。

    值传递(pass by value)是指在调用方法时将实际参数拷贝一份传递到方法中,这样在方法中如果对参数进行修改,将不会影响到实际参数。

    Java都是“值传递”,关键看这个值是什么,简单变量就是复制了具体值,引用变量就是复制了地址。字符串是不可变的,不会被修改

    ① 当传的是基本类型时,传的是值的拷贝,对拷贝变量的修改不影响原变量;

    ② 当传的是的引用类型时,传的是引用地址的拷贝,但是拷贝的地址和真实地址指向的都是同一个真实数据,因此可以修改原变量中的值。

    一个方法中的局部变量是存在栈中的,

    ① 如果是基本类型的变量则直接存的是这个变量的值;

    ② 如果是引用类型的变量则存的是值的引用地址,指向堆中具体的对象。

5.String 的底层实现是怎样的?

   1.String类由final修饰,不可以被继承

    2.底层是由char数组实现的

    3.value用final修饰,不能修改value的引用地址(value不可变)

    4.private修饰和成员变量没有提供setter接口,保证了不可以通过外部接口来修改String的值

    5.在JDK9中,将底层的char[]数组改为了byte[]数组存储。

    原因:char类型是2字节的,使用 char[] 来表示 String 就导致了即使 String 中的字符只用一个字节就能表示,也必须占用两个字节。但是在实际使用中,只用单字节字符的频率远高于双字节字符,节省字符串的占用空间。

    仅仅将char[]数组存储修改为byte[]数组存储是远远不够的,jdk9中还配合了 Latin-1 的编码方式的编码方式( Latin-1:用单字节表示字符)。对于双字节字符使用UTF16的编码方式表示。因此在jdk9的String源码中引入了coder字段区分编码方式。

6.String,StringBuffer 和 StringBuilder 的区别?

1. String类是不可变的,每次对String对象进行修改都会创建一个新的String对象,因此在需要对字符串进行大量修改的场景下,使用String类会产生很多的开销。

2. StringBuffer和StringBuilder类是可变的,可以对其进行修改,而不会创建新的对象。

3. StringBuffer类是线程安全的,而StringBuilder类不是线程安全的。

4. 在单线程环境下,StringBuilder类的性能比StringBuffer类更高。

在实际开发中,应该根据具体的场景选择适合的字符串处理类。如果需要对字符串进行大量修改,并且在多线程环境下使用,应该使用StringBuffer类。如果在单线程环境下需要对字符串进行大量修改,应该使用StringBuilder类。如果字符串不需要修改,应该使用String类。

7.接口和抽象类有什么区别?

    相同之处:

1:都包含抽象方法,都不能被实例化

那么接口为什么不能被实例化呢?原因是:接口的定义和类很相似,分为接口声明和接口体两个部分,接口中能够定义的成员只有常量和抽象方法两种(或者两选一),不能包含构造方法,由于接口中只包含抽象方法,这就导致了它不能被实例化的原因和抽象类是相似的

2:他们的对象进行实例化操作的方法相同

无论是接口还是抽象类,虽然自身不能进行实例化操作,但由于抽象类和接口都是引用数据类型,可以声明抽象类和接口类型的引用变量,并将子类的对象实例赋给抽象类和接口变量,因此它们的对象都可利用其自身的多态性向上转型实现对接口或抽象类的实例化操作

不同之处:

1:关键字不同:

对于接口,使用interface修饰接口,它的子类使用implements关键字实现该接口,而对于抽象类,使用abstract用来修饰抽象类,它的子类使用extends关键字继承该抽象类

2:组成不同

在抽象类中可以定义常量,变量,抽象方法,普通方法,构造方法,而对于接口,在java8之前,接口中只能定义抽象方法和全局常量,java8之后,接口中可以定义静态方法,但静态方法必须有方法体。

3:权限不同

抽象类可以使用各种权限,而接口的权限只能是public

4:关系不同

一个抽象类可以实现多个接口,但接口不能继承抽象类,可以继承多个接口

5:局限性

由于java单继承的限制,一个子类只能继承一个抽象类,但一个子类可以实现多个接口

篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafc

需要全套面试笔记及答案【点击此处即可】免费获取

8.对象克隆浅拷贝和深拷贝的区别?

深拷贝(Deep Copy):

    适用场景:

        当源对象包含引用类型的属性时,如果需要复制对象及其子对象的所有属性,而不仅仅只是复制引用,就需要使用深拷贝。

        当希望修改副本对象的属性不影响原始对象时,需要使用深拷贝。

    工作原理:

        深拷贝将源对象及其关联的全部对象进行递归复制,每个对象都拥有独立的内存空间,修改副本对象不会影响原始对象。

    实现方式:

        使用递归或者拷贝构造函数来复制对象及其子对象的属性。

    示例场景:

        复制复杂对象的副本,使其成为独立的个体,例如:拷贝一个包含集合、嵌套对象等的数据结构。

        对象图的克隆,当原对象包含子对象,并且对子对象的修改不应该影响原对象时。

浅拷贝(Shallow Copy):

    适用场景:

        当源对象的属性全为基本数据类型或者不可变对象,并且不需要复制引用类型的属性时,可以使用浅拷贝。

        当希望修改副本对象的属性同时影响原始对象时,可以使用浅拷贝。

    工作原理:

        浅拷贝只复制对象及其引用,而不复制引用指向的实际对象,新旧对象将共享同一个引用对象。修改副本对象会影响原始对象。

    实现方式:

        通常使用对象的 clone() 方法来进行浅拷贝。

    示例场景:

        快速创建对象副本,以便在某些操作中对其进行修改,同时保留原始对象。

        在某些情况下,共享一部分数据以节省内存和提高性能

9.Java 反射机制的优缺点?

       什么是反射

    AVA反射机制是在运行状态中,对于任意一个类,都能够获取这个类的所有属性(属性的权限修饰符,数据类型,变量名等)和方法(方法名,返回值,参数等),构造器,运行时类实现的接口,所在的包,注解等,对于任意一个对象,都能够调用它的任意一个方法和属性,这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。本质是JVM得到class对象之后,再通过class对象进行反编译,从而获取对象的各种信息。

  反射的优缺点:

1、优点:在运行时获得类的各种内容,进行反编译,对于Java这种先编译再运行的语言,能够让我们很方便的创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代码的链接,更加容易实现面向对象。

2、缺点:

(1)反射会消耗一定的系统资源,因此,如果不需要动态地创建一个对象,那么就不需要用反射;

(2)反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。

10.Java创建对象有几种方式?

   1.使用new关键字

    这是最常见的创建对象的方式。使用 new 关键字,调用对象的构造方法来创建一个新的实例。

    2.使用Class类的newInstance()方法

    Java中的每个类都有一个名为 Class 的类对象,可以使用它的 newInstance() 方法创建类的实例。这种方式要求类必须有一个无参的构造方法。

    3.使用Constructor类的newInstance()方法

    Java的反射机制允许在运行时动态地获取类的信息并操作类。通过 Class 类的 getConstructor() 和 newInstance() 方法可以创建对象。这种方式可以使用带参的构造方法。

    4.使用clone()方法

    需要实现Cloneable,对象拷贝是创建对象的一种方式,它通过复制一个已有对象的值来创建一个新的对象。被拷贝的类需要实现 Cloneable 接口,并重写 clone() 方法,浅clone()不会调用构造方

    法。

 5 反序列化

    需要实现Serializable,反序列化是将对象从字节流(如文件、网络传输等)转换回内存中的对象的过程。在进行反序列化时,Java会使用特定的机制创建对象,并将字节流中的数据填充到对象中。

    反序列化创建对象的原理如下:

    类加载:在进行反序列化之前,Java需要加载类的定义。如果已加载的类与序列化数据中的类完全匹配,则直接使用已加载的类。否则,Java会尝试根据序列化数据中的类名去加载类定义。

    对象实例化:一旦类定义加载完成,Java使用类的构造函数创建一个新的对象实例。通常情况下,会调用类的无参构造函数来创建对象。如果类中没有无参构造函数,或者无法访问无参构造函数(如私有构造函数),则可能会导致反序列化失败。

    数据填充:在对象实例化后,Java将会将序列化数据中的字段值逐个填充到对象的对应字段中。这个过程会使用反射来访问对象的字段,并根据字段的类型和值进行填充。

    如果是文件可以使用FileInputStream和ByteArrayOutputStream的原理一样。

jvm 内存

1.JVM 内存区域分类哪些?

   (1)虚拟机栈

    (2)本机方法栈

    (3)程序计数器

    一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等功能都需要依赖这个计数器来完成。

    程序计数器是唯一一个不会出现OutOfMemoryError的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

线程共享:

    (1)堆

    堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

    从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代;再细致一点有:Eden、Survivor、Old 等空间

    创建对象时,该对象首先会在Eden区域分配空间,在一次新生代的垃圾回收之后,如果对象还存活,则会进入Survivor区的S0或者S1,并且这个对象的年龄会加1(在对象头中记录对象的年龄),当在Survivor区中的对象年龄增加到一定程度后(默认是15岁),就会被转移到老年代中。对象转移到老年代的年龄阈值可以通过参数 -XX:MaxTenuringThreshold来设置。

    (2)方法区(1.8之前叫永久代,1.8之后叫元空间)

    方法区的作用是保存一个类的信息,当我们需要使用一个类的时候,需要先加载并解析这个类的Class文件,获取类的信息,其中类的信息包括字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。

    (3)直接内存(在Java运行时内存区域之外)

2.jvm调优实战

以下是整理自网络的一些JVM调优实例:

网站流量浏览量暴增后,网站反应页面响很慢

1、问题推测:在测试环境测速度比较快,但是一到生产就变慢,所以推测可能是因为垃圾收集导致的业务线程停顿。

2、定位:为了确认推测的正确性,在线上通过jstat -gc 指令 看到JVM进行GC 次数频率非常高,GC所占用的时间非常长,所以基本推断就是因为GC频率非常高,所以导致业务线程经常停顿,从而造成网页反应很慢。

3、解决方案:因为网页访问量很高,所以对象创建速度非常快,导致堆内存容易填满从而频繁GC,所以这里问题在于新生代内存太小,所以这里可以增加JVM内存就行了,所以初步从原来的2G内存增加到16G内存。

4、第二个问题:增加内存后的确平常的请求比较快了,但是又出现了另外一个问题,就是不定期的会间断性的卡顿,而且单次卡顿的时间要比之前要长很多。

5、问题推测:练习到是之前的优化加大了内存,所以推测可能是因为内存加大了,从而导致单次GC的时间变长从而导致间接性的卡顿。

6、定位:还是通过jstat -gc 指令 查看到 的确FGC次数并不是很高,但是花费在FGC上的时间是非常高的,根据GC日志 查看到单次FGC的时间有达到几十秒的。

7、解决方案: 因为JVM默认使用的是PS+PO的组合,PS+PO垃圾标记和收集阶段都是STW,所以内存加大了之后,需要进行垃圾回收的时间就变长了,所以这里要想避免单次GC时间过长,所以需要更换并发类的收集器,因为当前的JDK版本为1.7,所以最后选择CMS垃圾收集器,根据之前垃圾收集情况设置了一个预期的停顿的时间,上线后网站再也没有了卡顿问题。

后台导出数据引发的OOM

**问题描述:**公司的后台系统,偶发性的引发OOM异常,堆内存溢出。

1、因为是偶发性的,所以第一次简单的认为就是堆内存不足导致,所以单方面的加大了堆内存从4G调整到8G。

2、但是问题依然没有解决,只能从堆内存信息下手,通过开启了-XX:+HeapDumpOnOutOfMemoryError参数 获得堆内存的dump文件。

3、VisualVM 对 堆dump文件进行分析,通过VisualVM查看到占用内存最大的对象是String对象,本来想跟踪着String对象找到其引用的地方,但dump文件太大,跟踪进去的时候总是卡死,而String对象占用比较多也比较正常,最开始也没有认定就是这里的问题,于是就从线程信息里面找突破点。

4、通过线程进行分析,先找到了几个正在运行的业务线程,然后逐一跟进业务线程看了下代码,发现有个引起我注意的方法,导出订单信息。

5、因为订单信息导出这个方法可能会有几万的数据量,首先要从数据库里面查询出来订单信息,然后把订单信息生成excel,这个过程会产生大量的String对象。

6、为了验证自己的猜想,于是准备登录后台去测试下,结果在测试的过程中发现到处订单的按钮前端居然没有做点击后按钮置灰交互事件,结果按钮可以一直点,因为导出订单数据本来就非常慢,使用的人员可能发现点击后很久后页面都没反应,结果就一直点,结果就大量的请求进入到后台,堆内存产生了大量的订单对象和EXCEL对象,而且方法执行非常慢,导致这一段时间内这些对象都无法被回收,所以最终导致内存溢出。

7、知道了问题就容易解决了,最终没有调整任何JVM参数,只是在前端的导出订单按钮上加上了置灰状态,等后端响应之后按钮才可以进行点击,然后减少了查询订单信息的非必要字段来减少生成对象的体积,然后问题就解决了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值