接口和抽象类

接口

抽象类

为什么需求

很多时候,我们实际上关心的,并不是对象的类型,而是对象的能力,只要能提供这个能力,类型并不重要。比如要拍照,很多时候,只要能拍出符合需求的照片就行,至于是用手机拍,还是用Pad拍,或者是用单反相机拍,并不重要,即关心的是对象是否有拍出照片的能力,而并不关心对象到底是什么类型,手机、Pad或单反相机都可以。

这种情况下,类型并不重要,重要的是能力。那如何表示能力呢?接口。

接口这个概念在生活中并不陌生,电子世界中一个常见的接口就是USB接口。计算机往往有多个USB接口,可以插各种USB设备,如键盘、鼠标、U盘、摄像头、手机等。接口声明了一组能力,但它自己并没有实现这个能力,它只是一个约定。接口涉及交互两方对象,一方需要实现这个接口,另一方使用这个接口,但双方对象并不直接互相依赖,它们只是通过接口间接交互。

针对接口而非具体类型进行编程,是计算机程序的一种重要思维方式。

接口很多时候反映了对象以及对对象操作的本质。

接口的好处:

  • 代码复用,同一套代码可以处理多种不同类型的对象,只要这些对象都有相同的能力。
  • 降低了耦合,提高了灵活性。使用接口的代码依赖的是接口本身,而非实现接口的具体类型,程序可以根据情况替换接口的实现,而不影响接口使用者。解决复杂问题的关键是分而治之,将复杂的大问题分解为小问题,但小问题之间不可能一点关系没有,分解的核心就是要降低耦合,提高灵活性,接口为恰当分解提供了有力的工具。

抽象方法和抽象类看上去是多余的,对于抽象方法,不知道如何实现,定义一个空方法体不就行了吗?而抽象类不让创建对象,看上去只是增加了一个不必要的限制。

引入抽象方法和抽象类,是Java提供的一种语法工具,对于一些类和方法,引导使用者正确使用它们,减少误用。使用抽象方法而非空方法体,子类就知道它必须要实现该方法,而不可能忽略,若忽略Java编译器会提示错误。使用抽象类,类的使用者创建对象的时候,就知道必须要使用某个具体子类,而不可能误用不完整的父类。

无论是编写程序,还是平时做其他事情,每个人都可能会犯错,减少错误不能只依赖人的优秀素质,还需要一些机制,使得一个普通人都容易把事情做对,而难以把事情做错。

抽象类就是Java提供的这样一种机制,使程序更为清晰,可以减少误用。

简介

Java中使用接口的一个典型例子就是“比较”,很多对象都可以比较,对于求最大值、求最小值、排序的程序而言,它们其实并不关心对象的类型是什么,只要对象可以比较就可以了,或者说,它们关心的是对象有没有可比较的能力。Java API中提供了Comparable接口,以表示可比较的能力。

public interface Comparable<T> {

    public int compareTo(T o);

}

定义接口:

  • Java使用interface这个关键字来声明接口,修饰符一般都是public。
  • interface后面就是接口的名字。
  • 接口定义里面,声明了方法,但没有定义方法体
  • 接口方法不需要加修饰符,加与不加相当于都是public abstract。方法的参数是一个相同类型的变量other,表示另一个参与比较的对象。第一个参与比较的对象是自己。返回结果是int类型,-1表示自己小于参数对象,0表示相同,1表示大于参数对象。

实现接口:

  • Java使用implements这个关键字表示实现接口,前面是类名,后面是接口名。
  • 实现接口必须要实现接口中声明的方法。
  • 一个类可以实现多个接口,表明类的对象具备多种能力,各个接口之间以逗号分隔

使用接口:

  • 接口不能new,不能直接创建一个接口对象,对象只能通过类来创建。
  • 可以声明接口类型的变量,引用实现了接口的类对象。
  • 如果一个类型实现了多个接口,那么这种类型的对象就可以被赋值给任一接口类型的变量。

