设计程序时怎么选择抽象类和接口

        抽象类和接口都是面向对象编程的重要概念,它们都可以用来表示某种抽象的概念,但在特定的情况下,需要根据具体的需求来选择使用哪种方式。

        抽象类可以包含一些具体的方法或成员变量,但同时也可以包含一些抽象方法,这些抽象方法必须由其子类去实现。抽象类一般表示“is-a”的关系,也就是说,如果一个类继承了某个抽象类,那么它可以被看作是这个抽象类的子类,同时也可以使用这个抽象类的方法和属性。

        接口则是一组抽象方法的集合,接口只定义了方法的签名而没有具体的实现。接口一般表示“can-do”的关系,也就是说,如果一个类实现了某个接口,那么它可以被看作是这个接口的实现类,同时也必须实现这个接口中的所有方法。

下面是一些选用抽象类和接口的一些指导原则:

  1. 如果需要在定义抽象类的同时,即包含某些具体方法又需要某些方法由子类来实现,可以选用抽象类。

  2. 如果需要定义一组规范,即只是定义一些方法的签名,而不需要具体的实现,可以选用接口。

  3. 如果需要创建一些类来表示不同的实体,在这些类中实现相同的方法并且可能包含一些共有的方法,那么可以选用抽象类。

  4. 如果需要创建一组不同的类来实现同一组接口,那么可以选用接口。

  5. 如果需要在接口中添加新的方法,会破坏原有的代码结构,这时候可以使用抽象类。

  6. 如果需要在不同的类之间共享某些抽象方法的实现代码,那么可以使用抽象类。
     

一、抽象类与接口的区别

        抽象类与接口都是面向对象编程中的重要概念,它们都具有表达抽象概念和规范性质的特点,但两者也有不同之处。

  • 抽象类是一种特殊的类,它不能被实例化,只能作为其他具体类的父类来使用。抽象类可以包含抽象方法和非抽象方法,抽象方法必须在子类中进行重写或实现。由于具体类只能继承一个抽象类,因此抽象类的用途不是很灵活。
  • 接口是一种完全抽象化的类型,它定义了对象应该具备的行为,并由实现类来完成具体的操作。接口中只包含抽象方法和常量,实现接口的类必须实现所有的抽象方法。可通过一个类实现多个接口,从而增加代码的灵活性。

        总结起来,抽象类与接口的主要区别如下:

抽象类接口
实例化不能被实例化不能被实例化
继承可以被具体类继承,一个具体类只能继承一个抽象类可以被具体类实现,一个具体类可以实现多个接口
方法包含成员变量、抽象方法和非抽象方法,抽象方法必须在子类中进行重写或实现只包含抽象方法和常量,实现接口的类必须实现所有方法

        JDK 1.8 后,抽象类和接口的主要区别。表格中 "√" 表示支持该特性,"×" 表示不支持。

特性抽象类接口
构造方法×
实例变量×
非抽象方法×
多重继承×
默认方法×
静态方法×
私有方法×
变量默认类型无限制常量

        需要注意的是,表格只是总结了一些主要特性,实际上在使用抽象类和接口时还需要考虑其他因素,如代码复用、可扩展性、可读性等。选择抽象类或接口应该根据具体情况进行分析和判断。

        Java 8 中,接口中除了常量外,还有默认方法(Default Method)、静态方法(Static Method)。

        默认方法是一种包含了接口实现的方法。它可以包含代码块和默认实现,当实现类没有实现该方法时,直接调用该方法的默认实现。

        静态方法仅能以接口的方式进行访问。这个特性主要为了保持向后兼容性,因为在 Java 8 之前,接口中只能定义常量。

        私有方法是在 Java 9 中引入的新特性可以用来组织并减少接口中公用代码的复制。私有方法只能在接口内部可见,不能被实现该接口的类或其他类访问。这些私有方法通常被默认方法或静态方法调用,用于减少代码重复。

        以下示例演示了默认方法、静态方法和私有方法:

interface MyInterface {
    // 常量
    public static final int MY_CONST = 10;

    // 默认方法
    default void defaultMethod() {
        System.out.println("This is a default method in MyInterface.");
    }

    // 静态方法
    static void staticMethod() {
        System.out.println("This is a static method in MyInterface.");
    }

    // 私有方法
    private void privateMethod() {
        System.out.println("This is a private method in MyInterface.");
    }

    // 默认方法调用私有方法
    default void methodUsingPrivateMethod() {
        privateMethod();
    }
}

class MyClass implements MyInterface {
    // 实现接口的抽象方法
    @Override
    public void defaultMethod() {
        System.out.println("This is a custom implementation of default method.");
    }
}

public class Main {
    public static void main(String[] args) {
        // 调用默认方法
        MyInterface obj1 = new MyClass();
        obj1.defaultMethod();  // 输出:This is a custom implementation of default method.

        // 调用静态方法
        MyInterface.staticMethod();  // 输出:This is a static method in MyInterface.

        // 私有方法只能在接口内部使用
        // MyInterface.privateMethod();  // 编译错误

        // 默认方法调用私有方法
        obj1.methodUsingPrivateMethod();  // 输出:This is a private method in MyInterface.
    }
}

