面试题篇-01-Java基础相关面试题

文章目录

1. 简介

本栏目收集了市面上一些常见的 Java基础相关面试题, 不涉及Juc和Jvm范畴.目的是为了辅助面试.快速了解核心

2. Java的设计原则

  • Java设计原则
    • 单一职责原则
    • 接口隔离原则
    • 依赖倒转原则
    • 里氏替换原则
    • 开闭原则 ocp
    • 迪米特原则
    • 合成复用原则

3. Java的设计模式

4. 深拷贝和浅拷贝的理解

深拷贝与浅拷贝的理解

5. 字符串拼接

Java里面字符串拼接有以下三种情况

  • 使用 “+” 拼接
  • 使用concat 拼接
  • 使用 StringBuffer/StringBuilder 拼接

其中,StringBuffer和StringBuilder都是继承于AbstractStringBuilder的;
最主要的区别还是在于StringBuffer使用了synchronized来保证线程安全,而StringBuilder则不适用于多线程环境。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5.1 效率比对

StringBuilder >> concat > +

5.2 原理分析

String类内部是维护了一个char[]字符数组的,字符串拼接的本质就是创建一个可以容纳下两个旧char[]的新char[],再将两个String内部的char[]数组分别copy过去,数组copy肯定最后用的都是System.arraycopy。

如图所示,String内部
在这里插入图片描述
如果所示,AbstractStringBuilder内部
在这里插入图片描述

5.3 字符串拼接- concat方法

每调用一次concat就会创建一个新String,新String内部char[]长度正好等于两个旧String的长度之和,循环100W次就要创建100W个char[]

5.4 字符串拼接- StringBuffer和StringBuilder

StringBuffer和StringBuilder则不一样,它们内部维护了一个可变长的数组。
数组的初始容量是大于你传入的字符串的长度的(多16),每次append就是将String的char[] copy到StringBuffer和StringBuilder的内部数组里面。空间不够用了就会进行扩容, 每次扩容新容量 = 旧容量 x 2 + 2,这样就可以大大减少扩容次数,即减少创建数组的数量。这就是在大量数据的情况下StringBuilder效率远超concat的原因。

5.5 字符串拼接- "+"号

至于加号,每一句str += "a ";会在编译期间被替换成:

str = new StringBuilder (str).append(“a”);

“a”+“a”+"a"被替换成:

new StringBuilder (str).append(“a”).append(“a”).append(“a”);

也就是每一句包含加号的语句都会new一个StringBuilder,加号用append替换。如果循环100次那么会创建100个对象;

6. 解决Hash冲突四种方法

6.1 开放定址法

在这里插入图片描述
其中 m 为表的长度
对增量di有三种取法
1:线性探测再散列 di = 1 , 2 , 3 , … , m-1
2:平方探测再散列 di = 1 , -1 , 2, -2 , 3 , -3 , … , k , -k(取相应数的平方)
3: 随机探测再散列 di 是一组伪随机数列

6.2 链地址法

在这里插入图片描述
先按照以上的hash算法:h(key) = key % 7,算出来对应的hash值,这个hash值暂时就决定,当前的这个值,存放在数组的位置。

这个做法就是Java的HashMap就是这么实现的,简单的解释下,这个HashMap源码的这个链表产生机制。
在put()方法里面,最后部分有个如下的调用。
addEntry(hash, key, value, i);
解释下几个参数的意思:

1,hash:就是根据key算出来的一个值,源码是这么滴–int hash = hash(key);,
2,key:key就是我们在往hashmap里面put键值对的时候的key
3,value:这个同上啦,就是存的键值对的值。
4,i:源码里面是这么滴–int i = indexFor(hash, table.length);实际意思就是这个键值对存放在底层数组的索引下标。然后这个i,可以对应到ppt上的那个取模之后的值,也就是确定在数组上的下标。
虽然在put的时候,可能会出现扩容的问题,但是在这咱就不考虑这个,只考虑如何生成链表,以及链表上的键值对的顺序。
createEntry(hash, key, value, bucketIndex);
这个方法就是真正的在创建一个节点到数组上。

6.3 再哈希

在这里插入图片描述

6.4 建立公共溢出区

建立一个公共溢出区域,就是把冲突的都放在另一个地方,不在表里面。

总结:
1.开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
2.再哈希法
3.链地址法(Java hashmap就是这么做的)
4.建立一个公共溢出区

