Java集合详解

Java集合

集合体系

Java集合详解

Collection接口

Collection接口没有直接的实现子类,它的子接口list(有序容器,可以重复)和set(无序容器,不可重复)是两个重要的子接口,接口实现子类都是单列集合。

Collection接口常用方法

Collection接口定义了有很多常用的方法,它的实现子类都可以使用这些方法

方法 作用 add 向集合添加元素,放回类型为boolean addAll 向集合添加多个元素,参数为接口对象 remove 删除指定的对象,实现子类还重载了更多方法 removeAll 删除多个元素,参数为接口对象 contains 是否包含某个元素 isEmpty 判断集合是否为空 size 返回集合元素个数 iterator 返回迭代器对象,用于遍历 clear 清空集合

Collection遍历

因为一些集合的实现子类不支持索引,所以集合类统一使用迭代器进行遍历,Collection继承了Iterable接口,Iterable接口定义了iterator()方法,所有实现了Collection接口的子类都实现了iterator()方法。

遍历方式一:

while (iterator.hasNext()) {//判断是否还有数据
    Object obj = iterator.next();//获得下一个元素
    System.out.println("obj=" + obj);
}

遍历方式二:

使用增强for循环也可以对集合进行遍历, 底层的执行还是在使用迭代器

for (Object o : list) {
    System.out.println("obj=" + obj);
}

Iterator接口

在看源码时,发现Collection接口及其实现子类都没有实现Itrator接口,因此没有next()和hasNext()方法,那为什么能用迭代器遍历呢?实际上(以ArrayList为例)是它定义了一个 内部类Itr实现了Iterator接口,并实现了next()和hasNext()方法,而 Iterator()方法返回的是Itr对象

List接口

List接口是有序容器,即添加元素的顺序和取出元素的顺序是一致的,且可以重复,可以添加空对象。List支持索引。

List接口方法

除了Collention接口的方法外,List接口还定义了以下方法。

方法 功能 get 按照索引获取某个元素 set 按照索引修改某个元素 inexOf 获取某个元素第一次出现的位置 lastIndexOf 获取某个元素最后一次出现的位置 subList 根据索引位置返回集合的子集合 sort 对集合进行排序,可以自定义排序规则

ArrayList

  • ArrayList使用数组来实现数据存储。
  • ArrayList等同于Vector,但是ArrayList线程不安全。

ArrayList扩容机制

  • ArrayList维护了一个Object对象数组 transient Object[] elementData
  • 创建ArrayList对象使用无参构造器时,则 初始化容量为0,第一次创建时会扩容为10,之后每次需要扩容则扩大为原来的 1.5倍
  • 创建ArrayList对象使用指定大小的构造器时, 初始化容量为指定大小,之后需要扩容则扩大 1.5倍

思考题一:

如果用0指定大小来扩容,然后添加元素发生什么?当一次性添加多个元素,扩容后空间也不够怎么办?

实际上扩容时会先计算容纳当前元素和添加元素的的所需最小空间大小,如果只添加一个元素则 minCapacity = size + 1,添加多个元素, minCapacity = size + numNew(新添加元素的数量),在进行扩容时,如果扩容后的容量 小于minCapacity,则使用minCapacity进行扩容 否则还是扩容为当前容量的1.5倍。

思考题二

ArrayList中的modCount是什么,有什么作用?

modCount记录集合被修改的次数,包括删除,添加和修改等操作,那么为什么要记录这个修改次数呢?这是因为ArrayList是 线程不安全的,在迭代器遍历的过程中是 允许对集合进行修改,这在 多线程下会引发严重的问题
modCount就是为了解决这样的问题:迭代器对象初始化时,属性expecedModCount的值初始化为modCount,每次迭代过程中都会检查两者是否相等,一旦在某个线程中对集合进行修改,检测出两者不相等就会抛出ConcurrentModificationException异常。

Vector

  • Vector也使用数组来实现数据存储。
  • Vector是线程同步的,多线程场景下使用Vector
  • Vector扩容机制等同于ArrayList,不过 *扩容大小为2倍

Stack

Stack是Vector的子类,实现了后进后出,除了Vector定义的方法,Stack还定义了自己的方法,如下。

方法 说明 empty 测试堆栈是否为空 peek 返回栈顶对象, pop 返回栈顶对象,并从栈中移出 push 将一个对象压入栈中 search 返回一个对象在栈中的索引,1为基数

  • LinkedList实现了 双向链表双端队列
  • LinkedList是线程不安全的。

