文章目录
- 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.List
和java.util.Set
。其中,List
的特点是元素有序、元素可重复。Set
的特点是元素无序,而且不可重复。List
接口的主要实现类有java.util.ArrayList
和java.util.LinkedList
,Set
接口的主要实现类有java.util.HashSet
和java.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)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。从上图中可以看出,哈希表实则是数组+链表的形式组成,数组指的上图中的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增加了红黑树部分)实现的,如下图所示。
以下是存储流程图:
面试题: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 接口下的集合,它们存储数据的形式不同,如下图。
- 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,一次扩容后是容量为15Vector:线程安全,但速度慢
底层数据结构是数组结构
加载因子为1:即当 元素个数 超过 容量长度 时,进行扩容
扩容增量:原容量的 1倍
如 Vector的容量为10,一次扩容后是容量为20HashSet:线程不安全,存取速度快
底层实现是一个HashMap(保存数据),实现Set接口
默认初始容量为16(为何是16,见下方对HashMap的描述)
加载因子为0.75:即当 元素个数 超过 容量长度的0.75倍 时,进行扩容
扩容增量:原容量的 1 倍
如 HashSet的容量为16,一次扩容后是容量为32
10.案例
按照斗地主的规则,完成洗牌发牌的动作。
具体规则:
- 组装54张扑克牌将
- 54张牌顺序打乱
- 三个玩家参与游戏,三人交替摸牌,每人17张牌,最后三张留作底牌。
- 查看三人各自手中的牌(按照牌的大小排序)、底牌
规则:手中扑克牌从大到小的摆放顺序:大王,小王,2,A,K,Q,J,10,9,8,7,6,5,4,3
案例分析:
- 准备牌:
完成数字与纸牌的映射关系:
使用双列Map(HashMap)集合,完成一个数字与字符串纸牌的对应关系(相当于一个字典)。 - 洗牌:
通过数字完成洗牌发牌 - 发牌:
将每个人以及底牌设计为ArrayList,将最后3张牌直接存放于底牌,剩余牌通过对3取模依次发牌。
存放的过程中要求数字大小与斗地主规则的大小对应。
将代表不同纸牌的数字分配给不同的玩家与底牌。 - 看牌:
通过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);
}
}