面向对象再探究

31次阅读

共计 6851 个字符,预计需要花费 18 分钟才能阅读完成。

在还不清楚怎样面向对象?一文中,已经简单介绍了面向对象的基本思想和三大特性,但是不够详细。本文再来具体探究一下 面向对象

1. 概述

1.1. 面向过程编程

面向过程编程 (Procedure Oriented Programming,POP) 是一种以过程为中心的编程思想,开发人员在解决问题时更专注于 过程

当我们遇到一个问题时,只需要分析出第 1 步要做什么、第 2 步要做什么……直到解决问题,然后把这些步骤一步步地实现即可。

比如,我现在要编写一个面向过程的程序来模拟「我要让我的好朋友行小观去帮我买瓶水」这个问题,如下:

  1. 给行小观 10 块钱
  2. 告诉行小观去哪买水
  3. 行小观找到我要的那瓶水
  4. 付钱,找零钱
  5. 把水带回来给我

在从「给行小观钱」到「行小观把水给我」的整个过程,我和行小观都专注于完成每一个步骤 (事件),行小观只是一个执行我 事先描述好的步骤 的无思想的 工具人 而已。


如果我还有其他问题需要行小观帮忙,那么我就还得把如何完成这些问题的详细步骤全都告诉他,这么麻烦那我还找别人帮忙干什么呢?还不如我自己去做。而且有些问题我自己也不会做,那怎么办?所以行小观并不是一个合格的工具人。

1.2. 面向对象编程

与面向过程编程不同,面向对象编程 (Object Oriented Programming,OOP) 是一种以对象为中心的编程思想,对象是现实世界中的一个个事物的实体。

对象包含了用户可以使用 (公开) 的功能部分和对用户隐藏的实现部分。

在开发过程中,我们可以使用功能部分来解决问题,但是并不关心功能是怎样实现的。

还是上面的那个买水的例子,使用面向对象来实现:

  1. 我给行小观 10 块钱,让他帮我买瓶水
  2. 行小观把水和零钱带回来给我

在这个例子中,我只需要给行小观钱,然后他就能帮我买水。我相信我请行小观有「买水的能力」,他一定能帮我买到水。至于去哪买?怎么买?行小观自己知道,我并不关心他是怎么买到水的,因为我的目的很简单:「我想要一瓶水」。

如果我有其他问题需要行小观帮忙,无论这些问题我会不会做,直接告诉他就好了,他会帮我完成。

我只专注于问题本身,具体的操作我并不关心。现在行小观是一个合格的工具人了。

2. 类和对象

2.1. 二者之间的关系

这里通过一个大家耳熟能详的神话——女娲造人,来说明类和对象之间的关系。

相传女娲以泥土 仿照自己 抟土造人,创造并构建人类社会。

在这个神话里,「女娲」是一个蓝图、模板,「人」是依据该蓝图被创造出来的个体。

「女娲」可以看做 类(Class),「人」可以看做 对象(Object)

跳出神话,来到真实世界。

我们目能所及的事物都可以看做是「对象」,比如说你用的桌子、坐的椅子、玩的电脑、养的狗……这些一个个真实存在,你能摸到的物品都是对象。

狗有千千万……高的、矮的、胖的、瘦的、黑色的、白色的等各不相同,但是总能在这些不同的狗之中找到相同的特性,这些不同品种的狗我们把它统称为「狗」。「狗」即为类,而我们养的真实存在的狗为对象。

总结一下:

  • 类是对一类具有共同特征的事物的抽象,是一类事物的统称,是一个抽象概念(比如“人类”这个名词)。
  • 对象是这类事物相对应的具体存在的实体,是一个具体存在的事物(比如“行小观”这个具体的人)。
  • 类是创建单个对象时的蓝图、模板。

2.2. 类

当我们说到「狗」这个类的时候,会很自然地想到和狗相关的一些特点和习性。

比如,名字、品种、颜色、年龄等,这些是 属性

还有,吠叫、看门等,这些是 行为

