引言
在软件开发的世界中,面向对象编程(Object-Oriented Programming, OOP)一直是一种备受推崇的编程范式。它是一种以对象为中心的编程思想,通过指挥对象实现具体的功能。在面向对象的世界里,一切都是对象。对象是程序的基本单元,它包含了数据和操作数据的方法。以汽车为例,汽车对象可以包含颜色、型号等属性,以及启动、停止等方法。对象将现实世界中的实体抽象成程序中的数据结构,使得我们可以更自然地建模和解决问题。面向对象的核心是类,类是对象的蓝图或模板,定义了对象的结构和行为。通过类,我们可以创建具体的对象实例。以动物类为例,动物类可以定义动物的共性属性(如呼吸、移动)和方法(如吃、睡)。类是一种抽象数据类型,它将数据和操作封装在一起,实现了代码的模块化和可维护性。
谈到面向对象时,我们总不可避免的提到面向对象的三大特征:封装、继承和多态。这些特征是面向对象编程的基石,使得代码更加模块化、可复用和可扩展。在本篇博客中,我们将深入探讨这三个特征的含义和作用。
1.封装
封装是指将数据和操作数据的方法(即行为)组合在一个单元中,并对外部隐藏实现细节。通过封装,我们可以将数据和操作封装在类内部,只暴露必要的接口给外界使用。这样做的好处是可以保护数据的完整性,防止外部直接访问和修改数据造成意外的错误。同时,封装也提供了代码的复用性,内部实现的变化不会影响到外部的使用。
封装通过访问修饰符(如private
、public
、protected
等)来控制对类的成员的访问权限。通常,我们将成员属性标记为private
,并提供公共的getter和setter方法来访问和修改这些属性。
以下是一个简单的封装例子:
public class Person {
private String name;
private int age;
private String gender;
// 构造方法用于初始化对象
public Person(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
// 公共方法用于获取姓名
public String getName() {
return name;
}
// 公共方法用于获取年龄
public int getAge() {
return age;
}
// 公共方法用于获取性别
public String getGender() {
return gender;
}
// 公共方法用于显示个人信息
public void showInfo() {
System.out.println("Name: " + name + ", Age: " + age + ", Gender: " + gender);
}
public static void main(String[] args) {
// 创建一个Person对象
Person person1 = new Person("Alice", 25, "Female");
// 通过公共方法访问个人信息
System.out.println("Person's Name: " + person1.getName());
System.out.println("Person's Age: " + person1.getAge());
System.out.println("Person's Gender: " + person1.getGender());
// 通过公共方法显示个人信息
person1.showInfo();
}
}
在这个例子中,name、age和gender被声明为私有(private)属性,只能在类的内部访问。为了允许外部访问这些属性,我们提供了公共的访问方法(getName、getAge和getGender),这样外部代码可以通过这些方法获取属性的值。通过这种方式,我们实现了对个人信息的封装,同时提供了受控的访问方式。
2.继承
继承是面向对象编程中的另一个重要特征,它允许一个类(子类)继承另一个类(父类)的属性和方法。在Java中,通过使用关键字extends来实现继承。继承使得代码的重用变得更加容易,同时也能够提高代码的可扩展性和灵活性。
继承的格式:class 子类 extends 父类 { }
例:class Dog extends Animal { }
2.1继承的好处与弊端
继承的好处:
- 代码的重用性:子类可以继承父类的属性和方法,避免了代码的重复性,大大提高了代码的复用性。
- 可维护性:如果需要修改或者添加某些功能,只需要在父类中进行修改或添加,而不需要修改所有的子类,这大大提高了代码的可维护性。
- 提高可读性:继承使得代码更加简洁易读,子类只需要关注自己需要实现的方法和属性,而无需关注父类的实现细节。
继承的弊端:
- 父类的变化可能影响到子类:如果父类的属性或者方法发生了改变,子类也会受到影响,可能需要修改子类的代码。
- 继承关系不够灵活:由于Java不支持多重继承,因此如果需要同时继承多个类的属性和方法,就无法使用继承了。
- 代码可读性下降:过多的继承层次可能会导致代码可读性下降,增加代码的复杂性。
2.2继承的特点
- 子类可以继承父类的非私有的属性和方法。
- 子类可以重写(Override)父类的方法,以实现自己的特定行为。
- 子类可以添加自己的属性和方法。
2.3继承实例
在编程的过程中,继承的应用场景一般如下:
- 当多个类需要共享某些属性和方法时,可以使用继承来避免代码重复。
- 当需要对某个已有的类进行修改或者扩展时,可以使用继承来实现。
一个典型的例子是动物类(Animal)和狗类(Dog)。假设我们需要实现一个系统,这个系统中需要定义不同种类的动物和它们的行为。首先我们可以定义一个基础的动物类,然后定义各种不同的动物类来继承基础类并实现自己特定的行为。比如:
在这个例子中,Dog类继承了Animal类,并重写了eat()方法来实现自己特定的行为。同时,Dog类也添加了自己的属性和方法(work)。这样做的好处是避免了代码的重复性,提高了代码的可读性和可维护性。同时,我们也可以通过定义不同种类的动物类来实现不同的行为。
3.多态
多态是面向对象编程中的第三大特征,它是指同一个对象在不同情况下表现出不同的行为。在Java中,多态主要通过方法的重载(Overloading)和重写(Overriding)来实现。多态使得程序在运行时能够根据对象的实际类型来调用相应的方法,从而提高了程序的灵活性和扩展性。
需要注意的是,多态有三个前提条件:
-
要有继承或实现关系
-
要有方法的重写
-
要有父类引用指向子类对象
举一个简单的例子:
public class Shape {
public void draw() {
System.out.println("画一个图形.");
}
}
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("画一个圆.");
}
}
public class Rectangle extends Shape {
@Override
public void draw() {
System.out.println("画一个矩形.");
}
}
在这个例子中,Shape是一个基础类,而Circle和Rectangle是它的子类。它们都重写了draw()方法来实现自己特定的绘制行为。
接下来,我们可以使用多态性来创建不同类型的形状对象,并调用它们的draw()方法,而无需关心具体是哪种形状。例如:
public class Main {
public static void main(String[] args) {
Shape shape1 = new Circle();
Shape shape2 = new Rectangle();
shape1.draw(); // 调用Circle类的draw()方法
shape2.draw(); // 调用Rectangle类的draw()方法
}
3.1多态中的成员访问特点
想要清楚多态中成员的访问特点,我们可以记住两句口诀:
对于成员变量:编译看父类,运行看父类;
对于成员方法:编译看父类,运行看子类。
下面,我们用一段代码来具体解释:
class Animal {
public String name = "Animal";
public void sound() {
System.out.println("动物发出声音");
}
}
class Dog extends Animal {
public String name = "Dog";
@Override
public void sound() {
System.out.println("小狗在叫");
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Animal();
Animal animal2 = new Dog();
System.out.println(animal1.name); // 输出 "Animal"
System.out.println(animal2.name); // 输出 "Animal"
animal1.sound(); // 输出 "动物发出声音"
animal2.sound(); // 输出 "小狗在叫."
}
}
在上面的例子中,我们创建了一个Animal对象赋值给animal1,再创建了一个Dog对象赋值给animal2。两个对象都有一个名为name的成员变量和一个名为sound()的方法。
当我们通过animal1.name和animal2.name访问name成员变量时,由于成员变量的访问以编译时类型为准,所以无论实际对象是什么类型,访问的都是父类Animal中定义的name变量,输出结果都是"Animal"。
而当我们通过animal1.sound()和animal2.sound()调用sound()方法时,由于成员方法的调用以实际对象类型为准,所以对于animal1,调用的是Animal类中的sound()方法;而对于animal2,虽然编译时类型是Animal,但实际对象是Dog,因此调用的是Dog类中重写的sound()方法,输出结果分别是"动物发出声音"和"小狗在叫"。
这个例子具体地展示了多态中成员访问的特点,即成员变量的访问以编译时类型为准(编译看父类,运行看父类),而成员方法的调用以实际对象类型为准(编译看父类,运行看子类)。
3.2多态的转型
多态虽然能够提高程序的扩展性,但它也有一个弊端,那就是不能使用子类的特有成员。在多态情况下,通过父类引用只能访问父类中声明的成员变量和方法,而不能访问子类中特有的成员。为此,想要解决这个问题,我们自然就想到了将父类型转为子类型,从而调用子类型中特有的成员。多态中的转型分为两种,分别是向上转型和向下转型。
向上转型即父类引用指向子类对象,也被称为父身子像,向上转型时,子类对象当成父类对象,只能调用父类的功能,如果子类重写了父类中声明过的方法,方法体执行的就是子类重写过后的功能。但是此时对象是把自己看做是父类类型的,所以其他资源使用的还是父类型的。
向下转型的格式为:子类型 对象名 = (子类型)父类引用;
需要注意的是,与向上转型不同,向下转型是大转小,因此需要进行强制类型转换,而向上转型是小转大,会自动转换。
在向下转型的过程中,如果被转的引用类型变量,对应的实际类型和目标类型不是同一种类型,那么在转换的时候就会出现ClassCastException错误,表示在运行时尝试将一个对象强制转换为不兼容的类型时抛出的异常。因此向下转型的步骤一般分为两步:
1.用instanceof关键字进行类型判断,格式为:变量名 instanceof 类型,理解为判断关键字左边的变量,是否是右边的类型,返回boolean类型结果,例如if(parentObj instanceof ChildClass)。
2.进行类型转换,即子类型 对象名 = (子类型)父类引用;
例如下面这个例子:
package com.object;
abstract class Animal {
public abstract void eat();
}
class Dog extends Animal {
public void eat() {
System.out.println("狗吃肉");
}
public void watchHome(){
System.out.println("看家");
}
}
class Cat extends Animal {
public void eat() {
System.out.println("猫吃鱼");
}
}
class Test4Polymorpic {
public static void main(String[] args) {
useAnimal(new Dog());
useAnimal(new Cat());
}
public static void useAnimal(Animal a){ // Animal a = new Dog();Animal a = new Cat();
a.eat();
if(a instanceof Dog){
Dog dog = (Dog) a;
dog.watchHome();
}
}
}
在本例中,使用a instanceof Dog判断a对象是不是Dog类型,因此在执行useAnimal(new Dog());时会输出结果:
而在执行useAnimal(new Cat());时因为此时的a不是Dog类型而是Cat类型,所以只会执行a.eat()方法,而不会执行dog.watchHome()方法,因此输出结果为: