028-86922220

建站动态

根据您的个性需求进行定制 先人一步 抢占小程序红利时代

03.Java数据结构问题

目录介绍

好消息

3.0.0.1 在arrayList中System.arraycopy()和Arrays.copyOf()方法区别联系?System.arraycopy()和Arrays.copyOf()代码说明?
3.0.0.2 SparseArray基本介绍,相比HashMap为什么性能会好?
3.0.0.3 Arrays和Collections 对于sort的不同实现原理?说一说它们的区别……
3.0.0.4 Java集合框架中有哪些类?都有什么特点?Java集合的快速失败机制 “fail-fast”?
3.0.0.5 ArrayList,Vector和LinkList的区别,底层分别是怎么实现的?存储空间是如何扩容的?什么是加载因子?
3.0.0.6 如何理解ArrayList的扩容消耗?Arrays.asList方法后的List可以扩容吗?ArrayList如何序列化?
3.0.0.7 如何理解list集合读写机制和读写效率?什么是CopyOnWriteArrayList,它与ArrayList有何不同?
  • 读写机制
    • ArrayList在执行插入元素是超过当前数组预定义的最大值时,数组需要扩容,扩容过程需要调用底层System.arraycopy()方法进行大量的数组复制操作;在删除元素时并不会减少数组的容量(如果需要缩小数组容量,可以调用trimToSize()方法);在查找元素时要遍历数组,对于非null的元素采取equals的方式寻找。
    • LinkedList在插入元素时,须创建一个新的Entry对象,并更新相应元素的前后元素的引用;在查找元素时,需遍历链表;在删除元素时,要遍历链表,找到要删除的元素,然后从链表上将此元素删除即可。
    • Vector与ArrayList仅在插入元素时容量扩充机制不一致。对于Vector,默认创建一个大小为10的Object数组,并将capacityIncrement设置为0;当插入元素数组大小不够时,如果capacityIncrement大于0,则将Object数组的大小扩大为现有size+capacityIncrement;如果capacityIncrement<=0,则将Object数组的大小扩大为现有大小的2倍。
  • 读写效率
    • ArrayList对元素的增加和删除都会引起数组的内存分配空间动态发生变化。因此,对其进行插入和删除速度较慢,但检索速度很快。
    • LinkedList由于基于链表方式存放数据,增加和删除元素的速度较快,但是检索速度较慢。
  • 什么是CopyOnWriteArrayList,它与ArrayList有何不同?
    • CopyOnWriteArrayList是ArrayList的一个线程安全的变体,其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的。相比较于ArrayList它的写操作要慢一些,因为它需要实例的快照。
    • CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差,但是读操作因为操作的对象和写操作不是同一个对象,读之间也不需要加锁,读和写之间的同步处理只是在写完后通过一个简单的"="将引用指向新的数组对象上来,这个几乎不需要时间,这样读操作就很快很安全,适合在多线程里使用,绝对不会发生ConcurrentModificationException ,因此CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。
    • 技术博客大总结
3.0.1.0 HashSet和TreeSet的区别?是如何保证唯一值的,底层怎么做到的?
  • HashSet
    • 不能保证元素的排列顺序;使用Hash算法来存储集合中的元素,有良好的存取和查找性能;通过equal()判断两个元素是否相等,并两个元素的hashCode()返回值也相等
  • TreeSet
    • 是SortedSet接口的实现类,根据元素实际值的大小进行排序;采用红黑树的数据结构来存储集合元素;支持两种排序方法:自然排序(默认情况)和定制排序。前者通过实现Comparable接口中的compareTo()比较两个元素之间大小关系,然后按升序排列;后者通过实现Comparator接口中的compare()比较两个元素之间大小关系,实现定制排列
