Java面试题剖析(基础篇) | 第二篇: 深入理解Java接口、抽象类、抽象方法

抽象是面向对象编程的一大特征,Java关于抽象最常被讨论的是抽象类和接口。本文详细介绍下这两者的异同。

一、抽象类

在介绍抽象类之前,先来了解一下抽象方法。

1.1 抽象方法

抽象方法是一种特殊的方法:它只有声明,而没有具体的实现。抽象方法的声明格式为:

abstract void fun();

抽象方法必须用abstract关键字进行修饰。

1.2 抽象类

抽象类是对事物的抽象。如果一个类含有抽象方法,则称这个类为抽象类。抽象类必须在类前用abstract关键字修饰,如果不加,会报编译错误。如下:

这里要注意,如果一个类前用abstract关键字修饰,但是类里面并没有抽象方法,那它也是一个抽象类,只是失去了设计抽象类的意义,等于白白定义了一个抽象类,却不能用它来做任何事情。

在《JAVA编程思想》一书中,将抽象类定义为“包含抽象方法的类”,但这并不意味着抽象类中只能有抽象方法,它和普通类一样,同样可以拥有成员变量和普通的成员方法。如下图:

抽象类是为了继承而存在的。对于一个父类,如果它的某个方法在父类中实现出来没有任何意义,必须根据子类的实际需求来进行不同的实现,那么就可以将这个方法声明为抽象方法,此时这个类也就成为抽象类了。下面通过具体代码来介绍下抽象类的使用。

1.3 抽象类使用

举个简单的例子,鸟是一个种类,自然界有不同种类的鸟,可能有的鸟是4条腿,有的鸟是2条腿。这时,我们就可以把鸟类设计成一个抽象类,不同种类的鸟直接继承这个抽象类。Bird类(代码A):

package com.moi.test.abstractTest;

public abstract class Bird {
    abstract int getLegs();
}

Bird类中包含一个抽象方法,用于获取鸟类有几条腿。

TwoLegsBird类(代码B):

package com.moi.test.abstractTest;

public class TwoLegsBird extends Bird{

    @Override
    int getLegs() {
        return 2;
    }

}

TwoLegsBird类继承了Bird类,并实现了getLegs方法。

FourLegsBird类(代码C):

package com.moi.test.abstractTest;

public class FourLegsBird extends Bird{

    @Override
    int getLegs() {
        // TODO Auto-generated method stub
        return 4;
    }

}

FourLegsBird类同样继承了Bird类,和TwoLegsBird类区别在于getLegs方法的实现不同。

1.4 抽象类总结

对于Java中的抽象类,需要注意以下3点:

1、抽象方法不能是private。因为如果是private,那么该类在被继承时,它的子类便无法实现该方法。如图:

2、抽象类不能被实例化,但它可以通过它的子类(派生类)产生它的对象。

new对象是最常用的实例化的途径,抽象类也可以new,但是要实现它的抽象方法。如下(代码D):

package com.moi.test.abstractTest;

public class Test{
    public static void main(String[] args) {
        Test test1 = new Test();
        Bird bird = new Bird() {
            @Override
            int getLegs() {
                // TODO Auto-generated method stub
                return 3;
            }
        };
        NormalClass normalClass = new NormalClass();
        InnerClass innerClass = test1.new InnerClass();
        System.out.println("bird:"+bird.getClass());
        System.out.println("normalClass:"+normalClass.getClass());
        System.out.println("innerClass:"+innerClass.getClass());
    }
    class InnerClass {//内部类
        public InnerClass() {

        }
        public void InnerClassMethod() {
            System.out.println("innerClassMethod");
        }
    }
}

上述代码中new了Bird,并实现了它的抽象方法,但并不是实例化了一个Bird对象。这是怎么回事儿呢?为了对比说明,我新建了一个普通类NormalClass,同时在Test类里面创建了一个内部类InnerClass,然后分别输出getClass()方法。NormalClass代码(代码E):

package com.moi.test.abstractTest;

public class NormalClass {

}

输出结果如下:

从图中可以看出,bird和normalClass在getClass()方法上输出不一致,反而和innerClass输出相似,它们都带着一个$符号。我们知道,在Java中编译后的class带$符代表内部类,不同的是InnerClass是普通内部类,Bird是匿名内部类,因为它输出后用数字1表示。所以,normalClass是NormalClass的实例化对象,而bird不是Bird的实例化。

抽象类中含有无具体实现的方法,所以不能被实例化。但它可以通过它的子类(派生类)产生它的对象。如下(代码F):

package com.moi.test.abstractTest;

public class BirdSubClass extends Bird{

    @Override
    int getLegs() {
        // TODO Auto-generated method stub
        return 6;
    }

}

我新建了Bird的子类BirdSubClass,并实现了getLegs方法,然后在Test类中添加如下代码:

Bird bird2 = new BirdSubClass();
System.out.println(bird2.getLegs());

输出结果是6。

所以说,Java抽象类不可以直接借助操作符new被实例化,但它可以通过完善了它的抽象方法定义的子类(派生类),借助操作符new,产生它 (Bird) 的对象。这种案例,被称作"向上转型 Upcasting"。其优点是,可以将多种不同子类对象的引用,储存于单一种类 (抽象类/父类 Bird) 的 引用之中。

3、如果一个类继承了一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为abstract类。

这个在上面的TwoLegsBird和FourLegsBird中都已经体现了,如果子类不实现抽象方法,会报编译错误。如下:

要么实现抽象方法,要么定义为抽象类。

二、接口

接口,英文称作 Interface,在软件工程中,接口泛指供别人调用的方法或者函数。接口是对行为的抽象。在Java中,定一个接口的形式如下:

public interface InterfaceTest {
    
}

接口中可以含有 变量和方法。但是要注意,接口中的变量会被隐式地指定为public static final变量,并且接口中的方法必须都是抽象方法,不能有具体的实现。从这里可以隐约看出接口和抽象类的区别,接口是一种极度抽象的类型,它比抽象类更加“抽象”,并且一般情况下不在接口中定义变量。

下图中定义了一个接口InterfaceTest,如果我们在接口中的方法和变量前面加上private修饰符,会编译报错:

如果我们在接口中创建有实现的方法,同样会编译报错:

如何证明接口中的变量是final的呢?我们创建一个类InterfaceSubClass,实现InterfaceTest接口,当试图改变num值的时候,会出现如下提示:

2.1 接口的使用

一个类实现某个接口需要使用implements关键字,具体格式如下:

public class InterfaceSubClass implements InterfaceTest,InterfaceTest2{

}

可以看到,Java中允许一个类实现多个接口。如果一个非抽象类实现了某个接口,就必须实现该接口中的所有方法。对于遵循某个接口的抽象类,可以不实现该接口中的抽象方法。如下,并没有提示编译错误:

下面用具体的代码示例来介绍接口的使用。

在抽象类的介绍中,我们创建了Bird类,我们知道,鸟是会飞的,飞机也是会飞的,飞是一种行为,所以我们在InterfaceTest接口里面创建 fly 方法。

package com.moi.test.abstractTest;

public interface InterfaceTest {
    public void fly();
}

我们再创建一个类 AirPlain,实现InterfaceTest接口:

package com.moi.test.abstractTest;

public class AirPlain implements InterfaceTest{
    @Override
    public void fly() {
        System.out.println("飞机会飞");
    }
}

然后我们把Bird类也实现InterfaceTest接口:

package com.moi.test.abstractTest;

public abstract class Bird implements InterfaceTest{
    abstract int getLegs();
}

由于Bird类是抽象类,所以它不需要实现 fly 方法,交由它的子类来实现。

TwoLegsBird类需要实现 fly 方法:

package com.moi.test.abstractTest;

public class TwoLegsBird extends Bird{
    @Override
    int getLegs() {
        return 2;
    }

    @Override
    public void fly() {
        System.out.println("2条腿的鸟会飞");
    }
}

FourLegsBird类也是同样的实现:

package com.moi.test.abstractTest;

public class FourLegsBird extends Bird implements InterfaceTest{
    @Override
    public void fly() {
        System.out.println("4条腿的鸟会飞");
    }

    @Override
    int getLegs() {
        return 4;
    }
}

Test类分别创建AirPlain、FourLegsBird、TwoLegsBird的实例化对象,并调用方法进行输出:

package com.moi.test.abstractTest;

public class Test{
    public static void main(String[] args) {
        TwoLegsBird twoLegsBird = new TwoLegsBird();
        System.out.println("腿的个数:"+twoLegsBird.getLegs());
        twoLegsBird.fly();
        System.out.println("------------");
        FourLegsBird fourLegsBird = new FourLegsBird();
        System.out.println("腿的个数:"+fourLegsBird.getLegs());
        fourLegsBird.fly();
        System.out.println("------------");
        AirPlain airPlain = new AirPlain();
        airPlain.fly();
    }
}

输出结果如下:

以上介绍了接口的使用。其实,上述代码是存在问题的,并不是所有的鸟都会飞,例如企鹅是鸟类,但是不会飞,所以我们可以在Bird类的子类中实现InterfaceTest接口,这样只有会飞的鸟自行实现 fly 方法;同时,飞机也会分为多种,飞机都是会飞的,所以可以将AirPlain定义为一个抽象类并实现InterfaceTest接口,它的子类都会实现 fly 方法。代码就不再赘述,读者可自行处理。

三、抽象类和接口的区别

1、语法层面上的区别

  • 抽象类中可以存在非抽象方法,即包含实现体的方法,而接口中只能存在抽象方法;
  • 抽象类中的成员变量可以是各种类型的(public、private、protected),而接口中的成员变量只能是public static final类型,并且是隐式定义,需要变量初始化;
  •  接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法;
  • 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

2、设计层面上的区别

  • 抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象。抽象类是对整个类整体进行抽象,包括属性、行为,但是接口却是对类局部(行为特性)进行抽象。抽象类是需要继承的,接口是需要实现的。继承是一个 "是不是"的关系,而接口实现则是 "有没有"的关系。如果一个类继承了某个抽象类,则子类必定是抽象类的种类,而接口实现则是有没有、具备不具备的关系,比如鸟是否能飞(或者是否具备飞行这个特点),能飞行则可以实现这个接口,不能飞行就不实现这个接口。
  • 抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。什么是模板式设计?举个例子,如果子类A和子类B继承了抽象类C,那么如果需要改动A和B的公共部分,那么只需要改动C就可以了,不需要对A和B进行改动。而辐射式设计,如果类1和类2实现了接口3,那么如果3内新增了某个方法,那么都需要在1和2中进行实现。也就是说对于抽象类,如果需要添加新的方法,可以直接在抽象类中添加具体的实现,子类可以不进行变更;而对于接口则不行,如果接口进行了变更,则所有实现这个接口的类都必须进行相应的改动。

下面针对网上门和警报的例子说明抽象类和接口的使用。

门都有open( )和close( )两个动作,然后有的门具有报警功能,火灾报警器也具有报警的作用,门和火灾报警器并没有任何的关系。我们该怎么设计呢?

首先,门是一个种类,所以肯定是需要将门设计成一个类,门又可以分为好多种,并且它们都可以开和关,所以把门设计成抽象类,里面包含open( )和close( )两个抽象方法:

package com.moi.test.abstractTest;

public abstract class Door {
    abstract void open();
    abstract void close();
}

报警是一个功能,也就是一个行为,并不是所有的门都具有报警功能,所以我们将报警设计为接口,包含alarmVoice()行为:

package com.moi.test.abstractTest;

public interface Alarm {
    void alarmVoice();
}

有的门具有报警的功能,所以有报警功能的门AlarmDoor就需要继承Door并实现Alarm接口:

package com.moi.test.abstractTest;

public class AlarmDoor extends Door implements Alarm{

    @Override
    public void alarmVoice() {
        System.out.println("报警");
    }

    @Override
    void open() {
        System.out.println("开门");
    }

    @Override
    void close() {
        System.out.println("关门");
    }

}

以上介绍了Java中抽象方法、抽象类、接口的相关知识,同时,也是Java面试中常见的知识点。

欢迎在评论区留言,我会尽快回复~

如有任何问题,也可在公众号下留言,工程师们会逐一答疑。公众号二维码:

                                  

猜你喜欢

转载自blog.csdn.net/fanguoddd/article/details/87980422