Skip to content

S02-04 面向对象-静态成员

[TOC]

static

在 Java 中,static 是一个出场率极高、也是极其重要的关键字。

如果用一句话来概括 static 的核心思想,那就是:“属于类,而不属于对象”。 它打破了面向对象中“一切皆需实例化”的常规,提供了一种全局共享的机制。在 Java 中,static 关键字主要有 5 种 使用场景。我们将它们梳理成一份完整的“全景图”。

核心特性@

深入剖析 static 的核心特点是非常有必要的!如果说上一节我们看的是 static能干什么(应用场景)”,那么这一节我们来透视它 “本质上是什么(核心特点)”。

理解了这几个核心特点,以后遇到任何关于 static 的报错或面试题,你都能一眼看穿其底层逻辑。

我们可以把 static 的核心特点总结为以下 5 个维度

  1. 归属性:属于类,而非属于对象:

    这是 static 最根本的特点,也是其他所有特点的基础。

    • 普通成员: 是“私有财产”。必须先有具体的对象(比如先造出一辆具体的特斯拉),才能讨论它的颜色、速度。
    • static 成员: 是“公共设施”或“图纸属性”。它直接绑定在“类”这图纸上。即使你一个对象都没有创建static 属性和方法也已经存在,并且可以直接通过 类名.成员名 来使用。
  2. 唯一性:内存中只有一份拷贝:

    不管你用这个类 new 出了 1 个对象,还是 10000 个对象,被 static 修饰的变量在内存中永远只有一份

    • 内存位置: 它不存放在专门存放对象的堆内存(Heap)中,而是存放在方法区(Method Area / 现代 JVM 称之为 Metaspace 元空间) 的静态存储区里。
    • 现实比喻: 就像一个班级(类)里有 50 个学生(对象)。每个学生都有自己的课本(实例变量),但教室前面只有一块黑板(static 变量)。黑板只有一块,所有人看的内容都是一样的。
  3. 先后性:初始化时机极早:

    static 成员的出生时间比对象要早得多。

    • 加载时机: 当 JVM 第一次把这个类的字节码(.class 文件)加载到内存时,static 变量就会被分配内存并初始化,static 代码块也会立刻执行。
    • 底层逻辑推演: 因为它生得太早了,这时候堆内存里根本还没有任何该类的对象。这就引出了下面第 4 个、也是程序员最容易踩坑的特点。
  4. 局限性:严格的访问壁垒:

    这是初学者最容易遇到编译报错的地方:“静态方法中不能引用非静态成员”。

    • 静态禁止访问非静态: static 方法内部,绝对不能直接访问实例变量,也不能调用实例方法,更不能使用 thissuper 关键字。

      • 为什么? 用时间线来解释很简单:static 成员出生的时候,对象连个影子都没有(没有 this)。你让一个早早就出生的“静态爷爷”,去叫一个还没出生的“对象孙子”的名字,他上哪找去?

      image-20260227181436521

    • 非静态允许访问静态: 反过来完全没问题。普通对象创建的时候,static 成员早就存在于内存中了,对象随时可以去访问这块“公共黑板”。

  5. 共享与风险性:全局状态的并发灾难:

    因为唯一,所以共享;因为共享,所以危险。

    • 一处修改,处处改变: 任何一个对象修改了 static 变量的值,其他所有对象看到的都会是修改后的最新值。
    • 线程不安全: 在多线程(Web 后端开发最常见)环境下,如果多个线程同时去读写同一个 static 变量,极其容易发生数据覆盖、脏读等线程安全问题 (Race Condition)
    • 这就是为什么在业务开发中,除了定义常量(static final)或者无状态的工具方法外,我们极其反感定义全局的 static 变量。

五大应用场景

五大应用场景

  1. 静态属性
  2. 静态方法
  3. 静态代码块
  4. 静态内部类
  5. 静态导包

静态属性

在 Java 中,静态属性(静态变量,静态字段,Static Field / Variable) 是用 static 关键字修饰的成员变量。

理解静态属性的关键在于一句话:它属于“类”本身,而不属于某个具体的“对象”。

独享 vs 共享

核心概念:独享 vs 共享:

为了理解静态属性,我们先对比一下它和普通(实例)属性的区别。

  • 实例属性 (Instance Variable):

    • 拥有者: 对象。
    • 比喻: 每个人手中的水杯。张三喝了一口他杯子里的水,李四杯子里的水不会变少。每个对象都有自己独立的一份拷贝。
  • 静态属性 (Static Variable):

    • 拥有者: 类。
    • 比喻: 办公室里的饮水机。它是全公司公用的。如果张三把饮水机的水接干了,李四去接水时,饮水机也是干的。所有对象共享同一份数据。

语法与使用

定义方式:在变量类型前加上 static 关键字。

java
public class Student {
  // 实例属性:每个学生的名字不一样
  String name;

