面向对象编程的四大特性
文章目录
前言
本文通过实例总结了面向对象编程的四大特性,并对抽象类和接口进行了比较。
一、封装(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 (increasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("InvalidAmountException");
}
this.balance.add(increasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
public void decreaseBalance(BigDecimal decreasedAmount) {
if (decreasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("InvalidAmountException");
}
if (decreasedAmount.compareTo(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 {
// ...省略其他属性...
@Override
public void savePicture(Picture picture) { ... }
@Override
public Image getPicture(String pictureId) { ... }
@Override
public void deletePicture(String pictureId) { ... }
@Override
public 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 {
@Override
public void eat() {
System.out.println("cat eat fishes");
}
}
public class Dog extends Animal {
@Override
public 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);
}
@Override
public 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;
}
@Override
protected 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 {
@Override
public void doFilter(RpcRequest req) throws RpcException {
//...鉴权逻辑..
}
}
// 接口实现类:限流过滤器
public class RateLimitFilter implements Filter {
@Override
public void doFilter(RpcRequest req) throws RpcException {
//...限流逻辑...
}
}
// 过滤器使用Demo
public 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 关系,并且是为了解决抽象而非代码复用问题,那我们就用接口
总结
四大特性是设计原则及设计模式的基础。
本文地址:https://blog.csdn.net/weixin_42612223/article/details/113967255
上一篇: 对控制反转和依赖注入的突然顿悟
下一篇: day03