概述

接口(Interface) 是 Java 中用于定义行为契约的一种机制。它定义了一组方法,而这些方法不包含实现(除非是 default 方法或 static 方法)。接口允许类实现这些方法并提供具体的功能,实现了代码的灵活性和模块化。

接口的特点和作用:

  1. 抽象行为:接口定义了一组抽象方法,这些方法没有实现,由实现接口的类负责具体实现。接口可以看作是一组行为的约定。
  2. 多实现支持:类只能继承一个父类,但可以实现多个接口。接口为 Java 提供了类多重行为继承的机制,弥补了类不支持多继承的限制。
  3. 实现解耦:接口可以将方法的定义和实现分开,增强了代码的解耦性。通过接口,类可以在不依赖具体实现的情况下使用其他类提供的功能。
  4. 设计灵活性:接口可以定义多个类都应该遵循的行为。不同的类可以实现同一个接口,但实现细节可以不同,这提高了设计的灵活性。
  5. 多态性支持:接口为 Java 的多态性提供了支持,可以通过接口类型引用对象,使代码更加通用。

接口的组成:

  • 抽象方法:接口中的方法默认是 public abstract,即这些方法必须在实现类中实现。
  • 默认方法(default 方法):接口可以包含带有默认实现的 default 方法。实现类可以选择继承或重写这些方法。
  • 静态方法:接口可以包含 static 方法,静态方法只能通过接口名调用,而不能通过接口的实现类调用。
  • 常量:接口中的变量默认是 public static final,即它们是常量,必须初始化并且不可更改。

接口示例:

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
// 定义接口
interface Animal {
// 抽象方法
void eat();

// 默认方法
default void sleep() {
System.out.println("Animal is sleeping.");
}

// 静态方法
static void breathe() {
System.out.println("Animals need to breathe.");
}
}

// 实现接口的类
class Dog implements Animal {
@Override
public void eat() {
System.out.println("Dog is eating.");
}

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

public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // 调用实现的抽象方法
dog.sleep(); // 调用重写的默认方法

// 调用接口的静态方法
Animal.breathe();
}
}

Java 接口的优点:

  1. 实现多重继承:通过接口,一个类可以实现多个接口,达到多继承的效果,而不会引发多继承的复杂性问题。
  2. 松耦合设计:接口可以将系统中的各个模块解耦,使模块之间的依赖更灵活,通过接口定义行为,而不关心具体实现细节。
  3. 提高代码的可维护性:接口使得程序更加清晰,接口实现类可以根据需要进行更换,而不影响使用接口的其他部分。
  4. 支持回调机制:通过接口,可以实现回调机制,例如 Java 中的事件处理、线程执行等都是通过接口回调实现的。

Java 接口的常见用途:

  1. 定义行为契约:接口用于定义一个类必须实现的一组方法。例如 Runnable 接口定义了 run() 方法,任何实现 Runnable 接口的类都必须提供 run() 方法的实现。
  2. 回调机制:例如 ActionListener 接口用于定义用户界面中的动作事件处理机制。
  3. 多态性:接口可以用于实现多态性,允许不同的类通过相同的接口引用实现不同的行为。
  4. 与类库的交互:Java 的许多标准库通过接口定义类的行为,使得库可以扩展和应用于不同的实现。

总结:
接口在 Java 中是一个强大的工具,用于定义类应该具备的行为,而不限定具体的实现方式。通过接口,Java 实现了面向对象编程中的多态性抽象化模块化,同时提供了多重继承的灵活性而避免了多继承的复杂性问题。

接口规则

Java 接口是一种用于定义类必须遵循的契约。它提供了一种抽象的方式来定义行为而不实现具体的功能。接口的规则有助于实现代码的解耦、模块化和灵活性。

以下是 Java 接口的一些主要规则:

接口不能有实例化对象

  • 接口是纯抽象的,不能被直接实例化。你不能使用 new 关键字来创建接口的对象。

接口中的方法默认是public abstract**

  • 在接口中定义的方法默认是 public abstract,即这些方法必须被实现类提供实现。无需显式声明 public abstract

接口中的变量默认是 public static final

  • 接口中的变量默认是常量,即 public static final。它们必须被赋值,并且是不可修改的。

接口可以包含 default方法

  • 接口可以包含带有默认实现的 default 方法。实现类可以选择继承这个默认方法或重写它。

接口可以包含 static 方法

  • 接口可以包含 static 方法,且只能通过接口名直接调用,不能通过实现类或对象来调用。