  // 静态属性:所有学生都在同一个学校
  static String school = "清华大学";

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

访问方式:虽然你可以用对象去访问静态属性,但这是一种不推荐的写法。推荐直接使用类名访问。

java
public class Main {
  public static void main(String[] args) {
    Student s1 = new Student("张三");
    Student s2 = new Student("李四");

    // 1. 推荐:通过类名访问
    System.out.println(Student.school); // 输出:清华大学

    // 2. 不推荐:通过对象访问 (虽然语法允许,但容易引起误解)
    System.out.println(s1.school);      // 输出:清华大学

    // 3. 修改静态属性:牵一发而动全身
    Student.school = "北京大学";

    System.out.println(s1.school); // 输出:北京大学
    System.out.println(s2.school); // 输出:北京大学 (s2 也跟着变了!)
  }
}

快速入门

入门需求:一群小孩玩堆雪人,不时有新小孩加入,如何统计当前玩游戏的总人数?

  1. 传统方式实现

    java
    public class ChildGame {
      public static void main(String[] args) {
        // 1. 定义计数器
        public int count = 0;
    
        // 2. 每创建一个小孩,计数器 +1
        Child child1 = new Child("白骨精");
        child1.join();
        count++;
    
        Child child2 = new Child("狐狸精");
        child2.join();
        count++;
    
        Child child3 = new Child("老鼠精");
        child3.join();
        count++;
    
        // 3. 打印小孩总数
        System.out.println("共有" + Child.count + " 小孩加入了游戏...");
      }
    }
    java
    class Child {
      private String name;
    
      public Child(String name) {
        this.name = name;
      }
    
      public void join() {
        System.out.println(name + " 加入了游戏..");
      }
    }
    • 问题
      1. count 是定义的独立变量,与 Child 并没有关系,不遵循 OOP 思想
      2. 访问和维护麻烦,不共享于对象
  2. 静态属性实现

    java
    public class ChildGame {
      public static void main(String[] args) {
        Child child1 = new Child("白骨精");
        Child child2 = new Child("狐狸精");
        Child child3 = new Child("老鼠精");
    
        child1.join();
        child2.join();
        child3.join();
    
        // 3. 访问静态属性(类名.静态属性,推荐)
        System.out.println("共有" + Child.count + " 小孩加入了游戏...");
    
        // 不推荐通过对象访问静态属性
        System.out.println("child1.count=" + child1.count); // 3
        System.out.println("child2.count=" + child2.count); // 3
        System.out.println("child3.count=" + child3.count); // 3
      }
    }
    java
    class Child {
      private String name;
      // 1. 定义静态属性(所有实例对象共享)
      public static int count = 0;
    
      public Child(String name) {
        this.name = name;
      }
    
      public void join() {
        System.out.println(name + " 加入了游戏..");
        // 2. 修改静态属性(自增)
        count++;
      }
    }

内存原理@

内存原理 (Under the Hood):

理解内存模型是掌握静态属性的关键。

  1. 存储位置:
    • 实例属性 存储在 堆内存 (Heap) 中,跟随对象的创建而分配。
    • 静态属性
      • Java 8 之前:存储在方法区 (Method Area)的静态区 中。
      • Java 8 及之后:存储在类加载时堆内存中生成的 Class 实例的尾部(在 Java 8+ 称为元空间 Metaspace)。
  2. 生命周期:
    • 出生: 随着类加载 (Class Loading) 而创建。这意味着,即使你没有 new 任何对象,静态属性也可以使用了!
    • 死亡: 随着 JVM 关闭或类被卸载而销毁(通常伴随整个程序的生命周期)。

image-20260228163852041

应用场景

什么时候使用静态属性 (常见场景):

静态属性不能滥用,通常只在以下三种场景使用:

  1. 共享变量 / 计数器:

    当你希望所有对象共享同一个状态时。

    • 示例: 统计一共创建了多少个对象。

      java
      class User {
       // 1. 静态计数器
       public static int count = 0;
      
       public User() {
           // 2. 每次创建对象,计数器 +1
           count++;
       }
      }
      
      // 3. User.count 的值就是当前创建的对象总数
  2. 全局常量 (Constants):

    这是静态属性最常用的场景,通常配合 final 关键字一起使用。

    • 格式: public static final 类型 常量名 = 值;

    • 示例: Math.PI,或者项目配置。

      java
      public class AppConfig {
       // 数据库 URL,全局唯一且不可变
       public static final String DB_URL = "jdbc:mysql://localhost:3306/mydb";
      }
  3. 工具类的数据:

    如果一个类只是提供工具方法(如 Math 类),通常不需要创建实例,其属性也会设为 static

静态代码块

静态代码块 (Static Initialization Block):

如果你需要对静态属性进行复杂的初始化(比如读取配置文件、循环赋值),可以使用静态代码块。

  • 特点: 在类加载时自动执行,且只执行一次