二、抽象类与接口各自的优点

        抽象类的优点

  1. 抽象类可以包含具体方法和抽象方法,子类继承抽象类后只需要实现其中的抽象方法即可。
  2. 抽象类可以作为某些类的基类,表示某个范围或某个共性。
  3. 抽象类可以包含成员变量和构造方法,因此可以用来封装一些公共行为。

        接口的优点

  1. 接口定义了一种规范,声明了一个类应该具备哪些方法,使得代码更加规范、易于维护和扩展。
  2. 接口可以被多个类实现,从而实现了多态的效果,更好地利用了面向对象编程的特性。
  3. 接口没有具体的实现,从而避免了父类与子类之间的耦合问题,增加了代码的灵活性和可拓展性。

        总之,抽象类和接口在不同的情况下都有它们独特的优势,在设计程序时可以根据具体的需求来选择使用哪种方式。如果需要提供一些通用的行为和属性,可以考虑使用抽象类。如果需要明确指定某些规范,并且允许多个类实现,则可以使用接口。

三、设计程序时怎么选择抽象类和接口

        在设计程序时,抽象类和接口是实现代码重用和实现多态性的两种重要手段。通常来说,可以根据以下几个方面来选择应该使用哪一种方式:

  1. 设计目的

            如果你需要定义一个共性的类或者需要提供默认实现,那么应该使用抽象类。抽象类可
    以包含部分具体实现,并且可以被子类继承,所以比较适合描述一类事物的共有特征和行为。

            如果你需要定义一组规范或者需要实现某些特定行为,那么应该使用接口。接口不能包含任何具体实现,但可以定义一组方法签名,用于描述外部和内部的交互规范,所以更适合描述某种规范或行为约束。

  2. 继承关系

            抽象类通常作为类层次结构中的父类存在,并被其子类继承。由于 Java 只支持单继承,所以如果已经存在一个类层次结构,且你需要添加新的行为或者属性,那么可以将这些共性的行为和属性定义在抽象父类中,然后让子类继承并实现自己的特定行为。

            接口则常常作为行为层次结构中的一部分存在,可以通过实现接口来表明类所支持的一组行为或规范。由于一个类可以实现多个接口,因此可以给类赋予更多的行为和能力,而不用改变继承层次结构。

  3. 设计风格

            抽象类通常采用自下而上的设计风格,即从具体实现逐步抽象出共性的特征和行为,最终形成类层次结构。这种设计方式便于实现代码重用和提高程序的可读性和可维护性。

            接口则常常采用自上而下的设计风格,即从需要满足的规范或行为开始,逐步细化为一组方法签名。这种设计方式便于提高程序的灵活性和扩展性,同时也可以降低程序的耦合度。
     

四、简单的例子:

         当需要定义一棵类继承树,并为子类提供一些行为实现时,通常应该选择使用抽象类。例如:

abstract class Shape {
    // 抽象类中可以包含具体方法实现和成员变量
    double area() {
        System.out.println("Shape.area()");
        return 0.0;
    }
    // 需要子类实现的抽象方法
    abstract void draw();
}

        在这个例子中,Shape 类是一个抽象类,其中包含了一个具体方法 area() 和一个抽象方法 draw()。子类继承 Shape 类后,可以调用 area() 方法,并重写 draw() 方法以实现自己的特定行为。

        当需要定义一组规范,但是不需要为其提供默认实现时,通常应该选择使用接口。例如:

interface Drawable {
    // 接口中只能定义方法签名
    void draw();
}

class Circle implements Drawable {
    @Override
    public void draw() {
        System.out.println("Circle.draw()");
    }
}

class Rectangle implements Drawable {
    @Override
    public void draw() {
        System.out.println("Rectangle.draw()");
    }
}

        在这个例子中,Drawable 接口定义了一个方法签名 draw(),该接口可以用来描述所有可绘制对象的行为。类 CircleRectangle 实现了该接口,并重写了 draw() 方法以实现自己的特定行为。

        需要注意的是,Java 中一个类可以继承一个抽象类,并实现多个接口,这样就可以同时享受抽象类和接口的优势。另外,当一个类需要多种行为时,也可以将多个接口组合起来使用。例如:

abstract class Animal {
    void eat() {
        System.out.println("Animal.eat()");
    }
}

interface Flyable {
    void fly();
}

interface Swimable {
    void swim();
}

class Bird extends Animal implements Flyable {
    @Override
    public void fly() {
        System.out.println("Bird.fly()");
    }
}

class Fish extends Animal implements Swimable {
    @Override
    public void swim() {
        System.out.println("Fish.swim()");
    }
}

        在这个例子中,Animal 类是一个抽象类,FlyableSwimable 接口定义了飞行和游泳的行为,Bird 类继承 Animal 类并实现了 Flyable 接口,Fish 类继承 Animal 类并实现了 Swimable 接口。这样,Bird 类就具备了动物基本行为和飞行能力,Fish 类就具备了动物基本行为和游泳能力。

        抽象类的主要作用是定义类族之间的共性,它可以包含构造方法、普通方法、抽象方法等成员,也可以包含成员变量和静态代码块,因此具有一定的灵活性和复杂度。抽象类一般用于描述一类对象的通用行为和属性,并提供默认实现,而留下一些抽象方法由子类进行具体实现。

        以下是抽象类的一个简单示例:

