coding……
但行好事 莫问前程

Java编程拾遗『接口与抽象类』

上篇文章讲了Java中类和对象的一些基础概念,本篇文章讲述一下,Java中两种特殊的”类”,接口和抽象类。熟悉Java编程的都知道,Java中是不允许多继承的,有人讲Java中可以通过接口实现多继承,但其实这种说法是不对的,Java之父James Gosling在1995年2月发表了一篇名为”Java: an Overview”的Java白皮书,文章解释了Java不支持多重继承的原因。

JAVA omits many rarely used, poorly understood, confusing features of C++ that in our experience bring more grief than benefit. This primarily consists of operator overloading (although it does have method overloading), multiple inheritance, and extensive automatic coercions.

也就是讲,Java为了使用的高效快捷,去除了C++中多继承这一不是很常用,但却容易出错的特性。其实也有些人讲,Java不支持多继承是Java语言源生的缺陷。关于是不是的问题,这里不讨论了,Java之父都这么讲了,我们暂时也不以”批判”的观点来看这个问题了。转而,来看一下Java中这两个特殊的”类”—接口和抽象类。

1. 为什么Java不支持多继承

有两个类B和C继承自A。假设B和C都继承了A的方法并且进行了覆盖,编写了自己的实现。假设D通过多重继承继承了B和C,那么D应该继承B和C的重载方法,那么它应该继承哪个的呢?是B的还是C的呢?如果不两外进行特殊规定(比如C++的虚基类),肯定是无法区分继承哪一个方法,产生二义性,这就是多重继承的菱形继承问题。

James Gosling对Java的定义如下:

Java: 一种简单的,面向对象的,分布式的,解释型的,健壮的,安全的,架构中立的,可移植的,高性能的,支持多线程的,动态语言。

也就是讲,Java为了其简单的特性,放弃了面向对象多重继承这一特点。但其实多重继承在开发中使用的并不是很多(作为一个Java程序员的理解),即使有多重继承的需求,也可以通过替代方案来解决(比如之前讲的内部类),放弃多重继承这一特性,对开发者而言,是一个减负的操作。

2. 接口

相信很多人都有一种概念,Java中接口就是用来实现多继承的。其实这是一种比较常见的误区。Java中的接口其实是表示一种能力或者讲约定一种契约,接口并没有实现这个能力,它只是一个约定,它涉及交互两方对象,一方需要实现这个接口,另一方使用这个接口,但双方对象并不直接互相依赖,它们只是通过接口间接交互。举个简单的例子,USB协议规定了USB设备所需要具有的能力,具体的USB设备都需要实现这些能力,电脑使用USB协议与USB设备交互,电脑和USB设备互不依赖,但可以通过USB接口跟所有的USB设备交互。

Java中接口可以理解为一种高度抽象的类,跟类不同的是,类是对物理对象的抽象,并定义了该类对象相关的一系列属性和行为。而接口更倾向于定义一类对象具备的一些通用能力,并且本身并不去实现这种能力,而将实现交给具体要拥有这种能力的类实现。通过接口进行交互可以更加灵活且耦合性比较小。接口定义的格式如下:

public interface interfaceName {
	[成员变量]
	[成员方法]
}

//比如
public interface MyInterface {
	Integer max_num = 1000;

	Integer compareTo(Object other);
}
  • interface关键字用来声明接口,修饰符一般都是public。
  • 接口中的成员变量都是public static final,声明时必须给赋初值,可以不添加public static final修饰,效果一样。
  • 接口中的成员方法都是public abstract的,所以没有方法实现,在声明接口时,可以不必添加public abstract修饰,效果一样。
  • 接口也可以继承,一个接口可以继承别的接口,接口的继承使用extends关键字,多个父接口之间以逗号分隔。
  • Java8之后,接口中允许实现default方法。

2.1 接口使用示例

2.1.1 接口定义

首先我们模仿java.long.Comparable声明一个自定义接口MyComparable,表示对象具有可比较的能力。

