面向对象的四种特性是优化代码的最基本的方法,也是我们需要掌握的最基本的技能。四种特性分别为封装,抽象,继承,多态。这个部分其实很多时候初级的开发人员都掌握了,所以知识大概的过一遍,加强一点印象。
封装
封装也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类 提供的方式来访问内部信息或者数据。一般需要编程语言进行支持,比如Java中的 private、protected、public 关键字。封装特性存在的意义,一方面是保护数据 不被随意修改,提高代码的可维护性;另一方面是仅暴露有限的必要接口,提高类的易用 性。
package OOP;
import java.math.BigDecimal;
/**
* @time: 2022/10/17
* @author: yuanyongan
* @description: 面向对象的Encapsulation,这里的对象是一个虚拟钱包的例子
*/
public class Wallet {
private String id;
private long createTime;
private BigDecimal balance; // 银行卡余额一般是浮点类型,为了保证精度,需要用BigDecimal类型
private long balanceLastModifiedTime; // 余额最近发生变化的时间
public Wallet(){
}
public String getId() {
return id;
}
public long getCreateTime() {
return createTime;
}
public BigDecimal getBalance() {
return balance;
}
public long getBalanceLastModifiedTime() {
return balanceLastModifiedTime;
}
public void increaseBalance(BigDecimal increaseAmount){
if (increaseAmount.compareTo(BigDecimal.ZERO) > 0){
this.balance.add(increaseAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
}
public void decreaseBalance(BigDecimal decreaseAmount){
if (decreaseAmount.compareTo(BigDecimal.ZERO) > 0 && decreaseAmount.compareTo(this.balance) > 0){
this.balance.subtract(decreaseAmount);
this.balanceLastModifiedTime = System.currentTimeMillis();
}
}
}
我们参照封装特性,对钱包的这四个属性的访问方式进行了限制。调用者只允许通过六个public方法来访问或者修改钱包里的数据。这就跟我们实际的应用场景有很大关系了,比如我们的id和createTime 在创建钱包的时候就确定好 了,之后不应该再被改动,所以,我们并没有在 Wallet 类中,暴露 id、createTime 这两 个属性的任何修改方法,比如 set 方法。
并且对于余额balance这个属性,从业务角度来说,也只有增加和减少,并不会被重新设置。所以也不需要set方法暴露出去,如果暴露出去了,被调用,那么就会造成验证的后果。并且我们可以看到,对于balanceLastModifiedTime 属性,只有在balance发生变化的方法中才会进行修改,这也是我们封装的特点。
所以对于特定的业务场景,合理的使用封装的特性,能够让我们的代码更加安全实用。
抽象
抽象及其前面讲到的封装都是人类处理复杂性的有效 手段。在面对复杂系统的时候,人脑能承受的信息复杂程度是有限的,所以我们必须忽略掉 一些非关键性的实现细节。而抽象作为一种只关注功能点不关注实现的设计思路,正好帮我 们的大脑过滤掉许多非必要的信息。
在很多有共同特征的类创建的时候,统一通过抽象的方法将他们的共同点抽象出来,可以节省我们很多工作量以及脑力去记住他们的共同点,并且不易出错。
package OOP;
/**
* @time: 2022/10/17
* @author: yuanyongan
* @description: 面向对象的抽象Abstraction,这里的对象是一个动物的例子
*/
interface Animal{
void eat(String food);
int getAge();
}
public class Dog implements Animal{
@Override
public void eat(String food) {
}
@Override
public int getAge() {
return 0;
}
}
class Cat implements Animal{
@Override
public void eat(String food) {
}
@Override
public int getAge() {
return 0;
}
}
就比如上面的代码,Animal接口是我们抽象出来的动物的共同特征,这样我们在编写其他类实现的时候,就不必要去向太多必备的方法了,只要实现了Animal接口就能解决这个问题。然后我们只需要根据当前类和接口的不同,去增加一些其他属性即可。
继承和多态
继承大的一个好处就是代码复用。假如两个类有一些相同的属性和方法,我们就可以将这 些相同的部分,抽取到父类中,让两个子类继承父类。这样,两个子类就可以重用父类中的 代码,避免代码重复写多遍。
但是过度的使用继承关系其实也是不好的,如果继承关系过多,我们查看一个方法可能要经过多层父类查找才能找到,这从另外一方面其实也加大了开发的工作量。
所以,继承这个特性也是一个非常有争议的特性。很多人觉得继承是一种反模式。我们应该 尽量少用,甚至不用。关于这个问题,在后面讲到“多用组合少用继承”这种设计思想的时 候,我会非常详细地再讲解,这里暂时就不展开讲解了。
在继承和抽象的基础上,我们可以更好的实现多态,因为我们可以在子类上对父类的方法进行重写这样实现相同命名的方法在不同的子类中实现不同的功能。
package OOP;
/**
* @time: 2022/10/17
* @author: yuanyongan
* @description: 面向对象的多态性Polymorphism和继承性Inheritance,这里的对象是一个动态数组的例子
*/
public class DynamicArray {
private static final int DEFAULT_CAPACITY = 10;
protected int size = 0;
protected int capacity = DEFAULT_CAPACITY;
protected Integer[] elements = new Integer[DEFAULT_CAPACITY];
public int size() {
return this.size;
}
public void add(Integer e){
ensureCapacity();
elements[size++] = e;
}
protected void ensureCapacity() {
// ...如果数组满了就扩容,代码省略...
}
}
class SortedDynamicArray extends DynamicArray{
@Override
public void add(Integer e){
ensureCapacity();
for(int i = size - 1; i >= 0; --i){
if (elements[i] > e){
elements[i + 1] = elements[i];
}else {
elements[i + 1] = e;
++size;
break;
}
}
}
}
在上述的例子中,我们就可以看到SortedDynamicArray 继承了DynamicArray,那么他们共有的ensureCapacity在子类中我们就不用再次实现,这样节省了很大的工作量。而当我们要实现不一样的add方法时,在子类中直接对继承的方法add进行重写就可以了,当我们通过SortedDynamicArray 调用sort方法时,就能得到排序后的结果。
getter和setter的使用误区
我们在实际业务中,也会有很多问题要进行处理,这些其实早就在面向对象的四大特性中提到了,我们要针对场景进行灵活的处理。这里我们也有几个常见的场景需以及对应的处理方法。
现在我们开发的时候经常为了方便,随意的使用IDEA的generate和Lombok插件自动生成get和set方法,但是对于一些特殊的场景,这样是会带来很多问题的。比如购物车场景,代码如下:
package OOP;
import java.util.ArrayList;
import java.util.List;
/**
* @time: 2022/10/17
* @author: yuanyongan
* @description: 该类是阐述get和set中的一些特殊场景以及对应的解决方法
*/
class ShoppingCartItem{
// 购物车每个商品的类,这里只是为了代码方便,代码省略...
}
public class ShoppingCart {
private int itemCount; // 购物车中的商品数目
private double totalPrice; // 总价钱
private List<ShoppingCartItem> items = new ArrayList<>(); // 商品明细
public int getItemCount(){return this.itemCount;}
public void setItemCount(int itemCount){
this.itemCount = itemCount;
}
public double getTotalPrice(){
return this.totalPrice;
}
public void setTotalPrice(double totalPrice){
this.totalPrice = totalPrice;
}
public List<ShoppingCartItem> getItems() {
return items;
}
public void setItems(List<ShoppingCartItem> items) {
this.items = items;
}
}
首先我们这个购物车的三个属性,itemCount,totalPrice,items 他们虽然是三个属性,但其实我们可以发现,他们三个之间其实是有十分紧密的联系的,itemCount其实是items的大小,totalPrice是每个item价钱的总和。那么如果我们这里打开了三个属性的set方法就会造成一个很大的问题,当我们改变了其中一个属性,那么这三个属性之间的关系就会被打破,那么我们得到的结果其实就不正确了。
其次,我们发现对items开放了getter方法,一个getter方法好像只是返回结果,无伤大雅。但是我们根据Java的特性,这里对items返回的结果是List,那么其实我们在调用getItems方法得到list后,是可以对对象里面的items进行修改的,因为Java都是引用,返回的是同一个对象。就比如下面代码。
ShoppingCart cart = new ShoppingCart();
...
cart.getItems().clear(); // 清空购物车,虽然逻辑是对的,但这里的另外两个属性没有改变,会导致三个属性的结果不一致。
// 正确返回购物车所有item,返回一个不会修改原list的结果,当然虽然这个list不能被修改,但是每个ShoppingCartItem元素还是会被修改。
public List<ShoppingCartItem> getItems(){
return Collections.unmodifiableList(this.items);
}
四大特性在基本的使用上比较简单,但是我们的设计模式其实都时基于这四大特性来实现的,所以熟练的掌握基本的四大特性时对设计模式深入学习的基础。
学习代码在gitlab仓库中,欢迎大家交流学习