第三次授课

接口 集合 泛型 异常处理


接口

在面向对象编程中,我们经常会遇到这样的问题:

比如我们要写一个程序,电脑可以连接不同的外设:鼠标、键盘、打印机……
它们都有共同的功能:连接断开

那么我们能不能用一个统一的“规范”来描述这些外设,让电脑只管“能不能用”,而不管“是什么设备”?

✅ 答案是——接口(Interface)

接口,在JAVA编程语言中是一个抽象类型,是抽象方法的集合,接口通常以interface来声明。一个类通过继承接口的方式,从而来继承接口的抽象方法。

接口并不是类,编写接口的方式和类很相似,但是它们属于不同的概念。类描述对象的属性和方法。接口则包含类要实现的方法。

除非实现接口的类是抽象类,否则该类要定义接口中的所有方法。

接口无法被实例化,但是可以被实现。一个实现接口的类,必须实现接口内所描述的所有方法,否则就必须声明为抽象类。

接口特性

  • 接口中每一个方法也是隐式抽象的,接口中的方法会被隐式的指定为 public abstract(只能是 public abstract,其他修饰符都会报错)。
  • 接口中可以含有变量,但是接口中的变量会被隐式的指定为 public static final 变量(并且只能是 public,用 private 修饰会报编译错误)。
  • 接口中的方法是不能在接口中实现的,只能由实现接口的类来实现接口中的方法。

接口与类的区别

  • 接口不能用于实例化对象。
  • 接口没有构造方法。
  • 接口中所有的方法必须是抽象方法,Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。
  • 接口不能包含成员变量,除了 static 和 final 变量。
  • 接口不是被类继承了,而是要被类实现。
  • 接口支持多继承。

接口的实现

当类实现接口的时候,类要实现接口中所有的方法。否则,类必须声明为抽象的类。

类使用implements关键字实现接口。在类声明中,Implements关键字放在class声明后面。

下面我们通过这个例子来看看接口的使用

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
interface Test {
// 1. 常量(默认 public static final)
int a = 100;

// 2. 抽象方法(默认 public abstract)
void start();

// 3. 默认方法
default void showInfo() {
System.out.println("默认方法可以有方法体");
}

// 4. 静态方法
static void description() {
System.out.println("接口可以定义静态方法。");
}
}

class Car implements Test {

@Override
public void start() {
System.out.println("抽象方法+常量" + a);
}

// 可以选择重写默认方法
@Override
public void showInfo() {
System.out.println("默认方法可以被重写");
}
}

public class Interface01 {
public static void main(String[] args) {
// 创建实现类对象
Car car = new Car();

// 调用抽象方法(由实现类实现)
car.start();

// 调用默认方法(可被重写)
car.showInfo();

// 调用接口的静态方法(只能用接口名调用)
Test.description();
}
}
内容 说明 示例
1. 常量 接口中的变量默认是 public static final,即“公共静态常量”。 int a = 100;
2. 抽象方法 只有方法名、没有方法体。由实现类负责实现。 void start();
3. 默认方法 有方法体,实现类可直接使用或重写。 default void showInfo() {}
4. 静态方法 只能用接口名调用,不可被重写。 Test.description();

接口的特点

  1. 接口不能被实例化

    1
    Test t = new Test(); // ❌ 错误:接口不能直接创建对象
  2. 实现类(implements)必须实现接口中的抽象方法

    1
    2
    3
    class Car implements Test {
    public void start() { ... }
    }
  3. 一个类可以实现多个接口(解决 Java 单继承的限制)

    1
    2
    3
    4
    5
    6
    7
    interface A { void methodA(); }
    interface B { void methodB(); }

    class Demo implements A, B {
    public void methodA() {}
    public void methodB() {}
    }
  4. 接口之间可以继承(extends)并且可多继承

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 文件名: Sports.java
    public interface Sports
    {
    public void setHomeTeam(String name);
    public void setVisitingTeam(String name);
    }

    // 文件名: Football.java
    public interface Football extends Sports
    {
    public void homeTeamScored(int points);
    public void visitingTeamScored(int points);
    public void endOfQuarter(int quarter);
    }

    // 文件名: Hockey.java
    public interface Hockey extends Sports
    {
    public void homeGoalScored();
    public void visitingGoalScored();
    public void endOfPeriod(int period);
    public void overtimePeriod(int ot);
    }

