文章目录
前言一、封装(Encapsulation)1.概念2.作用二、抽象(Abstraction)1.概念2.作用三、继承(Inheritance)1.概念2.作用四、多态(Polymorphism)1.概念2.作用接口vs抽象类总结前言
本文通过实例总结了面向对象编程的四大特性,并对抽象类和接口进行了比较。
一、封装(Encapsulation)
1.概念
封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据
有一个钱包类
public class Wallet {private String id;private long createTime;private BigDecimal balance;// 钱包中的余额private long balanceLastModifiedTime;public Wallet() {this.id = String.valueOf(Snowflake.getDefaultInstance().nextId());this.createTime = System.currentTimeMillis();this.balance = BigDecimal.ZERO;this.balanceLastModifiedTime = System.currentTimeMillis();}public String getId() {return id;}public long getCreateTime() {return createTime;}public BigDecimal getBalance() {return balance;}public long getBalanceLastModifiedTime() {return balanceLastModifiedTime;}public void increaseBalance(BigDecimal increasedAmount) {if (pareTo(BigDecimal.ZERO) < 0) {throw new IllegalArgumentException("InvalidAmountException");}this.balance.add(increasedAmount);this.balanceLastModifiedTime = System.currentTimeMillis();}public void decreaseBalance(BigDecimal decreasedAmount) {if (pareTo(BigDecimal.ZERO) < 0) {throw new IllegalArgumentException("InvalidAmountException");}if (pareTo(this.balance) > 0) {throw new IllegalArgumentException("InsufficientAmountException");}this.balance.subtract(decreasedAmount);this.balanceLastModifiedTime = System.currentTimeMillis();}}
从代码中,我们可以发现,Wallet 类主要有四个属性(也可以叫作成员变量)。
我们参照封装特性,对钱包的这四个属性的访问方式进行了限制。调用者只允许通过下面这六个方法来访问或者修改钱包里的数据。
getId()
getCreateTime()
getBalance()
getBalanceLastModifiedTime()
increaseBalance(BigDecimal increasedAmount)
decreaseBalance(BigDecimal decreasedAmount)
之所以这样设计,是因为从业务的角度来说,id、createTime
在创建钱包的时候就确定好了,之后不应该再被改动,所以,我们并没有在Wallet
类中,暴露id、createTime
这两个属性的任何修改方法,比如set
方法。而且,这两个属性的初始化设置,对于Wallet
类的调用者来说,也应该是透明的,所以,我们在Wallet
类的构造函数内部将其初始化设置好,而不是通过构造函数的参数来外部赋值。
对于钱包余额balance
这个属性,从业务的角度来说,只能增或者减,不会被重新设置。所以,我们在Wallet
类中,只暴露了increaseBalance()
和decreaseBalance()
方法,并没有暴露set
方法。对于balanceLastModifiedTime
这个属性,它完全是跟balance
这个属性的修改操作绑定在一起的。只有在balance
修改的时候,这个属性才会被修改。所以,我们把balanceLastModifiedTime
这个属性的修改操作完全封装在了increaseBalance()
和decreaseBalance()
两个方法中,不对外暴露任何修改这个属性的方法和业务细节。这样也可以保证balance
和balanceLastModifiedTime
两个数据的一致性。
2.作用
如果我们对类中属性的访问不做限制,那任何代码都可以访问、修改类中的属性,虽然这样看起来更加灵活,但从另一方面来说,过度灵活也意味着不可控,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、可维护性。
除此之外,类仅仅通过有限的方法暴露必要的操作,也能提高类的易用性。如果我们把类属性都暴露给类的调用者,调用者想要正确地操作这些属性,就势必要对业务细节有足够的了解。而这对于调用者来说也是一种负担。相反,如果我们将属性封装起来,暴露少许的几个必要的方法给调用者使用,调用者就不需要了解太多背后的业务细节,用错的概率就减少很多
二、抽象(Abstraction)
1.概念
封装主要讲的是如何隐藏信息、保护数据,而抽象讲的是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。
java开发中我们常用interface
和abstract
这两种语法机制实现抽象。
public interface IPictureStorage {void savePicture(Picture picture);Image getPicture(String pictureId);void deletePicture(String pictureId);void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo);}public class PictureStorage implements IPictureStorage {// ...省略其他属性...@Overridepublic void savePicture(Picture picture) {... }@Overridepublic Image getPicture(String pictureId) {... }@Overridepublic void deletePicture(String pictureId) {... }@Overridepublic void modifyMetaInfo(String pictureId, PictureMetaInfo metaInfo) {... }}
在上面的这段代码中,调用者在使用图片存储功能的时候,只需要了解IPictureStorage
这个接口类暴露了哪些方法就可以了,不需要去查看PictureStorage
类里的具体实现逻辑。
实际上PictureStorage
类本身就是一种抽象。因为该类中有函数,函数包裹了具体的实现逻辑。单看函数名也可知道功能,本身就是抽象。
2.作用
很多设计原则都体现了抽象这种设计思想,比如基于接口而非实现编程、开闭原则(对扩展开放、对修改关闭)、代码解耦(降低代码的耦合性)等
我们在定义(或者叫命名)类的方法的时候,也要有抽象思维,不要在方法定义中,暴露太多的实现细节,以保证在某个时间点需要改变方法的实现逻辑的时候,不用去修改其定义。
比如getAliyunPictureUrl()
就不是一个具有抽象思维的命名,因为某一天如果我们不再把图片存储在阿里云上,而是存储在私有云上,那这个命名也要随之被修改。相反,如果我们定义一个比较抽象的函数,比如叫作getPictureUrl()
,那即便内部存储方式修改了,我们也不需要修改命名。
三、继承(Inheritance)
1.概念
继承是用来表示类之间的 is-a 关系,比如猫是一种哺乳动物。Java 使用 extends 关键字来实现继承
2.作用
继承最大的一个好处就是代码复用。假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的代码,避免代码重复写多遍。不过,这一点也并不是继承所独有的,我们也可以通过其他方式来解决这个代码复用的问题,比如利用组合关系而不是继承关系。
四、多态(Polymorphism)
1.概念
多态是指,子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。
public abstract class Animal {public abstract void eat();}public class Cat extends Animal {@Overridepublic void eat() {System.out.println("cat eat fishes");}}public class Dog extends Animal {@Overridepublic void eat() {System.out.println("dog eat bone ");}}class Example{public static void test(Animal animal){animal.eat();}public static void main(String[] args) {Animal a1 = new Cat();test(a1);Animal a2 = new Dog();test(a2);}}
在上面的例子中,我们用到了三个语法机制来实现多态。
编程语言要支持父类对象可以引用子类对象,也就是可以将Cat
传递给Animal
编程语言要支持继承,也就是Cat
继承了Animal
,才能将Cat
传递给Animal
。编程语言要支持子类可以重写(override)父类中的方法,也就是Cat
重写了Animal
中的eat()
方法。
还可以用接口实现多态。
2.作用
多态特性能提高代码的可扩展性和复用性。
上面的类型,如果要增加一种动物类型,那就直接继承Animal
,实现自己的eat
方法,完全不用改动其他地方的代码。多态提高了代码的可扩展性。
除此之外,多态也是很多设计模式、设计原则、编程技巧的代码实现基础,比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等。
接口vs抽象类
// 抽象类public abstract class Logger {private String name;private boolean enabled;private Level minPermittedLevel;public Logger(String name, boolean enabled, Level minPermittedLevel) {this.name = name;this.enabled = enabled;this.minPermittedLevel = minPermittedLevel;}public void log(Level level, String message) {boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());if (!loggable) return;doLog(level, message);}protected abstract void doLog(Level level, String message);}// 抽象类的子类:输出日志到文件public class FileLogger extends Logger {private Writer fileWriter;public FileLogger(String name, boolean enabled,Level minPermittedLevel, String filepath) {super(name, enabled, minPermittedLevel);this.fileWriter = new FileWriter(filepath); }@Overridepublic void doLog(Level level, String mesage) {// 格式化level和message,输出到日志文件fileWriter.write(...);}}// 抽象类的子类: 输出日志到消息中间件(比如kafka)public class MessageQueueLogger extends Logger {private MessageQueueClient msgQueueClient;public MessageQueueLogger(String name, boolean enabled,Level minPermittedLevel, MessageQueueClient msgQueueClient) {super(name, enabled, minPermittedLevel);this.msgQueueClient = msgQueueClient;}@Overrideprotected void doLog(Level level, String mesage) {// 格式化level和message,输出到消息中间件msgQueueClient.send(...);}}
通过上面例子总结抽象类特点:
抽象类不允许被实例化,只能被继承抽象类可以包含属性和方法。方法既可以包含代码实现(比如Logger
中的log()
方法),也可以不包含代码实现(比如Logger
中的doLog()
方法)。不包含代码实现的方法叫作抽象方法。子类继承抽象类,必须实现抽象类中的所有抽象方法。
下面看接口
// 接口public interface Filter {void doFilter(RpcRequest req) throws RpcException;}// 接口实现类:鉴权过滤器public class AuthencationFilter implements Filter {@Overridepublic void doFilter(RpcRequest req) throws RpcException {//...鉴权逻辑..}}// 接口实现类:限流过滤器public class RateLimitFilter implements Filter {@Overridepublic void doFilter(RpcRequest req) throws RpcException {//...限流逻辑...}}// 过滤器使用Demopublic class Application {// filters.add(new AuthencationFilter());// filters.add(new RateLimitFilter());private List<Filter> filters = new ArrayList<>();public void handleRpcRequest(RpcRequest req) {try {for (Filter filter : filters) {filter.doFilter(req);}} catch(RpcException e) {// ...处理过滤结果...}// ...省略其他处理逻辑...}}
AuthencationFilter
和RateLimitFilter
是接口的两个实现类,分别实现了对 RPC 请求鉴权和限流的过滤功能。
jkd8以后,接口中既可以包含成员变量,也可以有默认方法跟静态方法,这样接口跟抽象类在java中的语法特点基本一致。这里主要从抽象类跟接口所要解决的问题出发,看两者的主要区别是什么。
抽象类的作用
抽象类不能实例化,只能被继承。而继承能解决代码复用的问题。所以,抽象类也是为代码复用而生的。但是继承不一定要求是抽象类,普通类也可以达到继承的目的。下面通过改造上面的例子,比较普通类跟抽象类的区别
// 父类:非抽象类,就是普通的类. 删除了log(),doLog(),新增了isLoggable().public class Logger {private String name;private boolean enabled;private Level minPermittedLevel;public Logger(String name, boolean enabled, Level minPermittedLevel) {//...构造函数不变,代码省略...}protected boolean isLoggable() {boolean loggable = enabled && (minPermittedLevel.intValue() <= level.intValue());return loggable;}}// 子类:输出日志到文件public class FileLogger extends Logger {private Writer fileWriter;public FileLogger(String name, boolean enabled,Level minPermittedLevel, String filepath) {//...构造函数不变,代码省略...}public void log(Level level, String mesage) {if (!isLoggable()) return;// 格式化level和message,输出到日志文件fileWriter.write(...);}}// 子类: 输出日志到消息中间件(比如kafka)public class MessageQueueLogger extends Logger {private MessageQueueClient msgQueueClient;public MessageQueueLogger(String name, boolean enabled,Level minPermittedLevel, MessageQueueClient msgQueueClient) {//...构造函数不变,代码省略...}public void log(Level level, String mesage) {if (!isLoggable()) return;// 格式化level和message,输出到消息中间件msgQueueClient.send(...);}}
这个设计思路虽然达到了代码复用的目的,但是无法使用多态特性了。像下面这样编写代码,就会出现编译错误,因为 Logger 中并没有定义 log() 方法。
Logger logger = new FileLogger("access-log", true, Level.WARN, "/users/wangzheng/access.log");logger.log(Level.ERROR, "This is a test log message.");
虽然可以在Logger
父类中,定义一个空的log()
,但是显然没有抽象类来的优雅:
在Logger
中定义一个空的方法,会影响代码的可读性,对一个空log()
方法疑惑当创建一个新的子类继承Logger
父类的时候,我们有可能会忘记重新实现log()
方法Logger
可以被实例化,换句话说,我们可以 new 一个 Logger 出来,并且调用空的log()
方法。这也增加了类被误用的风险。
接口的作用
抽象类更多的是为了代码复用,而接口就更侧重于解耦。接口是对行为的一种抽象,相当于一组协议或者契约,可以联想类比一下 API 接口。调用者只需要关注抽象的接口,不需要了解具体的实现,具体的实现代码对调用者透明。
实际上,接口是一个比抽象类应用更加广泛、更加重要的知识点。比如,我们经常提到的“基于接口而非实现编程”,就是一条几乎天天会用到,并且能极大地提高代码的灵活性、扩展性的设计思想。
小结:如果要表示一种 is-a 的关系,并且是为了解决代码复用问题,我们就用抽象类;如果要表示一种 has-a 关系,并且是为了解决抽象而非代码复用问题,那我们就用接口
总结
四大特性是设计原则及设计模式的基础。