7146 字
36 min
0

封装、继承和多态

面向对象编程的三大核心特性 —— 封装、继承和多态,是构建高效、可维护系统的基石。掌握这些特性,能够帮助开发者设计出更灵活、更易扩展的代码结构。

封装(encapsulation)

  1. 面向对象编程的三大核心特性:封装、继承和多态,是实现代码灵活性与高可维护性的基础。
  2. 封装的核心思想是信息隐藏,即只暴露给用户必须知道的部分,隐藏不需要让用户了解的实现细节。
  3. 在 Java 中,封装通过隐藏对象的属性和实现细节,仅对外提供访问这些属性的公共方法(通常是 gettersetter 方法)来实现。
  4. 封装的优点
    • 提高代码安全性:可以控制对对象属性的访问,避免外部直接修改导致的不一致或非法数据。
    • 提高代码复用性:通过将公共访问方式与实现细节分开,可以使类更加模块化,便于在其他部分重用。
    • 简化使用:使用者无需了解复杂的内部实现,只需要了解如何通过提供的公共方法访问对象。
  5. 封装的原则
    • 隐藏不必要对外提供的内容:例如,隐藏类内部的具体实现和数据,避免暴露内部细节。
    • 通过公共方法访问私有属性:属性一般设为私有(private),并提供公共方法(如 getter 和 setter)来访问或修改它们。这样可以对属性的访问加以控制。

访问控制权限修饰符

在 Java 中,访问控制权限修饰符用于控制类、属性、方法以及构造方法的访问范围,从而决定哪些细节需要封装,哪些细节可以暴露给外部。

修饰符同一个类同一个包子类所有类
private
缺省
protected
public
  1. 类中属性和方法的访问权限修饰符

    Java 中有四种访问权限修饰符来控制属性、方法和构造方法的可见性

    • private:表示私有的,只有当前类内部可以访问,外部无法访问。适用于需要严格封装的属性或方法。
    • 缺省(默认):即没有使用任何修饰符,只有同一个包中的类才能访问,其他包的类无法访问。这种修饰符具有包级别的访问权限。
    • protected:表示保护的,允许同一个包中的类和不同包中的子类访问。通常用于继承相关的场景。
    • public:表示公共的,任何类都可以访问。适用于需要被外部直接访问的属性或方法。
  2. 类的访问权限

    类的访问权限只有两种

    • public:表示该类可以被任何其他类访问。
    • 缺省(默认):如果没有指定访问权限修饰符,则表示该类只有在同一个包内才能被访问。无法在包外访问。

    其他修饰符无法用于顶层类,比如 protectedprivate 都不允许,因为它们没有意义:

    • private 顶层类:如果类本身对外不可见,那么无法被任何类使用,也就无法实例化,这在设计上没有意义。
    • protected 顶层类:protected 修饰符意味着“同包类可访问,跨包子类可访问”。但是对于顶层类来说,跨包类在继承它之前,还不是它的子类,这时无法访问该类本身,因此无法完成继承。所以顶层类不能使用 protected,这个修饰符对它没有意义。

    如果是成员内部类(定义在类里面的类),就可以使用 publicprotectedprivate 和默认访问修饰符,这与普通成员变量/方法类似。


⚠️ 访问权限修饰符不能修饰局部变量。局部变量通常是在方法、构造方法或者代码块中定义的变量,它们只在其所在的作用域内有效。因此,局部变量不需要访问修饰符。

成员变量封装

封装是面向对象编程的一个重要特性,它通过隐藏对象的内部实现细节来提高代码的安全性和可维护性。通过封装,外部类不需要知道对象的内部结构,只需通过公共的接口(如 getset 方法)来与对象交互。

  1. 实现方式
    • 通过 getset 方法,我们可以对对象的属性进行访问和修改,从而避免直接访问属性,提供了更多的灵活性和控制。
  2. 私有属性
    • 通常情况下,类中的属性会使用 private访问权限,这样其他类无法直接访问这些属性,而是通过提供的公共 getset 方法来进行访问。这样做的好处是可以对属性的访问进行更加细致的控制,甚至可以对数据进行验证或处理。
  3. 公共方法
    • 一般来说,getset 方法都是public的,以允许其他类访问和修改类的属性。封装提供了对属性的间接访问,而不允许直接修改数据,从而可以在设置值时进行必要的校验、转换或逻辑判断
    • 💡一些仅在类内部使用的辅助性方法通常会使用 private 修饰符,这样可以避免外部不必要的调用。对于需要外部类访问的方法,才会使用 public 修饰符。