接口在实际开发中应用非常广泛,尤其是在 Spring Boot 框架中,接口常用于定义服务层的业务规范、数据访问层的操作规则。
我们在后续学习 Spring Boot 时,会看到:接口定义标准,类实现功能,Spring 负责管理和注入,这正是“面向接口编程”的体现。


集合

集合是什么,在数学中:集合是由若干个确定的元素所构成的整体。例如我们现在在教室中的同学。

而在Java中,我们引入集合是为了处理一组类似的数据:

  • 列出商品名称
  • 计算所有同学的平均分

事实上,我们这些例子可以通过数组来实现,那为什么为什么需要学习集合呢,这是因为数组是有很大的弊端的:

  • 数组初始化后大小不可变
  • 数组不方便操作(索引访问,增删改难)
  • 存储类型单一

而我们的集合,则具有以下的优势:

  • 大小可变,动态删减元素
  • 操作灵活
  • 提供List(有序可重复),Set(无序不可重复),Map(键值对)
  • ……

集合框架体系

在Java中,集合框架主要包含两种类型的容器。一种是集合(Collection),储存一个元素集合。而另一种是图(Map),用于储存映射、键值对。这么说可能会有些抽象,那么我们举个具体点的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.ArrayList;
import java.util.HashMap;

public class Main {
public static void main(String[] args) {
//集合形式
ArrayList<String> list = new ArrayList<>();
list.add("bys");
list.add("xm");

//图形式
HashMap<String,String> map = new HashMap<>();
map.put("B24011451","xm");
map.put("B24011452","bys");

System.out.println(list.get(0));
System.out.println(map.get("B24011452"));


}
}

介绍完了上述两种容器,我们现在来讲讲集合框架。

集合框架是一个用来代表和操纵集合的统一架构。所有的集合框架都包含如下内容:

  • **接口:**是代表集合的抽象数据类型。例如 Collection、List、Set、Map 等。之所以定义多个接口,

    是为了以不同的方式操作集合对象

  • **实现(类):**是集合接口的具体实现。从本质上讲,它们是可重复使用的数据结构,例如:

    ArrayList、LinkedList、HashSet、HashMap。

  • **算法:**是实现集合接口的对象里的方法执行的一些有用的计算,例如:搜索和排序,这些算法实现

    了多态,那是因为相同的方法可以在相似的接口上有着不同的实现。

    1762668260390

集合接口

集合框架定义了一些接口,以下是这些接口的概述:

Collection 接口

Collection 是最基本的集合接口,一个 Collection 代表一组 Object,即 Collection 的元素, Java不提供直

接继承自Collection的类,只提供继承于的子接口(如List和set)。

Collection 接口存储一组不唯一,无序的对象。

List 接口

List接口是一个有序的 Collection,使用此接口能够精确的控制每个元素插入的位置,能够通过索引(元素

在List中位置,类似于数组的下标)来访问List中的元素,第一个元素的索引为 0,而且允许有相同的元

素。

List 接口存储一组不唯一,有序(插入顺序)的对象。

**Set ** **接口 **

Set 具有与 Collection 完全一样的接口,只是行为上不同,Set 不保存重复的元素。

Set 接口存储一组唯一,无序的对象。

SortedSet

继承于Set保存有序的集合。

Map

Map 接口存储一组键值对象,提供key(键)到value(值)的映射。

Map.Entry

描述在一个Map中的一个元素(键/值对)。是一个 Map 的内部接口。

SortedMap

继承于 Map,使 Key 保持在升序排列。

一些注意点

所有集合类都位于 java. util包下。 Java的集合类主要由两个接口派生而出: Collection和 Map,

Collection和 Map是 Java集合框架的根接口, 这两个接口又包含了一些子接口或实现类。

Collection 接口是一组允许重复的对象。

Set 接口继承 Collection, 集合元素不重复。

List接口继承 Collection, 允许重复, 维护元素插入顺序。

Map接口是键-值对象, 与 Collection接口没有什么关系。