一个类包括了属性和行为,行为可以操纵属性。

对应到代码中,属性即为成员变量,行为即为成员方法,成员方法可以操纵成员变量。

下面是一个具体的类:

程序 2 -1
/**
 * 狗类
 * @author Xing Xiaoguan
 */

public class Dog {
    // 属性——成员变量
    String name;
    int age;
    int legs;

    // 行为——成员方法
    // 行为——吠叫
    public void say() {System.out.println("我是" + name + "汪汪汪");
    }

    // 行为——看门
    public void watchDoor() {System.out.println("赶走陌生人");
    }
}

2.3. 对象

类是一个抽象概念,而对象则是一个具体的实例。

以狗为例,我们养的不可能是「一类狗」,而是在和一只「具体的狗」玩耍,比如说哮天犬。

回到女娲 (类) 造人 (对象) 这个神话中,人是以女娲为模板被造出来的,女娲造人的过程,即 由类构造对象的过程称为创建类的实例(instance)。

程序 2 -2
public static void main(String[] args) {Dog dog = new Dog();// 创建类的实例,对象 dog
   dog.name = "哮天犬";
   dog.age = 2;
   dog.legs = 4;
   dog.say();}

对于被创建出来的对象而言,它们都不一样,每一个特定的对象 (实例) 都有一组特定的属性值 (成员变量),这些 属性值的集合就是这个对象的当前状态 ,只要对象使用者通过行为(成员方法) 向该对象发送消息,这些状态就可能被改变。

上面这句话怎么理解?

现在有两只狗(两个对象):哮天犬和哮地犬,这两个对象的名字、年龄等属性不同,即当前状态不同。每只狗都有一个行为:可以「每过一年,年龄增长 1 岁」,当通过该行为向哮天犬发送消息时,哮天犬的状态就被改变了。

可以看出,一个对象由 状态 (state) 和行为 (behavior) 组成,对象在成员变量中存储状态,通过成员方法公开其行为

研究一个对象,我们要去关注它处于什么状态?具有哪些行为?

对象的三个主要特性:

  • 对象的行为——可以对对象施加哪些操作(方法)?
  • 对象的状态——当施加那些方法时,对象如何响应?
  • 对象标识——如何辨别具有相同状态与行为的不同对象?

3. 三大特性

3.1. 封装

封装 (encapsulation) 是 Java 面向对象的三大特行之一。

一个对象具有属性和行为,封装把其属性和行为组合在了一起,但是为什么需要封装?

上文已经介绍了,一个对象由其状态和行为组成。我们回看 程序 2 -1,虽然这段代码表示出了 Dog 类,但是有一个很大的问题:创建出来的 Dog 对象的状态很容易被改变

比如我们可以直接修改 程序 2 -2中的对象的状态:

dog.name = "哮地犬";
dog.legs = 3;

你的狗的名字被 <u> 不怀好意的人 </u> 给改了,腿也少了一条!这种事情是 危险、可怕的

我们希望别人能够“知道”自己的狗叫什么名字、有几条腿等信息,但是又要防止不怀好意的人随便“伤害”自己的狗,怎么办呢?答案是 封装

将对象的状态和行为封装起来,使用该对象的用户 只能 通过对象本身提供的方法来访问该对象的状态。前面也说过,对象的当前状态可能会改变,但是这种改变不是对象自发的,必须 通过调用对象本身提供的方法来改变。如果不通过调用方法就能改变对象状态,只能说明封装性被破坏了。

换句话说,我们将对象的属性 (状态) 对外隐藏 起来,这些状态能否被访问或修改,由对象自己来决定,决定的方式就是「给对象的使用者提供可调用的方法,用户通过这些方法来进行访问和修改」。

程序 2 -1可以改进为:

程序 3 -1
/**
 * 封装后的狗类
 * @author Xing Xiaoguan
 */

public class Dog {
    private String name;
    private int age;
    private int legs;

    public String getName() {return name;}

    public int getAge() {return age;}

