Java集合List、Set和Map史上最详细讲解

一、集合概念

集合是java中提供的一种容器,可以用来存储多个数据。

二、集合与数组的区别(集合和数组都是容器,它们有啥区别呢?)

  • 数组的长度是固定的;集合的长度是可变的
  • 数组中存储的是同一类型的元素,可以存储基本数据类型值。集合存储的都是对象;而且对象的类型可以不一致(比如python语言;但是java属于静态语言要除外);在开发中一般当对象多的时候,使用集合进行存储。

三、集合框架

  • 集合按照其存储结构可以分为两大类,分别是单列集合java.util.Collection和双列集合java.util.Map
  • Collection:单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是java.util.Listjava.util.Set。其中,List的特点是元素有序、元素可重复。Set的特点是元素无序,而且不可重复。List接口的主要实现类有java.util.ArrayListjava.util.LinkedListSet接口的主要实现类有java.util.HashSetjava.util.TreeSet
  • 集合本身是一个工具,它存放在java.util包中。在Collection接口定义着单列集合框架中最最共性的内容。

四、List接口介绍

4.1 ArrayList

优点: 底层数据结构是数组,查询快,增删慢。
缺点: 线程不安全,效率高

4.2 Vector

优点: 底层数据结构是数组,查询快,增删慢。
缺点: 线程安全,效率低

4.3 LinkedList

优点: 底层数据结构是链表(双向链表),查询慢,增删快。
缺点: 线程不安全,效率高

五、Set接口介绍

5.1 HashSet(底层实现是HashMap)

底层数据结构是哈希表。(无序,唯一)
如何来保证元素唯一性?

  • 依赖两个方法:hashCode()和equals()

5.2 LinkedHashSet

底层数据结构是链表和哈希表。(FIFO插入有序,唯一)

  • 由链表保证元素有序
  • 由哈希表保证元素唯一

5.3 TreeSet

底层数据结构是红黑树。(唯一,有序)
1.如何保证元素排序的呢?

  • 自然排序、比较器排序

2.如何保证元素唯一性的呢?

  • 根据比较的返回值是否是0来决定

5.4 什么是哈希表

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。
在这里插入图片描述
从上图中可以看出,哈希表实则是数组+链表的形式组成,数组指的上图中的01233456…,链表则指的是在这里插入图片描述

5.5 哈希表实现

  1. 在JDK1.8之前,哈希表底层采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
  2. 链表与红黑树之间相互转换
    2.1 当链表的长度 > 8,且entry数组的长度 < 64,此时会扩容(链表不转换为红黑树);
    2.2 只有当链表的长度 > 8,且entry数组的长度 > 64,才会将链表转为红黑树;
    2.3 在resize() 方法扩容的时候,在将原Node数组迁移到扩容后的新Node数组的时候,如果该Node 元素是一个红黑树,则对其进行拆分、然后才迁移到新的Node数组中,如果拆分之后的子树的数量小于等于6了,则将该子树转回链表结构。
  3. 简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。
    在这里插入图片描述

以下是哈希表存储流程图:
在这里插入图片描述

5.6 JDK8中HashMap中 put() 和 get() 方法实现

5.6.1 put()方法

1、计算 key 的 Hash 值
2、如果节点数组是空,则初始化节点数组
3、将 hash 值转换成数组的下标,下标位置上如果没有任何元素,就把 Node 添加到这个位置上
4、如果说下标对应的位置上有节点,并且该节点的 hash 值、key 与传入的相等,则进行覆盖;如果与传入的key不相等,则进行后续判断
5、如果说下标对应的位置上是树节点,则调用树节点处理方法
6、如果说下标对应的位置上有节点链表,则将新节点放入链表的末尾。
7、如果链表的长度超过阀值 8,则调用 treeifyBin 方法进行扩容或者树化:如果数组的长度小于 64 则进行 resize()扩容操作,如果数组的长度大于 64 则将链表转为红黑树

5.6.2 get()方法

1、先调用 key 的 hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标
2、通过数组下标快速定位到某个位置上,如果这个位置上什么都没有,则返回 null
3、检查第一个节点的 hash 与 k,如果相等则返回;否则进入后续判断
4、如果第一节点是树节点则按照 hash 和 key 查找树,如果相等则返回
5、如果第一节点是链表节点则遍历,比较 hash 与 key,如果相等则返回

六、针对Collection集合我们到底使用谁?(掌握)

唯一吗?

是:Set

排序吗?

是:TreeSet或LinkedHashSet
否:HashSet
如果你知道是Set,但是不知道是哪个Set,就用HashSet。

否:List

要安全吗?

是:Vector
否:ArrayList或者LinkedList

查询多:ArrayList
增删多:LinkedList
如果你知道是List,但是不知道是哪个List,就用ArrayList。

6.1 如果你知道是Collection集合,但是不知道使用谁,就用ArrayList。
6.2 如果你知道用集合,就用ArrayList。

