文章目录

  • 1.集合概念
  • 2.集合与数组的区别(集合和数组都是容器,它们有啥区别呢?)
  • 3.集合框架
  • 4.List接口介绍
  • 5.Set接口介绍
  • 6.针对Collection集合我们到底使用谁?(掌握)
  • 7.Map接口介绍
  • 8.TreeSet, LinkedHashSet and HashSet 的区别
  • 9.ArrayList、Vector和hashset扩容问题
  • 10.案例


1.集合概念

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

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

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

3.集合框架

  • 集合按照其存储结构可以分为两大类,分别是单列集合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接口定义着单列集合框架中最最共性的内容。

4.List接口介绍

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

5.Set接口介绍

  • HashSet(底层实现是HashMap)
    底层数据结构是哈希表。(无序,唯一)
    如何来保证元素唯一性?
    1.依赖两个方法:hashCode()和equals()
  • LinkedHashSet
    底层数据结构是链表和哈希表。(FIFO插入有序,唯一)
    1.由链表保证元素有序
    2.由哈希表保证元素唯一
  • TreeSet
    底层数据结构是红黑树。(唯一,有序)
    1.如何保证元素排序的呢?
    自然排序
    比较器排序
    2.如何保证元素唯一性的呢?
    根据比较的返回值是否是0来决定

ps:什么是哈希表呢?

散列表(Hash table,也叫哈希表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

java对象集合用流去重 java集合list存储对象_java


从上图中可以看出,哈希表实则是数组+链表的形式组成,数组指的上图中的01233456…,链表则指的是在这里插入图片描述

ps:哈希表实现:

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增加了红黑树部分)实现的,如下图所示。

java对象集合用流去重 java集合list存储对象_数组_02


以下是存储流程图:

java对象集合用流去重 java集合list存储对象_集合_03


面试题:jdk8中hashmap中 put() 和 get() 方法实现

put()方法
1、计算 key 的 Hash 值
2、如果节点数组是空,则初始化节点数组
3、将 hash 值转换成数组的下标,下标位置上如果没有任何元素,就把 Node 添加到这个位置上
4、如果说下标对应的位置上有节点,并且该节点的 hash 值、k 与传入的相对,则进行覆盖
5、如果说下标对应的位置上是树节点,则调用树节点处理方法
6、如果说下标对应的位置上有节点链表,则将新节点放入链表的末尾。
7、如果链表的长度超过阀值 8,则调用 treeifyBin 方法进行扩容或者树化:如果数组的长度小于 64 则进行 resize,如果数组的长度大于 64 则将链表转为红黑树
get() 方法
1、先调用 k 的 hashCode()方法得出哈希值,并通过哈希算法转换成数组的下标
2、通过数组下标快速定位到某个位置上,如果这个位置上什么都没有,则返回 null
3、检查第一个节点的 hash 与 k,如果相等则返回
4、如果第一节点是树节点则按照 hash 和 k 查找树,如果相等则返回
5、如果第一节点是链表节点则遍历,比较 hash 与 k,如果相等则返回

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

唯一吗?

是:Set

排序吗?

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

否:List

要安全吗?

是:Vector
否:ArrayList或者LinkedList

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

1.如果你知道是Collection集合,但是不知道使用谁,就用ArrayList。
2.如果你知道用集合,就用ArrayList。

7.Map接口介绍

1.现实生活中,我们常会看到这样的一种集合:IP地址与主机名,身份证号与个人,系统用户名与系统用户对象等,这种一一对应的关系,就叫做映射。Java提供了专门的集合类用来存放这种对象关系的对象,即 java.util.Map 接口。

2.我们通过查看 Map 接口描述,发现 Map 接口下的集合与 Collection 接口下的集合,它们存储数据的形式不同,如下图。

java对象集合用流去重 java集合list存储对象_集合_04

  • Collection 中的集合,元素是孤立存在的(理解为单身),向集合中存储元素采用一个个元素的方式存储。
  • Map 中的集合,元素是成对存在的(理解为夫妻)。每个元素由键与值两部分组成,通过键可以找对所对应的
    值。
  • Collection 中的集合称为单列集合, Map 中的集合称为双列集合。
  • 需要注意的是, Map 中的集合不能包含重复的键,值可以重复;每个键只能对应一个值。

Map接口有三个比较重要的实现类,分别是HashMap、TreeMap和HashTable。
1.TreeMap是有序的,HashMap和HashTable是无序的。
2.HashTable的方法是同步的,HashMap的方法不是同步的。这是两者最主要的区别。
这就意味着:
1.HashTable是线程安全的,HashMap不是线程安全的。
2.HashMap效率较高,HashTable效率较低。如果对同步性或与遗留代码的兼容性没有任何要求,建议使用HashMap。 查看HashTable的源代码就可以发现,除构造函数外,HashTable的所有 public 方法声明中都有 synchronized关键字,而HashMap的源码中则没有。
3.Hashtable不允许null值,HashMap允许null值(key和value都允许)
4.父类不同:HashTable的父类是Dictionary,HashMap的父类是AbstractMap

8.TreeSet, LinkedHashSet and HashSet 的区别

1.介绍

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

2.相同点

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

3.不同点

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

9.ArrayList、Vector和hashset扩容问题

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

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

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

10.案例

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

  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);
	}
}