Java入门系列之类继承、抽象类、接口

前言

C#和Java关于类、抽象类、接口使用方式基本相似,只是对应关键字使用不同罢了,本节呢,我们只是对照C#和Java中关于这三个概念在具体使用时,看看有哪些不一样的地方。

类继承

C#和Java在定义类方式上是一致的,这点没有什么太多要讲解的,我们直接进入到类继承上,在Java中实现继承通过extends关键字,而在C#中则是以冒号(:)来继承,非常优雅而简洁,Java如下:

class  Animal{}

class  Tiger extends  Animal{}

在C#中如下:

class Animal { }

class Tiger : Animal { }

既然讲解到了继承,必然也就涉及到方法重写了,无论Java还是C#对于重写的概念一致:方法重写意味着在子类中定义一个方法,该子类已经在父类中定义,具有相同的方法签名 - 相同的名称,参数和返回类型。Java中对于重写如下:

class Animal {
    void Run() {
        System.out.println("动物可能会跑");
    }
}

class Tiger extends Animal {
    void Run() {
        System.out.println(this.getClass().getSimpleName() + "会跑");
    }
}
 Tiger tiger = new Tiger();
 tiger.Run();

在C#中我们也可以如上代码进行,但是会有警告,如下:

    class Animal
    {
        public void Run()
        {
            Console.WriteLine("动物可能会跑");
        }
    }
    class Tiger : Animal
    {
        public void Run()
        {
            Console.WriteLine($"{GetType().Name}会跑");
        }
    }

我们通过如上写了之后,我们会发现编译器提示如下警告,为何呢?,不难想象,因为我们子类继承了父类,这就相当于父类的方法就在子类中一样(我们这里说的是相当于,因为如果在一个类中要是有两个一模一样的方法也就是方法名称、签名都一样,肯定就报错了,要区分开重载和重写的概念),所以会进行警告提示。

实际上在C#中的重写是如下这样的,父类方法若根据业务来看后续存在被重写的可能则通过virtual关键字修饰,在子类中重写父类中的方法时通过override关键字修饰

    class Animal
    {
        public virtual void Run()
        {
            Console.WriteLine("动物可能会跑");
        }
    }
    class Tiger : Animal
    {
        public override void Run()
        {
            Console.WriteLine($"{GetType().Name}会跑");
        }
    }

接口 

C#和Java中对于抽象类使用基本无差异,都是可定义抽象方法和非抽象方法,而抽象方法只能在抽象类和接口中,有的人就说了,为何不能在类中定义,这就涉及到学习方法了,因为都是面向对象的语言,所以我们就要以人的思维方式去思考和举例(不要每学一门语言就感觉是全新的概念,很多都是相通的),因为类实例化后就是一个具体的对象,既然是具体的对象,那么在对象中的变量和方法必须是完全实现了的,这么讲想必我们就恍然大悟、豁然开朗了。抽象方法在接口中的定义唯一一点的小区别则是在idea编译器中会提示abstract完全没必要,因为接口就是抽象的类型,而在vs编译中不会进行提示。那么在Java中接口的定义是什么呢?接口是一种抽象类型,包含方法和常量变量的集合, 它是Java中的核心概念之一,用于实现抽象,多态和多重继承。接下来我们定义一个电子产品接口,如下:

interface Electronic {

    //常量
    String LED = "LED";

    //抽象方法
    int getElectricityUse();

    //静态方法
    static boolean isEnergyEfficient(String electronicType) {
        if (electronicType.equals(LED)) {
            return true;
        }
        return false;
    }
    
    //默认方法
    default void printDescription() {
        System.out.println("Electronic Description");
    }
}

接下来我们来通过具体的电子产品来实现上述接口,通过implements关键字来实现接口。

class Computer implements Electronic {
    
    public int getElectricityUse() {
        return 1000;
    }
}

接下来我们进行如下调用:

 System.out.println(Computer.LED);
 Computer computer = new Computer();
 System.out.println(computer.getElectricityUse());

