JAVA SE 集合及其底层源码剖析

一、前言

大家好!集合是在以后工作面试中出现的必考题,本章我们将一起来学习集合的使用以及深入剖析它的底层原理。我们知道,在没有学习和接触到集合这一概念之前,当我们遇到要存储两个以上数据时,一般都是利用数组进行存储,虽然解决了多数据存储这一个问题,但是同时面临的是利用数组存储所带来的缺点。

  1. 数组长度在定义时就已经确定,而一旦确定无法更改,这就造成容量拓容问题
  2. 数组里面保存的元素必须是同一类型,造成类型单一化问题
  3. 最后是使用数组进行增加/删除元素比较麻烦(低效)

基于以上问题,我们引出了集合这一概念,集合在实现存储多个数据的基础上,又可以动态保存多个对象,再者提供了一系列方便操作对象的方法:add、remove、set、get等,最后就是使用集合添加和删除新元素能简化代码,便于阅读及操作。

二、集合的框架体系

JAVA集合类很多,主要分为两大类及单列集合(collectio)双列集合(map),下面整理出集合的框架体系图(罗列出部分常用实现类),请大家在学习的过程中务必对这个体系图十分熟悉。

(图为单列集合的类接口继承图)

(图为双列集合类接口继承图)

三、Collection接口和常用方法

3.1Collection接口实现类的特点

  1. collection实现子类可以存放多个元素,每个元素的类型可以是Object(底层是List的实现类的字段中有Object[] elementData这个数组)详细后面会在后面的源码剖析中进一步说明,这里知道多个元素是维护在一个Object类型的数组中的就好
  2. 有些Collection的实现类可以存放重复的元素,有些不可以
  3. 有些Collection的实现类,有些是有序的(List),有些是无序的(Set)
  4. Collection接口没有直接的实现子类,是通过它的子接口Set和list来实现的

下面是list集合的常用方法使用案例

public class CollectionMethod {
public static void main(String[] args) {

List list = new ArrayList();

// add:添加单个元素
list.add("jack");
list.add(10);//list.add(new Integer(10))
list.add(true);
System.out.println("list=" + list);

// remove:删除指定元素
//list.remove(0);//删除第一个元素
list.remove(true);//指定删除某个元素
System.out.println("list=" + list);

// contains:查找元素是否存在
System.out.println(list.contains("jack"));//T

// size:获取元素个数
System.out.println(list.size());//2

// isEmpty:判断是否为空
System.out.println(list.isEmpty());//F

// clear:清空
list.clear();
System.out.println("list=" + list);

// addAll:添加多个元素
ArrayList list2 = new ArrayList();
list2.add("红楼梦");
list2.add("三国演义");
list.addAll(list2);
System.out.println("list=" + list);

// containsAll:查找多个元素是否都存在
System.out.println(list.containsAll(list2));//T

// removeAll:删除多个元素
list.add("聊斋");
list.removeAll(list2);
System.out.println("list=" + list);//[聊斋]
}
}

3.2Collection接口遍历元素方式

3.2.1使用Iterator(迭代器)遍历

由集合类和接口继承图我们知道,Collection接口的父接口是Iterable,接口里面定义了一个Iterator()方法,用于获取一个Iterator对象(迭代器),我们可以利用获得的迭代器对集合进行遍历。注意:Iterator仅用于遍历集合,它本身并不存放对象。

3.2.2迭代器的执行原理说明

下面给出Iterator的结构图。

Iterator iterator = collection.iterator();//得到一个集合的迭代器

while(iterator.hasNext()){    //判断是否还有下一个元素

    iterator.next();    //指针下移,并将下移后的元素返回
}

注意事项:使用iterator.next()方法之前必须要调用iterator.hasNext()进行检测。若不调用下一条记录无效,直接调用it.next()会抛出NoSuchElementException异常。 当遍历结束后,由于此时迭代器中遍历的指针已到达底部,若想重新遍历,需要重置迭代器(即重新调用一下iterator()方法)

3.2.3 增强for循环遍历

增强for循环,可以代替iterator迭代器,其本质就是简化的iterator,只能用于遍历集合或数组。基本语法如下:

for(元素类型 元素名:集合或者数组名){
    //获得元素进行操作......
}

for(Object o : collection){

    System.out.println(o);
}

 四、List接口和常用方法

4.1List接口基本介绍

  1. List接口是Collection接口的子接口
  2. List集合类中的元素是有序的(即添加顺序和取出的顺序一致)、且元素可以重复存入
  3. List集合中的元素都有其对应的索引,即支持索引查找
  4. List接口常用的实现类有:Vector、ArrayList、LinkedList

