在之前的文章中,已经发布了常见的面试题,这里我花了点时间整理了一下对应的解答,由于个人能力有限,不一定完全到位,如果有解答的不合理的地方还请指点,在此谢过。

本文主要描述的是java面试中几乎必问的集合内容,java的集合是jdk里面重要的内容,也是我们平时开发过程中最常用到的,所以无论是否为了准备面试,我们都要掌握好集合相关的知识。既然是重点,那就意味着集合类在java面试中的题目会非常多,本文中点描述map,尤其会解析hashmap的基本操作和相关的源码解析,对于可能涉及到的多线程concurrenthashmap,会放到后续多线程里面说明。

说下HashMap的put和get流程?

我们直接看下代码,put流程:

//存入值  final V putVal(int hash, K key, V value, boolean onlyIfAbsent,  boolean evict) {  Node<K,V>[] tab; Node<K,V> p; int n, i;  if ((tab = table) == null || (n = tab.length) == 0)//如果hash未初始化  n = (tab = resize()).length;//扩容,返回大小  if ((p = tab[i = (n - 1) & hash]) == null)//(n - 1) & hash 是对hash值对n求余,代表该hash桶为空  tab[i] = newNode(hash, key, value, null);//该hash桶存入第一个元素  else {  Node<K,V> e; K k;  if (p.hash == hash &&  ((k = p.key) == key || (key != null && key.equals(k))))//如果是同一个key,直接覆盖  e = p;  else if (p instanceof TreeNode)  e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);//如果是红黑树,添加一个节点  else {  for (int binCount = 0; ; ++binCount) {  if ((e = p.next) == null) {//p的下一个节点是空值  p.next = newNode(hash, key, value, null);//创建新的节点  if (binCount >= TREEIFY_THRESHOLD - 1) // 判断是否大于门限值  treeifyBin(tab, hash);//将链表转换为红黑树  break;  }  if (e.hash == hash &&  ((k = e.key) == key || (key != null && key.equals(k))))//如果是当前这个节点  break;//直接退出  p = e;//否则,进行下一个节点循环  }  }  if (e != null) { // 找到 为该key的值  V oldValue = e.value;  if (!onlyIfAbsent || oldValue == null)//判断是否更新  e.value = value;  afterNodeAccess(e);//回调获取到相同key的动作  return oldValue;  }  }  ++modCount;  if (++size > threshold)  resize();//如果节点大于门限值,扩容  afterNodeInsertion(evict);//回调节点添加完成的动作  return null;  }  

我们把上面的代码的基本流程图梳理如下:

Get方法的代码如下:

