大纲
面向对象
基本概念
什么是面向对象编程(OOP)?面向对象编程是一种程序设计思想,它将计算机程序视为一系列相互作用的对象。每个对象都代表现实世界中的一个实体,具有自己的状态(属性)和行为(方法)。核心特性包括封装(隐藏数据,保护对象内部状态)、继承(复用已有类,创建新的类)、多态(同一个方法可以对不同对象起作用)。
讲人话就是:
面向对象把程序看成是由很多小的对象组成的。这些对象就像是我们生活中的各种事物,比如一辆车、一只狗或者一本书。
每个对象都有它自己的属性(就像一辆车的颜色、速度)和行为(车能开动、刹车)。这些对象之间可以互相交流和合作,共同完成一些任务。
面向对象编程的意义
这样子做有什么好处呢?
当你在构建一个复杂系统或大型项目时,如果使用面向对象编程时,整个项目将变得易于管理和维护,且具备很高的扩展性。
我们举一个例子对比一下面向对象和面向过程的区别,依次来说明面向编程的意义
假设我们经营一家餐厅:
面向过程的思考方式:
步骤1: 顾客点餐
步骤2: 厨师做菜
步骤3: 服务员上菜
步骤4: 顾客结账
面向对象的思考方式:
对象1:顾客
属性:姓名,订单
行为:点餐,付款
对象2:厨师
属性:菜品种类
行为:做菜
对象3:服务员
属性:服务台
行为:上菜,收钱
对比:
面向过程: 代码可能写成一个长长的函数,每个步骤对应一段代码。如果要增加新的功能(比如外卖),就需要修改原来的函数,代码耦合度高,不易维护。
面向对象: 每个对象都有自己的职责,代码结构清晰。如果要增加外卖功能,只需要增加一个外卖员对象,并修改顾客对象的行为即可,代码可扩展性强。
封装
基本概念
封装是面向对象编程(OOP)中的一个重要概念,它指的是将对象的属性(数据)和方法(功能)隐藏起来,防止外部随意访问或修改,并通过提供特定的接口(方法)来与外界交互。这样,外界只能通过对象提供的方法来访问或修改其内部数据,而不能直接操作数据本身,从而保护了对象的内部状态。
封装的意义
- 数据保护: 防止外部直接修改对象内部数据,保证对象的状态一致性。
- 简化复杂性: 隐藏不必要的细节,只暴露出重要的接口,降低了系统的复杂性。
- 增强代码的可维护性: 如果内部实现改变了,只要接口保持不变,外部代码不会受到影响。
- 控制访问: 可以通过设置访问权限(如private、public等)控制哪些部分对外公开,哪些部分只能在内部使用。
举个简单的例子:
public class Car {
private int speed; // 速度是私有的,外部不能直接修改
// 提供公共的方法来操作速度
public void setSpeed(int speed) {
if (speed >= 0) { // 控制输入的合法性
this.speed = speed;
}
}
public int getSpeed() {
return speed;
}
}
在这个例子中,speed是私有的,外界无法直接修改它,必须通过setSpeed和getSpeed方法来操作,这就是封装的一个典型应用。
继承
在 Java 中,继承是面向对象编程的一个重要特性,它允许一个类(子类)从另一个类(父类)继承属性和方法,从而实现代码的重用和扩展。
基本概念
- 父类(Superclass):也称为基类或超类,是被继承的类。
- 子类(Subclass):也称为派生类,是继承父类的类。
继承的语法
在 Java 中,使用关键字 extends
来实现继承。以下是一个简单的例子:
// 父类
class Animal {
void eat() {
System.out.println("This animal eats food");
}
}
// 子类
class Dog extends Animal {
void bark() {
System.out.println("The dog barks");
}
}
public class Main {
public static void main(String[] args) {
Dog dog = new Dog();
dog.eat(); // 调用继承自父类的方法
dog.bark(); // 调用子类自己的方法
}
}
继承的特性
-
单继承:Java 只支持单继承,一个类只能继承一个直接父类。
-
super 关键字:用于调用父类的构造方法和父类的方法。
-
构造方法:子类的构造方法会默认调用父类的无参构造方法。可以使用
super
关键字显式调用父类的构造方法。
super 关键字
super
关键字用于引用父类的成员(变量和方法),以及调用父类的构造方法。
class Animal {
String name = "Animal";
Animal() {
System.out.println("Animal constructor");
}
void eat() {
System.out.println("This animal eats food");
}
}
class Dog extends Animal {
String name = "Dog";
Dog() {
super(); // 调用父类的构造方法
System.out.println("Dog constructor");
}
void displayNames() {
System.out.println("Name: " + name); // 输出 Dog
System.out.println("Super name: " + super.name); // 调用父类属性,输出 Animal
}
@Override
void eat() {
super.eat(); // 调用父类的方法
System.out.println("The dog eats bones");
}
}
继承的意义
根据上面的例子,我们有一个Animal父类,当你想要有一只狗的类时,建立一个Dog类继承Animal类,这样的意义是父类的通用代码不用再重新写一遍,如果你想要新增一些狗的独特行为,可以在Dog类中新增一个方法,就相当于在父类的基础上扩展了功能。
- 代码重用:通过继承,子类可以重用父类的代码,避免代码重复。
- 代码扩展:子类可以扩展父类的功能,添加新的属性和方法。。
- 多态性:继承是实现多态性的基础,允许对象以多种形式存在。
多态
基本概念
多态指的是同一个方法在不同对象上可以表现出不同的行为。通过多态,程序可以在不修改代码的情况下处理不同类型的对象,从而增强代码的灵活性和可扩展性。
多态的工作原理
在 Java 中实现多态主要有两种方式:
-
方法的重载(Overloading):在同一个类中,可以创建多个名称相同但参数列表不同的方法。这称为方法的重载。
-
方法的重写(Overriding):当子类有一个与父类或接口中声明的方法具有相同名称和参数列表的方法时,子类的方法会覆盖父类或接口的方法。
多态的使用
方法的重载
方法重载是在同一个类中定义多个同名但参数不同的方法。这些方法通过不同的参数(类型或数量)区分开,虽然方法名称相同,但它们的行为可以根据传入的参数类型和数量有所不同。
特点:
- 发生在同一个类中。
- 方法名相同,但参数列表不同(参数的数量或类型不同)。
- 返回类型可以相同或不同。
- 是一种编译时多态,编译器在编译时根据传入的参数选择调用哪个方法。
例子:
public class Calculator {
// 重载方法1:两个int类型参数
public int add(int a, int b) {
return a + b;
}
// 重载方法2:三个int类型参数
public int add(int a, int b, int c) {
return a + b + c;
}
// 重载方法3:两个double类型参数
public double add(double a, double b) {
return a + b;
}
在这个例子中,add
方法被重载了,允许你根据不同的参数类型和数量调用
“不同的” add
方法,例如调用add(1,2)
时,java会知道调用的是第一个add
方法。
方法的重写
要实现多态,通常需要使用继承或实现接口的方式,子类或实现类重写父类或接口中的方法。在运行时,Java 虚拟机(JVM)决定要调用哪个方法,这取决于对象的实际类型,而不是变量的声明类型。
假设我们有一个父类 Animal
和它的两个子类 Dog
和 Cat
。
class Animal {
void sound() {
System.out.println("This is animal sound");
}
}
class Dog extends Animal {
@Override
void sound() {
System.out.println("Bark");
}
}
class Cat extends Animal {
@Override
void sound() {
System.out.println("Meow");
}
}
现在我们可以通过父类 Animal
的引用来调用 sound()
方法,并体现出多态性。
public class TestPolymorphism {
public static void main(String[] args) {
Animal myAnimal = new Animal(); // 创建 Animal 对象
Animal myDog = new Dog(); // 创建 Dog 对象
Animal myCat = new Cat(); // 创建 Cat 对象
myAnimal.sound(); // 输出:This is animal sound
myDog.sound(); // 输出:Bark
myCat.sound(); // 输出:Meow
}
}
在上面的例子中,尽管变量 myDog
和 myCat
在编译时被声明为 Animal
类型,但在运行时,它们实际上分别引用 Dog
和 Cat
对象。因此,myDog.sound()
和 myCat.sound()
调用的是 Dog
和 Cat
类中覆盖的 sound()
方法,而不是 Animal
类中的方法。这就是多态性的表现。
多态的意义
-
提高代码的灵活性
通过多态,程序可以以同一种方式处理不同类型的对象,而不需要修改代码。
-
减少代码冗余
不同对象的操作可以通过相同的父类方法来实现,减少了重复代码的出现,提高了代码的可维护性。
-
提高代码的可拓展性
多态允许程序员在不修改现有代码的情况下,通过添加新的子类来增强程序功能。未来需要扩展时,可以通过继承父类并实现新的方法来实现新的功能,而不会影响到已有的功能。
多态题外话
在 Java 中,向上转型(Upcasting)和向下转型(Downcasting)是面向对象编程中多态性体现的两个重要概念,它们与类的继承体系紧密相关。上面讲到的多态例子就使用到了向上转型
向上转型
向上转型是指将子类类型的引用赋给父类类型的引用。这是自动进行的,不需要进行任何特殊语法操作。向上转型后,通过父类引用可以访问子类对象中继承自父类的方法和属性,但不能访问子类特有的方法和属性。
优点:
- 向上转型是安全的,因为子类是父类的扩展,子类的对象自然包含了父类的所有属性和方法。
- 向上转型后,可以使用父类类型的引用操作不同的子类对象,体现多态性。
示例:
class Animal {
void eat() {
System.out.println("Animal is eating");
}
}
class Dog extends Animal {
void bark() {
System.out.println("Dog is barking");
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog();
Animal animal = dog; // 向上转型
animal.eat(); // 正确,调用的是 Dog 类的 eat() 方法
// animal.bark(); // 编译错误,Animal 类中不存在 bark 方法
}
}
向下转型
向下转型是指将父类类型的引用转换为子类类型的引用,一般是为了重新获得因为向上转型而丢失的子类特性。这需要进行显式的类型转换,因为不是所有的父类对象都可以转换成为特定的子类对象,存在类型安全的问题。
注意事项:
- 在进行向下转型之前,应该使用 instanceof (它判断的是运行类型)关键字检查引用变量所指向的对象是否是目标类型或其子类型,以避免运行时的
ClassCastException
。 - 向下转型可以访问子类特有的方法和属性。
示例:
Animal animal = new Dog(); // 向上转型
if (animal instanceof Dog) { // 检查 animal 实际引用的对象是否是 Dog 类型
Dog dog = (Dog) animal; // 向下转型
dog.bark(); // 正确,现在可以调用 Dog 类的 bark() 方法
}
- 向上转型使得子类对象可以赋值给父类引用,体现了 Java 的多态性,是安全的操作。
- 向下转型是从父类引用转换到子类引用,需要进行显式转换,并且在转换前应该使用
instanceof
检查,以避免运行时错误。向下转型后可以访问子类特有的成员。