第六章 面向对象编程------进阶

【本章内容】

  • 面向对象的三个特征------封装、继承、多态
  • 类的另外两个成员------代码块、内部类

一、面向对象的基本特征------封装性

封装性是面向对象编程(OOP)的三大基本特征之一。其核心思想在于保护和隐藏对象内部的状态和实现细节,仅对外暴露有限的、受控的访问接口。

1. 核心设计原则:高内聚,低耦合

在讨论封装性之前,必须理解其背后的核心软件工程原则:高内聚与低耦合。

  • 高内聚 (High Cohesion) :指一个模块(在 Java 中通常指一个类)内部各个元素(属性、方法)之间结合的紧密程度。高内聚意味着一个类应该高度专注于完成一项独立的任务,其内部的方法和数据都应服务于这个核心任务。
  • 低耦合 (Low Coupling) :指不同模块(类)之间的相互依赖程度。低耦合意味着应最大限度地减少类与类之间的关联,避免一个类的改动引发其他关联类的大量修改,即所谓的"牵一发而动全身"。

封装正是实现"高内聚、低耦合"这一目标的关键手段。

  • 封装如何体现高内聚:将类的属性和实现细节隐藏在内部,由类自身负责管理和维护,形成一个功能高度集中的整体。
  • 封装如何实现低耦合:通过对外提供稳定的公共方法(public methods),隐藏内部复杂的实现逻辑。当内部实现变更时,只要公共接口不变,调用方就无需改动,从而降低了类之间的依赖关系。

2. 封装性的定义与优势

(1) 定义

封装性指将对象的状态信息(即属性)隐藏在对象内部,禁止外部程序直接访问。外部世界只能通过该类预先定义好的方法来操作或访问其内部数据。

通俗地讲,就是"把该隐藏的隐藏起来,把该暴露的暴露出来"。

(2) 优势

  • 增强安全性:防止外部代码随意篡改对象内部状态,保证数据的完整性和安全性。
  • 提高可维护性:当一个类的内部实现需要修改时,只要其对外暴露的接口保持不变,就不会影响到其他调用该类的代码,降低了维护成本。
  • 简化调用:调用者只需关心类提供的公共方法,无需了解其复杂的内部实现细节。
  • 实现逻辑控制 :可以在暴露的方法(如 setter)中加入数据校验和逻辑判断,确保赋给属性的值是合法有效的。

3. 实现机制:访问权限修饰符

Java 通过访问权限修饰符来控制类、属性、方法和构造器的可见性范围,从而实现封装。

(1) 四种访问权限

Java 提供了四种访问控制级别,其权限范围由小到大排列如下:

private​ < default (缺省)​ < protected​ < public

  • private(私有的)

    • 适用范围:只能修饰类的成员(属性、方法、构造器、内部类)。
    • 可见性:仅在当前类的内部可见。这是最严格的访问级别,是实现封装的关键。
  • default/ package-private(缺省/包私有)

    • 适用范围:可修饰类及其成员。当不写任何修饰符时,即为此权限。
    • 可见性:在当前类的内部,以及同一个包(package)下的其他类中可见。
  • protected(受保护的)

    • 适用范围:只能修饰类的成员。
    • 可见性 :除了具备 default 的所有可见性外,还对其他包中的子类可见。
  • public(公共的)

    • 适用范围:可修饰类及其成员。
    • 可见性:对所有类都可见,无论是否在同一个包中。这是最宽松的访问级别。

(2) 访问控制级别速查表

修饰符 本类内部 同一个包 其他包的子类 其他包的非子类
private × × ×
default × ×
protected ×
public

(3) 注意事项

  • 对于顶层类(非内部类),只能使用 publicdefault 修饰符。
  • 类的成员(属性、方法、构造器等)可以使用全部四种修饰符。

4 . 属性私有化与 Getter/Setter 方法

在实际开发中,封装最常见的应用模式就是:使用 private修饰属性,并提供 public Getter Setter方法作为统一的访问入口

(1) 实现步骤

  1. 私有化成员变量 :使用 private​ 关键字修饰类的属性。

    java 复制代码
    public class Person {
        private String name;
        private int age;
        private boolean married; // 变量名建议使用全称
    }
  2. 提供公共的 Getter/Setter 方法:为每个私有属性提供一对公共方法,用于读取和设置属性值。

    java 复制代码
    public class Person {
        private String name;
        private int age;
        private boolean married;
    
        // name 属性的 Setter 和 Getter
        public void setName(String n) {
            // 可以在这里添加逻辑控制,如:n不能为空
            name = n;
        }
        public String getName() {
            return name;
        }
    
        // age 属性的 Setter 和 Getter
        public void setAge(int a) {
            // 可以在这里添加数据校验,如:年龄必须在0-120之间
            if (a > 0 && a < 120) {
                age = a;
            } else {
                System.out.println("输入的年龄不合法!");
            }
        }
        public int getAge() {
            return age;
        }
    
        // boolean 类型属性的 Setter 和 is-er
        public void setMarried(boolean m) {
            married = m;
        }
        public boolean isMarried() { // 注意:boolean类型的getter通常命名为 isXxx()
            return married;
        }
    }
  3. 通过方法进行访问 :在其他类中,通过调用 Getter/Setter​ 方法来操作 Person​ 对象的属性。

    java 复制代码
    public class PersonTest {
        public static void main(String[] args) {
            Person p = new Person();
    
            // 必须通过公共方法来设置和获取属性值
            p.setName("张三");
            p.setAge(25);
            p.setMarried(true);
    
            System.out.println("姓名: " + p.getName());
            System.out.println("年龄: " + p.getAge());
            System.out.println("已婚: " + p.isMarried());
    
            /*
             * 以下直接访问属性的方式是错误的,因为属性是 private 的
             * p.name = "李四"; // 编译错误
             * System.out.println(p.age); // 编译错误
             */
        }
    }

(2) 关键点与命名规范

  1. 命名约定

    • Getter: getXxx()
    • Setter: setXxx()
    • 对于 boolean 类型的属性 xxx,其 Getter 方法通常命名为 isXxx()
  2. final属性 :如果一个属性被 final​ 修饰,它通常只在构造器中被初始化,并且不提供 Setter​ 方法,因为它一旦赋值就不能再被修改。

  3. static属性 :静态属性的 Getter/Setter​ 方法也必须是静态的 (static​)。

  4. static final常量 :对于公共常量(static final​),习惯上使用 public​ 修饰,并直接通过 类名.常量名​ 访问,无需 Getter​ 方法。例如 public static final int MAX_AGE = 120;​。

(3) 常见问题解答 (FAQ)

  1. 问:既然构造器可以赋值,为什么还需要 Setter方法?

    :构造器主要用于对象创建时的初始化 。而 Setter​ 方法提供了在对象生命周期中修改 属性值的能力。如果使用无参构造器创建对象,后续就需要依赖 Setter​ 来为属性赋值。

  2. 问:是不是所有属性都必须私有化并提供 Getter/Setter

    :不是绝对的。这是一种被广泛遵循的最佳实践,但并非强制规则。在某些特定场景下(例如,一个类的设计目的就是作为纯粹的数据载体,且不关心数据如何被修改),可能会有例外。然而,对于绝大多数业务类,遵循这一模式是保证代码健壮性和可维护性的基石。通过学习和分析优秀的开源项目源码,可以更深刻地理解何时遵循以及何时可以变通。

2. 标准化封装模式:JavaBean

JavaBean 是一种遵循特定规范的、可重用的 Java 类组件,它广泛应用于各种 Java 框架中(如 Spring、MyBatis 等)。封装是 JavaBean 规范的核心。