7. synchronized加在方法上和加在静态方法上有什么区别?

  • Synchronized修饰非静态方法,实际上是对调用该方法的对象加锁,俗称“对象锁”。

  • Synchronized修饰静态方法,实际上是对该类对象加锁,俗称“类锁”。

  • 对象锁:

    • 1.同一个对象访问同一个非stallc方法,对象锁一致,锁生效。
    • 2.同一个对象访可不同非static方法,对象锁一致,锁生效。
    • 3.同一个对象访问stalic方法和非static方法,锁失效、锁类型不一致。
    • 4.不同对象访问同一个非static方法,对象锁不一致,锁失效。
  • 类锁:

    • 1.不同对象访问同一个static方法,锁生效,同一个类锁。类锁一致,对象锁不一致;
    • 2.同一个对象访问不同的static方法,锁生效,同一个类锁,类锁一致,对象锁一致;
    • 3.同一个对象访问static方法和非static方法,锁失效,锁类型不一致;

8. fail-safe 机制与fail-fast 机制分别有什么作用

fail-safe 和fail-fast ,是多线程并发操作集合时的一种失败处理机制

8.1 fail-fast 机制

Fail-fast : 表示快速失败,在集合遍历过程中,一旦发现容器中的数据被修改了,会立刻抛出ConcurrentModificationException 异常,从而导致遍历失败
像这种情况
在这里插入图片描述

定义一个Map 集合,使用Iterator 迭代器进行数据遍历,在遍历过程中,对集合数据做变更时,就会发生 fail-fast。java.util 包下的集合类都是快速失败机制的, 常见的的使用fail-fast 方式遍历的容器有 HashMap 和ArrayList 等。

8.2 Fail-safe机制

表示失败安全,也就是在这种机制下,出现集合元素的修改,不会抛出 ConcurrentModificationException。

原因是采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,
在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到比如这种情况(贴下面这个图)
在这里插入图片描述

定义了一个CopyOnWriteArrayList,在对这个集合遍历过程中,对集合元素做修改后,不会抛出异常,但同时也不会打印出增加的元素。

9. 什么是受检异常和非受检异常吗?

  • 首先是异常的本质
    受检异常和非受检异常,都是继承自 Throwable 这个类中,分别是 Error 和Exception, Error 是程序报错,系统收到无法处理的错误消息,它和程序本身无关。
    Excetpion 是指程序运行时抛出需要处理的异常信息如果不主动捕获,则会被jvm 处理。

  • 对受检异常和非受检异常的定义
    前面说过受检异常和非受检异常均派生自 Exception 这个类。

    • 受检异常的定义是程序在编译阶段必须要主动捕获的异常,遇到该异常有两种处理方法
      通过try/catch 捕获该异常或者通过throw 把异常抛出去
    • 非受检异常的定义是程序不需要主动捕获该异常,一般发生在程序运行期间,比如 NullPointException
  • 受检异常优点有两个

    • 第一,它可以响应一个明确的错误机制,这些错误在写代码的时候可以随时捕获并且能很好的提高代码的健壮性。
    • 第二,在一些连接操作中,它能很好的提醒我们关注异常信息,并做好预防工作。
  • 不过受检异常的缺点是

  • 抛出受检异常的时候需要上声明,而这个做法会直接破坏方法签名导致版本不兼容。这个恶心特性导致我会经常使用 RuntimeException 包装。

  • 非受检异常的好处

    • 可以去掉一些不需要的异常处理代码,而不好之处是开发人员可能忽略某些应该处理的异常,导致带来一些隐藏很深的 Bug,比如流忘记关闭?连接忘记释放等。

10. JDK 动态代理为什么只能代理有接口的类?

这个问题的核心本质,是 JDK 动态代理本身的机制来决定的(如图)。
在这里插入图片描述
首先,在Java 里面,动态代理是通过 Proxy.newProxyInstance()方法来实现的,它需要传入被动态代理的接口类。

之所以要传入接口,不能传入类,还是取决于JDK 动态代理的底层实现(如图)。 JDK 动态代理会在程序运行期间动态生成一个代理类$Proxy0,这个动态生成的代理类会继承java.lang.reflect.Proxy 类,同时还会实现被代理类的接 IHelloService。

