java集合
主要写下java集合中的元素以及是如何实现的,包括数据结构等
List :按照进入先后顺序,可重复,可通过索引直接取到值,可存入空值
ArrayList : 动态数组,适合查询,线程不安全,初始化为0,add的时候长度会变成10,每次扩容是原来的1.5倍。
linkedList:链表,适合增删,线程不安全,是一个双向链表。。。有头插尾插??//todo
Vecotr :数组,线程安全。效率低,用的少。add的时候长度会变成10,每次扩容是原来的2倍。
除了Vecotr还能使用什么List保证线程安全
Vector > Synchronize dList > CopyOnWriteArrayList
Vector是锁是sys锁的方法
SynchronizedList锁的是同步代码快的方法,一般来说锁同步代码快的方式会好点 少消耗点性能。还可以进行扩展不同的List,但是它在用迭代器的时候的查询遍历的时候是没加锁的,用的时候多线程要自己加锁。
List<Integer> list= Collections.synchronizedList(new ArrayList<>());
CopyOnWriteArrayList 是在juc包下的 是适用于读多写少的时候,它新增数据的原理的是先加锁lock锁 然后把容易中的数据复制一份 避免多线程添加重复,然后读取的时候还是读的旧的读取的时候不加锁,新增完成后再复制给新的。 缺点就是比较耗内存,实时性不高
Set : //会内部排序?,值不重复,不能通过索引直接取到值。,元素的唯一性是靠所存储元素类型是否重写hashCode()和equals()方法来保证的,如果没有重写这两个方法,则无法保证元素的唯一性。
HasSet: 无序,唯一 hash表(数组) 底层用hasmap保存元素,可存入空值 。
当你把对象加入HashSet
时,HashSet
会先计算对象的hashcode
值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode
值作比较,如果没有相符的 hashcode
,HashSet
会假设对象没有重复出现。但是如果发现有相同 hashcode
值的对象,这时会调用equals()
方法来检查 hashcode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让加入操作成功。 因为不同对象hashcode也可能相同,所有还要要equals比较
//补充
==与 equals 的区别
对于基本类型来说,== 比较的是值是否相等;
对于引用类型来说,== 比较的是两个引用是否指向同一个对象地址(两者在内存中存放的地址(堆内存地址)是否指向同一个地方);
对于引用类型(包括包装类型)来说,equals 如果没有被重写,对比它们的地址是否相等;如果 equals()方法被重写(例如 String),则比较的是地址里的内容。
TreeSet: 有序,唯一 二叉树红黑树(自平衡的排序二叉树) 。不可存入空值 . 一般排序用
LinkedHashSet
:LinkedHashSet
是 HashSet
的子类,并且其内部是通过 LinkedHashMap
来实现的。
Map: 键值对
HasMap: 1.7和1.8不一样。1.7是数组+链表是,1.8是数组+链表/红黑树 为了减少搜索时间 是 (用连表应该是用链表法来解决散列冲突)
解决hash冲突的方式还有开放寻址法,就是插入的时候如果hash值一样了,会不断找空闲的位置去插入,查找的时候也是我们通过散列函数求出要查找元素的键值对应的散列值,然后比较数组中下标为散列值的元素和要查找的元素。如果相等,则说明就是我们要找的元素;否则就顺序往后依次查找。如果遍历到数组中的空闲位置,还没有找到,就说明要查找的元素并没有在散列表中。
发现1.8对key也进行了排序, 线程不安全。
JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
//具体放值时候的原理
JDK1.8 之前 HashMap
底层是 数组和链表 结合在一起使用也就是 链表散列。HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 (n - 1) & hash 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
扩容机制是
默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap
会将其扩充为 2 的幂次方大小(HashMap
中的tableSizeFor()
方法保证 负载因子为0.75 16*0.75=12 所以当map中存的数量大于12的时候就会开始扩容。
为什么扩容大小都要2的幂次方
为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash
”。(n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。 具体怎么设计这个
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。
concurrentHasMap: 1.7是数组+链表。1.8是数组+链表+红黑树,
TreeMap: 红黑树对所有的key进行排序,线程不安全 适合排序
Hashtable :数组+链表 线程安全 有 synchronized锁
LinkedHashMap
: LinkedHashMap
继承自 HashMap
,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap
在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑
ConcurrentHashMap
和 Hashtable
的区别主要体现在实现线程安全的方式上不同。
- 底层数据结构: JDK1.7 的
ConcurrentHashMap
底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8
的结构一样,数组+链表/红黑二叉树。Hashtable
和 JDK1.8 之前的HashMap
的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; - 实现线程安全的方式(重要): ① 在 JDK1.7 的时候,
ConcurrentHashMap
(分段锁) 对整个桶数组进行了分割分段(Segment
),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 到了 JDK1.8 的时候已经摒弃了Segment
的概念,而是直接用Node
数组+链表+红黑树的数据结构来实现,并发控制使用synchronized
和 CAS 来操作。(JDK1.6 以后 对synchronized
锁做了很多优化) 整个看起来就像是优化过且线程安全的HashMap
,虽然在 JDK1.8 中还能看到Segment
的数据结构,但是已经简化了属性,只是为了兼容旧版本;②Hashtable
(同一把锁) :使用synchronized
来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
1.7 ConcurrentHashMap
是由 Segment
数组结构和 HashEntry
数组结构组成。Segment 数组 + HashEntry 数组 + 链表,
Segment 实现了 ReentrantLock
,所以 Segment
是一种可重入锁,扮演锁的角色。HashEntry
用于存储键值对数据。
一个 ConcurrentHashMap
里包含一个 Segment
数组。Segment
的结构和 HashMap
类似,是一种数组和链表结构,一个 Segment
包含一个 HashEntry
数组,每个 HashEntry
是一个链表结构的元素,每个 Segment
守护着一个 HashEntry
数组里的元素,当对 HashEntry
数组的数据进行修改时,必须首先获得对应的 Segment
的锁