    java
    class Driver {
      static String driverVersion;
    
      // 静态代码块
      static {
        System.out.println("类加载了,初始化驱动...");
        // 模拟复杂的初始化逻辑
        driverVersion = "v1.0.0";
      }
    }
    
    // 第一次访问 Driver 类时,会先打印 "类加载了..."

静态属性 vs 实例属性

特性静态属性 (Static Variable)实例属性 (Instance Variable)
关键字static
所属类 (Class)对象 (Object)
内存位置方法区 (Method Area)堆内存 (Heap)
副本数量只有一份 (共享)每个对象一份 (独立)
生命周期长 (类加载 -> 程序结束)短 (对象创建 -> 垃圾回收)
调用方式类名.变量名 (推荐)对象名.变量名

注意事项

致命陷阱与注意事项:

虽然静态属性好用,但它是 Bug 的温床,一定要小心:

  1. 线程安全问题 (Thread Safety):

    • 因为所有线程共享同一个静态变量,如果多个线程同时修改它,会导致数据错乱(Race Condition)。
    • 解决: 尽量使用 static final(不可变),或者在修改时加锁 (synchronized)。
  2. 内存泄漏 (Memory Leak):

    • 静态变量的生命周期非常长。如果你在一个 static List 中不断添加对象而不删除,这些对象永远不会被垃圾回收(GC),最终导致内存溢出 (OOM)。
  3. 测试困难:

    • 在单元测试中,静态变量的状态会保留到下一个测试用例中,导致测试之间相互干扰(脏数据)。

面试:System.out.println

经典面试题:System.out.println:

我们每天都在写的这行代码,其实就是静态属性的完美应用:

  • System: 是一个类 (java.lang.System)。
  • out: 是 System 类中的一个 静态属性 (public static final PrintStream out)。
  • println: 是 out 这个对象(PrintStream 类)里的方法。

所以我们不需要 new System() 就能直接用,就是因为 out 是静态的。

静态方法

静态方法(Static Method)static 关键字的另一半壁江山。静态方法是属于类的方法,而不是属于对象的方法。

工具箱 vs 个人技能

核心概念:工具箱 vs 个人技能:

为了直观理解,我们可以打个比方:

  • 实例方法 (Instance Method): 就像是“唱歌”。

    • 这是一个个人技能。你必须先找到一个具体的“人”(对象),比如张三,然后让他唱歌。没人就没法唱。
    • 调用方式:zhangSan.sing()
  • 静态方法 (Static Method): 就像是“加法运算”。

    • 这是一个通用工具。你不需要找一个特定的人来算 。这个逻辑是通用的,存在于“数学规则”(类)中。
    • 调用方式:Math.add(1, 1)

语法与调用

定义:在方法返回值类型前加上 static 关键字。

java
public class Calculator {
  // 静态方法:不需要创建对象就能用
  public static int add(int a, int b) {
    return a + b;
  }

  // 实例方法:必须创建对象才能用
  public void sayHello() {
    System.out.println("Hello World");
  }
}

调用原则:推荐直接使用类名调用。

java
public class Main {
  public static void main(String[] args) {
    // 1. 调用静态方法 (标准写法)
    int result = Calculator.add(10, 20);

    // 2. 调用实例方法 (必须先 new)
    Calculator c = new Calculator();
    c.sayHello();

    // 3. 用对象调用静态方法 (不推荐,会被 IDE 警告)
    // c.add(10, 20); // 虽然能跑,但误导性强,以为它跟对象 c 有关
  }
}

原因:反编译后本质还是通过类名调用的静态方法。

image-20260228161400365

快速入门

快速入门静态方法 + 静态属性

java
public class Main {
  public static void main(String[] args) {
    // 4. 静态方法无需创建对象即可调用
    Student.payFee(100); // 可以使用对象访问静态方法,但不推荐
    Student.payFee(200);
    Student.showFee(); // 总学费有:300
  }
}

class Student {
  private String name;
  // 1. 静态变量:保存累积学费
  private static double fee = 0;

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

  // 2. 静态方法:访问静态变量并自增
  public static void payFee(double fee) {
    Student.fee += fee;
  }

  // 3. 静态方法:显示总学费
  public static void showFee() {
    System.out.println("总学费有:" + Student.fee);
  }
}

核心特性@

核心特性::

这是面试和开发中最重要的规则,静态方法由于没有“对象”的上下文,所以受到严格限制:

  1. 不准直接访问非静态成员变量:

    原因: 静态方法加载时,对象可能还没出生呢

    java
    class User {
      String name; // 实例变量
      static String school; // 静态变量
    
      public static void test() {
        // System.out.println(name); // ❌ 编译错误!我怎么知道是哪个 User 的名字?
        System.out.println(school);  // ✅ 可以,因为学校是大家共用的
      }
    }
  2. 不准直接调用非静态方法:

    原因: 同上。

    java
    public void run() { ... }
    
