集合知识点总结

集合、数组都是对多个数据进行存储操作的结构,简称Java容器。
说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi,数据库中)

一、数组

数组在存储多个数据方面的特点:

  • 数组开辟的是一块连续的空间,一旦初始化以后,其长度就确定了。
  • 数组一旦定义好,其元素的类型也就确定了。我们也就只能操作指定类型的数据了。
    比如:String[] arr;int[] arr1;Object[] arr2;

数组在存储多个数据方面的缺点:

  • 一旦初始化以后,其长度就不可修改。
  • 数组中提供的方法非常有限,对于添加、删除、插入数据等操作,非常不便,同时效率不高。
  • 获取数组中实际元素的个数的需求,数组没有现成的属性或方法可用
  • 数组存储数据的特点:有序、可重复。对于无序、不可重复的需求,不能满足。

二、集合

  • Collection接口:单列集合,用来存储一个一个的对象

    • List接口:存储有序的、可重复的数据。 -->“动态”数组
      • 实现类:ArrayList、LinkedList、Vector
    • Set接口:存储无序的、不可重复的数据
      • 实现类:HashSet、LinkedHashSet、TreeSet
  • Map接口:双列集合,用来存储一对(key - value)一对的数据 -->函数:y = f(x)

    • 实现类:HashMap、LinkedHashMap、TreeMap、Hashtable、Properties

1、List实现类

  • Collection接口:单列集合,用来存储一个一个的对象
    • List接口:存储有序的、可重复的数据。 -->“动态”数组
      • ArrayList:线程不安全的,效率高;底层使用Object[] elementData存储
      • LinkedList:对于频繁的插入、删除操作,使用此类效率比ArrayList高;底层使用双向链表存储
      • Vector:作为List接口的古老实现类;线程安全的,效率低;底层使用Object[] elementData存储

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()
遍历:

  1. 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());
        }
      方式2for(Object obj : coll){
            System.out.println(obj);
        }
      方式3for(int i=0;i<coll.size();i++)
            System.out.println(coll[i]);
    }

2、set实现类

  • Collection接口:单列集合,用来存储一个一个的对象
    • Set接口:存储无序的、不可重复的数据 -->“集合”

      • HashSet:作为Set接口的主要实现类;线程不安全的;可以存储null值
        • LinkedHashSet:作为HashSet的子类;遍历其内部数据时,可以按照添加遍历

      对于频繁的遍历操作,LinkedHashSet效率高于HashSet

      • TreeSet:可以按照添加对象的指定属性,进行排序。
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不同点:

===JDK7JDK8
new HashMap()JDK7创建长度为16的数组JDK8第一次put时创建数组
底层数组Entry[] tableNode[] 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
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-peiZAIJC-1646398729565)(https://dlnu19javastudy.yuque.com/api/filetransfer/images?url=https%3A%2F%2Fimg0.baidu.com%2Fit%2Fu%3D420857959%2C967169486%26fm%3D26%26fmt%3Dauto&sign=06eb7272282256e85c23c8000a5fb2ad2abcadd2a7933c78bf8ada02bfe6c95c#from=url&id=bvjdL&margin=%5Bobject%20Object%5D&originHeight=404&originWidth=849&originalType=binary&ratio=1&status=done&style=none)]

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dv4zqxSQ-1646398729566)(https://dlnu19javastudy.yuque.com/api/filetransfer/images?url=https%3A%2F%2Fimg2.baidu.com%2Fit%2Fu%3D2478313591%2C3460983906%26fm%3D26%26fmt%3Dauto&sign=0408f1885fd1ddf80996575091ef84602f9b2bfc7cd034e3d26fbef451540520#from=url&height=335&id=ukuxc&margin=%5Bobject%20Object%5D&originHeight=494&originWidth=964&originalType=binary&ratio=1&status=done&style=none&width=654)]

  • 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
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2i4zedZ5-1646398729567)(https://dlnu19javastudy.yuque.com/api/filetransfer/images?url=https%3A%2F%2Fimg-blog.csdnimg.cn%2F20190426100401737.png%3Fx-oss-process%3Dimage%2Fwatermark%2Ctype_ZmFuZ3poZW5naGVpdGk%2Cshadow_10%2Ctext_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQxODg0OTc2%2Csize_16%2Ccolor_FFFFFF%2Ct_70&sign=855dc4c4fa28fd0ee143d267e9a355e5c9e1acde30af4b39567c2437575f8c8c#from=url&height=374&id=ZVS2r&margin=%5Bobject%20Object%5D&originHeight=420&originWidth=654&originalType=binary&ratio=1&status=done&style=none&width=582)]

1.8
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FlYIQLpE-1646398729567)(https://dlnu19javastudy.yuque.com/api/filetransfer/images?url=https%3A%2F%2Fimg2.baidu.com%2Fit%2Fu%3D1066423476%2C261490557%26fm%3D253%26fmt%3Dauto%26app%3D138%26f%3DPNG%3Fw%3D499%26h%3D320&sign=f1f42df7b627d4b03f10924f4a20ca504f02216640b77301c43c26449c0f54e8#from=url&id=M3kGH&margin=%5Bobject%20Object%5D&originHeight=320&originWidth=499&originalType=binary&ratio=1&status=done&style=none)]

执行原理:
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内部类:

  1. Node

CHM中的内个节点都是Node,它包装了key-value键值对,其中value和next都用volatile修饰,保证某线程修改这两个变量的值时其他线程可见
CHM与HashMap中的定义很相似,但是有一些差别它对value和next属性设置了volatile同步锁,它不允许调用setValue方法直接改变Node的value域,它增加了find方法辅助map.get()方法

  1. TreeNode

树节点继承自node节点,所以其拥有了node的属性和方法。
当链表中挂载的节点数超过8时该链表转换为红黑树结构,但是与HashMap不相同的是,它并不是直接转换为红黑树,而是把这些结点包装成TreeNode放在TreeBin对象中,由TreeBin完成对红黑树的包装。

  1. TreeBin

TreeBin维护了一棵红黑树,保存了该红黑树的跟节点,代替了TreeNode的根节点,因为以该根节点作为锁,可能这个位置树结构改变了,不是当前的树节点,导致这个位置有多个线程操作,产生安全问题,所以用TreeBin封装了这颗红黑树,只有一个树对象,方便加锁,里面怎么变就咋变,如果有个线程也来插入,但没法获取该锁。
所以当位置存放的是树结构,则他存放的是TreeBin对象,而不是TreeNode对象,这是与HashMap的区别。

  1. 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

==========尚待完善

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值