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中出现的新特性,同时转载了一篇虽说文章比较久远但是思想没变,个人认为写的非常好的文章关于抽象类和接口的区别所在,希望带给如我一样的初学者更深层次的思考。

原文地址:https://www.cnblogs.com/CreateMyself/p/11437029.html

时间: 2024-10-14 17:55:13

Java入门系列之类继承、抽象类、接口(五)的相关文章

Java入门(一)——类、抽象类和接口

Java是一门面向对象语言,可以看出"对象"在Java有着举足轻重的位置.那么,"对象"从何而来呢?那必须是丈母娘造出来的,下面我们就先来说说这个丈母娘--类. Java类 对象: 对象具有状态和行为. 例如:一只狗的状态有:颜色,名称,品种,它的行为有:摇尾巴,吠叫,吃东西. 对象是类的实例 类: 类是一个模板,它描述一类具有相同状态和行为的对象.比如人类,都具有思考这个行为,而植物没有. 类可以看成是创建Java对象的模板,下面简单定义一个类: public c

java基础知识总结--继承和接口

什么是继承?什么是接口?他们之间的区别和联系是什么? 什么是继承? 继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能.多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那个类即可.多个类称为子类,单独的那个类称之为父类,超类.或者是称为基类. 什么是接口? 接口在Java编程语言中是一个抽象类型,是抽象方法的集合,接口并不是类,编写接口的方法和类很相似,但是他们属于不同的概念,类描述对

黑马程序员——JAVA学习笔记四(继承、接口、内部类)

1,    通过extends关键字让类与类之间产生继承关系.多个类中存在相同属性和行为时,将这些内容抽取到单独的一个类中,那么多个类无需定义这些属性和行为,只要继承那个类即可,已存在的类叫做超类,基类,或父类.新类称为子类,派生类,孩子类. 子类可以直接访问父类中的非私有的属性和行为.子类无法继承父类中私有的内容.JAVA不支持多继承,只支持单继承,多实现. 继承提高了代码复用性,让类与类之间产生了关系.为多态提供了前提. 2,    super关键字代表父类中成员变量内存空间的标示.两个作用

java实验六(继承、接口、异常相关知识点)

定义一个学生类,定义一个大学生类和小学生类,这两个类是学生类的子类:定义一个选课接口,由大学生类实现该接口,并定义一个年龄异常处理,当小学生 年龄小于7岁时,弹出异常. import java.util.Scanner; interface XuanKe        //选课接口{    void xuanKe(String xuanke);} class AgeException extends Exception{    String message; AgeException(int a

第三章:继承/抽象类/接口

继承 在面向对象编程中,有两种截然不同的继承类型,实现继承和接口继承;C#中不支持多重继承,C#类可以派生自另一个类和任意多的接口 实现继承:表示一个类型派生自一个基类型,它拥有该基类型的所有成员字段和函数,在需要给现有类型添加功能或者许多相关类型共享一组重要的公共功能时.这种类型继承非常有用 接口继承:表示一个类型只继承了函数的签名,没有继承任何的实现代码 实现继承virtual/override /// <summary> /// 基类 /// </summary> class

【Java】第10讲:抽象类.接口

用处:父类的方法的不确定性,不写实现结果的话就要抽象化(abstract),如下下 隐藏要求,继承抽象类时,必须要实现所有的抽象方法,因为作者不知道子类需要什么,只是提供方法 接口 抽象类可以有实现主体,接口不能有 接口变量可以有变量,必须初始化,final,piblic,static

Java入门系列-21-多线程

什么是线程 在操作系统中,一个应用程序的执行实例就是进程,进程有独立的内存空间和系统资源,在任务管理器中可以看到进程. 线程是CPU调度和分派的基本单位,也是进程中执行运算的最小单位,可完成一个独立的顺序控制流程,当然一个进程中可以有多个线程. 多线程:一个进程中同时运行了多个线程,每个线程用来完成不同的工作.多个线程交替占用CPU资源,并非真正的并行执行. 使用多线程能充分利用CPU的资源,简化编程模型,带来良好的用户体验. 一个进程启动后拥有一个主线程,主线程用于产生其他子线程,而且主线程必

Java入门系列-25-NIO(实现非阻塞网络通信)

还记得之前介绍NIO时对比传统IO的一大特点吗?就是NIO是非阻塞式的,这篇文章带大家来看一下非阻塞的网络操作. 补充:以数组的形式使用缓冲区 package testnio; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.ByteBuffer; import java.nio.channels.FileChannel; public class TestBufferArray { pub

Java入门系列:实例讲解ArrayList用法

本文通过实例讲解Java中如何使用ArrayList类. Java.util.ArrayList类是一个动态数组类型,也就是说,ArrayList对象既有数组的特征,也有链表的特征.可以随时从链表中添加或删除一个元素.ArrayList实现了List接口. 大家知道,数组是静态的,数组被初始化之后,数组长度就不能再改变了.ArrayList是可以动态改变大小的.那么,什么时候使用Array(数组),什么时候使用ArrayList?答案是:当我们不知道到底有多少个数据元素的时候,就可使用Array