static 关键字

静态关键字。Java 中的一个修饰符。

静态变量

被 static 修饰的成员变量,叫静态变量。

特点:

  • 被该类所有对象共享
  • 不属于对象而属于类
  • 随着类的加载而加载,优先于对象存在

调用方式:

  • 类名调用(推荐)
  • 对象名调用

静态方法

被 static 修饰的成员方法,叫静态方法。

特点:

  • 多用在测试类和工具类中
  • JavaBean 类中很少使用

调用方式:

  • 类名调用(推荐)
  • 对象名调用

一些类

  1. javabean 类:描述一些具体事物
  2. 测试类:用于检查其他类是否书写正确,带有 main 方法,是程序的入口
  3. 工具类:不描述事物,但帮我们做一些事情的类

工具类

帮助我们做一些事情,但不描述任何事物的类。需要满足以下要求:

  • 类名要做到见名知意
  • 使用私有化构造方式
  • 方法定义为静态方便调用

static 的注意事项

  • 静态方法只能访问静态变量和方法
  • 非静态方法既可以访问静态变量方法又可以访问非静态变量与方法
  • 静态方法中没有 this 关键字

继承

继承的概述

面向对象三大特征:封装、继承、多态。

先复习一下前面的知识:什么是封装?——对象代表什么,就要封装什么数据,并提供对应的行为。

如果我们此时封装两个类,一个 Student,一个 Teacher。他们都需要吃饭喝水睡觉,代码重复率有点高了。

这时就有了继承——Java 中提供 extends 关键字,可以让两个类建立起继承关系。

例如在如下代码中:

public class Student extends Teacher {}

Student 被成为子类(派生类),Person 被称为父类(基类、超类)。

这样就把重复代码抽取到父类中了,提高代码复用性。

子类能够继承的父类的内容:

public | 虚方法表private | 非虚方法表
构造方法××
成员变量
成员方法×

继承的特点

  • Java 只支持单继承(一个子类只能继承一个父类),不支持多继承(同上),但支持多层继承(间接父类、直接父类)。
  • 每一个类都直接或间接地继承于 Object 类(若无继承,JVM 自动添加默认继承关系)。
  • 子类只能访问父中非私有的成员

继承的体系

类之间形成树状层次结构。

虚方法表

虚方法表是一种内部数据结构,每个类在加载时会生成自己的虚方法表,用于支持方法的动态绑定(即运行时多态)。只有父类的虚方法才会被子类继承。

不在虚方法表中的方法:

  • private 修饰的方法:由于无法被子类重写,因此不会存储在虚方法表中。
  • static 修饰的方法:静态方法束缚于类本身,不会被重写。
  • final 修饰的方法:无法在子类中被重写,因而不在虚方法表中。

成员变量与方法的访问特点

成员变量的访问特点:就近原则。

方法的重写

重写的概述

父类的方法不能满足子类时,需要进行方法的重写。

继承体系中,若子类出现了和父类一样的方法声明,我们就称这个子类的方法是重写的方法。

子类重写的方法上方需要加上 @Override 进行提示。

重写的要求

  1. 重写的形参、名称必须与父类一致
  2. 子类重写父类方法时,访问权限必须≥父类
  3. 子类重写父类方法时,返回值类型必须≤父类
  4. 私有方法不能被重写
  5. 静态方法不能被重写

4 5 两条其实是因为方法重写的本质是覆盖了继承过来的虚方法表中的方法,而私有和静态方法都不能被存入虚方法表,自然无法被重写。所以这两条可以合起来:只有被添加进虚方法表的方法才能被重写。

构造方法的访问特点

子类中的所有构造方法默认先访问父类的无参构造方法,再执行自己。

这是因为子类初始化过程中有可能需要使用父类的数据。若父类未完成初始化,子类无法使用父类数据。所以,子类在初始化前,会先调用父类无参构造方法完成父类的初始化。

子类构造方法第一句默认是 super(),即使不主动写,也会被自动添加。

如果想要访问父类的有参构造,需要手动书写 super(method)

