Java 每日一刊(第14期):抽象类和接口
“抽象是所有能力的精髓。”
前言
这里是分享 Java 相关内容的专刊,每日一更。
本期将为大家带来以下内容:
- 抽象类
- 接口
- 抽象类和接口的区别
- 什么时候用抽象类,什么时候用接口
- 抽象类可以实现接口
- 接口中的常量其实是
public static final
- 标记接口(Marker Interface)
- 接口可以扩展多个接口
- 接口中的方法可以有多个默认实现来源
抽象类
抽象类是一种不能直接创建对象的类,它是一种模板,通常是为了给其他类用来继承的。抽象类可以包含两种方法:
- 抽象方法:只有方法名称,没有具体的功能代码,子类必须要实现这些方法。
- 具体方法:已经写好了功能代码的方法,子类可以直接用。
抽象类的特点
不能直接创建对象:你不能直接写 new Animal()
来创建一个抽象类的对象。它的主要作用是让其他类继承它。
可以有方法的具体实现:有一些方法在抽象类里已经实现了,所以子类可以直接用这些方法。
可以有构造方法:虽然你不能直接创建抽象类的对象,但你可以通过它的子类调用构造方法。
抽象类的例子
假设你想设计一个动物的类(Animal
),但你不知道每种动物发出的声音是什么样的。所以你把 makeSound()
定义成一个抽象方法,让不同的动物自己去实现它。
// 抽象类使用 abstract 关键字定义
abstract class Animal {// 抽象方法,没有实现,需要子类去实现public abstract void makeSound();// 具体方法,子类可以直接使用public void sleep() {System.out.println("Sleeping...");}
}// 使用 extens 关键字继承自抽象类 Animal 的具体类
class Dog extends Animal {// 实现抽象方法@Overridepublic void makeSound() {System.out.println("Woof");}
}public class Test {public static void main(String[] args) {Animal dog = new Dog(); // 创建 Dog 对象dog.makeSound(); // 输出:Woofdog.sleep(); // 输出:Sleeping...}
}
解释:
Animal
类是抽象类,它定义了一个抽象方法makeSound()
,不同的动物发出的声音不同,所以这个方法没有实现。Dog
类继承了Animal
类,并实现了makeSound()
方法。sleep()
方法在抽象类里已经实现了,子类(如Dog
)可以直接使用它。
抽象类的使用场景
当你想要让多个类(例如不同的动物)共享一些相同的功能(如 sleep()
方法),但有些方法(如 makeSound()
)需要不同的实现时,你可以使用抽象类。
接口
接口是一种比抽象类更抽象的东西,它定义了一些方法,但这些方法都没有实现。类可以通过 implements
关键字来“实现”接口里的方法。
接口的特点
不能有具体方法:接口里的方法在 Java 8 之前都是空的,没有具体实现。它只告诉你这个方法应该存在,但具体怎么做由类自己决定。
可以多实现:一个类可以实现多个接口,而不像继承只能继承一个父类。
接口强调行为:接口更像是“契约”或“协议”,它规定了一个类应该具备哪些功能。
接口的例子
假设你想定义一种交通工具的接口(Vehicle
),每种交通工具都应该有 start()
和 stop()
方法,但你不需要知道每种交通工具具体是怎么启动或停止的,这由具体的交通工具类去实现。
// 使用 interface 关键字定义一个接口
interface Vehicle {// 接口中的方法都是抽象的,不需要写具体实现void start();void stop();
}// 使用 implements 关键字实现接口的具体类
class Car implements Vehicle {// 实现接口中的方法@Overridepublic void start() {System.out.println("Car is starting");}@Overridepublic void stop() {System.out.println("Car is stopping");}
}public class Test {public static void main(String[] args) {Vehicle car = new Car(); // 创建 Car 对象car.start(); // 输出:Car is startingcar.stop(); // 输出:Car is stopping}
}
解释:
Vehicle
是一个接口,它定义了start()
和stop()
两个方法,但没有给出具体实现。Car
类实现了Vehicle
接口,并具体定义了这两个方法该怎么做。
接口的使用场景
当你希望不同的类(如汽车、飞机)有相同的行为(如 start()
和 stop()
),但它们的具体实现不同时,使用接口会更合适。
抽象类和接口的区别
对比点 | 抽象类 | 接口 |
---|---|---|
定义 | 可以包含抽象方法和具体方法 | 只能包含抽象方法(Java 8 之后可以有默认方法) |
成员变量 | 可以包含成员变量 | 只能包含常量(public static final ) |
构造函数 | 可以有构造函数 | 不能有构造函数 |
继承关系 | 只能继承一个抽象类 | 可以实现多个接口 |
适用场景 | 用于定义类的通用行为,提供部分实现 | 用于定义类的行为规范,不提供实现 |
访问修饰符 | 可以有不同的访问修饰符(public 、protected ) | 默认所有方法是 public |
什么时候用抽象类,什么时候用接口
用抽象类的情况:如果你有一些功能是所有子类共享的,但某些功能需要子类去实现,这时抽象类比较合适。例如,所有的动物都会“睡觉”(sleep()
),但发出的声音不同(makeSound()
)。
用接口的情况:如果你想为完全不同的类定义共同的行为,而不关心它们之间是否有继承关系,接口更合适。例如,汽车和飞机都是交通工具,都需要 start()
和 stop()
功能,但它们可能没有其他的共同点。
抽象类可以实现接口
尽管抽象类不能被直接实例化,但它们可以实现接口中的方法,而不一定要求子类必须重新实现。这种做法适合为接口提供部分实现,子类可以选择使用或覆盖它们。
interface Walkable {void walk();
}abstract class Animal implements Walkable {@Overridepublic void walk() {System.out.println("Animal is walking.");}// 抽象类中可以有自己的抽象方法public abstract void makeSound();
}
在这个例子中,抽象类 Animal
实现了 Walkable
接口中的 walk()
方法,而子类可以继承这个实现或进行自定义。
接口中的常量其实是 public static final
在接口中定义的字段默认是 public static final
,即它们都是常量,必须在声明时初始化,且不能修改。即使你没有显式声明这些修饰符,编译器也会自动加上。
interface MyConstants {// 实际上等同于:public static final int MAX_VALUE = 100int MAX_VALUE = 100;
}
这意味着接口中的所有字段都是全局常量,并且只能用于共享常量的场景。
标记接口(Marker Interface)
Java 中有一种特殊的接口叫做标记接口(Marker Interface),它没有任何方法,作用仅仅是标记某个类具有某种特性。比如 java.io.Serializable
,实现了这个接口的类可以被序列化。
interface MarkerInterface {// 没有方法,仅用于标记类具有某种属性
}class MyClass implements MarkerInterface {// 这个类被标记为拥有 MarkerInterface 的属性
}
标记接口本质上是一种元数据,用于表示类的某些行为或特性。这种设计理念后来被注解(Annotations)部分取代。
接口可以扩展多个接口
一个接口可以继承多个接口,这让 Java 实现了某种形式的“多继承”,但它不同于类的多继承,类的多继承在 Java 中是被禁止的。这种机制允许你通过继承多个接口来组合各种行为。
interface Eater {void eat();
}interface Sleeper {void sleep();
}interface Animal extends Eater, Sleeper {void makeSound();
}class Dog implements Animal {@Overridepublic void eat() {System.out.println("Dog is eating.");}@Overridepublic void sleep() {System.out.println("Dog is sleeping.");}@Overridepublic void makeSound() {System.out.println("Dog says: Woof!");}
}
Dog
类通过实现 Animal
接口,间接地获得了 Eater
和 Sleeper
接口中的方法。这种设计使得代码更具扩展性。
接口中的方法可以有多个默认实现来源
在 Java 8 引入了默认方法后,一个类可以从多个接口继承默认方法。如果这些接口中定义了相同的方法名,类必须手动解决冲突。这种情况在多重继承的情况下常见。
interface InterfaceA {default void show() {System.out.println("InterfaceA");}
}interface InterfaceB {default void show() {System.out.println("InterfaceB");}
}class MyClass implements InterfaceA, InterfaceB {@Overridepublic void show() {InterfaceA.super.show(); // 选择调用 InterfaceA 的 show()}
}
在这种情况下,你必须明确指明调用哪个接口的默认实现。通过 InterfaceA.super.show()
可以解决冲突。
本期小知识
在 Java 8 之后,接口中可以定义默认方法(default methods
)和静态方法。这是接口功能的重要扩展,因为传统的接口不能包含实现。
默认方法:允许在接口中定义方法的默认实现,不强制要求实现类去实现它们。这在维护接口的向后兼容性上起到了很大作用。
interface Vehicle {default void start() {System.out.println("Starting the vehicle");}
}
静态方法:接口中的静态方法只能通过接口名称调用,不能被子类继承或覆盖。
interface Vehicle {static void checkEngine() {System.out.println("Engine checked");}
}