欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

面试必备系列-Java基础相关(一)

程序员文章站 2024-03-23 12:18:58
...

目前网上的面试题泛滥成灾,真正有价值的很少,往往是烂大街的问题,同时也没有给出正确的解决方案 , 本文旨在整理一一系列对面试者有帮助的文章,后续会持续更新。

String 类为什么是final的

答案是为了 “效率”和安全,

安全: 由于String类被final修饰符修饰,那么他就是不可被继承,创建出来的对象之后也就是不可以被改变的。加上String字符串有常量池的概念,如果没有被final修饰符修饰,那么创建之后的对象是不可以保证的,容易产生安全性问题。同时,在String类使用非常广泛的情况下,这种设计是非常必要的。效率: 设计成final,JVM才不用对相关方法在虚函数表中查询,而直接定位到String类的相关方法上,提高了执行效率。 (涉及到了JVM的实现细节,在此笔者也不是很懂,在此分享给大家,有兴趣的可以详细了解)

HashMap的底层实现, jdk1.8有何变化

HashMap底层是通过数据+链表的形式实现的, HashMap初始化的时候,会默认创建长度为16的数组,put的时候通过HashCode求余的方式来确定KEY的位置,如果HashCode相同,那么相同hashCode的值会放在一个链表上面。 如此可见,当放入HashMap中的key的HashCode冲突比较大时,当严重影响效率。

JDK1.8之后,当链表数量达到8个以上时,会采用红黑树来实现。

HashSet集合实现原理是什么

HashSet底层采用的是哈希表来实现的,其原理和HashMap一致,只是对HashMap的API做了一层封装有点类似于value为null的hashMap

Java中的队列有哪些,有何区别

在此列出几个常用的队列。

阻塞队列

  • ArrayBlockingQueue :一个由数组支持的有界队列,阻塞队列,读写共享一把锁

  • LinkedBlockingQueue :一个由链接节点支持的可选有界队列,阻塞队列 , 读写分别采用的是不同的锁。

  • DelayQueue :基于时间的调度队列,比如说往这个队列中添加一个元素,同时指定3秒钟,那么只有过了3秒钟之后,才能够从该队列中取到这个元素

非阻塞队列

  • ConcurrentLinkedQueue

是一个基于链接节点*安全的队列,基于CAS操作保证队列里面数据的一致性 , 该队列的size()方法是会遍历整个队列的,因此及其耗费性能,通常使用isEmpty()来判断集合是否为空。

ConcurrentHashMap实现原理

采用分段锁的概念, ConcurrentHashMap在初始化时,默认会产生16个段(Segment),每个段内部又是一个数组,数组中的每个元素又是一个链表,说的通俗一点,Segment的结构有点类似于HashMap . 
当有元素需要添加进来的时候,首先进行一次Hash , 计算出该元素会被分配到哪个段里面,  确定好在哪个段里面之后,进行第二次Hash, 确定在段的数组中哪个位置,第二次Hash来确定在数组中哪个位置,和HashMap的原理一致。

ConcurrentHashMap 大量的采用volatile, CAS,final,lock等技术,减少锁的竞争对性能产生的影响、

异常的结构,运行时异常和非运行时异常,各举个例子

异常的结构:

Thorwable 是所有异常和错误的基类。

Error 和Exception .

Error : 是JVM运行产生的错误,是不可以感知的,当发生时,会导致程序中断,应用程序无法捕获

Exception : 程序可以捕获的异常,应用程序可以处理的异常信息,分为运行时异常和非运行时异常

非运行时异常 : 程序必须要处理的异常,否则不能编译通过,如: IOException, SQLException

运行时异常 :程序在运行过程中可能产生的异常,这个不强制要求处理。 如: RuntimeException

弱引用, 软引用 的概念和使用方式。

软引用:当JVM内存不够时,会强制回收软引用,如果回收了之后,还不能满足当前的内存需求,则会报内存溢出的错误,通常用于本地缓存的使用。

