java面试(10) 关于hashMap

刘大飞 / 2023-05-27 / 原文

1: java的基本类型有几种?String是不是基本类型?  

在 Java 中一共有 8 种基本类型(primitive type)。

  • 4 种整型 int、short、long、byte

  • 2 种浮点型 float、 double

  • 1 种表示 Unicode 编码字符的类型(字符型) char

  • 1 种表示真值的类型 boolean

 String不是基本类型

2: 为什么String是final的?

       首先看final的特性:

     1) final修饰类: 不能被继承

          2) final修饰方法: 不能被重写

          3) final修饰变量: 都需要进行初始化操作

               4) 避免使用字符串拼接。可使用StringBuilder或StringBuffer类

       为什么String类是final的:

           1)为了实现字符串常量池:常量池是公用的,不能随便改

           2)为了线程安全,不会被随便改。

           3)不能被继承,也就不会被子类乱改。

           3)hashcode是用string实现的,具有不可变性,String的hashcode就不会变,适合作为map的key

       但是也有缺点: 因此对于每一次操作都会导致新的String对象被创建。这样就会占用大量的内存空间,尤其是在处理大量字符串时。此外,一些操作(如拼接)也会导致性能问题。解决这个问题的方法是使用StringBuilder或StringBuffer类。

         使用String的建议:

   1)尽可能使用字符串字面量,而不是字符串对象。

           2)如果必须使用字符串对象,请使用intern()方法将其添加到String Pool中

                    intern()方法是String类提供的一个方法,它的作用是在字符串池中查找当前字符串对象的引用。如果池中已经存在相同值的字符串,则返回池中的引用;如                果池中不存在相同值的字符串,则将当前字符串对象添加到池中并返回引用

   3)避免使用字符串拼接。可使用StringBuilder或StringBuffer类 

           4)使用String.format替代字符串拼接

                 综合考虑,对于简单的字符串拼接和少量字符串拼接的场景,可以使用字符串拼接。而对于复杂的字符串格式化、大量字符串拼接或需要处理不同类型数据的                 情 况,建议使用String.format。在性能要求较高的情况下,String.format通常比字符串拼接更有效,因为它可以避免创建大量的中间字符串对象。

           5) 使用StringUtils类进行字符串处理,而不是手动处理字符串。

3: hashmap如何实现的? 线程安全吗?

          Java 8中,由数组 + 链表(链表(阈值6) 或 红黑树(阈值8))。

          数组是主体(继承自Entry的Node,其包含key,value,hash和一个next引用,指向红黑树)

          链表或红黑树用来解决冲突,当链表超过8并且数据总量超过64才会转红黑树,如果小于64,则扩容。

          解决hash冲突时,为什么不直接用红黑树?而是选先用链表,再转红黑树?

               红黑树要进行左旋,右旋变色这些操作来保持平衡,单链表不用。 

         红黑树是一种特殊的二叉搜索树,红黑树保持了较好的平衡性,使得查找、插入和删除操作的时间复杂度保持在对数级别(O(log n))

         加载因子是0.75?为什么不是0.6或是8

         HashMap的加载因子(Load Factor)是指哈希表在自动扩容之前可以达到的平均填充程度。具体而言,加载因子是用当前元素数量与哈希表容量(哈希表的容量不包含链表,而是指哈希表底层数组的长度)的比值表示的。在HashMap中,加载因子的默认值为0.75。这意味着当哈希表中的元素数量超过容量的75%时,就会触发自动扩容操作。

         为什么hashmap数组的长度使2的幂次方

            performance高,不容易碰撞

   HashMap的put流程:

    首先根据key值计算hash值,找到应该存储的数组下标;

    如果数组还没有初始化,则进行初始化;

    如果没有哈希冲突,则直接放在对应的数组下标;

    如果冲突了(hash值同),并且key已经存在(key也存在),不管是红黑树还是链表,则覆盖value.

              如果冲突了(hash值同),并且key不存在,发现该节点是红黑树,就把节点挂在上。

              如果冲突后发现是链表,并且key不存在,要判断个数是否大于8,如果大于8并且数组容量小于64,就需要扩容(扩容会导致重新计算hash);链表大于8,如果容量也大于64,就需要转化成红黑树。

         HashMap的扩容机制:

   HashMap的扩容机制是在元素数量超过加载因子和容量的乘积时触发的(另外一种情况是前面提到的put),通过扩大底层数组的长度,可以增加哈希表的容量,使元素在哈希表中分布更加均匀。

            在扩容过程中,HashMap会创建一个新的容量为原容量的两倍的新数组。然后,它会遍历旧数组中的每个桶,将每个桶中的元素重新计算哈希码,并根据新的容量重新分配到新数组的相应位置上。这样就完成了哈希表的扩容操作。

        HashMap可以用null作为key

        一般用什么作为hashmap的key:

      一般用Integer,String 这种不可变类当做key. String最常用,String因为是不可变的,其创建的时候hashcode就已经创建好了,不用再重新计算了,性能高。

       hashmap的初始化:

      懒加载,第一个put的时候采取初始化。

       如果get一个元素:

   HashMpa会计算这个key的hash值,并对hashMap的容量求余,得到数组中key的位置。

           判断Key是否相等,如果相等就得到了想要的元素;

           如果不相等,表示发生了哈希冲突。那么判断对应位置的对象是否为TreeNode,如果不是就是链表,否则是红黑树。

   如果是链表,就从头遍历,比较key,取出对应的vlaue

           如果是红黑树,则从树中找节点是key的元素。

     初始化默认容量是16,也可以指定大一点的容量

      为什么重写equals方法的时候也要重写hashCode方法

              Java中,对于两个对象来说,如果它们的equals()方法返回true,那么它们的hashCode()方法必须返回相同的值。

             这是为了确保在使用哈希数据结构(如HashMapHashSet等)时能够正确地进行元素查找、插入和删除操作。   

  链表的插入方式由java 7的头插法编程尾插法  

 

     ConcurrentHashMap如何实现的线程安全

        总体而言,ConcurrentHashMap通过细粒度的分段锁、CAS操作、volatile关键字和并发控制等技术,实现了对多线程环境下的高并发访问的线程安全。这使得多个线程可以同时读取和修改ConcurrentHashMap,而不需要显式地加锁,提高了并发性能和吞吐量。

       Hashtable虽然是线程安全的,get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,性能较差

       HashTable和ConcurrentHashMpa都不允许key和value为null

     ConcurrentHashMap迭代器是强一致性还是弱一致性?HashMap呢?

           弱一致性,HashMap强一直性。ConcurrentHashMap可以支持在迭代过程中,向map添加新元素,而HashMap则抛出了ConcurrentModificationException,因为HashMap包含一个修改计数器,当你调用他的next()方法来获取下一个元素时,迭代器将会用到这个计数器。