admin管理员组文章数量:1627760
Java面试题整理
Java基础
Java是解释性还是编译性语言?
既是编译性语言(需要由编译器编译为.class字节码文件),又是解释性语言(需要由JVM读一行执行一行,由解释器解释为操作系统能执行的命令)
Java的编译器是javac.exe,解释器是java.exe
为什么引入Hash?好处是什么?
简称散列算法,是将一个大文件映射成一个小串字符。与指纹一样,就是以较短的信息来保证文件的唯一性的标志,这种标志与文件的每一个字节都相关,而且难以找到逆向规律。
好处:
1) 在庞大的数据库中,由于哈希值更为短小,被找到更为容易,因此,哈希使数据的存储与查询速度更快。
2) 哈希能对信息进行加密处理,使得数据传播更为安全。
什么是动态代理?
动态代理就是,在程序运行期,创建目标对象的代理对象,并对目标对象中的方法进行功能性增强的一种技术。在生成代理对象的过程中,目标对象不变,代理对象中的方法是目标对象方法的增强方法。可以理解为运行期间,对象中方法的动态拦截,在拦截方法的前后执行功能操作。
什么是java的反射机制?
反射是动态获取信息以及动态调用对象方法的一种机制。
Java反射就是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;并且能改变它的属性。而这也是Java被视为动态语言的一个关键性质。
Java反射的功能是在运行时判断任意一个对象所属的类,在运行时构造任意一个类的对象,在运行时判断任意一个类所具有的成员变量和方法,在运行时调用任意一个对象的方法,生成动态代理。
final
一、final修饰类
final修饰一个类的时候,这个类不能被继承,final类中的方法都会被隐式的指定为final方法,JDK中被设计为final类的有String、System等。
二、final修饰方法
被final修饰的方法不能被重写,可以被重载,一个类的private方***隐式的被指定为final方法。
三、final修饰成员变量
被final修饰的成员变量必须要赋初始值,而且只能初始化一次,可以直接赋值或在构造方法中赋初值。如果final修饰的成员变量是基本类型,则表示这个变量的值不能改变,如果修饰的成员变量是一个引用类型,则引用的地址不能改变,但是这个引用所指向的对象里面的内容可以改变。
static修饰的方法可以被重写吗?重载呢?
不可以重写。
当我们在子类中改变方法体时,子类的该方法只是将父类的方法进行了隐藏,而非重写,这两个方法没有关系。父类引用指向子类对象时,只会调用父类的静态方法,所以不具有多态性。
可以重载。
Java的异常体系及异常的捕获和处理?
一、Java的异常体系
Throwable(表示可抛出)是所有异常和错误的超类,两个直接子类为Error和Exception,分别表示错误和异常。异常又可分为运行时异常(不检查异常)和非运行时异常(检查异常)。
1、Error和Exception:
Error是程序无法处理的错误,是由JVM产生和抛出的,比如OutOfMemoryError、ThreadDeath等。Error发生时JVM会选择线程终止。
Exception是程序可以处理的异常,程序中应当尽可能去处理这些异常。
2、运行时异常和非运行时异常:
运行时异常都是RuntimeException类及其子类异常,如NullPointerException、IndexOutOfBoundsException等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
非运行时异常是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常。
二、异常的捕获和处理
1、try catch finally
1)可以组成try...catch...finally、try...catch、try...finally三种结构,catch可以有一个或多个,finally最多一个。
2)try、catch、finally三个代码块中变量的作用域为代码块内部,分别独立而不能相互访问。如果要在三个块中都可以访问,则需要将变量定义到这些块的外面。
3)多个catch块时候,最多只会匹配其中一个异常类且只会执行该catch块代码,而不会再执行其它的catch块,且匹配catch语句的顺序为从上到下,也可能所有的catch都没执行
2、throw和throws
throw关键字用于方法体内部,用来抛出一个Throwable类型的异常。如果抛出了检查异常,则应该在方法头部声明方法可能抛出的异常类型,该方法的调用者必须处理或继续抛出异常。如果所有方法都层层上抛获取的异常,最终JVM会进行处理,处理方式就是打印异常消息和堆栈信息。
throws关键字用于方法体外部的方法声明部分,用来声明方法可能会抛出某些异常。仅当抛出了检查异常,该方法的调用者才必须处理或者重新抛出该异常。当方法的调用者无力处理该异常的时候,应该继续抛出。
Java的方法分派?
方法分派指的是虚拟机如何确定应该执行哪个方法。
静态分派(方法重载):编译器确定,根据调用者的声明类型和方法参数类型。
动态分派(方法重写):运行时确定,根据调用者的实际类型分派。
Serializable接口中serialVersionUID的作用?
在序列化的时候系统将serialVersionUID写入到序列化的文件中去,当反序列化的时候系统会先去检测文件中的serialVersionUID是否跟当前类的serialVersionUID是否一致,如果一致则反序列化成功,否则就说明当前类跟序列化后的类发生了变化,比如是成员变量的数量或者是类型发生了变化,那么在反序列化时就会发生crash,并且报错。
JVM
引用的几种方式?
- 强引用:强引用是在程序代码之中普遍存在的引用赋值,类似
Object o = new Object()
这种引用关系。无论任何情况下,只要强引用关系还在,垃圾收集器就永远不会回收掉被引用的对象。 - 软引用:用来描述一些还有用但非必须的对象。只被软引用关联的对象,在系统要发生内存溢出异常前,会把这些对象列进回收范围之中进行二次回收,如果这次回收还是没有足够的内存,才会抛出溢出异常。
应用场景:做缓存(浏览器的后退按钮) - 弱引用:也是用来描述那些非必须对象,但它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。
- 虚引用:最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用的唯一目的只是为了在这个对象被收集器回收时收到一个系统通知。
minorGC和MajorGC分别发生在什么时候?
minorGC:
1)Eden区满了 2)新创建对象的大小大于Eden所剩余空间
majorGC:
1)每次晋升到老年代的对象平均大小超过了老年代剩余空间
2)minorGC后存活的对象超过了老年代剩余空间
minor GC和major GC的过程?
minor GC:在GC开始的时候,对象只会存在于Eden区和名为“From”的Survivor区,Survivor区“To”是空的。紧接着进行GC,Eden区中所有存活的对象都会被复制到“To”,而在“From”区中,仍存活的对象会根据他们的年龄值来决定去向。年龄达到一定值(年龄阈值,可以通过-XX:MaxTenuringThreshold来设置)的对象会被移动到年老代中,没有达到阈值的对象会被复制到“To”区域。经过这次GC后,Eden区和From区已经被清空。这个时候,“From”和“To”会交换他们的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎样,都会保证名为To的Survivor区域是空的。Minor GC会一直重复这样的过程,直到“To”区被填满,“To”区被填满之后,会将所有对象移动到年老代中。
major GC:参考CMS的工作过程。
垃圾收集算法及各自的优缺点?
1、标记-清除算法
“标记-清除”算法是最基础的算法,分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
缺点:
- 执行效率不稳定;标记和清除两个过程的执行效率随对象数量增长而降低。
- 内存空间的碎片化问题;标记、清除之后会产生大量不连续的内存碎片,导致当需要分配较大对象时无法找到足够的连续空间而不得不提前触发另一次垃圾收集动作。
2、标记-复制算法(针对新生代)
标记-复制算法将可用内存按容量划分为大小相等的两块,每次使用其中的一块。当这块的内存用完了,就将还存活着的对象复制到另一块上面,然后再把已使用过的内存空间一次清理掉。
优点:
- 分配内存时不用考虑空间碎片的复杂情况,只要移动堆顶指针,按顺序分配即可。
缺点:
- 将可用内存缩小为了原来的一半,空间浪费多。
3、标记-整理算法(针对老年代)
复制算法在对象存活率较高时就需要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用复制算法。
根据老年代的特点提出了“标记-整理”算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向空间一端移动,然后直接清理掉边界以外的内存。
标记-清除算法与标记-整理算法的本质差异在于前者是一种非移动式的回收算法,而后者是移动式的。是否移动对象都存在弊端,移动对象操作必须全程暂停用户应用程序才能进行("Stop The World"),不移动对象会影响应用程序的吞吐量。
CMS收集器是怎样的?
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,基于标记-清除算法实现,是一款老年代收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。
整个工作流程包括四个步骤:
- 初始标记:仅仅标记GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
- 并发标记:从GC Roots的直接关联对象开始遍历整个对象图的过程,耗时较长但不需要停顿用户线程,可以与垃圾收集线程一起并发运行。
- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要“Stop The World”。
- 并发清除:清理删除标记阶段判断的已经死亡的对象,不需要移动存活对象,可以与用户线程同时并发。
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
优点:并发收集、停顿低
缺点:
- 对CPU资源敏感,总吞吐量会降低
- 无法处理浮动垃圾
- 标记-清除算法导致空间碎片
什么情况下会出现OOM?
OOM--OutOfMemoryError,当JVM因为没有足够的内存来为对象分配空间,并且垃圾回收器也已经没有空间可回收时,就会抛出这个error。
一、原因:
1、为虚拟机分配的内存太少
2、应用用的太多,并且用完没释放,此时就会造成内存泄露或者内存溢出
- 内存泄露:申请的内存在被使用完后没有释放,导致虚拟机不能再次使用该内存
- 内存溢出:申请的内存超出了JVM能提供的内存大小
大量的内存泄露可能会导致内存溢出。
二、出现OOM时的分析方法
Java堆内存的OOM异常是实际应用中常见的内存溢出异常情况,要解决这个区域的异常,一般的手段是先通过内存映像分析工具(如Eclipse Memory Analyzer)对Dump出来的堆转储快照进行分析,重点是确认内存中的对象是否是必要的,也就是要先分清除到底是出现了内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。
三、OOM的解决方法
1、如果是内存泄漏,可进一步通过工具查看泄漏对象到GC Roots的引用链。于是就能找到泄露对象是通过怎样的路径与GC Roots相关联并导致垃圾收集器无法自动回收他们的。掌握了泄漏对象的类型信息及GC Roots引用链的信息,就可以比较准确的定位出泄漏代码的位置。
2、如果不存在泄漏,换句话说,就是内存中的对象确实还必须存活着,那就应当检查虚拟机的堆参数(-Xmx与-Xms),与机器物理内存对比看是否还有向上调整的空间。再从代码上检查是否存在某些对象生命周期过长、持有状态时间过长、存储结构设计不合理等情况,尽量减少程序运行期的内存消耗。
Java类加载机制
一、类加载的时机
1、隐式加载:new创建类的实例
2、显示加载:loaderClass、forName
forName和loaderClass区别?
- Class.forName()得到的class是已经初始化完成的
- ClassLoader.loadClass()得到的class是还没有链接的
3、访问类的静态变量,或者为静态变量赋值
4、调用类的静态方法
5、使用反射创建某个类或者接口的Class对象
6、初始化某个类的子类
7、直接使用java.exe命令来运行某个类
二、类加载的过程
当需要某个类的时候,jvm会加载.class文件,并创建对应的class对象,将class文件加载到虚拟机的内存,这个过程被称为类的加载。
- 加载:ClassLoader通过全类名查找类的字节码文件并创建一个class对象
- 链接
- 验证
- 准备:为类变量(static修饰)分配内存并赋初始值
- static int i = 5这里只是将i赋值为0,初始化的阶段再把i赋值为5
- 不包含final修饰的static,因为final在编译的时候就已经分配了
- 解析
- 初始化:如果该类有父类就对父类进行初始化
三、双亲委派模式
1、原理:
双亲委派模式要求除了顶层的启动类加载器之外,其余的类加载器都应该有自己的父类加载器,但是在双亲委派模式中父子关系采取的并不是继承的关系而是组合来复用父类加载器的相关代码。
如果一个类收到了类加载的请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行,如果父类加载器还有父类加载器,则进一步向上委托,依次递归,请求最后到达顶层的启动类加载器,如果父类能够完成类的加载任务,就会成功返回,如果父类加载器无法完成任务,子类加载器才会尝试自己去加载。
通俗理解:每个儿子都很懒,遇到类加载的活都给爸爸干,直到爸爸说我也做不来的时候,儿子才会想办法自己去加载。
2、优点:
- 避免类的重复加载:父类加载器已经加载该类后子类加载器就没必要再加载一次
- 安全性:防止核心API库被篡改
四、如何破坏双亲委派机制?
自定义的类加载器重写loadClass()方法,如果不想破坏双亲委派,那么重写findClass()方法。
五、NoClassDefFoundError 和 ClassNotFoundException 有什么区别?
1、ClassNotFoundException:当应用程序运行的过程中尝试使用类加载器去加载Class文件的时候,如果没有在classpath中查找到指定的类,就会抛出ClassNotFoundException。一般情况下,当我们使用Class.forName()或者ClassLoader.loadClass()以及使用ClassLoader.findSystemClass()在运行时加载类的时候,如果类没有被找到,那么就会导致JVM抛出ClassNotFoundException。
2、NoClassDefFoundError:当JVM在加载一个类的时候,如果这个类在编译时是可用的,但是在运行时找不到这个类的定义的时候,JVM就会抛出一个NoClassDefFoundError错误。比如当我们在new一个类的实例的时候,如果在运行时类找不到,则会抛出一个NoClassDefFoundError的错误。
Java对象的创建过程
1、分配内存
2、初始化
-
实例变量初始化和实例代码块初始化:
如果对实例变量赋值或者使用实例代码块赋值,那么编译器会将其中的代码放到类的构造函数中去,并且这些代码会被放在对超类构造函数的调用语句之后,构造函数本身的代码之前。
-
构造函数初始化:
Java要求在实例化类之前,必须先实例化其超类,以保证所创建实例的完整性。
Java内存分区?
1、程序计数器:执行字节码的行号指示器,线程私有,没有OOM
2、Java虚拟机栈:存局部变量表、栈帧,线程私有
3、本地方法栈
4、Java堆:GC堆,存放对象实例,所有线程共享
5、方法区:常量池存在方法区,所有线程共享
数据结构与算法
LinkedList和ArrayList的区别?
1、数据结构不同
ArrayList是基于数组的数据结构,LinkedList是基于链表的数据结构
2、效率不同
当随机访问List(get和set操作)时,ArrayList比LinkedList的效率更高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
当对数据进行增加和删除的操作(add和remove操作)时,LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动。
3、自由性不同
ArrayList自由性较低,因为它需要手动的设置固定大小的容量(默认初始容量为10),但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;
LinkedList自由性较高,能够动态的随数据量的变化而变化,但是它不便于使用。
4、主要控件开销不同
ArrayList主要控件开销在于需要在List列表预留一定空间;
LinkList主要控件开销在于需要存储结点信息以及结点指针信息。
HashMap和Hashtable的区别?
1、Hashtable出现的时间早(JDK1.0),HashMap出现的时间晚(JDK1.2)
2、Hashtable继承Dictionary类(已废弃),HashMap实现Map接口
3、Hashtable线程安全但效率低,HashMap非线程安全但效率高
4、HashMap可以存储null键和null值,Hashtable不可以存储null键和null值
B树和B+树的区别?
1、B树的每个结点都存储了key和data,B+树的data存储在叶子节点上,节点不存储data,这样一个节点就可以存储更多的key。可以使得树更矮,所以IO操作次数更少。
2、B+树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录,由于数据顺序排列并且相连,所以便于区间查找和搜索。而B树则需要进行每一层的递归遍历。相邻的元素可能在内存中不相邻,所以缓存命中性没有B+树好。
JDK1.8中HashMap的get()和put()方法如何实现的?
put方法:
- 判断当前桶是否为空,为空则需要初始化(resize可以初始化桶数组或者进行扩容)
- 根据当前key的hashcode定位到具体的桶中并判断是否为空,为空表明没有Hash冲突就直接在当前位置创建一个桶即可
- 如果当前桶有值(Hash冲突),那么就要比较当前桶中的key、key的hashcode与写入的key是否相等,相等就赋值给e,之后会统一进行赋值及返回
- 如果当前桶为红黑树,那就按照红黑树的方式写入数据
- 如果是链表,就需要将当前的key、value封装成一个新节点写入到当前桶的后面(形成链表)
- 接着判断当前链表的大小是否大于阈值,大于时要转换为红黑树
- 如果在遍历过程中找到key和hashcode均相同时直接退出遍历
- 如果e不为空则存在相同的key和hashcode,就需要将值覆盖
- 最后判断是否需要进行扩容
get方法:
- 将key hash之后取得所定位的桶
- 如果桶为空则直接返回null
- 否则判断桶的第一个位置(可能是链表或红黑树)的key和hashcode是不是要查找的,如果是返回该node
- 如果第一个node不匹配,则判断它的下一个是红黑树还是链表
- 红黑树就按照树的查找方式返回值
- 否则就按照链表的方式遍历匹配返回值
HashMap为什么非线程安全?
HashMap在多线程情况下,在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发扩容操作,就是rehash,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,同时存在其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的
JDK1.8对hash算法和寻址算法进行了哪些优化?
1、hash算法:JDK1.7中通过key.hashcode()得到关键字的hash值&#x
版权声明:本文标题:2022Java后端实习春招面试题整理(含答案) 备战秋招 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/dianzi/1728998392a1182370.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论