[Java初学]抽象类与面向抽象编程

抽象类

把一个方法声明为abstract,表示它是一个抽象方法,本身没有实现任何方法语句。因为这个抽象方法本身是无法执行的,

Person类也无法被实例化。编译器会告诉我们,无法编译Person类,因为它包含抽象方法。

必须把Person类本身也声明为abstract,才能正确编译它:

abstract class Person {
    public abstract void run();
}
Cannot instantiate the type Income错误

Cannot instantiate the type Income

出现这个错误的原因是:尝试new一个抽象类的对象

例如:

	Income[] incomes = new Income[] {  new Income(7500), new RoyaltyIncome(12000) };

//Income是抽象类 其他两个是继承自Income的具体类

如下可以:

		Income[] incomes = new Income[] {  new SalaryIncome(7500), new RoyaltyIncome(12000) };
//Income是抽象类 其他两个是继承自Income的具体类

根据继承原理,子类能调用抽象类的方法。

面向抽象编程
下面的内容大部分选自他人博客内容,加以简化、总结,原文地址已在参考资料中给出,本文仅供学习交流使
什么是面向抽象编程?

在设计程序时,经常会使用到abstract类,其原因是,abstract类只关心操作,而不关心这些操作具体的实现细节,可以使程序的设计者把主要精力放在程序的设计上,而不必拘泥于细节的实现(将这些细节留给子类的设计者),即避免设计者把大量的时间和精力花费在具体的算法上。例如,在设计地图时,首先考虑地图最重要的轮廓,不必去考虑诸如城市中的街道牌号等细节,细节应当由抽象类的非抽象子类去实现,这些子类可以给出具体的实例,来完成程序功能的具体实现。在设计一个程序时,可以通过在abstract类中声明若干个abstract方法,表明这些方法在整个系统设计中的重要性,方法体的内容细节由它的非abstract子类去完成。

使用多态进行程序设计的核心技术之一是使用上转型对象,即将abstract类声明的对象作为其子类对象的上转型对象,那么这个上转型对象就可以调用子类重写的方法。

所谓面向抽象编程,是指当设计某种重要的类时,不让该类面向具体的类,而是面向抽象类,即所设计类中的重要数据是抽象类声明的对象,而不是具体类的声明的对象。

面向抽象编程的目的是为了应对用户需求的变化,将某个类中经常因需求变化而需要改变的代码从该类中分离出去。面向抽象编程的核心是让类中每种可能的变化对应地交给抽象类的一个子类去负责,从而让该类的设计者不去关心具体实现,避免所设计的类依赖于具体的实现。面向抽象编程使设计的类容易应对用户需求的变化。

举个例子

1.例如,我们已经有了一个Circle类(圆类),该类创建的对象Circle调用getArea()方法可以计算圆的面积。

2、现在要设计一个Pillar类(柱类),该类的对象调用getVolume()方法可以计算柱体的体积。

public class Pillar {
    Circle bottom;    //bottom是用具体类Circle声明的对象
    double height;
    Pillar (Circle bottom,double height){
        this.bottom=bottom;
        this.height=height;
    }
    public double getVolume() {
        return bottom.getArea()*height;
    }
}

上述Pillar类中,bottom是用具体类Circle声明的对象,如果不涉及用户需求的变化,上面Pillar类的设计没有什么不妥,

3、在某个时候,用户希望Pillar类能创建出底是三角形的柱体。显然上述Pillar类无法创建出这样的柱体,即上述设计的Pillar类不能应对用户的这种需求(软件设计面临的最大问题是用户需求的变化)。我们发现,用户需求的柱体的底无论是何种图形,但有一点是相同的,即要求该图形必须有计算面积的行为,因此可以用一个抽象类封装这个行为标准:在抽象类里定义一个抽象方法abstract double getArea(),即用抽象类封装许多子类都必有的行为

4、编写一个抽象类Geometry,该抽象类中定义了一个抽象的getArea()方法。

1 public abstract class Geometry {
2     public abstract double getArea();
3 }

5、Pillar类的设计不再依赖具体类,而是面向Geometry类(抽象类),即Pillar类中的bottom是用抽象类Geometry声明的对象,而不是具体类声明的对象。

