集合

集合的关注点

1.是否允许为空

2.是否允许重复数据

3.是否有序(有序的意思是读取数据的顺序和存放数据的顺序是否一致)

4.是否线程安全

image-20230927191717289

image-20230927191631158

Collection集合

集合概述

在前面基础班我们已经学习过并使用过集合ArrayList<E> ,那么集合到底是什么呢?

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

集合和数组既然都是容器,它们有什么区别呢?

  • 数组的长度是固定的。集合的长度是可变的。
  • 集合存储的都是引用数据类型。如果想存储基本类型数据需要存储对应的包装类类型。

单列集合常用类的继承体系

Collection:是单列集合类的根接口,用于存储一系列符合某种规则的元素,它有两个重要的子接口,分别是

  • java.util.List: List的特点是元素有序、元素可重复 ;
    • List接口的主要实现类有java.util.ArrayListjava.util.LinkedList
  • java.util.Set: Set的特点是元素不可重复
    • Set接口的主要实现类有java.util.HashSetjava.util.LinkedHashSetjava.util.TreeSet

为了便于初学者进行系统地学习,接下来通过一张图来描述集合常用类的继承体系

image-20220709082818693

注意:上面这张图只是我们常用的集合有这些,不是说就只有这些集合。

1
2
3
4
5
6
7
8
9
10
单列集合常用类的继承体系:
Collection集合:接口,是所有单列集合的顶层父接口,该集合中的方法可以被所有单列集合共享
List集合: 接口,元素可重复,元素有索引,元素存取有序
ArrayList集合: 实现类,查询快,增删慢
LinkedList集合: 实现类,查询慢,增删快

Set集合: 接口, 元素不可重复(唯一),元素无索引
HashSet集合: 实现类,元素存取无序
LinkedHashSet集合:实现类,元素存取有序
TreeSet集合:实现类,可以对集合中的元素进行排序

Collection 常用功能

Collection是所有单列集合的父接口,因此在Collection中定义了单列集合(List和Set)通用的一些方法,这些方法可用于操作所有的单列集合。方法如下:

  • public boolean add(E e): 把给定的对象添加到当前集合中 。
  • public void clear() :清空集合中所有的元素。
  • public boolean remove(E e): 把给定的对象在当前集合中删除。
  • public boolean contains(Object obj): 判断当前集合中是否包含给定的对象。
  • public boolean isEmpty(): 判断当前集合是否为空。
  • public int size(): 返回集合中元素的个数。
  • public Object[] toArray(): 把集合中的元素,存储到数组中

tips: 有关Collection中的方法可不止上面这些,其他方法可以自行查看API学习。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Test {
public static void main(String[] args) {
// 创建Collection集合对象,限制集合中元素的类型为String
Collection<String> col = new ArrayList<>();

// 往col集合中添加元素
col.add("范冰冰");
col.add("李冰冰");
col.add("林心如");
col.add("赵薇");

System.out.println("col集合:"+col);// col集合:[范冰冰, 李冰冰, 林心如, 赵薇]

// 清空集合中所有的元素
//col.clear();
//System.out.println("col集合:"+col);// col集合:[]

// 删除李冰冰这个元素
col.remove("李冰冰");
System.out.println("col集合:"+col);// col集合:[范冰冰, 林心如, 赵薇]

// 判断col集合中是否包含李冰冰这个元素
boolean res1 = col.contains("李冰冰");
System.out.println("res1:"+res1);// false
// 判断col集合中是否包含林心如这个元素
boolean res2 = col.contains("林心如");
System.out.println("res2:"+res2);// true

//判断当前集合是否为空。(判断集合中是否有元素)
boolean res3 = col.isEmpty();
System.out.println("res3:"+res3);// false
/*col.clear();// 清空元素
boolean res4 = col.isEmpty();
System.out.println("res4:"+res4);// true*/

// 获取集合中元素的个数
System.out.println("集合中元素的个数:"+col.size());// 3

// 把集合中的元素,存储到数组中
Object[] arr = col.toArray();
System.out.println(Arrays.toString(arr));// [范冰冰, 林心如, 赵薇]
}
}

Iterator迭代器

为什么用迭代器?

set集合没有索引,所以不能通过普通for循环遍历拿到数据,所以对于单列集合通用的遍历方式:迭代器

Iterator接口

在程序开发中,经常需要遍历单列集合中的所有元素。针对这种需求,JDK专门提供了一个接口java.util.Iterator

迭代的概念

迭代:即Collection集合元素的通用获取方式。在取元素之前先要判断集合中有没有元素,如果有,就把这个元素取出来,继续再判断,如果还有就再取出来。一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。

获取迭代器对象

Collection集合提供了一个获取迭代器的方法:

  • public Iterator iterator(): 获取集合对应的迭代器,用来遍历集合中的元素的。

Iterator接口的常用方法

  • public E next():返回迭代的下一个元素。
  • public boolean hasNext():如果仍有元素可以迭代,则返回 true。
案例演示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.nbchen.demo3_Iterator接口;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class Test {
public static void main(String[] args) {
/*
迭代:即Collection集合元素的通用获取方式。
在取元素之前先要判断集合中有没有元素,
如果有,就把这个元素取出来,继续再判断,如果还有就再取出来。
一直把集合中的所有元素全部取出。这种取出方式专业术语称为迭代。
获取迭代器对象: 使用Collection集合中的iterator()方法
public Iterator<E> iterator();

判断集合中是否有元素可以迭代: 使用Iterator接口中的方法
public boolean hasNext();

取出集合中可以迭代的元素: 使用Iterator接口中的方法
public E next();

*/
// 创建Collection集合对象,限制集合中元素的类型为String
Collection<String> col = new ArrayList<>();

// 往col集合中添加元素
col.add("范冰冰");
col.add("李冰冰");
col.add("林心如");
col.add("赵薇");

// 获取迭代器对象
Iterator<String> it = col.iterator();

// 循环判断集合中是否有元素可以迭代
while (it.hasNext()){
// 说明有元素可以迭代
String e = it.next();
System.out.println(e);
}
}
}

迭代器的常见问题

常见问题一

在进行集合元素获取时,如果集合中已经没有元素可以迭代了,还继续使用迭代器的next方法,将会抛出java.util.NoSuchElementException没有集合元素异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
package com.nbchen.demo4_迭代器的常见问题;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class Test1 {
public static void main(String[] args) {
/*
迭代器的常见问题 :
问题一:在进行集合元素获取时,如果集合中已经没有元素可以迭代了,还继续使用迭代器的next方法,
将会抛出java.util.NoSuchElementException没有集合元素异常。
*/
// 创建Collection集合对象,限制集合中元素的类型为String
Collection<String> col = new ArrayList<>();

// 往col集合中添加元素
col.add("范冰冰");
col.add("李冰冰");
col.add("林心如");
col.add("赵薇");

// 获取集合的迭代器对象
Iterator<String> it = col.iterator();

// 循环判断集合中是否有元素可以迭代
while (it.hasNext()) {
// 获取可以迭代的元素
String e = it.next();
System.out.println(e);
}

System.out.println("====================================");

// 再获取集合中的元素
//String next = it.next();// 运行异常NoSuchElementException
// 如果迭代完了,还想继续迭代集合元素,就可以重新再获取一个迭代器
Iterator<String> it2 = col.iterator();
while (it2.hasNext()) {
System.out.println(it2.next());
}
}
}

解决办法: 如果还需要重新迭代,那么就重新获取一个新的迭代器对象进行操作

常见问题二

在进行集合元素迭代时,如果添加或移除集合中的元素 , 将无法继续迭代 , 将会抛出ConcurrentModificationException并发修改异常.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package com.itheima.demo4_迭代器的常见问题;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

/**
* @Author:pengzhilin
* @Date: 2020/9/13 9:46
*/
public class Test2 {
public static void main(String[] args) {
/*
迭代器的常见问题:
问题二:在进行集合元素迭代时,如果添加或移除集合中的元素 ,
将无法继续迭代 , 将会抛出ConcurrentModificationException并发修改异常.
*/
// 创建Collection集合对象,限制集合中元素的类型为String
Collection<String> col = new ArrayList<>();

// 往col集合中添加元素
col.add("范冰冰");
col.add("李冰冰");
col.add("林心如");
col.add("赵薇");

// 获取集合的迭代器对象
Iterator<String> it = col.iterator();

// 循环判断集合中是否有元素可以迭代
while (it.hasNext()) {
// 获取可以迭代的元素
String e = it.next();
System.out.println(e);
// 添加元素到集合中
//col.add("高圆圆");// 报异常
// 删除元素
//col.remove(e);// 报异常
// 如果迭代出来的元素是李冰冰,就删除
if (e.equals("李冰冰")){
it.remove();
}
}

System.out.println("集合:"+col);
}
}

迭代器的实现原理

​ 我们在之前案例已经完成了Iterator遍历集合的整个过程。当遍历集合时,首先通过调用t集合的iterator()方法获得迭代器对象,然后使用hashNext()方法判断集合中是否存在下一个元素,如果存在,则调用next()方法将元素取出,否则说明已到达了集合末尾,停止遍历元素。