JavaBean 的核心规范:

  1. 公共类 :类必须是 public 的。
  2. 无参构造器 :必须提供一个 public 的无参数构造器。这是为了让框架能够通过反射方便地实例化对象。
  3. 属性私有化 :所有属性都必须是 private 的。
  4. 公共 Getter/Setter :为每个私有属性提供 publicGetterSetter 方法,并遵循标准命名规范。

【示例代码】 一个标准的 JavaBean Employee​ 类:

java 复制代码
// 1. 类是公共的
public class Employee { 
    
    // 3. 属性私有化
    private int id;
    private String name;
    private int age;
  
    // 2. 提供公共的无参构造器
    public Employee() {}
  
    // 4. 为每个属性提供公共的 Getter 和 Setter 方法
    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }
  
    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;
    }
}

二、面向对象的基本特征------继承性

继承是 Java 面向对象编程的三大基本特征之一,它允许一个类(子类)获取另一个类(父类)的属性和方法。这极大地促进了代码的复用,并为多态性的实现提供了基础。

1. 什么是继承?

在 Java 中,继承是类与类之间的一种关系。被继承的类称为 父类 (Superclass) 或基类,继承父类的类称为 子类 (Subclass) 或派生类。

  • 核心思想:子类是对父类的扩展和具体化。父类定义了所有子类共有的通用属性和行为,而子类可以在此基础上添加自己独有的特性。
  • is-a 关系 :继承体现了 "is-a" 的关系。例如,"狗" 是一个 "动物",那么 Dog 类就可以继承 Animal 类。父类通常比子类更通用,概念范围更广。

2. 为什么使用继承?

  1. 提高代码复用性:将多个类的共同属性和方法抽取到父类中,子类只需继承即可,减少了代码冗余。
  2. 便于功能扩展:子类可以在不修改父类代码的前提下,增加新的功能或重写父类的方法,以适应新的需求。
  3. 为多态提供前提:继承是实现多态性的必要条件。没有继承,就没有方法重写和父类引用指向子类对象。

注意:继承是一种强耦合关系。不要仅仅为了复用某个类的某个方法而随意使用继承。

3. 何时使用继承?

在程序设计中,我们可以从两个角度来思考继承的应用:

  • 自上而下:功能扩展

    当已存在一个通用类(如 Person​),需要创建一个更具体的类(如 Student​)时,可以让 Student​ 继承 Person​,并在其基础上添加新的属性(如 school​)和方法。

  • 自下而上:代码抽象

    当发现多个类(如 Dog​ 类和 Cat​ 类)中存在大量相同的属性(如 name​, age​)和行为(如 eat()​)时,应将这些共性内容抽取出来,封装成一个更抽象的父类(如 Animal​),然后让 Dog​ 和 Cat​ 类继承它。

4. 继承的实现与语法

Java 使用 extends​ 关键字来实现类之间的继承。

基本语法格式:

java 复制代码
// 父类定义
[修饰符] class SuperClass {
    // 属性和方法
}

// 子类通过 extends 关键字继承父类
[修饰符] class SubClass extends SuperClass {
    // 子类扩展的属性和方法
}

代码示例:Animal 与 Cat

1. 父类 Animal

java 复制代码
/*
 * 定义动物类 Animal,作为父类
 */
public class Animal {
    String name;
    int age;

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

2. 子类 Cat

java 复制代码
/*
 * 定义猫类 Cat,继承自动物类 Animal
 */
public class Cat extends Animal {
    // 子类扩展的属性
    int catchCount;

    // 子类扩展的方法
    public void catchMouse() {
        catchCount++;
        System.out.println(name + "抓到了一只老鼠,这是第" + catchCount + "只。");
    }
}

3. 测试类 TestCat

java 复制代码
public class TestCat {
    public static void main(String[] args) {
        // 1. 创建子类对象
        Cat tom = new Cat();

        // 2. 访问继承自父类的属性并赋值
        tom.name = "汤姆";
        tom.age = 2;

        // 3. 调用继承自父类的方法
        tom.eat();

        // 4. 调用子类自己的方法
        tom.catchMouse();
        tom.catchMouse();
    }
}

4. 运行结果

bash 复制代码
2岁的汤姆正在吃东西
汤姆抓到了一只老鼠,这是第1只。
汤姆抓到了一只老鼠,这是第2只。

5. 继承的核心规则与特性

(1)继承的层次结构

  1. 单继承性 :Java 不支持多重继承,即 一个类只能有一个直接父类。这避免了多个父类中存在同名方法时产生的调用歧义问题。

    java 复制代码
    class C extends A, B {} // 编译错误!
  2. 多层继承:Java 支持多层继承(继承体系),一个子类可以继承其父类,其父类也可以继承另一个父类,形成一条继承链。

    java 复制代码
    class A {}
    class B extends A {} // B 继承 A
    class C extends B {} // C 继承 B,间接继承 A
  3. 根父类 Object ​:在 Java 中,如果一个类没有显式地使用 extends​ 关键字指定父类,那么它将 默认继承 java.lang.Object 。因此,Object​ 类是所有类的最终父类(根父类)。

(2)成员的继承

从概念上讲,子类会继承其父类的 所有成员 ,包括属性(实例变量、静态变量)和方法(实例方法、静态方法)。然而,根据成员的修饰符(如 private​ 和 static​),继承后的可见性和行为会有所不同。

特殊情况一:private​ 成员
  • 结论 :父类的 private​ 成员 能被继承,但不能被直接访问

  • 深入理解

    1. 内存层面 :当创建一个子类对象时,JVM 确实会在内存中为父类的 private 属性分配空间。从这个角度看,子类拥有了父类的私有成员。
    2. 访问层面 :由于 private 访问权限的限制,这些成员被封装在父类内部,对子类是不可见的。因此,子类的代码无法通过成员名直接调用或修改它们,这正是封装原则的体现。
    3. 解决方案 :子类必须通过继承来的、父类提供的 publicprotected 方法(如 get/set 方法)来间接、安全地操作这些私有成员。
特殊情况二:static​ 成员
  • 结论 :父类的 static ​成员可以被子类直接访问,但它们并不像实例成员那样被"继承"

  • 深入理解

    1. 归属不同:静态成员属于类本身,而非类的某个具体实例。它们在类加载时就存在了。

    2. 隐藏(Hiding)而非重写(Overriding) :当子类中定义了一个与父类签名完全相同(同名、同参数列表)的静态方法时,父类的静态方法会被"隐藏"起来,而不是"重写"。

    3. 行为差异

      • 重写(Override) 是运行时行为,调用哪个方法取决于对象的 实际类型

      • 隐藏(Hiding) 是编译时行为,调用哪个方法取决于引用的 声明类型

        java 复制代码
        class Animal {
            public static void printInfo() {
                System.out.println("这是一个静态的动物。");
            }
        }
        
        class Cat extends Animal {
            // 这不是重写,而是隐藏了父类的同名静态方法
            public static void printInfo() {
                System.out.println("这是一只静态的猫。");
            }
        }
        
        public class TestHiding {
            public static void main(String[] args) {
                // 通过声明类型调用,结果符合直觉
                Animal.printInfo(); // 输出: 这是一个静态的动物。
                Cat.printInfo();    // 输出: 这是一只静态的猫。
        
                // 重点:通过对象引用调用,结果取决于引用的"声明类型"
                Animal animalRef = new Cat();
                animalRef.printInfo(); // 输出: 这是一个静态的动物。 (因为引用 animalRef 的类型是 Animal)
            }
        }

(3)继承状态下构造器的调用规则【重点】