    public static void test() {
      // run(); // ❌ 编译错误!run 需要依附于对象
    }

    注意:如果不直接调用,而是自己在静态方法里 new 了一个对象,那是可以调用该对象的实例方法的。

  3. 不准使用 thissuper 关键字:

    原因: this 代表“当前对象”。静态方法执行时,根本就没有“当前对象”这个概念。

  4. 静态方法可以被继承,不能被重写:

    这是一个经典的面试陷阱:静态方法可以被继承,但不能被“重写” (Override)。

    如果在子类中定义了和父类一模一样的静态方法,这叫 “隐藏” (Hiding)

    java
    class Father {
      public static void talk() {
        System.out.println("爸爸在说话");
      }
    }
    
    class Son extends Father {
      // 1. 这不是重写,这是“隐藏”了父类的方法
      public static void talk() {
        System.out.println("儿子在说话");
      }
    }
    
    public class Main {
      public static void main(String[] args) {
        // 2. 看起来是多态,实际上调用的是 Father 的静态方法!
        Father f = new Son();
        f.talk(); // 爸爸在说话
      }
    }

    输出结果: 爸爸在说话

    原理解析:

    • 重写 (Override) 是运行时多态,看对象是谁(右边 new 的是谁)。
    • 静态方法 是编译时绑定,看引用类型是谁(左边定义的变量类型是谁)。因为 f 被定义为 Father 类型,所以直接调用 Father.talk()

应用场景

三大应用场景:

通常在以下三种场景下,我们会把方法设计为 static

  1. 工具类 (Utility Classes):

    这是最常见的用法。如果一个方法完全独立于对象的状态,只根据传入的参数计算结果,它就应该是静态的。

    • 例子: java.lang.Math(算数)、java.util.Arrays(数组排序)、java.util.Collections
    • 代码: Math.max(10, 20)

    image-20260228162946224

  2. main 方法:

    程序的入口。

    java
    public static void main(String[] args) { ... }

    为什么 main 必须是 static?

    因为在程序启动时,JVM 还不知道该创建哪个对象。只有设为 static,JVM 才能直接通过 类名.main() 来启动程序。

  3. 工厂方法 (Factory Methods):

    用于替代构造函数创建对象,通常命名为 valueOf, getInstance 等。

    • 例子: LocalDate.now()

静态方法 vs 实例方法

特性静态方法 (Static)实例方法 (Instance)
关键字static
归属对象
调用ClassName.method()obj.method()
访问权限只能访问静态成员可以访问静态 + 非静态成员
关键字 this不可用可用 ✅
多态性不可重写 (隐藏)可以重写 (多态)
常见用途工具类、工厂、入口业务逻辑、对象行为

练习题

  1. 练习:静态属性

    java
    public class Test {
      static int count = 9;
    
      public void count() {
        System.out.println("count=" + (count++));
      }
    
      public static void main(String args[]) {
        new Test().count(); // 输出:9
        new Test().count(); // 输出:10
        System.out.println(Test.count); // 输出:11
      }
    }
  2. 练习:静态方法 + 静态属性(静态方法中不能访问非静态成员)

    java
    class Person {
      private int id;
      private static int total = 0;
    
      // 静态方法:返回总人数
      public static int getTotalPerson() {
        // id++;// ❌ 错误:静态方法不能访问非静态变量
        return total;
      }
    
      // 构造器
      public Person() {
        total++; // 总人数自增
        id = total; // 给id赋值
      }
    }
    
    public class TestPerson {
      public static void main(String[] args) {
        System.out.println("Number of total is " + Person.getTotalPerson()); // 0
        Person p1 = new Person(); // total -> 1; id -> 1
        System.out.println("Number of total is " + Person.getTotalPerson()); // 1
      }
    }
  3. 练习:静态方法 + 静态属性(静态方法中不能使用 this)

    java
    class Person {
      private int id;
      private static int total = 0;
    
      // 静态方法:设置总人数
      public static void setTotalPerson(int total) {
        // this.total = total; // ❌ 错误:静态方法不能使用 this
        Person.total = total;
      }
    
      public Person() { // 构造器
        total++;
        id = total;
      }
    }
    
    public class TestPerson {
      public static void main(String[] args) {
        Person.setTotalPerson(3); // total -> 3
        new Person(); // total -> 4
      }
    }

静态代码块

在 Java 中,静态代码块(Static Initialization Block) 是一个非常特殊且强大的代码区域。

它的标志是static 关键字修饰的一对大括号 { ... }

核心作用它在类被加载(Class Loading)时自动执行,且在整个程序运行期间,只执行一次。

它通常用于初始化复杂的静态数据(如读取配置文件、加载数据库驱动、填充静态 Map 等)。

基本语法

基本语法:

java
public class Demo {
  // 静态变量
  static int number;
  static ArrayList<String> list = new ArrayList<>();