JavaBean 规范

JavaBean 是一个遵循特定规范的 Java 类,主要用于封装数据并支持组件化开发。它有以下要求:

  1. public 类:JavaBean 必须是公共类,允许跨包访问
  2. 无参构造方法:必须提供无参数的公共构造方法,便于框架和反射调用。
  3. 私有属性:所有属性必须声明为 private,强制通过方法访问,保证封装性。
  4. get 和 set方法:为属性提供公共的 getter 和 setter 方法
  5. 实现 Serializable 接口:建议实现 Serializable 接口,支持对象持久化或网络传输。
  6. toString()hashCode()equals()方法:虽然 JavaBean 规范中未强制要求覆盖这些方法,但为了确保对象行为的合理性和可维护性,通常建议按需实现这些方法。

import java.io.Serializable;
 
public class Person implements Serializable {
    private String name;
    private int age;
 
    // 无参构造方法
    public Person() {}
 
    // 有参构造方法
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    // Getter 和 Setter 方法
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
    public int getAge() {
        return age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
 
    // toString 方法
    @Override
    public String toString() {
        return "Person[name=" + name + ", age=" + age + "]";
    }
 
    // equals 和 hashCode 方法
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (obj == null || getClass() != obj.getClass()) return false;
        Person person = (Person) obj;
        return age == person.age && name.equals(person.name);
    }
 
    @Override
    public int hashCode() {
        return 31 * name.hashCode() + age;
    }
}

继承(inheritance)

继承是面向对象编程的重要特性之一,它允许我们创建新类并继承已有类的属性和行为,从而实现代码的复用和扩展。

  1. 继承是指在现有类的基础上创建新类,新类可以继承父类的属性和方法。被继承的类叫做父类(或基类、超类),而继承类叫做子类(或派生类)。通过继承,子类不仅可以拥有父类的属性和方法,还可以根据需要进行扩展或重写。
  2. 在 Java 中,继承通过 extends 关键字来实现。
  3. 如果一个类没有显式地使用 extends 关键字继承父类,则默认继承自 java.lang.Object 类。Object 类是 Java 中所有类的根类,它提供了一些常用的方法,例如 toString()equals()hashCode() 等。
  4. 继承的优势
    • 代码复用:继承可以提高代码的复用性。子类可以继承父类的公共方法和属性,避免重复编写相同的代码。
    • 开发效率:通过继承,开发者可以构建出层次化的类结构,提升开发效率。
    • 多态性支持继承为多态提供了基础,子类可以重写父类的方法,形成不同的行为表现。
    • 多层继承:Java 支持多层继承,即一个类可以继承另一个继承其他类的类,形成继承体系。
  5. 继承的限制
    • 单继承Java 只支持单继承,即一个类只能有一个直接父类。无法实现像 C++ 中那样的多继承。
  6. 无法继承的情况:
    • 父类的private 属性和方法private 成员只在其所在的类内部可见,因此它们对外部类(包括子类)不可访问,无法被继承。
    • 父类的构造方法构造方法不会被继承。即使是子类创建对象时会调用父类的构造方法,但子类并不能直接继承父类的构造方法。但是,子类可以通过 super() 调用父类的构造方法来初始化父类部分。
    • final 修饰的类final 类无法被继承,因为 final 关键字表示该类不能被扩展或修改。如: StringInteger‌ 。
    • 包级私有(默认访问权限)的属性和方法:没有显式的访问修饰符时,类成员的访问权限是包级私有的(即默认访问权限)。这种属性或方法仅对同包中的类可见,跨包的子类无法访问这些成员。
    • ⚠️final 修饰的方法final 方法不能被子类重写(Override)。子类仍然可以继承该方法,但无法修改其实现