this 与 super 关键字的使用

this 关键字表示当前方法调用者的地址值。

super 代表父类存储空间。

多态

什么是多态

同类型的对象表现出不同的形态。

父类类型 对象名 = 子类对象;

多态的前提:

  • 有继承关系
  • 有父类引用指向子类对象
  • 有方法重写

示例:

public static viod method(Person p){
    执行体;
}

该代码便意味着此时可以传递 Person 类的所有子类对象。这边实现了方法的通用化。

// 定义父类和方法
public class Animal {
    public void makeSound() {
        System.out.println("动物叫");
    }
}

public class Dog extends Animal {
    public void makeSound() {
        System.out.println("汪汪汪");
    }
}

public class Cat extends Animal {
    public void makeSound() {
        System.out.println("喵喵喵");
    }
}

// 关键:方法参数是父类类型
public class Test {
    // 这个方法可以接收任何 Animal 及其子类
    public void letAnimalSpeak(Animal animal) {
        animal.makeSound(); // 这里会动态绑定,实际执行子类的方法
    }
    
    public static void main(String[] args) {
        Test test = new Test();
        test.letAnimalSpeak(new Dog()); // 输出:汪汪汪
        test.letAnimalSpeak(new Cat()); // 输出:喵喵喵
    }
}

多态调用成员的特点

变量调用:编译看左边,运行也看左边

方法调用:编译看左边,运行看右边

  • 编译看左边:编译器只根据引用变量的声明类型(左边类型)来判断该引用能访问哪些成员。如果左边类型中没有定义该成员,则编译报错。
  • 运行看左边 / 右边:在程序运行时,对于成员变量,访问的是左边类型中定义的变量(无多态);对于实例方法,实际执行的是右边实际对象类型中重写的方法(有多态)。

一、变量调用

成员变量(字段)没有多态性。无论是编译时还是运行时,访问的都是引用变量声明的类型中的变量。

示例

class Animal {
    String name = "动物";
}

class Dog extends Animal {
    String name = "狗";
}

public class Test {
    public static void main(String[] args) {
        Animal animal = new Dog();   // 向上转型
        System.out.println(animal.name); // 输出:动物
    }
}
  • 编译:编译器检查 animal 是 Animal 类型,Animal 类中有 name 字段,编译通过。
  • 运行:运行时 animal 引用指向 Dog 对象,但访问 name 时,仍然取的是 Animal 类中的 name(值为“动物”),而不是 Dog 类中的 name(值为“狗”)。
    因为字段不参与多态,它由声明类型决定。

二、 方法调用

实例方法有多态性(动态绑定)。编译时确保左边类型中有该方法,但运行时实际执行的是右边实际对象类型中重写的方法。

class Animal {
    void makeSound() {
        System.out.println("动物叫");
    }
}

class Dog extends Animal {
    void makeSound() {
        System.out.println("汪汪汪");
    }
}

class Cat extends Animal {
    void makeSound() {
        System.out.println("喵喵喵");
    }
}

public class Test {
    void letAnimalSpeak(Animal animal) {
        animal.makeSound();   // 多态调用
    }
    
    public static void main(String[] args) {
        Test test = new Test();
        test.letAnimalSpeak(new Dog());   // 输出:汪汪汪
        test.letAnimalSpeak(new Cat());   // 输出:喵喵喵
    }
}
  • 编译animal 是 Animal 类型,Animal 类中有 makeSound() 方法,编译通过。
  • 运行:当 animal 实际指向 Dog 对象时,执行 Dog 的 makeSound();指向 Cat 时,执行 Cat 的 makeSound()
    这就是多态:同一个方法调用,根据实际对象类型产生不同的行为。

多态的内存情况

最令编程初学者(指我)头大的环节。

JVM 运行时主要涉及三块内存区域:

  • 栈(Stack):存放局部变量、方法调用栈帧。每个线程私有。
  • 堆(Heap):存放所有对象实例(包括对象的实例变量)。线程共享。
  • 方法区(Method Area):存放类信息(类元数据、静态变量、常量池、虚方法表等)。JDK 8+ 为元空间(Metaspace)。

