1.List 和 Set有什么区别?
① List和Set之间很重要的一个区别是是否允许重复元素的存在,在List中允许插入重复的元素,而在Set中不允许重复元素存在。
② 与元素先后存放顺序有关,List是有序集合,会保留元素插入时的顺序,Set是无序集合。
③ List可以通过下标来访问,而Set不能。
常见List实现类:ArrayList、LinkedList ;
常见Set实现类:HashSet、TreeSet和LinkedHashSet
2.HashTable、HashMap有什么区别?
HashMap是非线程安全的,Hashtable是线程安全的
HashMap允许null作为键或值,Hashtable不允许
HashMap添加元素使用的是自定义hash算法,Hashtable使用的是key的hashCode
HsahMap在数组+链表的结构中引入了红黑树,Hashtable没有
HashMap初始容量为16,Hashtable初始容量为11
HsahMap扩容是当前容量翻倍,Hashtable是当前容量翻倍+1
3.JDK 7和JDK 8中ConcurrentHashMap的区别是什么?
底层数据结构
JDK7底层数据结构是使用Segment组织的数组+链表
JDK8中取而代之的是数组+链表+红黑树的结构,在链表节点数量大于8 (且数据总量大于等于64)时,会将链表转化为红黑树进行存储
查询时间复杂度
JDK7的遍历链表O(n)、JDK8 变成遍历红黑树O(logN)
保证线程安全机制
JDK7采用Segment的分段锁机制实现线程安全,其中Segment继承自ReentrantLock、JDK8采用CAS+synchronized保证线程安全
锁的粒度
JDK7是对需要进行数据操作的Segment加锁、JDK8调整为对每个数组元素的头节点加锁
4.JDK 8为什么使用同步锁synchronized替换ReentrantLock?
synchronized性能提升
在JDK6中对synchronized锁的实现引入了大量的优化,会从无锁->偏向锁->轻量级锁->重量级锁一步步转换就是锁膨胀的优化,以及有锁的粗化锁消除自适应自旋等优化
提升并发度和减少内存开销
CAS + synchronized方式时加锁的对象是每个链条的头结点,相对Segment再次提高了并发度,如果使用可重入锁达到同样的效果,则需要大量继承自ReentrantLock的对象,造成巨大内存浪费。
5.说说反射的原理?
编译之后会生成一个.class文件,反射就是通过字节码文件找到某一个类、类中的方法以及属性等。
创建类的三种方法:
a)调用类的 Class对象的 newInstance方法,该方法会调用对象的默认构造器,如果没有默认构造器,会调用失败;
b)调用默认Constructor 对象的 newInstance方法;
c)调用带参数 Constructor 对象的 newInstance方法;
6.反射中 Class.forName和 ClassLoader有什么区别?
a)Class.forName() 方法会主动初始化类,而 ClassLoader 不会。这意味着当你使用 Class.forName() 方法加载一个类时,该类的静态初始化块将会被执行。而 ClassLoader 只是加载类,但不会执行任何静态初始化块。
b)Class.forName() 方法还有一个可选的参数,可以指定是否要初始化该类。如果 你 将该参数设置为 false,那么该类的静态初始化块就不会被执行。
c)另外一个不同点是,Class.forName() 方法将会使用当前线程的上下文类加载器 来加载类,而 ClassLoader 则可以指定使用不同的类加载器来加载类。
场景:Spring框架中的 IOC的实现就是使用的 ClassLoader。而在我们使用 JDBC时通常是使用 Class.forName()方法来加载数据库连接驱动。
7.动态代理的几种实现方式?
基于接口的动态代理
基于接口的动态代理是通过 Java 标准库中的 java.lang.reflect.Proxy 类实现的。该类提供了一个静态工厂方法 newProxyInstance(),该方法接受三个参数:
-
ClassLoader: 用于加载代理类的类加载器。
-
Class<?>[]: 被代理接口的 Class 对象数组。
-
InvocationHandler: 实现 InvocationHandler 接口的对象,处理代理类中的方法调用。
InvocationHandler 接口只有一个方法 invoke(),该方法在代理类中的方法被调用时被执行。在该方法中,可以使用反射机制调用目标对象中的方法。
-
基于类的动态代理
基于类的动态代理是通过第三方库,如 CGLIB、ByteBuddy、Javassist 等实现的。这些库使用字节码操作技术,动态生成代理类的字节码,并将其加载到 JVM 中。相比于基于接口的动态代理,基于类的动态代理可以代理非接口类型的类。
8.描述下类的加载机制?
类的加载过程采用父亲委托机制。这种机制能更好的保证 java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。
类的加载过程采用父亲委托机制。这种机制能更好的保证 java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。
9.Error 和 Exception的区别?
a)Exception 和 Error体现了java平台设计者对不同异常情况的分类, Exception是程序正常运行中,可以预料的意外情况,可以被捕获,进行相应的处理.
b)Error 是指正常情况下,不大可能出现的情况,绝大部分的Error 都会导致程序处于非正常的,不可恢复的状态, 不需要捕获, 常见的OutOfMemoryError 是Error的子类.
c)Exception 分为可检查异常(checked) 和 不可检查异常(unchecked).可检查异常在源代码里必须显式的进行捕获处理,这是编译期检查的一部分,不可检查异常是指运行时异常, 比如NullPointerException, ArrayIndexOutOfBoundsException之类, 通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求.
10.finally 中的代码一定会执行吗?
在一般情况下,finally中的代码是一定会执行的,除非在try块中发生了以下两种情况之一:
-
程序在try块中抛出了一个未被try块中的任何catch块捕获的异常,这种情况下finally块中的代码不会执行。
-
在try块中使用了System.exit()方法或者在try块中死循环等导致程序无法正常退出的操作,这种情况下finally块中的代码也不会执行。
11.异常使用有哪些需要注意的地方?
-
不要过度使用异常:异常处理机制会带来额外的系统开销,因此不应该过度使用异常。只有在必要的情况下才应该使用异常。
-
异常应该只用于异常情况:异常是为了处理意外情况而设计的,因此不应该使用异常来处理预期的情况。如果一个异常是可以被预料到的,则应该使用其他方式来处理。
-
抛出有意义的异常:抛出异常时,应该使用有意义的异常类来描述异常情况,而不是简单地抛出通用的Exception类。
-
异常处理应该尽可能早地发生:异常处理应该在代码中尽可能早地发生。如果异常在更深的代码层次中被抛出,就会更难以处理。
-
不要忽略异常:异常是发生了错误或意外情况的信号,忽略异常会使得程序处于不可预料的状态。在捕获异常时,应该确保处理异常或者向上层抛出异常。
-
不要捕获不必要的异常:有些异常是可以被避免的,比如空指针异常。在编写代码时,应该尽可能避免这些异常的发生,而不是简单地捕获这些异常。
-
异常处理应该具体到方法级别:在捕获异常时,应该尽可能将处理异常的代码放到具体的方法中。这样可以让代码更加清晰,同时也方便调试和维护。
12.什么是泛型?有什么作用?
泛型(Generics)是Java中的一种特性,它允许在编写代码时使用参数化类型。通过泛型,我们可以在编译时检查类型安全,避免了在运行时出现类型转换异常的情况。泛型可以应用于类、接口和方法的定义中。
泛型的主要作用有以下几个:
例如,使用泛型可以将一个集合类变为一个通用的容器,可以存储多种类型的对象,而不需要在使用时进行类型转换。这样可以使代码更加简洁和易于维护。在Java中,常见的泛型类型有List、Map、Set等容器类。
13.数组和链表数据结构描述,它们时间复杂度?
数组是一种线性数据结构,它可以存储一组相同类型的数据,这些数据在内存中是连续存储的。数组可以通过下标来访问其中的元素,访问时间是常数级别的,因此在查找、访问元素时非常高效。但是在插入、删除元素时,需要移动大量的数据,时间复杂度是O(n),因此不适用于频繁的插入、删除操作。
时间复杂度:
链表也是一种线性数据结构,它可以存储一组相同类型的数据,这些数据在内存中是离散存储的,通过指针来连接各个节点。链表可以在常数时间内插入、删除节点,但是访问节点时需要遍历整个链表,因此时间复杂度是O(n),不如数组高效。
时间复杂度:
需要注意的是,数组和链表的时间复杂度并不是绝对的,它们的时间复杂度还会受到具体实现的影响。例如,在实现上,数组可能会使用缓存等机制提高访问效率,而链表可能会采用跳表、双向链表等变种结构来优化性能。
-
-
提高代码的重用性:通过泛型,可以编写出通用的类和方法,可以适用于多种类型的数据,提高了代码的重用性。
-
增加代码的类型安全性:通过泛型,编译器可以在编译时检查类型安全,避免了在运行时出现类型转换异常的情况。
-
简化代码:泛型可以避免手动进行类型转换的繁琐工作,简化了代码。
-
提高代码的可读性和可维护性:泛型可以让代码更加清晰和易于理解,提高了代码的可读性和可维护性。
-
数组
-
访问元素:O(1)
-
插入、删除元素:O(n)
-
链表
-
访问元素:O(n)
-
插入、删除元素:O(1)
-
14.什么是注解?
在Java中,注解是一种用于在代码中添加元数据的特殊形式。注解以“@”符号开头,可以附加到类、方法、变量等Java元素上。
注解提供了一种声明式的方法,可以为代码添加关于它们的信息,例如代码作者、版本信息、运行时行为等。注解还可以用于生成文档、代码分析、自动化测试等方面,使代码更加灵活和可读。
Java提供了许多内置注解,例如@Deprecated、@Override、@SuppressWarnings等。同时,Java也允许开发人员定义自己的自定义注解,以便满足特定的需求。
15.注解的解析方法有哪几种?
-
反射解析:使用Java反射机制,在运行时通过访问类的注解信息,来获取注解的内容。
-
编译时解析:在编译时,通过Java编译器扫描源代码,提取注解信息,生成相应的代码。
-
字节码解析:在类加载时,通过解析字节码文件,来获取注解信息。
其中,反射解析是最常用的方法,因为它可以在运行时动态地获取注解信息,并且不需要在编译时进行任何处理。但是,反射解析会带来一定的性能开销,因此在性能敏感的场景下,可以考虑使用编译时解析或字节码解析。
16.什么是SPI?
SPI(Service Provider Interface)是Java提供的一种服务发现机制,它允许应用程序在运行时动态地扩展框架或库的功能,而无需对框架或库进行修改。
在SPI机制中,框架或库定义一组接口,并提供一个用于查找和加载实现该接口的服务提供者的机制。服务提供者可以通过将自己的实现类打包成一个JAR文件,并在其中添加一个特定的文件,来注册自己的实现。这个特定的文件被称为“服务提供者配置文件”,其文件名为“META-INF/services/接口全限定名”,其中包含了实现该接口的类的全限定名。
当框架或库需要加载实现该接口的类时,它会使用Java的SPI机制去查找这个文件,并加载其中指定的类。
SPI机制在很多Java的标准库和框架中都得到了广泛的应用,例如Java数据库连接(JDBC)API、Java Servlet API等。
17.SPI 和 API 有什么区别?
SPI(Service Provider Interface)和API(Application Programming Interface)是两个不同的概念。
API是一组定义了应用程序与操作系统或框架之间交互的接口,它规定了调用者可以使用哪些方法、参数以及返回值。API提供了一种通用的方式,使得应用程序可以与不同的操作系统或框架进行交互,而不需要关心其具体实现细节。
SPI是一种Java的服务发现机制,它允许应用程序在运行时动态地扩展框架或库的功能。SPI规定了框架或库定义一组接口,并提供一个用于查找和加载实现该接口的服务提供者的机制。服务提供者可以通过将自己的实现类打包成一个JAR文件,并在其中添加一个特定的文件,来注册自己的实现。
因此,API是一种定义接口的方式,而SPI是一种实现接口的方式。API是一种静态的概念,而SPI是一种动态的概念。通常情况下,API和SPI是相互依存的,API定义了需要实现的接口,而SPI提供了实现接口的方式。
18.假如有些字段不想进行序列化怎么办?
如果有些字段不想进行序列化,可以使用transient关键字修饰这些字段。transient关键字表示该字段不参与序列化过程,即在将对象转化为字节流时,这些字段会被忽略。
以下是使用transient关键字示例:
public class User implements Serializable {
private String name;
private transient String password;
public User(String name, String password) {
this.name = name;
this.password = password;
}
// getter and setter methods
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}
在上述示例中,User类实现了Serializable接口,并且使用transient关键字修饰了password字段。在序列化过程中,password字段会被忽略,只有name字段会被序列化。
需要注意的是,使用transient关键字修饰的字段不会被序列化,但是在反序列化过程中,这些字段的值将被设置为默认值,例如对于String类型的字段,它的值将被设置为null。如果希望在反序列化过程中,这些字段的值能够得到正确的恢复,可以在对象中实现readObject()和writeObject()方法,手动控制序列化和反序列化过程。
19.常见序列化协议有哪些?
-
Java原生序列化协议(Java Serialization Protocol):Java原生序列化协议是Java标准库中提供的一种序列化协议,它将Java对象序列化为字节流,并可以将字节流反序列化为原始的Java对象。Java原生序列化协议的优点是简单易用,缺点是序列化后的字节流比较冗长,而且序列化和反序列化速度相对较慢。
-
JSON序列化协议(JSON Serialization Protocol):JSON序列化协议将Java对象序列化为JSON格式的文本,可以将序列化后的文本传输到其他平台进行反序列化。JSON序列化协议的优点是序列化后的文本格式简洁易读,而且与其他平台的兼容性较好,缺点是序列化和反序列化速度相对较慢。
-
Protobuf序列化协议(Protocol Buffers Serialization Protocol):Protobuf序列化协议
是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者序列化,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。
20.说说不推荐使用 JDK 自带的序列化的原因?
-
不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。
-
性能差 :相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。