final Node<K,V> getNode(int hash, Object key) {  Node<K,V>[] tab; Node<K,V> first, e; int n; K k;  if ((tab = table) != null && (n = tab.length) > 0 &&  (first = tab[(n - 1) & hash]) != null) {//map中存在值,长度>0,且该hash桶中存在值,求hash桶的过程  if (first.hash == hash && // 判断第一个hash值是否相等  ((k = first.key) == key || (key != null && key.equals(k))))//判断key是不是相等,且equeals是否相等  return first;  if ((e = first.next) != null) {//寻找同一个hash桶后面的数据  if (first instanceof TreeNode)//如果是树节点的话,从红黑树中找出该值  return ((TreeNode<K,V>)first).getTreeNode(hash, key);  do {  if (e.hash == hash &&  ((k = e.key) == key || (key != null && key.equals(k))))//从链表中找出该值  return e;  } while ((e = e.next) != null);  }  }  return null;  }  

从上面的流程我们可以看出,hashmap基本会找到hash桶的第一个值,立马判断,这样可以加快速度。

说说hashMap的hash函数的实现?

我们直接看下代码是怎么实现的:

static final int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}

这里有三个流程:求key的hashcode值h,h右移16位h’,h和h’进行异或。

为什么会有这么复杂,直接hashcode不行么?实际上,我们大概会有一个猜想就是hash函数的好坏决定了hash碰撞的次数。所以这样的设计很大程度上是为了减少hash碰撞。一个int值右移16位就变成了只有高16位。所以这个hash函数是原来hash值的高低16位进行异或。这样做是否能降低碰撞?上面我们在看put和get的过程时看到计算hash桶是n-1 &hash,n的取值是2^n(n>=4),(至于n的长度下面我们会说到)所以n-1基本就是15,31,63...对应的二进制就是:

在n不是很大的情况下,基本上高16位都是0,这个如果直接和key.hash进行&运算的话,那就会导致key的hash高16位全部为0.如果我们将key.hash高低位进行异或就能把高位的信息也保留下来。这样就大大减少hash碰撞的可能了。

说说hashMap的扩容过程?啥时候会发生扩容?

Hashmap的初始容量是16。这个在代码中定义如下:

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;

然后看下具体的resize过程:

final Node<K,V>[] resize() {  Node<K,V>[] oldTab = table;  int oldCap = (oldTab == null) ? 0 : oldTab.length;  int oldThr = threshold;  int newCap, newThr = 0;  if (oldCap > 0) {  if (oldCap >= MAXIMUM_CAPACITY) {//如果旧的容量大于等于最大值  threshold = Integer.MAX_VALUE;  return oldTab;  }  else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&  oldCap >= DEFAULT_INITIAL_CAPACITY)//老的容量*2  newThr = oldThr << 1; // double threshold门限值*2  }  else if (oldThr > 0) // initial capacity was placed in threshold  newCap = oldThr;  else {               // zero initial threshold signifies using defaults  newCap = DEFAULT_INITIAL_CAPACITY;  newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//默认初始值  }  if (newThr == 0) {  float ft = (float)newCap * loadFactor;  newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?  (int)ft : Integer.MAX_VALUE);  }  threshold = newThr;  @SuppressWarnings({"rawtypes","unchecked"})  Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//初始化扩容后hash桶数组  table = newTab;  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;//e.hash & (newCap - 1)为对应的新的hash桶的位置  else if (e instanceof TreeNode)  ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);//红黑树迁移  else { // 链表迁移,将被分成两个链表,高位和低位,这个是基于扩容为2倍,计算新的时候就是最高位的0或1来决定  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) {//为0是低位  if (loTail == null)  loHead = e;  else  loTail.next = e;  loTail = e;  }  else {//1是高位  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;//高位的被分离出去,移到(j + oldCap)位置  }  }  }  }  }  return newTab;  }  

整个扩容过程:

这里有两点是需要注意的:

  1. 每次扩容是2的倍数的原因在于:使用2的倍数可以将%转换为&。试想一下,一般的hash桶计算就是对长度%,如果长度为2的倍数的时候,%可以转换为&。比如:21%16 == 21&(16-1)
  2. 在进行数据迁移的时候,如果hash&oldCap ==0 放入原来的(old)桶位置,而==1 放入(old+oldCap)桶中,这个是什么意思呢?我们看一个示意图:假设原来的16,扩容后32,原来的hash值为15,31都存在第16号桶,扩容后就会变成15在原来的16号桶,31到了扩容后的32号桶。

说完了扩容的过程,我们看下啥时候扩容,这里有个概念需要提一下,负载因子,hashmap的默认负载因子是0.75(至于为什么是0.75,源码的解释是基于时间和空间的考虑)当以下条件时会发生扩容

  1. 当存入的个数大于hash桶长度的0.75倍时
  2. 当转换为红黑树的时候,发现长度比红黑树的最小值(64)还小的时候,会进行扩容。

说说hashMap的数据结构?

说说hashMap中使用红黑树的作用?为什么不一直使用红黑树?什么时候进行转换?

当hash桶某个节点的数量太多时,如果一直使用链表的话,会导致查询的速度也很慢,为了提高查询的数据,在某个hash桶中的数量大于等于8的时候,会发生转换,将链表转换为红黑树。在扩容时候,红黑树的节点数少于等于6个的时候,红黑树会退化成链表。

红黑树为了达到重平衡的功能,需要进行左右旋转和换色操作,在节点数较少时使用红黑树反而导致效率变低,所以在节点数过少的时候,采用链表。

用最简单的语言描述下红黑树,以及其涉及的三个操作,换色,左旋和右旋?

红黑树有几个重要的特性:

1)节点不是黑就是红

2)叶子节点是黑色

3)叶子节点到根节点之间的黑色节点数一样多(保证一定的查找速率)

4)红色节点的子节点是黑色