我们以如下代码为示例:

class Animal {
    String name = "动物";
    void makeSound() {
        System.out.println("动物叫");
    }
}

class Dog extends Animal {
    String name = "狗";
    void makeSound() {
        System.out.println("汪汪汪");
    }
    void wagTail() {
        System.out.println("摇尾巴");
    }
}

public class Test {
    public static void main(String[] args) {
        Animal animal = new Dog();
        animal.makeSound();
        System.out.println(animal.name);
    }
}

1.方法区中的虚方法表

Animal 虚方法表:
[0] : Object 的方法(如 toString, hashCode 等)
[1] : makeSound() → Animal.makeSound 的入口

Dog 虚方法表(继承自 Animal):
[0] : Object 的方法(覆盖了 Object 的)
[1] : makeSound() → Dog.makeSound 的入口  // 覆盖了 Animal 的方法
[2] : wagTail() → Dog.wagTail 的入口      // 新增方法,追加在表末尾

因为 Dog 重写了方法,所以虚方法表中的 makeSound() 方法被覆盖。至此,所有字节码文件加载完毕。

2.运行阶段:内存分配

Animal animal = new Dog();

(1) 堆内存:创建 Dog 对象

堆内存中的 Dog 对象结构(简化):
┌─────────────────────────┐
│ 对象头(标记字、类指针)    │  → 类指针指向方法区中 Dog 的类元数据
├─────────────────────────┤
│ 实例变量(从父类继承)      │
│   name(Animal 的)      │  "动物"
├─────────────────────────┤
│ 实例变量(子类定义)        │
│   name(Dog 的)         │  "狗"
└─────────────────────────┘

(2) 栈内存:main 方法的局部变量表

栈帧(main 方法):
┌──────────────┐
│ animal 引用   │ → 指向堆中 Dog 对象的起始地址
│ ...          │
└──────────────┘

animal 的声明类型是 Animal(编译时类型),但它存储的地址指向一个实际的 Dog 对象(运行时类型)。

3.方法调用:animal.makeSound();

编译阶段(编译器检查)

  • 编译器看到 animal 的声明类型是 Animal
  • 检查 Animal 类中是否有 makeSound() 方法 → 有,编译通过。
  • 生成字节码指令:invokevirtual #1(#1 对应方法区中 Animal 虚方法表里的 makeSound 索引)。

运行时(动态绑定)

  1. 通过 animal 引用找到堆中的 Dog 对象。
  2. 从对象的对象头中获取类指针,定位到方法区中 Dog 的类元数据。
  3. 根据 invokevirtual 指令中携带的索引(比如索引 1),在 Dog 的虚方法表中找到实际要执行的方法入口。
  4. 由于 Dog 的虚方法表索引 1 处存储的是 Dog.makeSound 的入口,所以执行 Dog 的 makeSound(),输出“汪汪汪”。

这就是“编译看左边,运行看右边”的内存依据:编译时只需确认左边类型有该方法;运行时通过实际对象的虚方法表找到重写后的版本。

用人话说,就是因为:

  • 方法虚方法表(vtable):编译时确定方法在表中的索引,运行时根据对象实际类型的表找到对应方法入口。
  • 字段固定偏移量:编译时就把字段在父类中的内存位置算死,运行时直接按那个位置取,跟对象实际类型无关。

那么,调用方法时,实际上是因为子类重写了父类的方法,

┌─────────────────────────────────────┐
│         Animal 类元数据              │
├─────────────────────────────────────┤
│  虚方法表(Animal 自己的)             │
│  ┌───────────┬───────────────────┐  │
│  │ 索引 0    │ Object 方法们       │  │
│  │ 索引 1    │ → Animal.makeSound │  │
│  └───────────┴───────────────────┘  │
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│          Dog 类元数据                │
├─────────────────────────────────────┤
│  虚方法表(Dog 自己的)                │
│  ┌───────────┬───────────────────┐  │
│  │ 索引 0    │ Object 方法们       │  │  ← 从 Animal 复制过来的
│  │ 索引 1    │ → Dog.makeSound    │  │  ← 覆盖了原来 Animal.makeSound 的指针
│  │ 索引 2    │ → Dog.wagTail      │  │  ← 追加的新方法
│  └───────────┴───────────────────┘  │
└─────────────────────────────────────┘