底层结构 增删的效率 改查的效率 ArrayList 数组 数组扩容,效率低 连续存储,效率高 LikedList 双向链表 动态扩容,效率高 链式存储,效率低

Deque

因为LinkedList实现了 Deque(Double ended Queue)接口,因此LinkedList可以当作双端队列来使用,同时也可以作为栈来使用。

//普通队列(一端进另一端出):
Queue queue = new LinkedList()
//双端队列(两端都可进出),或者作为栈来使用
Deque deque = new LinkedList()

在Deque中提供了两套添加和修改的方法,一种在操作抛出异常,一种是返回一个特殊值(null或false)。

抛出异常 特殊值 抛出异常 特殊值 插入 addFirst(e) offerFirst(e) addLast(e) offerLast(e) 删除 removeFirst() pollFirst() removeLast() pollLast() 获取 getFirst() peekFirst() getLast() peekLast()

Deque原文链接:https://blog.csdn.net/devnn/article/details/82716447

set接口

set是无序容器,存取的顺序不一致,不允许存放重复元素,不支持索引

HashSet

  • Hashset的底层是HashMap,是一个链表数组结构,即数组中的每一个元素都是一个链表,当一条链表上的元素有过多时,该链表会转化为一颗红黑树来提高查找效率。
  • 为了保证set没有重复元素,添加的该对象对应的类必须重写equals()方法。
  • 重写hashCode()方法能由程序员来决定对象属性的哪些值一致会被映射到对象数组的同一位置中。

向HashSet添加元素时,先计算元素的hash值再映射到数组中的位置,如果这个位置已经由元素了,会用equals()方法比较,如果相同则退出,不会添加到HashSet中,然后遍历链表,最后挂到链表的末尾上。

HashSet元素添加的底层执行过程

  1. 在HashSet()构造器中,使用HashMap创建对象
public HashSet() {
    map = new HashMap<>();
}
  1. 添加元素,使用HashMap的put方法
    因为HashMap一个节点存放的是键值对,而HashSet是单列集合,所以值用PRESENT代替(实际上是一个空对象)。
public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}
  1. 执行 put()
    该方法会执行 hash(key) 得到 key 对应的 hash 值 算法 h = key.hashCode()) ^ (h >>> 16)
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
  1. 执行putVal
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node[] tab; Node p; int n, i;
    //table 就是 HashMap 的一个数组,类型是 Node[]

    //if 语句表示如果当前 table 是 null, 或者 大小=0
    //就是第一次扩容到 16 个空间.

    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //(1)根据 key(key是添加的这个对象),得到 hash 去计算 key 应该存放到 table 表哪个个索引位置。
    //并把索引位置赋给i,这个位置存放的对象引用赋给 p
    //(2)判断 p 是否为 null
    //(2.1) 如果 p 为 null, 表示这个位置还没有存放元素, 就创建一个 Node 放在该位置
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node e; K k;

        //如果当前索引位置对应的链表的第一个元素和准备添加的 key 的 hash 值一样
        //并且满足 下面两个条件之一:
        //(1) 准备加入的 key 和 p 指向的 Node 结点的 key 是同一个对象
        //(2) p 指向的 Node 结点的 key 的 equals() 和准备加入的 key 比较后相同
        //就不能加入
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
            e = p;

        //再判断 p 是不是一颗红黑树, //如果是一颗红黑树,就调用 putTreeVal , 来进行添加
        else if (p instanceof TreeNode)
            e = ((TreeNode)p).putTreeVal(this, tab, hash, key, value);
        else {
            //使用for循环遍历链表
            //(1) 依次和该链表的每一个元素比较后,都不相同, 则加入到该链表的最后。
            //把元素添加到链表后,立即判断 该链表是否已经达到 8 个结点,如果是就调用 treeifyBin() 对当前这个链表进行树化(转成红黑树)
            // 在treeifyBin()中进行如下判断是否对链表树化
            // if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY(64))
            // 如果上面条件成立,先 table 扩容. // 只有上面条件不成立时,才进行转成红黑树
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) {
                    //if语句内部让e指向p的下一个
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD -1)
                        treeifyBin(tab, hash);
                    break;
                }
                //(2) 依次和该链表的每一个元素比较过程中,如果有相同情况,就直接 break
                if (e.hash == hash &&
                ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;//因为e指向p的下一个,这里p指向e实际上实现了对链表的遍历
            }
        }
        //这里e实际上指向了在哈希表中与我们要添加的对象相等的那个对象。
        //if语句块的内容是向HashMap加入同一个键,不同值的对象时,值被替换的原理。
        //但是HashSet的value都是空对象,对HashSet不起作用。
        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++
    //threshold是临界值,大小为当前容量的0.75倍
    if (++size > threshold)
        resize();//扩容
    afterNodeInsertion(evict);
    return null;
}