public interface MyComparable {
    int compareTo(Object other);
}

compareTo方法有”两个参数”,其中一个是显式参数other,表示用于比较的另一个对象。另一个是”隐式参数”,表示对象本身。compareTo方法返回值未一个int型数字,1表示本对象比另一个对象大,0表示两个对象相等,-1表示本对象小于另一个对象。任何实现了MyCompareable的类都需要覆盖compareTo方法,用于对象的比较。

2.1.2 接口实现

自定义一个Point类,并实现MyComparable接口,表示对象是可比较的。比较规则未point距离原点的距离。

@Getter
@Setter
@AllArgsConstructor
public class Point implements MyComparable {

    private int x;
    private int y;

    public double distance() {
        return Math.sqrt(x * x + y * y);
    }

    public int compareTo(Object other) {
        if (!(other instanceof Point)) {
            throw new IllegalArgumentException();
        }
        Point otherPoint = (Point) other;
        double delta = distance() - otherPoint.distance();
        if (delta < 0) {
            return -1;
        } else if (delta > 0) {
            return 1;
        } else {
            return 0;
        }
    }

    @Override
    public String toString() {
        return "(" + x + "," + y + ")";
    }

    public static void main(String[] args) {
        MyComparable p1 = new Point(2,3);
        MyComparable p2 = new Point(1,2);
        System.out.println(p1.compareTo(p2));
    }
}

main方法中定义两个MyComparable变量,指向Point对象,之所以能赋值是因为Point实现了MyComparable接口。如果一个类型实现了多个接口,那这种类型的对象就可以被赋值给任一接口类型的变量。p1和p2可以调用MyComparable接口的方法,运行时,执行的是具体实现类的代码(其实就是多态的概念),比较两个Point对象距离远点的大小。运行结果如下:

Connected to the target VM, address: '127.0.0.1:4729', transport: 'socket'
1
Disconnected from the target VM, address: '127.0.0.1:4729', transport: 'socket'

Process finished with exit code 0

说明p1距离原点更远。但是上述代码存在一个问题,我在代码中本来就知道比较的两个对象是Point对象,但是却赋给了MyComparable类型的变量,其实是没有什么意义的,完全可以直接赋给Point类型变量。但假如有些情况下,我们无法预知对象的具体类型时,这时候使用接口多态的这种特性的优势就体现出来了。

public class CompareUtils {

    /**
     * 获取最大值
     */
    public static Object getMax(MyComparable[] comparables) {
        if (comparables == null || comparables.length == 0) {
            return null;
        }
        MyComparable max = comparables[0];
        for (int i = 1; i < comparables.length; i++) {
            if (max.compareTo(comparables[i]) < 0) {
                max = comparables[i];
            }
        }
        return max;
    }

    /**
     * 升序排序
     */
    public static void sort(MyComparable[] comparables) {
        for (int i = 0; i < comparables.length; i++) {
            int min = i;
            for (int j = i + 1; j < comparables.length; j++) {
                if (comparables[j].compareTo(comparables[min]) < 0) {
                    min = j;
                }
            }
            if (min != i) {
                MyComparable temp = comparables[i];
                comparables[i] = comparables[min];
                comparables[min] = temp;
            }
        }
    }

    public static void main(String[] args) {
        Point[] points = new Point[]{
                new Point(2, 3),
                new Point(3, 4),
                new Point(1, 2)
        };
        System.out.println("max: " + CompareUtils.getMax(points));
        CompareUtils.sort(points);
        System.out.println("sort: " + Arrays.toString(points));
    }
}

如上CompareUtils类,我们定义了两个静态方法,分别用来获取一个MyComparable数组的最大值及对数组进行排序。这里注意一下参数类型为MyComparable[],根据多态的特点,方法可以接收任何实现了MyComparable接口类的数组,所以所有实现了MyComparable接口的类都可以使用该类获取最大值、排序。