3.0.1.5 HashMap和Hashtable的区别?HashMap在put、get元素的过程?体现了什么数据结构?
  • HashMap
    • 基于AbstractMap类,实现了Map、Cloneable(能被克隆)、Serializable(支持序列化)接口; 非线程安全;允许存在一个为null的key和任意个为null的value;采用链表散列的数据结构,即数组和链表的结合;初始容量为16,填充因子默认为0.75,扩容时是当前容量翻倍,即2capacity
  • Hashtable
    • 基于Map接口和Dictionary类;线程安全,开销比HashMap大,如果多线程访问一个Map对象,使用Hashtable更好;不允许使用null作为key和value;底层基于哈希表结构;初始容量为11,填充因子默认为0.75,扩容时是容量翻倍+1,即2capacity+1
    • HashTable里使用的是synchronized关键字,这其实是对对象加锁,锁住的都是对象整体,当Hashtable的大小增加到一定的时候,性能会急剧下降,因为迭代时需要被锁定很长的时间。
  • HashMap在put、get元素的过程
    • 向Hashmap中put元素时,首先判断key是否为空,为空则直接调用putForNullKey(),不为空则计算key的hash值得到该元素在数组中的下标值;如果数组在该位置处没有元素,就直接保存;如果有,还要比较是否存在相同的key,存在的话就覆盖原来key的value,否则将该元素保存在链头,先保存的在链尾。
    • 从Hashmap中get元素时,计算key的hash值找到在数组中的对应的下标值,返回该key对应的value即可,如果有冲突就遍历该位置链表寻找key相同的元素并返回对应的value
  • 体现了什么数据结构
    • HashMap采用链表散列的数据结构,即数组和链表的结合,在Java8后又结合了红黑树,当链表元素超过8个将链表转换为红黑树
    • 技术博客大总结
3.0.1.6 如何保证HashMap线程安全?底层怎么实现的?HashMap是有序的吗?如何实现有序?
  • 使用ConcurrentHashMap可保证线程安全
    • ConcurrentHashMap是线程安全的HashMap,它采取锁分段技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。在JDK1.8中对ConcurrentHashmap做了两个改进:
      • 取消segments字段,直接采用transient volatile HashEntry[] table保存数据,将数组元素作为锁,对每一行数据进行加锁,可减少并发冲突的概率
      • 数据结构由“数组+单向链表”变为“数组+单向链表+红黑树”,使得查询的时间复杂度可以降低到O(logN),改进一定的性能。
    • 通俗一点解释:ConcurrentHashMap引入了分割(Segment),可以理解为把一个大的Map拆分成N个小的HashTable,在put方法中,会根据hash(paramK.hashCode())来决定具体存放进哪个Segment,如果查看Segment的put操作,我们会发现内部使用的同步机制是基于lock操作的,这样就可以对Map的一部分(Segment)进行上锁,这样影响的只是将要放入同一个Segment的元素的put操作,保证同步的时候,锁住的不是整个Map(HashTable就是这么做的),相对于HashTable提高了多线程环境下的性能,因此HashTable已经被淘汰了。技术博客大总结
  • 使用LinkedHashMap可实现有序
    • HashMap是无序的,而LinkedHashMap是有序的HashMap,默认为插入顺序,还可以是访问顺序,基本原理是其内部通过Entry维护了一个双向链表,负责维护Map的迭代顺序
3.0.1.7 HashMap存储两个对象的hashcode相同会发生什么?如果两个键的hashcode相同,你如何获取值对象?
  • HashMap存储两个对象的hashcode相同会发生什么?
    • 错误回答:因为hashcode相同,所以两个对象是相等的,HashMap将会抛出异常,或者不会存储它们。
    • 正确回答:两个对象就算hashcode相同,但是它们可能并不相等。如果不明白,可以先看看我的这篇博客:Hash和HashCode深入理解。回答“因为hashcode相同,所以它们的bucket位置相同,‘碰撞’会发生。因为HashMap使用链表存储对象,这个Entry(包含有键值对的Map.Entry对象)会存储在链表中。
  • HashMap1.7和1.8的区别
    • 在JDK1.6,JDK1.7中,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个链表中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
    • JDK1.8中,HashMap采用位数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
  • 如果两个键的hashcode相同,你如何获取值对象?
    • 当调用get()方法,HashMap会使用键对象的hashcode找到bucket位置,然后获取值对象。当然如果有两个值对象储存在同一个bucket,将会遍历链表直到找到值对象。
    • 在没有值对象去比较,如何确定确定找到值对象的?因为HashMap在链表中存储的是键值对,找到bucket位置之后,会调用keys.equals()方法去找到链表中正确的节点,最终找到要找的值对象。
    • 技术博客大总结
3.0.1.8 HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
  • 不直接使用hashCode()处理后的哈希值
    • hashCode()方法返回的是int整数类型,其范围为-(2^31)~(2^31-1),约有40亿个映射空间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最大值的,并且设备上也难以提供这么多的存储空间,从而导致通过hashCode()计算出的哈希值可能不在数组大小范围内,进而无法匹配存储位置;
  • HashMap是使用了哪些方法来有效解决哈希冲突的
    • 1.使用链地址法(使用散列表)来链接拥有相同hash值的数据;
    • 2.使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;
    • 3.引入红黑树进一步降低遍历的时间复杂度,使得遍历更快;
  • 如何解决匹配存储位置问题
    • HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
    • 在保证数组长度为2的幂次方的时候,使用hash()运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;
  • 为什么数组长度要保证为2的幂次方呢?
    • 只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率;
    • 技术博客大总结
    • 如果 length 为 2 的次幂 则 length-1 转化为二进制必定是 11111……的形式,在于 h 的二进制与操作效率会非常的快,而且空间不浪费;如果 length 不是 2 的次幂,比如 length 为 15,则 length - 1 为 14,对应的二进制为 1110,在于 h 与操作,最后一位都为 0 ,而 0001,0011,0101,1001,1011,0111,1101 这几个位置永远都不能存放元素了,空间浪费相当大,更糟的是这种情况中,数组可以使用的位置比数组长度小了很多,这意味着进一步增加了碰撞的几率,减慢了查询的效率!这样就会造成空间的浪费。
