金三银四java面试锦集(一)

Java基础

1. List 和 Set 的区别

List和set同属于Collection的子类:

  • 是否有序:List是有序的,set是无序的(TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序。)
  • 是否重复:List是可重复的,set是不可重复的
  • 是否为空:List允许有多个null,set只允许有一个为null

拓展:

Map和List、set的区别(Map不属于Collection的子类):

  • Map是无序的
  • Map的键不允许重复,值允许重复
  • Map的键只允许有一个null,值允许多个null

2. HashSet 是如何保证不重复的

HashSet的底层实现结构是HashMap,它存放数据于HashMap的键中。而根据HashMap的特性,键是不允许重复的。

拓展:

HashMap是以键值对存储数据的。它底层存放数据是数组+链表!每一个键,都有它相对应的唯一的hashcode码。当调用put方法放置一个key-value时,会获取键的hashcode,然后根据算法(h & (length -1)),计算出应该存放的对应的数组块位置。如果多个hashcode计算出需要存放在同一数组块,则以链表的形式存储。

详情请看:HashMap的工作原理

3. HashMap 是线程安全的吗,为什么不是线程安全的(最好画图说明多线程环境下不安全)?

HashMap是线程不安全的。
hash碰撞和扩容的情况下会导致数据丢失或者形成闭环链表。

详情请看:HashMap为什么线程不安全(hash碰撞与扩容导致)
谈谈HashMap线程不安全的体现

4. HashMap 的扩容过程

  • 计算当前数组的长度是否到最大值,如果到最大值,则修改阈值(2^31-1)之后,不会扩容
  • 否则,初始化一个新的数组,将数据转移到新的数组里。
  • 将新数组引用到hashmap里
  • 修改阈值

拓展:

  • HashMap初始容量为16,负载因子:0.75,即当前数组长度超过总长度的75%,则进行扩容。每次扩容为2的倍数。
  • 数据转移过程(上接第二条)

    • 遍历数组中的每一个元素
    • 重新计算每个元素在数组中的位置
    • 将元素放在数组上
    • 访问下一个Entry链上的元素
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    void transfer(Entry[] newTable) {  
    Entry[] src = table; //src引用了旧的Entry数组
    int newCapacity = newTable.length;
    for (int j = 0; j < src.length; j++) { //遍历旧的Entry数组
    Entry<K, V> e = src[j]; //取得旧Entry数组的每个元素
    if (e != null) {
    src[j] = null;//释放旧Entry数组的对象引用(for循环后,旧的Entry数组不再引用任何对象)
    do {
    Entry<K, V> next = e.next;
    int i = indexFor(e.hash, newCapacity); //!!重新计算每个元素在数组中的位置
    e.next = newTable[i]; //标记[1]
    newTable[i] = e; //将元素放在数组上
    e = next; //访问下一个Entry链上的元素
    } while (e != null);
    }
    }
    }

详情请看:HashMap的扩容机制—resize()

5. HashMap 1.7 与 1.8 的 区别,说明 1.8 做了哪些优化,如何优化的?

  • JDK1.7中

    • 使用一个Entry数组来存储数据,用key的hashcode取模来决定key会被放到数组里的位置,如果hashcode相同,或者hashcode取模后的结果相同(hash collision),那么这些key会被定位到Entry数组的同一个格子里,这些key会形成一个链表。在hashcode特别差的情况下,比方说所有key的hashcode都相同,这个链表可能会很长,那么put/get操作都可能需要遍历这个链表。也就是说时间复杂度在最差情况下会退化到O(n)
    • 在扩容的时候,需要重新计算hashcode的值(hash collision),然后放到对应的数组格子里
  • JDK1.8中

    • 使用一个Node数组来存储数据,但这个Node可能是链表结构,也可能是红黑树结构。如果插入的key的hashcode相同,那么这些key也会被定位到Node数组的同一个格子里。如果同一个格子里的key不超过8个,使用链表结构存储。如果超过了8个,那么会调用treeifyBin函数,将链表转换为红黑树。那么即使hashcode完全相同,由于红黑树的特点,查找某个特定元素,也只需要O(log n)的开销、也就是说put/get的操作的时间复杂度最差只有O(log n)
    • 在扩容时候,可直接根据原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”