这就是使用接口的好处,针对接口而非具体类型进行编程,是一种重要编程思维。面向接口编程的优点有很多,首先是代码复用,同一套代码可以处理多种不同类型的对象,只要这些对象都有相同的能力。如CompareUtils可以对所有具有比较能力的对象数组进行排序、获取最大值。其次就是降低了耦合,提高了灵活性,使用接口的代码依赖的是接口本身,而非实现接口的具体类型,程序可以根据情况替换接口的实现,而不影响接口使用者。比如CompareUtils可以接收任意实现了MyComparable接口的类对象数组,实现相同的效果,但并不用修改代码。

2.2 Java8中接口的改动

Java8之前,接口规定只能声明方法,而不能有方法实现。Java8引入了一个重要的新特性—函数式编程,同时允许在接口中实现default method。从Java 8的设计主题来看,default method是为了配合JDK标准库的函数式风格而设计的。通过default method,很多JDK里原有的接口都添加了新的可以接收FunctionalInterface参数的方法,使它们更便于以函数式风格使用。

以java.util.List 接口为例,它在Java 7的时候还没有sort()方法,而到Java 8的时候添加了这个方法。那么如果我以前在Java 7的时候写了个自定义类MyList实现了List<T>接口,当时是不需要实现这个sort()方法的。当升级到JDK8的时候,突然发现接口上多了个方法,于是MyList类就也得实现这个方法并且重新编译才可以继续使用了,所以就有了default method。上述List.sort()方法在Java 8里就是一个default method,它在接口上提供了默认实现,于是MyList即便不提供sort()的实现,也会自动从接口上继承到默认的实现,于是MyList不必重新编译也可以继续在Java 8使用。

接口中可以进行方法实现,看起来好像跟抽象类很像了。那是不是可以放弃抽象类了?答案是否定的。因为接口中定义的成员变量默认都是public static final类型的,所以在声明时就要赋初值。所有子类持有相同的成员变量且无法改变,也就是讲Java8的接口是无状态的,而抽象类中可以定义实例变量也可以定义类变量,是有状态的。另外接口中的default method必须是public的,而抽象类中的方法可以是public、protected、private、default类型的。

由于Java中允许实现多个接口,Java8又允许接口中提供default method实现,那么肯定会带来一个问题—菱形继承问题。如下:

interface InterfaceA {
    default void f() {}
}

interface InterfaceB {
    default void f() {}
}

class InterfaceC implements InterfaceA, InterfaceB {
    
}

为了解决以上的冲突,需要手动重写(override)默认方法,例如:

class InterfaceC implements InterfaceA, InterfaceB {
    public void f() {
        System.out.println("my local f");
    }
}

如果想使用特定接口的默认方法,可以使用如下方式:

class InterfaceC implements InterfaceA, InterfaceB {
    public void f() {
        InterfaceA.super.f();
    }
}

现阶段接口还是无法代替抽象类的,Java 9的接口已经可以有非公有的静态方法了。未来的Java版本的接口可能会有更强的功能,或许能更大程度地替代原本需要使用抽象类的场景。

3. 抽象类

抽象类顾名思义就是抽象的类,相比于具体的类,抽象类并不是对一个物理实体的抽象,它是一个比类更抽象的概念。相比于接口,它又不是完全抽象的,可以在抽象类中对成员方法进行实现,可以讲抽象程度处于接口和类之间。一般而言,具体类有直接对应的对象,而抽象类没有,它表达的是抽象概念,一般是具体类的比较上层的父类。比如Java中List是一个接口,定义了链表的一系列的操作。AbstractList是个抽象类,ArrayList中实现了List接口,并覆盖了List中的多数方法(未覆盖的是abstract抽象方法)。ArrayList继承抽象类AbstractList,根据需求覆盖非抽象方法,实现AbstractList抽象方法。

3.1 抽象类定义

[类定义修饰符] abstract class <类名> {
	[类变量声明]
	[成员变量声明]
	[构造函数]
	[类方法]
	[成员方法]
	[抽象方法]
	[成员变量的get/set方法]
}