在Java 中,是不支持多重继承的。而每个动态代理类都会继承 Proxy 类(这也是JDK动态代理的实现规范),所以就导致 JDK 里面的动态代理只能代理接口,而不能代理实现类。

11. 请说一下对象的创建过程

  1. 在实例化一个对象的时候,JVM 首先会去检查目标对象是否已经被加载并初始化了。
  2. 如果没有,JVM 需要立刻去加载目标类,然后调用目标类的构造器完成初始化。 目标类的加载是通过类加载器来实现的,主要就是把一个类加载到内存里面。然后初始化的过程,主要是对目标类里面的静态变量、成员变量、静态代码块进行初始化。
  3. 当目标类被初始化以后,就可以从常量池里面找到对应的类元信息,并且目标对象的大小在类加载之后就已经确定了,所以这个时候就需要为新创建的对象,根据目标对象的大小在堆内存里面分配内存空间。
  4. 内存分配的方式一般有两种,一种指针碰撞,另一种是空闲列表,JVM 会根据Java 堆内存是否规整来决定内存分配方式。
  5. 接下来,JVM 会把目标对象里面的普通成员变量初始化为零值,比如 int 类型初始化为 0,对象类型初始化为null,(类变量在类加载的准备阶段就已经初始化过了)。这一步操作主要是保证对象里面的实例字段,不用初始化就可以直接使用,也就是程序能够获得这些字段对应数据类型的零值。
  6. 然后,JVM 还需要对目标对象的对象头做一些设置,比如对象所属的类元信息、对象的GC 分代年龄、hashcode、锁标记等等。
  7. 完成这些步骤以后,对于 JVM 来说,新对象的创建工作已经完成。但是基于 Java 语言来说,对象创建才算是开始。
  8. 接下来要做的,就是执行目标对象内部生成的 init 方法,初始化成员变量的值、执行构造块、最后执行目标对象的构造方法,完成对象的创建。
    其中,init 方法是 Java 文件编译之后在字节码文件中生成的,它是一个实例构造器,这个构造器会把语句块、变量初始化、调用父类构造器等操作组织在一起。所以调用 init方法能够完成一系列的初始化动作。
    在这里插入图片描述

11. new String(“abc”)到底创建了几个对象?

首先,这个代码里面有一个new关键字,这个关键字是在程序运行时,根据已经加载的系统类String,在堆内存里面实例化的一个字符串对象。

然后,在这个String 的构造方法里面,传递了一个“abc”字符串,因为String 里面的字符串成员变量是 final 修饰的,所以它是一个字符串常量。

下来,JVM 会拿字面量“abc” 去字符串常量池里面试图去获取它对应的 String 对象引用,如果拿不到,就会在堆内存里面创建一个”abc”的String 对象
并且把引用保存到字符串常量池里面。后续如果再有字面量“abc”的定义,因为字符串常量池里面已经存在了字面量

所以:
1.如果abc这个字符串常量不存在,则创建两个对象,分别是abc字符串常量和这个new String 对象
2.如果abc这个字符串常量存在,则只会创建一个对象

12. 为什么两个Integer 的对象不能用==号来判断?

Integer 是一个封装类型。它是对应一个int 类型的包装。

在Java 里面之所以要提供Integer 这种基本类型的封装类,是因为 Java 是一个面向对象的语言,
而基本类型不具备对象的特征,所以在基本类型上做了一层对象的包装并且提供了相关的属性和访问方法来完善基本类型的操作。在Integer 这个封装类里面,除了基本的int 类型的操作之外,还引入了享元模式的设计,

-128 到 127 之间的数据做了一层缓存(如图),也就是说,如果Integer 类型的目标值在-128 到 127 之间,
就直接从缓存里面获取Integer 这个对象实例并返回,否则创建一个新的 Integer 对象。
在这里插入图片描述
这么设计的好处是减少频繁创建Integer 对象带来的内存消耗从而提升性能。

因此在这样一个前提下,如果定义两个Integer 对象,并且这两个Integer 的取值范围正好在-128 到 127 之间。
如果直接用==号来判断,返回的结果必然是 true,因为这两个Integer 指向的内存地址是同一个。
否则,返回的结果是 false。

13. 可以讲一下 ArrayList 的自动扩容机制吗?