5)根节点是黑节点

在描述操作之前,我们需要知道所有插入的点为红色,

换色条件:当前节点的父亲节点和叔叔节点也是红色节点

操作:

  1. 父节点和叔叔节点设置为黑色
  2. 祖父节点设置为红色
  3. 移动指针到祖父节点

左旋:父节点为红色,叔叔节点为黑色,且当前节点是右子树

操作:以父节点进行左旋

右旋:父亲节点是红色,叔叔节点是黑色,在左边的时候

操作:

  1. 把父亲节点变为黑色
  2. 把祖父节点变为红色
  3. 以祖父节点右旋

下面看下左右旋转的示意图:

hashMap、hashtable和concurrentHashMap的区别?

Concurrent系列是java线程安全的集合类,在这里不做详细说明。只是简单的对比一下几个map的特性。可以认为hashtable是jdk一段时间的产物,在concurrenthashmap出现之后,基本就被完全替换了。Hashtable和concurrenthashmap都是线程安全的,而concurrenthashmap基本和hashmap实现一致,除了线程安全之外。具体的区别如下:

Hashmap

Hashtable

ConcurrenthashMap

初始容量

16

11

16

Key、value是否可为空

可以

不可以

不可以

线程是否安全

不安全

安全

安全

Hash桶的计算

hash&(tab.length-1)

(hash&0x7FFFFFFF) % tab.length

hash&(tab.length-1)

Hashmap、treemap、linkedhashmap的区别?

linkedHashMap 底层是hashmap+双向链表的结构,其作用是使用双向链表记录放入的顺序性,而hashmap默认存入是无序的。Treemap底层使用的是红黑树结构存储,其排列的先后顺序是按照key的顺序来排列的。所以基本上我们认为如果一个map想按照某个顺序排列,会使用treemap,如果想要记录插入的先后顺序会使用linkedhashmap。(更多关于linkedhashmap和treemap的源码知识,可以观看公众号里面jdk源码系列文章)

设计实现一个LRU?

LRU(Last recent used)是我们所说的最近最少访问,这是缓存替换的一个常用算法。由于linkedhashmap记录了先后顺序,所以我们一般会使用它来实现。

