Java面试系列——Java基础(2)

本期继续Java面试基础篇,这期主要讲

1、异常与错误

2、==和equals、hashcode

3、String、StringBuffer、StringBuilder

4、常用的集合类

1、谈谈exception与error的区别,你遇到过哪些异常与错误。

Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch)

Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序处于非正常的、不可恢复状态。不便于也不需要捕获。

Exception 又分为受检查(checked)异常和不受检查(unchecked)异常,受检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。

不受检查异常:就是所谓的运行时异常RuntimeException,类似 NullPointerException、ArrayIndexOutOfBoundsException 、 ClassCastException(类型转换异常) ;之类,通常是可以编码避免的逻辑错误,具体根据需要来判断是否需要捕获,并不会在编译期强制要求。

受检查:主要是RuntimeException以外的异常,SQLExceptionIOExceptionFileNotFoundExceptionClassNotFoundException

img有一个高频的考点可以记忆一下:

讲一下ClassNotFoundException、NoClassDefFoundError的区别。

答:ClassNotFoundException:当应用程序运行的过程中尝试使用类加载器去加载Class文件的时候,如果没有在classpath中查找到指定的类,就会抛出ClassNotFoundException。一般情况下,当我们使用Class.forName()或者ClassLoader.loadClass以及使用ClassLoader.findSystemClass()在运行时加载类的时候,如果类没有被找到,那么就会导致JVM抛出ClassNotFoundException。

NoClassDefFoundError:当JVM在加载一个类的时候,如果这个类在编译时是可用的,但是在运行时找不到这个类的定义的时候,JVM就会抛出一个NoClassDefFoundError错误。比如当我们在new一个类的实例的时候,如果在运行时类找不到,则会抛出一个NoClassDefFoundError的错误。

2、==和equals、hashcode各自作用与区别。

天天背天天忘的一个考点,关键点我认为在于区分他们各自的用途

  • 关系操作符

    本意是为了简化运算,使用判断两者的值是否相同,但是由于Java中基本数据类型和引用数据类型的关系,又分成了以下两种情况。

    若操作数的类型是基本数据类型,则该关系操作符判断的是左右两边操作数的值是否相等

    若操作数的类型是引用数据类型,则该关系操作符判断的是左右两边操作数的内存地址是否相同,但是如果使用=赋值时会使用常量池导致部分数据也返回true。也就是说,若此时返回true,则该操作符作用的一定是同一个对象。

  • equals 方法

    其本意是比较两个对象的内容是否相同,是Object类中的方法,很多类重写了,比如String首先比较两者是否是同一个对象,如果不是再逐个比较用来存储字符串的数组中内容是否一样

  • Hashcode

    是用来快速检索对象而创造的方法

    原则 1 :如果 x.equals(y) 返回 “true”,那么 x 和 y 的 hashCode() 必须相等 ;

    原则 2 :如果 x.equals(y) 返回 “false”,那么 x 和 y 的 hashCode() 应该尽可能不同,当然哈希冲突在所难免

    原则 3 :如果 x 和 y 的 hashCode() 不相等,那么 x.equals(y) 一定返回 “false” ;

    所以其实三种方法本质不同,本来没有什么比较的必要,但是面试却有可能会问到,有些同学自己写代码的时候也经常会把==和equals用混,还是有必要记一下的。

3、String、StringBuffer、StringBuilder区别联系。

这个问题就很简单了

答:

String 是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。由于字符串操作的普遍性,所以相关操作的效率往往对应用性能有明显影响。String 在 Java 6 以后提供了 intern() 方法,目的是提示 JVM 主动把相应字符串缓存起来放到字符串常量池,以备重复使用,intern会调用equals方法比较,如果有就返回地址,没有就放进去。字符串常量池是一个固定大小的HashTable,使用双引号创建的字符串默认会放到里面,目前字符串常量池是放在堆中的。

StringBuffer 是为解决上面提到拼接产生太多中间对象的问题而提供的一个类,我们可以用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer本质是一个线程安全的可修改字符序列,它的线程安全是通过把各种修改数据的方法都加上 synchronized 关键字实现的,它保证了线程安全,也随之带来了额外的性能开销,所以除非有线程安全的需要,不然还是推荐使用它的后继者,也就是 StringBuilder。

StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 没有本质区别,但是它去掉了线程安全的部分,有效减小了开销,是绝大部分情况下进行字符串拼接的首选。

StringBuffer和StringBuilder底层都是数组,默认长度是16,如果可预计长度应该提前指定、避免扩容带来的性能开销。

4、讲讲常用集合类。

这是个很宽泛的问题,可能会单独问某些集合,在这里对常用集合做个总结。

首先要明确我们常说的集合类,一般都是扩展自Collection接口,Collection接口扩展了三大类集合,分别为:

List,有序集合,它提供了方便的访问、插入、删除等操作。

Set,不允许重复元素

Queue/Deque,标准队列结构的实现,除了集合的基本功能,它还支持类似先入先出(FIFO, First-in-First-Out)或者后入先出(LIFO,Last-In-FirstOut)等特定行为。这里不包括 BlockingQueue,因为通常是并发编程场合,所以被放置在并发包里

img

(图片来源:菜鸟教程、侵删)

下面详细讲几种常用集合

List

常用的List有Vector、ArrayList、LinkedList

Vector 是 Java 早期提供的线程安全的动态数组,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据,扩容两倍

ArrayList 是应用更加广泛的动态数组实现,它本身不是线程安全的,所以性能要好很多。与 Vector 近似,ArrayList 也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50%(无参构造初始容量是10),扩容时将元素拷贝到新数组中。对元素下标的随机访问快,增删慢。

LinkedList 顾名思义是 Java 提供的双向链表,所以它不需要像上面两种那样调整容量,它也不是线程安全的,增删快,查找慢


