Skip to content

S02-09 面向对象-内部类

[TOC]

概述

什么是内部类

在 Java 中,内部类(Inner Class) 是指一个直接定义在另一个类或接口内部的类

内部类不仅是一种逻辑上的分组机制,更重要的是,它允许内部类直接访问其外部类(Outer Class)的成员(包括私有成员)。这种特性使得内部类在处理复杂的业务逻辑、事件驱动编程以及隐藏实现细节时非常有用。

为什么要使用内部类:

  1. 封装性(隐藏实现):可以将内部类隐藏在外部类中,不允许同一个包中的其他类访问。

  2. 访问特权:内部类可以无条件地访问外部类的所有成员,即使是 private 成员。

  3. 逻辑分组:如果一个类只为另一个类服务,将它作为内部类定义可以使代码结构更紧凑、可读性更强。

  4. 多重继承的补充:Java 的类不支持多继承,但通过让不同的内部类继承不同的父类,可以变相实现多重继承的效果。

内部类分类

内部类的四大分类:

Java 中的内部类主要分为四种:成员内部类静态内部类局部内部类匿名内部类

按定义位置分

  • 外部类的成员位置:
    • 有 static:静态内部类
    • 没有 static:成员内部类
  • 方法内或代码块内:
    • 有类名:局部内部类
    • 有没类名:匿名内部类

对比四大内部类