    public int getLegs() {return legs;}

    public void setName(String name) {this.name = name;}

    public void setAge(int age) {this.age = age;}

    // 行为——吠叫
    public void say() {System.out.println("我是" + name + "汪汪汪");
    }

    // 行为——看门
    public void watchDoor() {System.out.println("赶走陌生人");
    }

}

乍一看多了许多代码,其实就多了两个部分:

  1. 使用 private 修饰符来修饰成员变量,将其私有化,确保只能在本类内被访问到,实现隐藏。
  2. 给成员变量提供了对应的访问方法(getter方法)和修改方法(setter方法)。

其中我们给用户 能够访问或修改的 成员变量都编写上对应的 setter 或 getter 方法,如 name。用户 不能访问或修改 的成员变量不写 setter 或 getter 方法即可。

/**
 * 实例化一只小狗,并和它玩
 */
public class Play {public static void main(String[] args) {Dog dog = new Dog();
        dog.setName("哮天犬");;
        dog.setAge(2);
        dog.say();
        dog.watchDoor();}
}

现在,用户不能直接访问或修改对象的状态,必须通过提供的方法。对象并没有给 legs 变量提供 setter 方法,这样用户就只能访问狗有几条腿,但是不能修改。狗腿被人“偷”了的事情也不会再发生了。

而且,当我们使用 setter 方法修改成员变量时,可以进行其他的操作,如错误检查 。比如设置age 变量时:

// 狗的平均寿命为 10~15 年,太大了不合理
public void setName(String name) {if (name > 0 && name < 30)
        this.name = name;
}

现在,Dog类就比较 安全 了。

由上面的代码可以看出,如果要访问或修改成员变量,需要:

  • 成员变量是私有的
  • 一个公有的 getter 方法
  • 一个公有的 setter 方法

封装还有一个优点就是:对外隐藏了具体实现,这样的好处就是:我们可以修改内部实现,除了修改了该类的方法外,不会影响其他代码。

封装使对象对外变成了一个“黑箱”,用户只会使用,但不清楚内部情况。

前面买水的例子也体现了封装思想:行小观买水的方式有很多,走路去、骑车去、甚至找比人帮忙,但是他改变买水的方式并不会对我造成影响。

3.2. 继承

生活中除了狗,还有许多其他动物,比如猫、兔子……

程序 3 -2
/**
 * 猫类
 * @author Xing Xiaoguan
 */

public class Cat {
    private String name;
    private int age;
    private int legs;
    private String owner;// 主人

    //getters and setters……

    // 行为——叫
    public void say() {System.out.println("我是" + name + "喵喵喵");
    }

    // 行为——捉老鼠
    public void catchMouse() {System.out.println("捉到一只老鼠");
    }
}
程序 2 -5
/**
 * 兔子类
 * @author Xing Xiaoguan
 */

public class Rabbit {
    private String name;
    private int age;
    private int legs;
    private String home;// 住址

    //getters and setters……

    // 行为——叫
    public void say() {System.out.println("我是" + name + "咕咕咕");
    }

    // 行为——捣药
    public void makeMedicine() {System.out.println("在" + home + "捣药");
    }
}

写完这两个类,发现有许多属性和方法是重复的,如果需要再写 100 个动物的类,那得

这些动物形态各异,但是它们都被统称为“动物”,也就是说,我们仍能在它们身上找出相同的特点,比如它们都有名字、年龄、腿、能发出声音……

前面介绍类的时候已经说了,类是对一类具有共同特征的事物的抽象,所以此时我们还能从狗、猫、兔子这些类中再抽象出一个类——动物类。

程序 3 -3
/**
 * 动物类
 * @author Xing Xiaoguan
 */

public class Animal {
    private String name;
    private Integer age;
    private Integer legs;

    public void say() {System.out.println("我是"+name+"发出声响");
    }
    //setters and getters……
}

这个更抽象的类就是 父类 ,而狗、猫、兔子类是 子类 。子类可以使用extends 关键字 继承 父类的属性和方法,这意味着相同的代码只需要写一遍。