接口可以继承多个接口

  • 接口支持多继承。一个接口可以继承多个其他接口,并且必须实现所有父接口中的抽象方法。

接口是一种契约

  • 实现接口的类必须实现接口中的所有抽象方法,否则该类必须声明为抽象类。

类实现接口

当一个类实现接口时,它承诺将提供该接口中定义的所有抽象方法的具体实现。如果类没有实现接口中的所有方法,那么这个类必须是抽象的。

基本步骤:

  1. 使用 implements 关键字
    • 类通过 implements 关键字来实现接口,并且可以实现多个接口。
  2. 实现所有的抽象方法
    • 如果一个类实现了接口,它必须实现接口中的所有抽象方法。如果不实现,则必须将该类声明为抽象类。
  3. 可以选择重写 default 方法
    • 如果接口提供了 default 方法,实现类可以选择继承它,或者对其进行重写。

示例代码:

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
// 定义接口
interface Animal {
void eat(); // 抽象方法,必须由实现类实现
void sleep(); // 抽象方法,必须由实现类实现
}

// 定义接口的实现类
class Dog implements Animal {
@Override
public void eat() {
System.out.println("Dog is eating.");
}

@Override
public void sleep() {
System.out.println("Dog is sleeping.");
}
}

// 测试
public class Main {
public static void main(String[] args) {
// 创建实现类的对象
Dog dog = new Dog();
dog.eat();
dog.sleep();
}
}

接口的多实现

在 Java 中,多实现是指一个类可以同时实现多个接口。这与类的继承不同,Java 中不允许类多继承,但可以通过实现多个接口来实现类的多实现。多实现让类能够从多个接口中获取不同的功能契约,从而实现代码的灵活性和模块化。

特点:

  • 一个类可以实现多个接口:使用 implements 关键字,并用逗号分隔接口名称,一个类可以同时实现多个接口。
  • 必须实现接口中的所有方法:类在实现多个接口时,必须为所有接口中的抽象方法提供具体实现。
  • 支持多种行为组合:通过实现不同接口,类可以实现多种不同的行为接口,满足多重职责。

示例代码:

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
// 定义第一个接口
interface InterfaceA {
void methodA();
}

// 定义第二个接口
interface InterfaceB {
void methodB();
}

// 定义实现多个接口的类
class MyClass implements InterfaceA, InterfaceB {
@Override
public void methodA() {
System.out.println("Implementing methodA from InterfaceA");
}

@Override
public void methodB() {
System.out.println("Implementing methodB from InterfaceB");
}
}

// 测试
public class Main {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.methodA();
myClass.methodB();
}
}

解释:

  • InterfaceAInterfaceB:定义了两个接口,分别包含抽象方法 methodA()methodB()
  • MyClass:通过 implements 关键字同时实现了 InterfaceAInterfaceB。为了符合接口契约,MyClass 必须提供这两个接口的抽象方法的具体实现。
  • 实现的灵活性:通过实现多个接口,MyClass 类获得了多个接口的能力,可以在实例化对象时调用不同接口的方法。

接口多实现的优势:

  1. 灵活性:一个类可以实现多个不同的接口,这样可以灵活地混合多个功能或行为。
  2. 避免类多继承的问题:类多继承可能会导致”钻石继承问题”(多重继承导致父类方法冲突),而接口的多实现则不会,因为接口只是定义了契约,不涉及具体的实现逻辑。
  3. 更好地组织代码:通过接口实现,类可以分离出不同的职责,不同的接口可以定义不同的功能契约,而类可以选择性地实现它们。

接口的多继承

在 Java 中,接口支持多继承,即一个接口可以继承多个其他接口。这与类不同,类不支持多继承,但接口之间可以进行多继承,并且可以通过这种方式实现代码的复用和接口的扩展

特点:

  • 接口可以继承多个接口:一个接口可以通过 extends 关键字继承多个接口,而不必实现它们的具体方法。
  • 接口继承后可以扩展新方法:子接口可以定义新方法,或者继承并合并父接口中的方法。
  • 接口中的默认方法(default 方法)也可以被继承:如果父接口有默认方法,子接口可以选择继承它,或者重写它。

示例代码:

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
// 定义第一个父接口
interface InterfaceA {
void methodA();
}

// 定义第二个父接口
interface InterfaceB {
void methodB();
}

// 定义一个子接口继承多个父接口
interface ChildInterface extends InterfaceA, InterfaceB {
void methodC();
}