ArrayList 是一个数组结构的存储容器,默认情况下,数组的长度是 10.当然我们也可以在构建 ArrayList 对象的时候自己指定初始长度。
随着在程序里面不断的往ArrayList 中添加数据,当添加的数据达到 10 个的时候, ArrayList 就没有多余容量可以存储后续的数据。
这个时候ArrayList 会自动触发扩容。扩容的具体流程很简单
1.首先,创建一个新的数组,这个新数组的长度是原来数组长度的 1.5 倍。
2.然后使用Arrays.copyOf 方法把老数组里面的数据拷贝到新的数组里面。
扩容完成后再把当前要添加的元素加入到新的数组里面,从而完成动态扩容的过程。

14. HashMap 中的 hash 方法为什么要右移 16 位异或?

在这里插入图片描述
之所以要对hashCode 无符号右移 16 位并且异或,核心目的是为了让hash 值的散列度更高,尽可能减少hash 表的hash 冲突,从而提升数据查找的性能。

在 HashMap 的put 方法里面,是通过 Key 的hash 值与数组的长度取模计算得到数组的位置
在这里插入图片描述
而在绝大部分的情况下,n 的值一般都会小于 2^16 次方,也就是 65536。
所以也就意味着 i 的值 , 始终是使用hash 值的低 16 位与(n-1)进行取模运算,这个是由与运算符&的特性决定的。
这样就会造成key 的散列度不高,导致大量的 key 集中存储在固定的几个数组位置,很显然会影响到数据查找性能。

因此,为了提升 key 的hash 值的散列度,在 hash 方法里面,做了位移运算。
首先使用key 的hashCode 无符号右移 16 位,意味着把hashCode 的高位移动到了低位。
然后再用hashCode 与右移之后的值进行异或运算,就相当于把高位和低位的特征进行和组合。
从而降低了hash 冲突的概率。

15. HashMap 啥时候扩容,为什么扩容?

当HashMap 元素个数达到扩容阈值,默认是 12 的时候,会触发扩容。默认扩容的大小是原来数组长度的 2 倍,HashMap 的最大容量是 Integer.MAX_VALUE,也就是 2 的 31 次方-1。

当HashMap 中元素个数超过临界值时会自动触发扩容,这个临界值有一个计算公式。 threashold=loadFactor*capacity(屏幕显示)。
loadFactor 的默认值是 0.75,capacity 的默认值是 16,也就是元素个数达到 12 的时候触发扩容。
扩容后的大小是原来的 2 倍。

15.1 为什么扩容因子是 0.75?

扩容因子表示Hash 表中元素的填充程度,扩容因子的值越大,那么触发扩容的元素个数更多,
虽然空间利用率比较高,但是 hash 冲突的概率会增加。
扩容因子的值越小,触发扩容的元素个数就越少,也意味着hash 冲突的概率减少,但是对内存空间的浪费就比较多,而且还会增加扩容的频率。
因此,扩容因子的值的设置,本质上就是在 冲突的概率 以及 空间利用率之间的平衡。
0.75 这个值的来源,和统计学里面的泊松分布有关。

(如图)我们知道,HashMap 里面采用链式寻址法来解决 hash 冲突问题,为了避免链表过长带来时间复杂度的增加
所以链表长度大于等于 7 的时候,就会转化为红黑树,提升检索效率。
在这里插入图片描述
当扩容因子在 0.75 的时候,链表长度达到 8 的可能性几乎为 0,也就是比较好的达到了空间成本和时间成本的平衡。

16. Java 有几种文件拷贝方式,哪一种效率最高?

  • 第一种,使用 java.io 包下的库,使用 FileInputStream 读取,再使用 FileOutputStream写出。
  • 第二种,利用java.nio 包下的库,使用 transferTo 或transfFrom 方法实现。
  • 第三种,Java 标准类库本身已经提供了 Files.copy 的实现。

对于 Copy 的效率,这个其实与操作系统和配置等情况相关,
在传统的文件IO 操作里面,我们都是调用操作系统提供的底层标准 IO 系统调用函数 read()、write() ,
由于内核指令的调用会使得当前用户线程切换到内核态,然后内核线程负责把相应的文件数据读取到内核的 IO 缓冲区,
再把数据从内核IO 缓冲区拷贝到进程的私有地址空间中去,这样便完成了一次IO 操作。而NIO 里面提供的NIO transferTo 和transfFrom 方法,也就是常说的零拷贝实现。它能够利用现代操作系统底层机制,避免不必要拷贝和上下文切换,因此在性能上表现比较好。