HashSet扩容机制

  • 使用无参构造器时,初始化为空。第一次添加元素时,table扩容为16,临界值threshold = 16 * 加载因子(0.75) = 12。
  • 到了临界值后会继续扩容,容量扩充为原来两倍,注意这里达到临近不是指数组中有12个被占据了,而是HashSet添加了12个元素。
  • Java8中,如果一个链表的元素达到了8并且table的大小超过了64就会进行树化,否则对数组进行扩容。

LinkedHashSet底层维护了一个hash表和一个双向链表。向LinkedHashSet添加元素时,虽然数据不再同一个数组的索引位置上,但是双向链表把所有的数据串联了起来,因此 实现了存取顺序是一样的

Map接口

  • Map 与 Collection 并列存在。用于保存具有映射关系的数据:Key-Value(双列元素)
  • Map 中的 key 和 value 可以是任何引用类型的数据,会封装到 HashMap$Node 对象中
  • Map 中的 key 不允许重复,可以为 null, value 可以重复,也可以为 null。key 为 null,只能有一个, value 为 null ,可以多个
  • key 和 value 之间存在单向一对一关系,即通过指定的 key 总能找到对应的 value

Map接口方法

方法 说明 put 向Map添加元素 remove 根据键删除映射关系 get 根据键获得值 size 返回Map元素个数 isempty Map是否为空 containsKey 是否存在某个键 containsValue 是否存在某个值

HashMap

  • HashMap底层结构在前面HashSet中已经有详细的说明,底层是数据+链表+红黑树。
  • HashMap添加相同的Key,Value会被替换,在前面的源码分析提到过。
  • HashMap没有实现同步,线程不安全。

HashMap遍历机制-Entry

为了方便对Map的遍历,Map接口内部定义了Entry接口,Map接口也定义了entrySet()方法。

Java集合详解

Java集合详解

以HashMap为例 内部类Node实现了Map.Entry接口,实现了getKey(),getValue()等方法

Java集合详解

实现了entrySet()方法,返回的是一个EntrySet对象,EntrySet是HashMap的一个内部类,在这个EntrySet类中,定义了iterator()方法,返回的是一个EntryIterator对象。

Java集合详解

而EntryIterator也是HashMap定义的一个内部类,EntryIterator定义了next()方法,通过这个我们得以获得下一个对象。

Java集合详解

再深一步挖掘,这个next方法返回的是一个nextNode(),而这个nextNode()方法也正是在EntryIterator的父类HashEntryIterator中定义的,除此之外还定义了hasNext()方法,到此为止,迭代器的两个核心方法hasNext()和next()终于显出原型了。

Java集合详解

我们看到nextNode()返回的是一个Node对象,而这个Node对象实现了Entry接口的getKey(),getValue()以及toString()方法,这两个方法让我们能够访问到键值对,因此就实现了遍历Map的所有键值对。

如果只需要遍历键或者值可以使用Values()和KeySet()方法,它们遍历的底层逻辑和EntrySet是一样的。

进一步思考:EntrySet对象

再回过头来思考,entrySet对象到底是什么?它实际上是一个空对象,因为在类内部根本没有定义任何字段,也没有构造器,按照本人的理解,正如这个对象名,很形象的告诉了我们它只是一个入口,通过这个这个入口我们得以访问到Map里所有的键值对。这个设计非常的精妙,没有任何多余空间的开销我们就实现了堆Map的遍历。

最后一个问题,使用IDEA在Debug的时候发现这个entrySet存放了对象,这与我们之前认为他是空有冲突,这是什么原因呢?

这是因为IDEA在Debug是会调用toString()方法对象来显示对象的具体信息,它是一个视图,即对本身的一个映射,内部并没有存放数据