运行时就会执行子类自己的方法。

变量(字段)就应为写死了父类地址值。如果字段也支持多态(即访问子类的字段),那么同一个偏移量在子类对象中可能指向不同的内存位置(因为子类可能在父类字段后追加新字段,偏移量会变),这会导致混乱。所以 Java 规定字段没有多态,直接按声明类型的偏移量访问。

字段的偏移量

学着学着又出来一个偏移量,让人头大。

偏移量的存在是为了快速访问字段
CPU 读取内存时,最直接的指令就是“基址 + 偏移量”。编译器在编译阶段就为每个字段算好偏移量,生成的机器码里直接硬编码这个数字,运行时一算地址就能取到值,没有任何中间步骤。

这种设计使字段访问效率极高,和 C 语言的结构体一样快。

——以上就是 AI 给我的答复,但这实在太难理解了。所以,我们不妨来打个比方:

如果每个对象就像一个档案柜,字段就像抽屉,那么偏移量就是抽屉的编号。

  • 每个抽屉里放一个字段(变量)。
  • 偏移量就是抽屉的编号(第1个抽屉、第2个抽屉…)。

编译时,编译器根据声明的类型(Animal)来分配抽屉编号:

  • Animal 的 name 放在第 1 号抽屉。
  • 如果有其他字段,按顺序往下排。

它不会去管这个抽屉柜是不是 Dog 类型,因为 Dog 的抽屉柜只是在第1号之后多加了一些抽屉,第1号抽屉的位置没变过。

所以“变量看左边”就是,编译器根据左边类型知道要拿哪个编号的抽屉,运行时就直接拿那个抽屉。

多态的优势和弊端

优势

多态形态下,右侧对象可以实现解耦,提高可维护性,便于拓展和维护。

例如:

Person p = new Student;
p.work;
// 如果我以后想将 Student 改为 Teacher,只需修改为如下代码即可(而无需重构一遍代码)
Person p = new Teacher;
p.work;
// 由于方法的动态绑定,这里的 p.work 实际是调用 Teacher 的 work 方法,提高了可维护性

弊端

  1. 性能开销:方法调用需要通过虚方法表间接寻址,比静态方法调用多一次内存访问,对高频调用的场景有微小性能损耗
  2. 代码可读性降低
  3. 破坏封装性:为了实现多态,父类的方法必须暴露给子类(protected 或 public),这可能让内部实现细节泄露
  4. 多态链较长时,错误堆栈可能跨多个类,排查更耗时

包,就是”文件夹“,用于管理不同的 Java 类,方便后续维护。

包名的命名规则:公司域名反写 + 包的作用,同时保持全部小写。

使用包时,使用 import 进行导入。

使用其它类的规则:

  • 使用同一个包中的类时,不需要导包。
  • 使用 java.lang 中的类时,不需要导包。——这是 java 的核心包。
  • 其他情况都需要导包。
  • 如果同时使用两个包中的同名类,需要使用全类名。

final 关键字

意为不可改变的。

对方法来说,final 关键字表明该方法是最终方法,不能被重写。

对于类,表明该类是最终类,不能被继承。

对于变量,便叫作常量——一旦赋值,就不能再发生改变。

我们来演示一下:

final int a = 10;
a = 20;
// 语法错误无法编译

关于常量

实际开发中,常量一般作为系统的配置信息,方便维护,提高可读性。

常量的命名规范:

  • 单个单词:全部大写
  • 多个单词:全部大写,单词间使用_隔开

细节:

如果 final 修饰的变量是基本类型,那么变量存储的数据值不可改变。

