JAVA面经_自用
基础
基本概念
基本数据类型:8种
自动拆装箱
Java 语言是一个面向对象的语言,但是 Java 中的基本数据类型却是不面向对象的,这在实际使用时存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八个和基本数据类型对应的类统称为包装类(Wrapper Class)。使得基本数据类型也具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
- 自动装箱:就是将基本数据类型自动转换成对应的包装类。
- 自动拆箱:就是将包装类自动转换成对应的基本数据类型。
原理
通过代码反编译可以发现,自动装箱都是通过包装类的 valueOf()
方法来实现的。而自动拆箱都是通过包装类对象的 xxxValue()
来实现的。
使用场景
1)基本数据类型放入集合类;
2)包装类型和基本类型的大小比较;
3)包装类型的运算符计算;
4)函数参数与返回值。
基本数据类型的缓冲池
整型 Integer 对象通过使用相同的对象引用实现了缓存和重用。
即当需要进行自动装箱时,在 -128 至 127 之间的整型数字会直接使用缓存中的对象,而不是重新创建一个新对象。
抽象类和接口的异同
同:不能实例化; 都包含抽象方法; 都可以用default定义默认实现
异:
接口 | 抽象类 |
---|---|
接口是对类行为的约束和规范。 | 抽象类关注于代码复用 |
类可以实现多个接口 | 类只能继承一个类 |
成员变量只能是public static final(即不可变) | 抽象类的成员变量默认是default,可以在子类中被另外赋值 |
方法全是抽象方法 | 方法可以不全是抽象方法 |
四种引用类型
强引用(Strong Reference):
是 Java 中默认声明的引用类型,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足时, JVM 也会直接抛出 OutOfMemoryError 而不会去回收。
软引用(Soft Reference):
软引用是用来描述一些非必需但仍有用的对象。在内存足够的时候,软引用对象不会被回收,只有在内存不足时,系统则会回收软引用对象,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。 这种特性常常被用来实现缓存技术,比如网页缓存,图片缓存等。
弱引用(Weak Reference):
弱引用的引用强度比软引用要更弱一些,无论内存是否足够,只要 JVM 开始进行垃圾回收,那些被弱引用关联的对象都会被回收。 使用 java.lang.ref.WeakReference 来表示弱引用。
虚引用(Phantom Reference):
虚引用是最弱的一种引用关系,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,它随时可能会被回收
类初始化顺序
1)父类–静态变量/静态初始化块(按代码顺序)
2)子类–静态变量/静态初始化块;
3)父类–变量/初始化块;
4)父类–构造器;
5)子类–变量/初始化块;
6)子类–构造器。
权限修饰符
- public:共有访问,对所有的类都可见。
- protected:保护型访问,对同一个包可见,对不同的包的子类可见。
- default:默认访问权限,只对同一个包可见,注意对不同的包的子类不可见。
- private:私有访问,只对同一个类可见,其余都不见
static,用来创建类方法和类变量。
final,用来修饰类、方法和变量,final 修饰的类不能够被继承,修饰的方法不能被继承类重新定义,修饰的变量为常量,是不可修改的。
abstract,用来创建抽象类和抽象方法。
synchronized 用于多线程的同步。
volatile 修饰的成员变量在每次被线程访问时,都强制从共享内存中重新读取该成员变量的值。而且,当成员变量发生变化时,会强制线程将变化值回写到共享内存。
transient:序列化的对象包含被 transient 修饰的实例变量时,java 虚拟机(JVM)跳过该特定的变量。
Switch 支持哪些数据类型
整型类型,包括 byte、short、char 以及 int。在 JDK 1.5 和 1.7 又分别增加了对枚举类型和 String 的支持
静态方法与实例方法
静态方法不能调用非静态成员:
静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。
面向对象
面向对象和面向过程
- 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
- 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。
面向对象开发的程序一般更易维护、易复用、易扩展。
三大特征
封装
封装就是把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法。
继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
多态
多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
java如何实现多态
两种形式可以使用多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
方法重载(overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override)实现的是运行时的多态性(也称为后绑定)。
重写和重载的区别
重写(override):一般都是表示子类和父类之间的关系,其主要的特征是方法名相同,参数相同,但是具体的实现不同。
重载(Overload):首先是位于一个类之中或者其子类中,具有相同的方法名,但是方法的参数不同,返回值类型可以相同也可以不同。
主要区别如下:
1)重写发生在子类继承或接口实现类中,重载只发生在本类中;
2)重写的访问修饰符不能比父类中被重写的方法的访问权限更低,重载可以修改;
3)重写的异常可以减少或删除,但不能扩展,重载可以修改。
深拷贝、浅拷贝和引用拷贝
浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
引用拷贝:就是两个不同的引用指向同一个对象
常用类
1. equal 和 ==
2. String、StringBuilder、StringBuffer
StringBuilder线程不安全,已被淘汰。
StringBuffer线程安全(对方法加了同步锁或者对调用的方法加了同步锁)。
其他重要概念
异常
Java 异常是 Java 提供的一种识别及响应错误的一致性机制。
Java 异常机制可以使程序中异常处理代码和正常业务代码分离,保证程序代码更加优雅,并提高程序健壮性。
1. Error和Exception
Exception
:程序本身可以处理的异常,可以通过 catch
来进行捕获。Exception
又可以分为 Checked Exception (受检查异常,必须处理) 和 Unchecked Exception (不受检查异常,可以不处理)。
Error
:Error
属于程序无法处理的错误 ,我们没办法通过 catch
来进行捕获不建议通过catch
捕获 。例如 Java 虚拟机运行错误(Virtual MachineError
)、虚拟机内存不够错误(OutOfMemoryError
)、类定义错误(NoClassDefFoundError
)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
2. 受检异常与非受检异常
受检查异常:
编译器要求必须处理的异常。正确的程序在运行过程中,经常容易出现的、符合预期的异常情况。一旦发生此类异常,就必须采用某种方式进行处理。除 RuntimeException 及其子类外,其他的 Exception 异常都属于受检异常。编译器会检查此类异常,也就是说当编译器检查到应用中的某处可能会此类异常时,将会提示你处理本异常——要么使用try-catch捕获,要么使用方法签名中用 throws 关键字抛出,否则编译不通过。
非受检查异常:
编译器不会进行检查并且不要求必须处理的异常,也就说当程序中出现此类异常时,即使我们没有 try-catch 捕获它,也没有使用 throws 抛出该异常,编译也会正常通过。该类异常包括运行时异常(RuntimeException 及其子类)和错误(Error)。
3. Java 如何处理异常
在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
泛型
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList<Person> persons = new ArrayList<Person>()
这行代码就指明了该 ArrayList
对象只能传入 Person
对象,如果传入其他类型的对象就会报错。
借助通配符
反射
框架的灵魂。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
动态代理的实现也依赖反射
注解的实现也用到了反射
注解
注解只有被解析之后才会生效,常见的解析方法有两种:
- 编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 - 运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的
@Value
、@Component
)都是通过反射来进行处理的。
集合
概述
集合的作用
集合是用于存放对象的容器,而集合类是 Java 的一种数据结构
好处:
- 提供高效的的数据结构和算法,挺高程序运行效率;
- 提供通用 API 能力,降低开发和维护成本;
数组和集合的区别?
- 数组固定长度,集合成都可变;
- 数组可以存放基本数据类型,集合只能存储引用数据类型;
- 数组存储的元素必须是同一个数据类型,集合支持存储不同数据类型。
可以存储 NULL的集合
1)List 接口 ArrayList、LinkedList 以及 Vector 等都可以存储多个 null;
2)Set 接口中 HashSet、LinkedSet 可以存储一个 null,TreeSet 不能存储 null;
3)Map 接口中 HashMap、LinkedHashMa 的 key 与 value 均可以为 null。Treemap 的 key 不可以为 null,value 可以为 null。HashTable、ConcurrentHashMap 的 key 与 value 均不能为 null。
List
ArrayList 和 Vector 的区别
同步性不同
Vector 是线程安全的,它的方法之间都加了 synchronized 关键字修饰,而 ArrayList 是非线程安全的。因此在不考虑线程安全情况下使用 ArrayList 的效率更高。
数据增长不同
ArrayList 和 Vector 都有一个初始的容量大小,并在必要时对数组进行扩容。但 Vector 每次增长两倍,而 ArrayList 增长 1.5 倍。
Iterator 和 ListIterator 的区别
ListIterator 是一个继承于 Iterator 接口,功能更加强大的迭代器,但只能用于各种 List 类型的访问。
- Iterator 支持 List 和 Set,而 ListIterator 只支持 List;
- Iterator 只能单向访问,而 ListIterator 可以双向前后遍历;
- ListIterator 继承了 Iterator 的所有方法,并新增了如添加 add()、替换 set() 等更多好用方法。
Fail-Fast 和 Fail-Safe
对于线程不安全的类,并发情况下可能会出现快速失败(fail-fast) 情况,而线程安全的类,可能出现安全失败(fail-safe) 的情况。
这里需要注意的是,以上情况都是针对并发而言。
fail-fast 机制
当遍历一个集合对象时,如果集合对象的结构被修改了,就会抛出 ConcurrentModificationExcetion 异常。但需要注意的是,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器则会尽最大努力抛出 ConcurrentModificationException。
fail-safe 机制
Fail-Safe 机制主要是针对线程安全的集合类(如 ConcurrentHashMap),其的出现的目的是为了解决 fail-fast 抛出异常处理起来不方便的问题。
并发容器的 iterator() 方法返回的迭代器内部都是保存了该集合对象的一个快照副本,并且没有 modCount 等数值做检查。这也造成了并发容器迭代器读取的数据是某个时间点的快照版本。你可以并发读取,不会抛出异常,但无法保证遍历读取的值和当前集合对象的状态是一致的。 同时创建集合拷贝还需要时间和空间上的额外开销。
ArrayList 添加元素的流程或扩容机制
Set
Map
常用的 Hash 函数
除留余数法
也是 HashMap 中使用的方法,计算公式为 H(key) = key % p (p<=n),关键字除以一个不大于哈希表长度的正整数 p,所得余数为地址。
直接定址法
直接根据 key 映射到对应哈希表位置,如 i 就直接放到下标 i 的位置。
数字分析法
根据 key 的某些数字(如十位或者百位)作为映射的位置。
平方取中法
根据 key 平方的中间几位作为映射的位置。
折叠法
把 key 分割成位数相同的几段,然后将其叠加和作为映射的位置。
ConcurrentHashMap的Key 和 Value 不支持 null 的原因
因为 ConcurrentHashMap 是用于多线程的,如果 get(key) 方法到了 null,就无法判断存在的 value 是 null,还是没有找到对应的 key 而为 null,就有了二义性。
TreeMap 如何实现有序
TreeMap 是按照 Key 的自然顺序或者 Comprator 的顺序进行排序,内部是通过红黑树来实现。所以要么 key 所属的类实现 Comparable 接口,或者自定义一个实现了 Comparator 接口的比较器,传给 TreeMap 用于 key 的比较。
JVM
并发
树
1. 二叉搜索树
必要条件:
- 左子树节点(值) < 根节点(值) < 右子树节点(值)
- 没有值相等的
节点的前驱: 是该节点的左子树中的最大节点。 节点的后继: 是该节点的右子树中的最小节点。
2. 平衡二叉树
左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
实现:红黑树、AVL、替罪羊树、Treap、伸展树等
即用插入花费来换取查询效率
3. 红黑树(R-B)
性质:
- 每个结点要么是红的要么是黑的。(红或黑)
- 根结点是黑的。 (根黑);每个叶结点(叶结点即指树尾端NIL指针或NULL结点)都是黑的。 (叶黑)
- 如果一个结点是红的,那么它的两个儿子都是黑的。 (红不连续)
- 对于任意结点而言,其到叶结点树尾端NIL指针的每条路径都包含相同数目的黑结点。(路径下黑相同)
- 红黑树是一种近似平衡的二叉查找树,它能够确保任何一个节点的左右子树的高度差不会超过二者中较低那个的一倍。
平衡二叉树和AVL树的折中
红黑树与AVL树的比较:
- AVL树时间复杂度优,但现在计算机cpu太快,可忽略性能差异
- 红黑树插入删除比AVL树更便于控制操作
- 红黑树整体性能略优于AVL树(红黑树旋转情况少于AVL树)
重要概念
节点后继(树中大于t的最小元素):
- t的右子树不空,则t的后继是其右子树中最小的那个元素。
- t的右孩子为空,则t的后继是其第一个向左走的祖先。
增:
删:
4. 哈夫曼树
带权路径长度最短的二叉树,需要忽略非叶子节点
eg:
带权路径长度
: 从根节点到某个节点之间的路径长度与该节点的权的乘积。例如上图节点10的路径长度为3,它的带权路径长度为10 * 3 = 30;
树的带权路径长度
: 树的带权路径长度为所有叶子节点的带权路径长度之和,称为WPL。上图的WPL = 1x40+2x30+3x10+3x20 = 190,而哈夫曼树就是树的带权路径最小的二叉树。
5. B树
自平衡的树,能够保持数据有序。这种数据结构能够让查找数据、顺序访问、插入数据及删除的动作,都在对数时间内完成。
- 每个节点最多只有m个子节点。
- 每个非叶子节点(除了根)具有至少⌈ m/2⌉子节点。
- 如果根不是叶节点,则根至少有两个子节点。
- 如果一个结点有n-1个关键字,则该结点有n个分支,且这n-1个关键字按照递增顺序排列。
- 每个非终端结点中包含信息:(N,A0,K1,A1,K2,A2,...,KN,AN)其中:
- Ki(1≤i≤n)为关键字,且关键字按升序排序。
- 指针Ai(0≤i≤n)指向子树的根结点,Ai-1指向子树中所有结点的关键字均小于Ki,且大于Ki-1;
- 关键字的个数n必须满足:⌈m/2⌉-1≤n≤m-1。
- 结点内关键字各不相等且按从小到大排列。
- 所有的叶子节点都在同一层,子叶结点不包含任何信息。叶子结点处于同一层,可以用空指针表示,是查找失败到达的位置。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!