目录
8.假设引用了一个第三方的jar有个类和自己写的代码类一样,那么在类加载机制过程中是如何处理的?
9.final、finnally、finalize的区别是什么?
25.请你说说String、StringBuffer、StringBuilder区别及使用场景?
27.请你说说为什么sb.append("str")比s+="str"效率更高?
[28.请你说说String str="hello world"和String str=new String("hello world")的区别?](#28.请你说说String str="hello world"和String str=new String("hello world")的区别?)
29.请你说说StringBuffer和StringBuilder的区别是什么?性能对比?如何鉴定线程安全?
30.请你说说StringBuffer和StringBuilder底层怎么实现的?
37.ArrayList和LinkedList的区别是什么?
Java基础
1.请你说说Java中基本数据类型的bit长度?
在Java中,基本数据类型的位长度是固定的。具体来说,byte类型占8位,short类型占16位,int类型占32位,long类型占64位,char类型也是16位。对于浮点数,float类型占32位,而double类型占64位。布尔类型boolean的位长度没有明确指定,但它通常用于表示true或false,并且在内存中通常以整型(如int)的位数来存储。
2.switch支持哪些数据类型?支持long么?
switch语句支持的数据类型包括整型数据和枚举类型。例如整型数据类型包括byte、short、int和char。
自Java 5起,switch 语句也支持枚举类型。对于long类型数据,switch语句是不支持的。如果需要在switch语句中使用long类型数据,可以考虑将其转换为int或byte类型,或者使用一系列的if-else 语句来实现相同的逻辑。
3.讲一下常见编码方式?
ASCII编码是最基础的单字节编码方式,它能够表示英文字母、数字和一些特殊符号,共128个字符。
GBK是针对中文字符设计的双字节编码,一个中文占2个字节。
UTF-8是一种可变长度的Unicode编码,它使用1到4个字节表示一个字符,一个中文占3个字节,对英文来说效率很高,同时也能很好地支持多语言,是目前互联网上最常用的编码方式。
UTF-16则是使用2或4个字节表示一个字符的编码,它可以覆盖所有的Unicode字符,但在处理英文文本时可能不如UTF-8高效。在Java开发中,我们通常推荐使用UTF-8编码,因为它具有更好的国际化和兼容性。
4.char能不能存储中文?
可以存储中文,因为Java使用的是Unicode字符集,而中文字符在Unicode中有对应的编码。每个char类型变量占用16位,足以表示大多数中文字符。但是,需要注意的是,char只能存储单个Unicode码点,对于一些特殊的中文字符,如果它们超出了基本多文种平面的范围,可能需要使用两个char值来表示
5.为什么数组索引从0开始呢?假如从1开始不行吗?
首先,在C语言中,数组索引从0开始,而Java在设计时沿用了这一习惯。
其次,将数组索引从0开始可以使得数组的地址计算更加高效,因为数组元素的地址可以通过基地址加上索引乘以元素大小直接计算得到,无需额外减1操作。如果从1开始,每次计算地址时都需要额外减去1,这样会增加计算的开销。
6.请你说说JDK、JRE和JVM的概念。
JDK是Java开发工具包,提供了编译、运行Java程序所需的所有工具和库;JRE是Java运行时环境,包含了运行Java程序所必需的JVM和库文件,但不包含开发工具;JVM是Java虚拟机,是运行所有Java程序的抽象计算机,它实现了Java程序的跨平台运行能力。
简而言之,JDK包含了JRE,JRE包含了JVM,开发者需要JDK来开发Java程序,用户只需要JRE来运行Java程序。
7.Lambda表达式相比JDK7的处理,它优化了什么?
Lambda表达式是JDK8引入的一种新特性,它允许我们以更简洁的方式表示匿名内部类,尤其是那些只包含一个抽象方法的接口(称为函数式接口)。Lambda表达式本质上是一个闭包,它可以捕获作用域内的变量,并允许我们像传递数据一样传递代码。
相比JDK7,Lambda表达式解决了代码冗长和可读性差的问题,尤其是在使用匿名内部类时。Lambda使得我们可以用更少的代码行来实现同样的功能,特别是在集合的遍历和并行操作等场景中,Lambda表达式能够显著提高开发效率。此外,它也促进了函数式编程风格在Java中的应用,使得代码更加灵活和易于维护。
8.假设引用了一个第三方的jar有个类和自己写的代码类一样,那么在类加载机制过程中是如何处理的?
如果在加载某个类时遇到同名类,JVM 会按照以下步骤进行处理:
(1)委派给父类加载器:当前类加载器会首先委派给父类加载器进行加载。父类加载器会按照双亲委派模型,先尝试从自己的缓存中查找已加载的类,如果找到了则直接返回;如果没有找到,则继续委派给其父类加载器加载。
(2)依次向上委派:类加载请求会依次向上委派,直到达到顶层的启动类加载器。如果所有父类加载器都无法加载该类,则当前类加载器会尝试自己加载该类。
(3)本地加载:当前类加载器在自己的类路径下查找并加载该类。如果找到了同名类,则直接加载;如果没有找到,则抛出ClassNotFoundException。
9.final、finnally、finalize的区别是什么?
final,finally,finalize之间一点关系都没有,仅仅是长的像。
(1)final 表示不可修改的,可以用来修饰类,方法,变量。例如,final修饰class表示该class不可以被继承。修饰方法表示方法不可以被override(重写)。修饰变量表示变量是不可以修改。
(2)finally是Java的异常处理机制中的一部分。finally块的作用就是为了保证无论出现什么情况,finally块里的代码一定会被执行。
(3)finalize是Object类的一个方法,是GC进行垃圾回收前要调用的一个方法。
10.Array和ArrayList的区别是什么?
Array是Java中的基本数据结构,它是一个固定长度的数据容器,一旦创建后其大小不可变,而且只能存储相同类型的元素。而ArrayList是一个可调整大小的数组实现,是Java集合框架的一部分,它提供了更多方便的操作方法,如添加、删除元素,并且可以存储不同类型的对象(因为它是基于泛型的)。另外,Array在内存中是连续分配的,而ArrayList则是以数组的形式在内存中动态扩展。
11.Lambda和Stream流怎么使用?
Lambda表达式提供了一种简洁的方式来表示只有一个抽象方法的接口(即函数式接口)的实例,通常用于简化代码,特别是在处理集合和事件监听器时。Stream流则是用于支持数据处理操作的一系列操作的集合,它可以对集合进行声明式转换和遍历。使用Lambda和Stream流,可以通过链式调用进行高效的集合数据处理,例如,我们可以这样使用它们:list.stream().filter(item -> item > 10).map(item -> item * 2).collect(Collectors.toList()); 这行代码会过滤出大于10的元素,然后将每个元素乘以2,并收集结果到一个新的列表中。
12.Java中实现多态的机制是什么?
在Java中,实现多态的机制主要是通过继承和接口。当我们定义一个父类或接口,并创建多个子类实现这个父类或接口时,就可以通过父类或接口的引用变量来调用子类中重写后的方法,这就是多态的表现。在运行时,Java虚拟机会根据对象的实际类型来决定调用哪个方法,这个过程称为动态绑定,它是实现多态的关键。
13.如何将一个Java对象序列化到文件里?
要将一个对象序列化到文件里,首先需要确保该对象实现了Serializable接口,这是一个标记接口,用来指示对象可以被序列化。然后,可以使用ObjectOutputStream类来写入对象到文件。
代码示例:
java
// 确保类实现了Serializable接口
public class MyClass implements Serializable {
// 类的成员变量和方法
}
// 序列化过程
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.ser"))) {
MyClass obj = new MyClass();
oos.writeObject(obj); // 将对象写入文件
} catch (IOException e) {
e.printStackTrace();
}
面向对象
14.谈谈你对面向对象的理解?
面向对象是一种编程范式,核心思想是使用类和对象来模拟现实世界中的实体和关系。Java面向对象的特性主要包括封装、继承、多态和抽象。封装是将对象的实现细节隐藏起来,只暴露出必要的接口供外部使用,保证了数据的安全性和模块的独立性;继承允许我们通过扩展已有类来创建新的类,实现代码的复用;多态则是指同一个行为具有多个不同表现形式的能力,使得在运行时可以动态地选择合适的实现;而抽象则是通过抽象类和接口来定义对象的共同特征和行为,忽略细节,关注于对象本质。
15.谈谈接口和抽象类有什么区别?
接口和抽象类都是Java中用来定义抽象层次和实现多态的机制。主要区别在于:接口只能包含抽象方法和静态常量,而且一个类可以实现多个接口;而抽象类可以包含抽象方法、非抽象方法以及成员变量,但一个类只能继承一个抽象类。接口更侧重于定义一种能力,抽象类则侧重于类的抽象化。
16.请你说一下基本类型和包装的区别?
基本类型是Java语言中直接支持的数据类型,如int、double、float等,它们直接存储值,并且占据固定大小的内存空间。而包装类是基本类型的对应类,如Integer、Double、Float等,它们是对象,提供了更多的方法和属性。
区别:
(1)基本类型直接存储值,而包装类则是对基本类型值的封装;
(2)基本类型在栈上分配,而包装类在堆上分配;
(3)基本类型只有简单的赋值操作,而包装类可以调用方法,支持更多的功能,如类型转换、值比较等;
(4)基本类型是值传递,而包装类是引用传递。
(5)包装类还提供了自动装箱和拆箱的功能,这使得在需要时可以方便地在基本类型和包装类之间进行转换。然而,使用包装类也会带来额外的性能开销,因为对象创建和垃圾回收都需要时间。
17.请你说说实例方法和静态方法的区别是什么?
实例方法和静态方法在调用方式上有所不同,实例方法需要通过对象实例调用,而静态方法可以通过类名直接调用。
在访问权限上,实例方法可以访问所有成员,而静态方法只能访问静态成员。
内存分配方面,实例方法每次调用会分配内存来存储实例变量,静态方法不会分配内存来存储实例变量。
实例方法可以重写以实现多态,而静态方法则会被子类隐藏,且不支持多态。
18.请你说说Object的常用方法?
Object是所有类的父类,任何类都默认继承Object。
toString()方法用于返回对象的字符串表示,通常在打印对象或进行字符串连接时被调用,我们经常重写它以提供更清晰的输出。
equals()方法用于判断两个对象是否逻辑相等,它默认比较的是对象的引用,但通常我们会根据业务需求重写它来比较对象的状态。
hashCode()方法返回对象的哈希码,它是支持基于HashMap快速查找的关键,重写equals()时通常也需要重写hashCode()以保持一致性。
wait()、notify()和notifyAll()方法是线程同步的原语,用于线程间的通信。wait()使当前线程等待,直到另一个线程调用notify()或notifyAll()唤醒它。
finalize()方法是在对象被垃圾回收器回收前调用的,用于清理资源。
getClass()方法返回对象的运行时类,它是一个final方法,通常用于反射或在需要知道对象类型时使用。
19.请你说说hashCode()方法的作用。
hashCode()方法的作用是返回一个整数,该整数被用作对象的哈希码,它是HashMap和HashSet等中快速定位对象的关键,哈希码可以在插入和检索时提高效率。重写hashCode()方法时,我们需要确保相同的对象返回相同的哈希码,同时尽量减少不同对象产生相同哈希码的情况,以减少哈希冲突,提高哈希集合的性能。
20.请你说说创建一个类的实例都有哪些办法?
创建一个类的实例有多种方式:最常见的是使用new关键字直接实例化;反射机制可以通过Class对象的newInstance()方法或Constructor对象的newInstance()方法来创建实例;对象克隆可以通过实现Cloneable接口并调用clone()方法来创建一个对象的副本;反序列化可以通过ObjectInputStream的readObject()方法来重建对象;使用工厂模式或设计模式(如单例模式、建造者模式)可以封装对象的创建逻辑;匿名类则可以在定义时直接创建一个类的实例,通常用于实现接口或继承类的简单用例。
21.请你说说JDK7和JDK8都新增了哪些新特性?
JDK7新增特性:
(1)switch语句支持字符串类型:可以在switch语句中使用字符串进行比较。
(2)try-with-resources语句:用于自动关闭实现了AutoCloseable接口的资源,避免了手动关闭资源的繁琐操作。
(3)泛型实例化类型自动推断:在创建泛型对象时,可以省略泛型类型的重复声明。
(4)改进的类型推断:在实例化泛型对象时,编译器可以根据上下文推断出泛型的类型。
(5)数字字面量下划线支持:可以在数字字面量中使用下划线分隔以提高可读性。
JDK8新增特性:
(1)Lambda表达式:引入了函数式编程的概念,使得代码更简洁、可读性更高。
(2)Stream API:提供了一种更便利的处理集合数据的方式,支持并行处理。
(3)默认方法(Default Methods):接口中可以定义默认实现,允许在接口中添加新方法而不破坏现有实现类的兼容性。
(4)方法引用(Method References):可以通过方法的名字来引用已存在的方法。
(5)Optional类:提供了一种更好的处理可能为null的对象的方式,避免了空指针异常。
(6)新的日期/时间API(java.time包):提供了更好的日期和时间处理方式,解决了旧的日期API的一些问题。
(7)CompletableFuture类:新增的异步编程工具,支持更方便地处理异步任务和回调。
异常
22.请你说说Exception和Error的区别。
Exception和Error都是Java异常处理机制中的类,都继承了Throwable类,在Java中只有Throwable类型的实例才可以被抛出或者捕获。
Exception是程序运行中可能遇到的异常情况,通常是可以通过代码处理的,比如IOException、SQLException等,它们表示程序中的错误或意外情况,但不一定是严重到让程序无法继续执行的问题。
Error则是表示系统级别的错误,通常是不可恢复的,比如OutOfMemoryError、StackOverflowError等,它们表示运行时环境出现的错误,这些问题通常是程序员无法通过代码解决的,意味着程序很可能无法继续运行。
23.请你说说Java的异常处理机制。
Java的异常处理机制是通过try、catch、finally、throw和throws关键字来实现的。这种机制允许程序在发生异常时能够捕获并处理这些异常,保证程序的稳定运行。try块用于包围可能会抛出异常的代码,catch块用于捕获并处理特定的异常类型,finally块则用于执行无论是否发生异常都需要执行的代码,如资源释放。通过这种方式,Java将异常处理的逻辑与正常的业务逻辑分离开,提高了代码的可读性和可维护性。
在Java中,异常分为检查型异常(Checked Exceptions)和非检查型异常(Unchecked Exceptions)。检查型异常要求程序员必须显式地处理这些异常,要么通过try-catch捕获,要么在方法签名中通过throws声明抛出,而非检查型异常则不需要强制处理,通常是由于程序逻辑错误导致的,如NullPointerException。这种区分有助于提前识别和预防潜在的错误。
字符串
24.请你说说String类为什么是final的?
String类被设计为final的主要原因是保证字符串的不可变性,这样做的目的是为了确保字符串常量池的正常工作以及提升性能。不可变字符串可以缓存其hash值,确保在字符串常量池中共享时不被意外修改,这对于字符串比较操作尤其重要。此外,不可变字符串在多线程环境下是安全的,因为它不会因为一个线程的操作而影响其他线程。
另一个原因是防止子类化可能带来的问题。如果String类可以被继承,子类可能会破坏字符串的不可变性,这会导致安全性问题和代码的不可预测行为。
25.请你说说String、StringBuffer、StringBuilder区别及使用场景?
(1)String是不可变的字符序列,适用于字符串常量或少量字符串操作的场景
(2)StringBuffer是可变的字符序列,并且线程安全,适用于多线程环境下需要频繁修改字符串的场景
(3)StringBuilder也是可变的字符序列,但非线程安全,适用于单线程环境下需要频繁修改字符串的场景,相比StringBuffer有更好的性能。
26.请你说说==和equals的区别。
==是用于比较基本数据类型时比较变量值是否相等,而对于引用数据类型,则是比较两个对象的内存地址是否相同。而equals方法则是用于比较两个对象的内容是否相等,它是Object类中的一个方法,通常在自定义类中被重写,以实现根据对象的实际内容来判断是否相等。简而言之,==比较的是地址,而equals比较的是内容。
27.请你说说为什么sb.append("str")比s+="str"效率更高?
s+="str"在底层实际上是创建了一个新的String对象,并且将s和"str"连接后的结果赋值给这个新对象,然后将新对象的引用赋给s。这意味着每次执行这个操作时,都会涉及到新对象的创建和旧对象的回收,这在字符串频繁拼接的场景下会有较大的性能开销。而sb.append("str")使用的是StringBuilder(或StringBuffer),它们内部维护了一个字符数组,拼接操作是在这个数组上进行的,不需要每次都创建新的字符串对象,只有在需要返回最终结果时才可能创建新的字符串对象。因此,append方法在执行字符串拼接时效率更高。
28.请你说说String str="hello world"和String str=new String("hello world")的区别?
String str="hello world"在字符串常量池创建了一个字符串常量,并将其引用赋值给变量str。如果池中已经有了"hello world"这个字符串,那么这个表达式实际上不会在堆上创建新的字符串对象,而是直接引用池中的对象。
而String str=new String("hello world")则是显式地在堆上创建了一个新的字符串对象,并且在字符串常量池中也会有一个"hello world"的常量。使用new关键字会始终创建一个新的对象,即使内容相同。
29.请你说说StringBuffer和StringBuilder的区别是什么?性能对比?如何鉴定线程安全?
StringBuffer是线程安全的,因为它的大部分方法都是同步的,这意味着在多线程环境中,多个线程可以安全地调用StringBuffer的方法而不会导致数据不一致。
而StringBuilder不是线程安全的,它没有同步机制,因此在单线程环境下性能通常比StringBuffer更好。如果不需要考虑线程安全,应该优先使用StringBuilder。
要鉴定线程安全,可以通过查看源代码中的方法是否使用了synchronized关键字,synchronized修饰的方法是原子性的。对于StringBuffer里面的方法大部分方法都有synchronized关键字,因此可以认为它是线程安全的。
30.请你说说StringBuffer和StringBuilder底层怎么实现的?
StringBuffer和StringBuilder底层都是通过字符数组实现的,它们都继承自AbstractStringBuilder类。在AbstractStringBuilder中,有一个char类型的数组用来存储字符串的字符序列。当进行字符串拼接操作时,如果数组容量足够,就直接在数组后面追加字符;如果容量不足,则会进行扩容操作,通常是创建一个更大的新数组,并将原数组的内容复制过去。StringBuilder由于不需要考虑线程安全,因此它的操作更为高效;而StringBuffer则通过在方法上添加synchronized关键字来保证线程安全,这会带来一定的性能开销。
泛型
31.什么情况用到泛型,泛型是用来解决什么?
泛型通常在需要实现代码复用、提高类型安全以及减少类型转换的情况下使用。它解决了在集合操作中类型不安全的问题,使得在编译时期就能检测到非法的类型插入,避免了运行时出现ClassCastException,从而提高了程序的健壮性和可维护性。
32.说一下泛型原理,并举例说明。
泛型允许在编码时指定类型参数,使得代码可以适用于多种数据类型,同时还能在编译时提供类型检查,避免了类型转换的错误。泛型的原理是在编译阶段,编译器会根据泛型参数生成相应的字节码,这个过程称为"类型擦除",即将泛型类型参数替换为它们的限定类型(通常是Object),并在必要时插入类型转换的代码。例如,定义一个泛型方法public <T> T getValue(T t),当调用getValue("Hello")时,编译器会处理为String getValue(String t),从而保证了类型安全。
集合
常见集合篇-05-ArrayList-底层原理及构造函数相关面试题回答_哔哩哔哩_bilibili
33.常用的集合类有哪些?
(1)List接口的实现类
ArrayList:基于数组实现的动态数组,支持快速随机访问元素。
LinkedList:基于双向链表实现的列表,支持快速插入和删除操作。
Vector:线程安全的动态数组,性能比ArrayList差,不推荐使用。
(2) Set接口的实现类
HashSet:基于哈希表实现的集合,不保证元素的顺序。
TreeSet:基于红黑树实现的有序集合,元素按照自然顺序或者指定比较器的顺序进行排序。
LinkedHashSet:基于哈希表和链表实现的集合,元素按照插入顺序排序。
(3) Map接口的实现类
HashMap:基于哈希表实现的键值对映射,不保证元素的顺序。
TreeMap:基于红黑树实现的有序键值对映射,键按照自然顺序或者指定比较器的顺序进行排序。
LinkedHashMap:基于哈希表和链表实现的键值对映射,元素按照插入顺序排序。
34.List如何排序?
对于List接口的实现类,可以使用Collections类的静态方法sort()来排序。如果需要自定义排序规则,可以传入一个Comparator对象给sort() 方法。List排序的底层原理取决于具体使用的排序算法。在Java中,Collections.sort()方法使用了归并排序或者快速排序算法来对List进行排序。
(1)归并排序(JDK7之后)
归并排序是一种稳定的排序算法,它将待排序的List不断分割为更小的子序列,直到每个子序列只有一个元素,然后将这些子序列两两合并,直到整个List排序完成。
归并排序的时间复杂度为O(nlogn),空间复杂度为O(n),适用于大规模数据和外部排序。
(2)快速排序(JDK6之前)
快速排序是一种不稳定的排序算法,它通过选择一个基准元素,将List分割为两个子序列,其中一个子序列的元素都小于基准元素,另一个子序列的元素都大于基准元素,然后对这两个子序列分别递归进行快速排序。
快速排序的时间复杂度为O(nlogn),空间复杂度为O(logn),适用于大规模数据但可能会因为基准选择不当而导致性能下降。
35.ArrayList底层的实现原理是什么?
ArrayList底层是用动态的数组实现的。ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10,ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组。具体而言,ArrayList在添加数据的时候,确保数组已使用长度加1之后足够存下下一个数据。首先计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容到原来的1.5倍,确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上,最后返回添加成功布尔值。
36.如何实现数组和List之间的转换?
(1)数组转List,使用JDK中java.util.Arrays工具类的asList方法。当使用Arrays.asList转List后,如果修改了数组内容,list将要受到影响。因为它的底层仅仅是对传入的这个集合进行了包装,并没有new一个新的数组,最终指向的都是同一个内存地址。
(2)List转数组,使用List的toArray方法。无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组。list用了toArray转数组后,如果修改了list内容,数组不会影响,这是因为当调用了toArray以后,在底层是它是进行了数组的拷贝,已经是一个独立副本。
37.ArrayList和LinkedList的区别是什么?
(1)底层数据结构:ArrayList底层使用的是动态数组,而LinkedList底层使用的是双向链表。
(2)效率:对于随机访问操作,ArrayList的效率高于LinkedList;而对于插入和删除操作,LinkedList的效率高于ArrayList。
(3)空间:ArrayList的空间效率相对较高,因为它不需要额外的空间来存储节点之间的指针;而LinkedList则需要额外的空间来存储节点之间的指针。
(4)线程是否安全:ArrayList和LinkedList都不是线程安全的。如果需要在多线程环境下使用,可以考虑使用Collections.synchronizedList方法来包装它们,或者使用CopyOnWriteArrayList。
38.ArrayList是如何序列化和反序列化的?
ArrayList实现了java.io.Serializable接口,因此它可以被序列化。序列化时,ArrayList的非静态和瞬态字段都会被写入到输出流中。这包括数组elementData的容量和实际元素的数量。在反序列化过程中,会根据存储的信息重建数组,并恢复元素,保持原有的元素顺序和数量。需要注意的是,只有数组中实际存储的元素会被序列化,而未被使用的数组空间则不会,这是通过writeObject和readObject方法进行优化的。
39.HashMap底层的实现原理是什么?
HashMap基于哈希表实现,它使用数组来存储数据。每个数组元素称为桶(bucket),存放一个链表或红黑树,用于解决哈希碰撞。当插入一个键值对时,HashMap会计算键的哈希值,然后确定它应该存储在哪个桶中。如果多个键的哈希值相同,它们会被存储在同一个桶的链表或红黑树中。在查找、插入和删除操作时,HashMap通过键的哈希值快速定位到对应的桶,然后遍历链表或红黑树来找到具体的节点。扩容时,HashMap会创建一个新的更大的数组,并将所有元素重新哈希到新数组中。
需要说明的是,链表的长度大于8且数组长度大于64时,链表会转换为红黑树;当树的结点数小于等于临界值6个,红黑树会退化成链表。
40.为什么HashMap要用红黑树,而不用二叉平衡树?
HashMap使用红黑树而不是二叉平衡树(如AVL树)的主要原因在于,红黑树在插入和删除操作时能够提供更稳定的性能。红黑树与AVL树的插入和删除操作的平均时间复杂度均为O(log n),红黑树通过确保最长路径不超过最短路径的两倍,能够保持大致平衡,而AVL树则需要严格保持平衡,这会导致更多的旋转操作,从而在维护平衡时增加时间复杂度。
41.HashMap的JDK7和JDK8的区别是什么?
在JDK7中,HashMap使用数组+链表的结构,当发生哈希碰撞时,新元素会被添加到链表的头部,而遍历链表查找元素时采用的是头插法。JDK7的HashMap在多线程环境下扩容时链表的节点可能会形成环,造成死循环的问题。
而在JDK8中,HashMap在原有的数组+链表结构基础上,链表的长度大于8且数组长度大于64时,链表会转换为红黑树,以减少查找时间复杂度。此外,JDK8中的HashMap在插入元素时采用的是尾插法,解决了死循环的问题。
42.请你说一说JDK7中HashMap的死循环问题。
在JDK7中,HashMap在多线程环境下可能会出现死循环问题。这是因为HashMap的扩容操作采用了头插法,当多个线程同时进行扩容时,可能会导致链表中的节点形成环状,从而在get操作时形成死循环。
具体来说,线程1读取HashMap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来。比如原来的顺序是AB,扩容后的顺序是BA,线程1执行结束。
线程2并发执行。先将A移入新的链表,再将B插入到链头,由于线程1的原因,B的next指向了A,A需要重新插入到链表,会形成A到B的指针,也就是造成了环。
为了避免这个问题,可以在多线程环境下使用线程安全的并发集合,如ConcurrentHashMap,或者使用Collections.synchronizedMap方法包装HashMap,以保证线程安全。同时JDK8也采用了尾插法解决了该问题。
43.请你说一说HashMap中put方法的具体流程?
在HashMap的put方法中,首先判断键值对数组是否为空,如果是执行resize进行扩容。否则,对传入的key调用hashCode()方法计算哈希值,然后通过哈希值确定在内部数组中的索引位置。如果该位置为空,直接将键值对存储在数组中。如果不为空,即发生哈希碰撞,会检查当前节点的key是否与传入的key相等,如果相等则更新value;如果不等,则会遍历链表或红黑树,找到相等的key进行更新,或者插入新的节点。如果链表长度超过阈值,会将链表转换为红黑树。最后,如果插入后元素数量超过扩容阈值,会进行扩容操作。
44.请你说一说HashMap中get方法的具体流程?
在HashMap的get方法中,首先会对传入的key调用hashCode()方法计算哈希值,然后通过哈希值确定在内部数组中的索引位置。如果该位置为空,直接返回null,表示没有找到对应的键值对。如果该位置不为空,会检查当前位置的节点是否与传入的key相等,如果相等则返回对应的value。如果不相等,则会遍历链表或红黑树,通过key的equals方法比较找到相等的key,并返回对应的value。如果在遍历过程中没有找到相等的key,则最终返回null,表示没有找到对应的键值对。整个过程中,get方法的时间复杂度取决于链表或红黑树的长度。
45.请你说一说HashMap为什么要扩容?
哈希表扩容的主要目的是保持哈希表的负载因子在一个合适的范围内。负载因子是指当前哈希表中存储元素的数量与桶的数量之比。负载因子过高时,会导致哈希冲突的概率增加,即多个元素映射到同一个桶的可能性增大,进而降低哈希表的性能。通过扩容,可以增加桶的数量,从而降低负载因子,减少哈希冲突的发生,提高哈希表的效率和性能。
46.请你说一说HashMap的扩容机制。
当HashMap中的元素数量达到扩容阈值(即当前容量乘以加载因子,当前容量初始值是16,加载因子默认是0.75)时,会触发扩容机制。扩容过程包括创建一个新的Entry数组,其容量是原数组容量的两倍,然后将原数组中的所有元素重新哈希并插入到新数组中。这个过程中,每个元素的位置可能会发生变化,因为新的容量会影响哈希值的计算和索引的确定。JDK8中,如果是红黑树,走红黑树的添加;如果是链表,则需要遍历链表,可能需要拆分链表,新的元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上。
47.请你说一说HashMap的寻址算法。
首先会计算对象的hashCode值,然后通过hash方法进行二次哈希处理。这个过程是将hashCode值右移16位后与原hashCode值进行异或运算,这样做是为了让高位数据参与到低位运算中,从而提高哈希分布的均匀性。最后,通过将计算得到的哈希值与数组的容量减一(capacity-1)进行位与运算,得到最终的数组索引位置。需要说明的是,如果数组长度是2的n次幂可以使用位与运算代替取模,计算索引时和扩容时的效率更高。这种寻址方式既保证了哈希值的随机性,又能够有效地利用数组长度,减少哈希碰撞。
48.请你说一说HashMap和HashSet的区别。
HashSet是基于HashMap实现的,它内部维护了一个HashMap实例来存储元素,主要特点是存储无序且不允许重复元素。HashSet的值存储在HashMap的key位置,而value则是一个固定值(常量PRESENT)。
HashMap则是一个基于散列的映射接口的实现,它存储键值对,允许一个键对应一个值,且键不能重复,但值可以重复。HashMap提供了更丰富的操作,如根据键快速检索值、遍历键值对等。
简而言之,HashSet关注的是元素的存在性,而HashMap关注的是键值映射关系。
49.请你说一说HashMap和HashTable的区别。
HashMap与HashTable的主要区别在于线程安全性和性能。HashMap是非同步的,因此性能通常比同步的HashTable要好,但它不是线程安全的。HashMap允许使用一个null键和多个null值,而HashTable不允许使用null键或值。另外,HashMap实现了Map接口,而HashTable继承自Dictionary类。因此,在单线程环境中推荐使用HashMap,而在多线程环境中,如果需要线程安全,则应考虑使用ConcurrentHashMap而不是HashTable。因为ConcurrentHashMap 提供了更好的并发性能,因为它使用了分段锁而不是整个对象加锁。
50.请你说一说HashMap和TreeMap的区别。
HashMap和TreeMap在Java中都是实现Map接口的类,但它们在内部数据结构和性能上有所不同。HashMap使用哈希表来实现,提供了常数时间的插入和检索性能,但不保证元素的顺序。相比之下,TreeMap基于红黑树实现,它按自然顺序或者指定的比较器排序所有的键,因此它提供了有序的键值对集合,但在性能上通常略低于HashMap,因为树操作的时间复杂度是O(log n)。因此,选择HashMap还是TreeMap取决于是否需要键的有序性。如果需要排序功能,选择TreeMap;如果不需要排序且追求更高的性能,选择HashMap。
反射
51.请说说你对反射的了解。
反射是Java语言的一个特性,它允许运行时程序能够自省其结构,包括类的字段、方法和构造器等信息。通过反射,我们可以在运行时动态地创建对象、访问属性、调用方法,甚至修改访问权限,极大地增强了程序的灵活性和动态性。然而,反射也有其缺点,比如它会降低程序的性能,因为它涉及到动态类型解析,并且破坏了Java的封装性,增加了安全风险。
52.请说说你对注解的理解。
注解是JDK5引入的一种特殊类型的注释,它可以提供关于代码的额外信息,这些信息不是程序的一部分,但对编译器、开发工具和其他程序有指导作用。注解本身不执行任何操作,但可以通过注解处理器在运行时或编译时读取注解信息,并根据这些信息执行相应的处理。注解可以用于标记过时的代码、自动生成代码、进行依赖注入等。@Override注解是用来告诉编译器下面的方法是要覆盖超类中的方法,如果签名不匹配,编译器会报错。Spring框架中的@Controller注解,它标记一个类作为控制器,这样Spring容器在运行时就能自动识别并创建这个类的实例来处理HTTP请求。
53.请说说你对动态代理的了解。
动态代理是Java语言中一种强大的特性,它允许在运行时创建一个实现了一组接口的代理类和对象。这个代理可以拦截并处理对原始对象的调用,从而在不修改原始类代码的情况下,增加额外的功能,如日志记录、性能监控、事务管理等。动态代理主要通过Java的反射机制实现,常用的实现方式有Java内置的Proxy类和InvocationHandler接口。