  // 【静态代码块】
  static {
    System.out.println("静态代码块被执行了!");
    number = 100;
    list.add("初始化数据A");
    list.add("初始化数据B");
  }

  public static void main(String[] args) {
    System.out.println("main 方法执行");
    System.out.println("number = " + number);
  }
}

输出结果:

text
静态代码块被执行了!
main 方法执行
number = 100

注意观察: 静态代码块甚至在 main 方法的第一行代码之前就执行了(因为访问 main 方法所在的类会触发类的加载)。

核心特性

  1. 执行时机:类加载时:

    • 静态代码块不依赖于对象的创建。
    • 只要这个类被 JVM 加载(第一次用到该类时,比如 new 对象、访问静态属性、调用静态方法,或者 Class.forName()),静态代码块就会立即执行。
  2. 执行频率:仅此一次:

    • 无论你 new 了多少个对象,静态代码块只在第一次加载类时运行一次
    • 这非常适合用来做“全局只需做一次”的初始化工作。
  3. 只能访问静态成员:

    • 在静态代码块中,你不能访问非静态的成员变量(实例变量)或实例方法。
    • 你也不能使用 thissuper 关键字。

执行顺序

执行顺序(继承关系中):

这是 Java 笔试中最容易晕头转向的题目:静态块、构造代码块、构造方法的执行顺序。

原则:

  1. 静态优于非静态: 父类静态 -> 子类静态。

  2. 父类优于子类: 父类初始化 -> 子类初始化。

  3. 构造块优于构造器: 实例代码块 -> 构造函数。

代码实战:

java
class Father {
 static { System.out.println("1. 父类静态代码块"); }
 { System.out.println("3. 父类构造代码块 (非静态)"); }

 public Father() { System.out.println("4. 父类构造方法"); }
}

class Son extends Father {
 static { System.out.println("2. 子类静态代码块"); }
 { System.out.println("5. 子类构造代码块 (非静态)"); }

 public Son() { System.out.println("6. 子类构造方法"); }
}

public class Test {
 public static void main(String[] args) {
     System.out.println("--- 准备创建对象 ---");
     new Son();
     System.out.println("--- 再次创建对象 ---");
     new Son();
 }
}

输出结果(请仔细核对):

text

1. 父类静态代码块  (类加载,只执行一次)

2. 子类静态代码块  (类加载,只执行一次)
--- 准备创建对象 ---

3. 父类构造代码块

4. 父类构造方法

5. 子类构造代码块

6. 子类构造方法
--- 再次创建对象 ---

3. 父类构造代码块

4. 父类构造方法

5. 子类构造代码块

6. 子类构造方法

结论:

  • 静态代码块最先执行,且不再重复
  • 构造代码块构造方法每次创建对象都会执行。

应用场景

应用场景:

为什么我们需要它?直接在定义变量时赋值不就行了吗?

比如 static int a = 10;

是的,简单赋值可以。但如果初始化的逻辑很复杂(需要 for 循环、异常处理、读取文件),就必须用静态代码块。

初始化静态 Map

场景 1:初始化静态 Map:

java
class ErrorCodes {
    public static Map<Integer, String> map = new HashMap<>();

    static {
        // 复杂的 put 逻辑只能写在代码块里
        map.put(404, "Not Found");
        map.put(500, "Internal Server Error");
        map.put(403, "Forbidden");
        // 可能还需要从数据库读取更多错误码...
    }
}

JDBC 驱动加载

场景 2:JDBC 驱动加载 (经典案例):

在早期的 JDBC 编程中,我们经常看到这句话:

Class.forName("com.mysql.cj.jdbc.Driver");

这句话的作用就是触发 Driver 类的类加载。而在 Driver 类的源码内部,正是通过静态代码块将自己注册到驱动管理器的:

java
// com.mysql.cj.jdbc.Driver 源码片段
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    static {
        try {
            // 当类被加载时,把自己注册给 DriverManager
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }
    ...
}

总结与对比

总结与对比:

特性静态代码块 (static {})构造代码块 ({})构造方法 (Constructor)
关键字static 修饰无修饰类名相同
归属对象对象
执行时机类加载时创建对象时 (构造器前)创建对象时
执行次数仅一次每次 new 都执行每次 new 都执行
用途初始化静态资源、驱动加载抽取构造器共性代码 (少用)初始化对象属性

静态内部类

在 Java 的嵌套类(Nested Classes)家族中,静态内部类(Static Inner Class) 是最特殊、也是在优秀开源框架源码中出场率最高的一位。

简单来说:它是一个定义在其他类内部的类,并且被 static 关键字修饰。

虽然它“寄人篱下”写在别的类里面,但它的行为表现其实更像是一个完全独立的普通类

语法

语法与实例化方式:

静态内部类最大的特点是:它的创建,完全不需要依赖外部类的实例对象。

