大厂面试题-Java基础篇(一)

目录

一、fail-safe机制与fail-fast机制分别有什么作用

二、HashMap是怎么解决哈希冲突的?

三、说一下什么是受检异常和非受检异常吗

四、为什么阿里巴巴的Java开发手册不建议使用Java自带的线程池

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

六、说一下对象的创建过程

七、new String("abc")到底创建了几个对象?

八、HashMap是如何解决hash冲突的?

九、String、StringBuffer、StringBuilder 区别

十、Integer使用不当导致生产的事故


一、fail-safe机制与fail-fast机制分别有什么作用

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

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

定义一个Map集合,使用Iterator迭代器进行数据遍历,在遍历过程中,对集合数据做变更时,就会发生fail-fast

java.util包下的集合类都是快速失败机制的,常见的的使用fail-fast方式遍历的容器有HashMap和ArrayList等。

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

原因是采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到

比如这种情况(如图),定义了一个CopyOnWriteArrayList,在对这个集合遍历程中,对集合元素做修改后,不会抛出异常,但同时也不会打印出增加的元素。java.util.concurrent包下的容器都是安全失败的,可以在多线程下并发使用,并发修改。常见的的使用fail-safe方式遍历的容器有ConcerrentHashMap和CopyOnWriteArrayList等。

二、HashMap是怎么解决哈希冲突的?

常用数据结构基本上是面试必问的问题,比如HashMap、LinkList、ConcurrentHashMap等。

从三个方面来回答

  1.   要了解 Hash 冲突 ,那首先们要先了解 Hash 算法和 Hash 表。

a.    Hash 算法 ,就是把任意长度的输入 ,通过散列算法 ,变成固定长度的输出, 这个输出结果是散列值。

b.   Hash 表又叫做“散列表” ,它是通过 key 直接访问在内存存储位置的数据结 构 ,在体实现上 ,们通过 hash 函数把 key 映射到表中的某个位置 ,来获 取这个置的数据 ,从而加快查找速度。

c.   所 hash 冲突 ,是由于哈希算法被计算的数据是无限的 ,而计算后的结果范 围有,所以总会存在不同的数据经过计算后得到的值相同,这就是哈希冲突。

d.   通常解决 hash 冲突的方法有 4 种

i.   开放定址法,也称为线性探测法,就是从发生冲突的那个位置开始,按照一定的次序从hash表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲位置中。ThreadLocal就用到了线性探测法来解决hash冲突的

向这样一种情况 (如图)  ,在 hash 表索引 1 的位置存了一个 key=name,当再次添加 key=hobby 时 ,hash 计算得到的索引是 1 ,这个就是 hash 冲突。而开放定址法, 就是按顺序向前找到一个空闲的位置来存储冲突的 key。

ii.   链式寻址法 ,这是一种非常常见的方法 ,简单理解就是把存在 hash 冲突  key ,以单向链表的方式来存储 ,比如 HashMap 就是采用链式寻址法 来实现的。

向这样一种情况  (如图)  ,存在冲突的 key 直接以单向链表的方式进行存储。

iii.   再 hash 法 ,就是当通过某个 hash 函数计算的 key 存在冲突时 ,再用另 外一个 hash 函数对这个 key 做 hash,一直运算直到不再产生冲突。这种 方式会增加计算时间 ,性能影响较大。

iv.   建立公共溢出区,   就是把 hash 表分为基本表和溢出表两个部分,凡事存 在冲突的素 ,一律放入到溢出表中。

e.    HashMap 在 JDK1.8 版本中,通过链式寻址法+红黑树的方式来解决 hash 冲 突问题,其红黑树是为了优化 Hash 表链表过长导致时间复杂度增加的问题。 链表长度大于 8 并且 hash 表的容量大于 64 的时候 ,再向链表中添加元素  就会触发转化。

三、说一下什么是受检异常和非受检异常吗

从三个方面回答

一、首先是异常的本质

受检异常和非受检异常,都是继承自 Throwable 这个类中,分别是 Error 和 Exception Error 是程报错 ,系统收到无法处理的错误消息 ,它和程序本身无关。

Excetpion是指程序运行时抛出需要处理的异常信息如果不主动捕获,则会被jvm处理。

二、然后是对受检异常和非受检异常的定义

前面说过受检异常和非受检异常均派生自 Exception 这个类。

1. 受检异常的定义是程序在编译阶段必须要主动捕获的异常 ,遇到该异常有两种处理方法

通过 try/catch 捕获该异常或者通过 throw 把异常抛出去

2. 非受检异常的定义是程序不需要主动捕获该异常 ,一般发生在程序运行期间 ,比如 NullPointException

三、最后还可以说下他们优点和缺点受检异常优点有两个:

第一,它可以响应一个明确的错误机制,这些错误在写代码的时候可以随时捕获并且能很好的提高代码的健壮性。

