• java面试之基础问题


    上一篇写了jvm相关的问题,这一篇写一写java基础知识。其实对于工作经验少的岗位,这一类问题会问的比较多,但是工作年限要求比较高的岗位,这类问题问的比较少,虽然少但是问的都是比较深入的,所以这一部分可能会写的比较少。

    HashMap

    这是八股文里很经典的内容了。
    HashMap的底层数据结构是数组+链表/红黑树。具体的添加一个键值对的过程如下所示:
    在这里插入图片描述
    常见的问题有:
    1.hashmap如何计算hash值的:
    将key的hashCode()高16位与低16位进行异或运算(这样可以保留高位的特征,避免一些key的hashCode高位不同,低位相同,造成hash冲突)
    2.hashmap的容量及扩容时的size为什么都是2的幂次倍数?
    因为当n是2的整数次幂时,在数学上hash mod n的结果等于hash&(n-1),而对于计算机来说,计算位运算的速度更快。
    3.hashmap在resize的时候会出现成环问题
    这个问题只会发生在JDK1.8之前。1.7中扩容的代码为

    void transfer(Entry[] newTable, boolean rehash) {
        int newCapacity = newTable.length;
        for (Entry<K,V> e : table) {//遍历原数组的每一个下标
            while(null != e) {
                    //将数组下标下所有元素一个一个迁移到新数组,使用头插法
                Entry<K,V> next = e.next;
                if (rehash) {
                    e.hash = null == e.key ? 0 : hash(e.key);
                }
                int i = indexFor(e.hash, newCapacity);
                e.next = newTable[i];
                newTable[i] = e;
                e = next;
            } 
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    JDK1.7版本的HashMap中,两个线程同时添加一个新的键值对,然后同时触发扩容时,两个线程都会进行扩容,就会造成前一个线程将某个数组下标的元素迁移过去后,另一个线程又进行迁移。假设原数组下标下有node1->node2->null,这样一个链表,线程A和线程B都在迁移,都拿到了node1,假设线程A先执行,将两个节点迁移到新的数组,假设node1和node2在新数组还是在同一下标下,那么迁移后的链表是node2->node1->null,此时如果线程B还在迁移,拿到node1又迁移,会让node1-next=node2,从而让node1和node2形成环。
    在1.8中,更改了迁移的方法。首先迁移时不是拿到一个键值对就迁移一个了,而是对一个数组下标下的链表进行遍历,根据hash值的不同,分成两个链表,然后将两个链表分别设置到新的数组的下标下。

    if (oldTab != null) {
                for (int j = 0; j < oldCap; ++j) {
                    Node<K,V> e;
                    if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;
                        if (e.next == null)
                            newTab[e.hash & (newCap - 1)] = e;
                        else if (e instanceof TreeNode)
                            ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                        else { // preserve order
                            Node<K,V> loHead = null, loTail = null;
                            Node<K,V> hiHead = null, hiTail = null;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43

    在JDK1.8以后,不需要rehash,因为键值对的Hash值主要是根据key的hashCode()的高16位与低16位进行异或计算后得到,根据hash%length,计算得到数组的下标index,因为length是2的整数次幂,当扩容后length变为原来的两倍时,hash%(2length)的计算结果结果差别在于第length位的值是1还是0,如果是0,那么在新数组中的index与旧数组的一致,如果是1,在新数组中的index会是旧数组中的数组中的index+length。故而直接可以分成两个数组。
    4.hashmap什么时候会触发扩容?
    在没有指定初始长度的情况下,HashMap数组的默认长度为16,在添加一个新的键值对时,会调用putVal()方法,在方法中,成功添加一个新的键值对以后,会判断当前的元素个数是否超过阀值(数组长度
    负载因子0.75),如果超过那么调用resize方法进行扩容。
    为了避免链表长度过长,影响查找元素的效率,当链表的长度>8时,会将链表转换为红黑树,链表的长度<6时,将红黑树转换为链表(但是红黑树转换为链表的时机不是在删除链表时,而是在扩容时,发现红黑树分解后的两个链表<6,就按链表处理,否则就建立两个小的红黑树,设置到扩容后的位置)。之所以临界点为8是因为红黑树的查找时间复杂度为logN,链表的平均时间查找复杂度为N/2,当N为8时,logN为3,是小于N/2的,正好可以通过转换为红黑树减少查找的时间复杂度。
    5.concurrentHashMap是如何保证线程安全的?
    使用了CAS+synchronized来保证线程安全的。
    6.另外一个跟hashmap有关系的问题:为什么重写hashcode方法时也要重写equals方法?
    首先我们知道,在hashmap中计算key的位置是通过对key的hashcode方法计算hash值进一步处理得到的,而判断key是否存在是先通过hash值在通过equals方法的,且hashmap中是不允许有重复key的。tips:正常来说,hashcode不相等的两个对象一定不相等,重写的时候要保证这一条。
    ①只重写hashcode方法:可能会出现相同的两个对象,hash值相同,但是equals方法返回的是false(因为默认的equals方法比较的是地址),这样就达不到去重的效果了
    ②只重写equals方法:可能会出现相同的两个对象但是hash值不同,这样就不会再去调用equals方法比较,会导致hashmap中有重复的key,无法去重。
    (tips:曾经在面试中被问到过这个问题,尴尬的是一紧张全忘记了,说的糊糊涂涂)
    7.TreeMap的底层数据结构
    虽然它实现了map这个接口,但是treemap底层其实是红黑树,默认会根据key进行排序。

    String

    1.String类为什么设计成不可变的?
    ①jdk源码中是这么写的private final char value[] ,是用final来修饰的,所以是不可变的;但是这个不可变其实只是地址不可变,你对数组中的元素进行改变,这个final是限制不了的;
    ②工程师设计的时候避免了对数组元素的改变。这样可以使用常量池这个东西,另外在多线程的时候就不会存在线程安全的问题了,方便了大家的使用。
    2.String,StringBuffer,StringBuilder三个类有什么区别?
    String是不可变的,是字符串常量,不适合不停的修改字符串;
    StringBuffer是可变的,是字符串变量,线程安全,但是效率比较低;
    StringBuilder也是可变的,线程不安全,但是效率会比StringBuffer高一点。

    char

    这个方面问的比较少,只有快手问过一次,算是他们的经典面试题了吧。
    1.char在java中占几个字节?
    2个
    2.char能存储汉字吗?
    可以
    3.2个字节如何保存汉字?2个字节上线是65536
    java使用的Unicode编码,可以表示汉字,但是不能表示所有的汉字,只有在unicode字符集里边的汉字才能被标识。

  • 相关阅读:
    ETL实现实时文件监听
    都知道低代码,但你真的了解低代码吗?
    Autosys Introduction and Architecture
    vue router 路由跳转获取不到参数
    springboot+vue+nodejs游戏资讯攻略分享网站java
    cnpm的安装与使用
    形态学操作—膨胀
    Android入门第33天-Android里的弹出式对话框
    使用VSCode编辑与编译WSL2下源代码
    Linux
  • 原文地址:https://blog.csdn.net/weixin_45614626/article/details/125410245