  1. 规则一:构造器不可被继承

    构造器无法被子类继承,这是由其本质决定的:

    1. 从语法上,构造器的名称必须与它所在的类名完全相同。因此,父类的构造器在子类中无法成为一个合法的构造器。
    2. 从功能上,构造器的使命是初始化当前类的对象。父类的构造器只负责初始化从父类继承来的那部分状态,它无法、也不应该知道如何初始化子类自己新增的状态。因此,每个类都必须有自己专属的构造器。
  2. 规则二:子类构造器必须调用父类构造器

    创建子类对象时,必须先初始化从父类继承来的部分。因此,子类每个构造器的第一行都必须是隐式或显式地调用父类构造器的语句

    子类虽然从父类中继承了属性,但是如何初始化应该由父类的构造器负责,子类只需也必须调用父类构造器。这里需要特别注意的是,此处虽然调用了父类的构造器,但是并没有创建父类的对象。

  3. 隐式调用:super()

    默认情况下,子类中所有的构造器都会调用父类的空参构造器。Java 编译器会自动在第一行插入一个无参的 super()​ 调用。

    Java 提供 super ​关键字来引用父类的成员。在这里,super()特指调用父类的构造器。super ​关键字将在之后学习。

    【示例:隐式调用父类无参构造器】

    java 复制代码
    // 父类
    public class Person {
        Person() { System.out.println("执行 Person 无参构造器..."); }
    }
    // 子类
    public class Student extends Person {
        public Student() {
            // 此处隐藏了一行 super();
            System.out.println("执行 Student 无参构造器...");
        }
    }
    // new Student() 的输出:
    // 执行 Person 无参构造器...
    // 执行 Student 无参构造器...
  4. 显式调用:super(参数列表)

    当父类中没有空参数的构造器时,子类必须手动编写构造器,而且在子类的构造器的首行 必须通过 super(参数列表)​ 来指定调用父类的某一个有参构造器。

    如果父类没有提供无参构造器,而子类构造器又没有显式调用父类的有参构造器,则会 编译失败【示例:显式调用父类有参构造器】

    java 复制代码
    // 父类
    public class Person {
        String name;
        Person(String name) {
            this.name = name;
            System.out.println("执行 Person 有参构造器...");
        }
    }
    // 子类
    public class Student extends Person {
        public Student(String name) {
            super(name); // 必须在第一行,显式调用父类构造器
            System.out.println("执行 Student 有参构造器...");
        }
    }

从子类必须调用父类的构造器来看,保留一个类的无参构造是非常有必要的。

6. 方法的重写(Override)

父类的所有方法都会被继承到子类中,但有时父类中某些方法的实现不适用于子类对象,子类可以根据需要对从父类中继承的方法进行改造,称为方法的重写(Override)

(1)什么是方法重写?

方法重写是指子类创建一个与父类中 方法名、参数列表完全相同 的方法,从而覆盖掉从父类继承来的版本。当通过子类对象调用该方法时,实际执行的是子类重写后的版本。

(2)重写的规则

重写遵循"外壳不变,核心重写"的原则,具体规则可记为"两同两小一大":

  • 两同(完全相同)

    1. 方法名:必须与父类被重写的方法名完全相同。
    2. 参数列表:必须与父类被重写的方法的参数类型、数量、顺序完全相同。
  • 两小(小于或等于)

    1. 返回值类型

      • 如果父类方法返回 void 或基本数据类型,则子类重写方法必须返回相同的类型。
      • 如果父类方法返回引用类型 A,则子类重写方法可以返回类型 A 或 A 的子类。
    2. 抛出的异常:子类重写方法声明抛出的异常类型,必须是父类方法声明抛出异常的类型或其子类。(或者不抛出异常)

  • 一大(大于或等于)

    1. 访问权限修饰符 :子类重写方法的访问权限 不能比父类更严格(可以相同或更宽松)。

      • public > protected > default > private

(3)不能被重写的情况

  • 继承是重写的前提,如果不能继承一个类,则不能重写该类的方法。

  • private​ 方法:对子类不可见,无法重写。

  • final​ 方法:被 final​ 修饰的方法禁止被重写。

  • static​ 方法:静态方法属于类,不属于实例,可以被再次声明(隐藏),但不能被重写。

  • 构造器:不能被继承,更不能被重写。

(4)方法重写(Override)与方法重载(Overload)

方法的重写(Overriding)和重载(Overloading)是 java 多态性的不同表现:

  • 重载(Overload)同一个类 中的多态性表现。指一个类中允许多个方法拥有 相同的方法名 ,但它们的 参数列表必须不同(参数数量、类型或顺序不同)。重载与返回值类型无关。

    编译时多态(根据参数列表选择调用哪个方法)

  • 重写(Override)父子类之间 的多态性表现。子类重新实现了从父类继承而来的方法。

    运行时多态(一个接口,多种实现)

(5)代码举例

java 复制代码
//父子类中
class Father{
    public void method(int i){
        System.out.println("Father.method");
    }
}

//重写
class Son extends Father{
    public void method(int i){
        System.out.println("Son.method");
    }
}

//重载
class Daughter extends Father{
    public void method(int i,int j){
        System.out.println("Daughter.method");
    }
}

//测试类
public class Test {
    public static void main(String[] args) {
        Son s = new Son();
        s.method(1);  //调用Son的方法

        Daughter d = new Daughter();
        d.method(1);  //调用Father的方法
        d.method(1,2);  //调用Daughter的方法
    }
}

三、面向对象的基本特征------多态性

1. 什么是多态

多态的字面意义是"多种形态"。例如:一个人可以和不同的人有不同的关系,一个女人可以同时是母亲、女儿、姐妹和朋友,也就是说,她在不同的情境下会表现出其他的行为。

在 Java 中,多态核心体现在一个引用类型变量可以指向多种实际类型的对象。简单来说,就是同一个父类或接口的引用,在指向不同子类对象时,会表现出不同的行为。

任何满足 IS-A 关系(是一个)的对象本质上都是多态的。例如,Cat​ IS-A Animal​。由于 Java 中所有类都隐式继承自 Object​ 类,因此可以说 Java 中所有对象都具有多态性。

2. 多态的两种主要形式

Java 中的多态性主要分为编译时多态和运行时多态。

(1)编译时多态 (静态绑定)

编译时多态,也称为静态多态或静态绑定,主要通过方法重载实现。

  • 特点

    • 发生在同一个类中。
    • 方法重载:方法名相同,但参数列表不同(参数的类型、数量或顺序不同)。
    • 编译器在编译阶段,就能根据传入参数的不同,确定具体调用哪一个方法。
  • 示例

    java 复制代码
    class Calculator {
        int add(int a, int b) {
            return a + b;
        }
    
        double add(double a, double b) {
            return a + b;
        }
    }
    
    public class Test {
        public static void main(String[] args) {
            Calculator calc = new Calculator();
            calc.add(1, 2);       // 编译时确定调用 add(int, int)
            calc.add(1.0, 2.0);   // 编译时确定调用 add(double, double)
        }
    }

(2)运行时多态 (动态绑定)

运行时多态,也称为动态多态或动态绑定,是面向对象编程的精髓。主要通过方法重写接口实现。即子类重写父类方法,或类实现接口中的方法。(接口将在之后学习)

  • 特点

    • 子类重写父类的方法。
    • 涉及继承或接口实现。
    • 程序在运行时,系统会根据引用变量实际指向的对象类型来动态决定调用哪个方法。
  • 运行时多态的基础:父类引用指向子类对象