七、Map接口介绍

  1. 现实生活中,我们常会看到这样的一种集合:IP地址与主机名,身份证号与个人,系统用户名与系统用户对象等,这种一一对应的关系,就叫做映射。Java提供了专门的集合类用来存放这种对象关系的对象,即 java.util.Map 接口。
  2. 我们通过查看 Map 接口描述,发现 Map 接口下的集合与 Collection 接口下的集合,它们存储数据的形式不同,如下图。
    在这里插入图片描述
  3. Collection 中的集合,元素是孤立存在的(理解为单身),向集合中存储元素采用一个个元素的方式存储。
  4. Map 中的集合,元素是成对存在的(理解为夫妻)。每个元素由键与值两部分组成,通过键可以找对所对应的值。
  5. Collection 中的集合称为单列集合, Map 中的集合称为双列集合。
  6. 需要注意的是, Map 中的集合不能包含重复的键,值可以重复;每个键只能对应一个值。

7.1 Map接口有三个比较重要的实现类(HashMap、TreeMap和HashTable)

7.1.1 接口表层的特点

  • TreeMap是有序的,HashMap和HashTable是无序的。
  • HashTable的方法是同步的,HashMap的方法不是同步的。这是两者最主要的区别。

7.1.2 接口源码层面的特点

  • HashTable是线程安全的,HashMap不是线程安全的。
  • HashMap效率较高,HashTable效率较低。如果对同步性或与遗留代码的兼容性没有任何要求,建议使用HashMap。 查看HashTable的源代码就可以发现,除构造函数外,HashTable的所有 public 方法声明中都有 synchronized关键字,而HashMap的源码中则没有。
  • Hashtable不允许null值,HashMap允许null值(key和value都允许)
  • 父类不同:HashTable的父类是Dictionary,HashMap的父类是AbstractMap

八、TreeSet、LinkedHashSet and HashSet的区别

8.1 介绍

  • TreeSet的主要功能用于排序
  • LinkedHashSet的主要功能用于保证FIFO即有序的集合(先进先出)
  • HashSet只是通用的存储数据的集合

8.1 相同点

  • Duplicates elements: 因为三者都实现Set interface,所以三者都不包含duplicate elements
  • Thread safety: 三者都不是线程安全的,如果要使用线程安全可以Collections.synchronizedSet()

8.3 不同点

  • Performance and Speed: HashSet插入数据最快,其次LinkHashSet,最慢的是TreeSet因为内部实现排序
  • Ordering: HashSet不保证有序,LinkHashSet保证FIFO即按插入顺序排序,TreeSet安装内部实现排序,也可以自定义排序规则
  • null:HashSet和LinkHashSet允许存在null数据,但是TreeSet中插入null数据时会报NullPointerException

九、ArrayList、Vector和HashSet扩容问题

9.1 ArrayList

  1. ArrayList 默认初始容量为10
  2. 线程不安全,查询速度快
  3. 底层数据结构是数组结构
  4. 扩容时机:当元素个数 超过 容量长度时,进行扩容
  5. 扩容增量:原容量的 0.5倍
  6. 如 ArrayList的容量为10,一次扩容后是容量为15

9.2 Vector

  1. Vector:线程安全,但速度慢
  2. 底层数据结构是数组结构
  3. 加载因子为1:即当 元素个数 超过 容量长度 时,进行扩容
  4. 扩容增量:原容量的 1倍
  5. 如 Vector的容量为10,一次扩容后是容量为20

9.1 HashSet

  1. HashSet:线程不安全,存取速度快
  2. 底层实现是一个HashMap(保存数据),实现Set接口
  3. 默认初始容量为16(为何是16,见上文对HashMap的描述)
  4. 加载因子为0.75:即当元素个数超过容量长度的0.75倍 时,进行扩容
  5. 扩容增量:原容量的 1 倍
  6. 如 HashSet的容量为16,一次扩容后是容量为32

十、案例

按照斗地主的规则,完成洗牌发牌的动作
具体规则:

  1. 组装54张扑克牌将
  2. 54张牌顺序打乱
  3. 三个玩家参与游戏,三人交替摸牌,每人17张牌,最后三张留作底牌。
  4. 查看三人各自手中的牌(按照牌的大小排序)、底牌
    规则:手中扑克牌从大到小的摆放顺序:大王,小王,2,A,K,Q,J,10,9,8,7,6,5,4,3

案例分析:

  1. 准备牌:
    完成数字与纸牌的映射关系:
    使用双列Map(HashMap)集合,完成一个数字与字符串纸牌的对应关系(相当于一个字典)。
  2. 洗牌:
    通过数字完成洗牌发牌
  3. 发牌:
    将每个人以及底牌设计为ArrayList,将最后3张牌直接存放于底牌,剩余牌通过对3取模依次发牌。
    存放的过程中要求数字大小与斗地主规则的大小对应。
    将代表不同纸牌的数字分配给不同的玩家与底牌。
  4. 看牌:
    通过Map集合找到对应字符展示。
    通过查询纸牌与数字的对应关系,由数字转成纸牌字符串再进行展示。
