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)

大家都在看

  • LeetCode 13. 罗马数字转整数

    罗马数字包含以下七种字符: I, V, X, L,C,D 和 M。 字符 数值 I 1 V 5 X 10 L 50 C 100 D 500 M 1000 例如,罗马数字2写做II,…

    数据库 2023年6月11日
    089
  • 【likeshop】回收租凭系统100%开源无加密 商城+回收+租赁

    likeshop回收租赁系统适用于物品回收、物品租赁、二手买卖交易等三大场景。 系统支持智能评估回收价格,后台调整最终回收价,用户同意回收后系统即刻放款,用户微信零钱提现。支持在线…

    数据库 2023年6月14日
    074
  • 手把手教你分析MySQL查询性能瓶颈,包教包会

    当一条SQL执行较慢,需要分析性能瓶颈,到底慢在哪? 我们一般会使用 Explain查看其执行计划,从执行计划中得知这条SQL有没有使用索引?使用了哪个索引? 但执行计划显示内容不…

    数据库 2023年5月24日
    073
  • windows和乌班图使用固定的ip地址

    windows设置固定的ip地址:查看网上的方法很多人说修改无线网卡的配置:自动获取ip—-》使用下面的IP地址这样修改以后无法使用wifi上外网但是确实可以添加一个固…

    数据库 2023年6月11日
    0120
  • 9 &和&&的区别

    &运算符有两种用法 在解释按位与&之前,我们先了解一个知识:程序中的所有数在计算机内存中都是以二进制的形式存储的,位运算就是直接对内存中整数的二进制位进行操作。 按…

    数据库 2023年6月6日
    0130
  • 对于Java循环中的For和For-each,哪个更快

    Which is Faster For Loop or For-each in Java 对于Java循环中的For和For-each,哪个更快 通过本文,您可以了解一些集合遍历技…

    数据库 2023年6月11日
    088
  • 学习笔记——Django项目中请求与响应(json数据)

    2022-10-04 测试json数据与Django项目与pycharm连接,在”postman”软件中。”postman”是一个接…

    数据库 2023年6月14日
    0145
  • MySQL8.0 DDL原子性特性

    1. DDL原子性概述 8.0之前并没有统一的数据字典dd,server层和引擎层各有一套元数据,sever层的元数据包括(.frm,.opt,.par,.trg等),用于存储表定…

    数据库 2023年6月9日
    061
  • mybatis-plus详解

    旧的代码生成 记得导包,依赖如下 com.baomidou mybatis-plus-boot-starter 3.5.1 com.baomidou mybatis-plus-ge…

    数据库 2023年6月14日
    092
  • Vue3新特性API

    一、vue3介绍 vue3.0是在2.0的基础上重大优化调整后的升级版本,其响应式原理已经在vue2框架基础中介绍过,此文章重点介绍Vue 3 中一些新功能API及其使用,文章内容…

    数据库 2023年6月14日
    089
  • 函数式编程-Stream流

    函数式编程-Stream流 1. 概述 1.1 为什么学? 能够看懂公司里的代码 大数量下处理集合效率高 代码可读性高 消灭嵌套地狱 //查询未成年作家的评分在70以上的书籍 由于…

    数据库 2023年6月6日
    081
  • MySQL优化之索引解析

    索引的本质 MySQL索引或者说其他关系型数据库的索引的本质就只有一句话, 以空间换时间。 索引的作用 索引关系型数据库为了 加速对表中行数据检索的( 磁盘存储的) 数据结构 索引…

    数据库 2023年5月24日
    090
  • 排查线上问题的9种方式

    德国科技管理专家斯坦门茨早年移居美国,他以非凡的才能成为美国企业界的佼佼者。一次,美国著名的福特公司的一组电机发生故障,在束手无策之时,公司请斯坦门茨出马解决问题。 斯坦门茨在电机…

    数据库 2023年6月6日
    082
  • The user specified as a definer(‘mysql.infochema’@”localhost’) does not exist

    最近将之前用的 mysql5.5 升级到了 mysql8.0,第一天还能正常使用,几天没用后,登录发现报错:The user specified as a definer (&#8…

    数据库 2023年6月6日
    082
  • Mysql8+数据库安装和使用

    一、Mysql的版本选择 Mysql目前分文社区版和企业版,社区版在技术方面会加入许多新的未经严格测试的特性,而企业版经过严格测试认证,更加稳定、安全、可靠,性能也比社区版好。社区…

    数据库 2023年6月14日
    080
  • Java基础四—泛型、注解、异常、反射

    泛型 泛型的本质是为了参数化类型( 在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类…

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