集合(的大集合)

这篇文章很长,但是有各类经常用的所有集合,包括其底层源码分析哦!

小伙伴们可以选着看!!!

集合与数组的区别:

1.集合是可变的,其长度是固定不变的。

2.集合是存储引用数据类型。

应用场景:

1.集合:当我们储存的数据类型是引用数据类型或者元素个数不确定时。

2.数组:存储的是基本数据类型或者元素的个数确定时。

集合按照其存储的数据结构分为两大类:

单列集合:java.util.Collection

双列集合:java.util.Map

          Collection:单列集合的超类,用于存储一系列符合某种规则元素,它有两个重要的子接口:

java.util.Set:元素是无序的,并且不可有相同的元素。

java.util.List:元素是有序的(即存进去的是什么顺序,出来的就是什么顺序),并且可以有相同的元素。

List两个重要接口:

ArrayList

LinkedList

Set两个重要接口:

HashSet

TreeSet

Collection常用功能:

Collection是所有的单列的父类接口,因此定义了List和Map通用的方法,这些方法用于所有的单列集合。

Collection接口遍历元素方式1:使用Iterator(迭代器)

1.所有实现了Collection接口的集合类都有一个iterator() 方法,用于返回一个实现了Iterator接口的对象,即可以返回一个迭代器。

2.迭代器的结构(如下图)

3.迭代器仅用于遍历集合,Iterator本身不存放对象。

Collection col=new ArrayList();

col.add(new Book("三国演义",90));

col.add(new Book("水浒传",90));

col.add(new Book("红楼梦",90));

//遍历col集合

//先得到col 对应的 迭代器

Iterator iterator=col.iterator();

//2.使用while循环遍历(快捷键:itit+Enter)

while(iterator.hasNext()){//判断是否还有数据

//返回下一个元素,类型是Object

Object obj=iterator.next();

sout(obj);

}

//3.当退出while循环后,这是迭代器已经指向最后的元素

//如果再获取下一个元素

//iterator.next();//抛出异常NoSuchElementException

//如果希望再次遍历,需要重置迭代器

iterator=col.iterator();

方式2:增强for(在Collection和数组上使用)

快捷方式:大写的I

源码:底层仍然是迭代器,即简化版本的迭代器遍历

for(Object book:col){

 sout(book);

}

List接口:有序,可重复

内部类排序

ArrayList细节:

1.可以存放所有元素,包括null;

2.底层是由数组来实现存储的

3.基本等同与 Vector ,只是ArrayList是线程不安全的(源码:没有synchronized关键字)

多线程下,不建议使用ArrayList

ArrayList的底层源码分析:

1.ArrayList中维护的是Object[] elementData

源码:

transient Object[] elementData;//transient 表示瞬间,短暂的,表示该属性不会被序列化

2.当我们创建ArrayList对象时,使用无参构造器时,则初始elementData容量为0,第一次添加,则扩容elementData为10,如需要再次扩容,则扩容为elementData的1.5倍。

2.如果使用指定大小的构造器(有参构造器),则初始elementData容量为指定大小,如果需要扩容,则直接扩容elementData的1.5倍。

源码分析:

无参构造器:

有参构造器:

Vector类:

1.定义说明

2.Vector底层也是一个对象数组 ,protected Object[] elementData;

3.Vector是线程同步的,即线程安全,Vector类的操作方法带有synchronized

4.考虑线程安全时,用Vector

扩容机制:

LinkedList:

 1.LinkedList底层实现了双向链表和双端队列特点

2.可以添加任意元素(可以重复),包括null

3.线程不安全,没有实现同步

4.添加/删除 数据,不是数组的操作,效率较高

添加方法:

remove方法:

linkedList.remove();//默认删除第一个元素

底层源码分析:

如何选择ArrayList和LinkedList:

1.改查多,ArrayList

2.增删多,LinkedList

3.大部分是查,即ArrayList

Set接口:

1.无序(添加的顺序和取出的顺序不一致,但是取出的顺序是固定的,不会每次运行都改变 ),没有索引

2.不允许重复元素,最多只能包含一个null

//如果添加了重复的元素,将不会多次打印,但也没有异常

常用方法:

Set接口也是Collection的子接口。

遍历方法:

1迭代器

2.增强for

3.不能通过索引去获取

//这是下面测试的集合初始化 代码
Set set=new HashSet();

set.add("john");

set.add("jack");

set.add(null);

set.add("jack");

set.add("john");

sout(set);//[null,john,jack]