4.2List接口常用方法

public class ListMethod {

public static void main(String[] args) {
List list = new ArrayList();
list.add("xixixi");
list.add("xixi");

// void add(int index, Object ele):在 index 位置插入 ele 元素
//在 index = 1 的位置插入一个对象
list.add(1, "xixi");
System.out.println("list=" + list);

// boolean addAll(int index, Collection eles):从 index 位置开始将 eles 中的所有元素添加进来
List list2 = new ArrayList();
list2.add("hhh");
list2.add("hh");
list.addAll(1, list2);
System.out.println("list=" + list);

// Object get(int index):获取指定 index 位置的元素

// int indexOf(Object obj):返回 obj 在集合中首次出现的位置
System.out.println(list.indexOf("kkk"));//2

// int lastIndexOf(Object obj):返回 obj 在当前集合中末次出现的位置
list.add("kk");
System.out.println("list=" + list);
System.out.println(list.lastIndexOf("kk"));

// Object remove(int index):移除指定 index 位置的元素,并返回此元素
list.remove(0);
System.out.println("list=" + list);

// Object set(int index, Object ele):设置指定 index 位置的元素为 ele , 相当于是替换. list.set(1, "uuu");
System.out.println("list=" + list);

// List subList(int fromIndex, int toIndex):返回从 fromIndex 到 toIndex 位置的子集合

List returnlist = list.subList(0, 2);
System.out.println("returnlist=" + returnlist);
}
}

五、ArrayList底层结构和源码剖析

5.1注意事项

  1. ArrayList集合中可以添加多个null(String)
  2. ArrayList是由数组来实现数据的存储的
  3. ArrayList基本等同于Vector,除了ArrayList是线程不安全(执行效率高),在多线程的情况下,建议不使用

5.2底层源码剖析

为了能够降低理解和学习的难度,这一小结利用(总-分-总)的顺序来进行学习,即先总结结论,再深入剖析

  1. ArrayList中维护了一个Object类型的数组elementData --> Object[] elementData
  2. 当创建ArrayList对象时,如果使用的是无参构造器,则初始elementData的容量为0第一次添加,则扩容到10个容量,如果需要再次扩容,则扩容为原来的1.5倍
  3. 如果使用的是制定大小的构造器,则初始elementData容量为指定大小,如果需要扩容,则直接扩容到原来的1.5倍
public class ArrayListSource {

public static void main(String[] args) {

//ArrayList list = new ArrayList();
ArrayList list = new ArrayList(8);

//使用 for 给 list 集合添加 1-10 数据
for (int i = 1; i <= 10; i++) {
list.add(i);
}
//使用 for 给 list 集合添加 11-15 数据
for (int i = 11; i <= 15; i++) {
list.add(i);
}

}
}

调用无参构造器情况:

 

 

 

 调用有参构造器情况:

六、Vector底层结构和源码剖析

 6.1Vector的基本介绍

  1. vector集合底层也是一个对象数组,protected Object[] elementData
  2. vector集合是线程同步的,即线程安全,Vector类的操作方法中都带有synchronized关键字,表示支持多线程操作
  3. 在开发中若考虑到线程同步安全时,优先使用Vector

6.2底层源码剖析

import java.util.Vector;

public class Test {
    public static void main(String[] args) {
        Vector vector = new Vector();
        for (int i = 0; i < 10; i++) {
            vector.add(i);
        }
        vector.add(100);
        System.out.println("vector=" + vector);
    }
}

        

 

 

由上面剖析可大概清楚,ArrayList和Vector的底层算法其实大同小异

七、Vector和ArrayList的比较

底层结构版本线程安全(同步)效率扩容倍数
ArrayList

可变数组

Object[]

JDK1.2不安全,效率高如果有参构造1.5倍,如果无参,第一次10,第二次还是按1.5扩
Vector可变数组Object[]JDK1.0安全,效率低无参,默认10,满后按2倍,若指定大小,则每次按原来2倍扩

八、LinkedList底层结构和源码剖析

8.1LinkedList的介绍

  1. LinkedList底层实现了双向链表双端队列
  2. 可以添加任何元素(可重复),包含null
  3. 线程不安全

8.2LinkedList的底层操作机制

  1. LinkedList底层维护了一条双向链表
  2. LinkedList中维护了两个字段firstlast分别指向首结点尾结点
  3. 每个结点(Node对象-->内部类),里面又维护了prev、next、item三个字段,其中通过prev指向前一个、通过next指向后一个结点,实现双向链表
  4. 因为LinkedList的元素删除添加不是通过数组来实现的,所以相对效率比较高

8.3底层源码剖析

