一篇彻底搞懂Java中的集合框架!

0 阅读26分钟

认识集合

集合是一种容器,用来装数据的,类似于数组,但集合的大小可变,在开发中十分的常用。

第一部分 Collection单列集合

每个元素只包含一个值。Collection是代表(祖宗);

双列集合的代表是Map。

1.1 集合体系结构

①演示ArrayList集合(注意这里是多态方法写的 List是接口,正常写也行~)

输出:

证明了 ta有序、可重复、有索引。

②演示HashSet集合(同样是多态形式写的 Set是接口)

输出:

证明ta 无序、不可重复、无索引(get都报错)。

1.2 Collection的常用功能

为啥要先了解Collection的常用方法,是因为他是所有子类的父类,它所规定的所有方法、功能所有单列集合的子类都会被继承的。

1.3 Collection三种遍历方式

1.3.1 迭代器遍历

迭代器是用来遍历集合的专用方式(数组没有迭代器),在java中迭代器的代表是Iterator

直接通过代码来理解:

迭代器it一开始是站在第一个位置(后面看源码细讲),it.hasNext()判断当前it指的位置有没有数据,有就返回true;it.next()是获取当前it指向的内容并使it移到下一个元素上 (后面源码也有讲为),下面是大致流程:

到这儿it指向空了,it.hasNext( )就返回false,结束while循环。


迭代器it一开始是站在第一个位置(下标为0),我们看iterator的源码:

Cursor (游标): 表示下一个要返回的元素的位置。初始值通常是 0,表示从集合的第一个元素开始遍历。


我们再来看看刚刚用来判断while循环结束条件的it.hasNext() 源码:

这段代码是 hasNext() 方法的一个实现示例。在这个实现中,hasNext() 方法用于判断迭代器是否还有更多的元素可以遍历。具体来说:

cursor: 这个变量代表迭代器当前指向的位置,也就是下一个要返回的元素的位置。

size: 这个变量代表整个集合的大小,即元素的数量。

代码逻辑解释:

返回条件:

如果 cursor 不等于 size,则表示还有未遍历的元素。

如果 cursor 等于 size,则表示所有元素都已经遍历完毕。

工作原理:

当初始化迭代器时,cursor 通常被设置为 0,表示从集合的第一个元素开始遍历。

每次调用 next() 方法时,cursor 会递增,指向下一个元素。

cursor 达到 size 时,表示已经到达了集合的末尾,没有更多的元素可以遍历。


我们再来看看刚刚上面的it.next()的源码:

这就是为什么it.next()是每次取完一个数出来,再往后移一位了。

小结

注意不要下面这种,循环条件这儿查一次,里面取两次,这样会出问题的!

查一次,取一次!it.next( )不能乱用。

1.3.2 增强for循环

格式:for(元素的数据类型 变量名 : 数组或者集合){
}

· 增强for可以用来遍历集合或数组 (前一个迭代器方法只能是集合记得哦)

比如这样:

本质其实还是用的迭代器的方式遍历,只是简化了

1.3.3 Lambda表达式

需要使用Collection的如下方法来完成:

我们直接从源码开始讲起,最后再介绍简化的写法。

当我们去调用forEach方法时,它是需要我们给它提供一个Consumer对象(它是匿名内部类对象),我们看forEach的源码:

这个Consumer对象是一个接口,然而接口不能直接有对象,所以这里我们给他一个匿名内部类对象。(源码)


里面还有很多的逻辑,比如:为啥这里泛型是

这里的E是一开始送给集合的类型:

说明这个?必须是字符串或者字符串的父类(才能接字符串):

所以这个?是代表这边泛型的类型:

但这个不用过多重视,因为一般Idea直接会提示出来:

这个对象就可以帮我们遍历。


可为啥这个对象可以帮我们遍历?

我们调用forEach方法,它会把左边的整个匿名内部类对象 交给到 action:

然后先判断action是不是null,如果是null,是不能往下操作的,会报空指针异常。

瞅瞅源码:(如果是null会层层往外抛异常)

这段 Java 代码定义了一个 forEach 方法,用于遍历集合中的元素,并对每个元素执行一个给定的操作。下面是逐行的解释:

@Override: 表示此方法覆盖了父类中的同名方法。

public void forEach(Consumer<? super E> action): 定义了一个名为 forEach 的公共方法,它接受一个 Consumer 类型的参数 action,这个参数代表一个消费型操作,即接收一个输入并返回无结果的操作。这里的 ? super E 表示可以接受任何 E 及其超类型的实例。