抽象类就是抽象的类。抽象是相对于具体而言的,一般而言,具体类有直接对应的对象,而抽象类没有,它表达的是抽象概念,一般是具体类的比较上层的父类。比如,狗是具体对象,而动物则是抽象概念;樱桃是具体对象,而水果则是抽象概念;正方形是具体对象,而图形则是抽象概念。

只有子类才知道如何实现的方法,一般被定义为抽象方法。抽象方法是相对于具体方法而言的,具体方法有实现代码,而抽象方法只有声明,没有实现。接口中的方法(非Java 8引入的静态和默认方法)就都是抽象方法。

抽象方法和抽象类都使用abstract这个关键字来声明:

public abstract class Shape {

//其他代码

public abstract void draw();

}

一些细节

接口中可以定义变量:

public interface Interface1 {

    public static final int a = 0;

}

这里定义了一个变量int a,修饰符是public static final,但这个修饰符是可选的,即使不写,也是public static final。这个变量可以通过“接口名.变量名”的方式使用,如Interface1.a。

接口也可以继承,一个接口可以继承其他接口,继承的基本概念与类一样,但与类不同的是,接口可以有多个父接口:

public interface IBase1 {

    void method1();

}

public interface IBase2 {

     void method2();

}

public interface IChild extends IBase1, IBase2 {

}

IChild有IBase1和IBase2两个父类,接口的继承同样使用extends关键字,多个父接口之间以逗号分隔。

类的继承与接口可以共存,换句话说,类可以在继承基类的情况下,同时实现一个或多个接口:

public class Child extends Base implements IChild {

}

关键字extends要放在implements之前。

接口也可以使用instanceof关键字,用来判断一个对象是否实现了某接口。

静态方法和默认方法:

  • 在Java 8之前,接口中的方法都是抽象方法,都没有实现体,Java 8允许在接口中定义两类新方法:静态方法和默认方法,它们有实现体,如下

public interface IDemo {

    void hello();

    public static void test() {

        System.out.println("hello");

    }

    default void hi() {

        System.out.println("hi");

     }

}

  • 在接口不能定义静态方法之前,相关的静态方法往往定义在单独的类中,比如,Java API中,Collection接口有一个对应的单独的类Collections。
  • 默认方法有默认的实现,实现类可以改变它的实现。
  • 引入默认方法主要是函数式数据处理的需求,是为了便于给接口增加功能。在没有默认方法之前,Java是很难给接口增加功能的,比如List接口​,因为有太多非Java JDK控制的代码实现了该接口,如果给接口增加一个方法,则那些接口的实现就无法在新版Java上运行,必须改写代码,实现新的方法,这显然是无法接受的。函数式数据处理需要给一些接口增加一些新的方法,所以就有了默认方法的概念,接口增加了新方法,而接口现有的实现类也不需要必须实现。
  • 在Java 8中,静态方法和默认方法都必须是public的,Java 9去除了这个限制,它们都可以是private的,引入private方法主要是为了方便多个静态或默认方法复用代码。
  • 定义了抽象方法的类必须被声明为抽象类,不过,抽象类可以没有抽象方法。
  • 抽象类和具体类一样,可以定义具体方法、实例变量等,它和具体类的核心区别是,抽象类不能创建对象,要创建对象,必须使用它的具体子类。
  • 一个类在继承抽象类后,必须实现抽象类中定义的所有抽象方法,除非它自己也声明为抽象类。
  • 与接口类似,抽象类虽然不能使用new,但可以声明抽象类的变量,引用抽象类具体子类的对象。

对比和关系

相同之处:

  • 都不能用于创建对象

不同之处:

  • 接口中不能定义实例变量,而抽象类可以
  • 一个类可以实现多个接口,但只能继承一个类