方法的重写

方法的重写是子类对父类已有方法的重新定义。在子类中重写父类的方法时,可以改变或增强父类方法的功能。

方法重写需要符合以下要点:

  1. 子类重写的方法,方法名和父类中被重写的方法必须完全相同。包括方法名、参数个数、参数类型、参数顺序等。
  2. 子类重写方法的修饰符权限必须大于等于父类被重写方法的修饰符权限
    • public > protected > default > private
    • 如果父类方法是 privatestaticfinal,则不能在子类中重写。
      • private 方法:对子类不可见,不参与继承,子类同名方法是全新的方法而非重写。
      • static 方法:属于类,不属于实例,子类可以定义同名方法,但这是“隐藏(hiding)”,不是重写。
      • final 方法:明确禁止重写。
  3. 返回值类型
    • 如果父类方法的返回值类型是基本数据类型、void 或字符串类型,子类必须返回相同类型
    • 如果父类方法的返回类型是引用数据类型(不包括字符串),那么子类在重写该方法时,返回类型可以是父类返回类型的子类,这称为协变返回类型。也就是说,子类方法的返回类型要么与父类方法的返回类型相同,要么是父类返回类型的子类
  4. 异常抛出
    • 受检异常(Checked Exception):在子类重写方法时,子类方法可以抛出与父类方法相同的受检异常,或者是父类方法抛出异常的子类型(即继承自父类异常的类型)。但是,子类不能抛出比父类方法更多的受检异常或与父类方法不同的受检异常。
    • 非受检异常(Unchecked Exception):子类方法可以抛出与父类方法无关的非受检异常(即 RuntimeException 类及其子类),即使父类方法没有声明该异常。

⚠️ 注意:

  • 如果子类重写了父类的方法,那么通过子类对象调用该方法时,调用的是子类中的重写方法,而不是父类的方法。这是方法重写的基本特性。
  • 如果子类方法需要继承父类的方法功能,可以使用super关键字来调用父类的方法。super关键字引用的是当前对象的父类对象。这样做可以同时保留父类的功能,并在此基础上扩展或修改子类特有的功能。

方法重写和重载

区别重载重写
英文名OverloadOverride
范围发生在同一个类中发生在继承关系中
定义1. 方法名相同,形参列表不同
2. 对访问权限没有要求
3. 对返回值类型和异常类型没有要求
1. 方法名相同,形参列表相同
2. 访问权限,子类大于等于父类
3. 返回值类型和异常类型,子类小于等于父类
使用目的通过改变方法的参数数量或类型,实现方法的多样性通过子类修改父类的方法实现,来满足子类的需求
返回值类型重载方法可以有不同的返回值类型重写方法的返回值类型要与父类相同,或为其子类型(协变返回类型)
异常重载方法可以抛出不同的异常类型或没有异常重写方法不能抛出父类方法未声明的异常,必须遵守父类的方法签名
访问权限重载方法对访问权限没有要求重写方法的访问权限必须大于等于父类方法的访问权限

super关键字

  1. super可以理解为直接父类对象的引用,或者说super指向子类对象的父类对象存储空间。它用于访问父类的成员。
  2. 可以通过super来访问父类中被子类覆盖的方法或属性super的使用和this关键字非常相似,都是用于引用当前对象和父类对象。
  3. 在子类中不能通过super直接访问父类用private修饰的成员变量和成员方法,因为private修饰的成员只能在该类内部访问,无法通过子类访问。
  4. 子类继承父类,子类的构造方法必须调用父类的构造方法。如果子类的构造方法中没有显式调用父类的构造方法,系统默认调用父类的无参构造方法。如果父类没有无参构造方法,且子类没有显式调用父类的其他构造方法,编译时会报错There is no parameterless constructor available in 'xxx'
  5. 若想指定调用父类中的有参构造函数,则可以通过super(参数列表)指定调用父类中的某个构造函数,而且必须放在构造方法的第一条语句。