如果 final 修饰的变量是引用类型,那么变量存储的地址值不能发生改变,而对象内部的可以改变。

示例:

假设我们现在写好了一个标准的 Student JavaBean 类,包含 NameAge 成员,我们再书写如下代码:

final Student S = new Student(name:"zhangsan",age:23)
S.setName("李四");
S.setAge(24);
sout(S.getName() + "," + S.getAge)

程序运行后应当输出 李四,23。这就说明了对象内部是可以发生改变的。

权限修饰符

用来控制一个成员能够被访问的范围。

我们之前接触到的 private 就是一种权限修饰符。

private < 默认 < protected < public

修饰符/能否访问同一个类中同一个包中其它类不同包下的子类不同包下的无关类
private
留空
protected
public

实际开发中,一般只使用 private 与 public

  • 成员变量私有
  • 方法公开

特例:如果方法中的代码是抽取其它方法中的共性代码,这个方法一般也私有。

代码块

可分为三类:

  • 局部代码块(方法里)
  • 构造代码块(方法外,类里)
  • 静态代码块

局部代码块

public class Test {
    public static void main(String[] args){
        {
          int a = 10;
          sout(a);
        }
        // 局部代码块,只有在局部代码块中才可以使用变量 a。局部代码块后,a 就从内存中消失了
    }
}

所以这项技术可以用于节约内存。以前机载 RAM 较小时(例如 128k),十分广泛使用。现在很少使用。

构造代码块

构造代码块其实就是写在成员位置的代码块。创建对象是,优先于方法执行。我们可以将重复的代码写在构造代码块中。

例如,我们现在有一个 JavaBean 类如下:

package test;

public class Student {
    String name;
    int age;

    public Student() {
        System.out.println("开始创建对象");
    }

    public Student(String name, int age) {
        System.out.println("开始创建对象");
        this.name = name;
        this.age = age;
    }

    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;
    }
}

在有参构造会空参构造时,都是 Sout“开始创建对象”,我们就可以把它提取出来写在构造代码块部分从而提高代码可维护性:

public class Student {
    String name;
    int age;

    {
        System.out.println("开始创建对象");
    }

    public Student() {
    }

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

    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;
    }
}

二者效果是一致的。

静态代码块

static{}

特点:需停药通过 static 关键字修饰,随着类的加载而加载,并且自动触发,只执行一次。

使用场景:在类加载的时候,做一些数据初始化的时候使用。

抽象方法与抽象类

抽象方法使用 abstract 关键字修饰,被修饰的方法子类强制重写,否则报错。抽象方法所在的类成为抽象类。

抽象方法:将共性的行为抽取到父类之后,由于每一个子类执行的内容是不一样的,所以,父类中不能确定具体的方法体,该方法就可以定义为抽象方法。

抽象类:如果一个类中存在抽象方法,那么该类就必须声明为抽象类。

定义格式:

// 抽象方法的定义
public abstract 返回值类型 方法名(参数列表);
// 抽象类的定义
public abstract class 类名{}

抽象类与抽象方法的注意事项

  • 抽象类不能实例化
  • 抽象类可以有构造方法
  • 抽象类的子类要么重写抽象类中所有的抽象方法,要么本身本身也是一个抽象类

那么问题来了,既然说“抽象类可以有构造方法”,可是,抽象类无法实例化成对象,构造方法又有什么用呢?

因为抽象类作父类时,也需要抽取子类共有的特性。我们就可以通过这个特性在创建子类对象时,给这个抽象类的属性进行赋值。

这里注意一下,抽象类与方法的重写是不太一样的。

  • 抽象类是类层面的设计,用于定义“是什么”,强调继承和规范。
  • 方法重写是方法层面的行为,用于实现“怎么做”,强调多态和灵活性。

抽象类与抽象方法的意义

我不抽取共性内容到父类,直接写在子类不是更节约代码吗?

抽象类是类层面的设计,强调继承和规范。抽象类的适当使用可以降低代码混乱的可能,因为子类必须强制重写,所以各子类方法一定是统一的。