比较维度成员内部类
(Member Inner Class)
静态内部类
(Static Nested Class)
局部内部类
(Local Inner Class)
匿名内部类
(Anonymous Inner Class)
定义位置外部类的成员位置外部类的成员位置方法内或代码块内方法内或代码块内(声明与实例化合一)
static 修饰符无(Java 16 起内部可定义静态成员)有(static 修饰类本身)
是否有类名(直接给出父类或接口名)
实例化方式依赖外部类对象:outer.new Inner()不依赖外部类对象:new Outer.Inner()只能在所在的方法内部直接 new在定义的同时 newnew 接口/父类() {...}
访问外部类成员可以无限制访问(包括非静态、静态、私有)只能直接访问静态成员可以无限制访问(前提是所在方法非静态)可以无限制访问(前提是所在方法非静态)
访问所在方法局部变量不适用不适用必须是 final 或事实上的不可变必须是 final 或事实上的不可变
是否持有外部类引用是(隐式持有 Outer.this是(若在非静态方法中)是(若在非静态方法中)
编译后的 class 文件名Outer$Inner.classOuter$Inner.classOuter$1Inner.class (带数字序号)Outer$1.class (仅数字序号)
典型应用场景隐藏细节且需直接操作外部类状态(如 ArrayListItr 迭代器)高内聚辅助类、Builder 建造者模式、防内存泄漏仅在方法内需要用到复杂的局部类(极少使用)接口/抽象类的单次实现、事件监听器、回调函数、多线程

核心记忆口诀:

  • 成员内部类:寄生虫,没外部类活不了,外部类的东西随便用。
  • 静态内部类:独立单身汉,自己管自己,只能用外部类的共享(静态)资源。
  • 局部内部类:方法里的私房钱,出了门谁都不认,用外面的变量还得锁死(final)。
  • 匿名内部类:一次性筷子,没名字,用完就扔,现在经常被 Lambda 抢饭碗。

编译后的底层产物

编译后的底层产物:

需要注意的是,无论内部类嵌套得多深,Java 编译器最终都会把它们编译成独立的 .class 文件:

  • 成员内部类、静态内部类:会被编译为 外部类名$内部类名.class(例如 Outer$Inner.class)。
  • 局部内部类:会被编译为 外部类名$数字内部类名.class(例如 Outer$1LocalInner.class)。
  • 匿名内部类:会被编译为 外部类名$数字.class(例如 Outer$1.class)。

成员内部类

成员内部类(Member Inner Class) 是 Java 内部类中最基础、最核心的一种形态。它就像是外部类的一个普通成员(类似于一个实例变量或者实例方法),没有 static 修饰符

为了让你全面彻底地掌握成员内部类,我们将从它的核心特性、对象创建、变量访问规则以及底层原理来逐一拆解。

核心特性

核心特性:

  • 实例级别:成员内部类是寄生在外部类的实例(对象) 上的。这意味着,没有外部类的对象,就不可能存在成员内部类的对象。
  • 无限制访问:它可以无条件地访问外部类的所有成员属性和成员方法,包括被声明为 private 的成员。
  • 访问修饰符:和普通的类成员一样,成员内部类可以使用所有的访问修饰符进行限制,比如 publicprotected、默认(包可见)以及 private。你可以通过 private 将内部类完全隐藏在外部类内部。

对象创建方式

对象的创建方式:

由于成员内部类依赖于外部类的实例,因此在不同的位置实例化它,语法会有所不同。

方式 A:在外部类内部创建:

外部类的非静态方法中,由于当前已经存在外部类的实例(也就是 this),你可以直接像创建普通对象一样创建内部类对象。

java
public class Outer {
  public class Inner {
    public void work() {
      System.out.println("Inner is working.");
    }
  }

  public void createInner() {
    // 直接实例化
    Inner inner = new Inner();
    inner.work();
  }
}

方式 B:在外部类外部(或其他类的静态方法中)创建:

这是初学者最容易写错的地方。因为必须先有外部类对象,所以必须通过外部类的对象去 new 内部类

java
public class Test {
  public static void main(String[] args) {
    // 第一步:先实例化外部类
    Outer outer = new Outer();

    // 第二步:通过外部类实例,去 new 内部类
    // 注意语法格式:外部类名.内部类名 变量名 = 外部类对象.new 内部类();
    Outer.Inner inner = outer.new Inner();

    inner.work();
  }
}

变量同名遮蔽

变量的“同名遮蔽”与访问规则(重点):

当局部变量、内部类成员变量、外部类成员变量发生同名冲突时,Java 采用就近原则。如果想打破就近原则访问更外层的变量,需要使用特定的 外部类名.this 语法。

java
public class Outer {
  private String name = "外部类的属性";

  public class Inner {
    private String name = "内部类的属性";

    public void showName(String name) {
      // 1. 就近原则,访问方法参数(局部变量)
      System.out.println(name);

      // 2. 访问当前内部类的成员变量
      System.out.println(this.name);

      // 3. 访问外部类的成员变量(核心语法:外部类名.this.变量名)
      System.out.println(Outer.this.name);
    }
  }
}

说明Outer.this 是 Java 提供的一个特殊语法,专门用来获取指向当前所依附的外部类实例的引用

底层原理@

底层原理:为什么内部类能访问外部类的私有成员:

从 JVM 的角度来看,并不存在所谓的“内部类”,所有的类最终都会被编译成独立的 .class 文件。成员内部类会被编译为 Outer$Inner.class

魔法发生在这里

当 Java 编译器编译成员内部类时,会在其底层偷偷地执行以下操作:

  1. 自动为内部类添加一个隐式的成员变量,这个变量的类型就是外部类。

  2. 自动修改内部类的构造器,将外部类的实例作为参数传进去,并赋值给那个隐式变量。

也就是说,成员内部类在创建时,就悄悄绑定了创建它的那个外部类对象的引用。当我们调用外部类的私有属性时,实际上编译器帮我们转化为了类似 OuterClassReference.property 的调用。

JDK 16+ 新特性

JDK 16 及以后的规则变化(关于 Static):

在 Java 16 之前,有一个非常严格的规定:成员内部类中不能包含任何 static 声明的变量或方法(除非是 static final 的编译期常量)。因为成员内部类是依赖对象的,而 static 是类级别的,两者在逻辑上冲突。

更新:从 Java 16 开始(JEP 395 引入的特性),这个限制被打破了。现在的成员内部类中也可以定义静态成员和静态方法了。不过,这些静态成员仅仅属于这个内部类本身,通常用于内部类自身的辅助逻辑。

应用场景

经典源码中的应用场景:

成员内部类在 JDK 源码中最经典的应用莫过于迭代器模式(Iterator Pattern)

例如在 ArrayList 源码中,迭代器 Itr 就是一个成员内部类:

java
public class ArrayList<E> {
  // 外部类的实际数据
  transient Object[] elementData;
  private int size;

  // ... 其他代码 ...

  // 内部类实现了 Iterator 接口
  private class Itr implements Iterator<E> {
    int cursor; // 下一个要返回的元素的索引

    public boolean hasNext() {
      // 直接访问外部类的 size 属性!
      return cursor != size;
    }

    public E next() {
      // 直接操作外部类的 elementData 数组!
      return (E) elementData[cursor++];
    }
  }

  public Iterator<E> iterator() {
    return new Itr();
  }
}

好处Itr 作为一个内部类,它可以天经地义地、零距离地直接读取 ArrayList 内部的数组 elementDatasize,不需要 ArrayList 暴露额外的 get/set 方法,完美保证了 ArrayList 的封装性。

练习题【

image-20260306180856254

静态内部类

在 Java 中,静态内部类(Static Nested Class,静态嵌套类) 虽然名字里带有“内部类”,但严格来说,Java 官方文档将其定义为静态嵌套类

你可以把它理解为一个“恰好写在另一个类里面的普通类”。加上了 static 关键字后,它与外部类的关系变得非常松散,完全摆脱了对外部类实例的依赖

核心特性

核心特性:切断与外部类实例的羁绊:

  • 无需外部类实例:静态内部类不依赖于外部类的对象。它的创建不依赖于外部类的生命周期。
  • 没有隐式引用:在上一篇讲成员内部类时,我们提到它底层会偷偷绑定一个 Outer.this 引用。静态内部类没有这个引用。这是它与普通成员内部类最本质的区别。
  • 可包含任何成员:静态内部类内部既可以定义静态变量和静态方法,也可以定义普通的实例变量和实例方法。

对象的创建方式

对象的创建方式:

因为不需要依赖外部类的实例,创建静态内部类对象的语法非常直接,类似于调用外部类的静态属性。

java
public class Outer {
  // 1. 静态内部类
  public static class StaticInner {
    public void sayHello() {
      System.out.println("Hello from Static Inner!");
    }
  }
}

public class Test {
  public static void main(String[] args) {
    // 2. 直接通过 外部类名.内部类名 进行实例化,不需要 new Outer()
    Outer.StaticInner inner = new Outer.StaticInner();
    inner.sayHello();
  }
}

访问限制

访问规则(作用域限制)类似静态方法

因为静态内部类在内存中独立存在(不依赖外部类对象),所以它无法直接知道外部类实例的状态。

  • 只能访问外部类的静态成员:它可以无缝访问外部类的所有静态变量和静态方法(即使是 private 的)。

  • 绝对不能直接访问外部类的非静态成员:如果你想访问外部类的普通成员变量,必须自己手动创建一个外部类的对象,然后通过该对象去访问。

    java
    public class Outer {
      private static String staticDesc = "外部类的静态私有变量";
      private String instanceDesc = "外部类的普通实例变量";
    
      public static class StaticInner {
        public void testAccess() {
          // 1. 访问静态成员
          System.out.println(staticDesc); // ✅ 正确:直接访问外部类的静态成员
    
          // 2. 访问非静态成员
          // System.out.println(instanceDesc); // ❌ 报错:无法直接从静态上下文中引用非静态变量
          
          Outer outer = new Outer(); // ✅ 必须通过实例化外部类才能访问非静态成员
          System.out.println(outer.instanceDesc);
        }
      }
    }

应用场景

为什么要用静态内部类(黄金应用场景):

既然它和普通的类这么像,为什么不直接在外面单独写一个类呢?

  1. 场景 A:高内聚的辅助类(Namespace 分组)

    如果一个类 B 只为类 A 服务,并且不依赖 A 的对象状态,将 B 作为 A 的静态内部类,可以隐藏 B 的存在,让包结构更干净,代码具备极强的高内聚性。

    例如:Java 源码中 HashMapNode<K,V> 节点类,就是一个静态内部类。因为节点本身只是一种数据结构,不需要知道整个 HashMap 对象的具体状态。

  2. 场景 B:大名鼎鼎的 Builder(建造者)模式

    这是静态内部类最经典的实战场景。 建造者模式通过一个静态内部类来链式构造一个复杂的外部类对象。

    java
    public class User {
      private final String name; // 必填
      private final int age;     // 可选
    
      // 1. 私有化构造器,强制通过 Builder 创建对象
      private User(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
      }
    
      // 静态内部类 Builder
      public static class Builder {
        private String name;
        private int age = 0; // 默认值
    
        // 2. 必填属性通过构造器传入
        public Builder(String name) {
          this.name = name;
        }
    
        // 3. 可选属性通过链式调用
        public Builder age(int age) {
          this.age = age;
          return this; // 返回当前 Builder 对象
        }
    
        // 4. 最终构建外部类对象
        public User build() {
          return new User(this);
        }
      }
    }
    
    // 使用场景
    User user = new User.Builder("Alice").age(25).build();
  3. 场景 C:避免内存泄漏(特别是在 Android 和长生命周期应用中)

    普通成员内部类会隐式持有外部类的引用(Outer.this)。如果内部类的生命周期比外部类长(例如作为一个后台线程、定时任务或事件监听器),就会导致外部类哪怕没用了,也无法被垃圾回收器 (GC)回收,从而引发内存泄漏

    改为静态内部类 + 弱引用(WeakReference) 是解决这类内存泄漏的标准答案。

静态内部类 vs 成员内部类

总结对比:静态内部类 vs 成员内部类:

特性成员内部类 (Member Inner Class)静态内部类 (Static Nested Class)
static 修饰符
依赖外部类实例是(必须先 new Outer()否(直接 new Outer.Inner()
持有外部类引用是(隐式持有 Outer.this
访问外部类静态成员可以直接访问可以直接访问
访问外部类非静态成员可以直接访问不可直接访问
内存泄漏风险较高(容易因隐式引用导致泄漏)极低(互相独立)

局部内部类

在 Java 中,局部内部类(Local Inner Class) 就像是方法里的“私有财产”。它是定义在外部类的一个方法或者某个代码块(比如 if 语句块、for 循环、构造代码块等)内部的类。

如果说成员内部类是类的“实例变量”,那么局部内部类就是类的“局部变量”。它的存在感在日常开发中相对较低,但理解它对于彻底掌握 Java 的作用域和闭包(Closure)概念至关重要。

核心特性

访问权限

核心特性:访问权限(类似局部变量)

  • 访问外部类成员:可以无限制、直接访问(前提是所在方法非静态)。

  • 外部类访问局部内部类的方法:可以在内部类所在方法中创建内部类实例,通过该实例调用内部类的方法。

  • 外部其他类无法访问局部内部类:局部内部类的地位是一个局部变量。


作用域受限

核心特性:极度受限的作用域(类似局部变量):

  • 作用域受限

    局部内部类的作用域仅限于定义它的那个方法或代码块内部。出了这个大括号 {},外界(哪怕是同一个外部类中的其他方法)对它一无所知,更无法使用它。

  • 没有访问修饰符(除了 final

    • 就像你不能在一个方法里给局部变量加上 publicprivate 一样,局部内部类也不能使用 publicprotectedprivatestatic 来修饰
    • 可以用 final 修饰,表示不能在当前方法中被继承,局部变量也可以用 final 修饰。
  • 实例化位置受限

    你必须在定义了局部内部类之后,且在同一个方法/代码块内部,才能通过 new 关键字来创建它的对象。

基础示例

来看一个标准的使用场景:在方法内部定义使用局部内部类。

java
public class Outer {
  private String outerField = "外部类成员变量";

  public void myMethod() {
    // 方法内的局部变量
    String localStr = "方法局部变量";

    // 1. 定义局部内部类
    class LocalInner {
      public void printInfo() {
        // a. 可以直接访问外部类的成员变量和方法
        System.out.println(outerField);

        // b. 也可以访问当前方法的局部变量和方法(有特殊限制,后文详述)
        System.out.println(localStr);
      }
    }

    // 2. 只能在方法内部实例化
    LocalInner inner = new LocalInner();
    inner.printInfo();
  }

  public void anotherMethod() {
    // 3. ❌ 报错:在这里完全不知道 LocalInner 的存在
    // LocalInner inner = new LocalInner();
  }
}

灵魂考点:Effectively Final 规则

灵魂考点:Effectively Final 规则(重点!):

局部内部类最大的坑,也是面试最爱问的地方:局部内部类访问所在方法的局部变量时,该变量必须是 final 的。

从 Java 8 开始,引入了 Effectively Final(事实上的 final) 概念:你可以不显式地写 final 关键字,但这个变量在初始化之后,绝对不能再被修改。如果尝试修改它,编译器会直接报错。

java
public class Outer {
  public void test() {
    int count = 10; // 事实上的 final (effectively final)

    class LocalInner {
      public void display() {
        System.out.println(count);
      }
    }

    // ❌ 如果在这里修改 count,上面的 display() 方法里的 count 就会报错!
    // count = 20;

    new LocalInner().display();
  }
}

为什么会有这个奇葩的限制

为什么会有这个奇葩的限制(底层原理):

这本质上是生命周期不一致引发的问题:

  1. 局部变量的生命周期:存放在 JVM 的**栈(Stack)**内存中。当 test() 方法执行完毕后,栈帧出栈,局部变量 count 就灰飞烟灭了。

  2. 局部内部类对象的生命周期:通过 new 创建出来的对象存放在 JVM 的**堆(Heap)**内存中。即使 test() 方法执行结束,只要这个内部类对象还在被引用,它就不会被垃圾回收器(GC)回收。

矛盾点:如果方法结束了,变量 count 销毁了,但局部内部类的对象还活着,并且还要调用 display() 去访问 count,去哪找呢?

Java 的解决手段(变量拷贝)

Java 编译器在编译局部内部类时,会偷偷把局部内部类要访问的局部变量,复制一份作为内部类的私有成员变量

为了保证“方法里的原版变量”和“内部类里的复刻版变量”数据永远保持一致,Java 强行规定:这个变量不准改!(即必须是 final)。

编译后的底层产物

编译后的底层产物:

因为不同的方法里面可能会起同名的局部内部类,为了防止类名冲突,编译器在生成 .class 文件时,会加上数字序号来区分。

编译上述代码后,会生成类似这样的文件:

  • Outer.class
  • Outer$1LocalInner.class (带有数字前缀的类名)

局部内部类存在的意义是什么

局部内部类存在的意义是什么:

老实说,在现代 Java 开发中,你几乎不会直接手写局部内部类

它的存在主要是为了解决极其特殊的场景:

  1. 你需要在方法内部实现某个接口(或者继承某个类)。

  2. 并且,你在该方法内部需要多次实例化这个实现类(如果只实例化一次,我们通常会使用它的“究极进化版”——匿名内部类)。

匿名内部类@

概述

匿名内部类是 Java 内部类家族中出场率最高、在实际开发中最常用的一位。

如果说局部内部类是为了在方法内部重用代码,那么匿名内部类就是为了“用完即走”。当你只需要实例化某个类(或接口的实现类)一次时,专门为它写一个完整的类显得极其臃肿,这时候匿名内部类就派上用场了。

匿名内部类(Anonymous Inner Class) 本质上是一个没有名字的局部内部类。

既然没有名字,它就无法在后面被再次调用,因此它的定义和实例化必须在同一时间完成

基本语法

基础语法结构

java
new 父类构造器(参数) 或 接口() {
  // 匿名内部类的类体部分
  // 可以重写方法,也可以定义自己的变量和方法(但外界无法调用自定义的方法)
};

注意:末尾必须要有一个分号 ;,因为它本质上是一个表达式(也就是一行代码)。

应用场景

经典应用场景与代码演示:

匿名内部类最常见的场景是:创建线程(Runnable)事件监听器(Listener)、以及集合排序(Comparator)

  1. 场景 A:实现接口(以多线程为例)

    假设你想启动一个线程,传统做法是写一个类实现 Runnable 接口,然后再把这个类的对象传给 Thread。使用匿名内部类,你可以一步到位:

    java
    public class Test {
      public static void main(String[] args) {
        // 写法一:
        // 1. 直接 new 接口,并在花括号内实现抽象方法
        Runnable myRunnable = new Runnable() {
          @Override
          public void run() {
            System.out.println("匿名内部类实现的线程正在运行!");
          }
        };
    
        // 2. 启动线程
        new Thread(myRunnable).start();
    }
    java
    public class Test {
      public static void main(String[] args) {
        // 写法二:
        // 甚至可以写得更极致(直接当做参数传递):
        new Thread(new Runnable() {
          @Override
          public void run() {
            System.out.println("极致缩写的匿名内部类!");
          }
        }).start();
      }
    }
  2. 场景 B:继承父类并重写方法

    除了实现接口,匿名内部类也可以用来继承一个普通的类或抽象类,并重写它的方法。

    java
    class Animal {
      public void speak() {
        System.out.println("动物发出叫声");
      }
    }
    
    public class Test {
      public static void main(String[] args) {
        // 1. 创建一个继承自 Animal 的匿名内部类对象,并重写 speak 方法
        Animal dog = new Animal() {
          @Override
          public void speak() {
            System.out.println("汪汪汪!");
          }
        };
        
        // 2. 调用实例对象的方法
        dog.speak(); // 输出:汪汪汪!
      }
    }

匿名内部类的三大限制

匿名内部类的“三大限制”:

虽然它很好用,但因为它没有名字,所以在语法上有几条铁律:

  1. 二选一:匿名内部类必须且只能继承一个父类,或者实现一个接口。不能同时继承类又实现接口,也不能实现多个接口

  2. 没有构造器:因为构造器的名字必须和类名一样,而匿名内部类没有名字,所以它不可能有显式的构造方法。如果需要初始化,通常利用构造代码块 { ... } 来初始化

  3. 闭包限制(Effectively Final):因为它本质上还是一个“局部内部类”,所以它如果想访问所在方法的局部变量,该局部变量依然必须是 final 或者事实上不可变(effectively final) 的(底层原因我们在上一篇提到过,是为了防止局部变量被销毁后内外数据不一致)。

底层编译产物

底层编译产物:

因为没有名字,Java 编译器在把匿名内部类编译成 .class 文件时,会使用数字编号来给它命名,该类名可以通过 实例.getClass() 返回。

如果在 Outer 类中定义了两个匿名内部类,编译后会生成:

  • Outer.class
  • Outer$1.class (第一个匿名内部类)
  • Outer$2.class (第二个匿名内部类)

被 Lambda 替代

时代变迁:被 Lambda 表达式“降维打击”:

在 Java 8 之前,匿名内部类是处理回调和单次实现的王者。但它依然有一个缺点:太啰嗦了,模板代码太多。

Java 8 开始,引入了 Lambda 表达式。对于只有一个抽象方法的接口(即函数式接口,Functional Interface),Lambda 表达式可以直接替代匿名内部类,让代码变得极其优雅。

对比演示:

java
// 时代眼泪:匿名内部类写法
Thread t1 = new Thread(new Runnable() {
  @Override
  public void run() {
    System.out.println("Hello, 匿名内部类!");
  }
});

// 现代写法:Lambda 表达式
Thread t2 = new Thread(() -> System.out.println("Hello, Lambda!"));

关键注意点:

Lambda 表达式只能替代基于“函数式接口”的匿名内部类。如果你要继承一个抽象类,或者实现一个包含多个方法的接口,你依然只能老老实实地使用匿名内部类

练习题【

image-20260306164534393

image-20260306164604802

代码实现:

image-20260306164626264