set.remove(null);

Set接口实现类——HashSet

1.HashSet实现了Set接口

2.HashSet实际上是HashMap,看构造器的源码

3.HashSet可以存放null,但是只能由一个null,即元素不能重复

4.不保证存放的元素的顺序和取出顺序一致

5.不能存放相同的元素/对象

HashSet底层机制说明:

HashSet底层是HashMap,HashMap底层是(数组+链表+红黑树)

数组链表模拟:(实现如下效果)

代码:

public static void main(String[] args) {

        Node[] table=new Node[16];

        //创建节点

        Node john = new Node("john", null);

        table[2]=john;

        Node jack = new Node("jack", null);

        john.next=jack;//将jack 挂载到 john 后边

        Node rose = new Node("rose", null);

        jack.next=rose;//将rose 挂载到 jack 后边

        Node lucy = new Node("lucy", null);

        table[3]=lucy;

        System.out.println(table);

    }

   

public class Node {

    public  String item;

    public Node next;

    public Node(){}

    public Node(String item, Node next) {

        this.item = item;

        this.next = next;

    }

    public String getItem() {

        return item;

    }

    public Node setItem(String item) {

        this.item = item;

        return this;

    }

    public Node getNext() {

        return next;

    }

    public Node setNext(Node next) {

        this.next = next;

        return this;

    }

}

HashSet扩容机制:

1.HashSet的底层是HashMap

2.添加一个元素时,先得到hash值,会转成索引值

3.找到存储数据表table,看这个索引位置是否已经存放的有元素

4.如果没有,则直接加入

5.如果有,调用equals比较,如果相同就放弃添加,如果不相同则添加到next(如上代码的jack加到john后面)

6.在JDK8中,如果一条链表的元素个数到达TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化(红黑树);否则仍然采用数组扩容机制。//满足以上两个条件,数组arr[i]处的链表将自动转化为红黑树,其他位置如arr[i+1]处的数组元素仍为链表。

扩容和转成红黑树机制:

1.HashSet底层是HashMap,第一次添加时,table数组扩容到16,临界值(threshold)是16*加载因子(loadFactor)是0.75=12

2.如果table数组使用到了临界值12,就扩容到16*2=32,新的临界值就是32*0.75=24,依次类推

底层源码分析:

HashSet hashSet=new HashSet();

PRESENT:static final Object PRESENT=new Object();

该方法会执行hash(key) 得到key对应的hash值 算法(h=key.hashCode())^(h>>>16)

key="java",value=PRENSENT共享

hash(key)方法:

putVal()方法:(最重要的)

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,

                   boolean evict) {

        Node<K,V>[] tab; Node<K,V> p; int n, i;//定义了辅助变量

//if语句表示如果当前table是null,或者 大小=0

//就第一次扩容,到16个空间

        if ((tab = table) == null || (n = tab.length) == 0)

            n = (tab = resize()).length;//执行resize(),见下面

//(1)根据key,得到的hash值,去计算该key应该存放到table 的哪个索引位置

//并把这个位置的对象,赋给p

//(2)判断p是否为null

//(2.1)如果p为null,表示还没有存放元素,就创建一个Node(key="java",value=PRESENT)

//(2.2)就放在该位置tab[i]=newNode(hash,key,value,null)

        if ((p = tab[i = (n - 1) & hash]) == null)

            tab[i] = newNode(hash, key, value, null);

        else {

//开发技巧提示:在需要的局部变量(辅助变量)时候,再创建

            Node<K,V> e; K k;//辅助变量

//如果当前索引位置对应的链表的第一个元素和准备添加的key的hash值一样

            if (p.hash == hash &&

//并且满足 下面两个条件之一:

  • //(1)准备加入的key 和 p指向的Node 结点的key 时同一个对象

//(2)p 指向的Node 结点的 key 的 equals() 和准备加入的key比较后相同(equals() 是程序员可以重写来确定的比较的内容,不能只理解为比较内容)

//就不能加入

                ((k = p.key) == key || (key != null && key.equals(k))))

                e = p;

//再判断p 是不是一颗红黑树,

//如果是一颗红黑树,就调用putTreeVal(),来进行添加

            else if (p instanceof TreeNode)

                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);

//如果当前table对应的索引位置,已经是一个链表,就使用for循环比较

//1.依次和该链表的每一个元素比较后,都不相同,则加入到该链表的最后

//注意:在把元素添加到链表最后,之后,立即判断 该链表 是否已经到达8个结点

//如果是,就调用 treeifyBin()  对当前这个链表进行树化(转成红黑树)

//注意:在转成红黑树时,先判断,如果该table数组的大小<64,则先对table扩容;否则才进行转成红黑树

//2.依次和该链表的每一个元素比较过程中,如果有相同元素的情况,就直接break

            else {

                for (int binCount = 0; ; ++binCount) {

                    if ((e = p.next) == null) {

                        p.next = newNode(hash, key, value, null);

                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st

                            treeifyBin(tab, hash);

                        break;

                    }

                    if (e.hash == hash &&

                        ((k = e.key) == key || (key != null && key.equals(k))))

                        break;

                    p = e;//链表后移

                }

            }

            if (e != null) { // existing mapping for key

                V oldValue = e.value;

                if (!onlyIfAbsent || oldValue == null)

                    e.value = value;

                afterNodeAccess(e);

                return oldValue;

            }

        }

        ++modCount;

