List集合类_演练
参考:
- Java集合简介 - Java教程 - 廖雪峰的官方网站
- 金老师的自学网站
- 《Java核心技术卷1》
引入
集合的概念
见P241
什么是集合(Collection)?集合就是“由若干个确定的元素所构成的整体”。例如:
- 5只小兔构成的集合
- 一个班所有的同学构成的集合
- 全体自然数集合:1,2,3,……
这个概念源自数学,在数学中,将放在集合中的数称为“元素”。
计算机科学中的集合,其实是从数学中的“集合”概念衍生出来的,它可以看成是一组对象的“容器”,你可以把若干对象塞进集合这一容器中去。
不同的容器采用不同的方式管理这些对象(例如,有些是整齐有序的排放、有些是乱放)。
数组
很显然,Java的数组可以看作是一种集合。
定义学生类:
public class Student {
String name;
int score;
Student(String name, int score) {
this.name = name;
this.score = score;
}
public String toString() {
return name + ", " + score + "分";
}
}
现在需要保存管理一些学生信息,可以用数组来保存管理:
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("张3", 72);
students[1] = new Student("李4", 90);
students[2] = new Student("王5", 88);
Student student1 = students[0];
System.out.println(student1);
// 一旦创建了数组,就不能再改变它的长度
// students[3] = new Student("赵6", 66); x
}
}
既然Java提供了数组这种数据类型,可以充当集合,那么,我们为什么还需要其他集合类?
这是因为数组存在着许多缺陷,例如它的一大特点:一旦创建就不能改变长度。
为了克服这个缺陷我们引入了数组列表。
内容
见P241如同9.1
实际上数组列表只是整个Java集合框架的一小部分(虽然它最常用)
接下来我们首先回顾一下数组列表
然后介绍将接口与实现分离的设计思想
进一步引入List与LinkedList
最后将介绍如何用Collections.sort方法对列表进行排序,并针对以上内容完成一些相关的练习。
数组列表(ArrayList)
回顾数组列表的基本信息:
import java.util.ArrayList;
public class Main {
public static void main(String[] args) {
Student[] students = new Student[3];
students[0] = new Student("张3", 72);
students[1] = new Student("李4", 90);
students[2] = new Student("王5", 88);
Student student1 = students[0];
System.out.println(student1);
// 一旦创建了数组,就不能再改变它的长度
// students[3] = new Student("赵6", 66); x
ArrayList<Student> list = new ArrayList<>();
/*
ArrayList基本信息
0 被定义在 java.utility 包中,使用之前需要通过 import 导入
Java 的主要集合类都定义在 java.util 包中
1 可理解为数组的升级版本
2 ★有类型参数的泛型类(为了指定数组列表要保存的元素类型,需用尖括号
将类名括起来追加到ArrayList的后面,例如ArrayList<Student>)
实际上 绝大多数 java集合框架中的类和接口都是泛型化的
有时会看到类似于 ArrayList list = new ArrayList(); 的代码
这也是合法的,但实际上这是旧版本的代码,因为jdk5之前,java还没引入泛型的概念
这种 原始类型 不推荐使用,存在很多 潜在问题
3 ★最大特性——在添加或删除元素时,能自动调整容量,而不需要为此额外编写代码.(简称动态扩容)
4 ★数组列表的底层数据结构是 数组 ( 可通过看源码验证, 实际维护了一个Object数组 )
具体用法 见书253页 表9.3
这些方法 不需 要去死记,实际开发中都是需要用的时候去查一下文档
在这门课中,会用到的一些方法会在后续练习中呈现,也仅仅是这些方法的 一小部分
*/
list.add(new Student("张3", 72));
list.add(new Student("李4", 90));
list.add(new Student("王5", 88));
list.add(new Student("赵6", 60));
Student student2 = list.get(0);
System.out.println(student2);
}
}
列表(List)
除了上述基本信息外,还有一些补充说明
如图9.1:
- ArrayList → 实现了 → List 接口
- List 接口 → 继承了 → Collection 接口
以上内容可通过查看源码验证。
这意味着可以用List这个接口变量去引用实现了该接口的ArrayList类的对象。
List<Student> list = new ArrayList<>();
这其实是一种推荐的做法,为理解为何这么做,引入一个简单的小例子。
接口回顾
- 接口可理解为一种特殊的抽象类(没有实例字段、所有方法都是抽象方法)
- 两个关键字:用
interface
关键字声明接口,用implements
关键字实现接口 - 不能实例化,只能被实现,强迫实现类定义接口中的所有(抽象)方法,否则编译报错
举个例子,假设定义了一个接口WashingMachine
:
// 洗衣机的接口,描述洗衣机应该具备的功能
public interface WashingMachine {
void turnOn(); // 打开
void turnOff(); // 关闭
void use(); // 洗衣服
}
一旦实现这个接口,就意味着必须实现它所定义的所有功能(即:将接口中的所有抽象方法具体化。也就是需要去掉abstract
并添加方法体):
// 实现家用电器接口的洗衣机类,具体提供方法的实现
public class MyWashingMachine implements WashingMachine {
public void turnOn() {
System.out.println("我的洗衣机已开启。");
}
public void turnOff() {
System.out.println("我的洗衣机已关闭。");
}
public void use() {
System.out.println("我的洗衣机正在洗衣服。需要50分钟");
}
}
进一步,可以用接口变量去引用实现了该接口的类的对象,并调用接口中规定的方法:
public class Main {
public static void main(String[] args) {
WashingMachine washingMachine = new MyWashingMachine();
washingMachine.turnOff();
washingMachine.turnOn();
washingMachine.use();
}
}
输出结果:
我的洗衣机已关闭。
我的洗衣机已开启。
我的洗衣机正在洗衣服。需要50分钟
根据口诀:能调什么方法看左边,实际调什么看右边。假设MyWashingMachine
还有其他功能,使用接口引用它实际上阉割了它能够使用的功能,那这个有什么用?(例如MyWashingMachine
还有一个飞行的功能,一旦用接口变量引用时候就调用不了了!)。
将接口(interface)与实现(implementation)分离
这实际上是面向对象编程中的一种设计原则,叫作将接口(interface)与实现(implementation)分离。
便于替换是这种设计原则的一大好处,假设现在推出一个更好的洗衣机:
public class SuperWashingMachine implements WashingMachine {
public void turnOn() {
System.out.println("洗衣机已开启。");
}
public void turnOff() {
System.out.println("洗衣机已关闭。");
}
public void use() {
System.out.println("超级洗衣机正在洗衣服。需要5秒钟");
}
}
只需要改动很少的代码,就可以实现实际功能的替换:
WashingMachine washingMachine = new SuperWashingMachine();
输出结果:
洗衣机已关闭。
洗衣机已开启。
超级洗衣机正在洗衣服。需要5秒钟
更专业些的说法叫降低了耦合度,让系统的各部分就像组装起来的一样,一旦某一小部分需要替换,都可以很容易的实现,增加了代码的整体稳定性。
这个设计原则也是Java集合框架的一大特点(另一特点是使用了泛型)。
List与LinkedList
回到ArrayList,它实现了List接口,可通过List接口变量引用它。
实现了List接口的类其实很多,除了ArrayList外,其中最常用的是LinkedList。
不同于上面的例子,ArrayList与LinkedList没有优劣之分,只是适用的场景不同。
特性 | ArrayList | LinkedList |
---|---|---|
数据结构 | 基于动态数组实现 | 基于双向链表实现 |
随机访问时间复杂度 | O(1):支持高效的随机访问,通过索引直接访问 | O(n):不支持随机访问,需要从头或尾部遍历 |
插入/删除(中间位置) | O(n):需要移动元素,效率较低 | O(1):只需修改指针,效率高 |
插入/删除(首尾位置) | - 尾部:O(1),容量足够时;<br>- 头部:O(n),需要移动所有元素 | O(1):头尾均为常数时间(前提是有对首尾节点的引用) |
内存消耗 | 较低:仅存储数据本身,数组需要连续内存空间 | 较高:额外存储前后节点引用,节点分散在内存中 |
关于上面内容的探讨应该在《数据结构》或者《算法》课程中。这门课中,只需要知道题目让你用ArrayList就用ArrayList,让你用LinkedList就用LinkedList即可,实际上仅仅只需要做一个的简单替换,而不需要改动其它任何代码:
import java.util.LinkedList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Student> list = new LinkedList<>();
list.add(new Student("张3", 72));
list.add(new Student("李4", 90));
list.add(new Student("王5", 88));
list.add(new Student("赵6", 60));
Student student2 = list.get(0);
System.out.println(student2);
}
}
通常情况下,我们总是优先使用ArrayList
。
Collections.sort
在 Java 中,可以用Collections.sort
对 List 集合进行排序,但是有个前提,集合中的元素必须实现Comparable
接口(可比较的)
为便于观察对代码进行了简单的修改:
import java.util.LinkedList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Student> list = new LinkedList<>();
list.add(new Student("张", 72));
list.add(new Student("李", 90));
list.add(new Student("王", 88));
list.add(new Student("赵", 60));
System.out.println(list); // 可直接打印数组信息,这是因为覆盖了toString方法
// => [(张,72), (李,90), (王,88), (赵,60)]
}
}
目前List是无序的。现在调用Collections.sort(list);
将报错。
步骤一:实现 Comparable 接口
public class Student implements Comparable<Student> {
String name;
int score;
Student(String name, int score) {
this.name = name;
this.score = score;
}
public String toString() {
return "(" + name + "," + score + ")";
}
}
Comparable
接口是一个泛型接口,需要在尖括号中指定实际进行比较的类型。
通过查看源代码发现Comparable中只有一个方法:
public interface Comparable<T> {
public int compareTo(T o);
}
此时需要在Student中去实现这个方法。
public class Student implements Comparable<Student> {
String name;
int score;
Student(String name, int score) {
this.name = name;
this.score = score;
}
public String toString() {
return "(" + name + "," + score + ")";
}
public int compareTo(Student o) {
// 想要成绩从低到高排序
if (this.score < o.score) {
return -1; // 当前对象小于比较对象
} else if (this.score > o.score) {
return 1; // 当前对象大于比较对象
} else {
return 0; // 当前对象等于比较对象
}
}
}
为了简化代码,通常直接返回差值。
步骤二:使用Collections.sort(list)
排序
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
public class Main {
public static void main(String[] args) {
List<Student> list = new LinkedList<>();
list.add(new Student("张", 72));
list.add(new Student("李", 90));
list.add(new Student("王", 88));
list.add(new Student("赵", 60));
System.out.println(list);
// => [(张,72), (李,90), (王,88), (赵,60)]
Collections.sort(list);
System.out.println(list);
// => [(赵,60), (张,72), (王,88), (李,90)]
}
}
练习
热身练习
完成课本P251例9.5以及P253例9.6,熟悉List的基本用法。
练习1
完成刚才讲的例子:
- 按成绩用Collections.sort方法对学生列表排序
- 先用ArrayList然后使用LinkedList,观察运行结果
练习2(★★★★★)
请创建学生类(Student
),用于存储学生信息。创建ArrayList
集合,依次存储如下5位同学的信息,并使用Comparable
或Comparator
接口对5位同学的成绩做降序排序,如果成绩一样,那在成绩排序的基础上按照年龄由小到大排序。最后遍历并输出ArrayList
集合中的数据。
姓名(String) |
年龄(int) |
分数(double) |
张三 |
20 |
90.0 |
李四 |
22 |
90.0 |
王五 |
20 |
99.0 |
孙六 |
22 |
100.0 |
赵七 |
20 |
100.0 |
输出结果如下:
Student [name=赵七, age=20, score=100.0]
Student [name=孙六, age=22, score=100.0]
Student [name=王五, age=20, score=99.0]
Student [name=张三, age=20, score=90.0]
Student [name=李四, age=22, score=90.0]
练习3(★★★★★)
产生10个1-100的随机数,并放到一个数组中,把数组中大于等于10的数字放到一个list集合中,并打印到控制台。
参考输出:
[44, 15, 97, 28, 68, 19, 12, 73]
无固定方法,确保能独立完成即可。生成随机数的参考办法:
int[] numbers = new int[10]; // 用于存储随机数的数组
Random random = new Random();
// 生成10个1到100的随机数
for (int i = 0; i < numbers.length; i++) {
numbers[i] = random.nextInt(100) + 1; // nextInt(100)生成0到99的随机数,+1后范围是1到100
}
PS. 需要理解记忆,考试不会给出。