本文为 SnailClimb 的原创,目前已经收录自我开源的 JavaGuide 中(61.5 k Star!【Java学习+面试指南】 一份涵盖大部分Java程序员所须要掌握的核心知识。欢迎 Star!)。文末有个人公众号,公众号里有我最新整理的Java学习资料,免费分享。html
这么好的文章,必定好先赞后看!!!建议养成这个好习惯!!爱大家!😍java
当了会标题党,这是第一次,后面还有不少次!不过这文章全网阅读确定是超过 20 k 的,并且通过了不少同行的优化,质量有保障哦!git
ArrayList
和 LinkedList
都是不一样步的,也就是不保证线程安全;Arraylist
底层使用的是 Object
数组;LinkedList
底层使用的是 双向链表 数据结构(JDK1.6以前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)ArrayList
采用数组存储,因此插入和删除元素的时间复杂度受元素位置的影响。 好比:执行add(E e)
方法的时候, ArrayList
会默认在将指定的元素追加到此列表的末尾,这种状况时间复杂度就是O(1)。可是若是要在指定位置 i 插入和删除元素的话(add(int index, E element)
)时间复杂度就为 O(n-i)。由于在进行上述操做的时候集合中第 i 和第 i 个元素以后的(n-i)个元素都要执行向后位/向前移一位的操做。 ② LinkedList
采用链表存储,因此对于add(E e)
方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),若是是要在指定位置i
插入和删除元素的话((add(int index, E element)
) 时间复杂度近似为o(n))
由于须要先移动到指定位置再插入。 LinkedList
不支持高效的随机元素访问,而 ArrayList
支持。快速随机访问就是经过元素的序号快速获取元素对象(对应于get(int index)
方法)。public interface RandomAccess { }
查看源码咱们发现实际上 RandomAccess
接口中什么都没有定义。因此,在我看来 RandomAccess
接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具备随机访问功能。程序员
在 binarySearch(
)方法中,它要判断传入的list 是否 RamdomAccess
的实例,若是是,调用indexedBinarySearch()
方法,若是不是,那么调用iteratorBinarySearch()
方法github
public static <T> int binarySearch(List<? extends Comparable<? super T>> list, T key) { if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD) return Collections.indexedBinarySearch(list, key); else return Collections.iteratorBinarySearch(list, key); }
ArrayList
实现了 RandomAccess
接口, 而 LinkedList
没有实现。为何呢?我以为仍是和底层数据结构有关!ArrayList
底层是数组,而 LinkedList
底层是链表。数组自然支持随机访问,时间复杂度为 O(1),因此称为快速随机访问。链表须要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),因此不支持快速随机访问。,ArrayList
实现了 RandomAccess
接口,就代表了他具备快速随机访问功能。 RandomAccess
接口只是标识,并非说 ArrayList
实现 RandomAccess
接口才具备快速随机访问功能的!面试
下面再总结一下 list 的遍历方式选择:算法
RandomAccess
接口的list,优先选择普通 for 循环 ,其次 foreach,RandomAccess
接口的list,优先选择iterator遍历(foreach遍历底层也是经过iterator实现的,),大size的数据,千万不要使用普通for循环双向链表: 包含两个指针,一个prev指向前一个节点,一个next指向后一个节点。spring
双向循环链表: 最后一个节点的 next 指向head,而 head 的prev指向最后一个节点,构成一个环。shell
Vector
类的全部方法都是同步的。能够由两个线程安全地访问一个Vector对象、可是一个线程访问Vector的话代码要在同步操做上耗费大量的时间。segmentfault
Arraylist
不是同步的,因此在不须要保证线程安全时建议使用Arraylist。
详见笔主的这篇文章:经过源码一步一步分析ArrayList 扩容机制
synchronized
修饰。(若是你要保证线程安全的话就使用 ConcurrentHashMap 吧!);tableSizeFor()
方法保证,下面给出了源代码)。也就是说 HashMap 老是使用2的幂做为哈希表的大小,后面会介绍到为何是2的幂次方。HashMap 中带有初始容量的构造函数:
public HashMap(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity); if (initialCapacity > MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal load factor: " + loadFactor); this.loadFactor = loadFactor; this.threshold = tableSizeFor(initialCapacity); } public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR); }
下面这个方法保证了 HashMap 老是使用2的幂做为哈希表的大小。
/** * Returns a power of two size for the given target capacity. */ static final int tableSizeFor(int cap) { int n = cap - 1; n |= n >>> 1; n |= n >>> 2; n |= n >>> 4; n |= n >>> 8; n |= n >>> 16; return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; }
若是你看过 HashSet
源码的话就应该知道:HashSet 底层就是基于 HashMap 实现的。(HashSet 的源码很是很是少,由于除了 clone()
、writeObject()
、readObject()
是 HashSet 本身不得不实现以外,其余方法都是直接调用 HashMap 中的方法。
HashMap | HashSet |
---|---|
实现了Map接口 | 实现Set接口 |
存储键值对 | 仅存储对象 |
调用 put() 向map中添加元素 |
调用 add() 方法向Set中添加元素 |
HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值,对于两个对象来讲hashcode可能相同,因此equals()方法用来判断对象的相等性, |
当你把对象加入HashSet
时,HashSet会先计算对象的hashcode
值来判断对象加入的位置,同时也会与其余加入的对象的hashcode值做比较,若是没有相符的hashcode,HashSet会假设对象没有重复出现。可是若是发现有相同hashcode值的对象,这时会调用equals()
方法来检查hashcode相等的对象是否真的相同。若是二者相同,HashSet就不会让加入操做成功。(摘自个人Java启蒙书《Head fist java》第二版)
hashCode()与equals()的相关规定:
==与equals的区别
JDK1.8 以前 HashMap
底层是 数组和链表 结合在一块儿使用也就是 链表散列。HashMap 经过 key 的 hashCode 通过扰动函数处理事后获得 hash 值,而后经过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),若是当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,若是相同的话,直接覆盖,不相同就经过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数以后能够减小碰撞。
JDK 1.8 HashMap 的 hash 方法源码:
JDK 1.8 的 hash方法 相比于 JDK 1.7 hash 方法更加简化,可是原理不变。
static final int hash(Object key) { int h; // key.hashCode():返回散列值也就是hashcode // ^ :按位异或 // >>>:无符号右移,忽略符号位,空位都以0补齐 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); }
对比一下 JDK1.7的 HashMap 的 hash 方法源码.
static int hash(int h) { // This function ensures that hashCodes that differ only by // constant multiples at each bit position have a bounded // number of collisions (approximately 8 at default load factor). h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); }
相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,由于毕竟扰动了 4 次。
所谓 “拉链法” 就是:将链表和数组相结合。也就是说建立一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中便可。
相比于以前的版本, JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减小搜索时间。
TreeMap、TreeSet以及JDK1.8以后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,由于二叉查找树在某些状况下会退化成一个线性结构。
推荐阅读:
为了能让 HashMap 存取高效,尽可能较少碰撞,也就是要尽可能把数据分配均匀。咱们上面也讲到了过了,Hash 值的范围值-2147483648到2147483647,先后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,通常应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。因此这个散列值是不能直接拿来用的。用以前还要先作对数组的长度取模运算,获得的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash
”。(n表明数组长度)。这也就解释了 HashMap 的长度为何是2的幂次方。
这个算法应该如何设计呢?
咱们首先可能会想到采用%取余的操做来实现。可是,重点来了:“取余(%)操做中若是除数是2的幂次则等价于与其除数减一的与(&)操做(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 而且 采用二进制位操做 &,相对于%可以提升运算效率,这就解释了 HashMap 的长度为何是2的幂次方。
主要缘由在于 并发下的Rehash 会形成元素之间会造成一个循环链表。不过,jdk 1.8 后解决了这个问题,可是仍是不建议在多线程下使用 HashMap,由于多线程下使用 HashMap 仍是会存在其余问题好比数据丢失。并发环境下推荐使用 ConcurrentHashMap 。
详情请查看:https://coolshell.cn/articles...
ConcurrentHashMap 和 Hashtable 的区别主要体如今实现线程安全的方式上不一样。
二者的对比图:
图片来源:http://www.cnblogs.com/chengx...
HashTable:
JDK1.7的ConcurrentHashMap:
JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):
首先将数据分为一段一段的存储,而后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其余段的数据也能被其余线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment 实现了 ReentrantLock,因此 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。
static class Segment<K,V> extends ReentrantLock implements Serializable { }
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap相似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每一个 HashEntry 是一个链表结构的元素,每一个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先得到对应的 Segment的锁。
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构相似,数组+链表/红黑二叉树。Java 8在链表长度超过必定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提高N倍。
compareTo(Object obj)
方法用来排序compare(Object obj1, Object obj2)
方法用来排序通常咱们须要对一个集合使用自定义排序时,咱们就要重写compareTo()
方法或compare()
方法,当咱们须要对某一个集合实现两种排序方式,好比一个song对象中的歌名和歌手名分别采用一种排序方法的话,咱们能够重写compareTo()
方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种表明咱们只能使用两个参数版的 Collections.sort()
.
ArrayList<Integer> arrayList = new ArrayList<Integer>(); arrayList.add(-1); arrayList.add(3); arrayList.add(3); arrayList.add(-5); arrayList.add(7); arrayList.add(4); arrayList.add(-9); arrayList.add(-7); System.out.println("原始数组:"); System.out.println(arrayList); // void reverse(List list):反转 Collections.reverse(arrayList); System.out.println("Collections.reverse(arrayList):"); System.out.println(arrayList); // void sort(List list),按天然排序的升序排序 Collections.sort(arrayList); System.out.println("Collections.sort(arrayList):"); System.out.println(arrayList); // 定制排序的用法 Collections.sort(arrayList, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2.compareTo(o1); } }); System.out.println("定制排序后:"); System.out.println(arrayList);
Output:
原始数组: [-1, 3, 3, -5, 7, 4, -9, -7] Collections.reverse(arrayList): [-7, -9, 4, 7, -5, 3, 3, -1] Collections.sort(arrayList): [-9, -7, -5, -1, 3, 3, 4, 7] 定制排序后: [7, 4, 3, 3, -1, -5, -7, -9]
// person对象没有实现Comparable接口,因此必须实现,这样才不会出错,才可使treemap中的数据按顺序排列 // 前面一个例子的String类已经默认实现了Comparable接口,详细能够查看String类的API文档,另外其余 // 像Integer类等都已经实现了Comparable接口,因此不须要另外实现了 public class Person implements Comparable<Person> { private String name; private int age; public Person(String name, int age) { super(); this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } /** * TODO重写compareTo方法实现按年龄来排序 */ @Override public int compareTo(Person o) { // TODO Auto-generated method stub if (this.age > o.getAge()) { return 1; } else if (this.age < o.getAge()) { return -1; } return age; } }
public static void main(String[] args) { TreeMap<Person, String> pdata = new TreeMap<Person, String>(); pdata.put(new Person("张三", 30), "zhangsan"); pdata.put(new Person("李四", 20), "lisi"); pdata.put(new Person("王五", 10), "wangwu"); pdata.put(new Person("小红", 5), "xiaohong"); // 获得key的值的同时获得key所对应的值 Set<Person> keys = pdata.keySet(); for (Person key : keys) { System.out.println(key.getAge() + "-" + key.getName()); } }
Output:
5-小红 10-王五 20-李四 30-张三
主要根据集合的特色来选用,好比咱们须要根据键值获取到元素值时就选用Map接口下的集合,须要排序时选择TreeMap,不须要排序时就选择HashMap,须要保证线程安全就选用ConcurrentHashMap.当咱们只须要存放元素值时,就选择实现Collection接口的集合,须要保证元素惟一时选择实现Set接口的集合好比TreeSet或HashSet,不须要就选择实现List接口的好比ArrayList或LinkedList,而后再根据实现这些接口的集合的特色来选用。
做者的其余开源项目推荐: