Java基础加强重温_05:Iterator迭代器、增强for循环、集合综合案例-斗地主、数据结构(栈、队列、数组、链表、红黑树)、List接口、Set接口

摘要:

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
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值