baeldung 2024-01-08
1. 概述
在本教程中,我们将深入探讨 Java 语言中的 static 关键字。static 关键字表示某个成员(如字段或方法)属于类本身,而不是该类的任何特定实例。因此,我们可以无需创建对象实例,直接访问静态成员。
我们将首先讨论静态字段和方法与非静态字段和方法之间的区别;接着介绍静态内部类和静态代码块;最后解释为什么在静态上下文中无法访问非静态组件。
2. 静态字段(或称类变量)
在 Java 中,当我们声明一个字段为 static 时,该字段只会创建唯一一份副本,并被该类的所有实例共享。
无论我们创建多少个该类的对象,静态字段始终只有一份。这个值在所有同类型对象之间共享。从内存角度看,静态变量存储在堆内存中。
设想一个类包含多个实例变量:每次创建新对象时,这些变量都会拥有各自的副本。但如果我们希望有一个变量用于统计已创建对象的数量,就应该使用静态变量。这样,每创建一个新对象,计数器就会递增:
public class Car {
private String name;
private String engine;
public static int numberOfCars;
public Car(String name, String engine) {
this.name = name;
this.engine = engine;
numberOfCars++;
}
// getter 和 setter 方法
}
因此,每次实例化 Car 类时,静态变量 numberOfCars 都会递增。让我们创建两个 Car 对象,并期望计数器值为 2:
@Test
public void whenNumberOfCarObjectsInitialized_thenStaticCounterIncreases() {
new Car("Jaguar", "V8");
new Car("Bugatti", "W16");
assertEquals(2, Car.numberOfCars);
}
由此可见,静态字段在以下场景中非常有用:
- 变量的值与具体对象无关;
- 该值需要在所有对象之间共享。
此外需要注意的是,静态字段既可以通过实例访问(例如 ford.numberOfCars++),也可以直接通过类名访问(例如 Car.numberOfCars++)。推荐使用后者,因为它能清晰表明这是一个类变量,而非实例变量。
3. 静态方法(或称类方法)
与静态字段类似,静态方法也属于类本身,而非某个对象。因此,我们可以在不实例化类的情况下调用它们。通常,我们会使用静态方法来执行那些不依赖于对象创建的操作。
例如,我们可以使用静态方法在所有类实例之间共享逻辑:
static void setNumberOfCars(int numberOfCars) {
Car.numberOfCars = numberOfCars;
}
此外,静态方法常用于创建工具类或辅助类。一些著名的例子包括 JDK 中的 Collections 或 Math 工具类、Apache 的 StringUtils,以及 Spring Framework 的 CollectionUtils。
与静态字段一样,静态方法不能被重写(override)。这是因为 Java 中的静态方法在编译时就已确定(静态绑定),而方法重写是运行时多态的一部分。
以下是实例方法、类方法与变量之间合法的访问组合:
- 实例方法可以直接访问其他实例方法和实例变量;
- 实例方法也可以直接访问静态变量和静态方法;
- 静态方法可以访问所有静态变量和其他静态方法;
- 静态方法不能直接访问实例变量和实例方法,必须通过某个对象引用来访问。
4. 静态代码块
通常,我们会在声明时直接初始化静态变量。但如果静态变量的初始化需要多条语句的逻辑,就可以使用静态代码块(static block)。
例如,我们使用静态代码块来初始化一个带有预定义值的 List 对象:
public class StaticBlockDemo {
public static List<String> ranks = new LinkedList<>();
static {
ranks.add("Lieutenant");
ranks.add("Captain");
ranks.add("Major");
}
static {
ranks.add("Colonel");
ranks.add("General");
}
}
显然,我们无法在声明时一次性完成如此复杂的初始化,因此使用了静态代码块。
一个类可以包含多个静态成员(字段和代码块)。JVM 会按照它们在类中声明的顺序依次解析静态字段和静态代码块。
使用静态代码块的主要原因包括:
- 初始化静态变量需要除简单赋值之外的额外逻辑;
- 初始化静态变量时需要自定义异常处理。
5. 静态内部类
Java 允许我们在一个类内部定义另一个类。这种机制有助于将相关元素组织在一起,使代码更清晰、更具可读性。
嵌套类主要分为两类:
- 使用
static声明的嵌套类称为 静态嵌套类(static nested class); - 未使用
static声明的嵌套类称为 内部类(inner class)。
两者的主要区别在于:
- 内部类可以访问外部类的所有成员(包括私有成员);
- 静态嵌套类只能访问外部类的静态成员。
静态嵌套类的行为与其他顶层类完全相同,只是它被封装在唯一会使用它的外部类中,从而提供更好的封装性和代码组织性。
例如,我们可以使用静态嵌套类实现单例模式:
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
public static final Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
}
这种方法不需要同步机制,且易于理解和实现。
此外,静态嵌套类与外部类之间的可见性如下所示:
public class Pizza {
private static String cookedCount;
private boolean isThinCrust;
public static class PizzaSalesCounter {
private static String orderedCount;
public static String deliveredCount;
PizzaSalesCounter() {
System.out.println("Static field of enclosing class is "
+ Pizza.cookedCount);
System.out.println("Non-static field of enclosing class is "
+ new Pizza().isThinCrust);
}
}
Pizza() {
System.out.println("Non private static field of static class is "
+ PizzaSalesCounter.deliveredCount);
System.out.println("Private static field of static class is "
+ PizzaSalesCounter.orderedCount);
}
public static void main(String[] a) {
new Pizza.PizzaSalesCounter();
}
}
运行 main 方法后的输出为:
Static field of enclosing class is null
Non private static field of static class is null
Private static field of static class is null
Non-static field of enclosing class is false
本质上,静态嵌套类无法直接访问外部类的任何实例成员,只能通过对象引用来访问。
在代码中使用静态内部类的主要原因包括:
- 将仅在一个地方使用的类进行分组,增强封装性;
- 将代码放在唯一使用它的地方附近,提高可读性和可维护性;
- 如果嵌套类不需要访问外部类的实例成员,应将其声明为
static。这样可以避免与外部类耦合,同时减少堆栈内存开销。
6. 理解 “Non-static variable” 错误
错误信息 “Non-static variable cannot be referenced from a static context”(非静态变量无法在静态上下文中引用)发生在静态上下文中使用了非静态变量时。
如前所述,JVM 在类加载时就初始化静态变量,它们属于类本身;而非静态变量则必须通过对象实例才能访问。
因此,Java 编译器会报错,因为调用非静态变量必须依赖某个对象实例。
下面通过一个例子说明该错误:
public class MyClass {
int instanceVariable = 0;
public static void staticMethod() {
System.out.println(instanceVariable);
}
public static void main(String[] args) {
MyClass.staticMethod();
}
}
这里,我们在静态方法 staticMethod() 中使用了非静态变量 instanceVariable,因此会触发上述编译错误。
7. 结论
在本文中,我们深入探讨了 Java 中 static 关键字的用法,并详细说明了使用静态字段、方法、代码块和内部类的主要原因。
最后,我们还了解了导致编译器报错 “Non-static variable cannot be referenced from a static context” 的根本原因。
合理使用 static 能提升代码的性能与可维护性,但需注意其限制——尤其是静态上下文无法直接访问实例成员这一关键规则。