弱引用: 通过 WeakReference 标识这是一个弱引用,生命周期比较短,当系统触发垃圾回收的时候,如果一个对象只具有弱引用,那么就会被回收 , 比如:ThreadLocal中引用的ThreadLocalMap中的Entry就是一个弱引用,大家有时间可以取看一下。

volatile的理解

volatile是在多线程的环境中,对于变量的一个修饰符。

场景:

大家都知道,每个程序有主内存, 每个线程都有自己的工作内存, 线程工作内存之中的数据是不

共享的,在这里举个例子;

Thread A  查询  int  x   ,此时主内存中的x = 0 ,那么Thread A  通过read ,load 的操作将变量x加载到自己的工作内存当中,同时修改x = 1 , 这个时候主内存当中的x 还是等于0  ,所以当另外一个线程Thread B 过来加载x 的时候,获取到的还是0 。那么volatile的作用是什么呢? 该关键字主要是保证线程之间变量的可见性。 按照上面的例子,如果volatile int x = 0 , 然后被Thread A加载到他的工作内存中修改为了1 ,那么他在修改的同时,会将
x = 1 ,推送到主内存,修改主内存里面的值。

总而言之: volatile保证变量的可见性,但不保证原子性。

CAS是什么

CAS是CPU底层的原子命令 , 里面有三个概念,一定要记住

内存值,预期值,更新值

当: 内存值==预期值  的时候,才会将内存值修改为 更新值, 否则不做任何操作

AtomicInteger有用过吗? 通常用于什么场景? 基于什么原理实现的。

用过, 在项目当中一般用于高并发场景,主要用于计数的功能 。 该类是通过CAS的方式来保证线程安全的。

i++为什么不是线程安全的

因为在多线程的环境下, i++本身就不是一个原子操作,i++的执行过程过下,

Thread从主内存中读取i = 1 到线程的工作内存, 在线程中使用完毕之后,执行  i = i+1 , 这个变量i 并没有同步到主内存中区,Thread B又来读取了,这个时候读取到的还是 1  ,想到这里大家应该都明白了,在多线程的情况下,i++是会造成数据错误的

解决方式:

  1. 使用synchronized同步,但是比较耗费性能。不推荐

  2. 使用AtomicInteger , 其是通过CAS原子操作来保证数据的安全行的,推荐使用

SimpleDateFormat是线程安全的吗?

不是, SimpleDateFormat的内部都是使用Calendar这个对象来操作时间的,如果SimpleDateFormat是静态的,那么在多线程环境下,多个线程操作一个对象,是会有线程安全问题的。

下面是SimpleDateFormat的parse方法

Date parse() {

 calendar.clear(); // 清理calendar

 ... // 执行一些操作, 设置 calendar 的日期什么的

 calendar.getTime(); // 获取calendar的时间

}

这里会导致的问题就是, 如果 线程A 调用了 sdf.parse(), 并且进行了 calendar.clear()后还未执行calendar.getTime()的时候,线程B又调用了sdf.parse(), 这时候线程B也执行了sdf.clear()方法, 这样就导致线程A的的calendar数据被清空了(实际上A,B的同时被清空了). 又或者当 A 执行了calendar.clear()后被挂起, 这时候B 开始调用sdf.parse()并顺利i结束, 这样 A 的 calendar内存储的的date 变成了后来B设置的calendar的date

解决方案:

  1. 每次使用之前,创建一个SimpleDateFormat 对象  ,在高并发的情况下,每次都要创建和销毁该对象是非常耗费性能的,因此不推荐使用。

  2. 使用同步代码块synchronized,同步局部代码。

  3. 使用TheadLocal存储日期对象,为每个线程创建自己的SimpleDateFormat 对象,这样在高并发的情况对性能影响较小。

  4. 使用第三方的时间转换的库


PS: 以上是笔者自己的理解,如果有什么地方错误了,请留言告知我,  我马上修改 , 大家在面试过程中遇到过哪些奇葩问题? 快快留言告诉我,我整理出来分享给大家。


感兴趣的可以关注一下本人的公众号,大量技术干货分享

面试必备系列-Java基础相关(一)