//size 就是我们每加入一个结点Node(k,v,h,next):不论是结点还是结点链表的结点(横还是竖),size++

        if (++size > threshold)

            resize();

        afterNodeInsertion(evict);

        return null;

    }

执行resize()方法:

final Node<K,V>[] resize() {

        Node<K,V>[] oldTab = table;//oldTab=null

        int oldCap = (oldTab == null) ? 0 : oldTab.length;//oldCap=0

        int oldThr = threshold;

        int newCap, newThr = 0;

        if (oldCap > 0) {

            if (oldCap >= MAXIMUM_CAPACITY) {

                threshold = Integer.MAX_VALUE;

                return oldTab;

            }

            else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&

                     oldCap >= DEFAULT_INITIAL_CAPACITY)

                newThr = oldThr << 1; // double threshold

        }

        else if (oldThr > 0) // initial capacity was placed in threshold

            newCap = oldThr;

        else {               // zero initial threshold signifies using defaults

            newCap = DEFAULT_INITIAL_CAPACITY;//newCap=16

            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//newThr=(int)16*0.75=12

        }

        if (newThr == 0) {

            float ft = (float)newCap * loadFactor;

            newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?

                      (int)ft : Integer.MAX_VALUE);

        }

        threshold = newThr;  //临界值为12,即当链表的元素有12个时就扩容

        @SuppressWarnings({"rawtypes","unchecked"})

            Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//创建一个16个节点的链表

        table = newTab;  //将创建的链表赋给table

        if (oldTab != null) {

            for (int j = 0; j < oldCap; ++j) {

                Node<K,V> e;

                if ((e = oldTab[j]) != null) {

                    oldTab[j] = null;

                    if (e.next == null)

                        newTab[e.hash & (newCap - 1)] = e;

                    else if (e instanceof TreeNode)

                        ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);

                    else { // preserve order

                        Node<K,V> loHead = null, loTail = null;

                        Node<K,V> hiHead = null, hiTail = null;

                        Node<K,V> next;

                        do {

                            next = e.next;

                            if ((e.hash & oldCap) == 0) {

                                if (loTail == null)

                                    loHead = e;

                                else

                                    loTail.next = e;

                                loTail = e;

                            }

                            else {

                                if (hiTail == null)

                                    hiHead = e;

                                else

                                    hiTail.next = e;

                                hiTail = e;

                            }

                        } while ((e = next) != null);

                        if (loTail != null) {

                            loTail.next = null;

                            newTab[j] = loHead;

                        }

                        if (hiTail != null) {

                            hiTail.next = null;

                            newTab[j + oldCap] = hiHead;

                        }

                    }

                }

            }

        }

        return newTab;//返回新创建的长度为16的链表

    }

LinkedHashSet:

关系继承图:

基本介绍:

1.LinkedHashSet是HashSet的子类

2.LinkedHashSet底层是一个LinkedHashMap,底层维护了一个数组+双向链表

3..LinkedHashSet根据元素的hashCode 值来决定元素的存储位置,同时使用链表维护元素的次序(图),这使得元素看起来是以插入顺序保存的。

4..LinkedHashSet 不允许添加重复元素

底层机制:

源码说明:

1.在LinkedHashSet 中维护了一个hash 表和双向链表(LinkedHashSet 有 head 和tail)
2.每一个节点有before和after属性,这样可以形成双向链表

3.在添加一个元素时,先求hash值,再求索引,确定该元素在table的位置,然后将添加的元素加入到双向链表(如果已经存在,不添加【原则和hashSet一样】)

//示意代码

tail.next=newElement;