    运行时多态的实现,依赖于一个关键的语法现象:让一个父类的引用变量,指向一个子类的实例对象。这个语法是触发多态的"开关"。

java 复制代码
// 语法:父类类型 变量 = new 子类类型();
Animal animal = new Cat(); 

当出现这种引用方式时,该引用变量(如此处的 animal​)便具有了双重类型身份:

  • 编译时类型Animal。由声明变量时使用的类型决定,编译器会根据这个类型检查语法,比如检查方法是否存在。
  • 运行时类型Cat。由 new 关键字实际创建的对象类型决定,JVM 在运行时会根据这个类型来执行方法。

由于编译时和运行时类型不一致,调用方法时便遵循以下核心原则:

  • 编译看左边 (父类) :编译器只认引用变量的声明类型(Animal)。如果要调用的方法在父类中不存在,代码将无法通过编译。
  • 运行看右边 (子类) :程序实际运行时,JVM 会执行该引用变量所指向的实际对象(Cat)中已被重写的方法。

代码示例:

java 复制代码
class Animal {
    void makeSound() {
        System.out.println("动物发出声音");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("狗在汪汪叫"); // 子类重写方法
    }
    void run() {
        System.out.println("狗在跑"); // 子类特有方法
    }
}

public class Test {
    public static void main(String[] args) {
        // 父类引用 animal 指向 Dog 对象,触发多态
        Animal myAnimal = new Dog(); 
        
        // 调用 makeSound() 方法
        // 编译时:检查 Animal 类,有 makeSound() 方法,编译通过。
        // 运行时:实际对象是 Dog,所以执行 Dog 类中重写的 makeSound() 方法。
        myAnimal.makeSound(); // 输出: "狗在汪汪叫"
        
        // 尝试调用 run() 方法
        // 编译时:检查 Animal 类,没有 run() 方法,编译错误!
        // myAnimal.run(); 
    }
}

小结 :我们在此处看到的 Animal myAnimal = new Dog();​ 这种赋值方式,被称为向上转型 (Upcasting) 。正是这种转型机制,为运行时多态奠定了基础。我们将在"多态中的类型转换"中继续讨论。

3. 多态中的类型转换:向上转型与向下转型

(1)为什么要进行类型转换?

  1. 实现多态 :为了利用多态的优势,我们必须将子类的对象赋值给父类的引用。这个过程就是向上转型,它通常是自动发生的,也是多态机制的前提。
  2. 调用子类特有功能 :当对象通过父类引用进行操作时(向上转型后),我们只能访问在父类中声明的成员(方法和属性)。如果需要调用该对象在子类中扩展的、父类没有的特有功能,就必须将父类引用再转换回子类类型,这个过程称为向下转型

(2)向上转型 (Upcasting): 从子类到父类

  • 定义:将一个子类对象的引用赋值给一个父类类型的变量。这个过程因为是从一个更具体的类型转向一个更通用的类型(在继承树中向上移动),所以称为向上转型。

  • 语法父类类型 变量 = new 子类类型();

  • 核心特性

    • 自动进行 :向上转型是自动发生的,无需任何强制转换操作。
    • 绝对安全:因为子类必然包含了父类的所有公共成员,所以将子类视为父类来操作不会产生任何问题。
    • 功能限制:转型后,通过父类引用只能调用父类中已声明的成员,无法访问子类自己扩展的特有成员。
    • 行为多态 :尽管可访问的成员受限于父类,但当调用被子类重写的方法时,实际执行的仍然是子类中的方法体。

(3)向下转型 (Downcasting): 从父类到子类

  • 定义 :将一个已经向上转型的父类引用,重新强制转换回其原始的子类类型。这个过程因为是从一个更通用的类型转向一个更具体的类型(在继承树中向下移动),所以称为向下转型。

  • 语法子类类型 变量 = (子类类型) 父类引用;

  • 核心特性

    • 强制进行 :向下转型必须使用强制类型转换符 ( ),因为编译器无法保证这次转换的安全性。
    • 存在风险 :向下转型是有风险的。如果一个父类引用所指向的对象的实际类型 并不是目标子类(或其后代),那么在程序运行时会抛出 ClassCastException (类转换异常)。

(4)instanceof​ 关键字

为了避免 ClassCastException​ 带来的程序崩溃,Java 提供了 instanceof​ 关键字,它允许我们在进行强制类型转换之前,先检查对象的真实运行时类型。

  • 作用:判断一个对象引用是否是某个类(或其子类)的实例。

  • 语法对象引用 instanceof 数据类型​,该表达式返回一个布尔值(true​ 或 false​)。

    java 复制代码
    对象a instanceof 数据类型A
  • 判断规则

    1. 如果 instanceof 返回 true,那么将该对象引用强制转换为该数据类型是安全的。
    2. 如果一个对象是子类 B 的实例,而 B 继承自父类 A,那么 对象 instanceof A 的结果也为 true
    3. instanceof 两边的类型必须存在继承或实现关系,否则代码无法通过编译。
  • 代码示例:

    java 复制代码
    public class TestInstanceof {
        public static void main(String[] args) {
            Animal animal = new Cat();
            if(animal instanceof Cat){
                Cat cat = (Cat) animal;
                cat.setNickName("小黑");
                cat.action();
                }else if(animal instanceof Dog){
                Dog dog = (Dog) animal;
                dog.setNickName("小白");
                dog.action();
            	}else {
                // 处理其他未知的 Animal 子类型
                System.out.println("未知类型的动物");
                animal.makeSound(); // 仍然可以调用父类的方法
            }
        }
    }

4. 运行时多态的实现原理:虚方法调用

在掌握了多态的基本用法后,我们需要深入一层,去探究其底层的实现原理。我们常说多态是"编译看左边,运行看右边",那么 JVM 在运行时究竟是如何智能地做到"看右边"的呢?答案就藏在虚方法调用这一核心机制中。

(1)什么是虚方法?

虚方法 并不是一个需要我们编写的关键字,而是对一种方法类型的概念定义 。简单来说,在 Java 中,可以被子类重写(Override)的实例方法,就是虚方法。

  • 具体范畴 :所有非 static、非 final、非 private 的实例方法,默认都是虚方法。
  • 反例------非虚方法 :与之相对,static 方法、final 方法、private 方法以及构造器,它们在编译时就能确定唯一的调用版本,不参与运行时多态(动态绑定),因此被称为非虚方法

(2)虚方法调用是如何工作的?

虚方法的调用过程横跨编译和运行两个阶段,是一个从"不确定"到"确定"的过程。

第一步:编译阶段 (Compile Time)

  • 当编译器遇到 animal.makeSound() 这样的代码时,它首先进行静态检查
  • 它会检查引用变量 animal声明类型 (即 Animal 类)中,是否存在一个名为 makeSound() 的方法。如果不存在,编译直接报错。
  • 如果 makeSound() 是一个虚方法,编译器无法在此时确定最终该执行哪个版本(Animal 的还是 Dog 的),于是它不会直接生成调用指令,而是在字节码中放置一个符号引用 (Symbolic Reference) 。这个符号引用就像一个"待办事项",告诉 JVM:"运行时请到这里来确定真正要调用的方法"。

第二步:运行阶段 (Runtime)

