java作为一门面向对象编程语言,其面向对象关键就是理解其四大特性:封装、抽象、继承、多态。
不过,对于这四大特性,光知道它们的定义是不够的,我们还要知道每个特性存在的意义和目的,以及它们能解决哪些编程问题。
一、封装
首先,我们来看封装特性。封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。
java它具有的访问关键字可以用来控制访问权限,也就是可以实现封装的特性,平时的代码中,我们总是在定义一个类的属性的时候,将类的属性设置为private,即为只有
本类可以访问。同时我们会去暴露一些公共的可以操作这些变量的方法给外部操作,这样做的好处具有灵活性,可理解性,可以灵活选择暴露哪些变量操作和哪些变量不可操作,
并且告诉调用者怎么操作,比较符合面对对象的理念,这才是面向对象的意义所在。
要注意的是封装并不是创建一个类与私有成员变量后然后直接返回所有属性的get set方法,这样其实一点封装都没有,这样完全暴露了所有的成员变量,并且不可理解。
我们只需要暴露需要给设置的属性,哪些属性需要怎么设置,开发人员也会比较容易理解,然后一些关联属性不需要暴露,给其内部关联设定值,防止因为全暴露导致外部修改的不一致。
我之前看的文章一个钱包例子很好理解:
//钱包类
public class Wallet {
private String id; //id
private long createTime; //创建时间
private BigDecimal balance; //余额
private long balanceLastModifiedTime; //上次钱包余额变更时间
// ...省略其他属性...public Wallet() {
this.id = IdGenerator.getInstance().generate();
this.createTime = System.currentTimeMillis();
this.balance = BigDecimal.ZERO;
this.balanceLastModifiedTime = System.currentTimeMillis();
}//提供成员变量获取的方法
public String getId() { return this.id; }
public long getCreateTime() { return this.createTime; }
public BigDecimal getBalance() { return this.balance; }
public long getBalanceLastModifiedTime() { return this.balanceLastModifiedTime; }//钱包加钱的方法
public void increaseBalance(BigDecimal increasedAmount) {
if (increasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
this.balance.add(increasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}//钱包减钱的方法
public void decreaseBalance(BigDecimal decreasedAmount) {
if (decreasedAmount.compareTo(BigDecimal.ZERO) < 0) {
throw new InvalidAmountException("...");
}
if (decreasedAmount.compareTo(this.balance) > 0) {
throw new InsufficientAmountException("...");
}
this.balance.subtract(decreasedAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
}
我们参照封装特性,对钱包的这四个属性的访问方式进行了限制。调用者只允许通过下面这六个方法来访问或者修改钱包里的数据。
之所有这样设计,我们知道,钱包的id和创建时间从创建后就应该不应该修改,而余额从实际角度只有加或者减,没有set方法,而
上次钱包余额变更时间只会根据余额变更而变更,不能手动设置,所以再内部方法处理,这样是不是更显得有头里,好理解呢?
String getId()
long getCreateTime()
BigDecimal getBalance()
long getBalanceLastModifiedTime()
void increaseBalance(BigDecimal increasedAmount)
void decreaseBalance(BigDecimal decreasedAmount)
总结:如果我们对类中属性的访问不做限制,那任何代码都可以访问、修改类中的属性,虽然这样看起来更加灵活,但从另一方面来说,
过度灵活也意味着不可控,属性可以随意被以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性、
可维护性。比如某个同事在不了解业务逻辑的情况下,在某段代码中“偷偷地”重设了 wallet 中的 balanceLastModifiedTime 属性,
这就会导致 balance 和 balanceLastModifiedTime 两个数据不一致。
二、继承
继承最大的一个好处就是代码复用。假如两个类有一些相同的属性和方法,我们就可以将这些相同的部分,抽取到父类中,让两个子类继承父类。
这样,两个子类就可以重用父类中的代码,避免代码重复写多遍。
不过,这一点也并不是继承所独有的,我们也可以通过其他方式来解决这个代码复用的问题,比如利用组合关系而不是继承关系。
继承的概念很好理解,也很容易使用。不过,过度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。
为了了解一个类的功能,我们不仅需要查看这个类的代码,还需要按照继承关系一层一层地往上查看“父类、父类的父类……”的代码。还有,子类和父类高度耦合,
修改父类的代码,会直接影响到子类。所以,继承这个特性也是一个非常有争议的特性。很多人觉得继承是一种反模式。我们应该尽量少用,甚至不用。
所以现在呼吁声多的是,少用继承,多用组合。java的继承只支持单继承,并不支持多继承,在网上看到的理解是:
多重继承有副作用:钻石问题(菱形继承)。
假设类 B 和类 C 继承自类 A,且都重写了类 A 中的同一个方法,而类 D 同时继承了类 B 和类 C,那么此时类 D 会继承 B、C 的方法,那对于 B、C 重写的 A 中的方法,类 D 会继承哪一个呢?
这里就会产生歧义。 考虑到这种二义性问题,Java 不支持多重继承。但是 Java 支持多接口实现,因为接口中的方法,是抽象的(从JDK1.8之后,接口中允许给出一些默认方法的实现,这里不考虑这个),
就算一个类实现了多个接口,且这些接口中存在某个同名方法,但是我们在实现接口的时候,这个同名方法需要由我们这个实现类自己来实现,所以并不会出现二义性的问题。
三、多态
多态是指,子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态一般是为了实现代码可扩展性,复用性,主要实现是接口与实现类的使用
借鉴一段迭代器的例子:
在这段代码中,Iterator 是一个接口类,定义了一个可以遍历集合数据的迭代器。Array 和 LinkedList 都实现了接口类 Iterator。
我们通过传递不同类型的实现类(Array、LinkedList)到 print(Iterator iterator) 函数中,支持动态的调用不同的 next()、hasNext() 实现。
//迭代器接口
public interface Iterator {
boolean hasNext();
String next();
String remove();
}//数组集合的实现
public class Array implements Iterator {
private String[] data;
public boolean hasNext() { ... }
public String next() { ... }
public String remove() { ... }
//...省略其他方法...
}//链表集合的实现
public class LinkedList implements Iterator {
private LinkedListNode head;
public boolean hasNext() { ... }
public String next() { ... }
public String remove() { ... }
//...省略其他方法...
}public class Demo {
private static void print(Iterator iterator) {
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
//不同类型的集合实现自己不同的迭代器,通过多态的形式访问
public static void main(String[] args) {
Iterator arrayIterator = new Array();
print(arrayIterator);
Iterator linkedListIterator = new LinkedList();
print(linkedListIterator);
}
}
为什么说多态具有可扩展性和可复用性?
在那个例子中,我们利用多态的特性,仅用一个 print() 函数就可以实现遍历打印不同类型(Array、LinkedList)集合的数据。当再增加一种要遍历打印的类型的时候,比如 HashMap,
我们只需让 HashMap 实现 Iterator 接口,重新实现自己的 hasNext()、next() 等方法就可以了,完全不需要改动 print() 函数的代码。所以说,多态提高了代码的可扩展性。如果我们不使用多态特性,
我们就无法将不同的集合类型(Array、LinkedList)传递给相同的函数(print(Iterator iterator) 函数)。我们需要针对每种要遍历打印的集合,分别实现不同的 print() 函数,
比如针对 Array,我们要实现 print(Array array) 函数,针对 LinkedList,我们要实现 print(LinkedList linkedList) 函数。而利用多态特性,我们只需要实现一个 print() 函数的打印逻辑,就能应对各种集合数据的打印操作,这显然提高了代码的复用性。
四、抽象
在面向对象编程中,我们常借助编程语言提供的接口类(比如 Java 中的 interface 关键字语法)或者抽象类(比如 Java 中的 abstract 关键字语法)这两种语法机制,来实现抽象这一特性。
1.为什么有时候抽象被排除在四大特性之外?
抽象这个概念是一个非常通用的设计思想,并不单单用在面向对象编程中,也可以用来指导架构设计等。而且这个特性也并不需要编程语言提供特殊的语法机制来支持,
只需要提供“函数”这一非常基础的语法机制,就可以实现抽象特性、所以,它没有很强的“特异性”,有时候并不被看作面向对象编程的特性之一。
2.抽象的意义是什么?
在我的理解上,抽象就是对事物的一个大概描述,是什么东西,我们忽略掉关键的细节,只关注功能点不关注具体的设计思想。
抽象作为一个非常宽泛的设计思想,在代码设计中,起到非常重要的指导作用。很多设计原则都体现了抽象这种设计思想,
比如基于接口而非实现编程、开闭原则(对扩展开放、对修改关闭)、代码解耦(降低代码的耦合性)等。