集合框架
为什么要使用集合框架
之前我们学习过数组,知道了数组可以存储多个数据类型相同的元素,但面对 频繁增加、删除、修改 元素的要求以及 动态扩容 要求时显得捉襟见肘。为此,JDK 提供了一套 “集合” 框架,这套框架是对常见数据结构的实现,不仅可存储数据,还提供了丰富的访问和处理数据的操作。在面向对象思想里,数据结构也被认为是一个容器,所以集合、容器等词汇经常被交替使用。
Java 集合框架支持两种类型的容器:一种是为了存储一个元素集合,简称为 集合(collection) ;另一种是为了存储键/值对,称为 映射(map,或称图) 。
使用数组存放多个对象的信息会存在很多问题,首先,Java 语言中的数组长度是固定的,旦创建出指定长度的数组以后,就给内存分配了相应的存储空间,如果数组长度设置大了,又会造成空间浪费,删除数组元素全部要前移一位,但这种元素的移动是比较消耗系统资源的。
集合框架有两个基本接口:Collection 和 Map,此外还有一个迭代器接口:Iterator,及一个标记接口:RandomAccess。
其中 Collection 又叫单列集合,在添加数据时每次只能添加一个元素,Map 又叫双列集合,再添加数据时每次添加一对数据。
Collection 接口
集合框架可以分为 Collection 和 Map 两类。
Collection 是一个顶层接口,一些 Collection 接口的实现类允许有重复的元素,而另一些则不允许;一些 Collection 是有序的,而另一些则是无序的。
JDK 不提供 Collection 接口的任何直接实现类,而是提供了更具体的子接口,如 Set 接口和 List 接口。这些子接口继承 Collection 接口的方法,然后再对 Collection 接口从不同角度进行重写或扩充。
⭐ Collection 接口主要有 Set 接口、List 接口和 Queue 接口 三个子接口。
Set 接口
Set 实例用于存储一组 不重复的,无序的,无索引的 元素。
List 接口
List 实例是一个 有序集合。程序员可对 List 中每个元素的位置进行精确控制,可以根据 索引 来访问元素,此外 List 中的元素是 可以重复 的。
这里的有序和排序不一样,指存和取数据的顺序是一样的。
Queue 接口
Queue 中的元素遵循 先进先出 的规则,是对数据结构 “队列” 的实现。
Map 接口
Map 接口定义了存储和操作一组 “键(key)值(value)” 映射对的方法。
⭐ Map 接口和 Collection 接口的本质区别在于,Collection 接口里存放的是一系列单值对象,而 Map 接口里存放的是一系列 key-value 对象。
Map 中的 key 不能重复,每个 key 最多只能映射到一个值。
HashMap ,Hashtable 和 TreeMap 是 Map 接口的实现类。
总结
Ⅰ. 集合框架可以分为?
集合框架可以分为 Collection 和 Map 两类。
Ⅱ. Collection 接口主要有哪几个子接口?
Set 接口、List 接口和 Queue 接口。
Ⅲ. Set、List 和 Queue 各有什么特点?
Set 用于存储一组不重复、无序的元素;List 中的元素有序、可以重复;Queue 中的元素先进先出。
Ⅳ. Map 接口和 Collection 接口有什么区别?
Collection 接口里存放的是一系列单值对象,而 Map 接口里存放的是一系列 key-value 对象。
Ⅴ. Map 中的 key 有什么特点?
Map 中的 key 不能重复,每个 key 最多只能映射到一个值。
Set 接口
Set 接口的主要方法
Set 接口是 Collection 的子接口。Set 接口中的元素是不能重复的、无序的,这里的 “无序” 是指向 Set 中输入的元素,与从 Set 中输出元素的顺序是不一致的。
例如,向 Set 接口中依次增加 “北京”、“深圳” 和 “西安” 三个元素,但输出顺序却是 “西安”、“北京”和“深圳” 。对于开发者而言,只需要了解这一 “无序” 的特性即可,不必深究其原因。
下面列出了 Set 接口继承自 Collection 接口的主要方法。
1
2
3
4
5
|
public boolean add(Object obj)
Set name = new HashSet();
name.add("张三");
System.out.println(name.add("张三"));
|
向集合中添加一个 obj 元素,并且 obj 不能和集合中现有数据元素重复,添加成功后返回 true。如果添加的是重复元素,则添加操作无效,并返回 false。
1
2
3
|
public void clear()
name.clear();
|
移除此集合中的所有数据元素,即将集合清空。
1
2
3
|
public boolean contains(Object obj)
name.contains("张三");
|
判断此集合中是否包含 obj,如果包含,则返回 true。
1
2
3
|
public boolean isEmpty()
System.out.println(name.isEmpty());
|
判断集合是否为空,为空则返回 true。
返回一个 Iterator 对象,可用它来遍历集合中的数据元素。
1
2
|
public boolean remove(Object obj)
name.remove("张三");
|
如果此集合中包含 obj,则将其删除,并返回 true。
1
2
3
|
public int size()
System.out.println(name.size());
|
返回集合中真实存放数据元素的个数,注意与数组、字符串获取长度的方法的区别。
1
2
3
4
5
6
|
public Object[] toArray()
TreeSet num = new TreeSet();
...
Integer[] arr1 = new Integer[num.size()];
num.toArray(arr1);
|
返回一个数组,该数组包含集合中的所有数据元素。
HashSet 的底层原理
⭐ HashSet 和 TreeSet ?
HashSet 和 TreeSet 都是 Java 中的集合类,它们的主要区别在于底层数据结构和元素的排序方式。
-
底层数据结构: HashSet 使用 哈希表 作为底层数据结构,而 TreeSet 使用 红黑树 作为底层数据结构。
-
元素的排序方式: HashSet 中的元素是无序的,而 TreeSet 中的元素是有一定规律的排序的,且默认按照元素的自然顺序排序。如果需要按照其他方式排序,则需要在创建 TreeSet 时指定一个 Comparator 对象。
-
元素的唯一性: HashSet 中的元素是唯一的,不允许重复,而 TreeSet 中的元素也是唯一的,但是它是通过比较器或元素的自然顺序来判断元素是否相同的。
-
性能: HashSet 的插入、删除和查找操作的时间复杂度都是O(1),而 TreeSet 的这些操作的时间复杂度都是 O(log n)。
因此,如果需要 快速的插入、删除和查找操作,并且不需要元素有序,则可以选择 HashSet。如果需要 元素有序,或者需要按照其他方式进行排序,则可以选择 TreeSet。
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
|
HashSet 底层原理
- HashSet 底层集合采取哈希表存储数据
- 哈希表是一种对于增删改查数据性能都比较好的结构
// --------------------------------------------------------------------------------
哈希表的组成
- JDK8以前:数组+链表
// 属性值不一样,新元素添加到数组,旧元素挂在新元素下方
创建一个默认长度为16,默认加载因子为0.75的数组,数组名为table
int index = (数组长度 - 1) & 哈希值; 计算存入位置
如果数组中该位置为null,则存入,如果有元素,则使用equals方法比较属性值
- JDK8开始:数组+链表+红黑树
// 属性值不一样,新元素挂在旧元素下方
默认加载因子:数组长度 * 0.75 = 12,即数组中存了12个元素时,数组扩容到原来的两倍
当链表长度大大于等于8且数组长度大于等于64时,链表转换为红黑树
// --------------------------------------------------------------------------------
哈希表中的哈希值
- 哈希值是根据 hashCode 方法计算出来的 int 类型的整数
- 该方法定义在 Object 类中,所有对象都可以调用,默认使用地址值进行计算
- 一般情况下,会重写 hashCode 方法,利用对象内部的属性值计算哈希值
|
思考:哈希值是对象的整数表现形式,那为什么要把对象变成整数?
哈希表在底层有数组存在,如果此时我们需要添加数据,不是从 0 索引开始往后存储的,而是根据以下公式计算出在数组中应该存储的位置:
1
|
int index = (数组长度 - 1) & 哈希值;
|
对象无法参与计算,所以我们要把对象转换成整数进行计算,即哈希值。
对象的哈希值的特点
- 如果没有重写 hashCode 方法,则不同对象计算出的哈希值是不同的。
- 如果重写了 hashCode 方法,不同对象只要属性值相同,计算出的哈希值就是一样的。
- 在少数情况下,不同属性或者不同地址值计算出的哈希值也有可能一样。(哈希碰撞)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
// 重写 hashCode 方法
// alt + insert
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
StuTest stuTest = (StuTest) o;
return age == stuTest.age && Objects.equals(name, stuTest.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
|
-
使用 hashCode() 方法计算哈希值,重写前后进行比较。
-
少数情况出现哈希碰撞,哈希值计算结果相同。
1
2
3
4
5
6
7
8
9
10
11
12
|
// 实例化对象
StuTest s1 = new StuTest("张三",18);
StuTest s2 = new StuTest("张三",18);
// 没有重写 hashCode 方法,计算地址值
// 重写 hashCode 方法,计算属性值
System.out.println(s1.hashCode());
System.out.println(s2.hashCode());
// 哈希碰撞
System.out.println("abc".hashCode());
System.out.println("acD".hashCode());
|
-
HashSet 添加元素的过程?
首先通过 int index = (数组长度 - 1) & 哈希值; 计算元素应该存储的位置,然后判断当前位置是否为null,如果是则存储,否则则继续使用 equals 方法比较元素的属性值,根据情况挂载在旧元素后。
-
HashSet 为什么存和取的顺序不一样?
HashSet 是从内部数组中的0索引开始,逐条链表查询数据输出的,再输出后面索引的数据。
-
HashSet 为什么没有索引?
HashSet 是链表 + 数组 + 红黑树组成的,不能单纯的以索引来决定元素的位置,因为可能有多个元素挂在同一个索引下。
-
HashSet 是利用什么机制保证数据去重的?
利用 HashCode 获取哈希值,找到元素存储的位置,再调用 equals 方法比较元素内部的属性值。
HashSet 类的使用
-
Set 接口的特性:无重复,无序。
-
Set 接口下 继承父接口的方法。
-
Set 接口的实现类 HashSet 的使用:
-
通过 HashSet 无参构造器创建 Set 对象。
-
使用了 add()、contains()、remove()、size() 等常用的方法对集合元素进行操作。
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.company.collection;
import java.util.HashSet;
import java.util.Set;
public class CllDemo1 {
/*
* HashSet 类的使用
*/
public static void main(String[] args) {
// 创建一个HashSet对象,存放学生姓名信息
Set nameSet = new HashSet();
// add方法
nameSet.add("王云");
nameSet.add("刘静涛");
nameSet.add("南天华");
nameSet.add("雷静");
// add已有的数据元素
nameSet.add("王云");
System.out.println("再次添加王云是否成功:" + nameSet.add("王云"));
System.out.println("显示集合内容:" + nameSet);
// contains方法
System.out.println("集合里是否包含南天华:" + nameSet.contains("南天华"));
// remove方法
System.out.println("从集合中删除\"南天华\"...");
nameSet.remove("南天华");
System.out.println("集合里是否包含南天华:" + nameSet.contains("南天华"));
// size方法
System.out.println("集合中的元素个数为:" + nameSet.size());
// isEmpty方法
System.out.println(nameSet.isEmpty());
}
}
|
这里,contains 方法底层逻辑为 equals 方法。
如果集合中存储的是自定义对象,也想通过 contains 方法来判断是否包含的话,那么在 javabean 中,一定要重写 equals 方法。
我们可以选中 contains 方法,使用 ctrl + alt + B 转到 contains 方法的实现,可以看到,contains 方法调用了 indexOf,而 indexOf 又调用了一个叫做 indexOfRange 的方法,最后在 indexOfRange 中使用了equals 方法。
那么我们可以自己通过代码测试,了解为什么集合存储自定义对象需要重写 equals 方法,首先我们自定义一个 StuTest 对象,生成 javabean。
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
|
package com.company.collection;
public class StuTest { // 自定义对象StuTest
private String name;
private int age;
public StuTest() {
}
public StuTest(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String toString() {
return "StuTest{name = " + name + ", age = " + age + "}";
}
}
|
然后在代码中测试集合存储普通数据和存储自定义对象两种情况下,使用 contains 方法的不同之处。
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
|
package com.company.collection;
import java.util.HashSet;
import java.util.Set;
public class CllDemo5 {
public static void main(String[] args) {
Set nameSet = new HashSet();
// 存储信息,使用equals比较是否相等,如果相等则contians方法返回true
String a = "aaa";
nameSet.add(a);
String b = "aaa";
System.out.println(nameSet.contains(b));
// 创建实例化对象
StuTest s1 = new StuTest("张三",12);
StuTest s2 = new StuTest("李四",22);
StuTest s3 = new StuTest("王五",16);
// 添加到集合
nameSet.add(s1);
nameSet.add(s2);
nameSet.add(s3);
// 如果姓名和年龄相同,则是同一人
StuTest s4 = new StuTest("张三",12);
System.out.println(nameSet.contains(s4));
/*
如果集合中存储的是自定义对象,也想通过 contains 方法来判断是否包含的话,
那么在 javabean中,一定要重写 equals方法。
*/
}
}
|
练习: 自定义一个 Friend 对象,包含学号 id,姓名 name,年龄 age,身高 hight,爱好 hobby 等属性,创建五个实例化对象,并创建一个 HashSet 集合存储他们的信息,判断集合是否为空,删除第二个元素,并输出此时集合内元素的个数。(要求:对象的成员变量值相同,我们就认为是同一个对象)
1
2
3
|
我们知道,hashCode() 和 equals() 两个方法最初都是在 Object 类中定义的,能否直接继承并使用这两个方法?
不能。Object 中定义的 equals() 方法默认比较的是对象的内存地址;hashCode() 方法的前面有 native 修饰符,表示会通过操作系统底层提供的算法来计算 hash 值。显然,这两个方法的默认实现,与我们 “关注对象内容” 的侧重点不一致,因此需要重写。
|
HashSet 类无重复特性
我们已知 Set 接口的特性是无序的,不重复的,那么需要思考一个问题:HashSet 是如何判断元素重复的? 如果逐个比较 HashSet 中的全部元素,显然是一种效率低下的做法。因此 HashSet 的底层引入了 hashcode。
hashcode 最初定义在 Object 类中,如果两个对象相等,那么这两个对象的 hashcode 值相同,因此根据逆否定理可知如果两个对象的 hashcode 值不同,那么这两个对象不相等。但反之,如果两个对象的 hashcode 值相同,则这两个对象可能相等,也可能不等,需要再通过 equals() 方法 进一步比较这两个对象的内容是否相同。
⭐ “equals()” 和 “==” 的区别是什么?
equals 方法是 java.lang.Object 类的方法。
(1)对于 字符串 变量来说,使用 “==” 和 “equals()” 方法比较字符串时,其比较方法不同。
1
2
3
4
5
6
7
8
|
String s1,s2,s3 = "abc", s4 ="abc" ;
s1 = new String("abc");
s2 = new String("abc");
System.out.println(s1 == s2);
System.out.println(s1.equals(s2));
System.out.println(s3 == s4);
System.out.println(s3.equals(s4));
|
(2)对于 非字符串 变量来说,"==" 和 “equals” 方法的作用是相同的,都是用来比较其对象在堆内存的首地址,即用来比较两个引用变量是否指向同一个对象。
1
2
3
4
5
6
7
8
|
CllDemo4 obj1 = new CllDemo4(); // 实例化对象
CllDemo4 obj2 = new CllDemo4();
System.out.println(obj1 == obj2);
System.out.println(obj1.equals(obj2));
obj1 = obj2;
System.out.println(obj1 == obj2);
System.out.println(obj1.equals(obj2));
|
功能不同 |
定义不同 |
运行速度不同 |
“==” 是判断两个变量或实例是不是指向同一个内存空间。 |
“==” 在 JAVA 中只是一个运算符号。 |
“==” 比 “equals” 运行速度快,因为它只是比较引用。 |
“equals” 是判断两个变量或实例所指向的内存空间的值是不是相同。 |
“equals” 在 JAVA 中是一个方法。 |
“equals” 比 “==” 运行速度要慢。 |
当向 HashSet 中增加元素时,HashSet 会先计算此元素的 hashcode,如果 hashcode 值与 HashSet 集合中的其他元素的 hashcode 值都不相同,那么就能断定此元素是唯一的。否则,如果 hashcode 值与 HashSet 集合中的某个元素的 hashcode 值相同,HashSet 就会继续调用 equals() 方法进一步判断它们的内容是否相同,如果相同就忽略这个新增的元素,如果不同就把它增加到 HashSet 中。
因此,在实际开发中,当使用 HashSet 存放某个自定义对象时,就得先在这个对象的定义类中重写 hashCode() 和 equals() 方法。hashcode 值是对象的映射地址,而 equals() 用于比较两个对象,判断 2 个元素的地址是否相等。 在重写时,hashCode() 方法需要自定义 “映射地址” 的映射规则,equals() 方法需要自定义对象的 “比较” 规则。一般而言,映射规则和比较规则都需要借助于对象的所有属性进行计算。
实际上,如何在 hashCode() 方法中设计计算公式是一个数学问题,我们不必深究。计算公式的目的是为了 “尽可能的避免不同对象计算出的 hash 值相同” ,普通开发者通常只需在 hashCode() 方法中使用全部的属性值进行计算即可。 例如,也可以将上面程序中的 hashCode() 方法重写为以下的简易形式。
1
2
3
4
|
@Override
public int hashCode() {
return name.hashCode() & oil;
}
|
小结:
在向 HashSet 集合中增加元素时,会先计算此元素的 hashcode 值,如果 HashSet 集合中没有此 hashcode 值,那么此元素就可以插入。如果 hashcode 值与 HashSet 集合中的某个元素的 hashcode 值相同,HashSet 就会 继续调用 euqals() 方法 进一步判断它们的内容是否相同,如果相同就忽略这个新增的元素,如果不同才能把它增加到 HashSet 集合中。
TreeSet 类的使用
TreeSet 类在实现 Set 接口的同时,也实现了 SortedSet 接口,是一个具有排序功能的 Set 接口实现类。
TreeSet 集合中的元素默认是按照自然 升序排列,并且 TreeSet 集合中的对象需要实现 Comparable 接口。Comparable 接口用于 比较集合中各个元素的大小,常用于排序操作。
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
|
import java.util.Set;
import java.util.TreeSet;
public class TestTreeSet {
public static void main(String[] args) {
Set ts = new TreeSet();
ts.add("王云");
ts.add("刘静涛");
ts.add("南天华");
System.out.println(ts);
ts.add(111);
System.out.println(ts);
// class java.lang.String cannot be cast to class java.lang.Integer // (java.lang.String and java.lang.Integer are in module java.base of loader
// 'bootstrap')
// 不能将 java.lang.String 类转换为 java.lang.Integer 类(java.lang.String 和
// java.lang.Integer 位于加载器 "bootstrap "的 java.base 模块中)
Set num = new TreeSet();
num.add(9);
num.add(-3);
num.add(27);
num.add(12);
System.out.println(num);
}
}
|
从运行结果可以看出,TreeSet 集合 ts 里面的元素不是毫无规律的排序,而是按照自然升序(这里是指 “字典” 里的顺序)进行了排序。
这是因为 TreeSet 集合中的元素是 String 类的,而 String 类实现了 Comparable 接口。 但如果 TreeSet 中的元素不是 String 类,如何进行排序呢?后面 “ 比较器 ” 实验中会为大家进行讲解。
使用 TreeSet 生成数组
- toArray() 方法,把集合中的所有数据提取到一个新的数组中。
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
|
import java.util.Random;
import java.util.TreeSet;
/**
* 使用 TreeSet 生成数组
*/
public class RandomSortArray {
public static void main(String[] args) {
// 创建 TreeSet 对象
TreeSet num = new TreeSet();
// 创建 Random 对象
Random ran = new Random();
int count = 0;
while(count < 10){
// 提取 0 - 99 的随机数加入到集合中
boolean succeed = num.add(ran.nextInt(100));
if(succeed){
count ++;
}
}
int size = num.size();
// 创建整型数组
Integer[] arr1 = new Integer[size];
// int[] arr1 = new int[size];
// 将集合元素转换为数组元素
num.toArray(arr1);
System.out.print("生成不重复随机数组内容如下:");
// for each 循环
for(int value : arr1){
System.out.print(value + " ");
}
}
}
|
Integer 和 int
- Integer 是 int 的包装类、是复杂数据类型、是一个类,而 int 则是 java 的一种基本的数据类型;
- Integer 的默认值是 null,而 int 的默认值是 0。
- int 是基本数据类型,直接存数值,integer 是对象,用一个引用指向这个对象。
- Integer 变量必须实例化之后才能使用,而 int 变量不需要实例化;
- Integer 实际是对象的引用,当 new 一个 Integer 时,实际上生成一个指针指向对象,而 int 则直接存储数值;
- 类似的还有:float Float,double Double,string String 等。
⭐ 增强 for 循环
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 普通for循环,利用下标
for(int i =0;i<arr.length;i++){}
// 增强 for 循环 arr.for
// 又叫做 for each 语句 for(int a:arr){}
// for(int value : arr) 即:遍历 arr 数组,每次遍历的整型用 i 这个变量去接收。
for(int value:arr){}
// 相当于:
int value = 0; //用于接收arr数组中的某一个对象
for(int j = 0;j < arr.length;j++){
value = arr[j];
}
|
内部比较器 Comparable 接口
JDK 提供了 Comparable 和 Comparator 两个接口,都可以用于定义集合元素的排序规则。 如果程序员想定义自己的排序方式,一种简单的方法就是让加入 TreeSet 集合中的对象所属的类实现 Comparable 接口,通过实现 compareTo(Object o) 方法,达到排序的目的。
假设有这样的需求,学生对象有两个属性,分别是学号和姓名。希望将这些学生对象加入 TreeSet 集合后,按照学号从小到大进行排序,如果学号相同再按照姓名自然排序。来看 Student 类需要实现 Comparable 接口。
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
|
/**
* 学生类实现 Comparable 接口
*/
public class Student implements Comparable {
// 学生学号
int stuNum = -1;
// 学生姓名
String stuName = "";
Student(String name, int num) {
this.stuNum = num;
this.stuName = name;
}
// 返回该对象的字符串表示,利于输出
public String toString() {
return "学号为:" + stuNum + " 的学生,姓名为:" + stuName;
}
// 实现 Comparable 的 compareTo() 方法
public int compareTo(Object o) {
Student input = (Student) o;
// 此学生对象的学号和指定学生对象的学号比较
// 此学生对象学号若大则 res 为 1,若小则 res 为 -1,相同的话 res = 0
int res = stuNum > input.stuNum ? 1 : (stuNum == input.stuNum ? 0 : -1);
// 若学号相同,则按照 String 类自然排序比较学生姓名
// 因为 String 类实现了 Comparable 接口
if (res == 0) {
res = stuName.compareTo(input.stuName);
}
return res;
// 返回值为 -1,表示左边的数比右边的数小,左右的数不进行交换。
// 返回值为 0,表示左边的数等于右边的数,左右的数不进行交换。
// 返回值为 1,表示左边的数比右边的数大,左右的数进行交换。
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
import java.util.Set;
import java.util.TreeSet;
/**
* 测试类
*/
class TestComparable {
public static void main(String[] args) {
//用有序的 TreeSet 存储学生对象
Set stuTS = new TreeSet();
stuTS.add(new Student("王云", 1));
stuTS.add(new Student("南天华", 3));
stuTS.add(new Student("刘静涛", 2));
stuTS.add(new Student("张平", 3));
//循环输出
for(Object stu : stuTS)
System.out.println(stu);
}
}
|
Iterator 迭代器
Iterator 接口的使用
前面学习的 Collection 接口、Set 接口(无索引)和 List 接口,它们的实现类 都没有提供遍历集合元素的方法。
Iterator 接口为遍历集合而生,是 Java 语言解决集合遍历的一个工具。
iterator() 方法定义在 Collection 接口中,因此所有单值集合的实现类,都可以通过 iterator() 方法实现遍历。 iterator() 方法返回值是 Iterator 对象,通过 Iterator 接口的 hasNext() 和 next() 方法即可实现对集合元素的遍历。
下面是 Iterator 接口的三个方法:
判断是否存在下一个可访问的数据元素。
返回要访问的下一个数据元素,通常和 hasNext() 在一起使用。
从迭代器指向的 Collection 集合中移除迭代器返回的上一个数据元素。
iterator 使用案例
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
|
/**
* 自定义车辆信息类
*/
public class Vehicle {
private String name;
private int oil;
public Vehicle() {
}
public Vehicle(String name, int oil) {
this.name = name;
this.oil = oil;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getOil() {
return oil;
}
public void setOil(int oil) {
this.oil = oil;
}
}
//轿车类
class Car extends Vehicle{
//品牌
private String brand = "红旗";
//构造方法,指定车名和品牌
public Car(String name, String brand) {
super(name, 20);
this.brand = brand;
}
//获取品牌
public String getBrand() {
return brand;
}
}
//卡车类
class Truck extends Vehicle{
// 吨位
private String load = "10吨";
//构造方法,指定车名和品牌
public Truck(String name, String load) {
super(name, 20);
this.load = load;
}
//获取吨位
public String getLoad() {
return load;
}
}
|
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
|
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
/**
* 使用迭代器遍历集合数据
*/
public class TestIterator {
public static void main(String[] args) {
// 创建 HashSet 集合,用于存放车辆
Set<Vehicle> vehSet = new HashSet<>();
// 创建两个轿车对象、两个卡车对象,并加入 HashSet 集合中
Vehicle c1 = new Car("战神", "长城");
Vehicle c2 = new Car("跑得快", "红旗");
Vehicle t1 = new Truck("大力士", "5吨");
Vehicle t2 = new Truck("大力士二代", "10吨");
vehSet.add(c1);
vehSet.add(c2);
vehSet.add(t1);
vehSet.add(t2);
// 使用迭代器循环输出
Iterator<Vehicle> it = vehSet.iterator();
System.out.println("*** 显示集合中元素信息 ***");
while (it.hasNext()) {
Vehicle vehicle = it.next(); // 判断集合是否存在下一个元素
if (vehicle instanceof Car) { // 获取集合元素
Car car = (Car) vehicle;
//调用 Car 类的特有方法 getBrand()
System.out.println("该车是轿车,其品牌为:" + car.getBrand());
} else {
Truck truck = (Truck) vehicle;
//调用 Truck 类的特有方法 getLoad()
System.out.println("该车是卡车,其吨位为:" + truck.getLoad());
}
System.out.println("车辆名称:" + vehicle.getName());
// 集合元素分隔显式
System.out.println("--------------------------");
}
}
}
|
使用 Iterator 显示数据信息
我们已经学习到,Iterator 迭代器中有 hashNext()、next()、remove() 方法,迭代器的使用方法是:
1
|
Iterator 迭代器名 = 集合名.iterator();
|
练习:创建一个 HashSet 集合,只允许添加 String 类型的元素,向该集合中添加 5 个元素,使用迭代器将该集合内的元素个数删减至只有 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
|
package com.company.collection.Integer;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
public class Demo3 {
public static void main(String[] args) {
Set<String> TT = new HashSet<String>();
TT.add("aaa");
TT.add("bbb");
TT.add("ccc");
TT.add("ddd");
TT.add("eee");
TT.add("fff");
Iterator<String> it = TT.iterator();
while (it.hasNext() && TT.size() > 2){
it.next();
it.remove();
}
System.out.println(TT);
}
}
|
练习:键盘录入一个长度为 10 的 int 类型的数组,使用 TreeSet 和 Iterator 将数组中的数从小到大排序,并打印输出。
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
|
package com.company.collection.Integer;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;
public class Demo4 {
public static void main(String[] args) {
// 创建一个 TreeSet 集合,规定里面的元素必须是整数
Set<Integer> treeSet = new TreeSet<Integer>();
int[] arr = new int[10];
Scanner sc = new Scanner(System.in);
for (int i = 0; i < arr.length; i++) {
System.out.print("请输入第" + (i + 1) + "个数字:");
arr[i] = sc.nextInt();
}
// int[] a = {12, 45, 23, 86, 100, 78, 546, 1, 45, 99, 136, 23};
for (int i = 0; i < arr.length; i++) {
treeSet.add(arr[i]);
}
Iterator<Integer> it = treeSet.iterator();
// 遍历 TreeSet 集合
while (it.hasNext()) {
System.out.print(it.next() + " ");
}
}
}
|
List 接口
List简介
List 是 Collection 接口的子接口,List 中的元素是 有序 的,有索引 的,而且可以重复。List 集合中的数据元素都对应一个整数形式的序号索引,记录其在集合中的位置,可以根据此序号存取元素。JDK 中常用的 List 实现类是 ArrayList 和 LinkedList。
List 接口继承自 Collection 接口,除了拥有 Collection 接口所拥有的方法外,还拥有下列方法:
1
2
3
4
5
6
7
8
9
10
11
|
void add(int index,Object o)
List myList = new ArrayList();
myList.add("aaa");
myList.add("bbb");
myList.add("ccc");
System.out.println(myList);
myList.add(1,"---");
System.out.println(myList);
|
在集合的指定 index 位置处,插入指定的 o 元素,如果没有写 index,则默认添加在末尾。
1
2
3
|
Object get(int index)
System.out.println(myList.get(2));
|
返回集合中 index 位置的数据元素。
1
2
3
|
int indexOf(Object o)
System.out.println(myList.indexOf("---"));
|
返回此集合中第一次出现的指定 o 元素的索引,如果此集合不包含 o 元素,则返回-1。
1
2
3
|
int lastIndexOf(Object o)
System.out.println(myList.lastIndexOf("bbb"));
|
返回此集合中最后出现的指定 o 元素的索引,如果此集合不包含 o 元素,则返回-1。
1
2
3
4
5
6
7
|
Object remove(int index)
myList.remove("ccc");
myList.remove(3);
Integer i = Integer.valueOf(1);
myList.remove(i);
|
移除集合中 index 位置的数据元素,返回被删除的元素。
注意:此处有两个 remove,一个是通过索引号删除元素,另一个是通过元素本身删除元素,那么,如果 List 集合中有元素 1,又有索引 1,此时执行 myList.remove(1); 程序会怎样运行?
1
2
3
4
5
6
7
8
9
|
List myList = new ArrayList();
myList.add("aaa");
myList.add(1);
System.out.println(myList);
// List 集合中有元素 1,又有索引 1
System.out.println(myList.remove(1));
System.out.println(myList);
|
在调用方法时,如果方法出现重载现象,优先调用实参和形参类型一致的那个方法,而 1 默认是 int 数据类型,如果是调用 remove(Object o) 方法,还需要把 1 转换为 Integer 类型才能调用。
-
1
2
3
|
Object set(int index,Object o)
myList.set(1,"===");
|
用指定的 o 元素替换集合中 index 位置的数据元素,返回被修改的元素。
ArrayList 底层原理
ArrayList 实现了 List 接口,其底层采用的数据结构是数组。另一个 List 接口的实现类是 LinkedList,它在存储方式上采用链表进行链式存储。
根据数据结构的知识可知,数组(顺序表)在插入或删除数据元素时,需要批量移动数据元素,故性能较差;但在根据索引获取数据元素时,因为数组是连续存储的,所以在遍历元素或随机访问元素时效率高。
那么本实验需要学习的 ArrayList 实现类的底层就是数组,因此 ArrayList 实现类更加适合根据索引访问元素的操作。
1
2
3
4
5
6
7
8
9
10
|
ArrayList<String> list1 = new ArrayList<>();
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
System.out.println(list1);
// ArrayList 的 addAll() 方法,添加所有元素
ArrayList<String> list2 = new ArrayList<>();
list2.addAll(list1);
System.out.println(list2);
|
ArrayList 集合的扩容机制
-
利用空参创建集合,在底层创建一个 默认长度为 0 的数组。注意!没有存入元素的时候,数组长度为0。
-
添加第一个元素时,底层会创建一个 新的长度为10 的数组。
-
存满时,数组 扩容至 1.5 倍。
-
如果一次添加多个元素,即使进行 1.5 倍扩容后也存储不下的话,则新创建数组的长度 以实际需求为准。
ArrayList 的使用
假设车辆管理有如下需求:
-
用户可以按照车辆入库的顺序查阅车辆信息。
-
所有车辆有连续的编号,当用户输入车辆的编号后系统显示车辆完整信息。
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
|
/**
* 自定义车辆信息类
*/
public class Vehicle {
private String name;
private int oil;
public Vehicle() {
}
public Vehicle(String name, int oil) {
this.name = name;
this.oil = oil;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getOil() {
return oil;
}
public void setOil(int oil) {
this.oil = oil;
}
}
|
创建 ArrayList 集合,存放车辆信息
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
|
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* ArrayList 类的基本使用
*/
public class TestArrayList{
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
// 创建 ArrayList 集合,用于存放车辆
List vehAL = new ArrayList();
Vehicle c1 = new Car("战神","长城");
Vehicle c2 = new Car("跑得快","红旗");
Vehicle t1 = new Truck("大力士","5吨");
Vehicle t2 = new Truck("大力士二代","10吨");
// 将 c1 添加到 vehAL 集合的末尾
vehAL.add(c1);
vehAL.add(c2);
vehAL.add(t1);
vehAL.add(t2);
System.out.println("*** 显示全部车辆 ***");
// 用于显示序号
int num = 1;
// 增强for循环遍历 for(int a:arr){}
for(Object obj:vehAL){
if(obj instanceof Car) { // 判断其左边对象是否为其右边类的实例
Car car = (Car)obj;
System.out.println(num + " 该车是轿车,其车名为:" + car.getName());
}else{
Truck truck = (Truck)obj;
System.out.println(num + " 该车是卡车,其车名为:" + truck.getName());
}
num++;
}
System.out.print("请输入要显示车名的车辆编号:");
String name = ((Vehicle)vehAL.get(input.nextInt()-1)).getName();
System.out.println("车辆名称为:"+name);
}
}
//轿车类
class Car extends Vehicle{
//品牌
private String brand = "红旗";
//构造方法,指定车名和品牌
public Car(String name, String brand) {
super(name, 20);
this.brand = brand;
}
//获取品牌
public String getBrand() {
return brand;
}
}
//卡车类
class Truck extends Vehicle{
// 吨位
private String load = "10吨";
//构造方法,指定车名和品牌
public Truck(String name, String load) {
super(name, 20);
this.load = load;
}
//获取吨位
public String getLoad() {
return load;
}
}
|
练习:创建一个 ArrayList 集合,用来存储 Teacher 对象,其中,Teacher 对象的属性至少包含编号 id,姓名 name,性别 gender,创建5个实例化对象并将其添加到集合中,使用增强 for 循环遍历数组,分别输出每个老师的详细信息。
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
134
135
136
137
138
139
140
141
142
|
package com.company.collection;
import java.util.ArrayList;
import java.util.List;
public class ListDemo2 {
public static void main(String[] args) {
List<Teacher> l1 = new ArrayList<>();
Teacher t1 = new Teacher(1, "张三", "男");
Teacher t2 = new Teacher(2, "李四", "男");
Teacher t3 = new Teacher(3, "王五", "女");
Teacher t4 = new Teacher(4, "赵六", "男");
l1.add(t1);
l1.add(t2);
l1.add(t3);
l1.add(0, t4);
System.out.println(l1);
for (Teacher teacher : l1) {
if (teacher.getGender().equals("男")) {
System.out.println("男老师:" + teacher.getName() + ",编号为:" + teacher.getId());
} else {
System.out.println("女老师:" + teacher.getName() + ",编号为:" + teacher.getId());
}
}
}
}
class Teacher {
private int id;
private String name;
private String gender;
// toSting 方法
// get set 方法
// 构造函数 空参 全参
public Teacher() {
}
public Teacher(int id, String name, String gender) {
this.id = id;
this.name = name;
this.gender = gender;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String toString() {
return "Teacher{id = " + id + ", name = " + name + ", gender = " + gender + "}";
}
}
class Man extends Teacher {
private String car;
public Man() {
}
public Man(String car) {
this.car = car;
}
/**
* 获取
*
* @return car
*/
public String getCar() {
return car;
}
/**
* 设置
*
* @param car
*/
public void setCar(String car) {
this.car = car;
}
public String toString() {
return "Man{car = " + car + "}";
}
}
class Femal extends Teacher {
private String skill;
public Femal() {
}
public Femal(String skill) {
this.skill = skill;
}
/**
* 获取
*
* @return skill
*/
public String getSkill() {
return skill;
}
/**
* 设置
*
* @param skill
*/
public void setSkill(String skill) {
this.skill = skill;
}
public String toString() {
return "Femal{skill = " + skill + "}";
}
}
|
LinkedList 与 Comparator 组合
本实验将学习 LinkedList 与 Comparator 结合使用。LinkedList,它在存储方式上采用链表进行链式存储;而 Comparator 是外部比较器。
LinkedList 的底层是链表。LinkedList 和 ArrayList 在应用层面类似,只是底层存储结构上的差异导致了二者对于不同操作,存在性能上的差异。这其实就是顺序表和链表之间的差异。一般而言,对于 “索引访问” 较多的集合操作建议使用 ArrayList 实现类,而对于 “增删” 较多的集合操作建议使用 LinkedList 实现类。
LinkedList 实现类除了拥有 ArrayList 实现类提供的方法外,还增加了如下一些方法:
1
|
void addFirst(Object o)
|
将指定数据元素插入此集合的开头。
将指定数据元素插入此集合的结尾。
返回此集合的第一个数据元素。
返回此集合的最后一个数据元素。
移除并返回此集合的第一个数据元素。
移除并返回此集合的最后一个数据元素。
Comparator 可以理解为一个专用的比较器,当集合中的对象不支持自比较或者自比较的功能不能满足程序员的需求时,就可以写一个实现 Comparator 接口的比较器来完成两个对象之间的比较,从而实现按比较器规则进行排序的功能。
例如,要比较的对象是 JDK 中内置的某个类,而这个类又 没有实现 Comparable 接口,因此我们是 无法直接修改 JDK 内置类的源码 的,因此就 不能通过重写 compareTo(Object o) 方法来定义排序规则 了,而应该 使用 Comparator 接口实现比较器功能。
接下来,在外部定义一个姓名比较器和一个学号比较器,然后在使用 Collections 工具类的 sort(List list, Comparator c) 方法时选择使用其中一种外部比较器,对集合里的学生信息按姓名、学号分别排序输出。
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
|
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedList;
/**
* LinkedList 与 Comparator 结合使用
*/
public class TestLinkedList{
public static void main(String[] args){
//用LinkedList存储学生对象
LinkedList stuLL = new LinkedList();
stuLL.add(new Student("王云",1));
stuLL.add(new Student("南天华",3));
stuLL.add(new Student("刘静涛",2));
//使用sort方法,按姓名比较器进行排序
Collections.sort(stuLL,new NameComparator());
System.out.println("*** 按学生姓名顺序输出学生信息 ***");
for (Object object : stuLL) {
System.out.println(object);
}
//使用sort方法,按学号比较器进行排序
Collections.sort(stuLL,new NumComparator());
System.out.println("*** 按学生学号顺序输出学生信息 ***");
for (Object object : stuLL) {
System.out.println(object);
}
}
}
// 定义学生对象,未实现Comparable接口
class Student{
int stuNum = -1;
String stuName = "";
Student(String name, int num) {
this.stuNum = num;
this.stuName = name;
}
@Override
public String toString() {
return "学号为:" + stuNum + " 的学生,姓名为:" + stuName;
}
}
//定义一个姓名比较器
class NameComparator implements Comparator {
//实现Comparator接口的compare()方法
public int compare(Object op1, Object op2) {
Student eOp1 = (Student)op1;
Student eOp2 = (Student)op2;
//通过调用String类的compareTo()方法进行比较
return eOp1.stuName.compareTo(eOp2.stuName);
}
}
//定义一个学号比较器
class NumComparator implements Comparator {
//实现Comparator接口的compare()方法
public int compare(Object op1, Object op2) {
Student eOp1 = (Student)op1;
Student eOp2 = (Student)op2;
return eOp1.stuNum - eOp2.stuNum;
}
}
** <font color='red'>\\\</font> **
|
集合的遍历方式行
普通 for 循环
1
2
3
4
5
6
7
8
9
10
|
List<Teacher> l1 = new ArrayList<>();
Teacher[] arr = new Teacher[l1.size()];
for (int i = 0; i < l1.size(); i++) {
arr[i] = l1.get(i);
}
for (int i = 0; i < l1.size(); i++) {
System.out.println(arr[i]);
}
|
增强 for 循环
1
2
3
4
5
|
List<Teacher> l1 = new ArrayList<>();
for (Teacher teacher : l1) {
System.out.println(teacher);
}
|
迭代器 iterator
1
2
3
4
5
6
7
|
List<Teacher> l1 = new ArrayList<>();
Iterator it = l1.iterator();
while (it.hasNext()) {
it.next();
it.remove();
}
|
lambda 表达式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
List<Teacher> l1 = new ArrayList<>();
// 调用 forEach 方法,传递一个 Consumer 接口的实现类对象
l1.forEach(new Consumer<Teacher>() {
@Override
public void accept(Teacher t) {
System.out.println(t);
}
});
// 删除 new Consumer ~ accept,改用 -> 表示 lambda 特殊格式
l1.forEach((Teacher t) ->{
System.out.println(t);
}
);
// 省略数据类型,形参只有一个,省略(),方法体只有一行,省略{;}
l1.forEach( t -> System.out.println(t));
// 将 lambda 替换为方法引用
l1.forEach(System.out::println);
|
泛型
泛型的定义
泛型:是 JDK5 引入的特性,可以在编译阶段约束操作的数据类型,并进行检查。
泛型的格式:<数据类型>
1
|
集合<数据类型> 引用名 = new 集合实现类<数据类型> ();
|
⭐ 注意:使用泛型约束的数据类型必须是 对象类型(引用数据类型),而不能是基本数据类型。
1
2
3
4
5
6
7
8
9
10
|
ArrayList<String> list1 = new ArrayList<>();
list1.add("aaa");
list1.add("bbb");
list1.add("ccc");
// 如果我想存整数,需要将 String 改为 Integer
ArrayList<Integer> list2 = new ArrayList<>();
list2.add(111);
list2.add(222);
list2.add(333);
|
没有范式的时候,集合如何存储数据?
1
2
3
4
5
6
7
8
9
10
11
12
|
ArrayList list = new ArrayList();
list.add(123);
list.add("aaa");
list.add(new StuTest("张三",18));
Iterator it = list.iterator();
while (it.hasNext()){
// 由于没有写泛型,所有对象被提升为Object类型,此时可以往集合中添加任意的数据类型
// 而Object类型在获取时是不能获取子类的特有行为的
Object obj = it.next(); // ctrl + alt + v 自动生成左边
System.out.println(obj);
}
|
泛型的基本使用
在之前使用集合的时候,装入集合的各种类型的元素都被当作 Object 对待,而非元素自身的类型。因此从集合中取出某个元素时,就需要进行类型转换,这种做法效率低下且容易出错。
🤔 那么如何解决这个问题呢?——可以使用泛型。
泛型是指在定义集合的同时也定义集合中元素的类型,需要 “< >” 进行指定,其语法形式如下:
1
|
集合<数据类型> 引用名 = new 集合实现类<数据类型> ();
|
以下代码就限制了 List 集合中只能存放 String 类型的元素。
1
|
List<String> list = new ArrayList<String>();
|
在 JDK1.7 之后,= 右边 < > 中的的 String 等类型也可以省略,也可以写成以下的等价形式:
1
|
List<String> list = new ArrayList<>();
|
在定义集合的同时使用泛型,用 “< >” 进行指定集合中元素的类型后,再从集合中取出某个元素时,就不需要进行类型转换,不仅可以提高程序的效率,也让程序更加清晰明了,易于理解。
Map 接口
Map 接口,用于保存具有映射关系的键值对数据。Map 中的键无序,无索引,不重复。
Map<K,V> 接口中的 key 和 value 可以是任何引用类型的数据,key 不允许重复,原因和 HashSet 一样,value 可以重复,key 和 value 都可以是 null 值。
需要注意的是,key 为 null 只能有一个,value 为 null 可以多个,它们之间存在单向一对一关系,也就是说 通过指定存在的 key 一定找到对应的 value 值。
Map 接口的常用方法如下:
1
2
3
4
5
6
7
8
9
10
|
Object put(Object key,Object value)
Map<String,Integer> m = new HashMap<>();
m.put("张三",19);
m.put("李四",19);
m.put("张三",22);
System.out.println(m);
m.put("张三",19);
System.out.println(m);
|
将指定键值对(key 和 value)添加到 Map 集合中,如果此 Map 集合以前包含一个该键 key 的键值对,则用参数 key 和 value 替换旧值。
1
2
3
|
Object get(Object key)
System.out.println(m.get("张三"));
|
返回指定键 key 所对应的值,如果此 Map 集合中不包含该键 key,则返回 null。
1
2
3
|
Object remove(Object key)
System.out.println(m.remove("王五"));
|
如果存在指定键 key 的键值对,则将该键值对从此 Map 集合中移除。
1
2
3
|
Set keySet()
System.out.println(m.keySet());
|
返回此 Map 集合中包含的键的 Set 集合。
1
2
3
|
Collection values()
System.out.println(m.values());
|
返回此 Map 集合中包含的值的 Collection 集合。
1
2
3
|
boolean containsKey(Object key)
System.out.println(m.containsKey("老王"));
|
如果此 Map 集合包含指定键 key 的键值对,则返回 true。
1
2
3
|
boolean containsValue(Object value)
System.out.println(m.containsValue(19));
|
如果此 Map 集合将一个或多个键映射到指定值,则返回 true。
1
2
3
|
int size()
System.out.println(m.size());
|
返回此 Map 集合的键值对的个数。
HashMap 类的使用
Map 接口常用的实现类有 HashMap 和 Hashtable 以及 TreeMap。
HashMap 的特点
① HashMap 是 Map 里面的一个实现类。
② 没有额外需要学习的特有方法,直接使用 Map 里面的方法就可以了。
③ 特点都是由键决定的:无序、不重复、无索引,与值无关。
④ HashMap 跟 HashSet 底层原理是一模一样的,都是哈希表结构(数组,链表,红黑树)。
⑤ 依赖 hashCode 方法和 equals 方法保证键的唯一
⑥ 如果键存储的是自定义对象,需要重写 hashCode 和 equals 方法。
⑦ 如果值存储的是自定义对象,不需要重写 hashCode 和 equals 方法
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
|
import java.util.HashMap;
import java.util.Map;
/**
* HashMap 类的基本使用
*/
public class TestHashMap {
public static void main(String[] args) {
// 使用 HashMap 存储域名和含义键值对的集合
Map<String, String> domains = new HashMap<>();
domains.put("com", "工商企业");
domains.put("net", "网络服务商");
domains.put("org", "非营利组织");
domains.put("edu", "教研机构");
domains.put("gov", "政府部门");
// 通过键获取值
System.out.println("edu国际域名对应的含义为:" + domains.get("edu"));
// 判断是否包含某个键
System.out.println("domains键值对集合中是否包含gov:" + domains.containsKey("gov"));
// 删除键值对
domains.remove("gov");
System.out.println("删除后集合中是否包含gov:" + domains.containsKey("gov"));
// 输出全部键值对
System.out.println(domains);
}
}
|
Map 映射数据的遍历
我们已经掌握了遍历 Collection 的通用方法—使用增强 for 或者迭代器 Iterator,并且知道 Collection 是单值形式元素的集合,而 Map 是键值对形式的元素集合。
因此就能推测出,遍历 Map 的方法就是:先将 Map 集合或 Map 集合的部分元素 转换成单值集合 的形式,然后使用增强 for 或者迭代器 Iterator 遍历即可。
简单来说就是: Map –> 转换为单值集合 –> 使用增强 for 或者迭代器 Iterator 遍历。
我们可以将 Map 中的 key 全部提取出来,遍历 key,然后再根据 key 获取 value,如以下程序所示。
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
|
package com.company.collection.map;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
public class MapDemo1 {
public static void main(String[] args) {
HashMap<Student,String> hm = new HashMap<>();
Student s1 = new Student("张三",23);
Student s2 = new Student("李四",23);
Student s3 = new Student("王五",23);
Student s4 = new Student("赵六",23);
hm.put(s1,"长沙");
hm.put(s2,"上海");
hm.put(s3,"浙江");
hm.put(s4,"深圳");
// 遍历集合
// 使用 keySet 方法返回一个键的 Set 集合
Set<Student> k = hm.keySet();
// 增强 for 遍历该 Set 集合,得到每一个 student 对象
for (Student student : k) {
// 通过 get 方法获取 student 对象对应的值
String v = hm.get(student);
System.out.println(student + "=" + v);
}
System.out.println("------------------------------------");
// 使用 Iterator 迭代器遍历
Iterator<Student> i = k.iterator();
while (i.hasNext()){
System.out.println(i.next() + "=" + hm.get(i.next()));
}
}
}
class Student{
private String name;
private int age;
public Student() {
}
public Student(String name, int age) {
this.name = name;
this.age = age;
}
/**
* 获取
* @return name
*/
public String getName() {
return name;
}
/**
* 设置
* @param name
*/
public void setName(String name) {
this.name = name;
}
/**
* 获取
* @return age
*/
public int getAge() {
return age;
}
/**
* 设置
* @param age
*/
public void setAge(int age) {
this.age = age;
}
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);
}
}
|
另一种方式遍历:Map 中的每一组 key-value 对称为一个 entry 对象,即 entry = key + value。Map 接口提供了获取 Map 中全部 entry 对象的方法,因此就可以先获取全部的 entry 对象,然后再提取 entry 对象中的 key 和 value。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
package com.company.collection.map;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapDemo2 {
public static void main(String[] args) {
Map<String, String> m = new HashMap<>();
m.put("k1", "v1");
m.put("k2", "v2");
m.put("k3", "v3");
//获取 Map 的全部 entry 对象
// m.entrySet + ctrl + alt + v
Set<Map.Entry<String, String>> entries = m.entrySet();
// entries.for
for (Map.Entry<String, String> entry : entries) {
String k = entry.getKey();
String v = entry.getValue();
System.out.println(k + "," + v);
}
}
}
|
🏆 进阶:控制台录入一个字符串,程序经过统计最后输出这个字符串中每个字母出现的次数。
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
|
package com.company.collection.map;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
public class Statistics {
static HashMap<Character, Integer> hashMap = new HashMap<>();
public Map statis(String str) {
// 判断 map里面是否存过key=c的键
//有则 查找其value并且加1; 再存进map里面
for (char c : str.toCharArray()) {
if (hashMap.containsKey(c)) {
int value = hashMap.get(c);
value++;
hashMap.put(c, value);
} else {
hashMap.put(c, 1);
}
}
return hashMap;
}
public static void main(String[] args) {
Scanner input = new Scanner(System.in);
System.out.println("请输入一个字符串:");
String str = input.next();
//将非字母的字符去掉
String str1 = str.replaceAll("[^a-zA-Z]", "");
Statistics statistics = new Statistics();
statistics.statis(str1);
Set<Character> set = hashMap.keySet();
for (char c : set) {
int value = hashMap.get(c);
System.out.println(c + "=" + value);
}
}
}
|
工具类
Collections 工具类的使用
Collections 工具类,是集合对象的工具类,类中方法都是静态的,可以直接以 类名.静态方法() 的形式调用。
该类提供了操作集合的工具方法,如排序、复制、反转和查找等方法,如下所示:
1
|
boolean addAll(Collection<T> c, T ... elements)
|
向单列集合 c 中批量添加元素。
1
2
3
4
5
6
|
void sort(List list)
List<Integer> l1 = new ArrayList<>();
Collections.addAll(l1,11,22,33,44);
System.out.println(l1);
|
根据数据元素的排序规则对 List 集合进行排序,其中的排序规则是通过内部比较器设置的。例如 List 中存放的是 obj 对象,那么排序规则就是根据 obj 所属类重写内部比较器 Comparable 中的 compareTo() 方法定义的。
1
2
3
4
5
6
|
void sort(List list, Comparator c)
Collections.addAll(l1,44,11,22,33);
Collections.sort(l1);
System.out.println(l1);
|
根据指定比较器中的规则对 List 集合进行排序。通过自定义 Comparator 比较器 c,可以实现按程序员定义的规则进行排序。
1
2
3
4
|
void shuffle(List<?> list)
Collections.shuffle(l1);
System.out.println(l1);
|
对指定 List 集合进行随机排序。
1
2
3
4
|
void reverse(List list)
Collections.shuffle(l1);
System.out.println(l1);
|
反转 List 集合中数据元素的顺序。
1
2
3
4
|
Object max(Collection coll)
Collections.reverse(l1);
System.out.println(l1);
|
根据数据元素的自然顺序,返回给定 coll 集合中的最大元素。该方法的输入类型为 Collection 接口,而非 List 接口,因为求集合中最大元素不需要集合是有序的。
1
|
Object min(Collection coll)
|
根据数据元素的自然顺序,返回给定 coll 集合的最小元素。
1
|
int binarySearch(List list,Object o)
|
使用二分查找法查找 list 集合,以获得 o 数据元素的索引。如果此集合中不包含 o 元素,则返回-1。在进行此调用之前,必须根据 list 集合数据元素的自然顺序对集合进行升序排序(通过 sort(List list) 方法)。如果没有对 list 集合进行排序,则结果是不确定的。如果 list 集合中包含多个元素 “等于” 指定的 o 元素,则无法保证找到的是哪一个,这里说的 “等于” 是指通过 equals() 方法判断相等的元素。
1
|
int indexOfSubList(List source,List target)
|
返回指定源集合 source 中第一次出现指定目标集合 target 的起始位置,换句话说,如果 target 是 source 的一个子集合,那么该方法返回 target 在 source 中第一次出现的位置。如果没有出现这种集合间的包含关系,则返回 -1。
1
|
int lastIndexOfSubList(List source,List target)
|
返回指定源集合 source 中最后一次出现指定目标集合 target 的起始位置,如果没有出现这样的集合,则返回 -1。
1
2
3
4
5
6
7
8
9
|
void copy(List dest,List src)
List<Integer> l1 = new ArrayList<>();
Collections.addAll(l1, 11, 33, 22, 44);
List<Integer> l2 = new ArrayList<>();
Collections.addAll(l1, 55, 77, 66, 88);
Collections.copy(l1, l2);
System.out.println(l1);
|
将所有数据元素从 src 集合复制到 dest 集合。
1
2
3
4
|
void fill(List list,Object o)
Collections.fill(l1, 0);
System.out.println(l1);
|
使用 o 数据元素替换 list 集合中的所有数据元素。
1
|
boolean replaceAll(List list,Object old,Object new)
|
使用一个指定的 new 元素替换 list 集合中出现的所有指定的 old 元素。
1
2
3
4
|
void swap(List list,int i,int j)
Collections.swap(l1,2,3);
System.out.println(l1);
|
交换 List 集合中两个位置的元素。
sort()、shuffle()、reverse()、max()、min()、binarySearch()、fill()、replaceAll()、swap() 等方法的使用。
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
|
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class TestCollections {
public static void main(String[] args) {
List list = new ArrayList();
list.add("w");
list.add("o");
list.add("r");
list.add("l");
list.add("d");
System.out.println("排序前: " + list);
System.out.println("该集合中的最大值:" + Collections.max(list));
System.out.println("该集合中的最小值:" + Collections.min(list));
Collections.sort(list);
System.out.println("sort排序后:" + list);
//使用二分查找,查找前须保证被查找集合是自然有序排列的
System.out.println("r在集合中的索引为:" + Collections.binarySearch(list, "r"));
Collections.shuffle(list);
System.out.println("再shuffle排序后:" + list);
Collections.reverse(list);
System.out.println("再reverse排序后:" + list);
Collections.swap(list, 1, 4);
System.out.println("索引为1、4的元素交换后:" + list);
Collections.replaceAll(list, "w", "d");
System.out.println("把w都换成d后的结果:" + list);
Collections.fill(list, "s");
System.out.println("全部填充为s后的结果:" + list);
}
}
|
Arrays 工具类的使用
Arrays 类是操作数组的工具类,和 Collections 工具类相似,Arrays 类主要有以下功能:
- 对数组进行排序。
- 给数组赋值。
- 比较数组中元素的值是否相等。
- 进行二分查找。
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
|
import java.util.Arrays;
/**
* Arrays 工具类的使用
*/
public class TestArrays {
public static void output(int[] a) {
for (int num : a) {
System.out.print(num + " ");
}
System.out.println();
}
public static void main(String[] args) {
int[] array = new int[5];
//填充数组
Arrays.fill(array, 8);
System.out.println("填充数组Arrays.fill(array,8):");
TestArrays.output(array);
//将数组索引为1到4的元素赋值为6
Arrays.fill(array, 1, 4, 6);
System.out.println("将数组索引为1到4的元素赋值为6 Arrays.fill(array, 1, 4, 6):");
TestArrays.output(array);
int[] array1 = {12, 9, 21, 43, 15, 6, 19, 77, 18};
//对数组索引为3到7的元素进行排序
System.out.println("排序前,数组的序列为:");
TestArrays.output(array1);
Arrays.sort(array1, 3, 7);
System.out.println("对数组索引为3到7的元素进行排序:Arrays.sort(array1,3,7):");
TestArrays.output(array1);
//对数组进行自然排序
Arrays.sort(array1);
System.out.println("对数组进行自然排序 Arrays.sort(array1):");
TestArrays.output(array1);
//比较数组元素是否相等
int[] array2 = array1.clone();
System.out.println("数组克隆后是否相等:Arrays.equals(array1, array2):" +
Arrays.equals(array1, array2));
//使用二分查找法查找元素下标(数组必须是排好序的)
System.out.println("77在数组中的索引:Arrays.binarySearch(array1, 77):"
+ Arrays.binarySearch(array1, 77));
}
}
|
综合案例
某班有 40 个学生,学号为 180201-180240, 全部参加 Java 集合阶段检测,给出所有同学的成绩 (可随机产生,范围为 50-100),请编写程序将本班 各位同学成绩从高往低排序打印输出。
注:成绩相同时学号较小的优先打印。
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
|
package entity;
public class Student {
public int id;
String name;
public int score;
@Override
public String toString() {
return "学号:" + id + ", 姓名:" + name + ", 成绩:" + score;
}
public Student() {
}
public Student(int id, String name, int score) {
this.id = id;
this.name = name;
this.score = score;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
}
|
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
|
package main;
import entity.Student;
import java.util.*;
public class Results {
public static List<Student> data = new ArrayList<Student>();
public void initData() {
Random random = new Random();
int k;
for (int i = 0; i < 40; i++) {
k = i + 1;
Student student = new Student(180201 + i, ("J" + k), (random.nextInt(50) + 50));
data.add(student);
}
}
public void adjust() {
Collections.sort(data, new GradeComparator());
}
public void print() {
for (int i = 0; i < data.size(); i++) {
Student s = data.get(i);
System.out.println(s);
}
}
//定义一个成绩比较器
static class GradeComparator implements Comparator {
@Override
public int compare(Object o1, Object o2) {
Student O1 = (Student) o1;
Student O2 = (Student) o2;
if(O2.score == O1.score){
return O1.id-O2.id;
}
return O2.score - O1.score;
}
public static void main(String[] args) {
Results results = new Results();
results.initData();
results.adjust();
results.print();
}
}
}
|