super和this调用构造方法总结

  1. super()this() 都必须是构造方法中的第一条语句,因此它们不能同时出现在同一个构造方法中。
  2. super() 保证在子类构造方法访问父类时,父类已被初始化,确保父类的构造方法优先执行。
  3. 如果父类没有无参构造方法,且子类没有显式调用父类的其他构造方法,编译时会报错
  4. this() 用来调用本类的其他构造方法,使得构造代码可以复用。

final关键字

  1. final 关键字在 Java 中有多种用法,它可以修饰类、方法、变量以及局部变量,但是不能修饰构造方法。
  2. 当一个类被 final 修饰时,该类不能被继承,即不能作为其他类的父类。但是,final 类可以继承其他类。
  3. final 修饰的方法不能被子类覆盖(重写)。
  4. final 修饰的变量被称为常量,常量只能赋值一次,一旦初始化后不能改变。
    • 实例变量:如果 final 修饰实例变量,必须在声明时或者构造方法中给它赋值。
    • 静态变量:如果 final 修饰静态变量,必须在声明时或在静态块中进行初始化。
    • 局部变量final 修饰局部变量时,在赋值之后不能再修改。
  5. final 修饰引用类型的变量时,意味着该引用变量指向的对象不可改变,即不能再指向其他对象,但对象的属性或内容仍然可以修改
  6. final 不能修饰构造方法,因为构造方法不是可以被继承的成员。构造方法本身就只能在创建对象时调用,不能被重写。

⚠️ 注意:不能在实例初始化块中对final 修饰的变量进行赋值。实例初始化块(构造代码块)在每次创建对象时执行,而 final 变量有严格的赋值规则:

  1. final 变量可以在声明处或构造方法中赋值
  2. 如果你在实例初始化块中给 final 变量赋值,编译器无法保证构造方法中不会再次赋值
  3. 因此,为了保证 final 变量只赋值一次,Java 不允许在初始化块中赋值已经在构造方法中赋值的 final 变量
class MyClass {
    final int x;
 
    // 实例初始化块:尝试给final变量赋值
    {
        x = 10;  // 错误:final变量不能在实例初始化块中赋值
    }
 
    // 合法的构造方法赋值
    public MyClass(int value) {
        x = value;  // 合法:在构造方法中赋值
    }
}
 

Object类

  1. Object类存储在java.lang包中,是所有 Java 类的终极父类(除了 Object 类本身)。
  2. 如果在类的声明中未显式使用 extends 关键字指定基类,则该类默认继承自 Object 类。

Object类中的方法

Object 类提供了一些基本方法,这些方法通常被继承并在自定义类中进行重写或使用。

  1. public String toString()
  2. public boolean equals(Object obj)
  3. public native int hashCode()
  4. public final native Class<?> getClass()
  5. public final void wait() throws InterruptedException
  6. public final native void wait(long timeout) throws InterruptedException
  7. public final void wait(long timeout, int nanos) throws InterruptedException
  8. public final native void notify()
  9. public final native void notifyAll()

native的说明

  1. native 关键字用于标识一个方法是本地方法,这些方法由 C 或 C++ 等语言编写并链接到 Java 程序中。Java 程序可以通过 JNI(Java Native Interface)与 C、C++ 或其他本地语言交互
  2. 本地方法的实现通常会提供更高的性能,或者实现 Java 无法直接访问的操作系统特性(如直接操作硬件、操作系统底层功能等)。
  3. 使用 native 修饰的方法没有方法体,表示方法的具体实现是由其他编程语言提供的。

toString方法

  1. 功能:返回当前对象的字符串表示。默认实现返回的是对象的类名和哈希码。
  2. 重写建议:为了便于调试和打印对象信息,通常重写该方法,提供对象的有意义的描述。
public class Main {
    public static void main(String[] args) {
        Person p = new Person("Alice", 20);
        System.out.println(p); // 输出:Person{name='Alice', age=20}
    }
}
 
class Person {
    private String name;
    private int age;
 
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + "}";
    }
}
 

equals方法

  1. 功能:比较两个对象是否相等。默认实现是比较内存地址(引用)的相等性
  2. 重写建议:如果希望基于对象内容比较相等性,应重写此方法。
public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("Alice", 20);
        Person p2 = new Person("Alice", 20);
        System.out.println(p1.equals(p2)); // 输出:true
    }
}
 