程序 3 -4
/**
 * 狗类继承父类
 * @author Xing Xiaoguan
 */

public class Dog extends Animal{

    // 行为——吠叫
    public void say() {System.out.println("我是" + getName() + "汪汪汪");
    }

    // 行为——看门
    public void watchDoor() {System.out.println("赶走陌生人");
    }
}
程序 3 -5
/**
 * 猫类继承父类
 * @author Xing Xiaoguan
 */

public class Cat extends Animal {
    private String owner;

    public String getOwner() {return owner;}

    public void setOwner(String owner) {this.owner = owner;}

    // 行为——喵喵叫
    public void say() {System.out.println("我是" + getName() + "喵喵喵");
    }

    // 行为——捉老鼠
    public void catchMouse() {System.out.println("捉到一只老鼠");
    }
}
程序 3 -6
/**
 * 兔子类继承父类
 * @author Xing Xiaoguan
 */

public class Rabbit extends Animal {

    private String home;

    public String getHome() {return home;}

    public void setHome(String home) {this.home = home;}

    // 行为——叫
    public void say() {System.out.println("我是" + getName() + "咕咕咕");
    }

    // 行为——捣药
    public void makeMedicine() {System.out.println("在" + home + "捣药");
    }
}

观察上面的子类和父类,可以发现:

  1. 子类不用再重复父类中已有的属性和方法,能通过继承获取到。
  2. 子类比父类的功能更加丰富,子类可以拥有自己的属性和方法
  3. 子类如果感觉继承自父类的方法不合适,可以 重写 父类的方法的实现过程,注意返回值和形参不能改变。

使用继承的好处:

  • 提高代码的复用性,不用再写那么多重复代码了。
  • 使代码便于维护,当我们需要修改某个公用方法时,不需要一个个类去修改,只需修改父类的该方法即可。

3.3. 多态

看下面一段代码,我们来直观体验什么是多态。

程序 3 -7
public static void main(String[] args) {Animal dog = new Dog();
    dog.setName("哮天犬");
    dog.say(dog.getName());
    
    Animal cat = new Cat();
    cat.setName("加菲猫");
    cat.say();

    Animal rabbit = new Rabbit();
    rabbit.setName("玉兔");
    rabbit.say();}

运行,输出:
我是哮天犬汪汪汪
我是加菲猫喵喵喵
我是玉兔咕咕咕
  1. DogCatRabbit 都是 继承 Animal父类。
  2. DogCatRabbit重写 Animalsay(String name) 方法。
  3. 在创建实例对象时,我们 使用父类引用指向子类的对象

使用多态应当注意一下几点:Animal dog = new Dog()

  • 在多态中,子类对象只能调用父类中定义的方法,不能调用子类中独有的方法。

    比如 dog 不能调用 watchDoor() 方法。

  • 在多态中,子类可以调用父类的所有方法。
  • 在多态中,子类如果重写了父类的方法,那么子类调用该方法时,调用的是子类重写的方法。

在上面的代码中,狗、猫、兔子对象都运行了 say 方法,但是输出不同。

由此看出,不同的对象的同一行为具有不同的表现形式,这就是多态。

在实际的本例中,我们可以理解为:动物Animal,他们都会叫出声。如果是狗,则叫的是汪汪汪;如果是猫,则叫的是喵喵喵;如果是兔子,则叫的是咕咕咕。

4. 总结

面向对象思想使我们在编程更加贴近现实世界,类是现实世界的抽象,对象则是一个个具体的事物。

封装使一个个具体的事物更加独立,继承则使一些类似的事物之间具有联系,而多态则使事物的行为更加灵活多样。

面向对象编程提高了软件的重用性、灵活性、扩展性。

如有错误,还请指正

参考资料:

  • The Java Tutorials
  • 维基百科
  • 百度百科
  • Java 核心技术 卷 1

文章首发于公众号「行人观学」

正文完
 0