​ Iterator迭代器对象在遍历集合时,内部采用指针的方式来跟踪集合中的元素。在调用Iterator的next方法之前,迭代器的索引位于第一个元素之前,不指向任何元素,当第一次调用迭代器的next方法后,迭代器的索引会向后移动一位,指向第一个元素并将该元素返回,当再次调用next方法时,迭代器的索引会指向第二个元素并将该元素返回,依此类推,直到hasNext方法返回false,表示到达了集合的末尾,终止对元素的遍历。

image-20220709083233103

增强for

增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作,增删操作影响指针。

其实是语法糖,idea查看out反编译class文件,可以看到实际操作。数组的增强for底层是普通for集合的增强for递增是迭代器

格式:

1
2
3
for(元素的数据类型  变量 : Collection集合or数组){ 
//写操作代码
}

它用于遍历Collection和数组。通常只进行遍历元素,不要在遍历的过程中对集合元素进行增删操作。

代码演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package com.nbchen.demo5_增强for循环;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

public class Test {
public static void main(String[] args) {
/*
概述:增强for循环(也称for each循环)是JDK1.5以后出来的一个高级for循环,专门用来遍历数组和集合的。
它的内部原理其实是个Iterator迭代器,所以在遍历的过程中,不能对集合中的元素进行增删操作。
格式:
for(元素的数据类型 变量名 : 数组名\集合名){

}
*/
// 创建Collection集合对象,限制集合中元素的类型为String
Collection<String> col = new ArrayList<>();

// 往col集合中添加元素
col.add("范冰冰");
col.add("李冰冰");
col.add("林心如");
col.add("赵薇");

// 增强for循环遍历
for (String e : col) {
System.out.println(e);
}

System.out.println("======================================");

String[] arr = {"范冰冰",
"李冰冰",
"林心如",
"赵薇"};

for (String e : arr){
System.out.println(e);
}

System.out.println("======================================");
// 增强for循环快捷键: 数组名\集合名.for
for (String s : col) {
System.out.println(s);
}
System.out.println("=======================================");
for (String s : arr) {
System.out.println(s);
}

System.out.println("=======================================");
Iterator<String> it = col.iterator();
// 迭代器快捷键: itit 回车
while (it.hasNext()) {
String next = it.next();
System.out.println(next);
}

System.out.println("=======================================");
// 在遍历的过程中,不能对集合中的元素进行增删操作。
/*for (String s : col) {
if (s.equals("李冰冰")) {
col.remove(s);
}
}*/
}
}

tips:

增强for循环必须有被遍历的目标,目标只能是Collection或者是数组;

增强for(迭代器)仅仅作为遍历操作出现,不能对集合进行增删元素操作,否则抛出ConcurrentModificationException并发修改异常

泛型

泛型的作用

集合不使用泛型的时候,存的时候什么类型都能存。但是取的时候就懵逼了。取出来啥也不是。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class Test1 {
public static void main(String[] args) {
/*
泛型的作用:
- 集合不使用泛型: 集合不使用泛型的时候,存的时候什么类型都能存。但是取的时候就懵逼了。取出来啥也不是。
*/
// 集合不使用泛型
// 创建ArrayList集合对象
ArrayList list1 = new ArrayList();

// 往集合中添加元素
list1.add("杨颖");
list1.add("迪丽热巴");
list1.add(100);
list1.add(3.14);
System.out.println(list1);

// 循环遍历集合元素
for (Object obj : list1) {
// 在循环中,获取姓名的长度,打印输出
String name = (String)obj;// 很容易出现类型转换异常
System.out.println("姓名的长度:"+name.length());
}
}
}

使用泛型:使用泛型在编译期直接对类型作出了控制,只能存储泛型定义的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.nbchen.demo6_泛型的作用;

import java.util.ArrayList;

public class Test2 {
public static void main(String[] args) {
/*
泛型的作用:
- 集合使用泛型:使用泛型在编译期直接对类型作出了控制,只能存储泛型定义的数据
*/
// 集合使用泛型
// 创建ArrayList集合对象,限制集合中元素的类型为String
ArrayList<String> list1 = new ArrayList<>();

// 往集合中添加元素
list1.add("杨颖");
list1.add("迪丽热巴");
//list1.add(100);// 编译报错
//list1.add(3.14);// 编译报错
System.out.println(list1);

// 循环遍历集合元素
for (String s : list1) {
System.out.println(s.length());
}
}
}

  • 泛型:定义的时候表示一种未知的数据类型,在使用的时候确定其具体的数据类型。

tips:泛型的作用是在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为Object类型。

定义和使用含有泛型的类

定义含有泛型的类

定义格式:

1
2
修饰符 class 类名<代表泛型的变量> {  }
代表泛型的变量: 可以是任意字母 例如: T,E...

泛型在定义的时候不具体类型,使用的时候才具体类型。在使用的时候确定泛型的具体数据类型。

1
2
3
4
5
6
class ArrayList<E>{ 
public boolean add(E e){ }

public E get(int index){ }
....
}

确定泛型具体类型

在创建对象的时候确定泛型

例如,ArrayList<String> list = new ArrayList<String>();

此时,变量E的值就是String类型,那么我们的类型就可以理解为:

1
2
3
4
5
6
class ArrayList<String>{ 
public boolean add(String e){ }

public String get(int index){ }
...
}

课堂代码

  • 定义含有泛型的类
1
2
3
4
5
6
7
8
9
10
11
12
package com.nbchen.demo7_定义和使用含有泛型的类;

public class MyArrayList<E> {

E e;

public E method(E e){
return e;
}

}

  • 使用含有泛型的类—–>掌握
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.nbchen.demo7_定义和使用含有泛型的类;

public class Test {
public static void main(String[] args) {
/*
定义含有泛型的类:
public class 类名<泛型变量>{

}
泛型变量的位置: 写任意字母,例如:A,B,C,D,E,...a,b,c,...一般会写E

使用含有泛型的类: 创建该类对象的时候,确定该类泛型的具体数据类型

什么时候定义泛型的类:
当类中的成员变量或者成员方法的形参类型\返回值类型不确定的时候,就可以把该类定义为含有泛型的类
*/
MyArrayList<String> list1 = new MyArrayList<>();
list1.e = "itheima";
String res1 = list1.method("itcast");
System.out.println("res1:"+res1);// itcast

System.out.println("=======================================");

MyArrayList<Integer> list2 = new MyArrayList<>();
list2.e = 100;
Integer res2 = list2.method(10);
System.out.println("res2:"+res2);// 10

}
}

定义和使用含有泛型的方法

1
修饰符 <代表泛型的变量> 返回值类型 方法名(参数){  }

例如

1
2
3
4
5
6
public class Test {
// 定义含有泛型的方法
public static <T> T method1(T t){
return t;
}
}

确定泛型具体类型

调用方法时,确定泛型的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class Test {
public static void main(String[] args) {
/*
定义含有泛型的方法:
修饰符 <泛型变量> 返回值类型 方法名(形参列表){
方法体
}
泛型变量: 任意字母 一般会写T,M,...

使用含有泛型的方法: 调用含有泛型方法的时候确定其泛型的具体数据类型

什么时候会定义含有泛型的方法:
如果一个类中,某个方法的参数类型或者返回值类型不确定的时候,可以把该方法定义为含有泛型的方法
*/
Integer i1 = method1(100);// 指定泛型的具体数据类型为Integer
System.out.println(i1);// 100

System.out.println("============================");
String s = method1("itheima");// 指定泛型的具体数据类型为String
System.out.println(s);// itheima
}

// 定义含有泛型的方法
public static <T> T method1(T t){
return t;
}
}

定义和使用含有泛型的接口

1
修饰符 interface接口名<代表泛型的变量> {  }

例如

1
2
3
4
5
6
7
8
public interface IA<E> {

public abstract void method1(E e);

public default E method2(E e){
return e;
}
}

确定泛型具体类型

使用格式:

1、定义实现类时确定泛型的类型

例如

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.nbchen.demo09_定义和使用含有泛型的接口;

// 通过实现类的方式确定接口泛型的具体数据类型
public class Imp1 implements IA<String> {
@Override
public void method1(String s) {

}

@Override
public String method2(String s) {
return null;
}
}

此时,泛型E的值就是String类型。

2、始终不确定泛型的类型,直到创建对象时,确定泛型的类型

  • 实现类实现接口:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.nbchen.demo09_定义和使用含有泛型的接口;

// 实现类实现接口的时候不确定接口泛型的具体数据类型,
// 而是创建实现类对象的时候确定接口泛型的具体数据类型
public class Imp2<E> implements IA<E> {
@Override
public void method1(E e) {
System.out.println("实现类 method1");
}

@Override
public E method2(E e) {
return e;
}
}

确定泛型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.nbchen.demo09_定义和使用含有泛型的接口;


public class Test {
public static void main(String[] args) {
/*
定义含有泛型的接口:
public interface 接口名<泛型变量>{

}

泛型变量:任意字母,一般可以使用E

使用含有泛型的接口: 确定接口泛型的具体数据类型
1.通过实现类的方式确定接口泛型的具体数据类型
public class 类名 implements 接口名<具体的数据类型>{

}

2.实现类实现接口的时候不确定接口泛型的具体数据类型,
而是创建实现类对象的时候确定接口泛型的具体数据类型
public class 类名<泛型变量> implements 接口名<泛型变量>{

}
*/
// 创建实现类对象的时候确定接口泛型的具体数据类型
Imp2<String> imp1 = new Imp2<>();
imp1.method1("itheima");
String s1 = imp1.method2("itcast");
System.out.println(s1);// itcast

System.out.println("==========================");
Imp2<Integer> imp2 = new Imp2<>();
imp2.method1(100);
Integer i = imp2.method2(100);
System.out.println(i);// 100

}
}
1
2
3
4
5
6
泛型:定义的时候表示一种未知的数据类型,在使用的时候确定其具体的数据类型。
使用含有泛型的类: 创建该类对象的时候,指定泛型的具体数据类型
使用含有方向的方法: 调用该方法的时候,确定泛型的具体数据类型
使用含有泛型的接口:
1.创建实现类实现接口的时候,指定泛型的具体数据类型
2.创建实现类实现接口的时候,不知道泛型的具体数据类型,而是创建实现类对象的时候指定泛型的具体数据类型

泛型通配符

通配符基本使用

泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。

此时只能接受数据,不能往该集合中存储数据。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class Test {
public static void main(String[] args) {
/*
通配符基本使用:
泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?,?表示未知通配符。
注意: 不能往该集合中存储数据,只能获取数据.
*/
// 关系:String继承Object,Integer继承Number,Number继承Objec
ArrayList<Object> list1 = new ArrayList<>();
ArrayList<String> list2 = new ArrayList<>();
ArrayList<Integer> list3 = new ArrayList<>();
ArrayList<Number> list4 = new ArrayList<>();

list2.add("itheima");

//method1(list1);
method1(list2);
//method1(list3);
//method1(list4);

//method2(list1);
method2(list2);
//method2(list3);
//method2(list4);

// 泛型没有多态
//ArrayList<Object> list = new ArrayList<String>();// 编译报错
}
// 定义一个方法,可以接收以上4个集合
public static void method1(ArrayList list){
Object obj = list.get(0);
list.add("jack");
System.out.println("obj:"+obj);// itheima
System.out.println("list:"+list);// [itheima, jack]
}

public static void method2(ArrayList<?> list){
Object obj = list.get(0);
//list.add("jack");// 编译报错
System.out.println("obj:"+obj);// itheima
System.out.println("list:"+list);// [itheima]
}
}

通配符高级使用:受限泛型

之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的上限下限

泛型的上限

  • 格式类型名称 <? extends 类 > 对象名称
  • 意义只能接收该类型及其子类

泛型的下限

  • 格式类型名称 <? super 类 > 对象名称
  • 意义只能接收该类型及其父类型

比如:现已知Object类,String 类,Number类,Integer类,其中Number是Integer的父类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Test {
public static void main(String[] args) {
/*
通配符高级使用----受限泛型:
上限: <? extends 类名> 只能接收该类类型或者其子类类型
下限: <? super 类名> 只能接收该类类型或者其父类类型
*/
// 关系:String继承Object,Integer继承Number,Number继承Objec
ArrayList<Object> list1 = new ArrayList<>();
ArrayList<String> list2 = new ArrayList<>();
ArrayList<Integer> list3 = new ArrayList<>();
ArrayList<Number> list4 = new ArrayList<>();

method1(list1);
method1(list2);
method1(list3);
method1(list4);

//method2(list1);// 编译报错
//method2(list2);// 编译报错
method2(list3);
method2(list4);


method3(list1);
//method3(list2);// 编译报错
method3(list3);
method3(list4);
}

// 定义一个方法,只可以接收以上list3和list4集合
public static void method2(ArrayList<? extends Number> list){

}

// 定义一个方法,只可以接收以上list3和list4,list1集合
public static void method3(ArrayList<? super Integer> list){

}

// 定义一个方法,可以接收以上4个集合
public static void method1(ArrayList<?> list){

}
// 定义一个方法,可以接收以上4个集合
public static void method(ArrayList list){

}
}

List接口

List接口介绍

我们掌握了Collection接口的使用后,再来看看Collection接口中的子类,他们都具备那些特性呢?

接下来,我们一起学习Collection中的常用几个子类(java.util.List集合、java.util.Set集合)。

List接口的概述

java.util.List接口继承自Collection接口,是单列集合的一个重要分支,习惯性地会将实现了List接口的对象称为List集合。

List接口特点

  1. 它是一个元素存取有序的集合。例如,存元素的顺序是11、22、33。那么集合中,元素的存储就是按照11、22、33的顺序完成的)。
  2. 它是一个带有索引的集合,通过索引就可以精确的操作集合中的元素(与数组的索引是一个道理)。
  3. 集合中可以有重复的元素。

tips:我们在基础班的时候已经学习过List接口的子类java.util.ArrayList类,该类中的方法都是来自List中定义。

List接口中常用方法

List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法

List接口新增常用方法

List作为Collection集合的子接口,不但继承了Collection接口中的全部方法,而且还增加了一些根据元素索引来操作集合的特有方法,如下:

  • 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集合特有的方法都是跟索引相关,我们在基础班都学习过。

List接口新增常用方法的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
public class Test {
public static void main(String[] args) {
/*
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集合,限制集合中元素的类型为String类型
List<String> list = new ArrayList<>();

// 往集合中添加一些元素
list.add("苍老师");
list.add("波老师");
list.add("吉泽老师");
System.out.println(list);// [苍老师, 波老师, 吉泽老师]

// 在索引为1的位置添加小泽老师
list.add(1, "小泽老师");
System.out.println(list);// [苍老师, 小泽老师, 波老师, 吉泽老师]

// 获取索引为1的元素
System.out.println("索引为1的元素:"+list.get(1));// 小泽老师

// 删除索引为1的老师
String removeE = list.remove(1);
System.out.println("被删除的元素:"+removeE);// 小泽老师
System.out.println(list);// [苍老师, 波老师, 吉泽老师]

// 把索引为0的元素替换为大桥老师
String setE = list.set(0, "大桥老师");
System.out.println("被替换的元素:"+setE);// 苍老师
System.out.println(list);// [大桥老师, 波老师, 吉泽老师]
}
}

List的子类

ArrayList集合

java.util.ArrayList集合数据存储的结构是数组结构。元素增删慢,查找快,由于日常开发中使用最多的功能为查询数据、遍历数据,所以ArrayList是最常用的集合。

许多程序员开发时非常随意地使用ArrayList完成任何需求,并不严谨,这种用法是不提倡的。

原理:底层实现是Object[]数组

  • 存储
1
Transient Object[] elementData;

只能存储Object类型或者他的子类,引用类型只能存引用类型(除了包装类自动拆箱)。所以,ArrayList存不了int基本类型。ArrayList<int>是错的

  • 删除
1
System.arraycopy(es,i+1,es,i,newSize-i);

将数组后面的数组往前移动一位,进行覆盖

ArrayList源码:

1.为什么ArrayList源码中要用成员内部类表示迭代器的实现子类?从调用的时候封装具体内容方面理解这种定义的好处吗?
内部类,因为只在这个类里面用,定义在外面别的类也用不到,就更private封装类似。

调用:
外部类名.内部类名 对象名 = new 外部类型().new 内部类型();
Collection<String> coll=newArrayList<>();
Iterator<String> it=coll.iterator();

2.为什么要复制给局部变量,直接操作有什么弊端?

1
Object[]elementData=ArrayList.this.elementData;

这里存的是地址值,直接用也是可以的

LinkedList集合

java.util.LinkedList集合数据存储的结构是链表结构。方便元素添加、删除的集合。查询慢,增删快

LinkedList是一个双向链表,那么双向链表是什么样子的呢,我们用个图了解下

image-20220709085631208

实际开发中对一个集合元素的添加与删除经常涉及到首尾操作,而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):将元素推入此列表所表示的堆栈。

Pop(弹栈)和push(压栈)就是调用的removeFirst和addFirst

LinkedList是List的子类,List中的方法LinkedList都是可以使用,这里就不做详细介绍,我们只需要了解LinkedList的特有方法即可。在开发时,LinkedList集合也可以作为堆栈,队列的结构使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public class Test {
public static void main(String[] args) {
/*
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():从此列表所表示的堆栈处弹出一个元素。 removeFirst()
- public void push(E e):将元素推入此列表所表示的堆栈。addFirst()
*/
// 创建LinkedList集合,限制集合元素的类型为String类型
LinkedList<String> list = new LinkedList<>();

// 往集合中添加元素
list.add("蔡徐坤");
list.add("鹿晗");
list.add("吴亦凡");
System.out.println(list);// [蔡徐坤, 鹿晗, 吴亦凡]

// 在集合的首尾添加一个元素
list.addFirst("罗志祥");
list.addLast("陈冠希");
System.out.println(list);// [罗志祥, 蔡徐坤, 鹿晗, 吴亦凡, 陈冠希]

// 获取集合的首尾元素
String firstE = list.getFirst();
String lastE = list.getLast();
System.out.println("第一个元素是:"+firstE);// 罗志祥
System.out.println("最后一个元素是:"+lastE);// 陈冠希

// 删除首尾元素
String removeFirst = list.removeFirst();
String removeLast = list.removeLast();
System.out.println("被删除的第一个元素是:"+removeFirst);// 罗志祥
System.out.println("被删除的最后一个元素是:"+removeLast);// 陈冠希
System.out.println(list);// [蔡徐坤, 鹿晗, 吴亦凡]

// pop --->删除第一个元素
String popE = list.pop();
System.out.println("被删除的第一个元素是:"+popE);// 蔡徐坤
System.out.println(list);// [鹿晗, 吴亦凡]

// push --->添加一个元素在开头
list.push("蔡徐坤");
System.out.println(list); // [蔡徐坤, 鹿晗, 吴亦凡]
}
}

集合综合案例

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

使用54张牌打乱顺序,三个玩家参与游戏,三人交替摸牌,每人17张牌,最后三张留作底牌。

分析

  • 准备牌:

    牌可以设计为一个ArrayList<String>,每个字符串为一张牌。
    每张牌由花色数字两部分组成,我们可以使用花色集合与数字集合嵌套迭代完成每张牌的组装。
    牌由Collections类的shuffle方法进行随机排序。

  • 发牌

    将每个人以及底牌设计为ArrayList<String>,将最后3张牌直接存放于底牌,剩余牌通过对3取模依次发牌。

  • 看牌

    直接打印每个集合。

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
import java.util.ArrayList;
import java.util.Collections;

public class Test {
public static void main(String[] args) {
// 1.造牌:
// 1.1 创建一个pokerBox集合,用来存储54张扑克牌
ArrayList<String> pokerBox = new ArrayList<>();

// 1.2 创建一个ArrayList牌面值集合,用来存储13个牌面值
ArrayList<String> numbers = new ArrayList<>();

// 1.3 创建一个ArrayList花色集合,用来存储4个花色
ArrayList<String> colors = new ArrayList<>();

// 1.4 往牌面值集合中添加13个牌面值
numbers.add("A");
numbers.add("K");
numbers.add("Q");
numbers.add("J");
for (int i = 2; i <= 10; i++) {
numbers.add(i + "");
}

// 1.5 往花色集合中添加4个花色
colors.add("♥");
colors.add("♠");
colors.add("♣");
colors.add("♦");

// 1.6 添加大小王到存储到pokerBox集合中
pokerBox.add("大王");
pokerBox.add("小王");

// 1.7 花色集合和牌面值集合,循环嵌套
for (String number : numbers) {
for (String color : colors) {
// 1.8 在循环里面创建牌,并添加到pokerBox集合中
String pai = color + number;
pokerBox.add(pai);
}
}
// 1.9 打印pokerBox集合
System.out.println(pokerBox);
System.out.println(pokerBox.size());

// 2.洗牌:
// 使用Collections工具类的静态方法
// public static void shuffle(List<?> list)
// 打乱集合元素的顺序
Collections.shuffle(pokerBox);
System.out.println("打乱顺序后:" + pokerBox);
System.out.println("打乱顺序后:" + pokerBox.size());

// 3.发牌
// 3.1 创建4个ArrayList集合,分别用来存储玩家1,玩家2,玩家3,底牌的牌
ArrayList<String> play1 = new ArrayList<>();
ArrayList<String> play2 = new ArrayList<>();
ArrayList<String> play3 = new ArrayList<>();
ArrayList<String> diPai = new ArrayList<>();

// 3.2 循环遍历打乱顺序之后的牌
for (int i = 0; i < pokerBox.size(); i++) {
// 3.3 在循环中,获取遍历出来的牌
String pai = pokerBox.get(i);
// 3.4 在循环中,判断遍历出来的牌:
if (i >= 51) {
// 3.5 如果该牌的索引是51,52,53,给底牌
diPai.add(pai);
} else if (i % 3 == 0) {
// 3.5 如果该牌的索引%3==0,给玩家1
play1.add(pai);
} else if (i % 3 == 1) {
// 3.5 如果该牌的索引%3==1,给玩家2
play2.add(pai);
} else if (i % 3 == 2) {
// 3.5 如果该牌的索引%3==2,给玩家3
play3.add(pai);
}
}
// 3.6 打印各自的牌
System.out.println("玩家1:"+play1+",牌数:"+play1.size());
System.out.println("玩家2:"+play2+",牌数:"+play2.size());
System.out.println("玩家3:"+play3+",牌数:"+play3.size());
System.out.println("底牌:"+diPai);
}
}

Collections类

Collections常用功能

  • java.utils.Collections是集合工具类,用来对集合进行操作。

    常用方法如下:

  • public static void shuffle(List<?> list) :打乱集合顺序。

  • public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。

  • public static <T> void sort(List<T> list,Comparator<? super T> ):将集合中元素按照指定规则排序。

自定义规则:

1
2
3
4
5
6
7
8
1.对象必须实现Comparable接口:默认排序,较死板
2.排序时实现comparator比较器:compare(String o1,String o2);

升序:我-它
o1 < o2,返回负数,相等返回0,o1 > o2返回正数

降序:
它-我o1 < o2,返回正数,相等返回0,o1 > o2返回负数
  • shuffle方法代码演示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class Test1_shuffle {
public static void main(String[] args) {
/*
Collections常用功能:
public static void shuffle(List<?> list):打乱集合顺序。
*/
// 创建List集合,限制集合中元素的类型为Integer类型
List<Integer> list = new ArrayList<>();

// 往集合中添加元素
list.add(300);
list.add(100);
list.add(200);
list.add(500);
list.add(400);

System.out.println("打乱顺序之前的集合:"+list);// [300, 100, 200, 500, 400]
// 打乱顺序
Collections.shuffle(list); // 随机打乱顺序
System.out.println("打乱顺序之后的集合:"+list);// [500, 300, 100, 200, 400]

}
}

  • sort方法代码演示:按照默认规则排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class Student implements Comparable<Student>{
int age;

public Student(int age) {
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"age=" + age +
'}';
}

@Override
public int compareTo(Student o) {
// 指定排序规则
// 前减后 升序
// 后减前 降序
// 前:this 后: 参数o
return this.age - o.age;// 升序
}
}



public class Test2_sort {
public static void main(String[] args) {
/*
Collections常用功能:
public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。
默认规则: 事先写好的规则
排序规则: 集合元素所属的类一定要实现Comparable接口,重写compareTo方法,在compareTo方法中指定排序规则
*/
// 创建List集合,限制集合中元素的类型为Integer类型
List<Integer> list = new ArrayList<>();

// 往集合中添加元素
list.add(300);
list.add(100);
list.add(200);
list.add(500);
list.add(400);

System.out.println("排序之前的集合:"+list); // [300, 100, 200, 500, 400]
// 将集合中元素按照默认规则排序
Collections.sort(list);
System.out.println("排序之后的集合:"+list); // [100, 200, 300, 400, 500]

System.out.println("=========================================");

// 创建List集合,限制集合中元素的类型为Student类型
List<Student> list1 = new ArrayList<>();

// 往集合中添加元素
Student stu1 = new Student(19);
Student stu2 = new Student(18);
Student stu3 = new Student(20);
Student stu4 = new Student(17);
list1.add(stu1);
list1.add(stu2);
list1.add(stu3);
list1.add(stu4);
System.out.println("排序之前的集合:"+list1);
// 将集合中元素按照默认规则排序
Collections.sort(list1);
System.out.println("排序之后的集合:"+list1);


}
}

  • sort方法代码块演示: 指定规则排序
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class Test3_sort {
public static void main(String[] args) {
/*
Collections常用功能:
public static <T> void sort(List<T> list,Comparator<? super T> com):将集合中元素按照指定规则排序。
参数Comparator: 就是用来指定排序规则的
通过Comparator接口中的compare方法来指定排序规则
*/
// 创建List集合,限制集合中元素的类型为Integer类型
List<Integer> list = new ArrayList<>();

// 往集合中添加元素
list.add(300);
list.add(100);
list.add(200);
list.add(500);
list.add(400);

System.out.println("排序之前的集合:" + list); // [300, 100, 200, 500, 400]
// 将集合中元素按照指定规则排序---->降序
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
// 指定排序规则
// 前减后 升序
// 后减前 降序
// 前: 第一个参数o1 后:第二个参数o2
return o2 - o1;
}
});
System.out.println("排序之后的集合:" + list); // [500, 400, 300, 200, 100]

// 将集合中元素按照指定规则排序---->升序
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
System.out.println("排序之后的集合:" + list);// [100, 200, 300, 400, 500]

System.out.println("=======================================");

// 创建List集合,限制集合中元素的类型为Student类型
List<Student> list1 = new ArrayList<>();

// 往集合中添加元素
Student stu1 = new Student(19);
Student stu2 = new Student(18);
Student stu3 = new Student(20);
Student stu4 = new Student(17);
list1.add(stu1);
list1.add(stu2);
list1.add(stu3);
list1.add(stu4);
System.out.println("排序之前的集合:" + list1);
// 将集合中元素按照指定规则排序-->按照年龄降序排序
Collections.sort(list1, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 指定排序规则
// 前减后 升序
// 后减前 降序
// 前: 第一个参数o1 后:第二个参数o2
return o2.age - o1.age;
}
});
System.out.println("排序之后的集合:" + list1);