class Person {
    private String name;
    private int age;
 
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true; 
        if (obj == null || getClass() != obj.getClass()) return false;
        Person other = (Person) obj;
        return age == other.age && name.equals(other.name);
    }
}
 

hashCode方法

  1. 功能:返回对象的哈希码。默认实现通常基于对象的内存地址计算哈希码
  2. 重写建议:如果重写了 equals() 方法,应重写 hashCode(),确保哈希码的一致性。
import java.util.Objects;
 
public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("Alice", 20);
        Person p2 = new Person("Alice", 20);
        System.out.println(p1.hashCode() == p2.hashCode()); // 输出:true
    }
}
 
class Person {
    private String name;
    private int age;
 
    Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
 
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true; 
        if (obj == null || getClass() != obj.getClass()) return false;
        Person other = (Person) obj;
        return age == other.age && name.equals(other.name);
    }
 
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}
 

getClass 方法

  1. 功能返回表示当前对象的 Class 类型的对象。可以用它来动态检查对象的类信息。
public class Main {
    public static void main(String[] args) {
        Person p = new Person();
        Class<?> clazz = p.getClass();
        System.out.println(clazz.getName()); // 输出:Person
    }
}
 
class Person {}
 

多态(Polymorphism)

  1. 多态是面向对象编程中的一个核心概念,它允许同一方法根据不同对象表现出不同的行为。具体来说,多态指的是同一个方法调用,不同对象可能产生不同的行为。通过多态,可以减少代码的重复,提高代码的灵活性和可扩展性。

  2. 多态的实现需要满足以下三个条件:

    • 继承:子类必须继承父类,继承是多态的前提
    • 方法重写:子类必须重写父类的方法。
    • 父类引用指向子类对象:父类的引用变量指向子类对象,才能实现多态。
  3. 多态的使用场合:

    • 父类作为方法的形参:你可以将父类作为方法的参数类型,实参可以传递任意的子类对象,从而使用多态。

      void process(Animal a) { a.sound(); }
    • 父类作为方法的返回值类型:方法可以返回父类类型的引用,实际返回的对象可以是任意子类的实例。

      Animal getAnimal() { return new Dog(); }
  4. 多态的优势

    • 提高代码复用性:通过多态,可以使用相同的接口(方法),在不同的类中实现不同的功能,从而提高代码的复用性。
    • 灵活的代码扩展:增加新的子类时,不需要修改现有的代码,只需要继承父类并重写方法即可,符合开闭原则。
    • 简化代码:通过父类引用指向子类对象,可以减少代码中的条件判断和冗余代码,使代码更加简洁和清晰。
  5. 多态的实际应用

    • 多态在很多实际项目中非常有用,尤其是在设计框架或构建具有扩展性的系统时。例如,常见的策略模式、工厂模式、观察者模式等设计模式,都会用到多态。

引用数据类型转换

引用数据类型的类型转换主要包括向上转型和向下转型

向上转型

  1. 父类引用指向子类对象,属于自动类型转换。
  2. 格式:父类类型 变量名 = 子类对象;
  3. 优点:隐藏了子类类型,提高了代码的扩展性,多态本身就是向上转型的过程。
  4. 缺点:只能使用父类共性的内容,不能调用子类特有的方法。
public class Main {
    public static void main(String[] args) {
        Animal a = new Dog(); // 向上转型
        a.sound();            // 输出: Dog barks
        // a.fetch();         // 编译错误,父类引用不能调用子类特有方法
    }
}
class Animal {
    void sound() { System.out.println("Animal sound"); }
}
class Dog extends Animal {
    void sound() { System.out.println("Dog barks"); }
    void fetch() { System.out.println("Dog fetches ball"); }
}
 

向下转型

  1. 子类引用指向父类对象,属于强制类型转换
  2. 格式:子类类型 变量名 = (子类类型) 父类对象;
  3. 优点:向下转型之后,可以调用子类特有的方法。
  4. 缺点:向下转型有风险,容易发生ClassCastException异常。
