众所周知,HashMap是一个用于存储Key-Value键值对的集合,每个键值对也叫Entry。这些键值对分散在一个数组中,这个数组就是HashMap的主干。java
HashMap数组的每个初始值都是Null程序员
HashMap用数组+链表的形式解决Hash函数下index的冲突状况,好比下面这种状况面试
hash冲突你还知道哪些解决办法?(1) 开放地址法(2)链地址法(3)再哈希法(4)公共溢出区域法算法
HashMap数组的每个元素不止是一个Entry对象,也是一个链表的头节点。每个Entry对象经过Next指针指向它的下一个Entry节点。当新来的Entry映射到冲突的数组位置时,只须要插入到对应的链表便可。编程
因为刚才所说的Hash冲突,同一个位置有可能匹配到多个Entry,这时候就须要顺着对应链表的头节点,一个一个向下来查找。假设咱们要查找的Key是“apple”:数组
须要注意的是,新来的Entry节点插入链表时,使用的是“头插法”。是由于HashMap的发明者认为,后插入的Entry被查找的可能性更大。缓存
HashMap面试必问的6个点,你知道几个?原做者【程序员追风】连接 基于此文作了小量摘抄和补充安全
HashMap默认的初始长度是多少?为何这么规定?多线程
能够用LinkedList代替数组结构嘛? ps:Entry就是一个链表节点并发
意思源码中的
Entry[] table = new Entry[capacity]
复制代码
用
List<Entry> table = new LinkedList<Entry>();
复制代码
表示是否可行,答案很明显,必须是能够
既然能够,为何HashMap不用LinkedList,而使用数组?
由于数组效率更高,而且采用基本数组结构,能够本身定义扩容机制,好比HashMap中数组扩容是2的次幂,在作取模运算的时候效率高,而ArrayList的扩容机制是1.5倍扩容
HashMap在什么条件下扩容
load factor为0.75,为了最大程度避免哈希冲突,也就是当load factor * current cpacity(当前数组大小)时,就要resize
为何扩容是2的次幂?
HashMap为了存取高效,要尽可能较少碰撞,就是要尽可能把数据分配均匀,每一个链表长度大体相同,这个实现就在把数据存到哪一个链表中的算法;这个算法实际就是取模,hash%length。
可是,你们都知道这种运算不如位移运算快。
所以,源码中作了优化hash&(length-1)。
也就是说hash%length==hash&(length-1)
那为何是2的n次方呢?
由于2的n次方实际就是1后面n个0,2的n次方-1,实际就是n个1。
例如长度为8时候,3&(8-1)=3 2&(8-1)=2 ,不一样位置上,不碰撞。
而长度为5的时候,3&(5-1)=0 2&(5-1)=0,都在0上,出现碰撞了。
因此,保证容积是2的n次方,是为了保证在作(length-1)的时候,每一位都能&1 ,也就是和1111……1111111进行与运算。
知道hashmap中put元素的过程是什么样么?
对key的hashCode()作hash运算,计算index;
此处注意,对于key的判断以下((k = p.key) == key || (key != null && key.equals(k)))只要知足其一就会被算做重复,而后覆盖,也就是以下代码,Map的大小为1
HashMap<String,Integer> map = new HashMap(); map.put("A", 1); map.put(new String("A"), 1); System.out.println(map.size()); //大小为1 复制代码
若是没碰撞直接放到bucket里;
若是碰撞了,以链表的形式存在buckets后;
若是碰撞致使链表过长(大于等于TREEIFY_THRESHOLD),就把链表转换成红黑树(JDK1.8中的改动);
若是节点已经存在就替换old value(保证key的惟一性)
若是bucket满了(超过load factor*current capacity),就要resize。
hashmap中get元素的过程?
还知道哪些Hash算法?
比较出名的有MD四、MD5等
String的hashcode的实现?
public int hashCode() {
int h = hash;
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
return h;
}
复制代码
String类中的hashCode计算方法仍是比较简单的,就是以31为权,每一位为字符的ASCII值进行运算,用天然溢出来等效取模。
知道jdk1.8中hashmap改了啥么?
最后一条是重点,由于最后一条的变更,hashmap在1.8中,不会在出现死循环问题。
为何在解决hash冲突的时候,不直接用红黑树?而选择先用链表,再转红黑树?
由于红黑树须要进行左旋,右旋,变色这些操做来保持平衡,而单链表不须要。
当元素小于8个当时候,此时作查询操做,链表结构已经能保证查询性能。当元素大于8个的时候,此时须要红黑树来加快查询速度,可是新增节点的效率变慢了。
所以,若是一开始就用红黑树结构,元素太少,新增效率又比较慢,无疑这是浪费性能的。
我不用红黑树,用二叉查找树能够么?
能够。可是二叉查找树在特殊状况下会变成一条线性结构(这就跟原来使用链表结构同样了,形成很深的问题),遍历查找会很是慢。
当链表转为红黑树后,何时退化为链表?
为6的时候退转为链表。中间有个差值7能够防止链表和树之间频繁的转换。假设一下,若是设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,若是一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。
HashMap在并发编程环境下有什么问题啊?
在jdk1.8中还有这些问题么?
在jdk1.8中,死循环问题已经解决。其余两个问题仍是存在。
你通常怎么解决这些问题的?
好比ConcurrentHashmap,Hashtable等线程安全等集合类。
HashMap的键能够为Null嘛
能够
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
复制代码
你通常用什么做为HashMap的key?
通常用Integer、String这种不可变类当HashMap当key,并且String最为经常使用。
其实严格来讲,String并非一个严谨的不可变类,在Java1.5之后,咱们能够经过反射技术修改String里的value[]数组的值。
若是让你实现一个自定义的class做为HashMap的key该如何实现?
此题考察两个知识点
针对问题一,记住下面四个原则便可
(1)两个对象相等,hashcode必定相等
(2)两个对象不等,hashcode不必定不等
(3)hashcode相等,两个对象不必定相等
(4)hashcode不等,两个对象必定不等
针对问题二,记住如何写一个不可变类
(1)类添加final修饰符,保证类不被继承。
若是类能够被继承会破坏类的不可变性机制,只要继承类覆盖父类的方法而且继承类能够改变成员变量值,那么一旦子类以父类的形式出现时,不能保证当前类是否可变。
(2)保证全部成员变量必须私有,而且加上final修饰
经过这种方式保证成员变量不可改变。但只作到这一步还不够,由于若是是对象成员变量有可能再外部改变其值。因此第4点弥补这个不足。
(3)不提供改变成员变量的方法,包括setter
避免经过其余接口改变成员变量的值,破坏不可变特性。
(4)经过构造器初始化全部成员,进行深拷贝(deep copy)
若是构造器传入的对象直接赋值给成员变量,仍是能够经过对传入对象的修改进而致使改变内部变量的值。例如:
public final class ImmutableDemo {
private final int[] myArray;
public ImmutableDemo(int[] array) {
this.myArray = array; // wrong
}
}
复制代码
这种方式不能保证不可变性,myArray和array指向同一块内存地址,用户能够在ImmutableDemo以外经过修改array对象的值来改变myArray内部的值。
为了保证内部的值不被修改,能够采用深度copy来建立一个新内存保存传入的值。正确作法:
public final class MyImmutableDemo {
private final int[] myArray;
public MyImmutableDemo(int[] array) {
this.myArray = array.clone();
}
}
复制代码
这里要注意,Object对象的clone()方法,实现了对象中各个属性的复制,但它的可见范围是protected的,因此实体类使用克隆的前提是:
① 实现Cloneable接口,这是一个标记接口,自身没有方法。 ② 覆盖clone()方法,可见性提高为public。
也就是说,一个默认的clone()方法实现机制,仍然是赋值。
若是一个被复制的属性都是基本类型,那么只须要实现当前类的cloneable机制就能够了,此为浅拷贝。
若是被复制对象的属性包含其余实体类对象引用,那么这些实体类对象都须要实现cloneable接口并覆盖clone()方法。
(5)在getter方法中,不要直接返回对象自己,而是克隆对象,并返回对象的拷贝
这种作法也是防止对象外泄,防止经过getter得到内部可变成员对象后对成员变量直接操做,致使成员变量发生改变。
准备用HashMap存1w条数据,构造时传10000还会触发扩容吗?
在 HashMap 中,提供了一个指定初始容量的构造方法 HashMap(int initialCapacity)
,这个方法最终会调用到 HashMap 另外一个构造方法,其中的参数 loadFactor 就是默认值 0.75f。
从构造方法的逻辑能够看出,HashMap 并非直接使用外部传递进来的 initialCapacity
,而是通过了 tableSizeFor()
方法的处理,再赋值到 threshole
上。
在 tableSizeFor()
方法中,经过逐步位运算,就可让返回值,保持在 2 的 N 次幂。以方便在扩容的时候,快速计算数据在扩容后的新表中的位置。
那么当咱们从外部传递进来 1w 时,实际上通过 tableSizeFor()
方法处理以后,就会变成 2 的 14 次幂 16384,再算上负载因子 0.75f,实际在不触发扩容的前提下,可存储的数据容量是 12288(16384 * 0.75f)。
这种场景下,用来存放 1w 条数据,绰绰有余了,并不会触发咱们猜测的扩容。
概述:一般用于方法的返回值,通知调用者该方法是只读的,而且不指望对方修改状态
补充:Arrays.asList返回的Arrays.ArrayList其实并不是不可变,只是add方法没有重写,可是能够经过set进行下标数据交换
在Java 8咱们可使用Collections来实现返回不可变集合
方法名 | 做用 |
---|---|
List unmodifiableList(List<? extends T> list) | 返回不可变的List集合 |
Map<K,V> unmodifiableMap(Map<? extends K, ? extends V> m) | 返回不可变的Map集合 |
Set unmodifiableSet(Set<? extends T> s) | 返回不可变的Set集合 |
若是咱们用Java作开发的话,最经常使用的容器之一就是List集合了,而List集合中用的较多的就是ArrayList 和 LinkedList 两个类,这二者也常被用来作比较。由于最近在学习Java的集合类源码,对于这两个类天然是不能放过,因而乎,翻看他们的源码,我发现,ArrayList实现了一个叫作 RandomAccess
的接口,而 LinkedList 是没有的
RandomAccess 是一个标志接口,代表实现这个这个接口的 List 集合是支持快速随机访问的。也就是说,实现了这个接口的集合是支持 快速随机访问 策略的。
同时,官网还特地说明了,若是是实现了这个接口的 List,那么使用for循环的方式获取数据会优于用迭代器获取数据。
stream.of(),最好不要传基本类型,由于 Stream主要用于对象类型的集合 ,若是传基本类型,会将基本类型数组当作一个对象处理。
固然JDK还提供了基本类型的Stream,例如咱们能够直接使用IntStream,而不是Stream。
集合接口
java.util.Collection 接口
通用接口 * java.util.List(有序,容许重复,容许null)
* java.util.Set(无序,不可重复,不容许null)
* Set集合底层使用hash表实现,因此不可重复,每次add的时候都会调用本身的hashCode()方法去判断是否发生碰撞,而后是否替换等操做
* HashSet在一些特殊场景是有序的,如字母、数字
* java.util.SortedSet
* TreeSet实现了SortedSet,提供了Compare和CompareTo来定制排序
* java.util.NavigableSet(since Java 1.6)
复制代码
Collection接口下面已细化了List,Set和Queue子接口,未什么又定义了AbstractCollection这个抽象类?
三者都是Collection,总仍是有共同行为的。
ArrayList集合之subList
List.sublist(20,30).clear();
一般 Set 是 Map Key 的实现,Set 底层运用 Map 实现 在HashSet中,元素都存到HashMap键值对的Key上面,而Value时有一个统一的值private static final Object PRESENT = new Object();,(定义一个虚拟的Object对象做为HashMap的value,将此对象定义为static final)
java.util.Map接口
接口 | 哈希表 | 可变数组 | 平衡树 | 链表 | 哈希表+链表 |
---|---|---|---|---|---|
Set | HashSet | TreeSet | LinkedHashSet | ||
List | ArrayList | LinkedList | |||
Deque | ArrayDeque | LinkedList | |||
Map | HashMap | TreeMap | LinkedHashMap |
ArrayList和CopyOnWriteArrayList的区别
- CopyOnWriteArrayList
- 实现了List接口
- 内部持有一个ReentrantLock lock = new ReentrantLock();
- 底层是用volatile transient声明的数组 array
- 读写分离,写时复制出一个新的数组,完成插入、修改或者移除操做后将新数组赋值给array
- ArrayList
- 底层是数组,初始大小为10
- 插入时会判断数组容量是否足够,不够的话会进行扩容
- 所谓扩容就是新建一个新的数组,而后将老的数据里面的元素复制到新的数组里面
- 移除元素的时候也涉及到数组中元素的移动,删除指定index位置的元素,而后将index+1至数组最后一个元素往前移动一个格
Vector是增删改查方法都加了synchronized,保证同步,可是每一个方法执行的时候都要去得到锁,性能就会大大降低,而CopyOnWriteArrayList 只是在增删改上加锁,可是读不加锁,在读方面的性能就好于Vector,CopyOnWriteArrayList支持读多写少的并发状况
Collections便利实现
接口类型