newElement.pre=tail;

tail=newElement;

4.这样的话,我们遍历LinkedHashSet 也能确保插入顺序和遍历顺序一致

5.添加第一次时,直接将 数组table 扩容到16,存放的节点类型为 LinkedHashMap$Entry

6.数组是 HashMap$Node[] 存放的数据/元素为 linkedHashMap$Entry(说明两者存在继承关系:如下)

//继承关系是在内部类完成

Map接口实现类的特点:

JDK8

1.Map 和 Collection 并列存在。用于保存具有映射关系的数据:Key-Value(双列元素)。

2.Map 中的key 和 value可以是任何引用类型的数据,会封装到HashMap$Node 对象中

3.Map 中的key 不允许重复,原因和HashSet一样。但是value可以重复

Map map=new HashMap():

map.put("no1","张三");

map.put("no2","李四");

map.put("no1","王二狗");//当出现key重复时,会将no1(重复key)对应的value替换

//即存入map.put("no1","王二狗");和map.put("no2","李四");

4.Map 的key 可以为null,value也可以为null,注意:key为null,只能由一个,否则替换;value可以由多个

5.常用String来作为Map的key,也可以是其他Object的子类

6.key与value存在一对一的关系,可以通过key找到唯一的value

map.get("no2");//通过get方法,传入key,获取唯一对应的value

7.第7个特点

1.k-v 最后是 HashMap$Node node=newNode(hash,key,value,null);

2.k-v 为了方便程序员的遍历,还会创建 EntrySet 集合,该集合存放的元素的类型 Entry ,而一个Entry对象就有k,v 。

 EntrySet<Entry<K,V>>  即:tansient Set<Map.Entry<K,V>> entrySet;

3.entrySet 中,定义的类型是Map.Entry,但是实际上存放的还是HashMap$Node

  这是因为static class Node<K,V> implements Map.Entry<K,V>

4.当把HashMap$Node 对象存放到 entrySet 就方便我们的遍历,因为Map.Entry提供了两个重要的方法

  K getKey(); V getValue();

Map接口常用方法:

put();//添加元素

remove();//根据键删除映射关系

get();//通过键获取值,返回Object类型

size():获取元素个数,键盘对的对数

isEmpty();//判断个数是否为0

clear();//清空键值对

containsKey();//是否包含该键

Map接口遍历六大方式:

1.containsKey:查找键是否存在

2.keySet:获取所有的键

3.entrySet:获取所有的k-v关系

4.values:获取所有的值

示例:

Map map=new HashMap();

map.put("邓超","孙俪");

map.put("王宝强","马蓉");

map.put("刘林伯",null);

map.put(null,"刘亦菲");

map.put("鹿晗","关晓彤");

//第一组:先取出所有的key,通过key取出对应的value

Set keySet = map.keySet();

//(1)增强for

System.out.println("第一种方式:");

for (Object key :keySet) {

    System.out.println(key+"-"+map.get(key));

}

//(2)通过迭代器

System.out.println("第二种方式:");

Iterator iterator = keySet.iterator();

while (iterator.hasNext()) {

    Object key =  iterator.next();

    System.out.println(key+"-"+map.get(key));

}

//第二组:把所有的values取出来:只能取出值

Collection values = map.values();

//这里可以使用所有Collections使用的遍历方法

//增强for

for (Object value :values) {

    System.out.println(value);

}

//迭代器

Iterator iterator1 = values.iterator();

while (iterator1.hasNext()) {

    Object value =  iterator1.next();

    System.out.println(value);

}

//第三组:通过EntrySet 来获取k-v

Set entrySet = map.entrySet();//EntrySet<Map.Entry<K,V>>

//增强for

for (Object entry :entrySet) {

    //将entry转成 Map.Entry

    Map.Entry m=(Map.Entry) entry;

    System.out.println(m.getKey()+"-"+m.getValue());

}

//迭代器

Iterator iterator2 = entrySet.iterator();

while (iterator2.hasNext()) {

    Object entry =  iterator2.next();

    //System.out.println(next.getClass());//HashMap$Node implements Map.Entry<K,V>

    //向下转型 Map.Entry

    Map.Entry m=(Map.Entry)entry;

    System.out.println(m.getKey()+"-"+m.getValue());

}

HashMap接口小结:

1.Map接口的常用实现类:HashMap,Hashtable和Properties.

2.HashMap是Map接口使用频率最高的实现类

3.以键值对的方式来存储数据(HashMap$Node类型)