详情请看:HashMap在Java1.7与1.8中的区别

6. final finally finalize

  • final 是修饰符,关键字。被final修饰的类,就不能再派生新的子类,不能作为父类被子类继承,如String类。
  • finally是异常处理时,提供的finally块。不管是走try块还是catch块,都会执行finally块的代码。通常用来做清除操作或关闭流。
  • finalze是一个方法。java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。

7. 强引用 、软引用、 弱引用、虚引用

  • 强引用(Strong Reference),是最难被GC回收的,宁可虚拟机抛出异常,中断程序,也不回收强引用指向的实例对象。

  • 软引用 (SoftReference),在内存不足时,GC会回收软引用指向的对象

  • 弱引用(WeakReference),不管内存足不足,只要我GC,我都可能回收弱引用指向的对象

  • 虚引用(PhantomReference ),该回收就回收,无所谓了,虚引用,我随便回收你,也叫幽灵引用,其实就是相当于没有指向任何实例对象

8. Java反射

  • 在不用new的情况下,可以通过.class获取类中的方法和属性。
  • 反射就是把java类中的各种成分映射成一个个的Java对象

9. Arrays.sort 实现原理和 Collection 实现原理

事实上Collections.sort方法底层就是调用的array.sort方法,Colletions.sort()实际会将list转为数组,然后调用Arrays.sort(),排完了再转回List。

  • JDK6里用的是快速排序,对于对象类型(Object[]),JDK6则使用归并排序

  • JDK7的进步
    到了JDK7,快速排序升级为双基准快排(双基准快排 vs 三路快排);归并排序升级为归并排序的改进版TimSort,一个JDK的自我进化。

  • JDK8的进步
    再到了JDK8, 对大集合增加了Arrays.parallelSort()函数,使用fork-Join框架,充分利用多核,对大的集合进行切分然后再归并排序,而在小的连续片段里,依然使用TimSort与DualPivotQuickSort(双轴快排)。
    (TimSort算法就是找到已经排好序数据的子序列,然后对剩余部分排序,然后合并起来)

10. LinkedHashMap的应用

  • LinkedHashMap是HashMap的子类,它与HashMap的不同是HashMap是无序的,而LinkedHashMap维护着一个运行于所有条目的双重链接列表。
  • LinkedHashMap除了支持默认的插入顺序,还支持访问顺序。所谓访问顺序(access-order)是指在迭代遍历列表中的元素时最近访问的元素会排在LinkedHashMap的尾部

11. cloneable接口实现原理

Cloneable没有定义任何的接口方法,该接口在这里起到了一种标识的作用,表明实现它的类具备了实例拷贝功能。要想使一个类具备拷贝实例的功能,那么除了要重写Object类的clone()方法外,还必须要实现Cloneable接口

详情请看: JavaSE学习随笔(一) Cloneable接口源码分析与技术细节

12. 异常分类以及处理机制

  • Error是无法处理的异常,比如OutOfMemoryError(内存溢出),一般发生这种异常,JVM会选择终止程序。因此我们编写程序时不需要关心这类异常。
  • Exception也就是我们经常见到的一些异常情况,这些异常是我们可以处理的异常,是所有异常类的父类。

    • RuntimeException运行时异常
    • RuntimeNoException非运行时异常包括IOException、SQLException等
  • unchecked exception(非受查异常),包括Error和RuntimeException,比如常见的NullPointerException、IndexOutOfBoundsException。对于RuntimeException,java编译器不要求必须进行异常捕获处理或者抛出声明,由程序员自行决定。

  • checked exception(受查异常),也称非运行时异常(运行时异常以外的异常就是非运行时异常),由代码能力之外的因素导致的运行时错误。java编译器强制程序员必须进行捕获处理,比如常见的有IOExeption和SQLException。如果不进行捕获或者抛出声明处理,编译都不会通过。

处理:捕获机制:try-catch-finally

13. wait和sleep的区别

  • wait 休眠后会释放对象锁
  • sleep 休眠后不会释放对象锁

14. 数组在内存中如何分配

在堆内存中开辟一块空间,存放数组。在栈中添加引用。

本文题目提供:金三银四跳槽季,Java面试大纲 作者:占小狼