Java基础常见面试题总结(下)

本文详细介绍了Java编程中集合框架的区别与应用,包括List和Set的特点,HashMap与HashTable的差异,JDK7和JDK8中ConcurrentHashMap的变化,以及类加载机制、反射、动态代理、异常处理和泛型的相关知识。
摘要由CSDN通过智能技术生成

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(),该方法在代理类中的方法被调用时被执行。在该方法中,可以使用反射机制调用目标对象中的方法。

  1. 基于类的动态代理

基于类的动态代理是通过第三方库,如 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块中发生了以下两种情况之一:

  1. 程序在try块中抛出了一个未被try块中的任何catch块捕获的异常,这种情况下finally块中的代码不会执行。

  2. 在try块中使用了System.exit()方法或者在try块中死循环等导致程序无法正常退出的操作,这种情况下finally块中的代码也不会执行。

11.异常使用有哪些需要注意的地方?

  1. 不要过度使用异常:异常处理机制会带来额外的系统开销,因此不应该过度使用异常。只有在必要的情况下才应该使用异常。

  2. 异常应该只用于异常情况:异常是为了处理意外情况而设计的,因此不应该使用异常来处理预期的情况。如果一个异常是可以被预料到的,则应该使用其他方式来处理。

  3. 抛出有意义的异常:抛出异常时,应该使用有意义的异常类来描述异常情况,而不是简单地抛出通用的Exception类。

  4. 异常处理应该尽可能早地发生:异常处理应该在代码中尽可能早地发生。如果异常在更深的代码层次中被抛出,就会更难以处理。

  5. 不要忽略异常:异常是发生了错误或意外情况的信号,忽略异常会使得程序处于不可预料的状态。在捕获异常时,应该确保处理异常或者向上层抛出异常。

  6. 不要捕获不必要的异常:有些异常是可以被避免的,比如空指针异常。在编写代码时,应该尽可能避免这些异常的发生,而不是简单地捕获这些异常。

  7. 异常处理应该具体到方法级别:在捕获异常时,应该尽可能将处理异常的代码放到具体的方法中。这样可以让代码更加清晰,同时也方便调试和维护。

12.什么是泛型?有什么作用?

泛型(Generics)是Java中的一种特性,它允许在编写代码时使用参数化类型。通过泛型,我们可以在编译时检查类型安全,避免了在运行时出现类型转换异常的情况。泛型可以应用于类、接口和方法的定义中。

泛型的主要作用有以下几个:

例如,使用泛型可以将一个集合类变为一个通用的容器,可以存储多种类型的对象,而不需要在使用时进行类型转换。这样可以使代码更加简洁和易于维护。在Java中,常见的泛型类型有List、Map、Set等容器类。

13.数组和链表数据结构描述,它们时间复杂度?

数组是一种线性数据结构,它可以存储一组相同类型的数据,这些数据在内存中是连续存储的。数组可以通过下标来访问其中的元素,访问时间是常数级别的,因此在查找、访问元素时非常高效。但是在插入、删除元素时,需要移动大量的数据,时间复杂度是O(n),因此不适用于频繁的插入、删除操作。

时间复杂度:

链表也是一种线性数据结构,它可以存储一组相同类型的数据,这些数据在内存中是离散存储的,通过指针来连接各个节点。链表可以在常数时间内插入、删除节点,但是访问节点时需要遍历整个链表,因此时间复杂度是O(n),不如数组高效。

时间复杂度:

需要注意的是,数组和链表的时间复杂度并不是绝对的,它们的时间复杂度还会受到具体实现的影响。例如,在实现上,数组可能会使用缓存等机制提高访问效率,而链表可能会采用跳表、双向链表等变种结构来优化性能。

    1. 提高代码的重用性:通过泛型,可以编写出通用的类和方法,可以适用于多种类型的数据,提高了代码的重用性。

    2. 增加代码的类型安全性:通过泛型,编译器可以在编译时检查类型安全,避免了在运行时出现类型转换异常的情况。

    3. 简化代码:泛型可以避免手动进行类型转换的繁琐工作,简化了代码。

    4. 提高代码的可读性和可维护性:泛型可以让代码更加清晰和易于理解,提高了代码的可读性和可维护性。

    1. 数组

    • 访问元素:O(1)

    • 插入、删除元素:O(n)

    1. 链表

    • 访问元素:O(n)

    • 插入、删除元素:O(1)

14.什么是注解?

在Java中,注解是一种用于在代码中添加元数据的特殊形式。注解以“@”符号开头,可以附加到类、方法、变量等Java元素上。

注解提供了一种声明式的方法,可以为代码添加关于它们的信息,例如代码作者、版本信息、运行时行为等。注解还可以用于生成文档、代码分析、自动化测试等方面,使代码更加灵活和可读。

Java提供了许多内置注解,例如@Deprecated、@Override、@SuppressWarnings等。同时,Java也允许开发人员定义自己的自定义注解,以便满足特定的需求。

15.注解的解析方法有哪几种?

  1. 反射解析:使用Java反射机制,在运行时通过访问类的注解信息,来获取注解的内容。

  2. 编译时解析:在编译时,通过Java编译器扫描源代码,提取注解信息,生成相应的代码。

  3. 字节码解析:在类加载时,通过解析字节码文件,来获取注解信息。

其中,反射解析是最常用的方法,因为它可以在运行时动态地获取注解信息,并且不需要在编译时进行任何处理。但是,反射解析会带来一定的性能开销,因此在性能敏感的场景下,可以考虑使用编译时解析或字节码解析。

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.常见序列化协议有哪些?

  1. Java原生序列化协议(Java Serialization Protocol):Java原生序列化协议是Java标准库中提供的一种序列化协议,它将Java对象序列化为字节流,并可以将字节流反序列化为原始的Java对象。Java原生序列化协议的优点是简单易用,缺点是序列化后的字节流比较冗长,而且序列化和反序列化速度相对较慢。

  2. JSON序列化协议(JSON Serialization Protocol):JSON序列化协议将Java对象序列化为JSON格式的文本,可以将序列化后的文本传输到其他平台进行反序列化。JSON序列化协议的优点是序列化后的文本格式简洁易读,而且与其他平台的兼容性较好,缺点是序列化和反序列化速度相对较慢。

  3. Protobuf序列化协议(Protocol Buffers Serialization Protocol):Protobuf序列化协议

    是一种轻便高效的结构化数据存储格式,可以用于结构化数据串行化,或者序列化,可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。

20.说说不推荐使用 JDK 自带的序列化的原因?

  1. 不支持跨语言调用 : 如果调用的是其他语言开发的服务的时候就不支持了。

  2. 性能差 :相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值