4.HashMap没有实现同步,线程不安全,没有synchronized

HashMap底层机制:

扩容机制【和HashSet完全一样】:

1.底层维护了Node 类型的数组table,默认为Null

2.添加一个key相同的数据时,直接替换

 if (p.hash == hash &&

  ((k = p.key) == key || (key != null && key.equals(k))))

                e = p;

然后进入:

 if (e != null) { // existing mapping for key

                V oldValue = e.value;//将原来的与key相同键的value赋给oldValue

                if (!onlyIfAbsent || oldValue == null)//onlyIfAbsent 系统默认为false

                    e.value = value;//重要!将value值替换

                afterNodeAccess(e);

                return oldValue;//返回一个值,导致后面返回值不为null(即添加失败)

            }

Map接口实现类--Hashtable

基本介绍:

1.存放的元素是键值对:即K-V

2.Hashtable的键和值都不能为null,否则会抛出空指针异常

3.Hashtable使用方法基本上和HashMap一样

4.Hashtable 是线程安全的,HashMap是线程不安全的

Hashtable的底层:

1.底层有数组 Hashtable$Entry[]  初始化大小为11

2.临界值 threshold=11*0.75=8

3.扩容机制:

执行方法 addEntry(hash,key,value,index);

当if(count>=threshold)满足时,就进行扩容

HashMap与Hashtable对比:

Map接口实现类--Properties

基本介绍:

1.Properties类继承自Hashtable 类并且实现了Map接口,也是使用一种键值对的形式来保存数据。

2.它的特点和Hashtable类似

3.Properties 还可以用于 从xxx.properties 文件中,加载数据到Properties类对象,并进行读取和修改

put();//增加

remove();//删除

put();//修改

get();//查

getProperty();

选择集合规则:

1.先判断存储的类型(单列或双列)

2.单列:

2.1 允许重复:List

2.1.1 增删多:LinkedList[双向链表]

2.1.2 改查多:ArrayList[Object类型的可变数组]

2.2 不允许重复:Set

2.2.1 无序:HashSet[维护了一个哈希表,即(数组+链表+红黑树)]

2.2.2 排序:TreeSet

2.2.3 取出顺序和添加顺序一致:LinkedHashSet[维护了数组+双向链表]

3.双列:Map

3.1 键无序:HashMap[jdk7:数组+链表  jdk8:数组+链表+红黑树]

3.2 键排序:TreeMap

3.3 键插入和取出的顺序一致:LinkedHashMap

3.4 读取文件 Properties

TreeSet:

有参构造:

TreeSet  treeSet = new TreeSet(new Comparator(){

@Override

public int compare(Object o1,Object o2){

//进行字符哈希码做差

return((String)o1).compareTo((String)o2);

}

});

compareTo方法:

 

底层源码分析:

1.底层仍然是HashMap

2.第一次添加元素

返回null,添加成功!

第二次添加时,

首先将比较器赋给cpr,

判断比较内是否为空:

若不为空,

则调用比较器的compare方法,接收返回的值

以下结果主要由compareTo方法得到:

如果小于0,则表示第一个元素的第一个字母小于当前元素的第一个字符,即加到后边

如果大于0,反之

如果等于0,则代表相等,则不能加入

如果为null,

Collections工具类:

1.reverse(List):反转List集合中的元素

2.shuffle(List):对List的元素进行随机排序(每次运行都不一样)

3.sort(List):根据元素的自然顺序对指定List集合元素按升序排列

4.sort(List,Comparator):根据定制的规则对List集合中元素进行排序

5.swap(List,int i,int j):将指定List集合中i处和j处元素进行交换

6.Object max(Collection):根据元素的自然排序,返回给定集合中最大的元素

7.Object max(Collection,Comparator):根据Comparator指定的顺序,返回给定集合中最大的元素

8.int frequency(Collection,Onject):返回指定集合中指定元素的出现次数

9.void copy(List dest,List,src):将src中的内容复制到dest中

10.boolean replaceAll(List list,Object oldVal,Object new Val):使用新值替换List对象的所有旧值

练习题1:

答:会,ClassCastException,因为Person类未实现Comparable接口

源码如下:

练习

移除不能成功:因为p1.name已经被修改,hash值对应的计算出的索引也会变

第三次添加:添加时hash值计算的索引和原来的索引不一样

第四次添加:因为已经存在1001和AA对象对应的索引(即第一次的索引),则挂载到p1后面

遗留问题:new String();两个相同的,只能添加一个

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值