17. finally 块一定会执行吗?

finally 语句块在两种情况下不会执行:

  • 程序没有进入到 try 语句块因为异常导致程序终止,这个问题主要是开发人员在编写代码的时候,异常捕获的范围不够。
  • 在try 或者cache 语句块中,执行了System.exit(0)语句,导致JVM 直接退出

18. Java SPI 是什么?有什么用?

Java SPI,全称是Service Provider Interface。
它是一种基于接口的动态扩展机制,相当于 Java 里面提供了一套接口。然后第三方可以实现这个接口来完成功能的扩展和实现。

举个简单的例子:

  • 在Java 的SDK 里面,提供了一个数据库驱动的接口java.sql.Driver。它的作用是提供数据库的访问能力。
    不过,在 Java 里面并没有提供实现,因为不同的数据库厂商,会有不同的语法和实现。所以只能由第三方数据库厂商来实现,比如Oracle 是oracle.jdbc.OracleDriver,mysql是com.mysql.jdbc.Driver.
    然后在应用开发的时候,根据集成的驱动实现连接到对应数据库。

在这里插入图片描述
Java 中SPI 机制主要思想是将装配的控制权移到程序之外实现标准和实现的解耦,以及提供动态可插拔的能力,
在模块化的设立中,这种思想非常重要。
实现Java SPI,需要满足几个基本的格式:

  • 需要先定义一个接口,作为扩展的标准
  • 在classpath 目录下创建META-INF/service 文件目录
  • 在这个目录下,以接口的全限定名命名的配置文件, 文件内容是这个接口的实现类
  • 在应用程序里面,使用ServiceLoad,就可以根据接口名称找到classpath 所有的扩展时间

在这里插入图片描述
Java SPI 有一定的不足之处,比如,不能根据需求去加载扩展实现,每次都会加载扩展接口的所有实现类并进行实例化,实例化会造成性能开销,并且加载一些不需要用到的实现类,会导致内存资源的浪费;

总结:
Java SPI 是Java 里面提供的一种接口扩展机制。它的作用我认为有两个:

  • 把标准定义和接口实现分离,在模块化开发中很好的实现了解耦
  • 实现功能的扩展,更好的满足定制化的需求

除了Java 的SPI 以外,基于SPI 思想的扩展实现还有很多,比如Spring 里面的 SpringFactoriesLoader。
Dubbo 里面的ExtensionLoader,并且Dubbo 还在SPI 基础上做了更进一步优化,提供了激活扩展点、自适应扩展点。

19. Integer 和int 的区别?Java 为什么要设计封装类?

Integer 是基本数据类型 int 的封装类
在Java 里面,有八种基本数据类型,他们都有一一对应的封装类型。基本类型和封装类型的区别有很多,比如

  • int 类型,我们可以直接定义一个变量名赋值即可,但是Integer 需要使用new 关键字创建对象
  • 基本类型和Integer 类型混合使用时,Java 会自动通过拆箱和装箱实现类型转换
  • Integer 作为一个对象类型,封装了一些方法和属性,我们可以利用这些方法来操作数据。
    作为成员变量,Integer 的默认值是null,而int 的默认值是 0
  • 要是真正列数出来,还可以挖掘更多的差异点。

在Java 里面,之所以要对基础类型设计一个对应的封装类型。是因为Java 本身是一门面向对象的语言,对象是 Java 语言的基础单元,我们时时刻刻都在创建对象,也随时都在使用对象;

小总结:
Integer 和int 的区别有很多,我简单说 3 个方面

  • Integer 的初始值是null,int 的初始值是 0
  • Integer 存储在堆内存,int 类型是直接存储在栈空间
  • Integer 是对象类型,它封装了很多的方法和属性,我们在使用的时候更加灵活。至于为什么要设计封装类型,最主要的原因是Java 本身是面向对象的语言,一切操作都是以对象作为基础。
    比如像集合里面存储的元素,也只支持存储 Object 类型,普通类型无法通过集合来存储;

20. HashMap 与HashTable 区别

  • 从功能特性的角度来说

    • HashTable 是线程安全的,而 HashMap 不是
    • HashMap 的性能要比 HashTable 更好,因为,HashTable 采用了全局同步锁来保证安全性,对性能影响较大
  • 从内部实现的角度来说

    • HashTable 使用数组加链表、HashMap 采用了数组+链表+红黑树
    • HashMap 初始容量是 16、HashTable 初始容量是 11
    • HashMap 可以使用null 作为key,HashMap 会把null 转化为 0 进行存储,而Hashtable 不允许。