上述抽象类除了class的abstract修饰符,其他的都是可选的。对比之前具体类的定义格式可以发现,区别在于多了个abstract修饰符,类中有abstract抽象方法(可以没有),其它的跟普通类的定义一致。对于abstract抽象方法,只有声明,没有实现,方法由实现类来实现。抽象类和具体类最大的区别是,具体类可以实例化对象,抽象类无法实例化对象

3.2 为什么要使用抽象类

对于抽象方法,可以看作是当前类不知道如何来实现该方法,要结合具体的类才能确定并实现其功能,所以定义为抽象方法,保留给实现类实现。换一种思维,暂时不知道如何实现,先定义一个空方法体,好像也能解决这种需求。单独定义一个抽象类,而抽象类又不能创建对象,看上去好像增加了一个不必要的限制。其实,引入抽象方法和抽象类,是Java提供的一种语法工具,对于一些类和方法,引导使用者正确使用它们,减少被误用。使用抽象方法,而非空方法体,子类就知道他必须要实现该方法,而不可能忽略。使用抽象类,类的使用者创建对象的时候,就知道他必须要使用某个具体子类,而不可能误用不完整的父类。

3.3 抽象类的使用

抽象类和接口有很多相似之处,比如都不能用于创建对象,另外接口中的方法其实都是抽象方法。如果抽象类中只定义了抽象方法,那抽象类和接口就更像了。但抽象类和接口根本上是不同的,一个类可以实现多个接口,但只能继承一个类(包括抽象类)抽象类和接口是配合而非替代关系,它们经常一起使用,接口声明能力,抽象类提供默认实现,实现全部或部分方法,一个接口经常有一个对应的抽象类。比如:

  • Collection接口和AbstractCollection抽象类
  • List接口和AbstractList抽象类
  • Map接口和AbstractMap抽象类

对于具体类而言,有两个选择,一个是实现接口,自己实现全部方法,另一个则是继承抽象类,然后根据需要重写方法。继承的好处是复用代码,只重写需要的即可,容易实现。不过,如果这个具体类已经有父类了,那就只能选择实现接口了。下面展示以下这接口、抽象类和具体类的配合使用。

首先定义一个接口,可以向一个数组添加元素或批量添加或者获取对应索引的元素值。

public interface ArrayDemoInterface {
    
    void add(int number);

    void addAll(int[] numbers);
    
    int get(int index);
}

声明一个抽象类,实现ArrayDemoInterface接口,接口中实现addAll方法

public abstract class AbstractAdder implements ArrayDemoInterface {

    public void addAll(int[] numbers) {
        for(int num : numbers){
            add(num);
        }
    }
}

定义一个具体类,继承,在具体类中实现add和get方法

public class Base extends AbstractAdder {
    private static final int MAX_NUM = 1000;
    private int[] arr = new int[MAX_NUM];
    private int count;

    public void add(int number) {
        if (count < MAX_NUM) {
            arr[count++] = number;
        }
    }

    public int get(int index) {
        return arr[index];
    }

    public static void main(String[] args) {
        ArrayDemoInterface arrayDemoInterface = new Base();
        arrayDemoInterface.add(1);

        arrayDemoInterface.addAll(new int[]{4, 5, 6});

        System.out.println(arrayDemoInterface.get(1));
    }
}

对于具体类Base来说,它可以选择直接实现ArrayDemoInterface接口,或者从AbstractAdder类继承。如果选择实现ArrayDemoInterface接口,那么在具体类中要覆盖addAll、add、get方法。如果如果继承,只需要实现add和get方法就可以了。

参考链接:

  1. 《Java编程的逻辑》
  2. Why Multiple Inheritance is Not Supported in Java
  3. Java 8接口有default method后是不是可以放弃抽象类了?

赞(0) 打赏
Zhuoli's Blog » Java编程拾遗『接口与抽象类』
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址