第二 ,在一些连接操作中 ,它能很好的提醒们关注异常信息 ,并做好预防工作。 不过受检异常的缺点是:抛出受检异常的时候需要上声明,而这个做法会直接破坏方法 签名导致版本不兼容。这个恶心特性导致会经常使用 RuntimeException 包装。

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

四、为什么阿里巴巴的Java开发手册不建议使用Java自带的线程池

线程池这么好 ,为什么阿里巴巴的 Java 开发手册不建议使用呢?

是去手册上看到了原文内容。发现是“线程池不允许使用 Executors 去创建”。

这句话 ,就理解原因了。

Executors 里面默认提供的几个线程池是有一些弊端的,如果是不懂多线程、或者是新 手直接盲目使用 ,可能会造成比较严重的生产事故。

 第一个,FixedThreadPool 和 SingleThreadPool 中 ,阻塞队列长度是

Integer.Max_Value,一旦请求量增加 ,就会堆积大量请求阻塞在队列中 ,可能会 造成内存溢出的问题。

 第二个 ,CachedThreadPool 和 ScheduledThreadPool 中最大线程数量是

Integer.Max_value旦请求量增加,导致创建大量的线程,使得处理性能下降。 甚至可能会出现宕机的问题。

为了避免这类问题出现,们可以直接实例化 ThreadPoolExecutor,然后自己设置参 数的值 ,从而确保线程池的可控性

实际上 ,很多源码或者中间件里面,都是使用这类的方式。

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

个问题的核心本质 ,是 JDK 动态代理本身的机制来决定的(如图)。

首先,在 Java 里面,动态代理是通过 Proxy.newProxyInstance()方法来实现的,它需 要传入被动态代理的接口类。

之所以要传入接口 ,不能传入类 ,还是取决于 JDK 动态代理的底层实现  (如图)  。  

JDK 动态代理会在程序运行期间动态生成一个代理类$Proxy0,这个动态生成的代理类 会继承 java.lang.reflect.Proxy 类 ,同时还会实现被代理类的接口 IHelloService。

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

分析过动态代理的源码,发现Proxy这个类只是保存了动态代理的处理器InvocationHandler,如果不抽出来,直接设置到$Proxy0 动态代理类里面,也是可以的。如果这么做,就可以针对实现类来做动态代理了。作者为什么这么设计,认为有几个 方面的原因。

1.   动态代理本身的使用场景或者需求,只是对原始实现的一个拦截,然后去做一些功 能的增强或者扩展。而实际的开发模式也都是基于面向接口来开发,所以基于接口 来实现动态代理,从需求和场景都是吻合的。当然确实可能存在有些类没有实现接口的 ,那这个时候 ,JDK 动态代理确实无法满足。

2.   在 Java 里面 ,类的继承关系的设计 ,更多的是考虑到共性能力的抽象 ,从而提高  代码的重用性和扩展性,而动态代理也是在做这样一个事情,它封装了动态代理类 生成的抽象逻辑、断一个类是否是动态代理类、InvocationHandler 的持有等等, 那么把这些抽象的共逻辑放在 Proxy 这个父类里面,很显然是一个比较正常的设 计思路。

总的来说,认为这个设计上并没有什么特别值得讨论的地方,因为认为技术方案的设计是解决特定场景问题的。

如果一定要针对普通类来做动态代理,可以选择 cglib 这个组件 ,它会动态生成一个被 代理类的子 ,子类重写了父类中所有非 final 修饰的方法 ,在子类中拦截父类的所有 方法调用从而实现动态代理。

六、说一下对象的创建过程

(如图)  在实例化一个对象的时候 ,JVM 首先会去检查目标对象是否已经被加载并初 始化了。

没有,JVM 需要立刻去加载目标类,然后调用目标类的构造器完成初始化。    目标 加载是通过类加载器来实现的 ,主要就是把一个类加载到内存里面。

然后初始化的过程,主要是对目标类里面的静态变量、成员变量、静态代码块进行初始 

当目标类被初始化以后,就可以从常量池里面找到对应的类元信息,并且目标对的大 小在类加载之后就已经确定了,所以这个时候就需要为新创建的对象,根据目标对象的大小在堆内存里面分配内存空间。

内存分配的方式一般有两种,一种指针碰撞,另一种是空闲列表,JVM 会根据 Java 堆 内存是否规整来决定内存分配方式。

接下来,JVM 会把目标对象里面的普通成员变量初始化为零值,比如 int 类型初始化为 0 ,对象类型初始化为 null,类变量在类加载的准备阶段就已经初始化过了  。

这一步操作主要是保证对象里面的实例字段,不用初始化就可以直接使用,也就是程序 能够获得这些字段对应数据类型的零值。

然后 ,JVM 需要对目标对象的对象头做一些设置 ,比如对象所属的类元信息、对象 GC 分代年龄、hashcode、锁标记等等。