// 实现子接口的类必须实现所有继承的接口方法
class MyClass implements ChildInterface {
@Override
public void methodA() {
System.out.println("Implementing methodA from InterfaceA");
}

@Override
public void methodB() {
System.out.println("Implementing methodB from InterfaceB");
}

@Override
public void methodC() {
System.out.println("Implementing methodC from ChildInterface");
}
}

// 测试
public class Main {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.methodA();
myClass.methodB();
myClass.methodC();
}
}

解释:

  • InterfaceAInterfaceB:定义了两个父接口,分别包含 methodA()methodB()
  • ChildInterface:通过 extends 关键字同时继承了 InterfaceAInterfaceB,并且扩展了一个新方法 methodC()
  • MyClass:实现了 ChildInterface,因此需要实现所有继承来的方法:methodA()methodB()methodC()

要点:

  1. 方法合并:如果多个父接口中定义了签名相同的抽象方法,子接口会继承该方法,且只需要在实现类中实现一次。
  2. 默认方法冲突:如果多个父接口中有同名的 default 方法,则子接口或实现类必须重写该方法以解决冲突,类似于类实现多个接口的情况。

接口的多继承让 Java 中的接口更加灵活,能够实现接口间的复用,同时避免了类多继承带来的复杂性。

相关冲突

默认方法冲突

说明:

在 Java 中,如果一个类继承了一个父类并且实现了一个接口,而该接口中提供了默认方法(default 方法),且父类中也有一个同名的方法,那么在子类中将会遇到多继承冲突的情况。为了避免冲突,你需要在子类中显式地重写该方法,并选择调用父类的方法或接口的默认方法。

示例代码:

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
// 定义父类
class Parent {
public void sayHello() {
System.out.println("Hello from Parent");
}
}

// 定义接口并提供默认方法
interface MyInterface {
default void sayHello() {
System.out.println("Hello from MyInterface");
}
}

// 子类继承父类并实现接口
class Child extends Parent implements MyInterface {
@Override
public void sayHello() {
// 选择调用父类方法
super.sayHello();

// 或者选择调用接口默认方法
MyInterface.super.sayHello();
}
}

// 测试
public class Main {
public static void main(String[] args) {
Child child = new Child();
child.sayHello();
}
}

解释:

  1. Parent 类中有一个 sayHello() 方法。
  2. MyInterface 接口中提供了一个 defaultsayHello() 方法。
  3. Child 类同时继承了 Parent 类并实现了 MyInterface 接口,因此会产生方法冲突。
  4. Child 类中,我们必须重写 sayHello() 方法,并在方法内部使用 super.sayHello() 调用父类的方法,或使用 MyInterface.super.sayHello() 来调用接口的默认方法。

在这个例子中,Child 类通过显式重写 sayHello() 解决了冲突问题,并可以选择调用父类方法或者接口中的默认方法。

接口冲突

说明:

当一个类同时实现了多个接口,而这些接口中包含了签名相同的默认方法时,会发生方法冲突。为了处理这个问题,Java 要求在子类中显式地重写该方法,决定要调用哪个接口的默认方法,或者提供自己的实现。

示例代码:

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
// 定义第一个接口并提供默认方法
interface InterfaceA {
default void sayHello() {
System.out.println("Hello from InterfaceA");
}
}

// 定义第二个接口并提供相同签名的默认方法
interface InterfaceB {
default void sayHello() {
System.out.println("Hello from InterfaceB");
}
}

// 类同时实现了 InterfaceA 和 InterfaceB
class MyClass implements InterfaceA, InterfaceB {
// 必须重写 sayHello 方法来解决冲突
@Override
public void sayHello() {
// 选择调用 InterfaceA 的默认方法
InterfaceA.super.sayHello();

// 或者选择调用 InterfaceB 的默认方法
InterfaceB.super.sayHello();

// 或者提供自己的实现
System.out.println("Hello from MyClass");
}
}

// 测试
public class Main {
public static void main(String[] args) {
MyClass myClass = new MyClass();
myClass.sayHello();
}
}

解释:

  1. InterfaceAInterfaceB 都提供了签名相同的默认方法 sayHello()
  2. MyClass 类同时实现了这两个接口,因此会产生冲突。
  3. MyClass 中必须显式重写 sayHello() 方法,以解决接口中的冲突。
    • 可以选择调用 InterfaceA 的默认方法:InterfaceA.super.sayHello()
    • 可以选择调用 InterfaceB 的默认方法:InterfaceB.super.sayHello()
    • 也可以提供自己的实现,覆盖这两个接口的默认方法。