  • 当程序运行到 animal.makeSound() 这行代码时,JVM 介入工作。
  • JVM 会查看引用 animal 实际指向的对象 ,发现它是一个 Dog 实例。
  • 接下来,JVM 会拿着编译期留下的"符号引用",在 Dog 类的方法表 中查找与 makeSound() 匹配的方法。
  • 一旦找到,JVM 就将这个符号引用替换为指向 Dog 类中 makeSound() 方法的直接内存地址 。这个在运行时完成的转换过程,就叫做动态链接 (Dynamic Linking)
  • 最后,JVM 根据这个内存地址,执行 Dog 类中的 makeSound() 方法。

拓展:

静态链接(或早期绑定)​:当一个字节码文件被装载进 JVM 内部时,如果被调用的目标方法在编译期可知,且运行期保持不变,这种情况下将调用方法的符号引用转换为直接引用的过程称之为静态链接。那么调用这样的方法,就称为 非虚方法调用​。比如调用静态方法、私有方法、final 方法、父类构造器、本类重载构造器等。

动态链接(或晚期绑定)​:如果被调用的方法在编译期无法被确定下来,也就是说,只能够在程序运行期将调用方法的符号引用转换为直接引用,由于这种引用转换过程具备动态性,因此也就被称之为动态链接。调用这样的方法,就称为 虚方法调用​。比如调用重写的方法(针对父类)、实现的方法(针对接口)。

5. 多态的作用范围

一个核心原则是:Java 中的多态性仅适用于实例方法,不适用于属性(Fields/成员变量)和静态方法。

(1)成员变量:没有多态性

结论: 访问成员变量时,遵循"编译看左边,运行也看左边"的原则。即访问哪个类的成员变量,完全由引用变量的声明类型(编译时类型)决定

原理分析:

  • 字段隐藏 (Field Hiding): 当子类定义了与父类同名的成员变量时,这不叫"重写",而叫"隐藏"。子类对象在内存中会同时拥有父类的同名变量和自己的这个变量。
  • 编译时解析: 对于变量的访问,编译器在编译阶段就已经确定了要访问内存中的哪一份数据。它根据引用变量的类型来决定,而不是根据对象运行时的实际类型。

示例分析:

java 复制代码
class Base {
    int count = 10;
    public void display() {
        System.out.println("Base类的实例方法, count值: " + this.count);
    }
}

class Sub extends Base {
    int count = 20; // 隐藏了父类的count变量
    @Override
    public void display() { // 重写了父类的display方法
        System.out.println("Sub类的实例方法, count值: " + this.count);
    }
}

public class FieldMethodTest {
    public static void main(String[] args){
		
		Sub s = new Sub();// 创建Sub对象,本态引用

		// 访问成员变量
        // 因为引用s的声明类型是Sub,所以访问的是Sub类中定义的count
		System.out.println(s.count);//输出20

		// 调用实例方法
        // s为Sub对象,调用Sub类中重写的display()方法
		s.display();//输出20

		Base b = s;//向上转型,多态引用
        // 访问成员变量
        // 因为引用b的声明类型是Base,所以访问的是Base类中定义的count
        System.out.println(b.count); // 输出: 10

        // 调用实例方法
        // 因为发生了多态,方法调用是动态绑定的
        // JVM会根据b实际指向的Sub对象,调用Sub类中重写的display()方法
        b.display(); // 输出: Sub类的实例方法, count值: 20
    }
}

(2)静态方法:没有多态性

结论: 调用静态方法时,同样遵循"编译看左边,运行也看左边"的原则。调用哪个类的静态方法,完全由引用变量的声明类型决定。

原理分析:

  • 类级别绑定: 静态方法是属于类的,而不是属于某个具体对象的。在类加载到 JVM 时,静态方法就已经和类本身绑定在一起了。
  • 静态绑定: 因此,编译器在编译时就可以根据引用变量的类型,百分之百确定要调用哪个类的静态方法。这个过程称为静态绑定早期绑定,它不涉及运行时对象的类型。

示例分析:

java 复制代码
class Parent {
    static void greet() {
        System.out.println("Hello from Parent");
    }
}

class Child extends Parent {
    // 这不是重写,而是隐藏了父类的静态方法
    static void greet() {
        System.out.println("Hello from Child");
    }
}

public class StaticMethodTest {
    public static void main(String[] args) {
        Parent p = new Child(); // 向上转型

        // 调用静态方法
        // 因为引用p的声明类型是Parent,所以调用的是Parent类中的静态方法
        p.greet(); // 输出: Hello from Parent
    }
}

【建议】

永远不要通过对象引用来调用静态方法 (p.greet()​)。这是一种非常容易引起误解的坏习惯。

正确的做法是始终使用类名来调用静态方法 ,如 Parent.greet()​ 或 Child.greet()​。这样代码的意图会一目了然,完全杜绝了关于"多态性"的混淆。现代 IDE 通常也会对此类用法给出警告。

四、类的成员之:代码块 (Code Block)

1. 代码块概述

在 Java 中,代码块是指定义在类中、方法和构造器之外,并用 {}​ 包裹起来的一段代码。它的核心作用是在类加载或对象创建时执行特定的初始化操作

当成员变量的初始化逻辑比简单的赋值更复杂时(例如,需要逻辑判断、异常处理或调用方法),使用代码块是理想的选择。

何时触发类加载?

Java 虚拟机规范严格规定了首次主动使用一个类时,必须对其进行初始化(加载、链接、初始化)。常见的主动使用情况包括:

  • 创建类的实例 (new MyClass())。
  • 访问类的静态变量(不包括被 final 修饰的编译期常量)。
  • 调用类的静态方法。
  • 通过反射调用类。
  • 初始化一个类的子类(会首先触发父类的初始化)。
  • 启动类(包含 main 方法的类)。 "

根据有无 static​ 关键字修饰,代码块分为两种:

  • 静态代码块 (Static Block) :使用 static 修饰,用于类级别的初始化。
  • 实例代码块 (Instance Block) :不加任何修饰,用于对象实例级别的初始化。

2. 静态代码块

(1)定义与语法

静态代码块由 static​ 关键字和 {}​ 构成,直接定义在类中。

java 复制代码
class MyClass {
    static {
        // 此处为静态代码块
        // 用于执行类级别的初始化代码
    }
}

(2)特性

  1. 核心特性随着类的加载而执行,且在整个程序的生命周期中仅执行一次。
  2. 主要用途 :主要用于初始化静态变量 (static variables) 或执行类级别的预处理任务(如加载驱动、注册服务等)。
  3. 访问限制 :由于在类加载时执行(此时可能还没有任何实例对象),因此静态代码块内部不能访问任何非静态成员 (实例变量、实例方法),也不能使用 this super 关键字
  4. 执行顺序 :如果一个类中有多个静态代码块,它们将按照在代码中出现的顺序自上而下依次执行

代码示例:

java 复制代码
public class StaticBlockExample {
    static {
        System.out.println("1. 静态代码块执行:类被加载了。");
    }

    public StaticBlockExample() {
        System.out.println("3. 构造器执行:一个对象被创建了。");
    }

    public static void main(String[] args) {
        System.out.println("2. main方法开始执行。");
        new StaticBlockExample(); // 首次创建对象
        new StaticBlockExample(); // 再次创建对象
        System.out.println("4. main方法执行结束。");
    }
}

运行结果:

text 复制代码
1. 静态代码块执行:类被加载了。
2. main方法开始执行。
3. 构造器执行:一个对象被创建了。
3. 构造器执行:一个对象被创建了。
4. main方法执行结束。

3. 实例代码块

(1)定义与语法

实例代码块直接由 {}​ 包裹,不带任何修饰符,定义在类中。

java 复制代码
class MyClass {
    {
        // 此处为实例代码块
        // 用于执行对象实例的初始化代码
    }
}

(2)核心特性与执行时机