接口

如果我想给一个继承体系添加一个新的方法,但子类中有不包含这一共性的子类,我们就不能直接更改父类。但如果在各个需要的子类中各自添加这一功能,又有可能导致代码不统一的问题。

那么我们就可以使用 abstract 关键字修饰一个方法,作为带有该功能规则的接口。

接口不是具体的事物,就是一种规则,是对行为的抽象。

定义和使用接口

接口使用 interface 关键字来定义:

public interface 接口名 {}

接口不能实例化。

接口和类之间是实现关系,使用 implements 关键字:

public class 类名 implements 接口名 {}

对于接口类的子类即实现类,要么重写接口中所有抽象方法,要么本身就得是抽象类。

注意1

接口和类的实现关系,可以多实现:

public class 类名 implements 接口名1, 接口名2 {}

注意2

实现类还可以在继承一个类的同时实现多个接口:

public class 类名 extends 父类  implements 接口名1, 接口名2 {}

接口中成员变量的特点

  • 只能是常量,默认修饰符:public static final
  • 无构造方法
  • 一开始接口中只能定义抽象方法。JDK8 开始,接口中可以定义有方法体的方法。JDK9 开始,接口中可以定义私有方法。

JDK8 开始接口新增的方法

JDK8 开始,接口中可以定义有方法体的方法。JDK9 开始,接口中可以定义私有方法。

JDK7 及以前,一个接口类如果要添加新方法,那么所有实现类就必须跟着一起更改,否则实现类就会报错。——这时候就诞生了接口升级。

默认方法(Default Method)

  • 用 default 关键字修饰,可以有方法体。
  • 主要用于在不修改所有实现类的情况下,为接口添加新功能。实现类可以选择继承默认实现,也可以重写(覆盖)它。
interface Vehicle {
    default void start() {
        System.out.println("车辆启动");
    }
}

class Car implements Vehicle {
    // 可以继承默认实现,也可以重写
}

静态方法(Static Method)

  • 用 static 关键字修饰,有方法体。
  • 接口中的静态方法只能通过接口名调用,不能通过实现类实例调用。
  • 通常用于提供与接口相关的工具方法,避免单独创建工具类。
interface MathUtils {
    static int add(int a, int b) {
        return a + b;
    }
}

// 调用方式
int sum = MathUtils.add(3, 5);

私有方法(Private Method,Java 9+)

  • 用 private 关键字修饰,可以有方法体。
  • 用于在接口内部提取公共逻辑,供默认方法或静态方法复用,但不对外暴露。
  • 增强了接口内部的代码组织和可维护性。
interface Calculator {
    default int add(int a, int b) {
        return compute(a, b);
    }
    
    default int subtract(int a, int b) {
        return compute(a, -b);
    }
    
    // 私有方法,仅接口内部使用
    private int compute(int a, int b) {
        return a + b;
    }
}

可以看出来,十分地规范,十分地好用呐。

接口的多态

public interface transportation {}
public void 搬家(transportation c) {}

这样,搬家(车的对象)搬家(搬家公司)只要完成了 transportation 接口的继承,就都可以被传递至搬家这个方法,这就是接口的多态。也就是 接口类型 c = new 实现类对象;。同样遵循编译看左边,运行看右边的原则。

适配器设计模式

// 1. 目标接口
interface Target {
    void request();
}

// 2. 适配者(已有类,接口不匹配)
class Adaptee {
    void specificRequest() {
        System.out.println("适配者:执行具体功能");
    }
}

// 3. 适配器(实现目标接口,内部组合适配者)
class abstract Adapter implements Target {
    private Adaptee adaptee;

    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void request() {           // 这是客户端调用的方法
        // 适配器将 request 调用转换成 specificRequest 调用
        adaptee.specificRequest();    // 适配器调用适配者
    }
}

// 4. 客户端
public class Client {
    public static void main(String[] args) {
        // 客户端只知道目标接口
        Target target = new Adapter(new Adaptee());
        
        // 客户端调用目标接口的方法
        target.request();   // 这里实际上调用的是适配器的 request()
        // 输出:适配者:执行具体功能
    }
}

