Java中静态块的必要性

60

我发现在 Java 中有一个名为 static block 的特性,它包含在类首次加载时执行的代码(我不明白 'loaded' 是什么意思,它是否意味着初始化)。为什么要在静态块内做初始化而不是在构造函数中呢?我的意思是,即使构造函数也做同样的事情,在类第一次初始化时完成所有必要的工作。是否有任何静态块可以完成而构造函数无法完成的任务?


3
当类被类加载器加载时调用一次,而不是每次实例化类时都调用。 - Wolfgang Kuehn
2
请查看以下 Oracle 官方 Java 教程的章节:了解实例和类成员初始化字段。如果您仍有疑问,请回来咨询。 - Anthony Accioly
1
初始化代码(类加载时间)-> Main(应用程序入口点)-> 构造函数:这里有一个工作示例以进一步说明这一点。 - Anthony Accioly
1
@Cupidvogel main 方法并不会在每次创建类的实例时运行。它只会在显式调用时(就像任何其他静态方法一样)或 JVM 启动应用程序时运行。 - Thorn G
是的,看起来非常复杂。我需要花费相当多的时间和精力来消化它。 - SexyBeast
显示剩余9条评论
13个回答

54

我首先想强调你问题中的一件事情:

构造函数并不是在类第一次初始化时执行所有必要的操作,而是在创建一个类的实例时进行所有必要的初始化工作。

如果一个类有静态成员需要进行复杂的初始化操作,那么就可以使用static块。假设你需要一个静态映射(具体目的不重要),你可以像这样直接声明:

public static final Map<String, String> initials = new HashMap<String, String>();

然而,如果你想仅仅在初始化时填充它,那么你不能使用内联声明。你需要一个 static 代码块:

public static final Map<String, String> initials = new HashMap<String, String>();
static {
    initials.put("AEN", "Alfred E. Newman");
    // etc.
}

如果你想更加保护一些,你可以这样做:

public static final Map<String, String> initials;
static {
    Map<String, String> map = new HashMap<String, String>()
    map.put("AEN", "Alfred E. Newman");
    // etc.
    initials = Collections.unmodifiableMap(map);
}

请注意,您无法将initials作为不可修改的映射项直接初始化,否则您将无法填充它!您也不能在构造函数中这样做,因为调用其中一个修改方法(put等)会生成异常。

公平地说,这并不是对您问题的完整回答。通过使用私有静态函数仍然可以消除static块:

public static final Map<String, String> initials = makeInitials();

private static Map<String, String> makeInitials() {
    Map<String, String> map = new HashMap<String, String>()
    map.put("AEN", "Alfred E. Newman");
    // etc.
    return Collections.unmodifiableMap(map);
}

请注意,这并不是像您所提出的用构造函数替换static块的方法!此外,如果您需要以相互关联的方式初始化多个static字段,则此方法无法正常工作。

如果需要初始化多个其他类仅一次的“协调器”类,尤其是涉及依赖注入时,将很难替换使用static块。

public class Coordinator {
    static {
        WorkerClass1.init();
        WorkerClass2.init(WorkerClass1.someInitializedValue);
        // etc.
    }
}

特别是如果您不想在WorkerClass2中硬编任何依赖于WorkerClass1的内容,则需要类似于这样的协调器代码。这种东西绝对不属于构造函数。

请注意,还有一种称为实例初始化块的东西。它是一个匿名的代码块,在每个实例创建时运行。(语法与static块相同,但没有static关键字。)它对于匿名类非常有用,因为它们不能具有命名构造函数。以下是一个真实世界的例子。由于(难以理解)GZIPOutputStream没有构造函数或任何可以指定压缩级别的api调用,而默认压缩级别为none,因此您需要对GZIPOutputStream进行子类化才能获得任何压缩。您总是可以编写明确的子类,但编写匿名类可能更方便:

OutputStream os = . . .;
OutputStream gzos = new GZIPOutputStream(os) {
    {
        // def is an inherited, protected field that does the actual compression
        def = new Deflator(9, true); // maximum compression, no ZLIB header
    }
};

1
为什么我不能在构造函数内部完成它? - SexyBeast
2
静态初始化器的另一个典型用途是为具有本地方法的类加载本地库,例如:static { System.loadLibrary("foo"); } - biziclop
2
@Cupidvogel - 由于变量被声明为staticfinal(因此无法更改),语言将不允许您在构造函数中这样做。 - Ted Hopp
1
@Cupidvogel,你了解类和实例化的概念吗?如果你把那段代码放在构造函数里,那么每次创建一个新实例时都会再次添加相同的条目到映射表中。你需要让这种行为在类级别上只发生一次,而不是针对每个实例。 - John
1
@Cupidvogel - 我认为我发布的内容是一个构造函数无法完成但静态块可以完成的情况。(请注意,这不是构造函数不应该使用的情况;而是构造函数无法使用的情况。) - Ted Hopp
显示剩余9条评论