Set、 List和 Map可以看做集合的三大类:List 集合是有序集合, 集合中的元素可以重复, 访问集合中的元素可以根据元素的索引来访问。Set 集合是无序集合, 集合中的元素不可以重复,访问集合中的元素只能根据元素本身来访问(也是集合里元素不允许重复的原因) 。Map 集合中保存 Key-value 对形式的元素, 访问时只能根据每项元素的 key 来访问其 value。

集合实现类

AbstractCollection

实现了大部分的集合接口。

AbstractList

继承于 AbstractCollection 并且实现了大部分 List接口。

AbstractSequentialList

继承于 AbstractList, 提供了对数据元素的链式访问而不是随机访问。

LinkedList

继承于 AbstractSequentialList, 实现了一个链表。

ArrayList

通过继承 AbstractList, 实现动态数组。

AbstractSet

继承于 AbstractCollection并且实现了大部分 Set接口。

HashSet 继承了 AbstractSet, 并且使用一个哈希表。

LinkedHashSet

具有可预知迭代顺序的 Set接口的哈希表和链接列表实现。

TreeSet

继承于AbstractSet, 使用元素的自然顺序对元素进行排序.

AbstractMap

实现了大部分的 Map接口。

HashMap

继承AbstractMap,并且使用一个哈希表。

TreeMap

继承了 AbstractMap, 并且使用一颗树。

WeakHashMap

继承AbstractMap类, 使用弱密钥的哈希表。

LinkedHashMap

继承于 HashMap, 使用元素的自然顺序对元素进行排序

IdentityHashMap

继承 AbstractMap类, 比较文档时使用引用相等。