import java.util.LinkedHashMap;import java.util.Map;public class LRU<K, V> {private final float loadFactory = 0.75f;private LinkedHashMap<K, V> map;public LRU(int maxCacheSize) {int capacity = (int)Math.ceil(maxCacheSize / this.loadFactory) + 1;map = new LinkedHashMap<K, V>(capacity, loadFactory, true) {//核心在于重写该方法,当容量超过maxCacheSize会移除first@Overrideprotected boolean removeEldestEntry(Map.Entry eldest) {return size() > maxCacheSize;}};}public void put(K key, V value) {map.put(key, value);}public boolean contain(K key) {return map.containsKey(key);}public V remove(K key) {return map.remove(key);}public V get(K key) {return map.get(key);}}

再次强调无论是我面试别人或者被其他人面试,hashmap的都是面试的重点内容,其设计的优美让绝大部分面试官都对这里面的细节很感兴趣。所以务必重点关注本节内容。

本文的内容就这么多,如果你觉得对你的学习和面试有些帮助,帮忙点个赞或者转发一下哈,谢谢。

想要了解更多java内容(包含大厂面试题和题解)可以关注公众号,也可以在公众号留言,帮忙内推阿里、腾讯等互联网大厂哈

Java基础篇--集合(map)相关推荐

  1. Java基础进阶集合-map集合

    1.Map和Collection没有继承关系. 2.Map集合以key和value的方式存储数据:键值对 key和value都是引用数据类型. key和value都是存储对象的内存地址. key起到主 ...

  2. java把map值放入vector_Thinking in java基础之集合框架

    Thinking in java基础之集合框架 大家都知道我的习惯,先上图说话. 集合简介(容器) 把具有相同性质的一类东西,汇聚成一个整体,就可以称为集合,例如这里有20个苹果,我们把每一个苹果当成 ...

  3. Java基础篇--设计模式

    目录 前言 设计模式 创建型模式 单例模式 工厂方法模式 抽象工厂模式 建造者模式 原型模式 结构型模式 适配器模式 桥接模式 组合模式 装饰模式 外观模式 亨元模式 代理模式 行为型模式: 访问者模 ...

  4. 《Java 后端面试经》Java 基础篇

    <Java 后端面试经>专栏文章索引: <Java 后端面试经>Java 基础篇 <Java 后端面试经>Java EE 篇 <Java 后端面试经>数 ...

  5. 【程序员养成之路】Java基础篇 8-流进流出的IO流(二)

    以下内容若有误,欢迎私信我或在下方留言,谢谢^_− 目录 IO流(二) 1.特殊操作流 1.1 标准流 1.2 打印流 1.3 对象序列化流 1.4 Properties 拓展1:比较字节流和字节缓冲 ...

  6. Thinking in java基础之集合框架

    Thinking in java基础之集合框架 大家都知道我的习惯,先上图说话. 集合简介(容器) 把具有相同性质的一类东西,汇聚成一个整体,就可以称为集合,例如这里有20个苹果,我们把每一个苹果当成 ...

  7. 菜鸟学习笔记:Java基础篇6(数组、字符串)

    菜鸟学习笔记:Java常用类(数组.字符串) 数组 概述 数组的定义 二维数组 数组查找和排序 查找 排序 数组运用--字符串 不可变字符序列(String) 可变字符序列(StringBuilder ...

  8. 菜鸟学习笔记:Java基础篇2(变量、运算符、流程控制语句、方法)

    菜鸟学习笔记:Java基础篇2 变量 基本概念 变量作用域 final关键字 运算符 算术运算符 比较运算符 逻辑运算符 位运算符 赋值运算符 条件运算符 运算符优先级 Java三种流程控制语句 顺序 ...

  9. java基础篇---第一天

    今日开始在心中正式开始在培训班开始培训.一下是在培训的过程中发现自己在自学过的过程中发现的问题.这篇是java基础篇. 第一天 : 1)配置java环境变量 1.在系统变量中新建JAVA_HOME:j ...

  10. Java基础篇 学习笔记

    List item Java基础篇 学习笔记 java基础篇 第1章 计算机.程序和java概述 学习笔记 1.1什么是计算机 简单来说:计算机就是 ' 存储 ' 和 ' 处理 ' 数据的电子设备. ...

最新文章

  1. 我如何进入Docker容器的shell?
  2. SharePoint 2013 商城
  3. 200多位阿里工程师齐聚“光明顶”,双11模拟演习怎么搞?
  4. bootstrap-select实现下拉框多选效果
  5. oracle简版如何打开,如何打开和关闭Oracle游标
  6. feign一个接口多个方法_spring cloud 建一个服务消费者client-feign(最好用这种方式)...
  7. myeclipse 9.0安装 vss1.6.2不显示问题
  8. C#的同步和异步调用方法
  9. GARFIELD@11-20-2004
  10. The labor day's schedule and the rate of process
  11. 电子元器件选型——二极管
  12. Ubuntu软件仓库管理
  13. ccf csp 201612-2 工资计算
  14. python中oserror winerror,在python中将WindowsError转换为OSError
  15. Python爬虫技术及PyQt5界面编程实现12306火车票查询
  16. vue省市区三级联动(地区编码)
  17. 自定义 Oh My Zsh 主题 cchi.zsh-theme
  18. rxjava背压_rxjava3——背压
  19. mysql neq什么意思_【知识科普】标准中的Eqv、Idt和Neq分别代表什么?
  20. 2015年上半年数据库系统工程师考试上午真题

热门文章

  1. html比较长的单词不自动换行,HTML+CSS 对于英文单词强制换行但不截断单词的解决办法...
  2. 数据结构:按成绩输出名次排序
  3. 【Vivado那些事】OOC综合方式
  4. 数组方法的增删等19种操作:unshift 、shift,push、pop、splice等等...!
  5. 汤姆猫代码python_IOS 汤姆猫核心代码
  6. 单生狗必备之如何用Python给PLMM表白
  7. 为什么入侵地球的总是火星人,是别的星球不够努力吗?
  8. What is pessimistic locking in Hibernate
  9. 最大熵阈值python_使用python进行普适计算
  10. 如何编写开发人员简历招聘经理将实际阅读