完成这些步骤以后,对于 JVM 来说,新对象的创建工作已经完成。但是基于 Java 语言 来说 ,对象创建才算是开始

接下要做的,就是执行目标对象内部生成的 init 方法,初始化成员变量的值、执行构 造块、最后执行目标对象的构造方法 ,完成对象的创建。

其中 ,init 方法是 Java 文件编译之后在字节码文件中生成的 ,它是一个实例构造器 ,  这个构造器会语句块、变量初始化、调用父类构造器等操作组织在一起。所以调用 init 方法能够完成一系列的初始化动作。

七、new String("abc")到底创建了几个对象?

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

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

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

后续如果再有字面量“abc”的定义 ,因为字符串常量池里面已经存在了字面量   abc”的引 ,所以只需要从常量池获取对应的引用就可以了 ,不需要再创建。 所以 ,对于这个问题 ,认为的答案

1.   如果 abc 这个字符串常量不存在,则创建两个对象,分别是 abc 这个字符串常量, 以及 new String 这个实例对象。

2.   如果 abc 这字符串常量存在 ,则只会创建一个对象。

八、HashMap是如何解决hash冲突的?

首先,HashMap底层采用了数组的结构来存储数据元素,数组的默认长度是16,当们通过put方法加数据的时候,HashMap根据Key的hash值进行取模运算。最终保存到数组的指定位置。

但是这种设计会存在hash冲突问题,也就是两个不同hash值的key,最终取模后会落到同一个数组下标。

(如图)所HashMap引入了链式寻址法来解决hash冲突问题,对于存在冲突的keyHashMap 把这些 key 组成一个单向链表。

然后采用尾插法把这个 key 保存到链表的尾部

另外,为了避免表过长的问题,当链表长度大于8 并且数组长度大于等于 64 的时候, HashMap 会把链表转化为红黑树  (如图)  。

从而减少链表据查询的时间复杂度问题 ,提升查询性能。

最后 ,再补充一下 ,解决 hash 冲突问题的方法有很多 ,比如

  1.     1、再 hash 法 ,就是如果某个 hash 函数产生了冲突 ,再用另外一个 hash 进行计算,
  2. 比如布隆过滤器就采用了这种方法。
  3.     2、开放寻址法 ,就是直接从冲突的数组位置往下寻找一个空的数组下标进行数据存
  4. 储 ,这个在 ThreadLocal 里面有使用到。
  5.     3、建立公共溢出区 ,也就是把存在冲突的 key 统一放在一个公共溢出区里面。

九、String、StringBuffer、StringBuilder 区别

关于 String、StringBufferStringBuilder 的区别 ,从四个角度来说明:

一个 :可变性

String 内部的 value 值是 final 修饰的,所以它是不可变类。所以每次修改 String 的值, 都会产生一个新的对象。

StringBuffer 和 StringBuilder 是可变类 ,字符串的变更不会产生新的对象。

二个 :线程安全性

String 是不可变类 ,所以它是线程安全的。

StringBuffer 是线程安全的 ,因为它每个操作方法都加了 synchronized 同步关键字。 StringBuilder 不是线程安全的。

所以在多线程环境下对字符串进行操作 ,应该使用 StringBuffer ,否则使用StringBuilder

三个 :性能方面

String 的性能是最的低的,因为不可变意味着在做字符串拼接和修改的时候,需要重新 创建新的对象以及分配内存。

其次 StringBuffer 要比 String 性能高 ,因为它的可变性使得字符串可以直接被修改 最后是 StringBuilder ,它比 StringBuffer 的能高 ,因为 StringBuffer 加了同步锁。 

四个 :存储方面

String 存储在字符串常量池里面

StringBuffer 和 StringBuilder 存储在堆内存空间。

最后再补充一下,StringBuilder 和 StringBuffer 都是派生自 AbstractStringBuilder 个抽象类。

十、Integer使用不当导致生产的事故

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

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

在Integer这个封装类里面,除了基本的int类型的操作之外,还引入了享元模式的设计,对-128到127之间的数据做了一层缓存(如图),也就是说,如果Integer类型的目标值在-128到127之间,就直接从缓存里面获取Integer这个对象实例并返回,否则创建一个新的Integer对象。

这么设计的好处是减少频繁创建Integer对象带来的内存消耗从而提升性能。

因此在这样一个前提下,如果定义两个Integer对象,并且这两个Integer的取值范围正好在-128到127之

如果直接用==号来判断,返回的结果必然是true,因为这两个Integer指向的内存地址是同一个。否则,返回的结果是false。

之所以在测试环境上没有把这个问题暴露出来,是因为测试环境上验证的数据量限,使得取值的范围正好在Integer缓存区间,从而通过了测试。

但是在实际的应用里面,数据量远远超过IntegerCache的取值范围,所以就导致了校验失败的问题。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值