  1. 核心特性在每次创建类的实例时执行 ,并且其执行时机在构造器之前
  2. 主要用途 :一个关键用途是提取多个构造器中的通用初始化代码,以减少冗余并提高代码可维护性。
  3. 访问能力:可以访问类的所有成员,包括静态成员和非静态成员。
  4. 执行顺序 :如果一个类中有多个实例代码块,它们同样按照在代码中出现的顺序自上而下依次执行

代码示例:重构构造器

优化前:构造器中存在重复代码

java 复制代码
public class Dog {
    private String name;

    // 无参构造器
    public Dog() {
        // --- 重复的代码 ---
        System.out.println("一只小狗诞生了!"); 
        // ------------------
        this.name = "无名氏";
    }

    // 有参构造器
    public Dog(String name) {
        // --- 重复的代码 ---
        System.out.println("一只小狗诞生了!"); 
        // ------------------
        this.name = name;
    }

    public void bark() {
        System.out.println(this.name + "正在汪汪叫。");
    }
}

优化后:使用实例代码块提取公共逻辑

java 复制代码
public class Dog {
    private String name;

    // 将公共初始化代码放入实例代码块
    {
        System.out.println("一只小狗诞生了!");
    }

    // 无参构造器现在非常简洁
    public Dog() {
        this.name = "无名氏";
    }

    // 有参构造器现在也非常简洁
    public Dog(String name) {
        this.name = name;
    }

    public void bark() {
        System.out.println(this.name + "正在汪汪叫。");
    }
}

4. 初始化顺序

当一个类涉及继承时,其初始化顺序遵循一套严格的规则。

一、 类加载阶段(仅发生一次):

  1. 父类的静态变量初始化与静态代码块(按顺序)。
  2. 子类的静态变量初始化与静态代码块(按顺序)。

二、 对象实例化阶段(每次 new都会发生):

  1. 父类的实例变量初始化与实例代码块(按顺序)。
  2. 父类的构造器。
  3. 子类的实例变量初始化与实例代码块(按顺序)。
  4. 子类的构造器。

案例分析:

java 复制代码
// 父类
class Super {
    static { System.out.println("父类静态代码块"); }
    { System.out.println("父类实例代码块"); }
    public Super() { System.out.println("父类构造器"); }
}

// 子类
class Sub extends Super {
    static { System.out.println("子类静态代码块"); }
    { System.out.println("子类实例代码块"); }
    public Sub() { System.out.println("子类构造器"); }
}

// 测试类
public class TestInitialization {
    public static void main(String[] args) {
        System.out.println("--- 开始创建第一个Sub对象 ---");
        new Sub();
        System.out.println("\n--- 开始创建第二个Sub对象 ---");
        new Sub();
    }
}

运行结果:

text 复制代码
--- 开始创建第一个Sub对象 ---
父类静态代码块
子类静态代码块
父类实例代码块
父类构造器
子类实例代码块
子类构造器

--- 开始创建第二个Sub对象 ---
父类实例代码块
父类构造器
子类实例代码块
子类构造器

【总体顺序】

  1. 类初始化阶段(全局仅一次):

    1. 执行父类的静态变量初始化和静态代码块(按源码顺序)。
    2. 执行子类的静态变量初始化和静态代码块(按源码顺序)。
  2. 对象实例化阶段(每次 new时):

    1. 进入父类层面:

      1. 执行父类的实例变量初始化和实例代码块(按源码顺序)。
      2. 执行父类的构造器。
    2. 进入子类层面:

      1. 执行子类的实例变量初始化和实例代码块(按源码顺序)。
      2. 执行子类的构造器。

五、类的成员之:内部类(InnerClass)

1. 内部类概述

(1)什么是内部类?

在 Java 中,可以将一个类 A 定义在另一个类 B 的内部,此时类 A 就被称为 内部类 (Inner Class)嵌套类 (Nested Class) ,而类 B 则被称为 外部类 (Outer Class)

java 复制代码
class OuterClass { // 外部类
    // ...
    class InnerClass { // 内部类
        // ...
    }
}

(2)为什么使用内部类?

  • 增强封装

    当一个类(如 大脑​)的存在完全是为了服务于另一个类(如 身体​),并且它不应该被外界独立使用时,将其定义为内部类是最佳实践。

    这创建了一种强逻辑关联,使得代码结构更清晰、内聚性更高。

  • 便捷访问

    内部类最大的一个优势是可以直接、无障碍地访问其外部类的所有成员,包括私有 (private) 成员。这极大地简化了需要紧密协作的类之间的代码。

    身体​ 与 大脑​ 的例子中,大脑​(内部类)需要协调 身体​(外部类)的各种私有生理机能,内部类的这种访问权限使得这种复杂的协作变得直接而高效。如果将 大脑​ 定义为独立的类,它将无法直接访问 身体​ 的私有状态,从而破坏了封装或需要编写大量复杂的接口代码。

(3)内部类的四种类型

根据声明位置和修饰符的不同,内部类可分为四大类型:

  1. 成员内部类:在成员位置上,定义在类体中,方法的外面。

    • 非静态成员内部类: 没有 static 关键字修饰。
    • 静态成员内部类: 使用 static 关键字修饰。
  2. 局部内部类:在局部位置上,定义在方法或代码块内部。

    • (非匿名)局部内部类: 在方法内有明确的类名。
    • 匿名内部类: 在方法内定义,但没有类名,通常用于一次性使用。

2. 成员内部类

成员内部类是直接定义在外部类(Outer Class)的类体之中、与成员变量和方法平级的内部类。根据是否存在 static 关键字,它被清晰地划分为两种截然不同的类型。

(1)非静态成员内部类

非静态成员内部类 是我们通常语境下所说的"成员内部类"。它在概念上是外部类的一个实例成员

1. 定义与核心理念
  • 定义 : 在外部类的成员位置,未使用 static 关键字修饰的内部类。
  • 核心理念 : 内部类实例的创建必须 依托于一个已经存在的外部类实例。你可以将其视为外部类对象的一个专属"组件"或"器官",比如 汽车 对象的 引擎 对象。
2. 声明与修饰
  • 访问修饰符 : 可使用 publicprotectedprivatedefault
  • 其他修饰符 : 可使用 abstractfinal
  • 字节码 : 编译后生成独立的 .class 文件,命名格式为 外部类名$内部类名.class
3. 对外部访问的规则 (由内向外看)
  • 访问范围 : 内部类实例可以无条件地、直接地 访问其绑定的外部类实例的所有成员,包括 private 私有成员和 static 静态成员
  • 重名解决 : 当内外成员同名时,默认访问内部成员。要访问外部成员,需使用 外部类名.this.成员名 的语法进行明确区分。
4. 被外部访问的规则(由外向内看)
  • 访问前提 : 必须先创建外部类实例,然后通过外部类实例来创建内部类的实例。

  • 在外部类内部访问:

    1. 先在方法中创建内部类实例:内部类名 innerObj = new 内部类名();
    2. 通过该实例访问其所有成员(包括 private 成员):innerObj.内部类方法();
  • 在外部类之外访问:

    1. 确保内部类及目标成员的访问修饰符允许外部访问(如 public)。
    2. 先创建外部类实例:外部类名 outerObj = new 外部类名();
    3. 再通过外部类实例创建内部类实例:外部类名.内部类名 innerObj = outerObj.new 内部类名();
    4. 通过内部类实例访问其成员:innerObj.公开的内部类方法();
  • 推荐实践:

    当在外部类之外访问非静态内部类时,需要外部类的实例对象才能创建非静态内部类的实例对象,这点在语法格式上是有点别扭的,如 outerObj.new 内部类名();​,可以把它稍微改进一下,这样会让代码看起来简单一些。

    首先,修改外部类的代码,增加一个 getInner() ​的方法,然后就可以通过 外部类名.内部类名 innerObj = outerObj.getInner() ​来创建内部类实例。

    java 复制代码
    public class Outer{
    
    	// 内部类及其他成员
    
    	// getInner()方法
    	public Inner getInner(){
    		return new Inner();
    	}
    }
5 . 自身成员的定义规则
  • 静态限制 : 绝对禁止 声明任何 static 成员(方法或变量)。
  • 唯一例外 : 可以声明 static final 修饰的编译期常量(例如 static final String CONST = "...")。
  • 根本原因 : 自身的存在依赖于一个动态的外部类实例,而 static 成员不依赖任何实例,这在逻辑上是冲突的。

(2)静态成员内部类

静态成员内部类 通常直接简称为"静态内部类"。它在概念上是外部类的一个类成员 (静态成员) ,与外部类的实例无关。

1. 定义与核心理念
  • 定义 : 在外部类的成员位置,使用 static 关键字修饰的内部类。
  • 核心理念 : 它不依赖于任何外部类的实例,是一个独立的类,只是在逻辑上被组织在外部类的命名空间之下。 把它想象成一个被外部类"托管"的独立类,比如 HashMap 中的 Node 类。
2. 声明与修饰
  • static​ 关键字修饰

  • 访问修饰符 : 可使用 public​, protected​, private​, default​。

  • 其他修饰符 : 可使用 abstract​ 或 final​。

  • 字节码 : 编译后生成独立的 .class​ 文件,命名格式为 外部类名$内部类名.class​。

3. 对外部访问的规则 (由内向外看)
  • 访问范围 : 只能直接访问 外部类的所有 static 静态成员
  • 访问限制 : 不能直接访问外部类的非静态成员(实例成员)。
4. 被外部访问的规则(由外向内看)
  • 访问静态成员 : 无需创建任何实例。直接通过 外部类名.静态内部类名.静态成员​ 的形式访问(前提是访问修饰符允许)。

  • 访问非静态成员 : 注意,这需要外部类实例。

    1. 先创建静态内部类的实例:外部类名.内部类名 innerObj = new 外部类名.内部类名();
    2. 再通过该实例访问其非静态成员:innerObj.内部类方法();
5 . 自身成员的定义规则
  • 静态能力 : 与普通顶级类一样,可以自由地定义自己的静态成员(static 变量和 static 方法)和非静态成员

(3)静态内部类与非静态内部类的选择

关于静态和非静态的理解只要抓住一个点即可:静态的不需要实例对象,而非静态的需要实例对象。

这里的"实例对象"指的是外部类的实例

1. 何时使用非静态内部类
  • 核心规则 :当内部类需要访问其外部类的实例成员(实例变量或实例方法)时。
  • 原因 :非静态内部类的实例在创建时,必须"绑定"到一个已存在的外部类实例上,这样它才能访问到那些非静态成员。正因如此,它的实例化方式是 外部类实例.new 内部类名()
2. 何时使用静态内部类
  • 核心规则 :当内部类只是逻辑上属于外部类,但其功能完全独立,不依赖外部类的任何实例成员时。
  • 原因 :静态内部类不与任何外部类实例绑定,它是一个独立的类。因此,它的创建不需要外部类实例:new 外部类名.内部类名()
3. 结论
  • 默认用静态 :如果不需要访问外部类的实例成员,强烈建议使用静态内部类。这能减少不必要的内存占用,并避免潜在的内存泄漏风险。
  • 用非静态的场景 :如果要用到外部类的非静态成员,内部类必须是非静态的。

3、局部内部类

局部内部类是定义在方法体、构造器或代码块内部的类。

它的核心特征在于其作用域被严格限制,如同一个局部变量,仅在定义它的代码块内可见。这种特性决定了它的生命周期和访问规则。

局部内部类主要分为两种形式:拥有明确名称的非匿名局部内部类 和没有名称的匿名内部类

在实际开发中,由于其"用完即弃"的场景居多,匿名内部类的使用频率远高于非匿名局部内部类。

(1)规则与核心概念

无论是匿名还是非匿名的局部内部类,都遵循以下通用规则:

  1. 作用域限制

    • 其可见性和可使用范围仅限于声明它的方法或代码块内部,从声明处开始到代码块结束,相当于一个"黑盒",外部完全无法访问这个类本身。
  2. 修饰符 (Modifiers)

    • 不能使用 任何访问修饰符(public, protected, privatedefult)和 static
    • 可以使用 abstractfinal 进行修饰,与普通类含义相同。
  3. 对外部类成员的访问规则

    • 可以无条件、直接地访问其外部类的所有成员 (包括 privatestatic 成员)。
    • 注意 :如果局部内部类定义在静态方法 中,它将无法访问外部类的非静态成员,因为它不持有外部类实例的引用。
  4. 对方法内局部变量的访问规则

    • 局部内部类可以访问其所在方法中定义的局部变量。
    • 核心约束 :被访问的局部变量必须是 final事实上的 final
    • 原因剖析 :当方法执行完毕,其栈帧被销毁,局部变量也随之消失。但此时,在方法中创建的局部内部类对象可能仍然存活(例如被返回或传递给其他线程)。为了让内部类对象能继续访问该变量,Java 编译器会"捕获"该变量的值,并将其作为一个隐式的 final 字段存储在内部类实例中。因此,该变量必须是不可变的,以保证数据的一致性。从 Java 8 开始,如果一个局部变量在初始化后未被再次赋值,编译器会自动将其视为 final,无需手动声明。
  5. 字节码文件

    • 编译后会生成独立的 .class 文件。
    • 命名格式为:外部类名$编号局部内部类名.class(非匿名)或 外部类名$编号.class(匿名)。

(2)非匿名局部内部类

这是一种有名字的局部内部类,因其使用场景非常有限,作为简单了解即可。

  1. 定义与用途

    • 定义 :在方法体中用 class 关键字正常定义的类。
    • 用途 :其主要应用场景是在一个方法内需要多次创建某个特定类的对象,而这个类又只在该方法内使用。由于这种情况非常罕见,所以它在实际开发中很少出现。
  2. 实例化与使用

    • 在声明之后,于其作用域内通过 new 类名() 的标准方式创建实例。
  3. 自身成员的定义规则

    • 实例成员:可以定义实例变量、实例方法、构造器、实例初始化块,甚至还可以再定义内部类(但不推荐)。

    • 静态成员

      • JDK 16 之前禁止 声明除 static final 常量之外的任何 static 成员(包括静态变量、静态方法、静态初始化块)。
      • JDK 16 及之后 :限制被放宽。非匿名局部内部类可以 声明 static 成员。这使得它的行为更像一个普通的嵌套类,只是作用域受限。

示例代码:

java 复制代码
public class Outer {//外部类
    public void out(){//非静态方法
        final String a = "a";
        class Inner{//非匿名局部内部类
            public void in(){
                System.out.println("局部内部类的方法in:");
                System.out.println(a);//访问方法内final局部变量
            }
        }
        //out方法的里面,创建内部类的对象
        Inner in = new Inner();
        in.in();//调用非匿名局部内部类的方法
    }
}
java 复制代码
public class InnerTest {//非匿名局部类的测试类
    public static void main(String[] args) {
        new Outer().out();//out()为非静态方法,所以要先创建类的对象,即new Outer()
    }
}
//运行结果:
局部内部类的方法in:
a

(3)匿名局部内部类(重点)

匿名局部内部类简称"匿名内部类",它是局部内部类的核心与精髓,是一种没有类名、在声明的同时完成实例化的特殊类。

