MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Java封装、继承与多态详解

2023-01-124.8k 阅读

Java 封装

封装的概念

在 Java 中,封装是一种将数据和操作数据的方法捆绑在一起,对外部隐藏对象内部细节的机制。简单来说,就是把对象的属性和方法包装起来,只对外提供一些必要的接口来访问和操作这些属性和方法。这种方式提高了代码的安全性和可维护性。

例如,我们有一个 Person 类,它包含了 nameage 等属性。如果不进行封装,外部代码可以随意修改这些属性,可能会导致数据的不一致或不符合业务逻辑。通过封装,我们可以控制对这些属性的访问,只允许在符合业务规则的情况下进行修改。

访问修饰符

  1. private
    • private 是最严格的访问修饰符。被 private 修饰的属性或方法只能在本类内部被访问。
    • 示例代码:
public class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 提供公共的方法来获取和设置 name 属性
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    // 提供公共的方法来获取和设置 age 属性
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        if (age >= 0 && age <= 120) {
            this.age = age;
        } else {
            System.out.println("年龄不合法");
        }
    }
}
  • 在上述代码中,nameage 属性被声明为 private,外部代码不能直接访问。只能通过 getNamesetNamegetAgesetAge 这些公共方法来访问和修改属性。setAge 方法中还增加了对年龄合法性的检查,这体现了封装的优势,能够对数据进行有效的控制。
  1. default(默认,不写修饰符)
    • 具有默认访问修饰符的属性或方法可以被同一个包内的其他类访问。如果一个类没有指定包名,它被认为是在默认包中。
    • 示例代码:
package com.example.demo;

class Animal {
    String species;

    void showSpecies() {
        System.out.println("物种是:" + species);
    }
}

class Dog extends Animal {
    public Dog() {
        species = "狗";
    }
}
  • 在这个例子中,Animal 类中的 species 属性和 showSpecies 方法都没有显式的访问修饰符,属于默认访问级别。Dog 类与 Animal 类在同一个包 com.example.demo 中,所以 Dog 类可以访问 Animal 类的 species 属性并进行赋值。
  1. protected
    • protected 修饰的属性或方法可以被同一个包内的其他类以及不同包中的子类访问。
    • 示例代码:
package com.example.base;

public class Plant {
    protected String name;

    protected void grow() {
        System.out.println(name + "正在生长");
    }
}

package com.example.sub;

import com.example.base.Plant;

public class Flower extends Plant {
    public Flower(String name) {
        this.name = name;
    }

    @Override
    public void grow() {
        System.out.println(name + "作为花,正绚丽生长");
    }
}
  • 在上述代码中,Plant 类在 com.example.base 包中,Flower 类在 com.example.sub 包中并且继承自 Plant 类。Plant 类的 name 属性和 grow 方法被声明为 protectedFlower 类可以访问并使用它们。
  1. public
    • public 是最宽松的访问修饰符。被 public 修饰的属性或方法可以被任何类访问,无论在哪个包中。
    • 示例代码:
public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}
  • Calculator 类中的 add 方法是 public 的,任何其他类只要能访问到 Calculator 类的实例,就可以调用 add 方法。

封装的优点

  1. 数据安全性
    • 通过限制对属性的直接访问,只允许通过特定的方法进行修改,可以确保数据的完整性和一致性。例如在前面 Person 类的 setAge 方法中,对年龄进行了合法性检查,防止非法数据的设置。
  2. 可维护性
    • 封装使得类的内部实现细节对外部隐藏。如果类的内部实现需要修改,只要保持对外接口不变,其他依赖该类的代码就不需要修改。比如,Person 类中如果修改了 name 属性的存储方式,只要 getNamesetName 方法的接口不变,外部调用代码不受影响。
  3. 代码复用性
    • 封装良好的类可以在不同的项目或模块中复用。其他开发者只需要了解类提供的公共接口,而不需要关心内部实现细节,就可以轻松使用该类。例如,Calculator 类的 add 方法可以在各种需要进行加法运算的场景中复用。

Java 继承

继承的概念

继承是 Java 面向对象编程中的一个重要特性,它允许一个类(子类)从另一个类(父类)获取属性和方法。子类可以继承父类的非 private 属性和方法,并且可以根据自身需求进行扩展和重写。通过继承,代码的复用性得到了极大提高,同时也便于进行代码的维护和扩展。

例如,我们有一个 Vehicle 类作为父类,它包含了一些通用的属性和方法,如 speed(速度)和 start(启动)方法。Car 类和 Bicycle 类可以作为子类继承 Vehicle 类,它们继承了 speed 属性和 start 方法,同时还可以有自己特有的属性和方法,如 Car 类可以有 numOfDoors(车门数量)属性,Bicycle 类可以有 numOfGears(挡位数量)属性。