遍历所有键

//得到键,还可以用get方法得到值
Set keyset = map.keySet();

//(1) 增强 for
for (Object key : keyset) {
    System.out.println(key + "-" + map.get(key));
}

//(2) 迭代器
Iterator iterator = keyset.iterator();
while (iterator.hasNext()) {
    Object key = iterator.next();
    System.out.println(key + "-" + map.get(key));
}

遍历所有值

//获得所有值
Set values = map.values();

//(1) 增强 for
for (Object value : values) {
    System.out.println(value);
}

//(2) 迭代器
Iterator iterator2 = values.iterator();
while (iterator2.hasNext()) {
    Object value = iterator2.next();
    System.out.println(value);
}

遍历所有键值对

Set entrySet = map.entrySet();
//(1) 增强 for
for (Object entry : entrySet) {
    //将 entry 转成 Map.Entry,这样才能调用getKey()方法
    Map.Entry m = (Map.Entry) entry;
    System.out.println(m.getKey() + "-" + m.getValue());
}

//(2) 迭代器
Iterator iterator3 = entrySet.iterator();
while (iterator3.hasNext()) {
    Object entry = iterator3.next();
    Map.Entry m = (Map.Entry) entry;
    System.out.println(m.getKey() + "-" + m.getValue());
}

HashMap扩容机制

扩容机制在HashSet已经分析总结过。

  • LinkedHashMap是HashMap的子类,实现了存取顺序是一致的,如果业务上需求存取是一致的,可以使用LinkedHashMap。

HashTable

  • HashTable不能存放空对象,否则会抛出空指针异常
  • HashTable实现了线程同步,是线程安全的
  • HashTable的底层是一个Entry数组,Entry是HashTable的内部类,是一个链表的结构,这个Entry类实现了Map.Entry接口。

Hashtable遍历机制

遍历机制与和HashTable基本类似,也是通过entrySet方法返回EntrySet对象,调用EntrySet的迭代器方法进行遍历

Hashtable扩容机制

  • 使用无参构造器时,初始化容量为11.加载因子为0.75。
  • 当达到扩容的边界条件时,容量扩充为原来的2被 + 1。
  • Hashtable不会进行树化。

Properties

Properties继承了HashTable,主要用于从.properties配置文件中读取数据到Properties对象中,这个在IO中再给出具体的使用说明。

树集

树集比较特殊,包括TreeSet(Set的实现类)和TreeMap(Map的实现类),其中TreeSet底层使用和TreeMap来实现,树集保证了整个集合是按照自然规则或者自定义规则排好序的,其中TreeMap按照键来排序,每添加一个元素,它都能帮你在集合中插入到正确的位置上。

树集的使用注意事项

  • 用构造器创建一个对象时,如果没有传入一个实例化的Comparator接口,将会使用默认方法排序,并且必须要求对象实现Comparable接口,否则会抛出异常。
  • 当指定了排序规则后,TreeSet和TreeMap对重复元素的判定是按照排序规则,两者若应该在同一个位置,那么这个元素插入不成功。
public class CollectionTest {
    public static void main(String[] args) {
        TreeSet set = new TreeSet((s1,s2)
                ->{return s1.length()-s2.length();});
        set.add("meiguolao");
        set.add("taihaizhanzheng");
        set.add("dazhonghua");
        set.add("xiaoriben");
        System.out.println(set);
    }
}

输出结果:

[meiguolao, dazhonghua, taihaizhanzheng]

如上,”xiaoriben” 没有被插入set集合中,因外它和”meiguolao”字符串长度是一样的。

Collections

Collections是Java提供的一个操作list,set和map集合的工具类,在Collections中提供了一系列的静态方法用来对集合进行排序,修改和查看。
方法太多了,直接用IDEA的Structure查看吧,写累了,啊🥱🥱🥱🥱🥱🥱