public class Pillar {
    Geometry bottom;    //bottom是抽象类Geometry声明的变量
    double height;
    Pillar (Geometry bottom,double height){
        this.bottom=bottom;
        this.height=height;
    }
    public double getVolume() {
        if(bottom==null) {
            System.out.println("没有底,无法计算体积");
            return -1;
        }
        return bottom.getArea()*height;    //bottom可以调用子类重写的getArea方法
    }
}

参考资料:面向抽象编程和面向接口编程

消灭new的两件武器
依赖注入 控制反转——脏活让别人去干

还记得前面卖的关子吗?如果 animal 是类成员变量:

private Animal animal = new Tiger();

这并不是好写法,那么什么是好写法呢?这种情况下,比较简单的是对它进行参数化改造:

void setAnimal(Animal animal) {
 this.animal = animal;
}

然后让客户去调用注入:

Tiger tiger = new Tiger();
obj.setAnimal(tiger);

有了上面的注入代码,private Animal animal = new Tiger();这句话反而变得可以接受了。因为等号右边的 Tiger 仅仅是默认值,默认值当然是具体的。

上面的参数化改造手法,我们可以称为“依赖注入”,其核心思想是:不要调我,我会去调你!依赖注入分为属性注入、构造函数注入和普通函数注入。很明显,上面的例子是属性注入。

依赖注入和标题的“控制反转”还不能完全划等号。确切地说,“依赖注入”是实现“控制反转”的方式之一。

这种干脆把创建对象的任务甩手不干的事情,反而是个好写法,境界高!这样,你不知不觉把自己的代码完全变成了只负责数据流转的框架性代码,具备了通用性。

在通往架构师的道路上,你要培养出一种感觉:要创建一个跨作用域的实体对象(不是值对象)是一件很谨慎的事情(越接触大型项目,你对这点的体会就越深),不要随便创建。最好不要自己创建,让别人去创建,传给你去调用。那么问题来了:都不愿意去创建,谁去创建?这个丢手绢的游戏最终到底要丢给谁呢?

工厂模式——抽象的基础设施

工厂模式–简单工厂模式

我们回到这段Show 代码:

void Show() {
 Animal animal = new Tiger(); // 上面说过,这里的 new 目前是可以接受的
 ...... // 出场前的准备活动
 ShowAnimal(animal);
}

但如果Show 方法里创建动物的需求变得复杂,new 会变得猖狂起来:

void Show(string name) {
 Animal animal;
 if(name == "Tiger")
 animal = new Tiger();
 else if(name == "Lion")
 animal = new Lion();
 ...... // 其他种类
 ShowAnimal(animal);
}

此时将变得不可接受了。对付这么多同质的 new(都是创建Animal),一般会将它们封装进专门生产 animal 的工厂里:

Animal ProvideAnimal(string name) {//简单工厂
 Animal animal;
 if(name == "Tiger")
 animal = new Tiger();
else if(name == "Lion")
 animal = new Lion();
 ...... // 其他种类
}

进而优化了 Show 代码:

void Show(string name) {
 Animal animal = ProvideAnimal(name); // 等号两边都是同级别的抽象,这下彻底舒服了
 ShowAnimal(animal);
}

因此,依赖注入和工厂模式是消灭 new 的两种武器。此外,它们也经常结合使用。

抽象到何种程度?

不是越抽象越好,如果所有类都抽象到根上的Object类,那么将需要不停的下溯转换

需要一个平衡 平衡点就是用户需求

村里的家家户户都要提供一种动物去参加跑步比赛,于是每家都要实现一个ProvideAnimal函数。你家里今年养了一只老虎,老虎属于猫科。三层继承关系如下:

public abstract class Animal {
 public void Run();
}
public class Cat : Animal {
 public int Jump();
}
public class Tiger : Cat {
 public void Hunt(Animal animal);
}

现在有个问题:ProvideAnimal 函数的返回类型定义为什么好呢?Animal、Cat 还是Tiger?这就要看用户需求了。

如果此时是举行跑步比赛,那么只需要你的动物有跑步能力即可,此时返回Animal 类型是最好的:

public Animal ProvideAnimal() {
 return new Tiger();
}

如果要举办跳高比赛,是Cat 层级有的功能,那么返回Cat 类型是最好的:

public Cat ProvideAnimal() {
 return new Tiger();
}

切记,你返回的类型,是客户需求对应的最根上的那个类型节点。这是双赢!

参考资料:计算机基础原来可以如此好懂!——「面向抽象编程」

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值