继承的语法

在 Java 中,使用 extends 关键字来实现继承。语法如下:

class ParentClass {
    // 父类的属性和方法
}

class ChildClass extends ParentClass {
    // 子类新增的属性和方法
}

例如:

class Animal {
    String name;

    void eat() {
        System.out.println(name + "正在吃东西");
    }
}

class Dog extends Animal {
    String breed;

    void bark() {
        System.out.println(name + "正在汪汪叫,品种是" + breed);
    }
}

在上述代码中,Dog 类继承自 Animal 类。Dog 类拥有 Animal 类的 name 属性和 eat 方法,同时又定义了自己的 breed 属性和 bark 方法。

构造方法与继承

  1. 子类构造方法调用父类构造方法
    • 子类的构造方法在执行时,会默认先调用父类的无参构造方法。如果父类没有无参构造方法,子类构造方法必须显式调用父类的有参构造方法。
    • 示例代码:
class Shape {
    int side;

    Shape(int side) {
        this.side = side;
    }
}

class Square extends Shape {
    Square(int side) {
        super(side);
    }

    void printInfo() {
        System.out.println("正方形边长为:" + side);
    }
}
  • Square 类的构造方法中,通过 super(side) 显式调用了 Shape 类的有参构造方法。如果不这样做,编译器会报错,因为 Shape 类没有无参构造方法。
  1. super 关键字
    • super 关键字用于在子类中访问父类的属性和方法。除了在构造方法中调用父类构造方法外,还可以在子类方法中访问父类被重写的方法。
    • 示例代码:
class Animal {
    void sound() {
        System.out.println("动物发出声音");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        super.sound();
        System.out.println("狗汪汪叫");
    }
}
  • Dog 类的 sound 方法中,通过 super.sound() 调用了父类 Animalsound 方法,然后再输出子类特有的声音描述。

方法重写

  1. 重写的概念
    • 当子类继承父类后,子类可以根据自身需求重新实现父类的方法,这就是方法重写。重写的方法必须与父类被重写方法具有相同的方法名、参数列表和返回类型(返回类型可以是父类返回类型的子类,这称为协变返回类型)。
  2. 重写的规则
    • 访问修饰符:子类重写方法的访问修饰符不能比父类被重写方法的访问修饰符更严格。例如,如果父类方法是 protected,子类重写方法可以是 protectedpublic,但不能是 private
    • 异常抛出:子类重写方法不能抛出比父类被重写方法更多的异常,或者抛出比父类被重写方法更宽泛的异常类型。例如,如果父类方法抛出 IOException,子类重写方法可以抛出 IOException 或者 IOException 的子类异常,但不能抛出 SQLException 等无关异常。
  3. 示例代码
class Shape {
    double area() {
        return 0.0;
    }
}

class Circle extends Shape {
    double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double area() {
        return Math.PI * radius * radius;
    }
}
  • 在上述代码中,Circle 类继承自 Shape 类,并重写了 area 方法。Circle 类根据圆的面积计算公式重新实现了 area 方法,以满足自身需求。

继承的优缺点

  1. 优点
    • 代码复用:子类可以复用父类的属性和方法,减少了重复代码的编写。例如多个具体的动物类继承自 Animal 类,复用了 Animal 类的 eat 方法。
    • 便于扩展:在不修改父类代码的情况下,子类可以添加新的属性和方法,或者重写父类方法来实现新的功能。比如 Dog 类继承 Animal 类后,添加了 bark 方法。
  2. 缺点
    • 继承层次过深导致维护困难:如果继承层次过于复杂,一个父类的修改可能会影响到多个子类,增加了代码维护的难度。例如,如果修改了 Animal 类的某个方法签名,所有继承自 Animal 类且重写了该方法的子类都需要相应修改。
    • 打破封装性:虽然继承在一定程度上提高了代码复用,但也可能破坏父类的封装性。因为子类可以访问父类的非 private 属性和方法,如果子类随意修改父类的属性,可能会导致父类的内部状态不一致。

Java 多态

多态的概念

多态是 Java 面向对象编程的核心特性之一,它允许同一个引用类型在不同的情况下表现出不同的行为。简单来说,就是“一个接口,多种实现”。多态通过继承和方法重写来实现,使得程序在运行时能够根据对象的实际类型来决定调用哪个方法。

例如,我们有一个 Animal 类,Dog 类和 Cat 类继承自 Animal 类,并且都重写了 Animal 类的 sound 方法。当我们使用 Animal 类型的引用指向 DogCat 对象时,调用 sound 方法会根据对象的实际类型(DogCat)来执行相应的 sound 方法实现,这就是多态的体现。