那么问题来了,定义一个接口时,在接口中我们可以定义哪些内容呢?常量变量、抽象方法、静态方法、默认方法。对于静态方法和默认方法是在Java8中才出现的新特性,常量变量必须是以public、static、final修饰,这点好理解,我们来通过类访问新特性出现的静态方法,结果如下访问不到,这是啥情况?

Java8新特性对于接口添加了静态方法,既然是静态方法我们通过实现接口,通过类访问静态方法居然访问不到,那接口中的静态方法还有存在的意义?是不是脑子有坑呢?是不是有点开始怀疑人生了呢?莫慌,我们要学会分析问题:因为类可以实现多个接口,若一个类实现了多个接口,而且多个接口中定义了相同的静态方法, 此时类都将继承多个接口中相同的静态方法,此时会出现编译器不知道要调用哪个接口中的静态方法的问题。所以才出现了我们实现了接口却无法访问接口中的静态方法,这是Java8中对于接口中定义静态方法的限制即:接口中的静态方法不能由实现它的类所继承,只能通过其定义的接口访问静态方法。如下:

 System.out.println(Electronic.isEnergyEfficient("LED"));

那么问题又来了,要是我们如下在实现接口中的类中也定义接口中的静态方法,会不会出现重写的情况呢(为了观察是否重写,将判断条件取非)?

class Computer implements Electronic {

    public int getElectricityUse() {
        return 1000;
    }

    static boolean isEnergyEfficient(String electronicType) {
        if (!electronicType.equals(LED)) {
            return true;
        }
        return false;
    }
}
System.out.println(Computer.isEnergyEfficient("LED"));
System.out.println(Electronic.isEnergyEfficient("LED"));

由上我们知道其接口的实现类具有相同名称的静态方法,并且都不会重写。那么在接口中定义静态方法的意义是什么呢?在我看来:静态方法本属于类级别,在java8中将静态方法扩展到接口,相当于我们可以将接口作为类来使用。那么问题又来了,在java8新特性中出现了静态方法,也出现了默认方法,那么静态方法和默认方法有何区别呢?我们在其接口实现类中添加和接口中默认方法同名的方法,如下:

class Computer implements Electronic {

    public int getElectricityUse() {
        return 1000;
    }

    static boolean isEnergyEfficient(String electronicType) {
        if (!electronicType.equals(LED)) {
            return true;
        }
        return false;
    }

    public void printDescription() {
        System.out.println("Computer Electronic");
    }
}

接下来我们通过类实例和匿名类如下来调用上述方法:

        //实例类
        Electronic computer = new Computer();
        computer.printDescription();

        //匿名类
        Electronic e = new Electronic() {
            @Override
            public int getElectricityUse() {
                return 50;
            }

            @Override
            public void printDescription() {
                System.out.println("Anonymous Electronic Description");
            }
        };
        e.printDescription();

我们看到类实例和匿名类都可重写默认方法,由上我们可得出结论:接口静态方法只能由定义的接口所调用,而接口默认方法可由接口实现类实例或接口匿名类所重写,进一步阐述了在java8中接口被扩展到当做类使用。而接口默认方法的作用则是提供常用功能,一来扩展接口,二来不用破坏接口实现类。接口静态方法的作用则是提供公共帮助方法而无需额外再创建类。在C#接口我们可以定义属性,但是在Java中则不行,同时在C# 8.0上针对接口我们也可以定义默认方法,同时对于默认方法没有任何限制,也就是说我们既可以定义普通方法,也可以定义静态方法,这点是Java不可比拟的,比如如下:

    public interface Electronic
    {
        string Color { get; set; }

        bool isEnergyEfficient(string electronicType)
        {
            return true;
        }
    }

抽象类和接口

