• 接口 vs 抽象类:如何在Java中做出正确的选择


    前言

      接口和抽象类是面向对象编程中常用的概念,它们在软件设计中起着重要的作用。

    研究

    接口

    1. 接口特点

      接口是一种纯粹的规范,用于定义类之间的契约。它只包含方法的定义,没有具体的实现。接口的作用包括:

    • 强制实现:实现类必须提供接口中的方法实现,以确保一致的行为。
    • 多继承性:允许子类继承(实现)多个接口来扩充自己的能力;接口也可以继承接口,形成组合式接口来扩充自己的功能。
    • 实现多态:允许不同的类实现相同的接口,并以不同的方式实现方法。
    • 无法实例化:包含抽象方法无法被实例化的,因为如果实例化成对象,调用抽象方法时程序不知道怎么执行。
    • 成员类型:接口可以定义常量,这些常量被隐式声明为public static final

    2. 怎么设计接口

      接口是对行为的抽象,应该先把要干什么事定下来,然后依据接口隔离原则封装成接口。为了避免让子类实现不需要的方法,接口的功能就应该单一,而不是大而全。

    客户端不应该依赖它不需要的接口。一个类对另一个类的依赖应该建立在最小的接口上。 ——接口隔离原则

      这样就足够了,其实设计接口不太难,因为接口和实现类耦合度较低,即实现类主体是什么、如何实现的,接口并不关心,只需要保证设计的方法能够充分让实现类做自己想做的事。

      只需要让实现类做到继承一个接口就好像插上一件特定功能的装备,需要什么功能就继承什么接口。

      展开来说有以下几个点需要注意:

    1. 功能单一:使用多个专门的接口比使用单一的总接口要好,接口尽量小,具有清晰的目标和功能。
    2. 注意命名:使用有意义的方法和属性命名,使接口易于理解和使用。
    3. 足够抽象:考虑接口的一致性和可扩展性,尽量避免定义过于具体或不必要的方法。

    3. 接口的新特性

      从Java 8开始,接口引入了一些新的接口特性,这些特性让接口在功能和灵活性上得到了增强。

    1. 默认方法(Default Methods):接口的方法可以有默认实现了!这样一来,实现该接口的类可以选择性地覆盖默认方法,而不是强制实现所有方法。
    public interface Vehicle {
        void start();
    
        default void stop() {
            System.out.println("Vehicle stopped.");
        }
    }
    
    public class Car implements Vehicle {
        @Override
        public void start() {
            System.out.println("Car started.");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Vehicle car = new Car();
            car.start();  // 输出:Car started.
            car.stop();   // 输出:Vehicle stopped.
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    1. 静态方法(Static Methods):接口可以包含静态方法!静态方法属于接口,可以直接通过接口名称调用。
    public interface MathUtils {
        static int add(int a, int b) {
            return a + b;
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            int sum = MathUtils.add(3, 5);
            System.out.println("Sum: " + sum);  // 输出:Sum: 8
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    1. 私有方法(Private Methods):接口可以包含私有方法!私有方法只能在接口内部被调用,也就是可以被默认实现调用。但是私有方法不能被静态方法调用。
    public interface Calculator {
        default int add(int a, int b) {
            return addNumbers(a, b); //只允许内部调用
        }
    
        private int addNumbers(int a, int b) {
            return a + b;
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Calculator calculator = new Calculator() {};
            int sum = calculator.add(3, 5);
            System.out.println("Sum: " + sum);  // 输出:Sum: 8
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    抽象类

    1. 抽象类特点

      抽象类是一个类,包含抽象方法的类叫做抽象类,它自己也可以有具体的方法实现和属性定义,子类可以直接复用这些代码。普通类和抽象类相比只少了抽象方法。

    • 提供默认实现:抽象类的非抽象方法有具体逻辑,子类们可以直接复用父类的方法,也可以根据需要选择性地覆盖。
    • 表示共同特征:抽象类具有一些抽象方法,子类需要给抽象方法以具体实现,或者子类也成为一个抽象类,再写一次该抽象方法,目的是使子类们具有一致的行为和属性。
    • 单继承:子类只能继承一个抽象类(父类),这就对父类的要求比较高了,因为只能继承一个父类,子类需要父类提供所有子类的所有共性。
    • 无法实例化:包含抽象方法就无法被实例化,这点和接口是一样的。注意:抽象类也可以不包含抽象方法,但是仍然不能实例化,ClassLoader类就是如此设计的。

    2. 如何设计一个抽象类

      设计抽象类的难度比设计接口大,因为抽象类本质是类,包含子类的通用特性,接口是行为的抽象,抽象类内容上比接口复杂,功能上比接口强大。
      抽象类和它的子类关系紧密,耦合度非常高。设计抽象类是一个自底向上的过程,应该先有子类,再根据子类的共性抽象出抽象类。

      设计应考虑按照以下步骤:

    1. 确定类的目的:首先,确定抽象类的目的和职责,并让抽象类提供一些通用的行为或属性。可以通过具体的子类们,找出子类们的共同点(属性、方法)。
    2. 定义抽象类: 命名规范,使用有意义的类、方法和属性命名,易于理解和使用。
    3. 添加抽象方法:抽象方法是没有实现的方法,因为足够抽象,以至于本类没有对方法的实现。比如说动物这个类,是不具备吃饭这个方法的具体实现的,而继承动物类的猫类、鱼类则可以有吃饭这个方法的具体实现。找到这样特征的方法,将它标识为抽象方法。

    3.实际案例

      职责链模式就使用了抽象类,因为它既要提供抽象的处理方法让子类实现,也要提供存储下一链的属性,所以使用了抽象类而不是接口。

    public abstract class RuleHandler {
    
        protected RuleHandler successor;
    
        /**
         * 设置规则职责链继任者
         */
        public void setSuccessor (RuleHandler successor){
            this.successor = successor;
        }
    
        /**
         * 处理请求的抽象方法
         */
        public abstract ResponsibilityChainParameterModel handleRequest(ResponsibilityChainParameterModel parameterModel) ;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    延伸问题

    1. 什么时候一定用接口,什么时候一定用抽象类

      这要依据业务场景而定。
      以下几个情况时,可以使用接口:

    1. 要给类加强一个特定的功能,这个功能是单一行为,并且和类的本质无关,可以把这个功能定义成接口。
    2. 当需要解耦类与类之间的依赖关系,提高代码的灵活性和可扩展性,可以让不同的类实现同一接口。
    3. 当需要定义类之间的契约,并且强制实现类提供特定的方法,满足特定的规范,可以把规范定义成接口。

      以下情况可以使用抽象类:
      当有一组类具有重复的方法和属性,应抽象出一个父类,方便管理子类的行为规范,实现代码的复用。如果该父类有些方法没有默认实现,只有抽象概念,则转为抽象类。


    2. 普通父类能不能代替抽象类

      其实不能,抽象类的抽象方法规定子类必须实现,这样的强制行为规范是普通父类做不到的。另外还有一个区别是抽象类不能被实例化,普通父类可以。


    3. 接口新特性为什么可以写实现,直接用抽象类不行吗

    1. 默认方法的意义:
      在接口中添加默认方法不会破坏现有的实现类,因为实现类不需要强制实现该方法。这样就可以在接口中演化和扩展新的功能,而不会影响已有的代码。

    2. 静态方法的意义:
      静态方法可以在接口中定义一些独立于实例的方法,无需创建接口的实例就可以直接使用。这样可以避免在某些情况下创建实例的开销。并且扩展功能也不会影响已有的代码。

    3. 私有方法的意义:
      代码重用和封装:私有方法可以在接口内部封装一些共享的代码逻辑,供默认方法调用。这样可以避免代码的重复编写,提高代码的可维护性和可读性。

      内部实现细节隐藏:私有方法只能在接口内部被调用,对于接口的使用者来说是不可见的。这样可以隐藏接口的内部实现细节,只向外部暴露必要的方法。

      直接用抽象类不行。java中是单继承,只能继承一个类,使用抽象类一定会让类之间耦合关系紧密,但是接口可以实现多个,耦合关系较低。


    总结

      基础是非常重要的,这方面的思考不能少,有兴趣的可以在评论区发表见解,我们一起讨论。

  • 相关阅读:
    第二证券今日投资参考:苹果WWDC大会开幕 地产板块再迎催化
    高等教育学:课程
    Spring6(二):IoC容器
    STM32 HAL库F103系列之ADC实验(三)
    任意微信公众号短链实时获取阅读量、点赞数爬虫方案(不会Hook可用)
    又一微信自动化框架wxauto横空出世了!
    【JavaGuide学习笔记】Day.3
    解析数仓lazyagg查询重写优化
    git-仓库迁移并保留commit log
    【负荷预测】布谷鸟(CS)算法优化BP神经网络的负荷及天气预测(Matlab代码实现)
  • 原文地址:https://blog.csdn.net/CharmaineXia/article/details/134281331