// 将集合中元素按照指定规则排序-->按照年龄升序排序
Collections.sort(list1, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 指定排序规则
// 前减后 升序
// 后减前 降序
// 前: 第一个参数o1 后:第二个参数o2
return o1.age - o2.age;
}
});
System.out.println("排序之后的集合:" + list1);
}
}

小结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- public static void shuffle(List<?> list):打乱集合顺序。
- public static <T> void sort(List<T> list):将集合中元素按照默认规则排序。
默认规则: 事先写好的排序规则
在哪里写好排序规则?---->集合元素所属的类中写好排序规则(通过实现Comparable接口,重写compareTo(T o)方法写好排序规则)
排序规则:
前减后 升序
后减前 降序
前: this 后:参数

- public static <T> void sort(List<T> list,Comparator<? super T> com):将集合中元素按照指定规则排序。
指定规则排序: 通过Comparator参数来指定
通过传入Comparator接口的匿名内部类,重写compare(T o1,T o2)方法,在该方法中指定排序规则
排序规则:
前减后 升序
后减前 降序
前: 第一个参数 后:第二个参数

可变参数

可变参数的使用

JDK1.5之后,如果我们定义一个方法需要接受多个参数,并且多个参数类型一致,我们可以对其简化.

格式:

1
修饰符 返回值类型 方法名(参数类型... 形参名){  }

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Test1 {
public static void main(String[] args) {
/*
可变参数:
概述:在JDK1.5之后,如果我们定义一个方法需要接受多个参数,并且多个参数类型一致,我们可以对其简化.
格式:
修饰符 返回值类型 方法名(数据类型... 变量名){}

*/
/* method1(10,20,30,40,50);

int[] arr = {10,20,30,40,50};
method2(arr);*/

/*method3();
method3(10,20,30,40);*/
method3(10,20,30,40,50);

/* int[] arr = {10,20,30,40,50};
method3(arr);*/
}

// 定义一个方法,可以接收5个int类型的数
public static void method3(int... nums){
// 使用:把nums可变参数当成数组使用
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}
}

// 定义一个方法,可以接收5个int类型的数
public static void method2(int[] arr){// 接收数组
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}

// 定义一个方法,可以接收5个int类型的数
public static void method1(int num1,int num2,int num3,int num4,int num5){// 接收5个具体的数

}

}

注意事项

​ 1.一个方法只能有一个可变参数

​ 2.如果方法中有多个参数,可变参数要放到最后。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.itheima.demo2_可变参数;

/**
* @Author:pengzhilin
* @Date: 2020/9/15 9:55
*/
public class Test2 {
public static void main(String[] args) {
/*
可变注意事项:
1.一个方法,只能有一个可变参数
2.如果方法有多个参数,可变参数一定要放在末尾
*/
// method1(10,20,"itheima");
// method2(10,20,"itheima");
method3("itheima",10,20);
}

// 编译报错,因为一个方法,只能有一个可变参数
/*public static void method1(int... nums,String... strs){

}*/

// 编译报错,因为如果方法有多个参数,可变参数一定要放在末尾
/*public static void method2(int... nums,String str){

}*/

public static void method3(String str,int... nums){

}
}

应用场景: Collections

​ 在Collections中也提供了添加一些元素方法:

public static <T> boolean addAll(Collection<T> c, T... elements) :往集合中添加一些元素。

代码演示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Test3 {
public static void main(String[] args) {
/*
应用场景: Collections
在Collections中也提供了添加一些元素方法:
public static <T> boolean addAll(Collection<T> c, T... elements):往集合中添加一些元素。
*/
// 创建ArrayList集合,限制集合元素的类型为String类型
ArrayList<String> list = new ArrayList<>();

// 往list集合中添加批量元素
Collections.addAll(list,"2","A","K","Q","J","10","9","8","7","6","5","4","3");
System.out.println(list);
}
}

Set接口

Set接口介绍

1
2
3
4
5
6
7
8
Set接口:也称Set集合,但凡是实现了Set接口的类都叫做Set集合
特点: 元素无索引,元素不可重复(唯一)
HashSet集合: 实现类--元素存取无序
LinkedHashSet集合:实现类--元素存取有序
TreeSet集合:实现类--> 对元素进行排序
注意:
1.Set集合没有特殊的方法,都是使用Collection接口的方法
2.Set集合没有索引,所以遍历元素的方式就只有: 增强for循环,或者迭代器

HashSet集合

java.util.HashSetSet接口的一个实现类,它所存储的元素是不可重复的,并且元素都是无序的(即存取顺序不能保证不一致)。

底层实现是hashMap,采用的数据结构是哈希表结构,此结构特点:快

我们先来使用一下Set集合存储,看下现象,再进行原理的讲解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
public static void main(String[] args) {
/*
HashSet集合: 元素存取无序,元素不可重复,元素无索引
*/
// 创建HashSet集合对象,限制集合元素的类型为String
HashSet<String> set = new HashSet<>();

// 往集合中添加元素
set.add("nba");
set.add("cba");
set.add("bac");
set.add("abc");
set.add("nba");

System.out.println(set);// [cba, abc, bac, nba]
}
}

HashSet集合存储数据的结构(哈希表)

对象的哈希值

就是一个十进制的值,它就是一个对象的特征码(返回值int,-21亿~+21亿,哈希值有限,万物皆对象,hash值有可能重复!{通话和重地的hashCode一样})

Object的toString返回的地址值是hashCode(十进制)的十六进制表示形式。
Object,hashCode因为存的是真正的物理地址值,所以基本上不太可能重复,equals用==比较。

每个对象的特征码应该与该对象的内容有关,父类的hashCode不满足的需求:重写hashCode和equals

哈希表底层结构

JDK1.8之前,哈希表底层采用数组+链表实现,即使用数组处理冲突,同一hash值的链表都存储在一个数组里。但是当位于一个桶中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,哈希表存储采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。

简单的来说,哈希表是由数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的,如下图所示。

image-20230927191744646

HashSet保证元素唯一原理

hashCode+equals

hashCode不一样,对象肯定不一样
hashCode一样,equals不一样,对象不一样
hashCode一样,equals一样,对象一样
为啥不直接调用equals判断两个对象是否一样,而要先掉hashCode?
哈希加速,提高运算效率
这就是为什么用到set集合的时候必须重写hashCode和equals

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
-HashSet集合存储数据的结构---哈希表结构
哈希表结构:
jdk8以前: 数组+链表
jdk8以后: 数组+链表+红黑树
链表元素个数没有超过8: 数组+链表
链表元素个数超过8: 数组+链表+红黑树

-HashSet集合保证元素唯一的原理--依赖hashCode()和equals()方法
1.当存储元素的时候,就会调用该元素的hashCode()方法计算该元素的哈希值
2.判断该哈希值对应的位置上,是否有元素:
3.如果该哈希值对应的位置上,没有元素,就直接存储
4.如果该哈希值对应的位置上,有元素,说明产生了哈希冲突
5.产生了哈希冲突,就得调用该元素的equals方法,与该位置上的所有元素进行一一比较:
如果比较的时候,有任意一个元素与该元素相同,那么就不存储
如果比较完了,没有一个元素与该元素相同,那么就直接存储

补充:
Object类: hashCode()和equals()方法;
hashCode():Object类中的hashCode()方法是根据地址值计算哈希值
equals方法():Object类中的equals()方法是比较地址值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Demo {
public static void main(String[] args) {
// 创建一个HashSet集合,限制集合中元素的类型为String
HashSet<String> set = new HashSet<>();

// 往集合中添加一些元素
set.add("nba");
set.add("cba");
set.add("bac");
set.add("abc");
set.add("nba");

// 遍历打印集合
for (String e : set) {
System.out.println(e);// cba abc bac nba
}

System.out.println("nba".hashCode());// nba:108845
System.out.println("cba".hashCode());// cba:98274
System.out.println("bac".hashCode());// bac:97284
System.out.println("abc".hashCode());// abc:96354
}
}

image-20230927191703204

HashSet的源码分析

HashSet的成员属性及构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HashSet<E> extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable{

//内部一个HashMap——HashSet内部实际上是用HashMap实现的
private transient HashMap<E,Object> map;
// 用于做map的值
private static final Object PRESENT = new Object();
/**
* 构造一个新的HashSet,
* 内部实际上是构造了一个HashMap
*/
public HashSet() {
map = new HashMap<>();
}

}
  • 通过构造方法可以看出,HashSet构造时,实际上是构造一个HashMap

HashSet的add方法源码解析

1
2
3
4
5
6
7
public class HashSet{
//......
public boolean add(E e) {
return map.put(e, PRESENT)==null;//内部实际上添加到map中,键:要添加的对象,值:Object对象
}
//......
}