最后,他们两个的key 的散列算法不同,HashTable 直接是使用key 的hashcode 对数组长度做取模。
而HashMap 对key 的hashcode 做了二次散列,从而避免key 的分布不均匀问题影响到查询性能。

21. Java 反射的优缺点?

  • Java 反射的优点:
    • 增加程序的灵活性,可以在运行的过程中动态对类进行修改和操作
    • 提高代码的复用率,比如动态代理,就是用到了反射来实现
    • 可以在运行时轻松获取任意一个类的方法、属性,并且还能通过反射进行动态调用
  • Java 反射的缺点:
    • 反射会涉及到动态类型的解析,所以 JVM 无法对这些代码进行优化,导致性能要比非反射调用更低。
    • 使用反射以后,代码的可读性会下降
    • 反射可以绕过一些限制访问的属性或者方法,可能会导致破坏了代码本身的抽象性

22. 为什么重写 equals() 就一定要重写 hashCode() 方法?

如果只重写equals 方法,不重写hashCode 方法。

  • 就有可能导致 a.equals(b)这个表达式成立,但是hashCode 却不同。
  • 那么这个只重写了 equals方法的对象,在使用散列集合进行存储的时候就会出现问题。
  • 因为散列结合是使用 hashCode 来计算 key 的存储位置,如果存储两个完全相同的对象,但是有不同的hashcode
    就会导致这两个对象存储在 hash 表的不同位置,当我们想根据这个对象去获取数据的时候,就会出现一个悖论
  • 一个完全相同的对象会在存储在hash 表的两个位置,造成大家约定俗成的规则,出现一些不可预料的错误。

23. 哪些情况下的单例对象可能会破坏?

23.1 多线程破坏单例

在多线程环境下,线程的时间片是由 CPU 自由分配的,具有随机性,而单例对象作为共享资源可能会同时被多个线程同时操作,从而导致同时创建多个对象。当然,这种情况只出现在懒汉式单例中。如果是饿汉式单例,在线程启动前就被初始化了,不存在线程再创建对象的情况。

如果懒汉式单例出现多线程破坏的情况,我给出以下两种解决方案:

  • 改为DCL 双重检查锁的写法。
  • 使用静态内部类的写法,性能更高。

23.2 指令重排破坏单例

指令重排也可能导致懒汉式单例被破坏。
来看这样一句代码: instance = new Singleton();
看似简单的一段赋值语句:instance = new Singleton();
其实JVM 内部已经被转换为多条执行指令:

  • memory = allocate(); 分配对象的内存空间指令
  • ctorInstance(memory); 初始化对象
  • instance = memory; 将已分配存地址赋值给对象引用

如果直接使用就可能发生错误。如果出现这种情况,我该如何解决呢?只需要在成员变量前加 volatile,保证所有线程的可见性就可以了。

23.3 克隆破坏单例

在Java 中,所有的类就继承自 Object,也就是说所有的类都实现了clone()方法。如果是深clone(),每次都会重新创建新的实例。

那如果我们定义的是单例对象,岂不是也可调用clone()方法来反复创建新的实例呢?确实,这种情况是有可能发生的。
为了避免发生这样结果,我们可以在单例对象中重写 clone() 方法,将单例自身的引用作为返回值。这样,就能避免这种情况发生。

23.4 反序列化破坏单例

我们将Java 对象序列化以后,对象通常会被持久化到磁盘或者数据库。如果我们要再次加载到内存,就需要将持久化的内容反序列化成 Java 对象。反序列化是基于字节码来操作的,我们要序列化以前的内容进行反序列化到内存,就需要重新分配内存,也就是说,要重新创建对象。那如果要反序列化的对象恰恰是单例对象,我们该怎么办呢?我告诉大家一种解决方案,在反序列的过程中,Java API 会调用readResolve()方法,可以通过获取readResolve()方法的返回值覆盖反序列化创建的对象。

因此,只需要重写readResolve()方法,将返回值设置为已经存在的单例对象,就可以保证反序列化以后的对象是同一个了。之后再将反序列化后的对象中的值,克隆到单例对象中。

23.5 反射破坏单例

