Java数据结构
常见的数据结构
HashMap
迭代方式:
keyset
entryset
比较:keyset
比entryset
效率低 因为keyset
相当于做了两次遍历 一次转化为interacor
一次用key
取value
entryset
把值和value
都放在entry
中 所以只遍历了一次
使用:
Map map=new HashMap();
Iterator iterator=map.keySet().iterator();
while (iterator.hasNext()){
String key=iterator.next(); //keySet()的遍历方式 相当于遍历两次 效率较低
String value=map.get(key);
}
Iterator> iterator1=map.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry entry=iterator1.next(); //entrySet的遍历方式 效率相对较高
String key=entry.getKey();
String value=entry.getValue();
}
数组vs
链表
- 数组是通过下标找对应元素
- 链表是通过前后关系的指针找元素
所以数组查找快,增删慢 ,链表查找慢,增删快
Map,List与Set的区别
-
List
:允许重复,有序 -
Set
:没有重复,无序 -
Map
:区别于上述两者,它是单独的接口,并没有实现collection
,键值对
补充:TreeSet
,TreeList
,Tree**
:自动排序
Java中Map,List与Set的区别
ArrayList vs
LinkedList vs
HashMap
-
ArrayList
是线性表,相当于容量可以动态增长的数组 -
LinkedList
是链表,链表随机位置插入、删除数据时比线性表快,遍历比线性表慢 -
HashMap
结构的实现原理是将put
进来的key-value
封装成一个Entry
对象存储到一个Entry数组
中
HashMap vs
HashTable
- 线程安全。
HashMap
是非synchronized
的,并可以接受null(HashMap
可以接受为null的键值(key)和值(value),而HashTable
则不行) - 效率。由于线程安全问题,单线程下
Hashtable
比HashMap
效率低 - 顺序。
HashMap
不能保证随着时间的推移Map
中的元素次序是不变的
怎么让HashMap
同步?
Map m = Collections.synchronizeMap(hashMap);
JAVA5以上 ConcurrentHashMap
是HashTable
的替代 (即线程安全的)
HashSet vs
TreeSet
两者分别默认维护一个HashMap
、TreeMap
。HashMap
无序,TreeMap
可以实现有序
可以看出,Set
其实基于Map
实现
HashMap vs
LinkedHashMap
前者无序,后者有序。如果想保持进入的顺序和取出的顺序一致,优先考虑LinkedHashMap
SparseArray
用来替代key为int的HashMap
对比HashMap
- 优势
空间上 去除了hash值的存储空间
时间上 少了自动装箱的过程 效率提高 - 劣势
不适合数据量很大的数据 因为采用的二分查找(O(logn)) 大数据下明显效率低于hash查找(O(1))
并发集合有哪些?
同步集合类
- Hashtable 已经被ConcurrentHashMap替代
- Vector
- 同步集合包装类,Collections.synchronizedMap()和Collections.synchronizedList()
并发集合类
- ConcurrentHashMap
- CopyOnWriteArrayList
- CopyOnWriteHashSet
并发集合的实现原理
- ConcurrentHashMap
把整个Map 划分成几个片段,只对相关的几个片段上锁,同时允许多线程访问其他未上锁的片段 - CopyOnWriteArrayList
允许多个线程以非同步的方式读,当有线程写的时候它会将整个List复制一个副本给它
如果在读多写少这种对并发集合有利的条件下使用并发集合,这会比使用同步集合更具有可伸缩性
ConcurrentHashMap实现原理
ConcurrentHashMap是由Segment
数组结构和HashEntry
数组结构组成
- Segment是一种可重入锁ReentrantLock 结构和HashMap类似 一个Segment里包含一个HashEntry数组 修改HashEntry值时必须获取Segment锁
- HashEntry则用于存储键值对数据
CopyOnWrite容器
写时复制的容器
往容器添加数据时 不直接添加 而是先将容器copy 复制出一个新的容器 然后往新的容器添加数据 数据添加完成后将原容器引用指向新的容器 这样做的目的:可以并发的读 而不需要加锁
是种读写分离的思想
为什么读的时候不需要加锁?
如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList
public boolean add(T e) {
// 写的时候是需要加锁的
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
// 复制出新数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 把新元素添加到新数组里
newElements[len] = e;
// 把原数组引用指向新数组
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
应用场景
读多写少的并发场景 例如白名单 黑名单等
缺点
- 内存占用
- 数据一致性
不能确保实时一致性
HashMap原理
HashMap是数组加链表的方式实现
流程:
HashMap中的Hash计算和碰撞问题
HashMap的hash计算时先计算hashCode(),然后进行二次hash
// 计算二次Hash
int hash = hash(key.hashCode());
// 通过Hash找数组索引
int i = indexFor(hash, table.length);
为什么要做二次hash?
答:尽量让key尽可能均匀的分配到数组上去 避免造成Hash堆积
/**
* Returns index for hash code h.
*/
static int indexFor(int h, int length) {
return h & (length-1); //其实在length=2^n的情况下 就是h%length取余 分组下经常使用该算法 在hashmap中默认规定数组entry长度为2^n
}
关于h & (length-1)
由于h & (length-1)
相当于取余 那么大概率上会碰撞到同一个数组上 造成hash堆积 对性能造成影响
为了避免这种性能影响 我们利用二次hash来避免这种影响
链地址法提高查找性能:
// 二次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);
}
新:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap的put()解析
/**
* Associates the specified value with the specified key in this map.
* If the map previously contained a mapping for the key, the old
* value is replaced.
*
* @param key key with which the specified value is to be associated
* @param value value to be associated with the specified key
* @return the previous value associated with key, or
* null if there was no mapping for key.
* (A null return can also indicate that the map
* previously associated null with key.)
*/
public V put(K key, V value) {
if (key == null)
return putForNullKey(value); // ① key为null时单独做处理
int hash = hash(key.hashCode());
int i = indexFor(hash, table.length); // ② 由二次hash值获取对应数组索引
for (Entry e = table[i]; e != null; e = e.next) { // ③ 遍历table[i]开头的链表 如果hash和key都相同 则替换为新值 返回旧值
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++; // ④ volatile申明 其他线程可以立即获取到新值 在迭代的时候起到关键作用
addEntry(hash, key, value, i); // ⑤ 没有找到key和hash都相同的节点 那么就增加新的节点
return null;
}
步骤:
① key为null 调用putForNullKey
/**
* Offloaded version of put for null keys
*/
private V putForNullKey(V value) {
for (Entry e = table[0]; e != null; e = e.next) { // 存储到table[0]开头的链表
if (e.key == null) { // 发现key是null 则替换成新值 并且返回旧值
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0); // 没有发现key为null 则新增节点
return null;
}
② 由二次hash值获取对应数组索引
③ 遍历table[i]开头的链表 如果hash和key都相同 则替换为新值 返回旧值
④ volatile申明 其他线程可以立即获取到新值 在迭代的时候起到关键作用
HashIterator() {
// 每次迭代会将modCount值赋给expectedModCount
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}
public final boolean hasNext() {
return next != null;
}
final Entry nextEntry() {
// 这里就是关键 如果不相等则表明其他线程改变了hashmap的结构 那么就抛出异常
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Entry e = next;
if (e == null)
throw new NoSuchElementException();
if ((next = e.next) == null) {
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
current = e;
return e;
}
public void remove() {
if (current == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
Object k = current.key;
current = null;
HashMap.this.removeEntryForKey(k);
expectedModCount = modCount;
}
⑤ 没有找到key和hash都相同的节点 那么就增加新的节点
HashMap的get()解析
public V get(Object key) {
if (key == null)
return getForNullKey();
int hash = hash(key.hashCode());
for (Entry e = table[indexFor(hash, table.length)]; //由于费线程安全 此循环在扩容时易出现死循环
e != null;
e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k)))
return e.value;
}
return null;
}
HashMap的size()解析
不是实时计算的 而是每次新增加Entry的时候 size就递增 删除的时候就递减
空间换时间的做法
HashMap的reSize()解析
大小超过临界值时 需要扩容
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) { // 大小超过最大容量就返回
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry e = src[j];
if (e != null) {
src[j] = null;
do {
Entry next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}
ConcurrentHashMap实现原理
如何解决HashMap的线程安全问题?
- HashTable 或者Collections.synchronizedMap(hashMap)
两者都是加锁 对性能有影响 - ConcurrentHashMap
ConcurrentHashMap原理
采用分段锁的机制 底部是数组+链表的数据结构
包含两个核心静态内部类 Segment
HashEntry
-
Segment
继承ReentrantLock
充当锁 每个Segment
维护对应的散列表的若干个桶 -
HashEntry
封装映射表的键值对 - 每个桶是由若干个
HashEntry
链接起来的链表
注意 JDK1.8上已经抛弃分段锁的机制 利用CAS+Synchronized
来保证并发更新安全
CAS
(比较并替换 实现并发算法时常用的技术)
思想:三个参数 当前内存值V 旧的期望值A 即将更新的值B;当V==A时 V修改为B 并返回true 否则直接返回false
HashMap,ArrayMap,SparseArray三者对比
ArrayMap
SparseArray
都是Android系统API 用于取代HashMap
节省内存
-
ArrayMap
两个数组mHashes
用来保存每一个key的hash值mArrray
大小为mHashes
的2倍,依次保存key和value
关键源码:
mHashes[index] = hash;
mArray[index<<1] = key; //二分查找
mArray[(index<<1)+1] = value;
ArrayMap
每存储一条信息 需要保存一个hash值 一个key值 一个value值
空间上 对比HashMap只是少了指向下一个Entry的指针
效率上 查找没有hash快 顺序插入效率高
-
SparseArray
只有key为int时才能使用 注意不是Integer 去除了 自动拆箱 的操作 也是使用二分查找
空间上 不需要Hash值的存储空间 没有next的指针引用
效率上 和ArrayMap基本相同 但是少了拆箱的过程 稍有优势
TreeMap
对查找性能要求不高 对有序性要求比较高的应用场景
是SortedMap的一种实现 key是有序的 基于红黑树实现的 不允许key-value为空
HashTable
通过hash函数将特定的值映射到特定值的数据结构 维护键与值的一一关系
HashSet and HashMap
二者有相同的实现 前者仅仅是对后者做了一层包装(适配器模式) 即 HashSet中有一个HashMap
HashSet的遍历方式:
Set set = new HashSet();
set.add("aaa");
set.add("bbb");
set.add("ccc");
//遍历
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
//或者这样
for (String s:set) {
System.out.println(s);
}
HashSet核心代码:
//HashSet是对HashMap的简单包装
public class HashSet
{
......
private transient HashMap map;//HashSet里面有一个HashMap
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public HashSet() {
map = new HashMap<>();
}
......
public boolean add(E e) {//简单的方法转换
return map.put(e, PRESENT)==null; //利用HashMap的key不能重复的原理来实现Set的value不重复
}
......
}
HashSet和HashMap元素不能相同的实现原理
首先 了解一下equals(Object obj)
和hashcode()
两个方法的规范
- 重写
equals(Object obj)
必须重写hashcode()
因为需要确保equals(Object obj)
为truehashcode()
一定相等 -
equals(Object obj)
返回falsehashcode()
不一定相等
Hash碰撞问题
解决方式:
- 开放地址
- 再hash
- 链地址
- 公共溢出区
数组和链表的区别
- 内存存储
①数组从栈中分配内存
②链表从堆中分配内存 - 结构
①数组必须要事先定义固定的长度
②链表动态进行存储分配 查找元素需要遍历整个链表
二叉树的深度优先遍历与广度优先遍历
深度优先遍历采用栈
对每个可能的访问路径深入到不能再深入为止 每个节点只能访问一次
可以分为三类:
① 先序 根--左子树--右子树
② 中序 左子树--根--右子树
③ 后序 右子树--根--左子树广度优先遍历采用队列
层次遍历 从上往下每一层依次访问;每一层从左往右(或者从右往左) 访问节点 访问完一层进入下一层 直到最后一个节点
Java中堆和栈的区别
堆
用于存储对象(new 创建的对象和数组) 线程共享 虚拟机启动时创建
数组和变量在没有引用指向的时候会变成垃圾 但是仍然会占用堆内存 后续由垃圾回收器自动回收(Java占用内存大的一个重要原因)栈
用于存储栈帧 线程私有 随线程启动创建结束而销毁
函数中定义的基本数据类型的变量 和对象的引用变量 都是在栈内存中 但是实例变量作为对象的一部分保存在堆中
声明一个变量时 栈中分配这个内存空间 超过这个变量的作用域时 自动释放分配的内存空间
数据结构中的堆栈
堆
优势:
可以动态分配内存大小
缺点:
存取速度慢 因为动态分配内存空间栈
优势:
存取的速度都比堆要快,仅次于寄存器
缺点:
栈大小以及生命在后期必须确定 缺乏灵活性