面向对象_高级2
5. 关键字:final
5.1 final 修饰类
表示这个类不能被继承,没有子类。提高安全性,提高程序的可读性。例如:String类、System类、StringBuffer类。
final class Eunuch{//太监类
}
class Son extends Eunuch{//错误
}
5.2 final 修饰方法
表示这个方法不能被子类重写。例如:Object类中的getClass()
class Father{
public final void method(){
System.out.println("father");
}
}
class Son extends Father{
public void method(){//错误
System.out.println("son");
}
}
5.3 final 修饰变量
final修饰某个变量(成员变量或局部变量),一旦赋值,它的值就不能被修改,即常量,常量名建议使用大写字母。例如:final double MY_PI = 3.14;
public final class Test {
public static int totalNumber = 5;
public final int ID;
public Test() {
ID = ++totalNumber; // 可在构造器中给final修饰的“变量”赋值
}
public static void main(String[] args) {
Test t = new Test();
System.out.println(t.ID);
}
}
6. 抽象类
6.1 由来
随着继承层次中一个个新子类的定义,类变得越来越具体,而父类则更一般,更通用。类的设计应该保证父类和子类能够共享特征。有时将一个父类设计得非常抽象,以至于它没有具体的实例,这样的类叫做抽象类。
6.2 语法格式
- 抽象类:被abstract修饰的类。
- 抽象方法:被abstract修饰没有方法体的方法。
抽象类的语法格式
[权限修饰符] abstract class 类名{
}
[权限修饰符] abstract class 类名 extends 父类{
}
抽象方法的语法格式
[其他修饰符] abstract 返回值类型 方法名([形参列表]);
注意:抽象方法没有方法体
6.3 使用说明
-
抽象类不能创建对象,如果创建,编译无法通过而报错。只能创建其非抽象子类的对象。
理解:假设创建了抽象类的对象,调用抽象的方法,而抽象方法没有具体的方法体,没有意义。
抽象类是用来被继承的,抽象类的子类必须重写父类的抽象方法,并提供方法体。若没有重写全部的抽象方法,仍为抽象类。
-
抽象类中,也有构造方法,是供子类创建对象时,初始化父类成员变量使用的。
理解:子类的构造方法中,有默认的super()或手动的super(实参列表),需要访问父类构造方法。
-
抽象类中,不一定包含抽象方法,但是有抽象方法的类必定是抽象类。
理解:未包含抽象方法的抽象类,目的就是不想让调用者创建该类对象,通常用于某些特殊的类结构设计。
-
抽象类的子类,必须重写抽象父类中所有的抽象方法,否则,编译无法通过而报错。除非该子类也是抽象类。
理解:假设不重写所有抽象方法,则类中可能包含抽象方法。那么创建对象后,调用抽象的方法,没有意义。
6.4 注意事项
-
不能用abstract修饰变量、代码块、构造器;
-
不能用abstract修饰私有方法、静态方法、final的方法、final的类。
7. 接口
7.1 Java 接口的核心概念
接口(Interface)在 Java 中是一个完全抽象的类型,定义了一组方法的规范,而不包含这些方法的实现。接口用于指定类必须提供的行为,而不关心这些行为如何实现。Java 接口是面向对象编程中的一种重要工具,用来实现 多态、解耦 和 契约式编程。
7.2 接口的设计原则
-
契约式编程:
- 接口定义了一组契约(即方法),类通过实现这些接口来表明它们遵守这些契约。接口作为类与类之间的“协议”,确保实现接口的类提供特定的行为。
-
接口隔离原则:
- 每个接口应该定义一个独立的角色或功能,避免“大而全”的接口。通过细粒度的接口设计(即多个小接口而不是单个大的接口),可以使实现类更灵活。
-
解耦:
- 接口通过定义规范,将实现与使用分离。这种解耦让代码更容易扩展和维护,尤其是在大规模项目中,可以降低类之间的耦合度。
7.3 接口的特性和细节
-
抽象方法:
- 接口中的方法默认是
public
和abstract
,必须由实现类提供具体的实现。 - Java 8 之前,接口中只能包含抽象方法;Java 8 引入了默认方法和静态方法。
- 接口中的方法默认是
-
默认方法(Default Methods):
- Java 8 引入了
default
关键字,允许接口定义带有方法体的默认方法。默认方法使得接口可以在不破坏现有实现的情况下扩展功能。 - 例如:
interface Vehicle { default void start() { System.out.println("Vehicle is starting"); } }
- Java 8 引入了
-
静态方法:
- Java 8 还允许在接口中定义静态方法,这些方法可以直接通过接口名调用,不需要通过实例。
- 例如:
interface Utility { static void printInfo() { System.out.println("This is a utility method."); } }
-
常量:
- 接口可以包含常量,这些常量默认是
public static final
的。常量通常用来定义与接口相关的固定值。 - 例如:
interface MathConstants { double PI = 3.14159; }
- 接口可以包含常量,这些常量默认是
-
多继承的支持:
- Java 类不支持多继承,但接口可以通过
implements
关键字实现多个接口,从而模拟多继承的效果。这使得接口成为一个灵活的、多用途的工具。 - 例如:
interface A { void methodA(); } interface B { void methodB(); } class C implements A, B { public void methodA() { System.out.println("Method A"); } public void methodB() { System.out.println("Method B"); } }
- Java 类不支持多继承,但接口可以通过
-
函数式接口(Functional Interface):
- Java 8 引入了函数式接口,这是只有一个抽象方法的接口,常用于 Lambda 表达式和方法引用。
- 函数式接口使用
@FunctionalInterface
注解进行标注,以确保接口符合函数式接口的要求。 - 例如:
@FunctionalInterface interface Comparator<T> { int compare(T o1, T o2); }
7.4 接口与抽象类的对比
-
接口:
- 完全抽象,只能定义方法签名和常量。
- 允许多重继承,实现类可以同时实现多个接口。
- 适用于定义行为契约,不涉及实现细节。
-
抽象类:
- 可以包含抽象方法和具体方法,以及普通成员变量。
- 只能单继承,子类必须从一个父类继承。
- 适用于定义通用功能和共享代码的基类。
7.5 实际应用场景
-
设计 API:
- 接口在设计 API 时非常有用。API 可以定义一组接口,让开发者通过实现这些接口来扩展 API 的功能,而无需修改 API 的核心代码。
-
策略模式:
- 接口常用于设计模式中的策略模式,通过定义策略接口,不同的实现类代表不同的策略,使得算法可以在运行时互相替换。
-
事件处理机制:
- 在事件驱动的编程模型中,接口通常用于定义事件处理器。例如,在 GUI 编程中,按钮点击事件处理器通常是一个实现特定接口的类。
示例代码:
package com.atguigu.interfacetype;
public interface USB3{
//静态常量
long MAX_SPEED = 500*1024*1024;//500MB/s
//抽象方法
void in();
void out();
//默认方法
default void start(){
System.out.println("开始");
}
default void stop(){
System.out.println("结束");
}
//静态方法
static void show(){
System.out.println("USB 3.0可以同步全速地进行读写操作");
}
}
提问:既然有了父子类之间的继承,为什么还需要实现接口呢?
回答:这是是理解面向对象编程中继承和接口各自优势的关键。虽然父子类之间的继承和接口实现都涉及到代码的复用和行为的共享,但它们的用途和设计意图有所不同。
- 父子类继承和接口实现的区别
-
继承(Inheritance):
- 继承的含义:继承是一种**“is-a”**(是一个)的关系。子类继承父类,表示子类是父类的一种特化。比如
Dog
继承自Animal
,表示Dog
是一种Animal
。 - 代码复用:继承允许子类复用父类的代码。子类不仅继承了父类的行为(方法),还继承了父类的属性(字段)。
- 实现重用:通过继承,子类可以直接使用父类已经实现的方法,也可以通过重写(override)来改变父类方法的行为。
- 限制:Java 中的类是单继承的,即一个类只能继承自一个父类。这是为了避免多继承带来的复杂性问题,如菱形继承问题。
- 继承的含义:继承是一种**“is-a”**(是一个)的关系。子类继承父类,表示子类是父类的一种特化。比如
-
接口(Interface):
- 接口的含义:接口定义了一组方法规范,表示实现类必须具备某些行为,但不关心这些行为的具体实现。接口体现的是**“can-do”(能做)或者“like-a”**(像一个)的关系。比如,
Dog
实现Pet
接口,表示Dog
可以像一个Pet
那样行为。 - 解耦设计:接口用于解耦具体实现和行为规范。通过接口,可以规定某一类对象的行为,而不必关心它们的具体类型。
- 多态:接口支持实现类的多态性,不同的实现类可以以相同的方式被处理。例如,一个方法接受一个
List
接口类型的参数,它可以处理ArrayList
、LinkedList
,或任何实现了List
接口的类。 - 多继承:Java 中的接口支持多继承。一个类可以实现多个接口,从而同时拥有多种行为。这是接口比类继承更灵活的地方。
- 接口的含义:接口定义了一组方法规范,表示实现类必须具备某些行为,但不关心这些行为的具体实现。接口体现的是**“can-do”(能做)或者“like-a”**(像一个)的关系。比如,
- 为什么需要接口?
-
实现解耦和灵活性:
- 接口允许类之间的依赖更加松散。例如,假设你有一个方法
processPayment(PaymentMethod method)
,你可以使用CreditCard
、PayPal
、BankTransfer
等多种方式实现PaymentMethod
接口,而processPayment
方法不需要知道这些类的具体实现细节。
- 接口允许类之间的依赖更加松散。例如,假设你有一个方法
-
支持多重继承:
- Java 的类不能多继承,但接口可以。因此,如果一个类需要多种不同的行为,接口是唯一的选择。例如,一个
Robot
可能既需要实现Movable
接口(能够移动),又需要实现Soundable
接口(能够发声)。
- Java 的类不能多继承,但接口可以。因此,如果一个类需要多种不同的行为,接口是唯一的选择。例如,一个
-
统一行为契约:
- 接口定义了类应该具备的行为,而不关心类的继承关系。这种方式让不同的类可以表现出类似的行为,支持多态操作。例如,
Comparator
接口定义了对象的比较方式,任何实现了Comparator
接口的类都可以被用于排序,而不必在乎它们继承自哪个类。
- 接口定义了类应该具备的行为,而不关心类的继承关系。这种方式让不同的类可以表现出类似的行为,支持多态操作。例如,
-
提高代码可测试性:
- 在测试中,通过接口注入依赖对象,你可以很容易地替换具体的实现类,从而实现更灵活的单元测试。这是依赖注入和控制反转(IoC)模式的基础。
接下来通过一个简单的例子来说明接口如何支持实现类的多态性:
- 场景描述
假设你正在开发一个支付系统,该系统可以处理多种不同的支付方式,比如信用卡支付(CreditCardPayment
)、PayPal 支付(PayPalPayment
)、以及银行转账支付(BankTransferPayment
)。每种支付方式都有不同的实现细节,但它们都具备一个共同的行为,即“支付”。
- 定义接口
我们首先定义一个 PaymentMethod
接口,包含一个 pay()
方法,该方法表示支付操作:
interface PaymentMethod {
void pay(double amount);
}
- 实现接口的类
接下来,我们创建三个不同的类来实现这个接口,每个类代表一种支付方式:
class CreditCardPayment implements PaymentMethod {
@Override
public void pay(double amount) {
System.out.println("Processing credit card payment of $" + amount);
}
}
class PayPalPayment implements PaymentMethod {
@Override
public void pay(double amount) {
System.out.println("Processing PayPal payment of $" + amount);
}
}
class BankTransferPayment implements PaymentMethod {
@Override
public void pay(double amount) {
System.out.println("Processing bank transfer payment of $" + amount);
}
}
- 支持多态性的代码
现在,我们可以编写一个方法来处理支付操作,这个方法接受一个 PaymentMethod
类型的参数。这就是多态性的关键:我们不需要知道具体是哪一种支付方式,只需要知道它们都实现了 PaymentMethod
接口。
public class PaymentProcessor {
public void processPayment(PaymentMethod paymentMethod, double amount) {
paymentMethod.pay(amount);
}
public static void main(String[] args) {
PaymentProcessor processor = new PaymentProcessor();
PaymentMethod payment1 = new CreditCardPayment();
PaymentMethod payment2 = new PayPalPayment();
PaymentMethod payment3 = new BankTransferPayment();
processor.processPayment(payment1, 100.00);
processor.processPayment(payment2, 200.00);
processor.processPayment(payment3, 300.00);
}
}
- 输出结果
当你运行上面的代码时,你会看到以下输出:
Processing credit card payment of $100.0
Processing PayPal payment of $200.0
Processing bank transfer payment of $300.0
-
解释
-
多态性:通过接口
PaymentMethod
,我们可以以统一的方式处理不同类型的支付方式。processPayment()
方法接受任何实现了PaymentMethod
接口的对象,这使得代码非常灵活。 -
接口的作用:接口定义了支付行为(
pay()
),不同的支付方式类实现了这一行为。尽管每个类的实现细节不同,但它们都可以被统一处理。这就是多态性的体现。
-
-
为什么这很强大?
-
代码解耦:
PaymentProcessor
类不依赖具体的支付实现类,只依赖接口PaymentMethod
。这使得系统更加模块化和易于维护。 -
扩展性强:如果以后你要添加新的支付方式(例如
BitcoinPayment
),你只需实现PaymentMethod
接口,而不需要修改PaymentProcessor
类的代码。 -
统一接口,统一处理:多态性允许在处理不同类型的对象时使用相同的代码逻辑。这使得代码更简洁,更容易理解和维护。
- 总结
接口支持实现类的多态性意味着你可以用同一个接口类型的引用来指向不同的实现类对象,并通过这个引用来调用接口定义的方法。实际执行的是引用所指向的具体实现类的方法。这种设计极大地提升了代码的灵活性和可扩展性。