public class Main {
    public static void main(String[] args) {
        // 向上转型
        Animal a = new Dog();
        a.sound();           // 输出: Dog barks
        // a.fetch();        // ❌ 编译错误,父类引用不能调用子类特有方法
 
        // 安全向下转型
        Dog d = (Dog) a;
        d.fetch();           // 输出: Dog fetches ball
 
        // 不安全向下转型
        Animal b = new Animal();
        try {
            Dog d2 = (Dog) b;   // ❌ 运行时异常: ClassCastException
            d2.fetch();
        } catch (ClassCastException e) {
            System.out.println("向下转型失败: " + e);
        }
    }
}
 
class Animal {
    void sound() {
        System.out.println("Animal sound");
    }
}
 
class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Dog barks");
    }
 
    void fetch() {
        System.out.println("Dog fetches ball");
    }
}
 

instanceof运算符

instanceof 运算符用于在运行时判断对象是否是特定类的一个实例。

  • 返回值:boolean
  • 判断逻辑:对象是否属于指定类或其子类的实例。

编译阶段行为

  • 右边的类或接口必须是左边对象的父类、本身类或子类,否则编译失败。
public class Main {
    public static void main(String[] args) {
        Child child = new Child();
 
        // instanceof 判断
        boolean b = child instanceof GrandSon; // false,child 不是 GrandSon 的实例
        boolean b1 = child instanceof Child;  // true
        boolean b2 = child instanceof Parent; // true
 
        System.out.println("child instanceof GrandSon: " + b);
        System.out.println("child instanceof Child: " + b1);
        System.out.println("child instanceof Parent: " + b2);
 
        // ❌ 不兼容类型,编译错误
        // boolean b3 = child instanceof String;
    }
}
 
class Parent {}
class Child extends Parent {}
class GrandSon extends Child {}

运行阶段行为

  • 对象为 nullinstanceof 返回 false
  • 对象非 null
    • 如果右边是对象本身类或父类 → 返回 true
    • 如果右边是对象的子类或兄弟类 → 返回 false
public class InstanceofDemo {
    public static void main(String[] args) {
        Animal animal = new Dog();
 
        if (animal instanceof Animal) { // true
            Animal an = (Animal) animal;
            an.eat();
        }
 
        if (animal instanceof Dog) { // true
            Dog dog = (Dog) animal;
            dog.lookHome();
        }
 
        if (animal instanceof ShepherdDog) { // false
            ShepherdDog shepherdDog = (ShepherdDog) animal;
            shepherdDog.introduction();
        }
 
        if (animal instanceof Cat) { // false
            Cat cat = (Cat) animal;
            cat.catchMouse();
        }
 
        // boolean flag = animal instanceof String; // 编译失败
    }
}
 
class Animal {
    public void eat() { System.out.println("Animal eat ..."); }
}
 
class Dog extends Animal {
    public void lookHome() { System.out.println("Dog look home ..."); }
}
 
class ShepherdDog extends Dog {
    public void introduction() { System.out.println("Shepherd dog introduction ..."); }
}
 
class Cat extends Animal {
    public void catchMouse() { System.out.println("Cat catch mouse ..."); }
}
 

多态中成员变量的特点

  1. 无论编译还是运行,访问的都是 等号左边的引用类型所属的类 的变量。
  2. 注意:成员变量不会发生多态,始终使用 引用类型的变量
public class Main {
    public static void main(String[] args) {
        Parent p = new Child();
        System.out.println(p.name); // 输出: Parent
    }
}
 
class Parent {
    String name = "Parent";
}
 
class Child extends Parent {
    String name = "Child";
}
 
 

多态中成员方法的特点

  1. 编译阶段:编译器只检查左侧引用类型中是否声明了该方法;如果没有,则编译报错。
  2. 运行阶段:实际调用的是右侧对象所属类中重写后的方法(动态绑定 / 动态分派)。
public class Main {
    public static void main(String[] args) {
        Parent p = new Child();
        p.show(); // 输出: Child show
    }
}
 
class Parent {
    void show() { System.out.println("Parent show"); }
}
 
class Child extends Parent {
    @Override
    void show() { System.out.println("Child show"); }
}
 

相关文章

评论区