  1. 可以直接访问外部类的静态成员(即使是 private 的)
  2. 不能直接访问外部类的非静态(实例)成员
java
public class Outer {
  private static String staticName = "外部类静态变量";
  private String instanceName = "外部类实例变量";

  // 【静态内部类】
  public static class Inner {
    public void display() {
      // 1. 可以直接访问外部类的静态成员(即使是 private 的)
      System.out.println("访问: " + staticName);

      // 2. ❌ 编译报错!不能直接访问外部类的非静态(实例)成员
      // System.out.println(instanceName);
    }
  }
}

如何实例化(创建对象)?

因为它不依赖外部类对象,所以 new 的时候直接用 外部类.内部类 的形式即可。

java
public class Main {
  public static void main(String[] args) {
    // 不需要先 new Outer()!直接通过类名路径创建:
    Outer.Inner innerObj = new Outer.Inner();

    innerObj.display();
  }
}

核心特性

核心特性与铁律:

理解静态内部类,只要记住以下三条铁律:

  1. 没有“隐式引用” (No Hidden Reference):

    普通的成员内部类在底层会自动持有一个外部类对象的引用(这就是为什么普通内部类能随意使用外部类的变量)。但静态内部类没有这个引用。这使得它非常干净、轻量,不容易造成内存泄漏

  2. 只能访问静态成员:

    既然它不持有外部类对象的引用,它自然就不知道外部类的实例变量是什么。所以,静态内部类内部只能直接访问外部类的 static 变量和 static 方法。

  3. 可以拥有各种成员:

    静态内部类里面,既可以定义静态变量/方法,也可以定义非静态变量/方法。(而普通的成员内部类在 Java 16 之前是不允许定义静态成员的)。

应用场景

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

既然它表现得像个独立类,为什么不干脆把它拿出来单独建一个 .java 文件呢?主要有以下几个原因:

  1. 极致的封装与高内聚:

    如果一个类 B 只为类 A 服务,把 B 放在 A 里面作为静态内部类,可以隐藏 B 的存在,让包结构更清晰。

    • Java 源码代表作:HashMap.Node

      HashMap内部用来存储键值对的节点类Node,就是一个静态内部类。因为除了 HashMap,别人根本不需要用到 Node