3.0.1.9 为什么HashMap中String、Integer这样的包装类适合作为K?为啥不用其他作key值?
  • 为什么HashMap中String、Integer这样的包装类适合作为K?
    • String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
      • 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
      • 内部已重写了equals()、hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;
  • 想要让自己的Object作为K应该怎么办呢?
    • 重写hashCode()和equals()方法
      • 重写hashCode()是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
      • 重写equals()方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;
  • 总结
    • 采用合适的equals()和hashCode()方法的话,将会减少碰撞的发生,提高效率。不可变性使得能够缓存不同键的hashcode,这将提高整个获取对象的速度,使用String,Interger这样的wrapper类作为键是非常好的选择。
    • 技术博客大总结
3.0.2.0 HashMap是如何扩容的?如何理解HashMap的大小超过了负载因子定义的容量?重新调整HashMap大小存在什么问题吗?
  • HashMap是为啥要扩容
    • 当链表数组的容量超过初始容量*加载因子(默认0.75)时,再散列将链表数组扩大2倍,把原链表数组的搬移到新的数组中。为什么需要使用加载因子?为什么需要扩容呢?因为如果填充比很大,说明利用的空间很多,如果一直不进行扩容的话,链表就会越来越长,这样查找的效率很低,扩容之后,将原来链表数组的每一个链表分成奇偶两个子链表分别挂在新链表数组的散列位置,这样就减少了每个链表的长度,增加查找效率。
  • 如何理解HashMap的大小超过了负载因子(load factor)定义的容量?
    • 默认的负载因子大小为0.75,也就是说,当一个map填满了75%的bucket时候,和其它集合类(如ArrayList等)一样,将会创建原来HashMap大小的两倍的bucket数组,来重新调整map的大小,并将原来的对象放入新的bucket数组中。这个过程叫作rehashing,因为它调用hash方法找到新的bucket位置。
  • 重新调整HashMap大小存在什么问题吗?技术博客大总结
    • 当多线程的情况下,可能产生条件竞争。当重新调整HashMap大小的时候,确实存在条件竞争,因为如果两个线程都发现HashMap需要重新调整大小了,它们会同时试着调整大小。在调整大小的过程中,存储在链表中的元素的次序会反过来,因为移动到新的bucket位置的时候,HashMap并不会将元素放在链表的尾部,而是放在头部,这是为了避免尾部遍历(tail traversing)。如果条件竞争发生了,那么就死循环了。

其他介绍

01.关于博客汇总链接
  • 1.技术博客汇总
  • 2.开源项目汇总
  • 3.生活博客汇总
  • 4.喜马拉雅音频汇总
  • 5.其他汇总
02.关于我的博客
  • 我的个人站点:www.yczbj.org,www.ycbjie.cn
  • github:https://github.com/yangchong211
  • 知乎:https://www.zhihu.com/people/yang-chong-69-24/pins/posts
  • 简书:http://www.jianshu.com/u/b7b2c6ed9284
  • csdn:http://my.csdn.net/m0_37700275
  • 喜马拉雅听书:http://www.ximalaya.com/zhubo/71989305/
  • 开源中国:https://my.oschina.net/zbj1618/blog
  • 泡在网上的日子:http://www.jcodecraeer.com/member/content_list.php?channelid=1
  • 邮箱:yangchong211@163.com
  • 阿里云博客:https://yq.aliyun.com/users/article?spm=5176.100- 239.headeruserinfo.3.dT4bcV
  • segmentfault头条:https://segmentfault.com/u/xiangjianyu/articles
  • 掘金:https://juejin.im/user/5939433efe88c2006afa0c6e

名称栏目:03.Java数据结构问题
网站链接:http://www.tsicrk.com/article/iigiso.html
  • 网站建设专属方案

  • 网站定制化设计

  • 7X24小时服务

  • N对管家服务

让你的专属顾问为你服务

0.6976s