多态的实现方式

  1. 基于继承的多态
    • 当子类继承父类并重写父类的方法时,就可以实现多态。通过父类类型的引用指向子类对象,调用重写的方法时会执行子类的实现。
    • 示例代码:
class Animal {
    void sound() {
        System.out.println("动物发出声音");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("狗汪汪叫");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("猫喵喵叫");
    }
}

public class PolymorphismDemo {
    public static void main(String[] args) {
        Animal animal1 = new Dog();
        Animal animal2 = new Cat();

        animal1.sound();
        animal2.sound();
    }
}
  • PolymorphismDemo 类的 main 方法中,animal1Animal 类型的引用,但指向了 Dog 对象,animal2Animal 类型的引用,但指向了 Cat 对象。当调用 sound 方法时,会根据对象的实际类型分别执行 Dog 类和 Cat 类的 sound 方法,输出“狗汪汪叫”和“猫喵喵叫”。
  1. 基于接口的多态
    • 一个类实现一个或多个接口,并实现接口中定义的方法。通过接口类型的引用指向实现该接口的类的对象,调用接口方法时会执行具体实现类的方法。
    • 示例代码:
interface Shape {
    double area();
}

class Circle implements Shape {
    double radius;

    Circle(double radius) {
        this.radius = radius;
    }

    @Override
    public double area() {
        return Math.PI * radius * radius;
    }
}

class Rectangle implements Shape {
    double width;
    double height;

    Rectangle(double width, double height) {
        this.width = width;
        this.height = height;
    }

    @Override
    public double area() {
        return width * height;
    }
}

public class InterfacePolymorphismDemo {
    public static void main(String[] args) {
        Shape shape1 = new Circle(5);
        Shape shape2 = new Rectangle(4, 6);

        System.out.println("圆形面积:" + shape1.area());
        System.out.println("矩形面积:" + shape2.area());
    }
}
  • InterfacePolymorphismDemo 类中,Shape 是一个接口,CircleRectangle 类实现了 Shape 接口。shape1shape2Shape 类型的引用,分别指向 CircleRectangle 对象。调用 area 方法时,会根据对象的实际类型执行相应的 area 方法实现,输出圆形和矩形的面积。

编译时多态与运行时多态

  1. 编译时多态(方法重载)
    • 编译时多态是通过方法重载实现的。方法重载是指在同一个类中,多个方法具有相同的方法名,但参数列表不同(参数个数、参数类型或参数顺序不同)。编译器在编译时根据调用方法的参数列表来决定调用哪个方法。
    • 示例代码:
class Calculator {
    int add(int a, int b) {
        return a + b;
    }

    double add(double a, double b) {
        return a + b;
    }

    int add(int a, int b, int c) {
        return a + b + c;
    }
}
  • Calculator 类中,有三个 add 方法,它们的参数列表不同。当调用 add 方法时,编译器会根据传入的参数类型和个数来确定调用哪个 add 方法。例如,Calculator cal = new Calculator(); int result1 = cal.add(2, 3); 会调用第一个 add 方法,而 double result2 = cal.add(2.5, 3.5); 会调用第二个 add 方法。
  1. 运行时多态(方法重写)
    • 运行时多态是通过方法重写和对象的动态绑定实现的。在运行时,Java 虚拟机(JVM)根据对象的实际类型来决定调用哪个重写的方法。这是基于继承和向上转型的机制。
    • 例如前面基于继承的多态示例中,Animal 类型的引用在运行时根据实际指向的 DogCat 对象来调用相应的 sound 方法,这就是运行时多态的体现。

多态的优点

  1. 提高代码的可维护性和可扩展性:当需要添加新的子类时,只需要让新子类继承父类或实现接口,并实现相应的方法,而不需要修改其他已有的代码。例如,如果要添加一个 Bird 类继承自 Animal 类,只需要在 Bird 类中重写 sound 方法,其他使用 Animal 类多态特性的代码不需要修改。
  2. 增强代码的灵活性:多态使得程序可以根据对象的实际类型来执行不同的行为,这在很多复杂的业务场景中非常有用。比如在图形绘制程序中,通过 Shape 接口和不同的实现类(如 CircleRectangle 等),可以方便地实现不同图形的绘制逻辑,并且可以很容易地扩展新的图形类型。
  3. 提高代码的复用性:父类或接口中定义的通用方法可以被多个子类复用,同时子类又可以根据自身需求重写这些方法,实现个性化的行为。例如,Animal 类的 eat 方法可以被各种动物子类复用,而不同动物子类可以重写 eat 方法来表现出不同的进食行为。