遍历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
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class Collection02 {
public static void main(String[] args) {
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
list.add("c");

//1.for-each遍历
for (String s : list) {
System.out.println(s);
}

//2.链表转数组
String[] strArray = new String[list.size()];
list.toArray(strArray);
for (String s : strArray) {
System.out.println(s);
}

//3.迭代器遍历
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
}

通常情况下,你会希望遍历一个集合中的元素。例如,显示集合中的每个元素。

一般遍历数组都是采用for循环或者增强for,这两个方法也可以用在集合框架,但是还有一种方法是采用迭代器遍历集合框架,它是一个对象,实现了Iterator 接口或 ListIterator接口。

迭代器,使你能够通过循环来得到或删除集合的元素。ListIterator 继承了 Iterator,以允许双向遍历列表和修改元素。


泛型

Java 泛型(generics)是 JDK 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制,该机制允许

程序员在编译时检测到非法的类型。

为什么要使用泛型呢?

想象一下,你有一个方法需要返回结果数据,可能是用户信息、商品列表、或者错误提示。
如果不用泛型,你可能会写很多重复的类,比如:

1
2
3
class ProductResponse { Product data; }
class ErrorResponse { String message; }
class ErrorResponse { String message; }

这样繁琐而且不灵活

这时就可以使用 泛型,写一个通用的包装类:

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
// 泛型包装类
class GlobalResponse<T> {
private int code;
private String message;
private T data;

public GlobalResponse(int code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}

public void show() {
System.out.println("状态码:" + code + ",消息:" + message + ",数据:" + data);
}
}

// 示例数据类
class User {
private String name;
public User(String name) {
this.name = name;
}

@Override
public String toString() {
return "User{name='" + name + "'}";
}
}


public class Generics01 {
public static void main(String[] args) {
// 使用 String 类型的泛型
GlobalResponse response1 = new GlobalResponse<>(200, "OK", "操作成功");
response1.show();

// 使用自定义类型 User 的泛型
GlobalResponse response2 = new GlobalResponse<>(200, "OK", new User("张三"));
response2.show();
}
}

java 中泛型标记符:

  • E - Element (在集合中使用,因为集合中存放的是元素)
  • T - Type(Java 类)
  • K - Key(键)
  • V - Value(值)
  • N - Number(数值类型)
  • - 表示不确定的 java 类型

泛型的本质是为了参数化类型(在不创建新的类型的情况下, 通过泛型指定的不同类型来控制形参具体限

制的类型) 。也就是说在泛型使用过程中,

操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛

型接口、泛型方法

泛型方法

该方法在调用时可以接收不同类型的参数。根据传递给泛型方法的参数类型,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Util {
public static <T> void printArray(T[] arr) {
for (T t : arr) {
System.out.print(t + " ");
}
System.out.println();
}
}

public class Generics02 {
public static void main(String[] args) {
Integer[] nums = {1, 2, 3};
String[] strs = {"A", "B", "C"};
Util.printArray(nums);
Util.printArray(strs);
}
}

泛型类

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
// 定义一个泛型类 Box<T>
class Box<T> {
private T t;

// 添加元素
public void add(T t) {
this.t = t;
}

// 获取元素
public T get() {
return t;
}
}

public class Generics03 {
public static void main(String[] args) {
// 创建两个不同类型的 Box
Box<Integer> integerBox = new Box<>();
Box<String> stringBox = new Box<>();

// 添加不同类型的数据
integerBox.add(10);
stringBox.add("string");

// 输出结果
System.out.printf("整型值为 : %d\n\n", integerBox.get());
System.out.printf("字符串为 : %s\n", stringBox.get());
}
}

类型通配符

1、类型通配符一般是使用 ? 代替具体的类型参数。例如 List<?> 在逻辑上是 List,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
import java.util.*;

// 泛型通配符示例
public class Generics04 {

public static void main(String[] args) {
// 创建不同类型的 List
List<String> name = new ArrayList<>();
List<Integer> age = new ArrayList<>();
List<Number> number = new ArrayList<>();

// 添加元素
name.add("icon");
age.add(18);
number.add(3.14);

// 使用通配符方法
getData(name);
getData(age);
getData(number);
}

// 使用通配符 ? 的泛型方法
public static void getData(List<?> data) {
System.out.println("data: " + data.get(0));
}
}

结果为

1
2
3
data: 18
data: 3.14
data: icon

因为 getData() 方法的参数是 List<?> 类型的,所以 name,age,number 都可以作为这个方法的实参,这就是通配符的作用。

2、类型通配符上限通过形如List来定义,如此定义就是通配符泛型值接受Number及其下层子类类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.*;

public class Generics05 {

public static void main(String[] args) {
List<String> name = new ArrayList<String>();
List<Integer> age = new ArrayList<Integer>();
List<Number> number = new ArrayList<Number>();

name.add("icon");
age.add(18);
number.add(314);

//getUperNumber(name);//1
getUperNumber(age);//2
getUperNumber(number);//3

}

public static void getUperNumber(List<? extends Number> data) {
System.out.println("data :" + data.get(0));
}
}
1
2
data :18
data :314

//1 处会出现错误,因为 getUperNumber() 方法中的参数已经限定了参数泛型上限为 Number,所以泛型为 String 是不在这个范围之内,所以会报错。

3、类型通配符下限通过形如 List<? super Number> 来定义,表示类型只能接受 Number 及其上层父类类型,如 Object 类型的实例。


异常处理

在 Java 中,异常处理是一种重要的编程概念,用于处理程序执行过程中可能出现的错误或异常情况。

异常是程序中的一些错误,但并不是所有的错误都是异常,并且错误有时候是可以避免的。

比如说,你的代码少了一个分号,那么运行出来结果是提示是错误 java.lang.Error,如果你用 System.out.println(11/0),那么你是因为你用 0 做了除数,会抛出 java.lang.ArithmeticException 的异常。

异常发生的原因有很多,通常包含以下几大类:

  • 用户输入了非法数据。
  • 要打开的文件不存在。
  • 网络通信时连接中断,或者JVM内存溢出。

这些异常有的是因为用户错误引起,有的是程序错误引起的,还有其它一些是因为物理错误引起的。

要理解 Java 异常处理是如何工作的,你需要掌握以下三种类型的异常:

**检查性异常:**最具代表的检查性异常是用户错误或问题引起的异常,这些异常在编译时强制要求程序员处理。例如要打开一个不存在文件时,一个异常就发生了,这些异常在编译时不能被简单地忽略。

这类异常通常使用 try-catch 块来捕获并处理异常,或者在方法声明中使用 throws 子句声明方法可能抛出的异常。

1
2
3
4
5
try {
// 可能会抛出异常的代码
} catch (IOException e) {
// 处理异常的代码
}

或者

1
2
3
public void readFile() throws IOException {
// 可能会抛出IOException的代码
}
异常名 含义
IOException 文件读写、网络传输等 IO 操作失败时抛出
FileNotFoundException 试图访问不存在的文件时抛出(IOException 的子类)
SQLException 数据库访问出错时抛出
…… …….

运行时异常: 这些异常在编译时不强制要求处理,通常是由程序中的错误引起的,例如 NullPointerException、ArrayIndexOutOfBoundsException 等,这类异常可以选择处理,但并非强制要求。

1
2
3
4
5
try {
// 可能会抛出异常的代码
} catch (NullPointerException e) {
// 处理异常的代码
}

错误: 错误不是异常,而是脱离程序员控制的问题,错误在代码中通常被忽略。例如,当栈溢出时,一个错误就发生了,它们在编译也检查不到的。

Java 提供了以下关键字和类来支持异常处理:

  • try:用于包裹可能会抛出异常的代码块。
  • catch:用于捕获异常并处理异常的代码块。
  • finally:用于包含无论是否发生异常都需要执行的代码块。
  • throw:用于手动抛出异常。
  • throws:用于在方法声明中指定方法可能抛出的异常。
  • Exception类:是所有异常类的父类,它提供了一些方法来获取异常信息,如 getMessage()、printStackTrace() 等。
异常名 含义
NullPointerException 空指针异常,对象为 null 却调用了方法或属性
ArrayIndexOutOfBoundsException 访问数组时索引越界
StringIndexOutOfBoundsException 字符串索引越界
ArithmeticException 算术异常,比如除以 0
…… …….

异常处理机制

Java 提供了两种方式处理异常:

  1. 捕获异常(try-catch-finally)
  2. 声明抛出异常(throws)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Exception01 {
public static void main(String[] args) {
try {
int a = 10;
int b = 0;
int result = a / b;
System.out.println(result);
} catch (ArithmeticException e) {
System.out.println("除数不能为0!");
} finally {
System.out.println("程序执行完毕。");
}
}
}

throw声明异常

用于方法定义中,表示该方法可能抛出某种异常,需要调用者处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.io.FileReader;
import java.io.IOException;

public class Exception03 {

// 声明一个可能抛出 IOException 的方法
public static void readFile(String filePath) throws IOException {
FileReader fr = new FileReader(filePath);
System.out.println("文件读取成功!");
fr.close();
}

public static void main(String[] args) {
try {
readFile("data.txt"); // 可能抛出 IOException
} catch (IOException e) {
System.out.println("捕获到异常:文件不存在或无法读取。");
e.printStackTrace();
}
}
}

throw 抛出异常

手动抛出一个异常对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Exception04 {
public static void checkAge(int age) {
if (age < 18) {
throw new IllegalArgumentException("未满18岁,禁止进入!");
}
System.out.println("欢迎进入!");
}

public static void main(String[] args) {
try {
checkAge(16);
} catch (IllegalArgumentException e) {
System.out.println("捕获到异常:" + e.getMessage());
}
}
}

自定义异常

在 Java 中你可以自定义异常。编写自己的异常类时需要记住下面的几点。

  • 所有异常都必须是 Throwable 的子类。
  • 如果希望写一个检查性异常类,则需要继承 Exception 类。
  • 如果你想写一个运行时异常类,那么需要继承 RuntimeException 类。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyException extends Exception {
public MyException(String message) {
super(message);
}
}

public class Exception05 {
public static void main(String[] args) {
try {
throw new MyException("自定义异常被触发!");
} catch (MyException e) {
System.out.println("捕获到:" + e.getMessage());
}
}
}

异常处理的最佳实践

  • 在合适的位置捕获异常,并对异常进行适当的处理,以确保程序的稳定性和可靠性。
  • 避免过度捕获异常,应该尽量精确捕获特定类型的异常。
  • 使用finally块来释放资源,例如关闭文件或数据库连接等,以确保资源的正确释放。
  • 优先处理受检异常,避免将受检异常转换为非受检异常。

异常处理是编写健壮的 Java 应用程序的重要组成部分,合理地处理异常可以提高程序的可维护性和可靠性。