  1. 定义与核心理念

    • 定义 :一种只能使用一次的类,它在定义时必须继承一个父类或实现一个父接口必须直接创建对象new 父类/父接口([参数列表]){...}
    • 本质new 父类/父接口() { ... } 这个表达式的结果就是一个对象实例
    • 核心理念 :专为简化实现而生。当需要一个"一次性"的对象来重写某个方法时,使用匿名内部类可以极大地减少代码的冗余,让逻辑更紧凑。
  2. 声明语法

    • 形式一:继承父类(无参构造)

      java 复制代码
      new 父类名() {
          // 类体:重写或扩展父类的方法
      };
    • 形式二:继承父类(有参构造)

      java 复制代码
      new 父类名(实参列表) {
          // 类体:重写或扩展父类的方法
      };
    • 形式三:实现接口

      java 复制代码
      new 父接口() {
          // 类体:必须实现接口中的所有抽象方法
      };
  3. 自身成员的定义规则

    • 无构造器 :匿名内部类没有名称,因此无法定义构造器 。如果需要执行类似构造器的初始化逻辑,必须使用实例初始化块 { ... }​。

    • 单一继承/实现 :一个匿名内部类只能继承一个类实现一个接口,不能两者兼备。

    • 实例成员:可以自由定义实例变量、实例方法和实例初始化块。

    • 静态成员:规则与非匿名局部内部类完全相同。

      • JDK 16 之前禁止 声明除 static final​ 常量之外的任何 static​ 成员。

      • JDK 16 及之后 :限制被放宽,可以 声明 static​ 成员。

      开发建议:

      尽管语法允许,但强烈不建议 在匿名内部类中定义大量复杂的或新的 public​ 成员。其设计的初衷是提供一个简洁的实现,过多的扩展会严重破坏代码的可读性。如果逻辑复杂,应优先考虑使用具名类(局部内部类、成员内部类或独立的类)。

  4. 使用场景与限制

    • 场景一:直接调用 (一次性使用)

      这种方式可以调用匿名内部类中新增的方法,但因为没有引用指向该对象,所以只能调用一次。

      text 复制代码
      new 父类/父接口([实参列表]){
      	新增方法
      }.新增方法
    • 场景二:多态赋值 (最常用)

      将匿名内部类的实例赋值给父类或父接口的引用。这是最灵活和最常见的用法。

      限制:由于多态的限制,通过该引用只能调用在父类或接口的类型中声明过的方法。

      如果该方法被匿名内部类重写,实际执行的是重写后的版本。

      但是,在匿名内部类中新增的、父类型未声明过的方法则无法通过该引用调用

      text 复制代码
      父类/父接口 变量 = new 父类/父接口([实参列表]){}; 
      变量.父类/父接口方法
    • 场景三:作为方法参数

      直接将匿名内部类的实例作为参数传递给方法,代码非常紧凑。

      text 复制代码
      方法名(new 父类/父接口([实参列表]){});

示例代码:

java 复制代码
class Developer {
    // 父类的方法
    public void code() {
        System.out.println("父类方法被调用。");
    }
}

public class AnonymousClassUsage {
    public static void main(String[] args) {

// 场景一:直接调用:创建匿名内部类,并调用新增的方法。
        new Developer() {
            public void newMethod() {
                System.out.println("[场景一] 这是匿名内部类中新增的方法,已成功调用。");
            }
        }.newMethod(); // 创建后立刻调用,之后该实例无法再被访问。

// 场景二:多态赋值:创建匿名内部类,赋值给父类引用,并重写父类的方法。由于由于多态的限制,通过该引用只能调用父类中已有的方法。
        Developer developer = new Developer() {
            @Override
            public void code() {
                System.out.println("[场景二] 这是匿名内部类中重写的方法,已成功调用。");
            }

            public void newMethod1() {
                System.out.println("[场景二] 这是匿名内部类中新增的方法1,无法被调用。");
            }

        };
        // 通过父类引用,只能调用父类中已有的方法(这里是重写后的版本)。
        developer.code();
        // developer.newMethod1(); // 编译错误!只能调用父类中已有的方法。

// 场景三:作为方法参数:创建匿名内部类,作为方法参数,并重写父类的方法。
        test(new Developer() {
            @Override
            public void code() {
                // 在这里重写 code 方法,以实现在 performTask 方法中想要执行的逻辑。
                System.out.println("[场景三] 匿名内部类作为方法参数,重写的方法被成功调用。");
            }
        });
    }

    // 创建一个静态方法,接受一个 Developer 类型的对象作为参数,并调用它的 code() 方法。
    public static void test(Developer dev) {
        dev.code();
    }
}

// 输出结果:
// [场景一] 这是匿名内部类中新增的方法,已成功调用。
// [场景二] 这是匿名内部类中重写的方法,已成功调用。
// [场景三] 匿名内部类作为方法参数,重写的方法被成功调用。

拓展:关于匿名内部类 new 父类/父接口([实参列表]){} ​的形式与成员限制的说明

  1. 由于匿名内部类没有类名,因此在定义时必须依托一个父类或父接口;
  2. 由于匿名内部类没有类名,无法在定义它的作用域之外被引用,所以访问修饰符没有意义,没有任何修饰符
  3. 由于匿名内部类没有类名,而构造器的名称必须与类名一致,所以无法为其定义构造器。
  4. 继承性规则:创建子类对象时需调用父类构造器 ,且构造器的首行必须明确调用父类的哪个构造器

由于以上因为,匿名内部类的形式变成了:new 父类/父接口([实参列表]){}​,匿名内部类本质上是一个继承了某个父类或实现了某个接口的、没有名字的子类的实例。

4、内部类总结

总原则:

  • 被访问是静态的,那么不需要创建对象。没有限制。
  • 被访问的是非静态,必须先创建对象。静态不能直接访问非静态。

掌握使用匿名内部类的三种场景:

text 复制代码
new 父类/父接口([实参列表]){
	新增方法
}.新增方法
text 复制代码
父类/父接口 变量 = new 父类/父接口([实参列表]){}; 
变量.父类/父接口方法
text 复制代码
方法名(new 父类/父接口([实参列表]){});

内部类辨析表:

相关推荐
oioihoii15 分钟前
Visual Studio C++编译器优化等级详解:配置、原理与编码实践
java·c++·visual studio
没有羊的王K16 分钟前
SSM框架——Day4
java·开发语言
24kHT19 分钟前
2.3 前端-ts的接口以及自定义类型
java·开发语言·前端
Mr_Xuhhh29 分钟前
QT窗口(3)-状态栏
java·c语言·开发语言·数据库·c++·qt·算法
张人玉1 小时前
C#`Array`进阶
java·算法·c#
朱雨鹏1 小时前
同步队列阻塞器AQS的执行流程,案例图
java·后端
超浪的晨1 小时前
Java Map 集合详解:从基础语法到实战应用,彻底掌握键值对数据结构
java·开发语言·后端·学习·个人开发
qq_529835351 小时前
事务隔离:从锁实现到MVCC实现
java·开发语言·数据库
望获linux2 小时前
【实时Linux实战系列】实时系统的安全性架构
java·linux·服务器·开发语言·架构·嵌入式软件
苇柠2 小时前
Java数组补充v2
java·python·排序算法