public class Test {
    public static void main(String[] args) {
        LinkedList linkedList = new LinkedList();
        linkedList.add(1);
        linkedList.add(2);
        linkedList.add(3);
        System.out.println("linkedList=" + linkedList);

        linkedList.remove(); // 这里默认删除的是第一个结点
}
}

 

九、ArrayList和LinkedList的比较

底层结构增删效率改查效率
ArrayList可变数组效率低较高
LinkedList双向链表较高,通过链表追加较低

十、Set接口和常用方法

10.1使用Set接口注意事项:

  1. 添加在Set集合中的元素是无序的(即存入和取出顺序不一致)
  2. Set集合中不允许存在相同的元素,同理也最多能包含一个null
  3. JDK API中Set接口的实现类有(标记的为常用实现类)

10.2Set接口常用的方法

由于Set接口为Collection接口的子接口,所以常用的方法基本和Collection一样,API中接口方法如下:

以上就是Set接口的方法,详情请大家自行查阅API文档即可,就不再占用大篇幅进行示例了。

10.3 Set接口的遍历方式

同样由于Set接口是Iterable的子接口,同等会拥有Iterator()方法获得一个迭代器进行集合的遍历,与他拥有同等本质的增强For循环也是可以使用的,但要注意的一点是,List接口下的集合类都是支持利用索引方式来获取元素的,相反Set接口是不允许的。

十一*、HashSet底层结构和源码剖析(重难点)

11.1HashSet介绍

  1. 首先HashSet实现了Set接口
  2. HashSet实际上HashMap
  3. 可以存放null值,但仅能有一个
  4. HashSet不保证元素是有序的,取决于Hash后再确定再底层table数组的索引(即不保证存放元素的顺序于取出一致)
  5. 由于为Set接口实现类,所以同样不能存放相同元素

11.2HashSet底层源码剖析

HashSet底层其实是HashMap,而HashMap底层其实是利用数组+链表+红黑树构成

  1. 添加一个元素时,先会获取元素的哈希值(hashCode())
  2. 进而对获取的哈希值进行特殊运算处理(异或运算和位运算)得到一个索引值,该值即为要存放在哈希表中的位置号
  3. 如果发现该索引位上没有存放其他元素,则直接放入该位置,如果该位置已经有其他元素,则需要进一步通过equals判断如果相等,则不添加,否则以链表的方式进行添加在尾部
  4. 在JDK8版本中,如果一条链表的元素个数到达8个并且table数组的大小大于等于64,就会对链表进行树化(红黑树)

public class Test {
    public static void main(String[] args) {
        Set hashSet = new HashSet();
        hashSet.add(null);
        hashSet.add(null);
        System.out.println("hashSet=" + hashSet);
    }
}

下面是扩容和添加的核心源码,我们一步步来剖析

 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 ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            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;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

 

  

 

 

11.3LinkedHashSet底层结构和源码剖析

11.3.1LinkedHashSet说明

  1. LinkedHashSet是HashSet的子类
  2. LinkedHashSet底层是一个LinkedHashMap,底层维护的是一个数组+双向链表
  3. LinkedHashSet根据元素的hashCode值来决定元素在数组的索引,同时使用双向链表来连接节点,使得元素看起来是以插入的顺序保存的
  4. 不允许添加重复元素

下面是LinkedHashSet的底层存储结构图

  1. 在LinkedHashSet中维护了一个hash表双向链表(LinkedHashSet中有head和tail字段)
  2. 每一个节点(Node)对象里面都含有字段beforeafter,这样可以形成双向链表
  3. 同样在添加一个元素时,先求hash值,在求索引值,确定该元素在table数组中的位置,然后将添加的元素加入到双向链表中(同样要经过hashCode和equals)的检验

在一个案例中我们创建一个LinkedHashSet,并加入元素后,实现了有序的存储,对象的字段如下:

十二、Map接口实现类的特点

12.1Map底层存储说明

  1. Map与Collection并列存在。用于保存具有映射关系的数据(Key-Value)
  2. Map中的key和Value可以是任意的数据类型,这一对K-V会封装到HashMap$Node对象中去
  3. Map中的Key不允许重复,从HashSet的本质就同理可看出
  4. Map中Value可以重复
  5. 常用String类作为Key
  6. K-V存在单向一对一的关系,通过K可以找到V

Map存放数据的K-V的图解如下:

                                                                        (图解来自韩顺平老师)

 由上图可以看出,一对K-V是放在一个HashMap$Node中的,而Node这一HashMap的内部类是实现了Entry接口的。

12.2Map接口的遍历方法

