List集合类_演练

参考:

引入

集合的概念

见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集合框架的一小部分(虽然它最常用)

接下来我们首先回顾一下数组列表

然后介绍将接口与实现分离的设计思想

进一步引入ListLinkedList

最后将介绍如何用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位同学的信息,并使用ComparableComparator接口对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. 需要理解记忆,考试不会给出。

posted @ 2024-11-29 11:40  xkfx  阅读(87)  评论(0编辑  收藏  举报