前言
在 Java 编程的领域中,面向对象编程(Object - Oriented Programming,OOP)是其核心思想,而封装、继承和多态这三大特征则是 OOP 的基石。它们为开发者提供了强大的工具,使得代码更加模块化、可维护、可扩展以及可复用。
一、封装(Encapsulation)
1.1 封装的概念
封装,简单来说,就是把数据(属性)和操作这些数据的方法捆绑在一起,形成一个相对独立的单元,就像一个被密封的盒子,外部只能通过特定的接口来与这个单元进行交互,而无法直接访问其内部的具体实现细节。这种方式隐藏了对象的复杂性,只向外界暴露必要的信息,提高了代码的安全性和可维护性。
1.2 封装的作用
合理隐藏,合理暴露
1.2.1 数据保护
封装可以防止外部代码对对象的属性进行随意修改,确保数据的完整性和一致性。例如,在一个银行账户类中,账户余额是一个非常敏感的信息,如果不进行封装,任何外部代码都可以直接修改余额,可能会导致数据的错误和安全问题。通过封装,我们可以将余额属性设置为私有,只提供特定的方法(如存款、取款)来对余额进行操作,这样可以对操作进行严格的控制和验证。
1.2.2 提高代码可维护性
当一个类的内部实现发生变化时,只要对外提供的接口不变,外部代码就不需要进行修改。这是因为封装隐藏了类的内部细节,外部代码只依赖于类的公共接口。例如,如果我们修改了银行账户类中存款和取款方法的内部实现逻辑,只要这两个方法的参数和返回值不变,使用这个类的其他代码就不会受到影响。
1.2.3 隐藏复杂性
封装可以将复杂的实现细节隐藏起来,只对外提供简单易用的接口。这样,其他开发者在使用这个类时,不需要了解其内部的具体实现,只需要知道如何调用公共接口即可。就像我们使用手机时,只需要知道如何操作屏幕上的图标和按钮,而不需要了解手机内部的电路和软件是如何工作的。
1.3 封装的实现方式
在 Java 中,封装主要通过访问修饰符和访问器方法来实现。
1.3.1 访问修饰符
Java 提供了四种访问修饰符,它们的访问权限从严格到宽松依次为:private
、default
(默认,不写修饰符时)、protected
和 public
。
private
:使用private
修饰的成员(属性和方法)只能在本类中访问,外部类无法直接访问。这是封装中最常用的访问修饰符,通常用于隐藏类的内部属性。default
:如果成员没有使用任何访问修饰符,那么它具有默认的访问权限,也称为包访问权限。具有默认访问权限的成员只能在同一个包内的类中访问。protected
:protected
修饰的成员可以在本类、同一个包内的其他类以及不同包中的子类中访问。protected
访问修饰符常用于继承关系中,允许子类访问父类的一些受保护的成员。public
:public
修饰的成员可以在任何类中访问,没有任何访问限制。通常,我们会将类的公共方法声明为public
,以便外部类可以调用。
1.3.2 访问器方法(Getter 和 Setter)
当属性被声明为 private
时,外部类无法直接访问这些属性。为了让外部类能够获取和修改这些属性的值,我们需要提供公共的访问器方法,即 getter
方法和 setter
方法。
getter
方法:用于获取属性的值,通常命名为getXxx()
,其中Xxx
是属性的名称。getter
方法一般返回属性的值,并且通常是public
的。setter
方法:用于设置属性的值,通常命名为setXxx()
,其中Xxx
是属性的名称。setter
方法通常接受一个参数,将该参数的值赋给属性,并且通常也是public
的。
1.4 封装的示例代码
// 定义一个员工类
class Employee {
// 私有属性,外部无法直接访问
private String name;
private int age;
private double salary;
// 构造方法,用于初始化对象
public Employee(String name, int age, double salary) {
// 调用 setter 方法进行属性赋值,确保数据的有效性
this.setName(name);
this.setAge(age);
this.setSalary(salary);
}
// 获取姓名的方法
public String getName() {
return name;
}
// 设置姓名的方法
public void setName(String name) {
// 可以在这里添加对姓名的验证逻辑
if (name != null && !name.isEmpty()) {
this.name = name;
} else {
System.out.println("姓名不能为空!");
}
}
// 获取年龄的方法
public int getAge() {
return age;
}
// 设置年龄的方法,添加了年龄范围的验证
public void setAge(int age) {
if (age >= 18 && age <= 65) {
this.age = age;
} else {
System.out.println("年龄必须在 18 到 65 岁之间!");
}
}
// 获取工资的方法
public double getSalary() {
return salary;
}
// 设置工资的方法,添加了工资范围的验证
public void setSalary(double salary) {
if (salary >= 0) {
this.salary = salary;
} else {
System.out.println("工资不能为负数!");
}
}
// 显示员工信息的方法
public void displayInfo() {
System.out.println("姓名:" + name + ",年龄:" + age + ",工资:" + salary);
}
}
public class EncapsulationExample {
public static void main(String[] args) {
// 创建一个员工对象
Employee employee = new Employee("张三", 25, 5000.0);
// 显示员工信息
employee.displayInfo();
// 尝试设置一个不合法的年龄
employee.setAge(10);
// 尝试设置一个不合法的工资
employee.setSalary(-1000);
// 显示更新后的员工信息
employee.displayInfo();
}
}
在这个示例中,Employee
类的 name
、age
和 salary
属性被声明为 private
,外部类无法直接访问。通过提供 getter
和 setter
方法,外部类可以间接访问和修改这些属性的值。在 setter
方法中,我们添加了一些验证逻辑,确保输入的数据是合法的。
二、继承(Inheritance)
2.1 继承的概念
继承是指一个类(子类,也称为派生类)可以继承另一个类(父类,也称为基类)的属性和方法。通过继承,子类可以复用父类的代码,并且可以在父类的基础上添加新的属性和方法,或者重写父类的方法以实现不同的行为。继承体现了一种 “is - a” 的关系,例如,猫是动物,狗是动物,所以猫类和狗类可以继承动物类。
2.2 继承的作用
2.2.1 代码复用
继承可以让子类复用父类的代码,避免了代码的重复编写,提高了开发效率。例如,多个不同的图形类(如圆形、矩形、三角形)都有一些共同的属性(如颜色、位置)和方法(如绘制、移动),我们可以将这些共同的部分提取到一个父类(如图形类)中,然后让各个图形类继承这个父类,这样就可以复用父类的代码。
2.2.2 代码扩展
子类可以在父类的基础上添加新的属性和方法,实现功能的扩展。例如,在动物类的基础上,猫类可以添加抓老鼠的方法,狗类可以添加看家的方法。
2.2.3 多态的基础
继承是实现多态的基础,多态是指同一个方法可以根据对象的不同类型表现出不同的行为,而继承为多态提供了对象之间的层次关系。
2.3 继承的实现方式
在 Java 中,使用 extends
关键字来实现继承。一个子类只能继承一个父类,这称为单继承。虽然 Java 不支持多继承(一个子类不能同时继承多个父类),但可以通过接口来实现类似多继承的功能。
2.4 父类和子类的关系
2.4.1 子类继承父类的属性和方法
子类会继承父类的所有非 private
属性和方法。这意味着子类可以直接使用这些属性和方法,而不需要重新编写代码。例如,子类可以直接调用父类的 getter
和 setter
方法来访问和修改从父类继承的属性。
2.4.2 子类可以添加新的属性和方法
子类在继承父类的基础上,可以添加自己特有的属性和方法。这些属性和方法是子类独有的,父类无法访问。例如,在动物类的基础上,猫类可以添加 furColor
属性和 catchMouse
方法。
2.4.3 子类可以重写父类的方法
子类可以重写父类的方法,即重新定义父类中已经存在的方法。重写的方法需要满足方法名、参数列表和返回类型与父类中的方法相同,并且访问权限不能比父类方法的访问权限更严格。重写的目的是为了实现子类特有的行为。
2.5 方法重写(Override)
方法重写是继承中的一个重要概念,它允许子类改变父类方法的实现。在重写方法时,需要注意以下几点:
- 方法签名必须相同:方法名、参数列表和返回类型必须与父类中的方法相同。如果返回类型是父类方法返回类型的子类,也可以称为协变返回类型,这在 Java 中是允许的。
- 访问权限不能更严格:子类方法的访问权限不能比父类方法的访问权限更严格。例如,如果父类方法是
public
的,子类重写的方法不能是private
或protected
的。 - 异常抛出不能更多:子类方法不能抛出比父类方法更多的异常。如果父类方法抛出了某个异常,子类重写的方法可以不抛出该异常,或者抛出该异常的子类异常。
2.6 访问父类成员
在子类中,可以使用 super
关键字来访问父类的成员。super
关键字有以下几种用法:
- 调用父类的构造方法:在子类的构造方法中,可以使用
super()
或super(参数列表)
来调用父类的构造方法。super()
调用父类的无参构造方法,super(参数列表)
调用父类的有参构造方法。需要注意的是,super()
调用必须放在子类构造方法的第一行。 - 访问父类的成员变量:当子类的成员变量与父类的成员变量同名时,可以使用
super.成员变量名
来访问父类的成员变量。 - 调用父类的成员方法:当子类重写了父类的方法时,可以使用
super.方法名(参数列表)
来调用父类的原始方法。
2.7 继承的示例代码
// 定义一个动物类,作为父类
class Animal {
// 动物的属性
protected String name;
protected int age;
// 构造方法
public Animal(String name, int age) {
this.name = name;
this.age = age;
}
// 动物的方法
public void eat() {
System.out.println(name + " 正在吃东西");
}
public void sleep() {
System.out.println(name + " 正在睡觉");
}
}
// 定义一个猫类,继承自动物类
class Cat extends Animal {
// 猫类特有的属性
private String furColor;
// 猫类的构造方法
public Cat(String name, int age, String furColor) {
// 调用父类的构造方法
super(name, age);
this.furColor = furColor;
}
// 猫类特有的方法
public void catchMouse() {
System.out.println(name + " 正在抓老鼠");
}
// 重写父类的 eat 方法
@Override
public void eat() {
System.out.println(name + " 正在吃鱼");
}
// 显示猫的信息
public void displayInfo() {
System.out.println("名字:" + name + ",年龄:" + age + ",毛色:" + furColor);
}
}
public class InheritanceExample {
public static void main(String[] args) {
// 创建一个猫对象
Cat cat = new Cat("咪咪", 2, "白色");
// 调用从父类继承的方法
cat.sleep();
// 调用重写后的 eat 方法
cat.eat();
// 调用猫类特有的方法
cat.catchMouse();
// 显示猫的信息
cat.displayInfo();
}
}
在这个示例中,Cat
类继承自 Animal
类,通过 extends
关键字实现。Cat
类继承了 Animal
类的 name
和 age
属性以及 eat()
和 sleep()
方法,同时添加了自己特有的 furColor
属性和 catchMouse()
方法。在 Cat
类中,我们还重写了父类的 eat()
方法,实现了不同的行为。
三、多态(Polymorphism)
3.1 多态的概念
多态是指同一个方法可以根据对象的不同类型表现出不同的行为。简单来说,就是一个方法名可以有多种实现方式,具体调用哪个实现方式取决于调用该方法的对象的实际类型。多态是面向对象编程的一个重要特性,它可以提高代码的灵活性和可扩展性。
3.2 多态的作用
3.2.1 提高代码的灵活性
多态允许我们在编写代码时使用父类类型的引用变量来引用不同的子类对象,然后根据实际对象的类型调用相应的方法,这样可以在运行时动态地改变对象的行为,提高了代码的灵活性。例如,我们可以编写一个通用的方法,接受一个父类类型的参数,然后根据传入的实际子类对象调用相应的方法,而不需要为每个子类编写单独的方法。
3.2.2 代码可扩展性
当需要添加新的子类时,不需要修改现有的代码,只需要将新的子类对象赋值给父类类型的引用变量即可,这样可以方便地扩展代码的功能。例如,在一个图形绘制系统中,如果需要添加一个新的图形类,只需要让这个新的图形类继承自现有的图形父类,并重写相应的绘制方法,然后就可以将这个新的图形对象传递给通用的绘制方法进行绘制,而不需要修改绘制方法的代码。
3.2.3 降低代码的耦合度
多态使得代码之间的依赖关系更加松散,不同的类可以通过接口或父类进行交互,而不需要关心具体的实现细节,从而降低了代码的耦合度。例如,一个方法可以接受一个接口类型的参数,而不需要关心具体是哪个类实现了这个接口,只要实现类实现了接口中定义的方法,就可以传递给这个方法进行处理。
3.3 多态的实现方式
在 Java 中,多态主要通过两种方式实现:方法重载和方法重写。
3.3.1 方法重载(Overloading)
方法重载是指在同一个类中定义多个方法,这些方法具有相同的方法名,但参数列表不同(参数的类型、个数或顺序不同)。方法重载是一种编译时多态,编译器会根据调用方法时传递的参数类型和个数来决定调用哪个方法。
示例代码:
class Calculator {
// 加法方法,两个整数相加
public int add(int a, int b) {
return a + b;
}
// 加法方法,三个整数相加
public int add(int a, int b, int c) {
return a + b + c;
}
// 加法方法,两个双精度浮点数相加
public double add(double a, double b) {
return a + b;
}
// 加法方法,一个整数和一个双精度浮点数相加
public double add(int a, double b) {
return a + b;
}
}
public class MethodOverloadingExample {
public static void main(String[] args) {
Calculator calculator = new Calculator();
// 调用两个整数相加的方法
int result1 = calculator.add(3, 5);
System.out.println("3 + 5 = " + result1);
// 调用三个整数相加的方法
int result2 = calculator.add(3, 5, 7);
System.out.println("3 + 5 + 7 = " + result2);
// 调用两个双精度浮点数相加的方法
double result3 = calculator.add(3.5, 5.5);
System.out.println("3.5 + 5.5 = " + result3);
// 调用一个整数和一个双精度浮点数相加的方法
double result4 = calculator.add(3, 5.5);
System.out.println("3 + 5.5 = " + result4);
}
}
方法重载的特点及注意事项
- 特点
- 提高代码可读性:使用相同的方法名来完成相似的功能,让代码更符合人类的思维习惯,便于理解和维护。例如,在上述
Calculator
类中,无论传入的是两个整数、三个整数,还是不同类型的数值,都使用add
方法名,调用者可以直观地知道这是进行加法运算的方法。 - 灵活性高:可以根据不同的参数组合来实现不同的逻辑,增强了方法的通用性。比如
add
方法可以处理多种不同类型和数量的参数,满足多样化的加法需求。
- 提高代码可读性:使用相同的方法名来完成相似的功能,让代码更符合人类的思维习惯,便于理解和维护。例如,在上述
- 注意事项
- 仅参数列表不同:方法重载只关注参数的类型、个数或顺序,与方法的返回类型、访问修饰符等无关。例如,下面的代码是错误的重载示例:
class WrongOverloadExample {
public int getValue() {
return 1;
}
// 错误:仅返回类型不同,不能构成方法重载
public double getValue() {
return 1.0;
}
}
- **自动类型转换**:在调用重载方法时,如果没有完全匹配的参数类型,Java 会进行自动类型转换。例如,如果调用 `add` 方法时传入的是 `int` 和 `float` 类型的参数,由于没有 `add(int, float)` 方法,Java 会将 `float` 自动转换为 `double`,然后调用 `add(int, double)` 方法。
3.3.2 方法重写(Override)与运行时多态
方法重写是实现运行时多态的关键。当子类重写了父类的方法后,使用父类类型的引用变量来引用子类对象时,调用该方法会根据对象的实际类型调用子类重写后的方法。
示例代码:
// 定义一个形状类,作为父类
abstract class Shape {
// 抽象方法,用于计算面积
public abstract double area();
// 显示形状信息的方法
public void showInfo() {
System.out.println("这是一个形状。");
}
}
// 定义一个圆形类,继承自形状类
class Circle extends Shape {
private double radius;
public Circle(double radius) {
this.radius = radius;
}
// 重写父类的 area 方法
@Override
public double area() {
return Math.PI * radius * radius;
}
// 重写父类的 showInfo 方法
@Override
public void showInfo() {
System.out.println("这是一个半径为 " + radius + " 的圆形。");
}
}
// 定义一个矩形类,继承自形状类
class Rectangle extends Shape {
private double width;
private double height;
public Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
// 重写父类的 area 方法
@Override
public double area() {
return width * height;
}
// 重写父类的 showInfo 方法
@Override
public void showInfo() {
System.out.println("这是一个宽为 " + width + ",高为 " + height + " 的矩形。");
}
}
public class PolymorphismExample {
public static void main(String[] args) {
// 父类类型的引用变量引用子类对象
Shape circle = new Circle(5);
Shape rectangle = new Rectangle(3, 4);
// 调用 area 方法,根据对象的实际类型调用相应的实现
System.out.println("圆形的面积是:" + circle.area());
System.out.println("矩形的面积是:" + rectangle.area());
// 调用 showInfo 方法,根据对象的实际类型调用相应的实现
circle.showInfo();
rectangle.showInfo();
// 定义一个方法来统一处理形状对象
printShapeInfo(circle);
printShapeInfo(rectangle);
}
public static void printShapeInfo(Shape shape) {
shape.showInfo();
System.out.println("该形状的面积是:" + shape.area());
}
}
运行时多态的必要条件
- 继承关系:必须存在子类和父类的继承关系,这是多态的基础。在上述示例中,
Circle
类和Rectangle
类都继承自Shape
类。 - 方法重写:子类必须重写父类的方法,这样才能在运行时根据对象的实际类型调用不同的方法实现。例如,
Circle
类和Rectangle
类都重写了Shape
类的area()
和showInfo()
方法。 - 父类引用指向子类对象:使用父类类型的引用变量来引用子类对象,通过这个引用变量调用重写的方法时,会根据对象的实际类型来决定调用哪个子类的方法。如
Shape circle = new Circle(5);
和Shape rectangle = new Rectangle(3, 4);
。
多态的优势体现
- 可维护性提升:当需要添加新的形状类时,只需要创建新的子类并继承
Shape
类,重写相应的方法,而不需要修改printShapeInfo
方法。例如,若要添加一个三角形类,只需要创建Triangle
类继承Shape
类,重写area()
和showInfo()
方法,就可以将Triangle
对象传递给printShapeInfo
方法进行处理,原有的代码逻辑无需改变。 - 代码复用与扩展性:
printShapeInfo
方法可以处理各种不同的形状对象,实现了代码的复用。同时,随着新形状类的不断添加,系统的功能可以方便地扩展,而不会影响到现有的代码结构。
多态中的类型转换
在多态的使用过程中,会涉及到类型转换,主要分为向上转型和向下转型。
- 向上转型:将子类对象赋值给父类引用变量,这是自动进行的,是安全的。例如
Shape circle = new Circle(5);
就是将Circle
对象向上转型为Shape
类型。向上转型后,只能调用父类中定义的方法,如果子类重写了这些方法,则调用子类重写后的方法。 - 向下转型:将父类引用变量转换为子类类型。向下转型需要进行强制类型转换,并且在转换前需要使用
instanceof
运算符进行类型检查,以避免ClassCastException
异常。示例代码如下:
Shape shape = new Circle(5);
if (shape instanceof Circle) {
Circle circle = (Circle) shape;
// 可以调用 Circle 类特有的方法
// 例如 circle.someCircleSpecificMethod();
}
总结
封装、继承和多态是 Java 面向对象编程的三大核心特征,它们相互关联、相互促进,共同构建了 Java 强大的面向对象编程体系。
封装通过访问修饰符和访问器方法将数据和操作封装在一起,保护了数据的安全性,提高了代码的可维护性和可复用性。开发者可以将类的内部细节隐藏起来,只对外提供必要的接口,使得代码更加健壮和易于管理。
继承允许子类复用父类的代码,并在其基础上进行扩展,实现了代码的复用和功能的增强。同时,继承为多态提供了基础,使得不同的子类对象可以通过父类引用进行统一处理。
多态通过方法重载和方法重写,让同一个方法可以根据对象的不同类型表现出不同的行为,提高了代码的灵活性、可扩展性和可维护性。多态使得代码更加通用,能够适应不同的需求变化。