abstract class Shape {
    String color;

    public Shape(String color) {
        this.color = color;
    }

    // 抽象方法:计算图形的面积
    public abstract double getArea();
}

class Circle extends Shape {
    double radius;

    public Circle(String color, double radius) {
        super(color);
        this.radius = radius;
    }

    @Override
    public double getArea() {
        return Math.PI * radius * radius;
    }
}

class Rectangle extends Shape {
    double width;
    double height;

    public Rectangle(String color, double width, double height) {
        super(color);
        this.width = width;
        this.height = height;
    }

    @Override
    public double getArea() {
        return width * height;
    }
}

public class Main {
    public static void main(String[] args) {
        Shape shape1 = new Circle("Red", 3.0);
        Shape shape2 = new Rectangle("Green", 4.0, 5.0);
        System.out.println(shape1.getArea());
        System.out.println(shape2.getArea());
    }
}

        上述示例中,抽象类 Shape 描述了图形的通用属性和方法,其中定义了一个抽象方法 getArea(),用于计算图形的面积。类 CircleRectangle 继承自抽象类 Shape,并实现了抽象方法 getArea(),这样具体的图形对象就可以通过调用 getArea() 方法来计算自己的面积。

        相比之下,接口更加抽象和规范,它只包含常量、方法签名和默认方法,不包括实现代码,因此具有更高的可读性和可维护性。接口一般用于描述一组行为或功能的规范,让实现类来满足这些规范,以达到一定的解耦和灵活性。

        以下是接口的一个简单示例:

interface Drawable {
    void draw();
}

class Circle implements Drawable {
    double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public void draw() {
        System.out.println("Drawing a circle with radius " + radius);
    }
}

class Rectangle implements Drawable {
    double width;
    double height;

    public Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public void draw() {
        System.out.println("Drawing a rectangle with width " + width + " and height " + height);
    }
}

public class Main {
    public static void main(String[] args) {
        Drawable drawable1 = new Circle(3.0);
        Drawable drawable2 = new Rectangle(4.0, 5.0);
        drawable1.draw();
        drawable2.draw();
    }
}

        上述示例中,接口 Drawable 描述了可以被绘制的对象应该具有的行为规范,其中定义了一个方法 draw()。类 CircleRectangle 实现了接口 Drawable,并重写了方法 draw(),用于具体绘制自己的图形。

        总之,抽象类和接口都有自己的用处和适用场景,需要根据实际需求进行选择和设计。如果描述的是一组相似的对象或操作,使用抽象类更加合适;如果描述的是一组相似的行为或功能,使用接口更加合适。

五、实用的例子

        在 JDK 1.8 及之后,Java 接口除了常量、方法签名和默认方法外,还可以包含静态方法。这样做主要是为了提供更多的灵活性和功能,以下是一些例子:

  1. 工厂方法模式

    接口内的静态方法可以用来实现工厂方法模式。在接口中定义一个静态的工厂方法来创建对象,具体的实现由实现类完成。

    例如:
     

    public interface Product {
        void use();
    
        static Product create() {
            return new ConcreteProduct();
        }
    }
    
    public class ConcreteProduct implements Product {
        @Override
        public void use() {
            System.out.println("Using concrete product.");
        }
    }
    
    public class Main {
        public static void main(String[] args) {
            Product product = Product.create();
            product.use();
        }
    }
    

  2. 集合框架

     Java 集合框架中的 Collections 类提供了许多静态方法来操作集合,这些方法都是通过接口中的静态方法来实现的。

    例如:Collections.sort(List<T> list) 方法实现了对一个列表进行排序,这个方法就是通过接口 List<T> 中的 sort() 方法来调用实现的。

  3. 辅助方法

        接口中的静态方法还可以用来实现一些简单辅助功能,比如检查参数和校验数据等操作。

例如:

public interface StringUtils {
    static boolean isBlank(String str) {
        return str == null || str.trim().isEmpty();
    }

    static boolean isNotBlank(String str) {
        return !isBlank(str);
    }
}

public class Main {
    public static void main(String[] args) {
        String str1 = null;
        String str2 = "";
        String str3 = "  ";
        System.out.println(StringUtils.isBlank(str1));
        System.out.println(StringUtils.isNotBlank(str2));
        System.out.println(StringUtils.isBlank(str3));
    }
}

        上述代码定义了一个 StringUtils 接口,包含了两个静态方法 isBlank()isNotBlank(),用于判断字符串是否为空或只包含空格。这些方法可以在其他类中方便地调用。

        总之,接口中的静态方法可以用来实现工厂方法、集合框架、辅助方法等功能,提供更多的灵活性和功能。但是需要注意,静态方法不会被继承到实现类中,只能通过接口名来访问。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值