本文主要介绍了面向对象的基本思想和三大特征。
一、引入例子
创建一个Dog
小狗类
/**
* 小狗类
*/
public class Dog {
String name;
Integer age;
Integer legs;
public void say(String name) {
System.out.println("我是"+ name +",汪汪汪");
}
public void watchDoor() {
System.out.println("赶走陌生人");
}
}
小狗可以有名字、年龄、腿数这些属性,还可以有「叫」这个行为,这些是小狗共有的属性和行为。
从上面代码可以看出:类定义了一件事物的抽象特点。类的定义包含了数据的形式以及对数据的操作。
当我们和小狗玩耍时,并不是在和「一类狗」玩耍,而是在和「具体的狗」玩耍,比如说哮天犬。
所以只有Dog类是不能和狗玩耍的,我们需要「一只具体的狗」,这只具体的狗从何而来?在现实生活中,我们可能需要花钱去宠物店买一只具体的狗。在编程世界中,我们同样需要花「钱」去「买」,不过这里的「钱」指的是「内存」,「买」指的是「new」,「具体的狗」指的是实例化出的「对象」。
/**
* 实例化一只小狗,并和它玩
*/
public class Play {
public static void main(String[] args) {
Dog dog = new Dog();
dog.name = "哮天犬";
dog.age = 2;
dog.legs = 4;
dog.say(dog.name);
dog.watchDoor();
}
}
运行之,输出
我是哮天犬,汪汪汪
赶走陌生人
现在我们花钱(内存)买(new)了一只两岁的、有四条腿的、名叫哮天犬的小狗(对象),它有具体的属性和行为,是一只「活生生的狗。
从上面的代码看出:
-
对象是类的实例。
-
系统给对象分配内存空间,而不会给类分配内存空间。这很好理解,类是抽象的,系统不可能给抽象的东西分配空间,而对象则是具体的。
二、面向对象的三大特点
通过上面的例子,简单介绍了一下类和对象的概念和二者之间的联系。下面我们在来介绍一下面向对象的三大特点。
1. 封装
现在我们分析一下上面的小狗例子存在的问题。
- 对于
Dog
类来说,该类的属性和方法全部暴露在外,即从类外面看是可见的。这样一来,小狗就没了隐私权。 - 我们在实例化小狗时,是直接给它的属性赋值的,这就意味着可以在类外我们可以随意修改小狗的属性。这就意味着我们买小狗的时候可以随意修改它的名字、年龄甚至腿的条数(可怕)。
在上面的例子中,我们的类中的数据可以被随意访问和修改,这是大问题。
发现问题所在之后,我们就需要去解决它,而解决办法就是封装。
下面对Dog
类进行封装:
/**
* 对小狗类进行封装
*/
public class Dog {
private String name;
private Integer age;
private Integer legs;
public void say(String name) {
System.out.println("我是"+ name +",汪汪汪");
}
public void watchDoor() {
System.out.println("赶走陌生人");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public Integer getLegs() {
return legs;
}
public void setLegs(Integer legs) {
this.legs = legs;
}
}
我们将小狗类的属性设置为私有,对这些数据进行隐藏,只能本类才能访问。
/**
* 实例化一只小狗,并和它玩
*/
public class Play {
public static void main(String[] args) {
Dog dog = new Dog();
dog.setName("哮天犬");;
dog.setAge(2);
dog.setLegs(4);
dog.say(dog.getName());
dog.watchDoor();
}
}
外部类只有通过setter
和getter
方法才能修改和访问属性。这些setter
和getter
方法是Dog
类自愿暴露出来的,是外部类访问该类的属性的入口,如果Dog
类不想自己的某些属性被外部类访问和修改,那么关闭这些入口即可。
封装是将数据和操作数据的方法封装起来,对数据进行隐藏,避免了外部的干扰和误用,外部代码不被允许直接访问内部的对象数据,只能通过允许的方法来访问。这样就避免了外部代码和对象内部产生不必要的关系,在重构类内部代码时,(只要「入口」的调用方式不变)我们也不用修改外部代码。
在上文中,我们根据Dog
类实例化出一个具体的小狗「哮天犬」,而封装则使哮天犬这个具体的个体更加独立、自主、安全。
2. 继承
之前我们去宠物店买了一只叫哮天犬的狗,宠物店里还有其他许多种类的宠物,猫、兔子、鸟、鱼……
下面我们写出他们的类:
/**
* 猫类
*/
public class Cat {
private String name;
private Integer age;
private Integer legs;
public void say(String name) {
System.out.println("我叫"+name+",喵喵喵");
}
public void catchMouse() {
System.out.println("捉到一只老鼠");
}
setters and getters
}
/**
* 兔子类
*/
public class Rabbit {
private String name;
private Integer age;
private Integer legs;
private String home;
public void say(String name) {
System.out.println("我叫"+name+",咕咕咕");
}
public void makeMedicine() {
System.out.println("捣药");
}
setters and getters
}
写完猫类和兔子类后,我们发现了问题,有许多属性和方法是重复的,但是宠物店里还有许多宠物……
发现问题所在之后,我们就需要去解决它,而解决办法就是继承。
继承涉及到父类和子类,父类是对子类的进一步抽象,子类比父类更具体。子类会继承父类的属性和行为,这意味着相同的代码只需写一遍。
/**
* 动物类,父类
*/
public class Animal {
private String name;
private Integer age;
private Integer legs;
public void say(String name) {
System.out.println("我是"+name+"发出声响");
}
setters and getters
}
/**
* 小狗类,子类。继承Animal父类
*/
public class Dog extends Animal {
public void say(String name) {
System.out.println("我是"+ name +",汪汪汪");
}
public void watchDoor() {
System.out.println("赶走陌生人");
}
}
/**
* 猫类,子类。继承Animal父类
*/
public class Cat extends Animal {
public void say(String name) {
System.out.println("我是"+name+",喵喵喵");
}
public void catchMouse() {
System.out.println("捉到一只老鼠");
}
}
/**
* 兔子类,子类。继承Animal父类
*/
public class Rabbit extends Animal{
private String home;
public void say(String name) {
System.out.println("我是"+name+",咕咕咕");
}
public void makeMedicine() {
System.out.println("捣药");
}
public String getHome() {
return home;
}
public void setHome(String home) {
this.home = home;
}
}
我们将相同的代码抽取出来,放在Animal
这个更抽象的类中,称之为父类。Dog
、Cat
、Rabbit
这些更具体的、具有Animal
的成员变量和方法的类不必再重复那些代码,只需使用extend
关键字继承Animal
类即可。
- 子类可以拥有他自己的成员变量和方法,即扩展父类。比如
Rabbit
类的home
成员变量和makeMedicine()
方法。 - 子类如果感觉继承自父类的方法不合适,可以重写父类的方法的实现过程,注意返回值和形参不能改变。比如
void say(String name)
方法。
public class Play {
public static void main(String[] args) {
Rabbit rabbit = new Rabbit();
rabbit.setName("玉兔");
rabbit.setAge(1000);
rabbit.setLegs(4);
rabbit.setHome("广寒宫");
rabbit.say(rabbit.getName());
rabbit.makeMedicine();
}
}
运行之,输出
我叫玉兔,咕咕咕
捣药
通过以上的例子看出,使用继承可以:
- 提高代码的复用性,不用再写那么多重复代码了。
- 使代码便于维护,当我们需要修改某个公用方法时,不需要一个个类去修改,只需修改父类的该方法即可。
3. 多态
看下面一段代码,我们来直观体验什么是多态。
public static void main(String[] args) {
Animal dog = new Dog();
dog.setName("哮天犬");
dog.say(dog.getName());
Animal cat = new Cat();
cat.setName("加菲猫");
cat.say(cat.getName());
Animal rabbit = new Rabbit();
rabbit.setName("玉兔");
rabbit.say(rabbit.getName());
}
运行之,输出
我是哮天犬,汪汪汪
我是加菲猫,喵喵喵
我是玉兔,咕咕咕
Dog
、Cat
和Rabbit
都是继承了Animal
父类。Dog
、Cat
和Rabbit
都重写了Animal
的say(String name)
方法。- 在创建实例对象时,我们使用父类引用指向子类的对象。
使用多态应当注意一下几点:Animal dog = new Dog()
-
在多态中,子类对象只能调用父类中定义的方法,不能调用子类中独有的方法。
比如dog不能调用
watchDoor()
方法。 -
在多态中,子类可以调用父类的所有方法。
-
在多态中,子类如果重写了父类的方法,那么子类调用该方法时,调用的是子类重写的方法。
在上面的代码中,狗、猫、兔子对象都运行了say
方法,但是输出不同。
由此看出,不同的对象的同一行为具有不同的表现形式,这就是多态。
在实际的本例中,我们可以理解为:你去宠物店买宠物Animal,他们都会叫出声。如果买狗,则叫的是汪汪汪;如果买猫,则叫的是喵喵喵;如果买兔子,则叫的是咕咕咕。
三、总结
面向对象思想使我们在编程更加贴近现实世界,类是现实世界的抽象,对象则是一个个具体的事物。封装使一个个具体的事物更加独立,继承则使这些类似的事物具有联系,而多态则使事物的行为更加灵活多样。
面向对象编程提高了软件的重用性、灵活性、扩展性。
如有错误,还请指正。
文章首发于公众号『行人观学』。