HashMap的put方法源码解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
public class HashMap{
//......
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
//......
static final int hash(Object key) {//根据参数,产生一个哈希值
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//......
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; //临时变量,存储"哈希表"——由此可见,哈希表是一个Node[]数组
Node<K,V> p;//临时变量,用于存储从"哈希表"中获取的Node
int n, i;//n存储哈希表长度;i存储哈希表索引

if ((tab = table) == null || (n = tab.length) == 0)//判断当前是否还没有生成哈希表
n = (tab = resize()).length;//resize()方法用于生成一个哈希表,默认长度:16,赋给n
if ((p = tab[i = (n - 1) & hash]) == null)//(n-1)&hash等效于hash % n,转换为数组索引
tab[i] = newNode(hash, key, value, null);//此位置没有元素,直接存储
else {//否则此位置已经有元素了
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))//判断哈希值和equals
e = p;//将哈希表中的元素存储为e
else if (p instanceof TreeNode)//判断是否为"树"结构
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {//排除以上两种情况,将其存为新的Node节点
for (int binCount = 0; ; ++binCount) {//遍历链表
if ((e = p.next) == null) {//找到最后一个节点
p.next = newNode(hash, key, value, null);//产生一个新节点,赋值到链表
if (binCount >= TREEIFY_THRESHOLD - 1) //判断链表长度是否大于了8
treeifyBin(tab, hash);//树形化
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))//跟当前变量的元素比较,如果hashCode相同,equals也相同
break;//结束循环
p = e;//将p设为当前遍历的Node节点
}
}
if (e != null) { // 如果存在此键
V oldValue = e.value;//取出value
if (!onlyIfAbsent || oldValue == null)
e.value = value;//设置为新value
afterNodeAccess(e);//空方法,什么都不做
return oldValue;//返回旧值
}
}
++modCount;
if (++size > threshold)
resize();
afterNodeInsertion(evict);
return null;
}
}

HashSet存储自定义类型元素

给HashSet中存放自定义类型元素时,需要重写对象中的hashCode和equals方法,建立自己的比较方式,才能保证HashSet集合中的对象唯一.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class Person{
/**
* 姓名
*/
public String name;
/**
* 年龄
*/
public int age;

public Person() {
}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(name, person.name);
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}
}

创建测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class Demo {
public static void main(String[] args) {
// 创建多个Person对象
Person p1 = new Person("张三", 18);
Person p2 = new Person("李四", 38);
Person p3 = new Person("王五", 28);
Person p4 = new Person("张三", 18);

// 创建HashSet集合对象,限制集合中元素的类型为Person
HashSet<Person> set = new HashSet<>();

// 往集合中添加Person对象
set.add(p1);
set.add(p2);
set.add(p3);
set.add(p4);

// 遍历打印集合中的元素
for (Person p : set) {
System.out.println(p);
}

System.out.println(p1.hashCode());
System.out.println(p2.hashCode());
System.out.println(p3.hashCode());
System.out.println(p4.hashCode());
}
}

LinkedHashSet

我们知道HashSet保证元素唯一,可是元素存放进去是没有顺序的,那么我们要保证有序,怎么办呢?

在HashSet下面有一个子类java.util.LinkedHashSet速度很快,而且能够保证顺序,它是链表(有序)+哈希表(去重)组合的一个数据存储结构。

演示代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.HashSet;
import java.util.LinkedHashSet;

/**
* @Author:pengzhilin
* @Date: 2020/9/15 11:34
*/
public class Test {
public static void main(String[] args) {
/*
LinkedHashSet集合: 元素存取有序,元素无索引,元素不可重复(唯一)
采用哈希表+链表结构,由哈希表保证元素唯一,由链表保证元素存取有序
*/
// 创建LinkedHashSet集合,限制集合中元素的类型为Integer类型
LinkedHashSet<Integer> set = new LinkedHashSet<>();// 存取有序
//HashSet<Integer> set = new HashSet<>();// 存取无序

// 往集合中存储数据
set.add(300);
set.add(100);
set.add(200);
set.add(500);
set.add(400);
set.add(400);

System.out.println(set);// [300, 100, 200, 500, 400]
}
}

TreeSet集合

TreeSet集合是Set接口的一个实现类,底层依赖于TreeMap,是一种基于红黑树的实现,其特点为:

  1. 元素唯一
  2. 元素没有索引
  3. 使用元素的自然顺序对元素进行排序,或者根据创建 TreeSet 时提供的Comparator 比较器
    进行排序,具体取决于使用的构造方法:
1
2
public TreeSet():								根据其元素的自然排序进行排序
public TreeSet(Comparator<E> comparator): 根据指定的比较器进行排序

案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import java.util.Comparator;
import java.util.LinkedHashSet;
import java.util.TreeSet;

public class Test {
public static void main(String[] args) {
/*
TreeSet集合: 元素无索引,元素唯一,对元素进行排序
通过构造方法实现排序:
public TreeSet(): 根据其元素的自然排序进行排序
默认规则排序:集合元素所属的类需要实现Comparable接口,重写compareTo方法,在compareTo方法中指定默认排序规则

public TreeSet(Comparator<E> comparator): 根据指定的比较器进行排序
指定规则排序: 通过传入Comparator接口的实现类对象,在实现类对象中重写compare方法,在compare方法中指定排序规则
*/
// 按照默认规则排序---->默认升序
// 创建TreeSet集合,限制集合中元素的类型为Integer类型
TreeSet<Integer> set = new TreeSet<>();

// 往集合中存储数据
set.add(300);
set.add(100);
set.add(200);
set.add(500);
set.add(400);
set.add(400);
System.out.println(set);// [100, 200, 300, 400, 500]

System.out.println("===========================================");
// 按照指定规则排序---->降序
// 创建TreeSet集合,限制集合中元素的类型为Integer类型
TreeSet<Integer> set1 = new TreeSet<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
/*
指定排序规则:
前减后 升序
后减前 降序
前:第一个参数 后:第二个参数
*/
return o2 - o1;
}
});

// 往集合中存储数据
set1.add(300);
set1.add(100);
set1.add(200);
set1.add(500);
set1.add(400);
set1.add(400);
System.out.println(set1);// [500, 400, 300, 200, 100]

System.out.println("===========================================");
// 按照指定规则排序---->升序
// 创建TreeSet集合,限制集合中元素的类型为Integer类型
TreeSet<Integer> set2 = new TreeSet<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
/*
指定排序规则:
前减后 升序
后减前 降序
前:第一个参数 后:第二个参数
*/
return o1 - o2;
}
});

// 往集合中存储数据
set2.add(300);
set2.add(100);
set2.add(200);
set2.add(500);
set2.add(400);
set2.add(400);
System.out.println(set2);// [100, 200, 300, 400, 500]
}
}

Map集合

Map概述

image-20220709090324966

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Map<K,V>集合的特点: K用来限制键的类型,V用来限制值的类型
1.Map集合存储元素是以键值对的形式存储,每一个键值对都有键和值
2.Map集合的键是唯一,值可以重复,如果键重复了,那么值就会被覆盖
3.根据键取值

Map集合子类:
- HashMap<K,V>:存储数据采用的哈希表结构,元素的存取顺序不能保证一致。
由于要保证键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。

- LinkedHashMap<K,V>:HashMap下有个子类LinkedHashMap,存储数据采用的哈希表结构+链表结构。
通过链表结构可以保证键值对的存取顺序一致;
通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。

- TreeMap<K,V>:TreeMap集合和Map相比没有特有的功能,底层的数据结构是红黑树;
可以对元素的键进行排序,排序方式有两种:自然排序和比较器排序

Map的常用方法

Map接口中定义了很多方法,常用的如下:

  • public V put(K key, V value): 把指定的键与指定的值添加到Map集合中。
  • public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
  • public V get(Object key) 根据指定的键,在Map集合中获取对应的值。
  • public boolean containsKey(Object key):判断该集合中是否有此键
  • public Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。
  • public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。

Map接口的方法演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