Original: https://www.cnblogs.com/yjh1024/p/16576431.html
Author: Nights_Watch
Title: Java集合详解

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/620440/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • MySQL45讲之InnoDB加锁规则

    前言 本文介绍 MySQL InnoDB 的加锁规则,以及一些需要注意的点。 总结 可重复读隔离级别下,两个原则,两个优化,一个 bug: 原则1:加锁的基本单位是 next-ke…

    数据库 2023年5月24日
    0106
  • Lambda表达式

    1.常见单方法接口 Comparator Runnable Callable @FunctionalInterface 只定义了单方法的接口称之为 FunctionalInterf…

    数据库 2023年6月16日
    0119
  • Filter 过滤器

    什么是Filter过滤器? 1、Filter 过滤器它是 JavaWeb 的三大组件之一。三大组件分别是:Servlet 程序、Listener 监听器、Filter 过滤器2、F…

    数据库 2023年6月11日
    0108
  • 报错One record is expected, but the query result is multiple records

    总结:出现这种情况,显而易见,就是查询的数据在数据库中不止一条,而我调用的selectOne方法,返回值是一个User对象,导致报错 点击查看错误代码 LambdaQueryWra…

    数据库 2023年6月11日
    0948
  • Javaweb-JSP详解

    一、什么是JSP Java Server Pages:Java服务器端页面,和Servlet一样,用于动态web技术 最大的特点: 写JSP就像在写HTML 区别: HTML只给用…

    数据库 2023年6月16日
    0121
  • 设计模式之(2)——工厂方法模式

    定义:工厂方法模式又称为工厂模式、多态工厂模式和虚拟构造器模式,通过定义工厂父类来定义创建对象的接口,而子类负责生成具体的对象; 主要作用:将类的实例化延迟到工厂类的子类中进行,由…

    数据库 2023年6月14日
    0129
  • MYSQL性能优化以及建议

    1、业务代码组合逻辑后进行数据库操作,如根据波次进行库存分配,可以将波次里面的订单所有明细进行分组,然后匹配库存。2、将大字段、不常用字段放置到扩展表中,将经常使用(状态、数量、编…

    数据库 2023年5月24日
    099
  • Java面向对象程序设计(2)封装,继承和多态

    面向对象的三大特征是:封装,继承和多态 访问修饰符 java 提供四种访问控制修饰符号,用于控制方法和属性(成员变量)的访问权限(范围) : 访问级别 访问控制修饰符 同类 同包 …

    数据库 2023年6月16日
    099
  • 做自动化测试选择Python还是Java?

    你好,我是测试蔡坨坨。 今天,我们来聊一聊测试人员想要进阶,想要做自动化测试,甚至测试开发,如何选择编程语言。 自动化测试,这几年行业内的热词,也是测试人员进阶的必备技能,更是软件…

    数据库 2023年6月11日
    0130
  • 第16章 变量、流程控制与游标

    第16章 变量、流程控制与游标 1. 变量 在MySQL数据库的存储过程和函数中,可以使用变量来存储查询或计算的中间结果数据,或者输出最终的结果数据。 在 MySQL 数据库中,变…

    数据库 2023年6月6日
    0117
  • Sonarqube安装(Docker)

    一,拉取相关镜像并运行 拉取sonarqube镜像 docker pull sonarqube:9.1.0-community 在运行之前要提前安装postgres并允许,新建数据…

    数据库 2023年6月11日
    0122
  • 运维DBA要不要学python

    我个人认为是:要 现在python在运维数据库的工作中主要用在 1、编写一些运维脚本 2、编写运维管理平台 3、研究互联网大厂的运维脚本/工具并应有 特别是运维开源数据库的时候,第…

    数据库 2023年6月9日
    0110
  • knn算法详解

    1.什么是knn算法 俗话说:物以类聚,人以群分。看一个人什么样,看他身边的朋友什么样就知道了(这里并没歧视谁,只是大概率是这样) 对于判断下图绿色的球是哪种数据类型的方法就是根据…

    数据库 2023年6月16日
    0123
  • Asp.Net Core 发布和部署( MacOS + Linux + Nginx )

    在上篇文章中,主要介绍了 Dotnet Core Run 命令,这篇文章主要是讲解如何在Linux中,对 Asp.Net Core 的程序进行发布和部署。 有关如何在 Jexus …

    数据库 2023年6月11日
    0137
  • Vue插件

    Vue插件 插件作用 插件通常用来为 Vue 添加全局功能 例如:1、添加全局资源:指令/过滤器/过渡等。如 vue-touch2、通过全局混入来添加一些组件选项。3、添加 Vue…

    数据库 2023年6月11日
    098
  • 数据库读写分离

    ———-数据库读写分离———- 环境准备:(两台虚拟机(centos7)可以连接外网 步骤1: 安装数据库,…

    数据库 2023年6月16日
    0155
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球