二者区别
1.接口和抽象类时Java面向对象设计的基础机制
2.接口是行为的抽象,抽象方法的集合,子类可以多重实现,利用接口达到API定义和实现分离目的。不能实例化,不包括任何非常量成员,任何field都隐含public static final意义
同时,非静态方法实现,也就是抽象方法,或静态方法,如Java标准库中的List接口
3.抽象类,用abstract关键字修饰class,只能被单一继承,大多用于抽取相关Java类的共用方法或共同成员变量,然后通过继承方式达到代码复用的目的,其目的主要是代码复用。除了不能实例化,形式和一般Java类并无太大区别,可以有一个或多个
抽象方法,也可无抽象方法。抽象类。Java标准库中,collection框架很多同样部分就别抽取成抽象类,例如java。util.AbstractList.
4.例子:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//...
}
知识扩展
不同角度理解接口后抽象类
-
能否定义出语法基本正确的接口,抽象类或相关继承实现,涉及重载(overload),重写(override)
-
软件设计开发中妥善利用接口和抽象类,知道典型应用场景,掌握继承类库重要接口使用;掌握设计方法,能够在review代码看出明显不利于未来维护的设计。
接口
1.接口实现多重继承功能
java 相比其他语言,不支持多重继承,接口某种意义上实现了多重继承的功能,但又有区别。
java可以通过实现多个接口,因为接口是抽象方法集合,所以是声明性的,但不能通过扩展多个抽象类重用逻辑。
2.优点
特定场景,抽象出具体实现、实例化无关的通用逻辑,或纯调用关系逻辑;此设计避免使用传统抽象类陷入的单继承窘境(如有静态方法组成的工具类java.util.Collections)。
3.缺点
为接口添加任何抽象方法,相应实现类都必须实现新增方法,否则会编译错误;而对于抽象类,其子类只会享受新增方法带来的能力扩展,而不需被强制实现对应方法
4.接口分类
其不仅先于抽象方法集合,其实有不同实践。除了抽象方法集合接口,还有一类没有任何方法接口,叫做Marker Interface,顾名思义,其目的为了声明某些东西,如Cloneable,Serializable等
与Annotation异曲同工,简单直接,对于annotation,因为可以指定参数和值,表达能力更强大,故更多人选择annotation。
java8 新增函数式编程,又增加了functional interface,简答说就是只有一个抽象方法接口,通常建议@functionalInterface注解。lambda表达式本身可以看做一类functional interface,某种程度和面向对象两码事。
**java8 以后接口方法也有实现**,其增加default method支持,主要增加lambda,stream功能。java9后支持private default method,其提供一种二进制兼容的扩展已有接口方法。如java.util.Collection很多方法都适合作为default method实现在基础接口里。
public interface Collection<E> extends Iterable<E> {
/**
* Returns a sequential Stream with this collection as its source
* ...
**/
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
面向对象设计
- 封装
目的是隐藏事物内部实现细节,已提供安全性和简化编程。其提供合理边界,避免外部调用者解除到内部细节。
日常开发,无意暴露细节导致bug太高,如多线程暴露内部状态,导致并发修改问题。从另一角度,封装这种隐藏,提供简化界面,避免太多无意义浪费调用者的经理
- 继承
代码复用基础机制,类似对兔,野兔,流氓兔的归纳总结。
但要注意,继承是一种紧耦合关系,父类代码修改,子类行为也会变动(涉及向上转型),不能过度滥用。
- 多态:允许不同类的对象对统一消息做出响应,给出不同的回应。
允许不同类对象对同一消息作出不同响应。注入重写,重载,向上转型等。
简单来说,重写是父类和子类方法名相同,方法签名(参数个数,类型,顺序)相同的实现
重载则是方法名相同,本质是方法签名不同。另外方法名相同,方法签名,但返回值不同,不是重载,编译会出错。如下面代码
public int doSomething() {
return 0;
}
// 输入参数不同,意味着方法签名不同,重载的体现
public int doSomething(List<String> strs) {
return 0;
}
// return类型不一样,编译不能通过
public short doSomething() {
return 0;
}
面向对象编程原则
- 单一职责
类或对象最好只有单一职责,在设计中如发现某个类承担多种义务,可以考虑进行拆分。
- 开关原则
设计要开发扩展(继承),关闭修改。换句话,程序设计应保证平滑扩展性,避免因为新增同类功能而修改已有实现,这可以少产出回归问题
- 里氏替换
进行继承关系抽象时,凡是可以用父类或者基类的地方,都可以用子类替换
- 接口分离
类和接口设计,如果一个接口定义太多方法,其子类可能面临两难,即部分方法对其有意义,这就破坏程序内聚性。
对该种情况,可以将其拆分成多个接口,将行为解耦,根据需求进行单一或多重实现。维护时,不会对其他接口子类构成影响
- 依赖反转
实体应该依赖于抽象而不是实现。也就是说高层次模块,不应该依赖于底层次模块,而是基于抽象。
OOP原则取舍
- 例外
随着语言发展,很多时候并不完全遵守上述原则,如java中引入本地方法类型推断(类似Scala变量,返回值类型推断),按照里氏替换,可以定义为
List<String> list = new ArrayList<>();
使用var类型(其为其他类型的父类或基类),list汇报推断为ArrayList<String>,可以简化为:
var list = new ArrayList<String>();