public class Test {
public static void main(String[] args) {
/*
Map<K,V>的常用方法:
- public V put(K key, V value): 把指定的键与指定的值添加到Map集合中。
- public V remove(Object key): 把指定的键 所对应的键值对元素 在Map集合中删除,返回被删除元素的值。
- public V get(Object key) 根据指定的键,在Map集合中获取对应的值。

- public boolean containsKey(Object key):判断该集合中是否有此键
- public boolean containsValue(Object value):判断该集合中是否有此值

- public Set<K> keySet(): 获取Map集合中所有的键,存储到Set集合中。
- public Collection<V> values(): 获取Map集合中所有的值,存储到Collection集合中。

- public Set<Map.Entry<K,V>> entrySet(): 获取到Map集合中所有的键值对对象的集合(Set集合)。
Map.Entry<K,V>:表示键值对对象---把键值对包装成一个对象,该对象的类型就是Entry类型
*/
// 创建Map集合,限制键的类型为String,值的类型为String
Map<String, String> map = new HashMap<>();

// 往map集合中添加键值对
map.put("黄晓明", "杨颖");
map.put("文章", "马伊琍");
map.put("谢霆锋", "王菲");
System.out.println(map);// {文章=马伊琍, 谢霆锋=王菲, 黄晓明=杨颖}

// Map集合键唯一,如果键重复了,值会覆盖
String v1 = map.put("文章", "姚笛");
System.out.println("v1:"+v1);// 马伊琍
System.out.println(map);// {文章=姚笛, 谢霆锋=王菲, 黄晓明=杨颖}

// Map集合值可以重复
String v2 = map.put("李亚鹏", "王菲");
System.out.println("v2:"+v2);// null
System.out.println(map);// {文章=姚笛, 谢霆锋=王菲, 李亚鹏=王菲, 黄晓明=杨颖}

// 删除文章这个键对应的键值对
String v3 = map.remove("文章");
System.out.println("被删除键值对的值:"+v3);// 姚笛
System.out.println(map);// {谢霆锋=王菲, 李亚鹏=王菲, 黄晓明=杨颖}

// 获取黄晓明这个键对应的值
String value = map.get("黄晓明");
System.out.println("value:"+value);// 杨颖

// 判断是否包含指定的键
System.out.println(map.containsKey("黄晓明"));// true
System.out.println(map.containsKey("文章"));// false

// 判断是否包含指定的值
System.out.println(map.containsValue("杨颖"));// true
System.out.println(map.containsValue("马伊琍"));// false

// 获取所有的键
Set<String> keys = map.keySet();
System.out.println("keys:"+keys);// [谢霆锋, 李亚鹏, 黄晓明]

// 获取所有的值
Collection<String> values = map.values();
System.out.println("values:"+values);// [王菲, 王菲, 杨颖]

// 获取Map集合中所有键值对对象
Set<Map.Entry<String, String>> set = map.entrySet();
System.out.println(set);// [谢霆锋=王菲, 李亚鹏=王菲, 黄晓明=杨颖]

}
}

tips:

使用put方法时,若指定的键(key)在集合中没有,则没有这个键对应的值,返回null,并把指定的键值添加到集合中;

若指定的键(key)在集合中存在,则返回值为集合中键对应的值(该值为替换前的值),并把指定键所对应的值,替换成指定的新值。

Map的遍历

方式1:键找值方式

通过元素中的键,获取键所对应的值

分析步骤:

  1. 获取Map中所有的键,由于键是唯一的,所以返回一个Set集合存储所有的键。方法提示:keyset()
  2. 遍历键的Set集合,得到每一个键。
  3. 根据键,获取键所对应的值。方法提示:get(K key)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Demo {
public static void main(String[] args) {
// 创建Map集合对象,限制键的类型为String,值的类型为String
Map<String, String> map = new HashMap<>();
// 往map集合中添加键值对
map.put("黄晓明", "杨颖");
map.put("文章", "马伊琍");
map.put("谢霆锋", "王菲");

// 遍历map集合
// 获取集合中所有的键 Set<K> keySet()方法
Set<String> keys = map.keySet();
// 遍历所有的键的集合
for (String key : keys) {
// 在循环中,根据键找值 V get(K key)方法
String value = map.get(key);
System.out.println("键:"+key+",值:"+value);
}
}
}

方式2:键值对方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Entry<K,V>接口:简称Entry项,表示键值对对象,用来封装Map集合中的键值对
Entry<K,V>接口:是Map接口中的内部接口,在外部使用的时候是这样表示: Map.Entry<K,V>

Map集合中提供了一个方法来获取所有键值对对象:
public Set<Map.Entry<K,V>> entrySet()

根据键值对对对象获取键和值:
- public K getKey():获取Entry对象中的键。
- public V getValue():获取Entry对象中的值。

Map遍历方式二:根据键值对对象的方式
1.获取集合中所有键值对对象,以Set集合形式返回。 Set<Map.Entry<K,V>> entrySet()
2.遍历所有键值对对象的集合,得到每一个键值对(Entry)对象。
3.在循环中,可以使用键值对对对象获取键和值 getKey()和getValue()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Demo {
public static void main(String[] args) {
// 创建Map集合对象,限制键的类型为String,值的类型为String
Map<String, String> map = new HashMap<>();
// 往map集合中添加键值对
map.put("黄晓明", "杨颖");
map.put("文章", "马伊琍");
map.put("谢霆锋", "王菲");

// 获取集合中所有键值对对象 Set<Map.Entry<K,V>> entrySet()
Set<Map.Entry<String, String>> entrySet = map.entrySet();

// 遍历所有键值对对象的集合
for (Map.Entry<String, String> entry : entrySet) {
// 在循环中,可以使用键值对对对象获取键和值 getKey()和getValue()
String key = entry.getKey();
String value = entry.getValue();
System.out.println("键:"+key+",值:"+value);
}
}
}

HashMap存储自定义类型

练习:每位学生(姓名,年龄)都有自己的家庭住址。那么,既然有对应关系,则将学生对象和家庭住址存储到map集合中。学生作为键, 家庭住址作为值。

注意,学生姓名相同并且年龄相同视为同一名学生。

编写学生类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Student {
/**
* 姓名
*/
public String name;
/**
* 年龄
*/
public int age;

public Student() {
}

public Student(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

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

编写测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Demo {
public static void main(String[] args) {
// 创建Map集合,指定键的类型为Student,值的类型为String
HashMap<Student,String> map = new HashMap<>();

// 创建多个学生对象
Student stu1 = new Student("张三", 18);
Student stu2 = new Student("李四", 38);
Student stu3 = new Student("王五", 28);
Student stu4 = new Student("张三", 18);

// 把学生对象作为键,家庭地址作为值,存储到map集合中
map.put(stu1,"北京");
map.put(stu2,"上海");
map.put(stu3,"深圳");
map.put(stu4,"广州");

// 打印map集合
System.out.println(map);
System.out.println(map.size());// 3
}
}
  • 当给HashMap中存放自定义对象时,如果自定义对象作为key存在,这时要保证对象唯一,必须复写对象的hashCode和equals方法(如果忘记,请回顾HashSet存放自定义对象)。
  • 如果要保证map中存放的key和取出的顺序一致,可以使用java.util.LinkedHashMap集合来存放。

LinkedHashMap介绍

我们知道HashMap保证成对元素唯一,并且查询速度很快,可是成对元素存放进去是没有顺序的,那么我们要保证有序,还要速度快怎么办呢?

  • 通过链表结构可以保证元素的存取顺序一致;
  • 通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.util.LinkedHashMap;

public class Test {
public static void main(String[] args) {
/*
LinkedHashMap: 元素存取有序,键唯一,值可重复
- 通过链表结构可以保证元素的存取顺序一致;
- 通过哈希表结构可以保证的键的唯一、不重复,需要重写键的hashCode()方法、equals()方法。
*/
// 创建LinkedHashMap集合,限制键的类型为Integer,值的类型为String
LinkedHashMap<Integer, String> map = new LinkedHashMap<>();

map.put(400, "武汉");
map.put(400, "深圳");
System.out.println(map);
}
}

TreeMap集合

TreeMap介绍

TreeMap集合和Map相比没有特有的功能,底层的数据结构是红黑树;可以对元素的进行排序,排序方式有两种:自然排序比较器排序;到时使用的是哪种排序,取决于我们在创建对象的时候所使用的构造方法;

构造方法

1
2
public TreeMap()									使用自然排序
public TreeMap(Comparator<? super K> comparator) 通过比较器指定规则排序

案例演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.TreeMap;

public class Test {
public static void main(String[] args) {
/*
TreeMap集合: 键唯一,值可以重复,如果键重复了,值就覆盖,可以根据键对键值对进行排序
public TreeMap() 根据键按照默认规则进行排序
public TreeMap(Comparator<? super K> comparator) 通过比较器指定规则排序
*/
// 按照键的默认规则排序: ---->升序
// 创建TreeMap集合,限制键的类型为Integer,值的类型为String
TreeMap<Integer, String> map = new TreeMap<>();

// 往map集合中添加键值对
map.put(300, "深圳");
map.put(100, "北京");
map.put(200, "广州");
map.put(500, "上海");
map.put(400, "武汉");
map.put(400, "深圳");
System.out.println(map);

System.out.println("+=================================");

// 按照指定规则排序: ---->降序
// 创建TreeMap集合,限制键的类型为Integer,值的类型为String
TreeMap<Integer, String> map1 = new TreeMap<>(new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
/*
前减后: 升序
后减前: 降序
前:第一个参数 后:第二个参数
*/
return o2 - o1;
}
});

// 往map集合中添加键值对
map1.put(300, "深圳");
map1.put(100, "北京");
map1.put(200, "广州");
map1.put(500, "上海");
map1.put(400, "武汉");
map1.put(400, "深圳");
System.out.println(map1);
}
}

Map集合练习

需求

  • 输入一个字符串统计该字符串中每个字符出现次数。

分析

  • 获取一个字符串对象
  • 创建一个Map集合,键代表字符,值代表次数。
  • 遍历字符串得到每个字符。
  • 判断Map中是否有该键。
  • 如果没有,第一次出现,存储次数为1;如果有,则说明已经出现过,获取到对应的值进行++,再次存储。
  • 打印最终结果

实现

方法介绍

public boolean containKey(Object key):判断该集合中是否有此键。

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.HashMap;
import java.util.Scanner;