抽象类和接口是配合而非替代关系,它们经常一起使用,接口声明能力,抽象类提供默认实现,实现全部或部分方法,一个接口经常有一个对应的抽象类。比如,在Java类库中:

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

对于需要实现接口的具体类而言,有两个选择:

  • 一个是实现接口,自己实现全部方法;
  • 另一个则是继承抽象类,然后根据需要重写方法。

继承的好处是复用代码,只重写需要的部分即可,需要编写的代码比较少,容易实现。不过,如果这个具体类已经有父类了,那就只能选择实现接口了。

抽象类和接口经常相互配合,接口定义能力,而抽象类提供默认实现,方便子类实现接口。

继承是把双刃剑

一方面继承非常强大,至少有两个好处:

  • 复用代码;
  • 利用多态和动态绑定统一处理多种不同子类的对象。

封装就是隐藏实现细节,提供简化接口。使用者只需要关注怎么用,而不需要关注内部是怎么实现的。实现细节可以随时修改,而不影响使用者。函数是封装,类也是封装。通过封装,才能在更高的层次上考虑和解决问题。可以说,封装是程序设计的第一原则,没有封装,代码之间会到处存在着实现细节的依赖,则构建和维护复杂的程序是难以想象的。

另一方面继承的破坏力也很强:

  • 继承的破坏力体现在不恰当的继承会破坏封装,主要是因为子类和父类之间可能存在着实现细节的依赖。子类在继承父类的时候,往往不得不关注父类的实现细节,而父类在修改其内部实现的时候,如果不考虑子类,也往往会影响到子类。可以看这个问题来帮助理解这个问题。
  • 继承关系是设计用来反映is-a关系的,但很难完全保证这点:
  1. 现实中,设计完全符合is-a关系的继承关系是困难的。比如,绝大部分鸟都会飞,可能就想给鸟类增加一个方法fly()表示飞,但有一些鸟就不会飞,比如企鹅。
  2. Java没有办法约束这种关系,父类有的属性和行为,子类并不一定都适用,子类还可以重写方法,实现与父类预期完全不一样的行为。

对于通过父类引用操作子类对象的程序而言,它是把对象当作父类对象来看待的,期望对象符合父类中声明的属性和行为。如果不符合,结果将会非常混乱。

如何应对继承的双面性呢?

正确使用继承

使用继承大概主要有三种场景:

第一种:基类是别人写的,我们写子类

基类主要是Java API、其他框架或类库中的类,在这种情况下,我们主要通过扩展基类,实现自定义行为,这种情况下需要注意的是:

  • 重写方法不要改变预期的行为;
  • 阅读文档说明,理解可重写方法的实现机制,尤其是方法之间的依赖关系;
  • 在基类修改的情况下,阅读其修改说明,相应修改子类。

第二种:我们写基类,别人可能写子类

这种情况需要注意:

  • 使用继承反映真正的is-a关系,只将真正公共的部分放到基类;
  • 对不希望被重写的公开方法添加final修饰符;
  • 写文档,说明可重写方法的实现机制,为子类提供指导,告诉子类应该如何重写;
  • 在基类修改可能影响子类时,写修改说明。

第三种:基类、子类都是我们写的

这种情况需要同时注意情况一和二中的事项。

避免使用继承

三种方法可以避免使用继承:

第一种:使用final关键字

  • final方法,给方法加final修饰符,父类就保留了随意修改这个方法内部实现的自由,使用这个方法的程序也可以确保其行为是符合父类声明的。
  • final类,给类加final修饰符,父类就保留了随意修改这个类实现的自由,使用者也可以放心地使用它,而不用担心一个父类引用的变量,实际指向的却是一个完全不符合预期行为的子类对象。

第二种:优先使用组合而非继承

使用组合可以抵挡父类变化对子类的影响,从而保护子类,应该优先使用组合。这样,子类就不需要关注基类是如何实现的了,基类修改实现细节,增加公开方法,也不会影响到子类了。但组合的问题是,子类对象不能当作基类对象来统一处理了。解决方法是使用接口。