Set&Map

常用的Set&Map有TreeSet、HashSet、HashMap

TreeSet

添加、删除、包含等操作要相对低效(log(n) 时间)。根据键升序排序。基于TreeMap,TreeMap基于红黑树,它的整体顺序是由 Comparator 决定。key 必须实现 Comparable 接口或者在构造 TreeMap 传入自定义的Comparator,否则会在运行时抛出 java.lang.ClassCastException 类型的异常。

HashSet

基于HashMap,利用哈希算法,理想情况下,如果哈希散列正常,可以提供常数时间的添加、删除、包含等操作,但是它不保证有序。他的value是一个final的 PRESENT(常量)。

HashMap

HashMap是面试重中之重,不仅要掌握HashMap的构成,还要了解一些专有名词,另外还要区分jdk1.7和1.8HashMap源码的区别。

HashMap是由一个个hash桶来放置元素,通过一定的散列算法将元素放置到hash桶中,同一个hash桶中的元素采用下图所示的拉链法存储。Java中使用数组作为一个个桶,数组每个位置又由链表构成。

img

补充:拉链法(链表法,chaining)只是解决hash冲突的一种方法,常用的还有开放寻址法(open addressing),开放寻址法是跳过当前位置,在新的位置插入,一般有线性探测法、二次探测和双重散列。

下面对比一下jdk1.7版本与1.8版本的区别,主要是存储方式、put操作以及扩容操作

1.71.8
存储实例化后创建了一维的Entry[] table(默认长度是16)用来存储元素,可以自己指定长度,但是会被自动修改为2的n次幂不再使用Entry[] table存储数据,也没有默认长度。而是使用Node[]数组,首次调用put方法时,底层创建长度为16的数组
put添加操作put(key,value),首先计算key对应的hash值,哈希值与数组长度按位与,得到在Entry数组中的存放位置。如果此位置没有数据就直接添加;如果数据不为空,则比较此位置上的已存放的key哈希值是否相同。不同时:添加成功,相同继续使用equals进行比较‘改成了尾插法
扩容当HashMap使用的桶数达到总桶数Entry*加载因子的时候且当前添加的Entry位置有哈希冲突会触发扩容;扩容变为原来的两倍。负载因子默认是0.75,因为统计学表明该数值符合泊松分布。当某个桶中的链表长度达到8进行链表扭转为红黑树的时候,会检查总桶数是否小于64,如果总桶数小于64会优先进行扩容再判断是否需要转变为红黑树;因为也有可能把链表和红黑树拆分到不同的桶中

补充:负载因子:https://blog.csdn.net/penghao_1/article/details/107631820

Hashtable

是早期 Java 类库提供的一个哈希表实现,本身是同步的,不支持 null 键和值,由于同步导致的性能开销,所以已经很少被推荐使用。

HashMap和Hashtable的区别:

  • 添加key-value的hash值算法不同:HashMap添加元素时,是使用自定义的哈希算法,而HashTable是直接采用key的hashCode()

  • 迭代器不同:HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),将会抛出ConcurrentModificationException

LinkedHashMap

内部类Entry继承了Hashmap 1.8中的Node内部类,添加了before和after节点

内部构建了一个记录所有元素插入顺序的双向链表,标志位accessOrder (值为true时,表示按照访问顺序迭代;值为false时,表示按照插入顺序迭代)。因此提供了按照插入顺序遍历的能力,与此同时,也保证了常数时间的添加、删除、包含等操作,这些操作性能略低于 HashSet,因为需要维护链表的开销。

补充:

LRU(最近最少使用策略,Least Recently Used)

LRU是一种回收策略,比如当我们要自动清理某些文件时,使用这种策略就会优先 清理最近最少使用的文件

基于LinkedHashMap可以实现 LRU,这里暂不展开讲了

ConcurrentHashMap
ConcurrentHashMap是一个并发容器,是为了解决HashMap线程不安全问题而提出的。
1.71.8
构成是由 Segment 分段数组、HashEntry 组成,Segment 继承于 ReentrantLock(可重入锁),一个segment包含一个HashEntry数组(是一个链表),在并发修改期间,相应 Segment 是被锁定的 ,默认有 16 个 Segments。另外虽然HashEntry 中的value加了volatile关键字,这只能保证读取的可见性get操作线程安全,但put操作仍然是危险的。初始化时:使用CAS操作比较sizeCtl的值是否是-1,如果是-1则进行初始化。初始化时,如果sizeCtl的值为0,则创建默认容量的table。查找、替换、赋值:取消segments字段,引入CAS保证只有一个线程能设置成功,失败的自旋。synchronized 只锁定当前链表或红⿊⼆叉树的⾸节点,读依然采用volatile保证可见性
扩容扩容是在每个hashEntry内部进行的 ,不会影响其他的HashEntry扩容并发进行,阻塞所有读写

关于HashMap和ConcurrentHashMap确实不是三言两语能讲清楚的,在此稍作总结只做面试速记的作用,后续我还会继续给大家退出解读源码的文章,敬请期待。

TreeMap

对于在 Map 中插入、删除、定位一个元素这类操作,HashMap 是最好的选择,因为相对而言 HashMap 的插入会更快,但如果你要对一个 key 集合进行有序的遍历,那 TreeMap 是更好的选择

补充:怎么确保一个集合不能被修改?

结只做面试速记的作用,后续我还会继续给大家退出解读源码的文章,敬请期待。

TreeMap

对于在 Map 中插入、删除、定位一个元素这类操作,HashMap 是最好的选择,因为相对而言 HashMap 的插入会更快,但如果你要对一个 key 集合进行有序的遍历,那 TreeMap 是更好的选择

补充:怎么确保一个集合不能被修改?

可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值