public static void main(String[] args) {
Map map = new HashMap();

map.put();
map.put();
map.put();
map.put();
map.put();
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 使用的遍历方法

//(1) 增强 for
System.out.println("---取出所有的 value 增强 for----");
for (Object value : values) {
System.out.println(value);
}

//(2) 迭代器
System.out.println("---取出所有的 value 迭代器----");
Iterator iterator2 = values.iterator();
while (iterator2.hasNext()) {
Object value = iterator2.next();

System.out.println(value);
}

//第三组: 通过 EntrySet 来获取 k-v
Set entrySet = map.entrySet();// EntrySet<Map.Entry<K,V>>

//(1) 增强 for
System.out.println("----使用 EntrySet 的 for 增强(第 3 种)----");
for (Object entry : entrySet) {
//将 entry 转成 Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + "-" + m.getValue());
}

//(2) 迭代器
System.out.println("----使用 EntrySet 的 迭代器(第 4 种)----");
Iterator iterator3 = entrySet.iterator();
while (iterator3.hasNext()) {
Object entry = iterator3.next();
//System.out.println(next.getClass());//HashMap$Node -实现-> Map.Entry (getKey,getValue)
//向下转型 Map.Entry
Map.Entry m = (Map.Entry) entry;
System.out.println(m.getKey() + "-" + m.getValue());
}

 十三、HashMap底层结构和源码剖析

13.1结论

  1. Map接口的常用实现类:HashMap、HashTable、Properties
  2. HashMap是以key-value对的方式来存储数据的
  3. 如果添加相同的Key,会覆盖原来的Val
  4. 与HashSet一样,数据存储是无序的
  5. HashMap没有实现线程同步,不安全,没有互斥处理

13.2底层源码剖析

                                                                                (图解来自韩顺平老师)

  1. HashMap底层维护了Node类型的数组table,默认null
  2. 当创建对象时,将加载因子(loadFactor)初始化了0.75
  3. 当添加k-v时,通过Key的哈希值得到在table处对应的索引,然后判断该索引处是否有元素,如果没有则直接添加,如果有,则继续通过判断该元素的Key与准备加入的Key是否相等,如果相等,则会替换旧Key,不相等则需要判断是利用树的方式添加还是链表方式添加,当添加时发现到达扩容量临界值时,则需要扩容
  4. 在JDK8中,如果一条链表的元素超过8个并且table容量大于等于64就会进行树化(红黑树)

下面是源码剖析,由于HashSet的底层是HashMap,所以在这一小节就不重复赘述,只例举异同点和重点

if (e.hash == hash && //如果在循环比较过程中,发现有相同,就 break,就只是替换 value
((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; //替换,key 对应 value
afterNodeAccess(e);
return oldValue;
}

 

十四、HashTable介绍

14.1注意事项:

  1. 存放的元素是K-V
  2. hashtable的键值都不能为null,否则会抛出空指针异常
  3. 用法于HashMap一致
  4. 线程安全

14.2 HashMap与HashTable比较

Version线程安全效率允许null键null值
HashMap1.2不安全允许
HashTable1.0安全不允许

十五、Properties介绍

  1. Properties类继承自HashTable类并且实现了Map接口,也是使用K-V来存储数据
  2. 与HashTable相似
  3. (常用)Properties常用于xxx.properties配置文件中,进行读取或者修改

十六、集合用法和选择总结

在日常开发中,对于不同集合的选用主要取决于不同的业务逻辑需要,具体选择思路如下:

  1. 先判断存储的类型(单列或者双列)
  2. 单列:Collection接口(重复:list(增删多:LinkedList  改查多: ArrayList)   不重复:  Set(无序: HashSet  排序:TreeSet  插入和取出顺序一致:LinkedHashSet)      )
  3. 双列:Map接口(键无序:HashMap 键排序:TreeMap  键插入和取出顺序一致LinkedHashMap  读取文件:Properties

十七、Collection工具类

                                                                (部分)

自己查询API即可

十八、总结

天啊!!!!终于结束了,麻了吧,本章单纯文本就超过了一万字,我也算重复学习了一遍,真的从学习到写帖子花了我将近两个星期的时间,终于熬过来了。集合这一章真的太重要了,太重要了,太重要了,重要的话说三遍,有工作经验的小伙伴可能知道,集合是工作面试中基本必问的知识点,使用它固然没有多大难度,但是底层原理才是面试中会遇到的,我们不仅仅是为了通过考核而去学习它的底层,更是在阅读高质量代码的同时提升自己,认识自己的不足,在以后遇到使用集合方面出现错误的时候,能够更加快速解决问题以及高效地使用集合,本章部分图有所借鉴。

我亦无他,唯手熟尔

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值