abstract class和interface在Java语言中都是用来进行抽象类(本文中的抽象类并非从abstract class翻译而来,它表示的是一个抽象体,而abstract class为Java语言中用于定义抽象类的一种方法,请读者注意区分)定义的,那么什么是抽象类,使用抽象类能为我们带来什么好处呢?《转载:https://www.ibm.com/developerworks/cn/java/l-javainterface-abstract/index.html

在面向对象的概念中,我们知道所有的对象都是通过类来描绘的,但是反过来却不是这样。并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类往往用来表征我们在对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。比如:如果我们进行一个图形编辑软件的开发,就会发现问题领域存在着圆、三角形这样一些具体概念,它们是不同的,但是它们又都属于形状这样一个概念,形状这个概念在问题领域是不存在的,它就是一个抽象概念。正是因为抽象的概念在问题领域没有对应的具体概念,所以用以表征抽象概念的抽象类是不能够实例化的。

在面向对象领域,抽象类主要用来进行类型隐藏。我们可以构造出一个固定的一组行为的抽象描述,但是这组行为却能够有任意个可能的具体实现方式。这个抽象描述就是抽象类,而这一组任意个可能的具体实现则表现为所有可能的派生类。模块可以操作一个抽象体。由于模块依赖于一个固定的抽象体,因此它可以是不允许修改的;同时,通过从这个抽象体派生,也可扩展此模块的行为功能。熟悉OCP的读者一定知道,为了能够实现面向对象设计的一个最核心的原则OCP( Open-Closed Principle),抽象类是其中的关键所在。

从语法定义层面看abstract class和interface

在语法层面,Java语言对于abstract class和interface给出了不同的定义方式,下面以定义一个名为Demo的抽象类为例来说明这种不同。

使用abstract class的方式定义Demo抽象类的方式如下:

 

使用interface的方式定义Demo抽象类的方式如下:

 

 

在abstract class方式中,Demo可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface方式的实现中,Demo只能够有静态的不能被修改的数据成员(也就是必须是static final的,不过在interface中一般不定义数据成员),所有的成员方法都是abstract的。从某种意义上说,interface是一种特殊形式的abstract class。

 

从编程层面看abstract class和interface

从编程的角度来看,abstract class和interface都可以用来实现"design by contract"的思想。但是在具体的使用上面还是有一些区别的。首先,abstract class在Java语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。也许,这是Java语言的设计者在考虑Java对于多重继承的支持方面的一种折中考虑吧。其次,在abstract class的定义中,我们可以赋予方法的默认行为。但是在interface的定义中,方法却不能拥有默认行为,为了绕过这个限制,必须使用委托,但是这会 增加一些复杂性,有时会造成很大的麻烦。

在抽象类中不能定义默认行为还存在另一个比较严重的问题,那就是可能会造成维护上的麻烦。因为如果后来想修改类的界面(一般通过abstract class或者interface来表示)以适应新的情况(比如,添加新的方法或者给已用的方法中添加新的参数)时,就会非常的麻烦,可能要花费很多的时间(对于派生类很多的情况,尤为如此)。但是如果界面是通过abstract class来实现的,那么可能就只需要修改定义在abstract class中的默认行为就可以了。同样,如果不能在抽象类中定义默认行为,就会导致同样的方法实现出现在该抽象类的每一个派生类中,违反了"one rule,one place"原则,造成代码重复,同样不利于以后的维护。因此,在abstract class和interface间进行选择时要非常的小心。

 

从设计理念层面看abstract class和interface

上面主要从语法定义和编程的角度论述了abstract class和interface的区别,这些层面的区别是比较低层次的、非本质的。本小节将从另一个层面:abstract class和interface所反映出的设计理念,来分析一下二者的区别。作者认为,从这个层面进行分析才能理解二者概念的本质所在。

前面已经提到过,abstarct class在Java语言中体现了一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"is a"关系,即父类和派生类在概念本质上应该是相同的(参考文献〔3〕中有关于"is a"关系的大篇幅深入的论述,有兴趣的读者可以参考)。对于interface 来说则不然,并不要求interface的实现者和interface定义在概念本质上是一致的,仅仅是实现了interface定义的契约而已。为了使论述便于理解,下面将通过一个简单的实例进行说明。