这样,子类通过重写来明确选择调用哪个接口的默认方法,或者直接提供一个新的实现,避免了方法冲突。

常量冲突问题

说明:

当子类继承自父类且实现多个接口,而父类中有一个与接口中常量同名的成员变量时,可能会产生命名冲突。在这种情况下,由于接口中的常量是 public static final 的,并且父类的成员变量也在子类中可见,你需要显式地使用接口名或父类名来访问它们,以区分冲突的成员。

代码示例:

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
// 定义父类,包含一个与接口常量同名的成员变量
class Parent {
public String NAME = "Parent NAME";
}

// 定义第一个接口,包含同名常量
interface InterfaceA {
String NAME = "InterfaceA NAME";
}

// 定义第二个接口,包含同名常量
interface InterfaceB {
String NAME = "InterfaceB NAME";
}

// 子类继承父类并实现多个接口
class Child extends Parent implements InterfaceA, InterfaceB {
public void printNames() {
// 访问父类中的成员变量
System.out.println("Parent's NAME: " + super.NAME);

// 访问 InterfaceA 中的常量
System.out.println("InterfaceA's NAME: " + InterfaceA.NAME);

// 访问 InterfaceB 中的常量
System.out.println("InterfaceB's NAME: " + InterfaceB.NAME);
}
}

// 测试
public class Main {
public static void main(String[] args) {
Child child = new Child();
child.printNames();
}
}

解释:

  1. Parent 类中有一个成员变量 NAME
  2. InterfaceAInterfaceB 中都有一个名为 NAME 的常量。
  3. Child 类中,这些同名的变量和常量会产生冲突,因此需要显式地使用各自的上下文来区分它们。
    • 使用 super.NAME 访问父类 Parent 中的成员变量 NAME
    • 使用 InterfaceA.NAME 访问接口 InterfaceA 中的常量 NAME
    • 使用 InterfaceB.NAME 访问接口 InterfaceB 中的常量 NAME

这种方式解决了成员变量和接口常量同名导致的冲突问题。通过明确指明使用的是哪个上下文中的 NAME,避免了歧义。

接口体现多态性

  • 接口定义统一的行为:接口定义了一组方法,而具体实现类根据自己的特性来实现这些方法。
  • 使用接口类型引用对象:通过接口类型来引用不同的实现类对象,利用接口引用来调用方法,而实际执行的是引用对象的实现方法。
  • 接口引用的多态性:接口引用可以指向任何实现了该接口的类对象,这样调用方法时,可以根据引用指向的对象类型来决定执行哪个具体的实现方法。

定义接口:

1
2
3
4
// 定义接口
interface Animal {
void sound();
}

不同类实现相同的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 实现接口的类1:Dog
class Dog implements Animal {
@Override
public void sound() {
System.out.println("Dog barks: Woof Woof!");
}
}

// 实现接口的类2:Cat
class Cat implements Animal {
@Override
public void sound() {
System.out.println("Cat meows: Meow Meow!");
}
}

使用接口类型实现多态性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Main {
public static void main(String[] args) {
// 创建不同的实现类对象
Animal dog = new Dog();
Animal cat = new Cat();

// 调用接口的方法,实际执行的为具体类的实现
dog.sound(); // 输出: Dog barks: Woof Woof!
cat.sound(); // 输出: Cat meows: Meow Meow!

// 动态绑定:接口类型可以引用任意实现了该接口的对象
Animal myAnimal = new Dog();
myAnimal.sound(); // 输出: Dog barks: Woof Woof!

myAnimal = new Cat();
myAnimal.sound(); // 输出: Cat meows: Meow Meow!
}
}

接口与抽象类的对比