Java 中的反射机制是可以拿到对象的私有的构造方法,也就是说,反射可以任意调用私有构造方法创建单例对象。当然,没有人会故意这样做,但是如果出现意外的情况,该如何处理呢?我推荐大家两种解决方案,

  • 第一种方案是在所有的构造方法中第一行代码进行判断,检查单例对象是否已经被创建,如果已经被创建,则抛出异常。这样,构造方法将会被终止调用,也就无法创建新的实例。
  • 第二种方案,将单例的实现方式改为枚举式单例,因为在 JDK 源码层面规定了,不允许反射访问枚举。

24. 责任链模式的实现原理

关于责任链模式的定义,官方原文是这样描述的:
Avoid coupling the sender of a request to its receiver by giving more than one object a chance to handle the request. Chain the receiving objects and pass the request along the chain until an object handles it.
翻译过来就是:
将链中每一个节点都看作一个对象,每个节点处理的请求均不同,且内部自动维护下一个节点对象。当一个请求从 的首端发出时,会沿着责任链预设的路径依次传递到每一个节点对象,直至被链中的某个对象处理为止。

简答一句话总结就是:将处理不同逻辑的对象连接成一个链表结构,每个对象都保存下一个节点的引用。

责任链又分为单向责任链和双向责任链,单向责任链比较简单也容易理解,双向责任链相当于是一个执行闭环,较为复杂。

24.1 单向责任链

它的结构是这样设计的,首先,设计一个单向链表的上下文 Context,保存链表的头(head )引用 和 尾(tail)引用,Context 的代码结构是这样的。
然后,在上下文中加入Handler,也就是处理业务逻辑的节点类,每个Hnandler 都保存了下一个执行节点的引用,形成一条完整的执行链路。Handler 的通用代码结构是这样的。
在这里插入图片描述
J2EE 中的Filter 过滤器、Spring 中的Interceptor 拦截器都是采用这样的单向链表的设计。

24.2 双向责任链

它和单向链表的基本结构一致,我们来看,它只是在Handler 中增加了对上一个节点的引用。这样责任链就行了一个执行闭环,就好比是环线地铁。来看它的通用代码结构是这样的。
在这里插入图片描述
Netty 中的Piepline 管道就是采用这样的双向链表的设计。

24.3 责任链模式的优缺点

  • 优点:

    • 实现了将请求与处理完全解耦。
    • 请求处理者只需关注自己职责范围内的请求进行处理即可,对于不是职责范围内的请求,直接转发给下一个节点对象。
    • 具备链式传递处理请求的功能,请求发送者不需要知晓链路结构,只需等待请求处理结果即可。
    • 链路结构灵活,可以通过改变链路结构动态地新增或删减责任。
  • 缺点:

    • 如果责任链路太长或者处理时间过长,会影响程序执行的整体性能。
    • 如果节点对象存在循环引用,有可能会造成死循环,从而导致程序崩溃。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
java面试题真的很多,下面我来回答一个有关多线程的问题。 在Java中实现多线程有两种方式,一种是继承Thread类,另一种是实现Runnable接口。这两种方式有何区别? 继承Thread类的方式是直接定义一个类继承Thread,并重写它的run()方法。然后创建该类的对象,并调用对象的start()方法来启动线程。这种方式简单直接,但因为Java是单继承的,所以如果某个类已经继承了其他类,就不能再直接继承Thread类实现多线程。 实现Runnable接口的方式是定义一个类实现Runnable接口,并实现其唯一的抽象方法run()。然后创建Thread类的对象,将实现了Runnable的对象作为参数传递给Thread类的构造方法。最后调用Thread对象的start()方法来启动线程。这种方式灵活性更大,因为Java允许一个类实现多个接口,所以即使某个类已经继承了其他类,仍然可以通过实现Runnable接口来实现多线程。 另一个区别在于资源共享的问题。继承Thread类的方式,不管是数据还是方法,都是线程自己拥有的,不存在共享的情况。而实现Runnable接口的方式,多个线程可以共享同一个对象的数据和方法,因为多个线程共同操作的是同一个Runnable对象。 总结来说,继承Thread类方式简单直接,但只能通过单继承来实现多线程;实现Runnable接口方式更灵活,可以解决单继承的限制,并且多个线程可以共享同一个Runnable对象的数据和方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Alan0517

感谢您的鼓励与支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值