考虑这样一个例子,假设在我们的问题领域中有一个关于Door的抽象概念,该Door具有执行两个动作open和close,此时我们可以通过abstract class或者interface来定义一个表示该抽象概念的类型,定义方式分别如下所示:

使用abstract class方式定义Door:

 

使用interface方式定义Door:

 

其他具体的Door类型可以extends使用abstract class方式定义的Door或者implements使用interface方式定义的Door。看起来好像使用abstract class和interface没有大的区别。

如果现在要求Door还要具有报警的功能。我们该如何设计针对该例子的类结构呢(在本例中,主要是为了展示abstract class和interface反映在设计理念上的区别,其他方面无关的问题都做了简化或者忽略)?下面将罗列出可能的解决方案,并从设计理念层面对这些不同的方案进行分析。

解决方案一:

简单的在Door的定义中增加一个alarm方法,如下:

 

那么具有报警功能的AlarmDoor的定义方式如下:

 

这种方法违反了面向对象设计中的一个核心原则ISP(Interface Segregation Priciple),在Door的定义中把Door概念本身固有的行为方法和另外一个概念"报警器"的行为方法混在了一起。这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为"报警器"这个概念的改变(比如:修改alarm方法的参数)而改变,反之依然。

 

解决方案二:

既然open、close和alarm属于两个不同的概念,根据ISP原则应该把它们分别定义在代表这两个概念的抽象类中。定义方式有:这两个概念都使用abstract class方式定义;两个概念都使用interface方式定义;一个概念使用abstract class方式定义,另一个概念使用interface方式定义。

显然,由于Java语言不支持多重继承,所以两个概念都使用abstract class方式定义是不可行的。后面两种方式都是可行的,但是对于它们的选择却反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理。我们一一来分析、说明。如果两个概念都使用interface方式来定义,那么就反映出两个问题:1、我们可能没有理解清楚问题领域,AlarmDoor在概念本质上到底是Door还是报警器?2、如果我们对于问题领域的理解没有问题,比如:我们通过对于问题领域的分析发现AlarmDoor在概念本质上和Door是一致的,那么我们在实现时就没有能够正确的揭示我们的设计意图,因为在这两个概念的定义上(均使用interface方式定义)反映不出上述含义。

如果我们对于问题领域的理解是:AlarmDoor在概念本质上是Door,同时它有具有报警的功能。我们该如何来设计、实现来明确的反映出我们的意思呢?前面已经说过,abstract class在Java语言中表示一种继承关系,而继承关系在本质上是"is a"关系。所以对于Door这个概念,我们应该使用abstarct class方式来定义。另外,AlarmDoor又具有报警功能,说明它又能够完成报警概念中定义的行为,所以报警概念可以通过interface方式定义。如下所示:

 

这种实现方式基本上能够明确的反映出我们对于问题领域的理解,正确的揭示我们的设计意图。其实abstract class表示的是"is a"关系,interface表示的是"like a"关系,大家在选择时可以作为一个依据,当然这是建立在对问题领域的理解上的,比如:如果我们认为AlarmDoor在概念本质上是报警器,同时又具有Door的功能,那么上述的定义方式就要反过来了。abstract class和interface是Java语言中的两种定义抽象类的方式,它们之间有很大的相似性。但是对于它们的选择却又往往反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理,因为它们表现了概念间的不同的关系(虽然都能够实现需求的功能)。这其实也是语言的一种的惯用法。

总结

本节我们详解讲解了类继承、抽象类、接口以及在java8中出现的新特性,同时转载了一篇虽说文章比较久远但是思想没变,个人认为写的非常好的文章关于抽象类和接口的区别所在,希望带给如我一样的初学者更深层次的思考。

posted @ 2019-08-31 11:32  Jeffcky  阅读(328)  评论(0编辑  收藏  举报