面试题目

119 阅读2分钟

1、List 和 Set 的区别

Java中的集合包括三大类,Set、List和Map,他们都是接口,都有各自的实现类,Set的是实现类主要有HashSet和TreeSet,List的实现类主要有ArrayList,LinkedList和vector。

list是一个有序容器,会保持每个元素的插入顺序,即输入顺序就是输出顺序,而set方法是无序容器,无法保证每个元素的存储顺序,TreeSet通过Comparator或者Comparable维护一个排序顺序。list可以插入多个null,而Set只能插入一个。

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

向HashSet中add()元素时,判断元素是否存在的依据,不仅需要比较HashCode,还要结合equals()方法比较。

HashSet的add()方法会使用到HashMap的add()方法。

private static final Object PRESENT=new Object();
private transient HashMap<E,Object>map;
public HashSet(){
    map=new HashMap<>();
}
public boolean add(E e){
}

HashMap的key是唯一的,由上面的代码可以看出,HashSet添加进去的值,就是作为HashMap的key。所以不会重复(HashMap比较key是否相等,先比较hashCode,再比较equals)。HashMap线程是不安全的。

3.HashMap 是线程安全的吗,为什么不是线程安全的

线程是不安全的。如果有两个线程A和B,都进行插入数据,刚好这两条不同的数据经过哈希计算后得到的哈希码是一样的,且该位 置还没有其他的数据。所以这两个线程都会进入我在上面标记为1的代码中。假设一种情况,线程A通过if判断,该 位置没有哈希冲突,进入了if语句,还没有进行数据插入,这时候 CPU 就把资源让给了线程B,线程A停在了if语句 里面,线程B判断该位置没有哈希冲突(线程A的数据还没插入),也进入了if语句,线程B执行完后,轮到线程A执 行,现在线程A直接在该位置插入而不用再判断。这时候,你会发现线程A把线程B插入的数据给覆盖了。发生了线 程不安全情况。本来在 HashMap 中,发生哈希冲突是可以用链表法或者红黑树来解决的,但是在多线程中,可能 就直接给覆盖了。 上面所说的是一个图来解释可能更加直观。如下面所示,两个线程在同一个位置添加数据,后面添加的数据就覆盖 住了前面添加的。 private static final Object PRESENT = new Object(); private transient HashMap<E,Object> map; public HashSet() { map = new HashMap<>(); }public boolean add(E e) { return map.put(e, PRESENT)==null; } 如果上述插入是插入到链表上,如两个线程都在遍历到最后一个节点,都要在最后添加一个数据,那么后面添加数 据的线程就会把前面添加的数据给覆盖住。则 在扩容的时候也可能会导致数据不一致,因为扩容是从一个数组拷贝到另外一个数组。

4.HashMap的扩容过程

影响Resize的因素有两个:

1. Capacity:HashMap的当前长度。长度为2的幂

2.LoadFactor: HashMap负载因子,默认值为0.75f.

也就是现有长度(size)是目前容量的0.75的时候扩容。

Resize步骤:

1.扩容

创建一个新的Entry空数组,长度为元数组的2倍。

2.ReHash

遍历原Entry数组,把所有的Entry重新hash到新数组。

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

(1)JDK1.7用的是头插法,而JDK1.8及之后使用的都是尾插法。那么他们为什么要这样做呢?因为JDK1.7是用单链表进行的纵向延伸,当采用头插法时会容易出现逆序且环形链表死循环问题。但是在JDK1.8之后是因为加入了红黑树使用尾插法,能够避免出现逆序且链表死循环的问题。

(2)扩容后数据存储位置的计算方式也不一样:1.在JDK1.7的时候是直接用hash值和需要扩容的二进制数进行&,2.而在JDK1.8的时候,直接用了JDK1.7的时候计算的规律,也就是扩容前的原始位置+扩容的大小值=JDK1.8的计算方式,而不再是JDK1.7的那种异或的方法。但是这种方式就相当于只需要判断Hash值的新增参与运算的位是0还是1就直接迅速计算出了扩容后的储存方式。

(3)JDK1.7的时候使用的是数组+ 单链表的数据结构。但是在JDK1.8及之后时,使用的是数组+链表+红黑树的数据结构(当链表的深度达到8的时候,也就是默认阈值,就会自动扩容把链表转成红黑树的数据结构来把时间复杂度从O(n)变成O(logN)提高了效率)

6、final finally finalize

a、final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个 常量不能被重新赋值。
b、finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是 否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
c、finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调 用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。

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

1.强引用: 强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。如下:

Object o=new Object();   //  强引用

当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。如果不使用时,要通过如下方式来弱化引用,如下:

o=null;     // 帮助垃圾收集器回收此对象

2.软引用: 如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。

 String str=new String("abc");                                     // 强引用
 SoftReference<String> softRef=new SoftReference<String>(str);     // 软引用 

3.弱引用: 弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

4.虚引用: “虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

5.总结

强引用 > 软引用 > 弱引用 > 虚引用

8、Java反射

Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为Java语言的反射机制。

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

不论是Collections.sort方法或者是Arrays.sort方法,底层实现都是TimSort实现的,这是jdk1.7新增的,以前是归并排序。TimSort算法就是找到已经排好序数据的子序列,然后对剩余部分排序,然后合并起来

10、LinkedHashMap的应用