集合、数组都是对多个数据进行存储操作的结构,简称Java容器。
说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi,数据库中)
一、数组
数组在存储多个数据方面的特点:
- 数组开辟的是一块连续的空间,一旦初始化以后,其长度就确定了。
- 数组一旦定义好,其元素的类型也就确定了。我们也就只能操作指定类型的数据了。
比如:String[] arr;int[] arr1;Object[] arr2;
数组在存储多个数据方面的缺点:
- 一旦初始化以后,其长度就不可修改。
- 数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
- 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
- 数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。
二、集合
-
Collection接口:单列集合,用来存储一个一个的对象
- List接口:存储有序的、可重复的数据。 -->“动态”数组
- 实现类:ArrayList、LinkedList、Vector
- Set接口:存储无序的、不可重复的数据
- 实现类:HashSet、LinkedHashSet、TreeSet
- List接口:存储有序的、可重复的数据。 -->“动态”数组
-
Map接口:双列集合,用来存储一对(key - value)一对的数据 -->函数:y = f(x)
- 实现类:HashMap、LinkedHashMap、TreeMap、Hashtable、Properties
1、List实现类
- Collection接口:单列集合,用来存储一个一个的对象
- List接口:存储有序的、可重复的数据。 -->“动态”数组
- ArrayList:线程不安全的,效率高;底层使用Object[] elementData存储
- LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储
- Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储
- List接口:存储有序的、可重复的数据。 -->“动态”数组
1.1 List实现类的异同
三者相同点:三个类都是实现了List接口,存储数据的特点相同:存储有序的、可重复的数据
不同点:
1)ArrayList list = new ArrayList();
JDK7:调用无参构造函数时,创建了长度是10的Object[]数组elementData
JDK8:调用无参构造函数时,底层Object[] elementData初始化为{}.在第一次add()的时候才创建长度为10的数组
- 在add过程中如果此次的添加导致底层elementData数组容量不够,则扩容。 默认情况下,扩容为原来的容
量的1.5倍,同时需要将原有数组中的数据复制到新数组中。
- 这也就是arrayList扩容的缺点:当插入大量数据时,需要扩容,copy数组造成效率变低,一般使用带参构造
函数,传入一个预估容量,一次性将容量确定好
- JDK8ArrayList的对象的创建类似于单例的懒汉式,延迟了数组的创建,节省内存。
2)LinkedList
LinkedList list = new LinkedList(); 内部声明了Node类型的first和last属性,默认值为null
list.add(123);//将123封装到Node中,创建了Node对象
其中,Node定义为:体现了LinkedList的双向链表的说法
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
private static class Node<E> {//linkedList内部类
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
}
3)vector
jdk7和jdk8中通过Vector()构造器创建对象时,底层都创建了长度为10的数组。
在扩容方面,默认扩容为原来的数组长度的2倍
1.2 List接口中的常用方法
void add(int index, Object ele):在index位置插入ele元素
boolean addAll(int index, Collection eles):从index位置开始将eles中的所有元素添加进来
Object get(int index):获取指定index位置的元素
int indexOf(Object obj):返回obj在集合中首次出现的位置
int lastIndexOf(Object obj):返回obj在当前集合中末次出现的位置
Object remove(int index):移除指定index位置的元素,并返回此元素
Object set(int index, Object ele):设置指定index位置的元素为ele
List subList(int fromIndex, int toIndex):返回从fromIndex到toIndex位置的子集合
总结:常用方法
增:add(Object obj)
删:remove(int index) / remove(Object obj)
改:set(int index, Object ele)
查:get(int index)
插:add(int index, Object ele)
长度:size()
遍历:
-
Iterator迭代器方式
Collection接口继承了java.lang.iterator,该接口有一个iterator()方法。所以实现了Collection接口的集合类都有一个iterator()方法,用以返回实现了iterator接口的对象,默认游标在第一个元素之前
iterator仅用于遍历集合,iterator本身不提供承装对象的能力。如果需要创建iterator对象,则必须有一个被迭代的集合- 内部方法:
- hashNext();判断是否有下一个元素
- next()将指针下移,并返回当前元素
- remove(),删除迭代器指定的元素
- 内部方法:
2)增强for循环
3) for(;😉
@Test
public void test1(){
Collection coll = new ArrayList();
coll.add(123);
coll.add(456);
coll.add(new Person("Jerry",20));
coll.add(new String("Tom"));
coll.add(false);
方式1:
Iterator iterator = coll.iterator();
while(iterator.hasNext()){
System.out.println(iterator.next());
}
方式2:
for(Object obj : coll){
System.out.println(obj);
}
方式3;
for(int i=0;i<coll.size();i++)
System.out.println(coll[i]);
}
2、set实现类
- Collection接口:单列集合,用来存储一个一个的对象
-
Set接口:存储无序的、不可重复的数据 -->“集合”
- HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
- LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加遍历
对于频繁的遍历操作,LinkedHashSet效率高于HashSet
- TreeSet:可以按照添加对象的指定属性,进行排序。
- HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
-
2.1 Set实现类的异同
相同点:存储无序的【不等于随机性。存储的数据在底层数组中并非按照数组索引的顺序添加,而是根据数据的哈希值决定的】,不可重复的数据【保证添加的元素按照equals()判断时,不能返回true.即:相同的元素只能添加一个】
1)HashSet底层:底层实际上是用HashMap存:数组+链表
添加元素的过程
向HashSet中添加元素a,首先调用元素a所在类的hashCode()方法,计算元素a的哈希值,此哈希值接着通过某种算法计算出在HashSet底层数组中的存放位置(即为:索引位置),判断数组此位置上是否已经有元素:
如果该位置上为空,则直接将元素添加
如果此位置上有元素,则遍历该位置的所有元素,比较hash值
如果hash值一样,且equals返回true,修改数据
如果hash值一样,且equals返回false,直接返回结果
元素a 与已经存在指定索引位置上数据以链表的方式存储,JDK7:头插法,JDK8:尾插法
2)TreeSet
TreeSet中添加的数据,要求是相同类的对象。
两种排序方式:自然排序(实现Comparable接口) 和 定制排序(Comparator)
自然排序,定制排序中,比较两个对象是否相同的标准为:compareTo()返回0.不再是equals()
- Set接口中没有额外定义新的方法,使用的都是Collection中声明过的方法。
- 向Set(主要指:HashSet、LinkedHashSet)中添加的数据,其所在的类一定要重写hashCode()和equals()
- 重写的hashCode()和equals()尽可能保持一致性:相等的对象必须具有相等的散列码
- 重写两个方法的小技巧:对象中用作 equals() 方法比较的 Field,都应该用来计算 hashCode 值。
3、Map实现类
- Map:双列数据,存储key-value对的数据
- HashMap:作为Map的主要实现类;线程不安全的,效率高;存储null的key和value
- LinkedHashMap:保证在遍历map元素时,可以按照添加的顺序实现遍历
- TreeMap:保证按照添加的key-value对进行排序,实现排序遍历。此时考虑key的自然排序或定制排序, 底层使用红黑树
- Hashtable:作为古老的实现类;线程安全的,效率低;不能存储null的key和value
- Properties:常用来处理配置文件。key和value都是String类型
Map中的key:无序的、不可重复的,使用Set存储所有的key —> key所在的类要重写equals()和hashCode() (以HashMap为例)
Map中的value:无序的、可重复的,使用Collection存储所有的value —>value所在的类要重写equals()
一个键值对:key-value构成了一个Entry对象。
Map中的entry:无序的、不可重复的,使用Set存储所有的entry
1)Map中的常用方法:
Map中定义的方法:
添加、删除、修改操作:
Object put(Object key,Object value):将指定key-value添加到(或修改)当前map对象中
void putAll(Map m):将m中的所有key-value对存放到当前map中
Object remove(Object key):移除指定key的key-value对,并返回value
void clear():清空当前map中的所有数据
元素查询的操作:
Object get(Object key):获取指定key对应的value
boolean containsKey(Object key):是否包含指定的key
boolean containsValue(Object value):是否包含指定的value
int size():返回map中key-value对的个数
boolean isEmpty():判断当前map是否为空
boolean equals(Object obj):判断当前map和参数对象obj是否相等
元视图操作的方法:
Set keySet():返回所有key构成的Set集合
Collection values():返回所有value构成的Collection集合
Set entrySet():返回所有key-value对构成的Set集合
总结:常用方法:
添加:put(Object key,Object value)
删除:remove(Object key)
修改:put(Object key,Object value)
查询:get(Object key)
长度:size()
遍历:keySet() / values() / entrySet()
2) HashMap
- 存储null的key和value
- 底层用数组+链表+红黑树的存储,也叫哈希桶
- HashMap是线程不安全的
- jdk 1.8之前都是数组+链表的结构,在链表中的查询操作都是O(N)的时间复杂度
- 为了提高效率,1.8之后改为数组+链表+红黑树,当链表节点数量达到一定值,链表转换为红黑树结构,增删改查都是O(log n)。
HashMap存储过程:
HashMap map = new HashMap()
当调用HashMap的构造函数,只是对相关属性初始化,只有在第一次put元素时才对散列表进行初始化,容量初始为16
如put(key1,value1):
首先,使用hash()计算key1的哈希值,哈希值经过路由算法(table.length-1)^hash计算出元素在数组中的存储位置
情况一:如果此位置上的数据为空,则将key1-value1添加;
情况二:如果此位置上的数据不为空,(意味着此位置上存在一个或多个数据(以链表形式或红黑树形式存在)),遍历该桶里的节点
1.如果key1的哈希值与所有节点key的哈希值都不相同,则将key1-value1添加到该位置上。
2.如果key2的哈希值和某个节点的key哈希值相同,继续比较:调用key1所在类equals(key2),比较:
如果equals()返回false:则将key1-value1添加到该buket中
如果equals()返回true:使用value1替换value2。
再添加过程中如果容量达到了临界值(且要存放的位置非空),则使用resize()进行扩容,并将原有的数据复制过来。【扩容长度必须是2^n,默认的扩容方式:扩容为原来容量的2倍】
当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 或当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。
jdk8、jdk7不同点:
=== | JDK7 | JDK8 |
---|---|---|
new HashMap() | JDK7创建长度为16的数组 | JDK8第一次put时创建数组 |
底层数组 | Entry[] table | Node[] table |
底层实现 | 数组+链表 | 数组+链表+红黑树 |
插入元素形成链表时 | 头插法 | 尾插法 |
Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
3) LinkedHashMap
继承自HashMap,在原有HashMap基础上,添加了一对指针,指向前一个和后一个元素,保证在遍历map元素时,可以按照添加或LRU(最近最久为访问策略)的顺序实现遍历。对于频繁的遍历操作,其执行效率高于HahsMap
HashMap中提供了三个方法,都是用来给LinkHashMap重写的,分别在访问节点,插入节点,删除节点时对链表进行维护
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after;//能够记录添加的元素的先后顺序
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
private static final long serialVersionUID = 3801124242820219131L;
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
final boolean accessOrder;
}
4) HashTable
- Hashtable继承了Dictionary抽象类,Dictionary用来存储键/值对,其提供了一些操作k-v对的方法,实现Serializable,Cloneable,Map接口
- Hashtable底层用数组+链表进行存储,大部分函数用synchonized修饰,保证线程安全,但多线程操作时执行效率低
- HashTable默认数组大小为11,按照2*table.length+1,进行扩容
- 不可以存储为null的key、value都不可以为null
5)Properties
常用来处理配置文件,key和value都是String类型
6)TreeMap:
添加key-value,要求key必须是由同一个类创建的对象,可按照key进行排序:自然排序 、定制排序
7)ConcurrentHashMap
1.7
1.8
执行原理:
HashMap:是线程不安全的,在并发环境下会造成死循环
HashTable:给每个方法都加了synchronized,整个表都被锁住了,在执行同步方法时相当于单线程工作,其他线程都处于阻塞状态,执行效率低
ConcurrentHashMap1.7
使用分段所技术,在高并发环境下,多个线程可访问不同分段的数据表,相当于将整个表外层套了一层,将数据分为一段一段的进行存储。每一段数据拥有一把锁,线程之间通过获取不同段的锁来操作数据
底层存储结构:
数组(Segment) + 数组(HashEntry) + 链表(HashEntry节点)
整体用Segments数组存储锁,即一个Segments对象,一个Segment对象中储存一个HashEntry数组,存储的每个Entry对象又是一个链表头结点
ConcurrentHashMap1.8:
底层和HashMap的存储结构一样,使用数组+链表+红黑树来实现
没有使用JDK1.7的分段锁技术,即没有segement对象,利用CAS + synchronized来保证并发更新的安全
,JDK1.7的是锁住一段数据,1.8只对散列表的单个元素上锁,增加了并发度,且减少了锁的粒度。
- ConcurrentHashMap 不支持 key 或者 value 为 null
ConcurrentHashMap1.8内部类:
- Node
CHM中的内个节点都是Node,它包装了key-value键值对,其中value和next都用volatile修饰,保证某线程修改这两个变量的值时其他线程可见
CHM与HashMap中的定义很相似,但是有一些差别它对value和next属性设置了volatile同步锁,它不允许调用setValue方法直接改变Node的value域,它增加了find方法辅助map.get()方法
- TreeNode
树节点继承自node节点,所以其拥有了node的属性和方法。
当链表中挂载的节点数超过8时该链表转换为红黑树结构,但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。
- TreeBin
TreeBin维护了一棵红黑树,保存了该红黑树的跟节点,代替了TreeNode的根节点,因为以该根节点作为锁,可能这个位置树结构改变了,不是当前的树节点,导致这个位置有多个线程操作,产生安全问题,所以用TreeBin封装了这颗红黑树,只有一个树对象,方便加锁,里面怎么变就咋变,如果有个线程也来插入,但没法获取该锁。
所以当位置存放的是树结构,则他存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。
- ForwardingNode
ForwardingNode:作为标记节点,hash值为-1,其中存储nextTable的引用。
只有table发生扩容的时候,ForwardingNode才会发挥作用,作为一个占位符放在table中表示当前节点为null或则已经被迁移到新数组。
常用方法执行过程
1)initTable()
初始化方法根据sizeCtl的值实现只有一个线程初始化扩扩容, sizeCtl<0,表示其他线程正在进行初始化或扩容,将该线程挂起,如果等于0,则利用Cas将sizeCtl置为-1,执行初始化操作并sizeCtl的值改为n - (n >>> 2)
sizeCtl初始默认为0,表示线table还未初始化,所以第一个线程不会被阻塞,保证只有一个线程对他初始化
负数代表正在进行初始化或扩容操作
-1代表正在初始化
-N 表示有N-1个线程正在进行扩容操作
正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小
2)put
根据 key 计算出 hash 值;
判断是否需要进行初始化;
定位到 Node,拿到首节点 f,判断首节点 f:
如果为 null ,则通过 CAS 的方式尝试添加;
如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容;
如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入;
当在链表长度达到 8 的时候,数组扩容或者将链表转换为红黑树。
3)get()
ConcurrentHashMap的get方法就是从Hash表中读取数据,而且与扩容不冲突。该方法没有同步锁。
通过键值的hash计算索引位置,如果满足条件,直接返回对应的值;
如果相应节点的hash值小于0 ,即该节点在进行扩容,直接在调用ForwardingNodes节点的find方法进行查找。
否则,遍历当前节点直到找到对应的元素。
查找key对应的值
读操作没有加锁原因:
1.添加节点时,如果是链表结构,添加的节点会放置在链表的尾部,而查找时是从链表头部开始,
不影响链表的循环
2.如果是红黑树的结构,当红黑树正在调整时,使用的是较慢的方式:链表迭代进行查找节点,
而不是等待树调整后再查找;如果再循环的过程中,红黑树已经调整完毕,则又会自动采用红黑树
查找方式进行遍历
3.如果是ForwardNode,则会进入nextTab进行查找,查找方式同样是链表或红黑树查找方式
进行遍历
4)计数
baseCount虽然是统计table表中的节点数,但他只做一个基本计数,这个table里到底装了* Moves and/or copies the nodes多少东西其实是个不确定的数量,因为不可能在调用
size()方法的时候像GC的“stop the world”一样让其他线程都停下来让你去统计,因此只能说这个数量是个估计值。其利用了LongAdder类统计的原理进行统计
ConcurrentHashMap的元素个数等于baseCounter和数组里每个CounterCell的值之和,这样做的原因是,当多个线程同时执行CAS修改baseCount值,失败的线程会将值放到CounterCell中。所以统计元素个数时,要baseCount和counterCells数组都考虑。
5)treeifyBin方法
链表树化,当由于hash冲突导致一个hash槽内的链表节点数>=8个时,链表结构将会进化成红黑树.。但是他并不是直接转换,而是进行一次容量判断,如果容量没有达到转换的要求,直接进行扩容操作并返回;如果满足条件才将链表的结构转换为TreeBin ,这与HashMap不同的是,它并没有把TreeNode直接放入红黑树,而是利用了TreeBin这个小容器来封装所有的TreeNode
==========尚待完善