Objects.requireNonNull(action);: 确保传入的 Consumer 对象不为 null,如果为 null 则抛出 NullPointerException。

final int expectedModCount = modCount;: 保存当前的 modCount 值作为期望的修改计数。modCount 是一个内部字段,通常用于检测集合的结构修改。

final Object[] es = elementData;: 将集合的元素数据复制到局部数组 es 中。

final int size = this.size;: 保存集合的大小到局部变量 size。

for (int i = 0; modCount == expectedModCount && i < size; i++): 使用 for 循环遍历数组中的所有元素。循环条件包括检查 modCount 是否与开始时相同以及索引 i 是否小于集合的大小。

action.accept(elementAt(es, i));: 对每个元素调用 Consumer 的 accept 方法。

if (modCount != expectedModCount): 如果在遍历过程中 modCount 发生了变化,则表示集合被其他线程或进程修改过。

throw new ConcurrentModificationException();: 抛出 ConcurrentModificationException 异常,表明集合在迭代过程中被非法修改。

整体上,这个方法实现了一种安全的遍历机制,确保在遍历过程中不会发生并发修改错误。如果集合在遍历期间被修改,会抛出异常来通知调用者。这种方法常见于 Java 集合框架中的实现。

上面都是属于拓展了解的部分,其实你只用知道它能遍历就完了~接着就来简化

这是一个典型的函数式接口的标记,我们知道函数式接口的匿名内部类可以简化成Lambda表达式:一行代码一个参数,这样就简便多啦~

其实还可以再简化haha(方法引用)

因为他是在调system里面的这个out对象,out它是一个常量 记住了一个打印对象,前后参数又一样,所以就可以用方法引用去简化

1.4 三种遍历

1.4.1 认识并发修改异常

在学习区别之前,我们先要认识并发修改异常问题

· 遍历集合的同时又存在增删集合元素的行为时可能出现业务异常,这种现象被称之为并发修改异常问题。

这里我们举一个实际的案例:

关键之处:遍历的同时在删除数据!!!(删除全部枸杞)

我们看结果会发现,并没有删除干净!(第一行是原始数据,第二行是删后)

我们就要搞清楚为什么会出现这个问题!

上面是核心代码部分,下面我们来手动模拟 遍历集合中并删除的整个过程:

① i 在Java入门,不删,i++;