public class Poker {
	public static void main(String[] args) {
		/*
		* 1组装54张扑克牌
		*/
		// 1.1 创建Map集合存储
		HashMap<Integer, String> pokerMap = new HashMap<Integer, String>();
		// 1.2 创建 花色集合 与 数字集合
		ArrayList<String> colors = new ArrayList<String>();
		ArrayList<String> numbers = new ArrayList<String>();
		// 1.3 存储 花色 与数字
		Collections.addAll(colors, "♦", "♣", "♥", "♠");
		Collections.addAll(numbers, "2", "A", "K", "Q", "J", "10", "9", "8", "7", "6", "5", "4",
		"3");
		// 设置 存储编号变量
		int count = 1;
		pokerMap.put(count++, "大王");
		pokerMap.put(count++, "小王");
		// 1.4 创建牌 存储到map集合中
		for (String number : numbers) {
		    for (String color : colors) {
		       String card = color + number;
		       pokerMap.put(count++, card);
		    }
		}
		/*
		* 2 将54张牌顺序打乱
		*/
		// 取出编号 集合
		Set<Integer> numberSet = pokerMap.keySet();
		// 因为要将编号打乱顺序 所以 应该先进行转换到 list集合中
		ArrayList<Integer> numberList = new ArrayList<Integer>();
		numberList.addAll(numberSet);
		// 打乱顺序
		Collections.shuffle(numberList);
		// 3 完成三个玩家交替摸牌,每人17张牌,最后三张留作底牌
		// 3.1 发牌的编号
		// 创建三个玩家编号集合 和一个 底牌编号集合
		ArrayList<Integer> noP1 = new ArrayList<Integer>();
		ArrayList<Integer> noP2 = new ArrayList<Integer>();
		ArrayList<Integer> noP3 = new ArrayList<Integer>();
		ArrayList<Integer> dipaiNo = new ArrayList<Integer>();
		// 3.2发牌的编号
		for (int i = 0; i < numberList.size(); i++) {
			// 获取该编号
			Integer no = numberList.get(i);
			// 发牌
			// 留出底牌
			if (i >= 51) {
			  dipaiNo.add(no);
			} else {
				if (i % 3 == 0) {
				  noP1.add(no);
				} else if (i % 3 == 1) {
				  noP2.add(no);
				} else {
				  noP3.add(no);
				}
			}
		}
		// 4 查看三人各自手中的牌(按照牌的大小排序)、底牌
		// 4.1 对手中编号进行排序
		Collections.sort(noP1);
		Collections.sort(noP2);
		Collections.sort(noP3);
		Collections.sort(dipaiNo);
		// 4.2 进行牌面的转换
		// 创建三个玩家牌面集合 以及底牌牌面集合
		ArrayList<String> player1 = new ArrayList<String>();
		ArrayList<String> player2 = new ArrayList<String>();
		ArrayList<String> player3 = new ArrayList<String>();
		ArrayList<String> dipai = new ArrayList<String>();
		// 4.3转换
		for (Integer i : noP1) {
			// 4.4 根据编号找到 牌面 pokerMap
			String card = pokerMap.get(i);
			// 添加到对应的 牌面集合中
			player1.add(card);
		}
		for (Integer i : noP2) {
			String card = pokerMap.get(i);
			player2.add(card);
		}
		for (Integer i : noP3) {
			String card = pokerMap.get(i);
			player3.add(card);
		}
		for (Integer i : dipaiNo) {
			String card = pokerMap.get(i);
			dipai.add(card);
		}
		//4.5 查看
		System.out.println("令狐冲:"+player1);
		System.out.println("石破天:"+player2);
		System.out.println("鸠摩智:"+player3);
		System.out.println("底牌:"+dipai);
	}
}

参考文章
集合
集合
哈希表
扩容问题

  • 10
    点赞
  • 74
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Java 2集合框架图  集合接口:6个接口(短虚线表示),表示不同集合类型,是集合框架的基础。  抽象类:5个抽象类(长虚线表示),对集合接口的部分实现。可扩展为自定义集合类。  实现类:8个实现类(实线表示),对接口的具体实现。  在很大程度上,一旦您理解了接口,您就理解了框架。虽然您总要创建接口特定的实现,但访问实际集合的方法应该限制在接口方法的使用上;因此,允许您更改基本的数据结构而不必改变其它代码。  · Collection 接口是一组允许重复的对象。  · Set 接口继承 Collection,但不允许重复,使用自己内部的一个排列机制。  · List 接口继承 Collection,允许重复,以元素安插的次序来放置元素,不会重新排列。  · Map接口是一组成对的键-值对象,即所持有的是key-value pairs。Map中不能有重复的key。拥有自己的内部排列机制。  · 容器中的元素类型都为Object。从容器取得元素时,必须把它转换成原来的类型。  Java 2简化集合框架图  集合接口  1.Collection 接口  用于表示任何对象或元素组。想要尽可能以常规方式处理一组元素时,就使用这一接口。  (1) 单元素添加、删除操作:   boolean add(Object o):将对象添加给集合   boolean remove(Object o): 如果集合中有与o相匹配的对象,则删除对象o  (2) 查询操作:   int size() :返回当前集合中元素的数量   boolean isEmpty() :判断集合中是否有任何元素   boolean contains(Object o) :查找集合中是否含有对象o

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值