摘要:
Java基础加强重温_05:
Iterator迭代器(指针跟踪元素)、
增强for循环(格式、底层)、
集合综合案例-斗地主(代码规范抽取代码,集合元素打乱)、
数据结构【栈(先进后出,子弹夹)、队列(先进先出,火车过山洞)、
数组(查找增删原理)、
链表(多结点互相连接、单/双向)、
红黑树(二叉查找树)】、
List的子类【ArrayList集合(底层数组、扩容原理)、
LinkedList集合(底层链表)、
Vector集合(看作ArrayList的兄弟类,线程安全集合,效率低)、Enumeration(Iterator前身)】、
Set接口【HashSet集合(数据结构哈希表、重写hashCode和equals,hashCode设计规定)、LinkedHashSet集合】
一、Iterator迭代器
在程序开发中,经常需要遍历集合中的所有元素。针对这种需求,JDK专门提供了一个接口 java.util.Iterator 。想要遍历Collection集合,那么就要获取该集合迭代器完成迭代操作
迭代:是Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续在判断,如果还有就再取出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
1、为什么要用迭代器
统一解决遍历所有Collection集合的问题。
Collection集合里面没有get(int index)方法获取某个元素。因为List是有索引的,Set没有索引的,Collection作为父接口,没有用索引的get方法,所以Collection提供了迭代器供遍历集合的所有元素。
迭代器和集合的关系
Collection是所有集合的接口,Iterator是迭代器的意思,也是一个接口
Collection提供获取迭代器的方法 Iterator iterator()
List是Collection的子接口,ArrayList是List的实现类,重写了Iterator iterator()。集合的所有实现类都重写了Iterator iterator()
2、迭代器常用方法
public Iterator iterator() : 获取集合对应的迭代器,用来遍历集合中的元素的。访问方式:集合对象.iterator()
boolean hasNext() 判断集合是没有下一个元素,有就返回true,否则false。访问方式:迭代器对象.hasNext()
E next() 1、获取当前迭代器指向的元素 2、把迭代器指向下一元素。访问方式:迭代器对象.next()
迭代器使用步骤
1、创建集合
2、获取该集合的迭代器
3、进行迭代
Iterator迭代集合代码示例
public class IteratorDemo {
public static void main(String[] args) {
// 使用多态方式 创建对象
Collection<String> coll = new ArrayList<String>();
// 添加元素到集合
coll.add("串串星人");
coll.add("吐槽星人");
coll.add("汪星人");
//遍历
//获得该集合的迭代器
Iterator<String> it = coll.iterator();
// 泛型指的是 迭代出 元素的数据类型
while(it.hasNext()){ //判断是否有迭代元素
String s = it.next();//获取迭代出的元素
System.out.println(s);
}
}
}
1、在进行集合元素获取时,如果集合中已经没有元素了,还继续使用迭代器的next方法,将会抛出java.util.NoSuchElementException没有集合元素异常。
2、在进行集合元素获取时,如果添加或移除集合中的元素 , 将无法继续迭代 , 将会抛出ConcurrentModificationException并发修改异常.
3、迭代器的实现原理
迭代器迭代原理:判断一次,获取一次
遍历集合时,首先调用集合的iterator()方法获得迭代器对象,再使用hashNext()方法判断集合中是否存在下一个元素。如果存在,则调用next()方法将元素取出。不存在则说明已到达了集合末尾,停止遍历元素。
Iterator迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素。图例如下:
在调用Iterator的next方法之前,迭代器的索引位于第一个元素之前,不指向任何元素。当第一次调用迭代器的next方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,当再次调用next方法时,迭代器的索引会指向第二个元素并将该元素返回,依此类推,直到hasNext方法返回false,表示到达了集合的末尾,终止对元素的遍历。
4、增强for循环(foreach)
foreach只是一个技术名称,是一种遍历方式,学习foreach遍历的关键是记住foreach的遍历格式。
foreach形式遍历既可以遍历集合也可以遍历数组。
foreach遍历集合底层就是使用了Iterator迭代器。
增强for格式
for(被遍历集合或者数组中元素的类型 变量 : 被遍历集合或者数组){
System.out.println(变量);
}
//通俗
for(集合中元素的数据类型 变量:集合) {
处理每一个元素
}
for(数组中元素的数据类型 变量:数组) {
处理每一个元素
}
增强for循环特点
好处: 写法简单
缺点: 遍历不带下标
什么时候使用增强for?
- 不关注下标的时候(如set集合,没有索引。遍历set集合时可以用增强for)
增强for循环的底层
- 集合的增强for遍历底层是 使用迭代器遍历
- 数组的增强for遍历底层是 使用带下标的for循环
增强for案例
public static void main(String[] args){
// 定义一个数组
String[] names = new String[]{"张三","李四","王五"};
// name
for(String name : names){
System.out.println(name);
}
System.out.println("-------------------------------------");
int[] scores = new int[]{90,100,89,70};
for(int ele : scores){
System.out.println(ele);
}
}
foreach遍历是自动进行的,它会让变量依次等于每一个元素,然后取出数组的每一个元素值。
foreach遍历在写法上显得更加的简洁和方便,但是它无法知道当前遍历到了数组的哪个索引位置处。
二、集合综合案例(斗地主)
案例介绍
按照斗地主的规则,完成洗牌发牌的动作。 具体规则:
使用54张牌打乱顺序,三个玩家参与游戏,三人交替摸牌,每人17张牌,最后三张留作底牌。
案例分析
1、准备牌:
- 牌可以设计为一个ArrayList,每个字符串为一张牌。 每张牌由花色数字两部分组成,我们可以使用花色集合与数字集合嵌套迭代完成每张牌的组装。 牌由Collections类的shuffle方法进行随机排序。
2、发牌
- 将每个人以及底牌设计为ArrayList,将最后3张牌直接存放于底牌,剩余牌通过对3取模依次发牌。
3、看牌
- 直接打印每个集合。
斗地主规则:54张 = 52张通常牌+2张大小王
发牌:三个玩家,每个人发17张牌,剩下3张,选地主,地主可以拥有这3张牌
1、创建牌
a、创建牌的对象,属性(花色,名字)
b、创建54张牌,使用集合存储54张牌
* 2张大小王
* 4花色*13名字的牌
2、看牌(写代码看一下是不是54张)
3、洗牌: Collections是一个工具类,Objects
Collections.shuffle(List集合),把List的元素打乱
4、发牌
5、看每个玩家的牌
代码规范
1、代码模块化
模块化:一个类/一个方法只做一件事情
- 就是把每个独立的功能包装成方法,公用的变量通过参数的形式传递
2、main方法的规范
作为一个优秀的开发者应该遵循的规范,在main方法中只能出现:
- 只出现变量定义(局部变量,匿名内部类…)
- 方法的调用
- 不应该出现for、if、switch、while,带逻辑的代码都不应该出现
代码实现
非代码规范版
定义Poker类
public class Poker {
private String name;
private String color;
public Poker() {
}
public Poker(String color, String name) {
this.name = name;
this.color = color;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "Poker{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
'}'+"\n";
}
}
测试类
public class Demo12 {
public static void main(String[] args) {
// 创建一个ArrayList用于存放一副牌
ArrayList<Poker> pokers = new ArrayList<>();
pokers.add(new Poker("大王", ""));
pokers.add(new Poker("小王", ""));
String[] colors = new String[] {"♠", "♥", "♣", "♦"};
String[] numbers = new String[] {"2", "A", "K", "Q", "J", "10", "9", "8", "7", "6","5", "4", "3"};
// 组合牌, 嵌套循环的流程:外循环一次,内循环所有次
// 2.使用嵌套循环生成一副牌
for (String n : numbers) {
// "2", "A",...
for (String c : colors) {
// "♠", "♥", "♣", "♦"
Poker p = new Poker(c, n);
// 3.将54张牌放到集合
pokers.add(p);
}
}
//打印,未洗牌
//System.out.println(pokers);
// 洗牌: 使用Collections集合工具类的方法
// static void shuffle•(List<?> list) 将集合中元素的顺序打乱
Collections.shuffle(pokers);
System.out.println("洗牌后:" + pokers);
// 发牌
// 1.创建3个玩家集合,创建底牌集合
ArrayList<Poker> player01 = new ArrayList<>();
ArrayList<Poker> player02 = new ArrayList<>();
ArrayList<Poker> player03 = new ArrayList<>();
ArrayList<Poker> diPai = new ArrayList<>();
// 2.遍历牌的集合
//0 1 2 3 4 5 6 7 8 9 10 ... 51 52 53
// pokers = [♦5], [♣4], [♦8], [♣A], [♣7], [♦2], [♠6], [♣J], [♥A], [♥7], [♥6], [♣5],[♦7], [♥10]
// 玩家1: 只拿到索引0,3,6...的牌 索引 % 3 == 0
// 玩家2: 索引1,4,7 索引 % 3 == 1
// 玩家3: 索引2,5,8 索引 % 3 == 2
// 3.根据索引将牌发给不同的玩家
for (int i = 0; i < pokers.size(); i++) {
// i表示索引,poker就是i索引对应的poker
Poker poker = pokers.get(i);
if (i >= 51) { // 最后3张给底牌
diPai.add(poker);
} else if (i % 3 == 0) { // 玩家1
player01.add(poker);
} else if (i % 3 == 1) { // 玩家2
player02.add(poker);
} else if (i % 3 == 2) { // 玩家3
player03.add(poker);
}
}
// 看牌
System.out.println("玩家1: " + player01);
System.out.println("玩家2: " + player02);
System.out.println("玩家3: " + player03);
System.out.println("底牌: " + diPai);
// 还要创建一副牌
// 创建一个ArrayList用于存放一副牌
}
}
代码规范版
抽取斗地主的代码
定义Poker类
public class Poker {
private String name;
private String color;
public Poker() {
}
public Poker(String color, String name) {
this.name = name;
this.color = color;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getColor() {
return color;
}
public void setColor(String color) {
this.color = color;
}
@Override
public String toString() {
return "Poker{" +
"name='" + name + '\'' +
", color='" + color + '\'' +
'}'+"\n";
}
}
定义Poker的工具类,抽取功能代码(静态方法封装)
public class PokerTool {
//创建牌方法
public static ArrayList<Poker> createPokers() {
//1、创建牌
//a、创建牌的对象,属性(花色,名字)
//b、创建54张牌,使用集合存储54张牌
// * 2张大小王
// * 4花色*13名字的牌
//joker是小丑
Poker bigJoker = new Poker("🃏","大王");
Poker smallJoker = new Poker("🃏","小王");
//两次嵌套for循环
String[] colors = {"♠","♥","♣","♦"};
//J:JACK王子 Q:Queen皇后 K:King国王 A:ACE
String[] names = {"A","2","3","4","5","6","7","8","9","10","J","Q","K"};
ArrayList<Poker> pokers = new ArrayList<>();
pokers.add(bigJoker);
pokers.add(smallJoker);
for (String color : colors) {
for (String name : names) {
Poker poker = new Poker(color,name);
pokers.add(poker);
}
}
return pokers;
}
//洗牌方法
public static void shufflePokers(ArrayList<Poker> pokers) {
//3、洗牌: Collections是一个工具类,Objects
//Collections.shuffle(List集合),把List的元素打乱
Collections.shuffle(pokers);
System.out.println("洗牌后"+pokers.size()+"张牌:"+pokers);
}
//发牌方法
public static void sendPokers(ArrayList<Poker> pokers) {
//4、发牌(底牌留最后三张)
// * 创建3个List来模拟玩家
// * 遍历54张牌,根据遍历的索引%3发给不同玩家
//i 0~53
ArrayList<Poker> player1 = new ArrayList<>();
ArrayList<Poker> player2 = new ArrayList<>();
ArrayList<Poker> player3 = new ArrayList<>();
for (int i = 0; i < pokers.size()-3; i++) {
// 0 1 2
int mod = i%3;
Poker poker = pokers.get(i);
switch (mod) {
case 0:player1.add(poker);break;
case 1:player2.add(poker);break;
case 2:player3.add(poker);break;
}
}
//5、看每个玩家的牌
System.out.println("玩家1有"+player1.size()+"张牌:"+player1);
System.out.println("玩家2有"+player2.size()+"张牌:"+player2);
System.out.println("玩家3有"+player3.size()+"张牌:"+player3);
//6、查看底牌
//List的截取方法:subList(int fromIndex,int toIndex)
//fromIndex从哪个索引开始截取,toIndex:截取到哪个>=fromIndex<toIndex
List<Poker> lastPokers = pokers.subList(pokers.size()-3,pokers.size()); //索引:51 52 53
System.out.println("底牌"+lastPokers.size()+"张牌:"+lastPokers);
}
}
三、数据结构
数据结构 : 数据用什么样的方式组合在一起。
常见数据结构
数据存储常用结构有:栈、队列、数组、链表和红黑树。
1、栈
栈,stack,又称堆栈。它是运算受限的线性表,其限制是仅允许在表的一端进行插入和删除操作,不允许在其他任何位置进行添加、查找、删除等操作。
特点
采用栈结构存储数据的集合,对元素的存取有如下的特点:
- 先进后出(先存进去的元素,要在它后面存进去的元素依次被取出后,才能取出该元素)。
例如,子弹压进弹夹,先压进去的子弹在下面,后压进去的子弹在上面,当开枪时,先弹出上面的子弹,然后才能弹出下面的子弹。 - 栈的入口、出口的都是栈的顶端位置。
学习栈需要注意两个名词
- 压栈:就是存元素。即把元素存储到栈的顶端位置,栈中已有元素依次向栈底方向移动一个位置。
- 弹栈:就是取元素。即把栈的顶端位置元素取出,栈中已有元素依次向栈顶方向移动一个位置。
栈代码模拟
public class TestStack {
public static void main(String[] args) {
//java虚拟机就有栈
//ExceptionStack 异常栈
one();
}
//one()第一个进栈
private static int one() {
return two();
}
//第二个进栈
private static int two() {
return three();
}
//three第三个进栈,返回3后,第一个退栈
private static int three() {
return 3;
}
}
2、队列
队列:queue,简称队。同堆栈一样,也是一种运算受限的线性表,其限制是仅允许在表的一端进行插入,而在表的另一端进行删除。
特点
采用队列结构存储数据的集合,元素存取有如下的特点:
- 先进先出(即存进去的元素,要在它前面的元素依次取出后,才能取出该元素)。
例如,小火车过山洞,车头先进去,车尾后进去;车头先出来,车尾后出来。
例如,安检 - 队列的入口、出口各占一侧。例如,下图中的左侧为入口,右侧为出口。
栈、队列(详细图解与代码实现:
https://blog.csdn.net/YOLO97/article/details/82494221
3、数组
数组,Array,是有序的元素序列。
数组是在内存中开辟一段连续的空间,并在此空间存放元素。就像是一排出租屋,有100个房间,从001到100每个房间都有固定编号,通过编号就可以快速找到租房子的人。
特点
采用数组结构存储数据的集合,元素存取有如下的特点:
查找元素快
通过索引,可以快速访问指定位置的元素
增删元素慢
指定索引位置增加元素
一般数组是不能添加元素的,因为他们在初始化时就已定好长度了,不能改变长度。所以需要创建一个新数组,将指定新元素存储在指定索引位置,再把原数组元素根据索引,复制到新数组对应索引的位置,跳过指定存储新元素的索引位置往后顺延。如下图
指定索引位置删除元素
需要创建一个新数组,把原数组元素根据索引,复制到新数组对应索引的位置,原数组中指定索引位置的元素不复制到新数组。如下图
小结:
4、链表
链表:linked list,由一系列结点node(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。
每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
我们常说的链表结构有单向链表与双向链表,这里介绍的是单向链表。
单向链表,每个结点只有数据域和指针域,指针域指向下一个结点地址
双向链表,每个结点有数据域和两个指针域,一个指针域指向上一个结点,一个指针与指向下一个结点
特点
采用链表结构存储数据的集合,元素的存取有如下的特点:
- 多个结点之间,通过地址进行连接。
例如,多个人手拉手,每个人使用自己的右手拉住下个人的左手,依次类推,这样多个人就连在一起了。 - 查找元素慢
想查找某个元素,需要通过连接的节点,从第一个开始遍历依次查找 - 增删元素快
增加元素只需要修改它前面的那个元素指向的地址就可以了
删除元素只需要将前一个元素指向的地址更改即可
(增加删除都一样,修改前一个元素指向下一个元素的地址)
单向链表示意图
链表的Java实现(双向链表)
定义节点类Node
//链表的java实现
//Node叫节点
public class Node {
//数据域
String value;
//指针域
//下一个
Node next;
//上一个
Node prev;
public Node(String value) {
this.value = value;
}
}
测试类
public class TestStack {
public static void main(String[] args) {
Node node1 = new Node("1");
Node node2 = new Node("2");
Node node3 = new Node("3");
node1.next = node2;
node1.prev = null;
node2.next = node3;
node2.prev = node1;
node3.next = null;
node3.prev = node2;
//想找第二个节点,要从第一个开始。查找第n个,也要从第一个开始遍历
Node node22 = node1.next;
System.out.println(node2==node22);
}
}
小结:
单向链表查询图示
单向链表增加、删除图示
双向链表结构图示
5、红黑树
红黑树是二叉树的一种。
树结构
二叉树
二叉树:binary tree ,是每个结点下子树不超过2的有序树(tree) 。
二叉树是每个节点下最多有两个子树的树结构。
最顶上的叫根结点
下面左右两边被称作“左子树”和“右子树”。
每个左子树、右子树相对它们下面两个左右子树又是节点
深入学习二叉树(一) 二叉树基础
https://www.jianshu.com/p/bf73c8d50dc2
二叉查找树(红黑树本质)
二叉查找树数据特点
节点的左子树小于节点本身
节点的右子树大于节点本身
左右子树同样为二叉搜索树
红黑树
红黑树是二叉树的一种,本身就是一颗二叉查找树,将新节点插入后,该树仍然是一颗二叉查找树。可以通过红色节点和黑色节点尽可能的保证二叉树的平衡,从而来提高效率。
红黑树特点
1、节点分为红色或者黑色。
2、根节点必为黑色。(最顶上的节点)
3、叶子节点都为黑色,且为 null。(最下面的节点)
4、连接红色节点的两个子节点都为黑色(红黑树不会出现相邻的红色节点)。
5、从任意节点出发,到其每个叶子节点的路径中包含相同数量的黑色节点。
6、新加入到红黑树的节点为红色节点。(通过变色的方式,使结构满足红黑树的规则)
红黑树查找数据步骤
从二叉树中找到值为 58 的节点。
第一步:首先查找到根节点,值为 60 的节点。
第二步:比较我们要找的值 58 与该节点的大小。
如果等于,那么恭喜,已经找到;如果小于,继续找左子树;如果大于,那么找右子树。
很明显 58<60,因此我们找到左子树的节点 56,此时我们已经定位到了节点 56。
第三步:按照第二步的规则继续找。
58>56 我们需要继续找右子树,定位到了右子树节点 58,恭喜,此时我们已经找到了。
30张图带你彻底理解红黑树
https://www.jianshu.com/p/e136ec79235c
图解“红黑树”原理,一看就明白!
https://www.sohu.com/a/335449747_463994
四、List集合
java.util.List 接口继承自 Collection 接口,是单列集合的一个重要分支,习惯性地会将实现了 List 接口的类的对象称为List集合。
在List集合中允许出现重复的元素,所有的元素是以一种线性方式(如一排出租房房间)进行存储的,在程序中可以通过索引来访问集合中的指定元素。
另外,List集合还有一个特点就是元素有序,即元素的存入顺序和取出顺序一致。
特点
- 有顺序(存入和取出顺序一致)
- 能重复
- 有下标
常用方法
public void add(int index, E element) : 将指定的元素,添加到该集合中的指定位置上。
public E get(int index) :返回集合中指定位置的元素。
public E remove(int index) : 移除列表中指定位置的元素, 返回的是被移除的元素。
public E set(int index, E element) :用指定元素替换集合中指定位置的元素,返回值的更新前的元素
五、List的实现类
1、ArrayList类(ArrayList集合)
java.util.ArrayList 集合数据存储的结构是数组结构。元素增删慢,查找快。
日常开发中使用最多的功能为查询数据、遍历数据,所以 ArrayList 是最常用的集合。
ArrayList底层结构
ArrayList存储数据的底层是Object[] 数组,数组默认长度是10,当添加的元素超个10个会触发扩容
ArrayList的扩容机制(arraycopy)
第一次初始化数组为10,当下次添加元素发现已经是第11个了,就需要扩容。扩容机制 每次扩容为旧容量的1.5倍。
- 创建新的数组,长度是原数组的1.5倍(新的容量 = 旧的容量 + 旧的容量/2)
1.5倍长度,有小数去掉小数。如15*1.5=22.5取22 - 使用System.arraycopy(),复制原数组元素到新数组
为什么需要每次扩容1.5倍
- 如果每次添加元素都扩容+1的话,不断创建新的数组,浪费资源,所以需要一次扩容多一些
2、LinkedList类(LinkedList集合)
java.util.LinkedList集合数据存储的结构是链表结构。与ArrayList集合相反:元素增删快,查找慢
LinkedList集合底层是一个双向链表,双向链表如图下
链表的数据结构:找元素的时候,元素的索引在中位数左边,就从头找起,如果在右边,就从尾找起
常用方法
实际开发中对一个集合元素的添加与删除经常涉及到首尾操作,而LinkedList提供了大量首尾操作的方法。这些方法了解即可
public void addFirst(E e) :将指定元素插入此列表的开头。
public void addLast(E e) :将指定元素添加到此列表的结尾。
public E getFirst() :返回此列表的第一个元素。
public E getLast() :返回此列表的最后一个元素。
public E removeFirst() :移除并返回此列表的第一个元素。
public E removeLast() :移除并返回此列表的最后一个元素。
public E pop() :从此列表所表示的堆栈处弹出一个元素。
public void push(E e) :将元素推入此列表所表示的堆栈。
public boolean isEmpty() :如果列表不包含元素,则返回true。
LinkedList是List的子类,List中的方法LinkedList都是可以使用,这里就不做详细介绍,在开发时,LinkedList集合也可以作为堆栈,队列的结构使用。
LinkedList集合的底层
LinkedList集合的底层结构是链表
链表代码示例:
class LinkedList<E> {
//开头的Node
Node<E> first;
//最后的Node
Node<E> last;
//节点的内部类
class Node<E> {
E item;
//双向链表存上一个,下一个
Node<E> next;
Node<E> prev;
}
}
LinkedList集合案例
public class Demo08 {
public static void main(String[] args) {
/*
//链表代码示例
class LinkedList<E> {
//开头的Node
Node<E> first;
//最后的Node
Node<E> last;
//节点的内部类
class Node<E> {
E item;
//双向链表存上一个,下一个
Node<E> next;
Node<E> prev;
}
}
*/
//创建一个LinkedList集合
LinkedList<String> list= new LinkedList<>();
list.add("相貌平平古天乐");
list.add("悔创阿里杰克马");
list.add("不爱美人刘强东");
list.add("桌子不齐邓紫棋");
list.add("桌子不齐邓紫棋");
//list 的 add、set、remove、get都有
//void addFirst(E e) 在该列表开头插入指定的元素。
//链表头部新增:
list.addFirst("家庭美满王宝强");
System.out.println(list);
//void addLast(E e) 将指定的元素追加到此列表的末尾。
list.addLast("家庭美满王宝强");
System.out.println(list);
//回去感受以下api
//E getFirst() 返回此列表中的第一个元素。
//E getLast() 返回此列表中的最后一个元素。
//E removeFirst() 从此列表中删除并返回第一个元素。
//E removeLast() 从此列表中删除并返回最后一个元素。
System.out.println("list.size() = " + list.size()); //7/2= 3
list.get(3);
/*
Node firstNode = getFirstNode();
Node targetNode;
for(int i=0;i<3;i++) {
targetNode = firstNode.next()
if(targetNode.equals(3)) {
return ...
}
}
*/
}
}
linkedlist和arraylist的区别
https://www.php.cn/faq/415621.html
3、Vector集合
Vector大致可以认为是ArrayList的兄弟类,但Vector是线程安全集合(每次只能做一件事情),效率低。
java.util.Vector集合数和ArrayList一样底层使用数组结构。元素增删慢,查找快。
与ArrayList不同的是Vector是线程安全的,速度慢,工作中很少使用。
Enumeration(迭代)
Enumeration 是 Iterator的前身,jdk早期版本比较多用。Enumeration的作用和Iterator一样
Enumeration<E> elements() 返回此Vector的枚举(迭代器的前身)。
elements.hasMoreElements();相当于 hasNext()
elements.nextElement();相当于 next()
Vector集合案例
public class Demo09 {
public static void main(String[] args) {
Vector<String> list= new Vector<>();
list.add("相貌平平古天乐");
list.add("悔创阿里杰克马");
list.add("不爱美人刘强东");
list.add("桌子不齐邓紫棋");
list.add("桌子不齐邓紫棋");
System.out.println("list.get(0) = " + list.get(0));
//Enumeration 是 Iterator的前身
Enumeration<String> elements = list.elements();
while(elements.hasMoreElements()) {
String element = elements.nextElement();
System.out.println("element = " + element);
}
}
}
六、Set接口
java.util.Set 接口和 java.util.List 接口一样,同样继承自 Collection 接口,它与 Collection 接口中的方法基本一致,并没有对 Collection 接口进行功能上的扩充,只是比 Collection 接口更加严格了。与 List 接口不同的是, Set 接口中元素无序,并且都会以某种规则保证存入的元素不出现重复。
Set集合特点
- 元素无序(存入和取出顺序不能保证一致)。
- 元素不重复
Set集合遍历元素的方式可以采用:迭代器、增强for。(没有下标)
List集合特点:有顺序、能重复、有下标
Set接口的方法:
boolean add(E e) 添加一个元素
boolean remove(Object o) 删除一个元素
Set集合存储案例
public class HashSetDemo {
public static void main(String[] args) {
//创建 Set集合
HashSet<String> set = new HashSet<String>();
//添加元素
set.add(new String("cba"));
set.add("abc");
set.add("bac");
set.add("cba");
//遍历
for (String name : set) {
System.out.println(name);
}
}
}
输出结果如下,字符串"cba"只存储了一个,说明集合中不能存储重复元素:
cba
abc
bac
1、HashSet集合
java.util.HashSet 是 Set 接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不能保证一致)。
java.util.HashSet 底层的实现其实是一个 java.util.HashMap 支持,HashMap存储数据的结构是哈希表。(HashSet底层是HashMap,HashSet、HashMap存储数据结构都是哈希表)
集合体系
什么是HashMap?
https://blog.csdn.net/qq_36711757/article/details/80394272
一文读懂HashMap
https://www.jianshu.com/p/ee0de4c99f87
HashSet集合方法
增:add
删:remove(Object obj)
查:遍历+判断是否存在contains()
改:删除+添加
public boolean list.contains(Object o)
当前列表若包含某元素,返回结果为true, 若不包含该元素,返回结果为false。
contains()内部是比较hash值
HashSet案例
public class Demo10 {
public static void main(String[] args) {
Set<Integer> sets = new HashSet<>();
//增
sets.add(1);
sets.add(0);
sets.add(0);
sets.add(8);
sets.add(6);
sets.add(9);
//删
sets.remove(9);
System.out.println("sets = " + sets);
//查
boolean contains = sets.contains(6);
System.out.println(contains);
//我要1
//只能遍历+判断
for (Integer each : sets) {
if(each == 1) {
System.out.println(each);
break;
}
}
//改,把0删除,再添加-1
sets.remove(0);
sets.add(-1);
}
}
java中list集合中contains()的用法:https://zhidao.baidu.com/question/1690852039634444108.html
2、HashSet集合存储数据的结构(哈希表)
HashSet存储对象后,是根据对象的哈希值来确定元素在集合中的存储位置,因此具有良好的存储和查找性能。
保证元素唯一性的方式依赖于:hashCode 与 equals 方法
哈希表
在JDK1.8之前,哈希表底层采用数组+链表实现,即使用数组处理冲突,同一hash值的链表都存储在一个数组里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。
JDK1.8中,哈希表底层采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间,大程度优化了HashMap的性能。
简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的。
哈希表图示
哈希表存储元素的过程(哈希表判断元素唯一的原理)
- 根据hashCode的尾数,确定哈希表数组中的下标作为存储位置
- 这个元素再跟该数组位置存储的链表数据,用equals方法一一比较。
如果已有重复元素,则不添加
如果无,则添加。
数据结构-Hash
https://www.jianshu.com/p/b468abd86f61
哈希
计算哈希值的过程就叫做哈希。哈希的主要应用是哈希表和分布式缓存。
哈希(hash)是一种生成固定长度序列的算法,哈希值(hashCode)相当于一个对象的身份证,默认情况下是内存的哈希地址
哈希值(hashcode)
把任意长度的输入(输入叫做预映射,知道就行),通过一种函数(hashCode() 方法),变换成固定长度的输出,该输出就是哈希值(hashCode)
可以理解为唯一编码、摘要值等,具体实现可能是内存地址,在java中可用于识别两个变量是否其实是同个对象。同个对象则此刻的值必定相等,但不同对象也可以是数值相等。
哈希函数
把任意长度的输入(输入叫做预映射,知道就行),通过一种函数(hashCode() 方法),变换成固定长度的输出,该输出就是哈希值(hashCode),这种函数就叫做哈希函数,而计算哈希值的过程就叫做哈希。
3、hashCode和equals方法
equals和hashCode是java.lang.Object类的两个重要的方法。
HashSet集合能保证元素的唯一(不重复),其实就是根据对象的hashCode和equals方法来决定的。
public native int hashCode():返回对象的10进制的内存哈希地址
//默认toString打印的hash地址其实就是:16进制的hashCode()
public boolean equals(Object anObject)
哈希值(hashcode)相当于一个对象的身份证,默认情况下是内存的哈希地址。
大部分不同对象情况下不重复,但不能完全保证不重复。如下面案例“方面”、“树人”输出同样的hashCode。所以,hashCode来替代equals不靠谱
String str1 = "方面";
String str2 = "树人";
System.out.println(str1.equals(str2));
int code1 = str1.hashCode();
System.out.println("code1 = " + code1); //846025
int code2 = str2.hashCode();
System.out.println("code2 = " + code2); //846025
hashCode方法可以重写(使用IDEA自动重写),重写后返回Objects.hash(name, age):会根据传入的变量,生成对应的hash码,使得哈希值(hashcode)与成员变量相关
@Override
public int hashCode() {
return Objects.hash(name, age);
}
重写后的equals方法比较效率较低,而且哈希值(hashcode)不等2个对象绝对不相等。所以采用先hashCode方法比较,再用equals方法比较的方式。
如果我们往HashSet集合中存放自定义的对象,要保证其唯一,就必须重写hashCode和equals方法,建立属于当前对象的比较方式。
如果不重写,这个对象用的是Object类的hashCode和equals方法。
为什么要重写equals方法?
equals方法通过某个特征值来判断两个对象是否“等价”,当这两个对象等价时,判断结果为true,否则结果为false。当然,这里的“特征值”不会只是简单的“对象引用”,事实上,Object类(Java的“对象世界”的根)中实现的equals方法,就是把“特征值”设定为“对象引用”来进行判断等价性的,因此可以得知,Object类中equals方法只是简简单单地返回this引用和被判断的obj的引用的“==运算”的值。但是很多情况下,并不是要求两个对象只有引用相同时(此时二者为一个对象)才“判定为等价”,这就需要ADT设计者来界定两个实例对象判断等价的条件,即设定要比较的特征值。
Java的equals方法实现及其细节:https://www.cnblogs.com/stevenshen123/p/9199354.html
重写hashCode方法和equals方法
使用IDEA自动生成即可重写
hashCode方法和equals方法的区别:
重写hashCode方法和equals方法有个规范
1、hashCode方法比较相等,equals方法比较不一定相等
2、2个对象相等,equals方法,hashCode方法比较必须要相等
因为重写hashCode方法可以自定义,通过自定义可以导致哈希值(hashcode)不等,所以定下这个设计规范,重写hashCode方法必须要遵循这个规范
阿里的开发者规范:重写equals的时候必须要重写hashCode
代码示例
现在有两个Student对象:
Student s1=new Student("小明",18);
Student s2=new Student("小明",18);
//此时s1.equals(s2)一定返回true。//重写后equals比较跟对象属性相关
假如只重写equals方法,而不重写hashCode方法,那么Student类的hashCode方法就是Object默认的hashCode方法。
默认的hashCode方法是根据对象的内存地址经哈希算法得来的哈希值(hashcode),s1,s2属性相同但内存地址不相同。显然此时s1!=s2,故两者的哈希值(hashcode)不一定相等。
然而重写了equals方法,且s1.equals(s2)返回true。根据哈希值(hashcode)的设计规则,两个对象相等其哈希值(hashcode)一定相等。所以矛盾就产生了,因此重写equalsf方法一定要重写hashCode方法,而且从Student类重写后的hashCode方法中可以看出,重写后返回的新的哈希值(hashcode)与Student的两个属性有关。所以当对象属性相同,哈希值(hashcode)也一定相同
理解:
- 重写的equals方法,会根据两个对象的属性进行比较,不重写的equals(即默认是Object类的equals),是根据内存地址比较,跟==相同。
- 重写equals方法不重写hashCode方法,equals判断相同属性的两个对象为相等,默认的hashCode方法是根据对象的内存地址经哈希算法得来的hashcode(哈希值),此时的两个对象的hashcode(哈希值)不一定相等,违法了哈希值(hashcode)的“两个对象相等,hashcode一定相等”的设计规则。
- 所以重写equals方法一定要重写hashCode
为什么重写equals一定要重写hashcode?https://blog.csdn.net/xl_1803/article/details/80445481
hashcode的生成规则
https://www.jianshu.com/p/b79c9a51e336
4、HashSet集合存储流程图(HashMap底层)
4、HashSet存储自定义类型元素案例
给HashSet中存放自定义类型元素时,需要重写对象中的hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一。
创建自定义Student类
public class Student {
private String name;
private int age;
//get/set
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
Student student = (Student) o;
return age == student.age &&
Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
创建测试类:
public class HashSetDemo2 {
public static void main(String[] args) {
//创建集合对象 该集合中存储 Student类型对象
HashSet<Student> stuSet = new HashSet<Student>();
//存储
Student stu = new Student("于谦", 43);
stuSet.add(stu);
stuSet.add(new Student("郭德纲", 44));
stuSet.add(new Student("于谦", 43));
stuSet.add(new Student("郭麒麟", 23));
stuSet.add(stu);
for (Student stu2 : stuSet) {
System.out.println(stu2);
}
}
}
执行结果:
Student [name=郭德纲, age=44]
Student [name=于谦, age=43]
Student [name=郭麒麟, age=23]
5、LinkedHashSet类(LinkedHashSet集合)
在HashSet下面有一个子类 java.util.LinkedHashSet ,它是链表和哈希表组合的一个数据存储结构。
LinkedHashSet使用了链表,链表可以维护顺序,能解决HashSet无序的问题,因为使用了链表维护顺序,但是每次增删都需要维护链表,效率会比HashSet低一些。
不想重复,又要有序,就使用LinedHashSet
- 有序:按什么顺序放入元素,取出来的元素也是什么顺序
理解:LinkedHashSet集合是有序的set集合
LinkedHashSet案例
public class LinkedHashSetDemo {
public static void main(String[] args) {
Set<String> set = new LinkedHashSet<String>();
set.add("bbb");
set.add("aaa");
set.add("abc");
set.add("bbc");
//迭代器取出
Iterator<String> it = set.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
}
输出结果
bbb
aaa
abc
bbc