public class Child extends Base {

//...

}

改为

public class Child {

private Base base;

//...

}

第三种:使用组合和接口结合起来替代继承

  • 使用组合替代继承,可以复用代码,但不能统一处理。
  • 使用接口替代继承,针对接口编程,可以实现统一处理不同类型的对象,但接口没有代码实现,无法复用代码。

将组合和接口结合起来替代继承,既可以统一处理,又可以复用代码,还不用担心破坏封装。

public class Base implements IAdd {

//...

}

public class Child implements IAdd {

private Base base;

//...

}

#java原理#
Java知识专辑 文章被收录于专栏

知其然知其所以然,只有掌握了底层原理,借助第一性原理,才可以在日常开发和项目中运用自如,潇洒走江湖。

全部评论

相关推荐

●&nbsp;项目中使用Redis做限流,具体是怎么防止刷单的?●&nbsp;如何限制“一人一单”?(追问:如果用户ID不是自增的,怎么限制?-&gt;&nbsp;引出用&nbsp;Set)●&nbsp;Redis&nbsp;的&nbsp;Set&nbsp;底层是怎么存储/实现的?●&nbsp;跳表(SkipList)底层是怎么实现的?●&nbsp;这个项目为什么选择用&nbsp;Redis&nbsp;来实现?●&nbsp;场景题:如果把所有用户的下单ID存入一个优惠券的&nbsp;Set&nbsp;中,会导致“大Key”问题。大Key会给系统性能带来什么影响?如何解决或规避大Key问题?●&nbsp;SQL&nbsp;手撕:有一张成绩表(包含字段:学生姓名、课程名、分数),请查出所有选修课程分数都大于80分的学生姓名。●&nbsp;你是怎么理解数据库的事务的?●&nbsp;事务的“一致性”(Consistency)是怎么保证的?●&nbsp;事务的“隔离性”(Isolation)是怎么保证的?(引出&nbsp;MVCC)●&nbsp;MVCC(多版本并发控制)底层是怎么实现的?●&nbsp;多线程下,控制资源并发访问(并发控制)有什么具体的方式或方法?●&nbsp;Java&nbsp;中有哪些具体的锁?●&nbsp;ReentrantLock&nbsp;是怎么实现的?●&nbsp;AQS(AbstractQueuedSynchronizer)底层是怎么实现的?●&nbsp;多线程下,线程之间的通信方式有哪些?●&nbsp;为什么会有垃圾回收器(GC)?在&nbsp;JVM&nbsp;中,什么是“垃圾”?●&nbsp;TCP&nbsp;的可靠性是怎么保证的?●&nbsp;具体讲讲&nbsp;TCP&nbsp;的流量控制和拥塞控制是什么?●&nbsp;了解&nbsp;HTTPS&nbsp;协议吗?它的交互通信过程是什么样的?●&nbsp;你了解哪些对称加密和非对称加密的具体算法?●&nbsp;设计模式了解过吗?责任链模式是什么样的?●&nbsp;你认为我们在工程设计中,为什么要使用这些设计模式(主要解决什么痛点)?●&nbsp;手撕:实现一个栈,包含最基本的&nbsp;push、pop、top&nbsp;方法,要求这三个方法的时间复杂度都是&nbsp;O(1);同时包含一个获取当前栈中最大元素的方法&nbsp;getMax,时间复杂度也严格要求为&nbsp;O(1)。
查看24道真题和解析
点赞 评论 收藏
分享
03-17 19:33
已编辑
门头沟学院 Java
鳕鱼堡ouo:别去。。。除了你的+2和hr其他人都不知道你的工资。也就是说你拿着最低的工资干着和别人一样的活承受着和别人一样的压力,同事半夜拉会也一样会拉你,辛苦和钱多至少得占一样吧,劝退价的话真没必要了
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务