特性接口 (Interface)抽象类 (Abstract Class)
定义定义行为契约,方法没有实现(除非是 defaultstatic方法)可以包含部分实现的方法,也可以包含抽象方法
关键字interfaceabstract class
多重继承一个类可以实现多个接口一个类只能继承一个抽象类
方法类型只能有抽象方法、default方法和 static方法可以有抽象方法、具体方法、static方法和 final方法
变量只能包含 public static final常量可以包含任何类型的变量(staticfinal、实例变量等)
构造函数没有构造函数可以有构造函数
访问修饰符接口中的方法默认是 public,不能有其他访问修饰符抽象类中的方法可以有任何访问修饰符(publicprotectedprivate
实现约束实现类必须实现接口中所有的抽象方法子类可以选择实现抽象类中的部分或全部抽象方法
使用场景用于定义一组行为,由多个类实现相同的行为规范用于表示具有相似属性和行为的类的共有部分
默认方法允许使用 default关键字提供默认方法实现不支持默认方法(只能包含具体实现的方法)
静态方法允许使用 static关键字定义静态方法支持静态方法
多态性通过接口实现多态性,类可以实现多个接口通过继承实现多态性,只能继承一个抽象类
适用性更适合行为抽象(行为的契约)更适合类的概念抽象(部分实现和共同特征的封装)

总结:

  • 接口 更注重定义行为契约,允许类实现多个接口,从而支持多重继承,是更灵活的选择。
  • 抽象类 更适合表示有相似属性和行为的类,支持部分实现,允许子类继承和扩展。

这两者的使用取决于具体需求,接口更适用于行为统一,而抽象类更适用于类的继承层次。

练习

匿名&多态

留意查看以下代码的四种不同写法、体现出接口使用的多态性

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
package com.base8._interface.apply;

public class USBTest {

public static void main(String[] args) {
Computer computer = new Computer();

//1.创建接口实现类的对象
Printer printer = new Printer();
computer.transferData(printer);
System.out.println("--------------");

//2.创建接口实现类的匿名对象
computer.transferData(new Camera());
System.out.println("--------------");

//3.创建接口匿名实现类的对象
USB usb1 = new USB(){
public void start(){
System.out.println("U盘开始工作");
}
public void stop(){
System.out.println("U盘结束工作");
}
};
computer.transferData(usb1);
System.out.println("--------------");

//4. 创建接口匿名实现类的匿名对象
computer.transferData(new USB(){
public void start(){
System.out.println("扫描仪开始工作");
}
public void stop(){
System.out.println("扫描仪结束工作");
}
});
}
}

class Computer{
public void transferData(USB usb){ //多态:USB usb = new Printer();
System.out.println("设备连接成功....");
usb.start();
System.out.println("数据传输的细节操作....");
usb.stop();
}
}

class Camera implements USB{

@Override
public void start() {
System.out.println("照相机开始工作");
}

@Override
public void stop() {
System.out.println("照相机结束工作");
}
}

class Printer implements USB{

@Override
public void start() {
System.out.println("打印机开始工作");
}

@Override
public void stop() {
System.out.println("打印机结束工作");
}
}

interface USB{
//方法
void start();
void stop();
}

排错 1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface A {
int x = 0;
}
class B {
int x = 1;
}
class C extends B implements A {
public void px() {
System.out.println(x); // 编译报错
}

public static void main(String[] args) {
new C().px();
}
}

解析:这里编译不通过,报错信息Reference to 'x' is ambiguous, both 'B.x' and 'A.x' match对 x 的引用不明确,类和接口是并列的结构,没有就近原则,编译器不知道是用接口 A 中的 x 还是用父类 B 中的 x,实际开发中一般不会遇到这种问题,命名的时候理论上不会相同。

修改后代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface A {
int x = 0;
}
class B {
int x= 1;
}
class C extends B implements A {
public void px() {
System.out.println(super.x); // 可以使用 super 关键字调用父类中的属性
System.out.println(A.x); // 这里 x 是一个全局常量,可以使用接口直接调用
}

public static void main(String[] args) {
new C().px();
}
}

排错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.base.learn;

interface Playable {
void play();
}

interface Bounceable {
void play();
}

interface Rollable extends Playable, Bounceable {
Ball ball = new Ball("PingPang"); // 省略了 public static final
}

public class Ball implements Rollable{
private String name;

public String getName() {
return name;
}

public Ball(String name) {
this.name = name;
}

@Override
public void play() {
ball = new Ball("Football"); // Cannot assign a value to final variable 'ball'
System.out.println(ball.getName());
}

}

首先解释代码:先是定义了两个接口,并且都有 play 抽象方法,然后定义了 Rollable 接口去继承了这两个接口,同时自身 new 了一个类 Ball 的对象,其次 Ball 类又实现了 Rollable 接口,直接定义了一个属性并且写了 get 方法,还定义了一个构造器,然后实现了 play 方法。

考点1:这个实现的 paly 方法,到底是实现的那个接口的?

答:因为两个接口的抽象方法重名,所以相当于两个都实现了

考点2:上述代码中的错误之处在那儿?

答:因为在接口中定义的是全局常量,省略了 public static final,后面在 Ball 类中对 ball 常量做了修改操作,所以编译器报错。