18

在创建类的实例时调用构造函数。

静态块是在类加载器加载此类定义时调用的,因此我们可以初始化此类的静态成员。 我们不应该从构造函数中初始化静态成员,因为它们是类定义的一部分,而不是对象。


1
请解释一下您所说的“load”的含义。 - SexyBeast
每个类都需要通过类加载器加载到JVM中,这可以在代码中请求该类类型的对象或请求代码中的静态变量时发生。例如:int b = ClassA.staticMenber; staticMember是ClassA中存在的静态变量,可以在静态块中初始化。 - Subin Sebastian
那么你的意思是,“loaded”指的是“当该类的一个实例首次被创建”对吗? - SexyBeast
请查看编辑。它可以在实例创建时或静态引用时发生。 - Subin Sebastian
1
+1 for 我们不应该从构造函数中初始化静态成员,因为它们是类定义的一部分而不是对象 - Aman Gupta
有时候在构造函数中修改static字段是有原因的。例如,这种技术被用来为类的每个实例生成一个唯一的ID:定义一个private static int nextID字段,静态地初始化为0,然后在构造函数中分配this.id = nextID++ - Ted Hopp

10

静态初始化程序将在我们初始化一个类时运行,这不需要我们实例化一个类。但构造函数仅在我们创建类的实例时运行。

例如:

class MyClass
{   
    static
    {
        System.out.println("I am static initializer");
    }
    MyClass()
    {
        System.out.println("I am constructor");
    }

    static void staticMethod()
    {
        System.out.println("I am static method");
    }
}

如果我们执行:

MyClass.staticMethod();

输出:

I am static initializer
I am static method

我们从未创建实例,因此构造函数没有被调用,但静态初始化程序被调用。

如果我们创建一个类的实例,则静态初始化程序和构造函数都会运行。毫无意外。

MyClass x = new MyClass();

输出:

I am static initializer
I am constructor
请注意,如果我们运行:

Note that if we run:

MyClass x;

输出: (空)

声明变量 x 不需要初始化 MyClass,因此静态初始化程序不会运行。


2
你误解了正在发生的事情。当类被初始化时,静态块确实会运行。只是简单地声明一个变量MyClass x并不需要加载或初始化该类。Java足够聪明,它知道可以推迟加载和初始化类,直到实际需要它(除了名称之外的其他内容)。 (顺便说一句,仅引用类的静态字段也会导致类被初始化。您无需创建实例或调用方法。) - Ted Hopp
@Ted Hopp 我编辑了我的答案以反映您的观点,现在基本上是我的答案要点。 - Akavall
有很大的改进。然而,我不认为这解决了楼主的问题。 - Ted Hopp
@Ted Hopp,OP基本上是在问静态初始化程序和构造函数之间的区别。 OP的最后一个问题是“静态块是否有任何构造函数无法完成的任务”。我的答案说明了当未创建MyClass实例时运行静态初始化程序,而构造函数则不会。这是静态初始化程序可以做到的,而构造函数无法做到的。 - Akavall

7

即使您不创建该类型的任何对象,静态初始化程序在类加载时运行。

  • 并非所有类都意味着要实例化。构造函数可能永远不会被调用。它甚至可以是私有的。
  • 您可能希望在运行构造函数之前访问类的静态字段。
  • 静态初始化程序仅在加载类时运行一次。对于每个您实例化的该类型的对象,都会调用构造函数。

5

你不能使用构造函数初始化静态变量,或者说你可能不应该这样做,而且这样做也不会有什么特别的用处。

特别是当你尝试初始化需要进行重要逻辑生成的静态常量时,最好在静态块中进行初始化,而不是在构造函数中。


我不是那个给你点踩的人,但我怀疑问题在于技术上你可以在构造函数中初始化静态变量。 - biziclop
我完全同意你的观点,这只是一个有关downvote原因的理论。 - biziclop

3

它们是两个不同的东西。你使用构造函数来初始化类的一个实例,静态初始化块在类被加载时初始化静态成员。


3
静态块在没有创建实例的情况下执行某些操作非常有用。例如,可以使用静态块将静态变量初始化为非静态值。

3

静态代码块与构造函数有不同的作用。基本上,它们是两个不同的概念。

静态代码块在类加载到内存中时初始化,也就是当JVM读取你的字节码时。初始化可以是变量初始化或任何应该由该类的所有对象共享的其他内容。

而构造函数仅为该对象初始化变量。


2

静态块在你想要初始化静态字段时非常有用。


1

您可以将静态块理解为:它充当构造函数。但两者之间的区别是,静态块实例化类或静态变量,而构造函数用于实例化对象变量

考虑以下类

public class Part{

  String name;
  static String producer;

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

  static {
    producer = "Boeing";
  }

}

从这个类创建的对象将其制造商设置为波音公司,但它们的名称取决于传递的参数。例如

Part engine = new Part("JetEngine");
Part Wheel = new Part("JetWheel");

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接