② i 在 宁夏枸杞,要删(进入if)后面的所有都要往前挪位!!(问题所在

但是 i 还要++,因为本次循环结束了,这就直接跳过黑枸杞了!!

③ i 在人字拖,不删,i++;

④ i 在特级枸杞,要删(进入if)后面的所有都要往前挪位(又是问题所在

并且i++,直接跳过了枸杞子!!

⑤ i 在西洋参,不删,i++,出界 循环结束~

结果就是这样:


所以问题本质就是,要漏删!

前提是支持索引的情况下:

解决方案1:

搞清楚了整个过程解决方案也很简单,就是在if中加一个 i-- 即可;

让它每次删完一个数据后退回一下,去再判断 (前提是支持索引)

解决方案2:

倒着遍历并删除 (前提是支持索引)

前提是不支持索引的情况下:

比如说set的子类集合(TreeSet\HashSet\LinkedSet),就是不支持索引。

所以我们就可以用到迭代器iterator;

但如果用上面这个代码,是要报 ConcurrentModificationException异常,导致<迭代器的内部状态与集合的状态不一致>

迭代器的内部状态与集合的状态一致性:

迭代器(Iterator)在遍历集合时,会维护一个内部状态,这个状态包括当前的位置和其他一些元数据。当集合发生修改时,迭代器需要保持其内部状态与集合的状态一致。如果迭代器的内部状态与集合的状态不一致,就会抛出 ConcurrentModificationException。

我们可以看迭代器的next()源码。

next中每次都会先检查是否修改(一个方法):

如果修改值不等于期望修改值,就会抛出异常,比如一开始expectedModCount=0,但循环中上一轮做出了一次修改(集合.remove),modCount就变为了1,下一轮就会报异常,所以迭代器天然就知道有这种bug存在,他只要知道你删数据 list.remove(),他一定会改modCount值++,看集合remove的源码:

所以我们应该用迭代器iterator中的remove,而不是集合ArrayList中的remove。

解决方案2: 用迭代器iterator中的remove

我们看迭代器iterator中的remove:

每次删完数据都会做期待值和默认值的一个同步,所以下一轮的it.next( )就不会再报错。

为什么使用迭代器的 remove() 方法可以避免 ConcurrentModificationException:

  1. 迭代器的 remove() 方法:

迭代器的 remove() 方法不仅删除了当前元素,还会更新迭代器的内部状态,使其与集合的状态保持一致。

这意味着迭代器会更新其内部的修改计数器,确保其状态与集合的状态一致。

  1. 直接调用集合的 remove() 方法:

直接调用集合的 remove() 方法会导致集合的状态发生变化,但迭代器的内部状态不会同步更新。

这种情况下,迭代器的内部状态与集合的状态不一致,从而引发 ConcurrentModificationException。

于是就改为这样就好啦!

但我在想if前面有个it.next();这个不是每次取出来当前元素,然后就会向后移动一位,可是后面if中it.remove岂不是删的不是上一步时的元素吗?

让我们详细看一下迭代器的工作原理以及为什么使用 it.remove() 是安全的。

迭代器的工作原理

迭代器(Iterator)在遍历集合时,会维护一个内部指针,指向当前正在处理的元素。当你调用 it.next() 时,迭代器会返回当前元素并将其内部指针向前移动一位。

使用 it.remove() 的安全性

当你在迭代过程中使用 it.remove() 时,迭代器会做以下几件事情:

删除当前元素:将当前元素从集合中删除。

更新内部状态:更新迭代器的内部状态,使其与集合的状态保持一致。

调整指针位置:如果删除了一个元素,迭代器会将内部指针回退一位,以便下次调用 it.next() 时能够正确地获取下一个元素。


而增强for和lambda表达式遍历都没法解决并发修改异常的问题

之前说过增强for的底层就是基于迭代器实现的,所以遍历并删除一定会出现并发修改异常,但你用增强for 你又拿不到这个迭代器,因为他的迭代器是隐藏的,不能使用迭代器自身的方法去remove

这是lambda表达式,也不可以。

1.4.2 三种遍历的区别

根据上面所诉的可以总结,当某个集合有索引,并要在遍历时删除数据,就可以用普通的for循环 在循环中删除数据后记得退一步i-- 或者倒着遍历也可以;如果这个集合没有索引,并要在遍历时删除数据,就用迭代器(注意用迭代器中的remove方法)

但是lambda表达式和增强for就只能用于遍历,不能再遍历的同时删除数据

第二部分 List集合

第一部分的时候我们了解过List的集合都是满足:有序、可重复、有索引。

但我们就要来研究一下List实现类 ArrayList<>集合与LinkedList<>集合的底层实现有什么不同、适合的场景有什么不同!

我们先来看看

2.1 List集合的特点和特有方法

· List集合因为支持索引,所以多了很多与索引相关的方法,当然,collection的功能List也都继承了。

支持的遍历方式:

这两个集合的功能都是一样的,那java咋会弄两个重复的东西出来,当然不可能嘛,是因为他们两个的底层原理不一样,采用的数据结构不一样,应用场景不一样(也即是存储、组织数据方式不同)

2.2 ArrayList底层原理

· ArrayList底层是基于数组存储数据的。

解释1: 如果下标是从1开始,效率会比从0开始低,是因为数组默认起始位置是首个元素,比如要查询arr[5],就直接arr+5就可以找到,但如果从1开始,计算机就会多一步 (6-1)的操作,所以效率就会低一些。

解释2: 增删数据的效率低,是因为,比如集合本身就只有那么大,但是你新添加数据进来,就只有让ArrayList集合进行扩容。再来说删除数据时,比如把上面的C给删除了,后面所有的元素都要往前移位,也比较费时间腻。

我们来看看ArrayList集合的源码:

可以看出它就是基于一个Object数组(elementData)进行存储数据的,

默认长度是为10.

size用来计算集合中的元素个数默认是0:

但要注意它并不是一开始new这个ArrayList对象的时候它就会分配一个长度为10的数组,而是在第一次添加数据进去的时候才会分配。

不信我们可以看add方法的底层,是不是进行了扩容:

这里的e就是添加的“张三”,elementData是刚开始的空数组{},size是元素个数。上面的modCount可以不管它,它就是去修改,集合默认可以修改的次数。我们在进去看这里的add方法:

s起初是0,elementData的长度也是0,所以就会grow()进行扩容:

这里还要再调用另一个真正帮我们进行扩容的grow(),这里的传参是1(size+1).再点进去才是真正的grow

oldCapacity旧容量是当前数组的长度为0,所以不满足if两个条件,直接进入else,new了一个Object数组,给到存数据的elementData数组,这个Object数组的大小是10和传参minCapacity中的较大值,所以第一次扩容大小就是10.

当是个空间的数组满了之后,要添加第11个数时,就要进行扩容,此时e是将要添加的元素,size=10,并且elementData是满的

s=10,ele.length=10,所以会grow();

可以得出minCapacity=11:

就会进入第一次跳过的这个if里面,newCapacity就是新容量,调用了一个工具类ArraysSupport中的newLength方法,第一个参数是老容量oldCapacity;第二个参数是minGrowth即是最小生长长度,11-10=1;第三个参数是优先增长长度,旧容量进行位运算相当于除以2,oldCapacity=10,preferred growth=5。所以newCapacity就是15。

可以得出每次扩容的大小就是原来的1.5倍!!!

其实还有很多类似的算法,比如remove中,会有移位的操作:

就不再详细拆解了。

2.3 LinkedList底层原理

· LinkedList底层是基于链表存储数据的。

而LinkedList是基于双向链表实现的:

提高了查询的速率,但是有缺点,一个结点得存放两个地址一个值。

并且有两个指针(头、尾),所以:

由于这个特点,所以LinkedList中新增了很多首尾操作的特有方法


根据LinkedList特点我们可以用来设计队列。(非常合适)

队列只在首尾增删元素!


也可以根据LinkedList来设计一个

只在首部增删(push/pop)元素,用LinkedList来实现很合适!

还可以用push,但其实就是包装了一下addFirst:

pop也是如此:

我们来看看LinkedList的底层源码是不是用链表实现的:

并且还是双链表,一个结点中存了本身的元素、下一个元素的地址、上一个元素的地址


总结:

所以当查询比较多,增删比较少时,就可以用ArrayList。(不是很占内存)

当增删比较多,查询比较少的时候,就用LinkedList更合适。

第三部分 Set集合

3.1 三种集合 初次见面(特点、功能)

HashSet是用的最多的Set集合,上面代码可以看出来它无序、不重复的特点

还有就是不支持索引(可以试着写下set.get(1)是没有的),至于为什么不支持索引,等后面讲到底层原理就知道啦

再来看看LinkedSet

会发现它是有序、不重复、无索引。

再来看看TreeSet

它会按大小帮你排好序,不重复、无索引

其实到这里Set的功能就已经学完啦,但我们还是继续拆解这三个Set实现类的底层原理!~

3.2 HashSet集合的底层原理

对于它的底层原理,我们要带着它的三个特点去学习(为什么往HashSet集合中存储的数据是无序、不重复、无索引)。

我们先要知道一个概念叫 hash值:

又因为HashSet是基于哈希表存储数据的。哈希表是一种增删改查数据性能都较好的数据结构。

JDK8之前的哈希表:是数组+链表

跟ArrayList一样,在第一次添加数据时,会自动创建一个长度为16的数组table,默认加载因子为0.75 (回忆:ArrayList是长度为10)

当要存入数据时,会使用元素的哈希值对数组的长度做运算计算出应存入的位置(就类似于求余运算 但实际不是哈,如:哈希值是16003%16=3)就去找到索引为3的位置,判断当前位置是否为null,如果是null直接存入;如果不为null,表示有元素,则调用equals方法比较

相等,则不存;不相等,则存入数组

● JDK 8之前,新元素存入数组,占老元素位置,老元素挂下面

● JDK 8开始之后,新元素直接挂在老元素下面

形成链表,这也就是为什么是无序的,后加的数据可能跑到前面去了。

但是有个问题,数据过多了之后,逐渐就会产生很多链表,链表查询速率又低,java很怕链表过长,所以提出了一个扩容机制,前面提到了一个加载因子0.75,16*0.75=12,也就是一旦数组长度超过了12,就扩容原来的一倍为32;但还是有可能会出现虽然扩容了 但某一个坑中堆了很多元素,还是存在链表过长,这样查询速率还是太低,所以在JDK8之后,新提出了一个概念:

进一步提高了检索性能,所以在JDK8之后 哈希表=数组+链表+红黑树(新加的数据比结点小就在左边,比结点大就在右边,一样的不存)

但我们用二叉查找树的时候还是少,因为如果一组数据已经是有序的,就会存在都在一边,又形成了链表存在这个不平衡的问题!!!

导致查询性能又与单链表一样了

所以就有了红黑树

● 红黑树,就是可以自平衡的二叉树

● 红黑树是一种增删改查数据性能相对都较好的结构。

要求:每条路径上的黑色结点数一样,就可以使其平衡,查找效率就会变高很多。

查询过程:先根据要查询元素的hash值对数组长度做运算 快速找到数组对应的索引,再进入链表或者红黑树中查询。 (虽然它本身还是比较占内存,但相比之下还是算很好的集合啦)


3.2.1 HashSet集合元素的去重操作

现在我们有一个需求:

可以发现HashSet并没有帮我们去重,这里的重是指的内容相同(即名字、年龄、地址、电话),不禁发问 Set不是本身就有去重的功能吗?

因为这里的几个学生对象都是单独new出来的,他们是不同的对象,他们的hash值、地址值都是不一样的,所以Set就默认是不同的元素,没有重复,那如何实现我们所想的内容全部相同就属于重复?

结论:如果希望Set集合认为2个内容一样的对象是重复的,必须重写对象的hashCode()和equals()方法,熟悉的Alt+insert 组合键

这样一来,只要是内容一样的对象就会被当作重复的,set就会自动去重

这样就没有重复的王五啦

3.3 LinkedHashSet集合的底层原理

我们根据前面知道它是有序、不重复、无索引。

而且它依旧是基于哈希表(数组、链表、红黑树实现的),但是他的每个元素都额外的多了一个双链表的机制记录它前后元素的位置。

一开始,头指针和尾指针都先指向第一个元素

当添加第二个数据时,第一个数据中会存储第二个数据的地址,第二个数据中会存放第一个数据的地址,尾指针并去指向第二个元素。当后面开始读的时候是从8这个首指针开始进入,而不是从头开始。

再添加元素时也是,第二个数据中会存储第三个数据的地址,第三个数据中会存放第二个数据的地址,尾指针并去指向第三个元素。最后查的时候就是按照指针指向顺序来查,这也就是为什么有序。

我们讲完了详细的过程,不妨再来看看LinkedHashSet的源码:

再往super里面走,这里的16就是他的容量,0.75就是他的加载因子;

怎么是LinkedHashMap也不管它,因为LinkedHashSet是基于这个Map的,后面会讲,再往里面走

这头尾指针就出来了不是嘿嘿,所以是双链表也得到了验证!

3.4 TreeSet集合

特点:不重复、无索引、可排序(默认升序排序、按照元素的大小,由小到大排序)

底层是基于红黑树实现的排序

注意:

对于数值类型: Integer, Double, 默认按照数值本身的大小进行升序排序。

对于字符串类型:默认按照首字符的编号升序排序。

对于自定义类型如 Student 对象:TreeSet 默认是无法直接排序的

会直接报错!因为它不知道大小规则不知道往排啊!

有两种方案:

1、对象类实现一个Comparable比较接口,重写compare方法,指定大小比较规则

// t2.compareTo(t1)

// t2 == this 比较者

// t1 == o 被比较者

// 规定1: 如果你认为左边大于右边 请返回正整数

// 规定2: 如果你认为左边小于右边 请返回负整数

// 规定3: 如果你认为左边等于右边 请返回0

然后我们再来排序看:

发现确实排上序了,但是有个重复的22王五只存了一个,是因为红黑树中如果存在就不存。解决也很简单,小技巧把compareTo里面改一下就好:

也即是当两者相等时也返回个1,不返回0;

其实一行代码就可以搞定:

2、public TreeSet (Comparator c) 集合自带比较器Comparator对象,指定比较规则

在括号中写比较器:

有比较器之后就是两两送进compare方法比较给到o1和o2

可以发现当集合自带规则、对象也有规则,那么就优先使用集合自带的规则,这里的降序就可以验证!

当然这里也可以用薪水来排序,但是如果直接写这种:

会报错,因为这个方法要求返回的是int类型的整数,而Salary是double类型的,有的uu又想到了用(int)强制转换,但是这样会有很多bug(如:9.9-9.5=0.4;0.4一转就变成了0,就认为是相等的 相等的就乱排,可能就会出现9.9在9.5之前了)。所以要么就老实写if判断

还有一种办法就是调包装类Double中的compare直接比较两个double类型的数,结果会返回整数

顺便看一眼compare的源码:

红框部分就是判断大于小于,紫框是判断等于(看着多只是它考虑的比较细)

没错还可以简化这个匿名内部类:用lambda表达式 一行就可以完成


到这里单列集合就算学完了,打个总结吧:

1、如果希望记住元素的添加顺序,需要存储重复的元素,又要频繁的根据索引查询数据?

• 用ArrayList集合(有序、可重复、有索引),底层基于数组的。(常用)

2、如果希望记住元素的添加顺序,且增删首尾数据的情况较多?

• 用LinkedList集合(有序、可重复、有索引),底层基于双链表实现的。

3.如果不在意元素顺序,也没有重复元素需要存储,只希望增删改查都快?

● 用HashSet集合(无序,不重复,无索引),底层基于哈希表实现的。(常用)

4.如果希望记住元素的添加顺序,也没有重复元素需要存储,且希望增删改查都快?

• 用LinkedHashSet集合(有序,不重复,无索引), 底层基于哈希表和双链表。

5.如果要对元素进行排序,也没有重复元素需要存储?且希望增删改查都快?

• 用TreeSet集合,基于红黑树实现。

其实开发中大多数就是ArrayList和HashSet用的多!

准备开始跟ArrayList相同地位的双列集合Map!!(超级大声)

第四部分 Map集合

4.1初识Map

Map是双列集合,与Collection集合不同,在Map中都是存的键值对,数据是一对一对的出现。

下面Map集合中有6个数据?不 下面集合中应该说有三个数据(成对出现)。

· Map集合也被叫做“键值对集合”, 格式: {key1=value1, key2=value2, key3=value3,...}

· Map集合的所有键是不允许重复的,但值可以重复,键和值是一一对应的,每一个键只能找到自己对应的值

Map的业务应用场景:

需要存储一一对应的数据时,就可以用Map

比如当在一个简易版的购物车中,就存在类似的情况,某个商品对应他的数量。

{商品1=2,商品2=5,商品3=7,商品4=3}

4.2 Map集合的体系及其特点

Map是接口,K和V是泛型,分别代表着键和值。然而接口不能实例化,所以有了很多关于Map的实现类,当然最常用的还是HashMap。

Map集合体系的特点

注意: Map系列集合的特点都是由键决定的,值只是一个附属品,值是不做要求的

• HashMap(由键决定特点):无序、不重复、无索引;(用的最多)

• LinkedHashMap(由键决定特点):由键决定的特点:有序、不重复、无索引。

• TreeMap (由键决定特点):按照大小默认升序排序、不重复、无索引。


4.3 初识HashMap

双列集合添加数据用put,单列用add;

根据运行结果可以看出HashMap的特点:无序、不重复、无索引

关于不重复是指如上添加了两次王五,前面的25会被后面的28所覆盖;

键和值都能是为null、无索引 但是有get方法,能通过去获取值;

而且对不做要求,可以重复。


4.4 初识LinkedHashMap

大致跟HashMap都差不多,只是LinkedHashMap变成了有序。

即是:有序、无索引、无重复

可以看粗输出顺序跟添加顺序是一样的,只是王五的25被28所覆盖 ----有序

4.5 初识TreeMap

特点:可排序、不重复、无索引

这个就不用太关注,实际用的少。

4.6 Map集合的常用方法

Map是双列集合的祖宗,它的功能是全部双列集合都可以继承过来使用的,所以我们要先来学习一下Map集合的常用方法:

很简单,直接在Idea中刷一遍就会了。

4.7 Map集合的遍历方式

有三种分别是:键找值、键值对、lambda;

4.7.1键找值

先获取Map集合全部的键,再通过遍历键去找值

需要用到下面两种方法

4.7.2 键值对

把“键值对”看成一个整体进行遍历(难度较大)

用增强for遍历会发现有问题!

所以就想把map整体打包放进一个Set集合中去(调用entrySet()方法

里面的元素类型都是键值对类型(Map.Entry<String,Integer>)

entrySet( )底层会遍历map中的每个数据,把每个键和值封装成一个entry对象,存入Set集合中

并且entry中有getKey()和getvalue()的两个方法。

4.7.3 lambda表达式

我们在任何集合中用lambda都会调用forEach方法,我们来看看源码:

下面被选中的蓝色区域就是一个匿名内部类上面forEach源码中的action,K代表String,V代表Integer,遍历完整个map后返回到accept

至于是lambda表达式那么肯定有简化写法:(就一行)

集合框架的知识点差不多到这儿就完结啦