public class Test {
public static void main(String[] args) {
/*
Map集合练习:输入一个字符串,统计该字符串中每个字符出现次数。
*/
// 0.输入一个字符串
Scanner sc = new Scanner(System.in);
System.out.println("请输入一个字符串:");
String s = sc.nextLine();

// 1.创建Map集合,键的类型为Character,值的类型为Integer
HashMap<Character, Integer> map = new HashMap<>();

// 2.循环遍历字符串
for (int i = 0; i < s.length(); i++) {
// 3.在循环中获取遍历出来的字符
char c = s.charAt(i);
// 4.判断集合中是否存在该字符的键
if (map.containsKey(c)) {
// 6.如果集合中已存在该字符的键,那么就取出该字符键对应的值,然后自增1,作为新的值,重新存储到Map集合中
Integer value = map.get(c);
value++;
map.put(c, value);
} else {
// 5.如果集合中不存在该字符的键,那么就该字符作为键,值为1,存储到Map集合中
map.put(c, 1);
}
}
// 7.循环结束,打印map集合
System.out.println(map);
}
}

集合的嵌套

任何集合内部都可以存储其它任何集合

List嵌套List

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Test1 {
public static void main(String[] args) {
/*
集合的嵌套:
- List嵌套List
- List嵌套Map
- Map嵌套Map
结论:任何集合内部都可以存储其它任何集合
*/
// List嵌套List
// 创建一个List集合,限制元素类型为String
List<String> list1 = new ArrayList<>();

// 往集合中添加元素
list1.add("王宝强");
list1.add("贾乃亮");
list1.add("陈羽凡");

// 创建一个List集合,限制元素类型为String
List<String> list2 = new ArrayList<>();

// 往集合中添加元素
list2.add("马蓉");
list2.add("李小璐");
list2.add("白百何");

// 创建一个List集合,限制元素类型为List集合 (List集合中的元素是List集合)
List<List<String>> list = new ArrayList<>();
list.add(list1);
list.add(list2);

// 遍历
for (List<String> e : list) {
for (String name : e) {
System.out.println(name);
}
System.out.println("=============");
}

System.out.println(list);
}
}


List嵌套Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class Test2 {
public static void main(String[] args) {
/*
List嵌套Map:

*/
// 创建Map集合对象
Map<String,String> map1 = new HashMap<>();
map1.put("it001","迪丽热巴");
map1.put("it002","古力娜扎");

// 创建Map集合对象
Map<String,String> map2 = new HashMap<>();
map2.put("heima001","蔡徐坤");
map2.put("heima002","李易峰");

// 创建List集合,用来存储以上2个map集合
List<Map<String,String>> list = new ArrayList<>();
list.add(map1);
list.add(map2);

System.out.println(list.size()); // 2

for (Map<String, String> map : list) {
// 遍历获取出来的map集合对象
Set<String> keys = map.keySet();// 获取map集合所有的键
// 根据键找值
for (String key : keys) {
System.out.println(key + ","+ map.get(key));
}
}

}
}

Map嵌套Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Test3 {
public static void main(String[] args) {
/*
Map嵌套Map:

*/
// 创建Map集合对象
Map<String,String> map1 = new HashMap<>();
map1.put("it001","迪丽热巴");
map1.put("it002","古力娜扎");

// 创建Map集合对象
Map<String,String> map2 = new HashMap<>();
map2.put("heima001","蔡徐坤");
map2.put("heima002","李易峰");

// 创建Map集合,把以上2个Map集合作为值存储到这个map集合中
Map<String, Map<String, String>> map = new HashMap<>();

map.put("传智博客",map1);
map.put("黑马程序员",map2);

System.out.println(map.size());// 2

// 获取map集合中的所有键
Set<String> keys = map.keySet();
// 遍历所有的键
for (String key : keys) {
// 根据键找值
Map<String, String> value = map.get(key);
// 遍历value这个Map集合
Set<String> keySet = value.keySet();
for (String k : keySet) {
String v = value.get(k);
System.out.println(k+","+v);
}
}
}
}

模拟斗地主洗牌发牌

需求

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

image-20220709090840612

具体规则:

  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<String>,将最后3张牌直接存放于底牌,剩余牌通过对3取模依次发牌。

存放的过程中要求数字大小与斗地主规则的大小对应。

将代表不同纸牌的数字分配给不同的玩家与底牌。

4.看牌:

通过Map集合找到对应字符展示。

通过查询纸牌与数字的对应关系,由数字转成纸牌字符串再进行展示。

image-20230927191731298

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;

public class Test {
public static void main(String[] args) {
/*
模拟斗地主洗牌发牌:
需求
按照斗地主的规则,完成洗牌发牌的动作。
具体规则:
1. 组装54张扑克牌
2. 54张牌顺序打乱
3. 三个玩家参与游戏,三人交替摸牌,每人17张牌,最后三张留作底牌。
4. 查看三人各自手中的牌(按照牌的大小排序)、底牌
规则:手中扑克牌从大到小的摆放顺序:大王,小王,2,A,K,Q,J,10,9,8,7,6,5,4,3
*/
// 造牌
// 1.创建Map集合对象,限制键的类型为Integer,值的类型为String
HashMap<Integer, String> pokeBox = new HashMap<>();
// 2.创建一个List集合,表示花色集合,
ArrayList<String> colors = new ArrayList<>();
// 3.创建一个List集合,表示牌面值集合
ArrayList<String> numbers = new ArrayList<>();

// 4.往花色集合中存储4个花色
Collections.addAll(colors, "♥", "♦", "♠", "♣");
// 5.往牌面值集合中存储13个牌面值
Collections.addAll(numbers, "2", "A", "K", "Q", "J", "10", "9", "8", "7", "6", "5", "4", "3");

// 6.定义一个int类型的变量,表示牌的编号,初始值为0
int id = 0;
// 7.往map集合中添加大王,编号为0,添加完后编号自增1
pokeBox.put(id++, "大王");

// 8.往map集合中添加小王,编号为1,添加完后编号自增1
pokeBox.put(id++, "小王");

// 9.牌面值的集合和花色集合循环嵌套遍历,注意牌面值集合作为外层循环,花色集合作为内层循环
for (String number : numbers) {
for (String color : colors) {
// 10.在循环中,遍历出来的牌面值和花色组成一张扑克牌
String pai = color + number;
// 11.在循环中,编号作为键,扑克牌作为值存储到map集合中,每存储一张牌后,编号自增1
pokeBox.put(id++,pai);
}
}

System.out.println(pokeBox.size());
System.out.println(pokeBox);

//2.洗牌 :--->洗牌的编号
//2.1 获取所有牌的编号,返回的是所有编号的Set集合
Set<Integer> keySet = pokeBox.keySet();

//2.2 创建ArrayList集合,用来存储所有的牌编号
ArrayList<Integer> idList = new ArrayList<>();

//2.3 把keySet集合中存储的所有牌编号,存储到这个新的ArrayList集合中
idList.addAll(keySet);

//2.4 使用Collections.shuffle方法对新的ArrayList集合中的元素打乱顺序
Collections.shuffle(idList);
System.out.println("打乱顺序后的牌编号:"+idList.size());// 54
System.out.println("打乱顺序后的牌编号:"+idList);


// 3.发牌-->发牌的编号--->对牌的编号进行从小到大排序---->再根据排好序的编号去map集合中获取牌
// 3.1 创建4个List集合,分别用来存储玩家一,玩家二,玩家三,底牌得到的牌编号
ArrayList<Integer> play1Id = new ArrayList<>();
ArrayList<Integer> play2Id = new ArrayList<>();
ArrayList<Integer> play3Id = new ArrayList<>();
ArrayList<Integer> diPaiId = new ArrayList<>();

// 3.2 循环把打乱顺序的牌编号,按照规律依次发给玩家一,玩家二,玩家三,底牌
for (int i = 0; i < idList.size(); i++) {
// 获取牌编号
Integer paiId = idList.get(i);
// 三人交替摸牌
if (i >= 51){
diPaiId.add(paiId);
}else if (i%3==0){
play1Id.add(paiId);
}else if (i%3==1){
play2Id.add(paiId);
}else if (i%3==2){
play3Id.add(paiId);
}
}

// 3.3 对获取到的牌编号进行从小到大排序
Collections.sort(play1Id);
Collections.sort(play2Id);
Collections.sort(play3Id);
Collections.sort(diPaiId);

// 3.4 根据排好序的牌编号去map集合中获取牌
// 遍历玩家一的牌编号
System.out.print("玩家一的牌:");
for (Integer paiId : play1Id) {// 1,2,3,4,5
String pai = pokeBox.get(paiId);
System.out.print(pai+" ");
}

System.out.println();

// 遍历玩家二的牌编号
System.out.print("玩家二的牌:");
for (Integer paiId : play2Id) {
String pai = pokeBox.get(paiId);
System.out.print(pai+" ");
}

System.out.println();

// 遍历玩家三的牌编号
System.out.print("玩家三的牌:");
for (Integer paiId : play3Id) {
String pai = pokeBox.get(paiId);
System.out.print(pai+" ");
}

System.out.println();

// 遍历底牌的牌编号
System.out.print("底牌的牌:");
for (Integer paiId : diPaiId) {
String pai = pokeBox.get(paiId);
System.out.print(pai+" ");
}
}
}