适配器使用 abstract 修饰防止被创建对象。

内部类

概述

类有五大成员:属性、方法、构造方法、代码块、内部类。

内部类,其实就是在一个类里再定义一个类。

例如车与引擎的关系。引擎是一个独立的个体,理应当拥有自己的属性。但同时引擎的存在又离不开汽车,这种关系便可定义为内部类:

public class Car {
    String carName;
    int carColor;
    class Engine {
        String engineName;
    }
}

内部类表示的事物是外部类的一部分。内部类单独出现没有任何意义。

  • 内部类可以直接访问包括私有的外部类成员。
  • 外部类若要访问内部类成员,必须先创建对象。

成员内部类

比方说刚刚提到的引擎与汽车的例子。引擎就是一个成员内部类。

  • 内部类可以直接访问包括私有的外部类成员(无条件访问外部类的所有成员)。
  • 外部类若要访问内部类成员,必须先创建对象。
  • 依赖外部类的实例存在,必须先创建外部类对象,才能创建内部类对象

适用场景:当内部类需要持有外部类的实例信息,且与外部类紧密相关时使用。

静态内部类

在成员内部类的基础上,加上 static 关键字。

  • 不依赖外部类的实例,可以独立创建对象
  • 只能访问外部类的静态成员,不能直接访问外部类的实例变量/方法
  • 它更像一个独立的类,只是为了代码组织方便而放在外部类内部

访问方式:

public class Outer {
    private static String staticName = "静态属性";
    private String instanceName = "实例属性";

    // 静态内部类
    static class StaticInner {
        public void show() {
            System.out.println("只能访问外部类静态属性: " + staticName);
            // System.out.println(instanceName); // 编译错误,无法访问
        }
    }
}

// 使用时,不需要外部类实例
Outer.StaticInner inner = new Outer.StaticInner();
inner.show();

适用场景:当内部类不需要访问外部类的实例状态,但又希望放在外部类中(比如构建者模式)时使用。

局部内部类

定义在方法内部或代码块内部,作用域仅限于该方法/代码块内。

  • 只在定义它的方法内可见,方法执行完就销毁
  • 可以访问外部类的所有成员
  • 如果想访问所在方法中的局部变量,该变量必须是finaleffectively final(即实际不会改变)
public class Outer {
    public void method() {
        int localVar = 10; // 实际上是 effectively final

        // 局部内部类
        class LocalInner {
            public void show() {
                System.out.println("访问外部类成员");
                System.out.println("访问方法内局部变量: " + localVar);
                // 这里不能修改 localVar 的值
            }
        }

        LocalInner inner = new LocalInner(); // 只能在方法内使用
        inner.show();
    }
}

适用场景:临时使用的类,只在某个方法内部需要,不希望被外部访问。

匿名内部类

定义的同时直接实例化,没有类名,通常用于创建接口或抽象类的实现。

  • 必须继承一个类或实现一个接口
  • 在需要的地方直接 new 并实现方法,代码简洁
  • 同样遵守局部内部类的变量访问规则
public class Outer {                // 定义一个外部类
    public void start() {           // 一个普通方法
        // 匿名内部类,实现一个接口
        Runnable task = new Runnable() {   // ① 创建匿名内部类对象
            @Override
            public void run() {            // ② 实现接口中的 run 方法
                System.out.println("匿名内部类执行");
            }
        };
        
        new Thread(task).start();          // ③ 将 task 传给 Thread,并启动线程
    }
}

虽说 Runnable 接口涉及到多线程一类的东西,对现在的我来说有点超纲了。不过大体上还是可以理解的。我创建一个匿名内部类并写好了详细的执行体,然后再去启动第二个线程。

也就是说,此时,主线程会继续执行后续的代码,而 run() 中的代码(指 sout)则被丢到了一个新线程上并发执行。

  • reward_image1
此作者没有提供个人介绍。
最后更新于 2026-03-21