在还不清楚怎样面向对象?一文中,已经简单介绍了面向对象的基本思想和三大特性,但是不够详细。本文再来具体探究一下面向对象。
1. 概述
1.1. 面向过程编程
面向过程编程(Procedure Oriented Programming,POP)是一种以过程为中心的编程思想,开发人员在解决问题时更专注于过程。
当我们遇到一个问题时,只需要分析出第1步要做什么、第2步要做什么……直到解决问题,然后把这些步骤一步步地实现即可。
比如,我现在要编写一个面向过程的程序来模拟「我要让我的好朋友行小观去帮我买瓶水」这个问题,如下:
- 给行小观10块钱
- 告诉行小观去哪买水
- 行小观找到我要的那瓶水
- 付钱,找零钱
- 把水带回来给我
在从「给行小观钱」到「行小观把水给我」的整个过程,我和行小观都专注于完成每一个步骤(事件),行小观只是一个执行我事先描述好的步骤的无思想的工具人而已。
如果我还有其他问题需要行小观帮忙,那么我就还得把如何完成这些问题的详细步骤全都告诉他,这么麻烦那我还找别人帮忙干什么呢?还不如我自己去做。而且有些问题我自己也不会做,那怎么办?所以行小观并不是一个合格的工具人。
1.2. 面向对象编程
与面向过程编程不同,面向对象编程(Object Oriented Programming,OOP)是一种以对象为中心的编程思想,对象是现实世界中的一个个事物的实体。
对象包含了用户可以使用(公开)的功能部分和对用户隐藏的实现部分。
在开发过程中,我们可以使用功能部分来解决问题,但是并不关心功能是怎样实现的。
还是上面的那个买水的例子,使用面向对象来实现:
- 我给行小观10块钱,让他帮我买瓶水
- 行小观把水和零钱带回来给我
在这个例子中,我只需要给行小观钱,然后他就能帮我买水。我相信我请行小观有「买水的能力」,他一定能帮我买到水。至于去哪买?怎么买?行小观自己知道,我并不关心他是怎么买到水的,因为我的目的很简单:「我想要一瓶水」。
如果我有其他问题需要行小观帮忙,无论这些问题我会不会做,直接告诉他就好了,他会帮我完成。
我只专注于问题本身,具体的操作我并不关心。现在行小观是一个合格的工具人了。
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;
你的狗的名字被不怀好意的人给改了,腿也少了一条!这种事情是危险、可怕的!
我们希望别人能够“知道”自己的狗叫什么名字、有几条腿等信息,但是又要防止不怀好意的人随便“伤害”自己的狗,怎么办呢?答案是封装!
将对象的状态和行为封装起来,使用该对象的用户只能通过对象本身提供的方法来访问该对象的状态。前面也说过,对象的当前状态可能会改变,但是这种改变不是对象自发的,必须通过调用对象本身提供的方法来改变。如果不通过调用方法就能改变对象状态,只能说明封装性被破坏了。
换句话说,我们将对象的属性(状态)对外隐藏起来,这些状态能否被访问或修改,由对象自己来决定,决定的方式就是「给对象的使用者提供可调用的方法,用户通过这些方法来进行访问和修改」。
程序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("赶走陌生人");
}
}
乍一看多了许多代码,其实就多了两个部分:
- 使用
private
修饰符来修饰成员变量,将其私有化,确保只能在本类内被访问到,实现隐藏。 - 给成员变量提供了对应的访问方法(
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 + "捣药");
}
}
观察上面的子类和父类,可以发现:
- 子类不用再重复父类中已有的属性和方法,能通过继承获取到。
- 子类比父类的功能更加丰富,子类可以拥有自己的属性和方法
- 子类如果感觉继承自父类的方法不合适,可以重写父类的方法的实现过程,注意返回值和形参不能改变。
使用继承的好处:
- 提高代码的复用性,不用再写那么多重复代码了。
- 使代码便于维护,当我们需要修改某个公用方法时,不需要一个个类去修改,只需修改父类的该方法即可。
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();
}
运行,输出:
我是哮天犬汪汪汪
我是加菲猫喵喵喵
我是玉兔咕咕咕
-
Dog
、Cat
和Rabbit
都是继承了Animal
父类。 -
Dog
、Cat
和Rabbit
都重写了Animal
的say(String name)
方法。 - 在创建实例对象时,我们使用父类引用指向子类的对象。
使用多态应当注意一下几点:Animal dog = new Dog()
- 在多态中,子类对象只能调用父类中定义的方法,不能调用子类中独有的方法。
比如
dog
不能调用watchDoor()
方法。 - 在多态中,子类可以调用父类的所有方法。
- 在多态中,子类如果重写了父类的方法,那么子类调用该方法时,调用的是子类重写的方法。
在上面的代码中,狗、猫、兔子对象都运行了say方法,但是输出不同。
由此看出,不同的对象的同一行为具有不同的表现形式,这就是多态。
在实际的本例中,我们可以理解为:动物Animal
,他们都会叫出声。如果是狗,则叫的是汪汪汪;如果是猫,则叫的是喵喵喵;如果是兔子,则叫的是咕咕咕。
4. 总结
面向对象思想使我们在编程更加贴近现实世界,类是现实世界的抽象,对象则是一个个具体的事物。
封装使一个个具体的事物更加独立,继承则使一些类似的事物之间具有联系,而多态则使事物的行为更加灵活多样。
面向对象编程提高了软件的重用性、灵活性、扩展性。
如有错误,还请指正
参考资料:
- The Java Tutorials
- 维基百科
- 百度百科
- Java核心技术 卷1