  2. Builder 建造者模式(最常用:

    在构建包含几十个属性的复杂对象时,静态内部类是实现链式调用的最佳搭档。

    java
    public class User {
        private String name;
        private int age;
    
        // 1. 私有化构造器,强迫使用 Builder
        private User(Builder builder) {
            this.name = builder.name;
            this.age = builder.age;
        }
    
        // 2. 静态内部类 Builder
        public static class Builder {
            private String name;
            private int age;
    
            public Builder setName(String name) {
                this.name = name;
                return this; // 返回当前 Builder 对象,实现链式调用
            }
    
            public Builder setAge(int age) {
                this.age = age;
                return this;
            }
    
            public User build() {
                return new User(this);
            }
        }
    }
    
    // 3. 优雅的调用方式:
    User user = new User.Builder().setName("Gemini").setAge(3).build();
  3. 单例模式(静态内部类实现法):

    正如我们在“单例模式”中提到的,这是最优雅、安全的单例写法,利用了 JVM 类加载机制的互斥性,既实现了懒加载(Lazy Loading),又天然保证了线程安全。

    java
    public class SingletonInner {
     // 1. 构造器设为 private
     private SingletonInner() {}
    
     // 2. 静态内部类,只有被调用时才会加载,加载时在类内部 new 一个实例对象,并且保证在内存中只有一个实例对象
     private static class SingletonHolder {
         private static final SingletonInner INSTANCE = new SingletonInner();
     }
    
     // 3. 对外暴露一个公共方法
     public static SingletonInner getInstance() {
         return SingletonHolder.INSTANCE;
     }
    }
    java
    // 4. 调用公共方法
    SingletonInner.getInstance()

静态内部类 vs 成员内部类

终极对比:静态内部类 vs 成员内部类:

维度静态内部类 (static class)成员内部类 (class)
外部类实例依赖完全独立(不需要先创建外部类对象)强依赖(必须先 new 外部类对象,再 new 内部类)
持有外部引用不持有 (防内存泄漏)隐式持有 Outer.this
访问外部成员只能访问外部 static 成员可以访问外部所有成员(包括私有实例变量)
自身包含静态成员可以包含任何成员Java 16 之前不能包含静态成员,之后可以
实例化语法new Outer.Inner()outerObj.new Inner()

静态导包

在 Java 中,静态导包(Static Import) 是 JDK 1.5(Java 5)引入的一个非常实用的“语法糖”。

如果用一句话来概括它的作用:它可以让你在调用某个类的静态变量或静态方法时,连“类名”都省略掉,直接写方法名或变量名。

这主要用于精简代码,让代码读起来更像自然语言或数学公式。

语法

我们先来看日常开发中最常见的例子:使用 java.lang.Math 类进行数学计算。

传统写法

哪怕我们已经导入了相关的类,每次调用静态方法时,还是必须带上 Math. 这个前缀。

java
// 1. 普通导包
import java.lang.Math;

public class NormalImportDemo {
  public static void main(String[] args) {
    // 2. 每次都要写 Math.
    double r = Math.PI;
    double area = Math.PI * Math.pow(r, 2);
    int maxVal = Math.max(10, 20);

    System.out.println("最大值是:" + maxVal);
  }
}

现代写法

import 后面加上 static 关键字,就可以把指定的静态成员直接“拉”到当前类的作用域里。

java
// 1. 【静态导包】导入 Math 类下的所有静态成员
import static java.lang.Math.*;

public class StaticImportDemo {
    public static void main(String[] args) {
        // 2. 直接使用 PI、pow 和 max,仿佛它们是这个类自己的方法一样!
        double r = PI;
        double area = PI * pow(r, 2);
        int maxVal = max(10, 20);

        System.out.println("最大值是:" + maxVal);
    }
}

情况分类

静态导包的两种精确度:

你可以选择导入具体的某一个方法/变量,也可以导入全部

导入指定成员

导入指定成员(推荐):

如果你只需要用到一两个静态方法,建议精确导入,这样别人看代码时明确知道来源。

java
import static java.lang.Math.max;
import static java.lang.Math.PI;

// 此时你只能省略 max 和 PI 的前缀,如果用 Math.abs() 还是得写类名。

导入所有静态成员

导入所有静态成员(用通配符 *:

如果你在这个类里要大量用到某个工具类的各种方法,可以使用 *

java
import static java.lang.Math.*;

应用场景

核心应用场景(什么时候用):

虽然静态导包能省几下键盘敲击,但我们绝不会滥用它。在实际企业级开发中,它几乎只出现在以下三种经典场景:

  1. 单元测试 (Unit Testing) —— 最广泛的使用地

    在使用 JUnit 或 TestNG 写测试用例时,我们需要大量调用断言方法(Assert)。使用静态导包可以让测试代码极其清爽。

    java
    // 静态导入所有的断言方法
    import static org.junit.jupiter.api.Assertions.*;
    
    public class CalculatorTest {
        public void testAdd() {
            Calculator calc = new Calculator();
            // 以前要写:Assertions.assertEquals(5, calc.add(2, 3));
            // 现在直接写:
            assertEquals(5, calc.add(2, 3));
            assertTrue(calc.isReady());
            assertNotNull(calc);
        }
    }
  2. 频繁使用常量的类

    如果你定义了一个全局常量类,在其他地方需要高频使用这些常量:

    java
    // 常量类
    package com.constant;
    public class Status {
        public static final int SUCCESS = 200;
        public static final int ERROR = 500;
    }
    
    // 业务类
    import static com.constant.Status.*;
    
    public class BizService {
        public void process() {
            int code = SUCCESS; // 直接写 SUCCESS,而不是 Status.SUCCESS
        }
    }
  3. 数学密集型计算

    当你编写复杂的算法,涉及大量的 sin(), cos(), abs(), pow() 时,去掉 Math. 前缀能让公式更接近数学原本的样子,提升可读性。

注意事项

致命缺陷与注意事项(为什么不推荐滥用):

阿里 Java 开发手册及诸多编码规范中都明确指出:慎用静态导包

原因只有两个字:歧义

  1. 方法重名导致的混乱 (Namespace Pollution)

    假设你静态导入了两个不同工具类中的所有方法,而这两个类恰好都有一个同名方法:

    java
    import static java.util.Collections.*;
    import static java.util.Arrays.*;
    
    public class Test {
      public void doSomething() {
        // ❌ 报错!
        // Collections 里面有个 sort(),Arrays 里面也有个 sort()
        // 编译器彻底懵了,它不知道你到底想用哪一个。
        // sort(list);
      }
    }
  2. 严重降低代码可读性

    对于不熟悉你代码的人(或者几个月后的你自己)来说,突然在代码里看到一个没头没尾的 checkConfig() 方法调用。

    你会本能地以为这是当前类定义的一个普通方法,于是按住 Ctrl 找半天,最后才发现它竟然是从几十公里外的另一个包里“静态导入”过来的工具方法。这就极大地增加了阅读代码的认知负担。

总结

特性传统导包 (import)静态导包 (import static)
导入目标类 (Class) 或 接口 (Interface)类的 静态变量静态方法
调用方式类名.方法名()直接写 方法名()
优点来源清晰,绝无重名歧义代码极致精简,尤其适合测试断言和复杂数学公式
缺点前缀重复,代码稍显冗长滥用会导致来源不明,极易引发同名方法冲突

main 方法

main() 方法是 Java 应用程序的入口点 (Entry Point)

它是 Java 程序执行的起点。当你运行一个 Java 程序时,JVM(Java 虚拟机) 会首先寻找这个特定的方法,并从这里开始一行一行地执行代码。

如果没有 main 方法,或者格式写错了,程序就无法启动,JVM 会抛出错误:Error: Main method not found in class...

标准签名

标准签名 (The Standard Signature):

这是你必须背下来的“标准公式”。每一个单词都有其深刻的含义:

java
public static void main(String[] args) {
  // 你的代码
}
  1. public (访问修饰符)

    • 含义: 公开的。
    • 原因: main 方法是由 JVM 调用的。JVM 是一个“外部”程序,它不在你的类内部。为了让 JVM 能“看见”并调用这个方法,必须将其设置为 public。(注:在 Java 9 之前的某些版本若不写 public 可能无法运行,但现在必须是 public)。
  2. static (静态)

    • 含义: 静态的,属于类而非对象。
    • 原因: 在程序启动时,内存中还没有任何对象
      • 如果 main 不是 static 的,JVM 就必须先创建你的类的一个对象(new MyClass())才能调用它。
      • 但在创建对象之前,JVM 怎么知道该如何初始化?
      • 所以,将 main 设为 static,JVM 就可以直接通过 类名.main() 来调用,而无需实例化对象。
  3. void (返回值)

    • 含义: 空,不返回任何值。
    • 原因:main 方法执行完毕,意味着主线程结束,Java 程序也就退出了。
      • 它不需要向 JVM 返回数据
      • 如果程序需要向操作系统报告状态(比如成功还是失败),通常使用 System.exit(int status),而不是通过方法返回值。
  4. main (方法名)

    • 含义: 主要的。
    • 原因: 这是 Java 规范硬性规定的名称。JVM 在启动时,就是死板地去寻找叫 main 的方法。不能叫 Main,也不能叫 start
  5. String[] args (参数)

    • 含义: 一个字符串数组。
    • 原因: 这是用来接收命令行参数 (Command-line arguments) 的。
      • 当你通过控制台(Terminal/CMD)运行程序时,可以在类名后面跟上一些参数,这些参数会被 JVM 封装成一个 String 数组传给 main 方法。

使用 String[] args

示例:使用 String[] args:

很多初学者写了 args 却从来没用过。让我们看看它是怎么工作的。

java
public class Demo {
  public static void main(String[] args) {
    System.out.println("收到了 " + args.length + " 个参数");

    for (int i = 0; i < args.length; i++) {
      System.out.println("参数 " + i + ": " + args[i]);
    }
  }
}

如何运行与传参:

  1. 编译: javac Demo.java

  2. 运行(带参数): java Demo hello world 123

输出结果:

text
收到了 3 个参数
参数 0: hello
参数 1: world
参数 2: 123

常见面试题:args 这个名字能改吗?

  • 能! 它可以叫 String[] paramsString[] x。只要类型是字符串数组即可。
  • 格式变种: 也可以写成可变参数形式 public static void main(String... args),JVM 同样承认。

IDEA中运行代码时传参

类似 CLI 命令行环境中的 java Main02 北京 上海 天津 tom jack

image-20260224175918079

常见疑问与骚操作

常见疑问与骚操作

  1. main 方法中可以访问静态属性和方法吗

    可以。可以将它看成是一个普通的静态方法。

    • 可以访问静态属性和方法。
    • 不能直接访问实例属性和方法,可以通过在 main 方法中 new 一个实例来访问实例属性和方法。
    java
    【示例】
  2. main 方法可以被重载 (Overload) 吗

    可以。

    你可以在类里写很多个叫 main 的方法,只要参数列表不同。

    java
    public static void main(String[] args) { ... } // JVM 只认这个
    public static void main(int a) { ... }         // 普通方法,JVM 不理它

    但是,JVM 启动时只会调用那个标准的 String[] args 版本。其他的 main 方法只是普通的静态方法,除非你在代码里手动调用它们。

  3. main 方法能被其他方法调用吗

    能。

    它就是一个普通的静态方法。你可以在别的代码里手动调用 Demo.main(new String[]{})。当然,这很少见,容易造成逻辑混乱或死循环

  4. Java 21 的新变化 (预览特性)

    如果你使用的是 Java 21+,Java 引入了 "Unnamed Classes and Instance Main Methods" 来简化新手入门。

    你不再需要写 public static void main(String[] args) 这么长一串了。

    Java 21 简写版 (JEP 445):

    java
    void main() {
        System.out.println("Hello, World!");
    }
    • 不需要 public
    • 不需要 static
    • 不需要 String[] args
    • 甚至不需要包裹在 class 里(如果放在未命名类文件中)。

总结

关键字作用能否修改?
public访问权限必须是 public (Java 21 前)
static无需对象即可调用必须是 static (Java 21 前)
void无返回值必须是 void
main方法名绝对不能改
String[]参数类型可改为 String...
args参数变量名可以随意改 (如 arguments)