Java中的非静态代码块有什么用途?

8

可能是重复问题:
实例初始化器和构造函数有什么区别?

当所有必需的工作都可以在构造函数中完成时,为什么我们仍然需要Java中的非静态块?

编辑:对于那些在构造函数之前每次运行非静态块的普通类呢?

2个回答

15

除了 @Bohemian 的答案之外。

如果您有多个构造函数,可以使用初始化块来避免重复代码。

public class A {
     final Map<String, String> map = new HashMap<String, String>(); {
        // put things in map.
     }
     final int number; {
        int number;
        try {
            number = throwsAnException();
        } catch (Exception e) {
            number = 5;
        }
        this.number = number;
     }

     public A() { /* constructor 1 */ }
     public A(int i) { /* constructor 2 */ }
}

普通类的非静态代码块会在构造函数之前运行,具体顺序如下:

  • 先调用父类构造函数
  • 按照代码中出现的次序依次执行所有的初始化块
  • 执行构造函数的代码

实际上,所有这些代码都被编译进每个构造函数的字节码中,因此在运行时不存在构造函数之前或之后的情况。


为了避免更多的初始化顺序混乱,请注意以上的初始化顺序。

public class Main extends SuperClass {
    {
        System.out.println("Initialiser block before constructor");
    }

    Main() {
        System.out.println("Main constructor");
    }

    {
        System.out.println("Initialiser block after constructor");

    }

    public static void main(String... args) {
        new Main() {{
            System.out.println("Anonymous initalizer block");
        }};
    }
}

class SuperClass {
    SuperClass() {
        System.out.println("SuperClass constructor");
    }
}

打印

SuperClass constructor
Initialiser block before constructor
Initialiser block after constructor
Main constructor
Anonymous initalizer block

将前导括号放在声明“number”的同一行上看起来有点混乱。 - obataku
@veer 这是一个样式问题。它的作用是显示该块与特定字段相关联。 - Peter Lawrey
1
可以通过在每个构造函数中重复所有代码或找到使用this()的方法来解决问题。问题是,如果您添加构造函数,则需要记住在之前和之后添加位。不需要这样做要简单得多。 - Peter Lawrey
1
@Ashwyn,说得好。正在修复。 - Peter Lawrey
2
@PeterLawrey:匿名类的实例块在构造函数之后按照出现顺序执行。也许你可以在回答中区分一下类内实例块的执行顺序。(顺便恭喜你获得了10万个赞,以及你的Java连接符字符回答被选入了新闻简报!) - Bohemian
显示剩余8条评论

8

您可以使用匿名类:

new MyClass() {
    {
         // do something extra on construction (after the constructor executes)
    }
}

我觉得这对于在一个地方初始化“查找”映射(即固定内容)特别有用:
Map<String, String> map = new HashMap<String, String>() {
    {
        put("foo", "bar");
        put("one", "two");
        // etc
    }
};

知道一下,这有时被称为“双括号初始化”,但实际上只是使用了一个初始化块

虽然这样的匿名类在技术上是一个子类,但当使用这种技术与更传统的方法创建一个不可修改的映射时,其优越性就显现出来了:

比较一下这个简单的一行代码,它将数据和赋值放在一起:

private static final Map<String, String> map = Collections.unmodifiableMap(
    new HashMap<String, String>() {{
        put("foo", "bar");
        put("one", "two");
        // etc
    }});

由于final只允许一次赋值,因此需要创建一个单独的对象来解决这个混乱问题:

private static final Map<String, String> map;

static {
    Map<String, String> tempMap = new HashMap<String, String>();
    tempMap.put("foo", "bar");
    tempMap.put("one", "two");
    // etc
    map = Collections.unmodifiableMap(tempMap);
}

此外,需要注意的是在混乱版本中,这两个语句不需要相邻,因此不太明显该不可修改地图的内容是什么。

2
请注意,像这样初始化容器(称为"double brace initialization")不适当地创建了一个子类--类似于仅想实现 Runnable 时认为子类化 Thread 是不恰当的。滥用继承要小心;其中提到的一个例子是它如何破坏 equals 的常见实现。您应该考虑使用 collection factory method - obataku
@veer 不错的链接/阅读。我认为在equals方法中最好对类的CTT(以及/或任何支持的超类)进行检查。 - user166390
@veer 我绝对不认为这是“滥用继承”; 这只是在行内填充对象。我经常在我的代码中使用它而没有任何问题。请参见编辑后的答案。 - Bohemian
@Bohemian,你可以使用工厂方法进一步简化代码,例如Collections.unmodifiableMap(newHashMap<String, String>("foo", "bar", "one", "two", ...))。除此之外,我认为你应该了解Liskov替换原则 - obataku
@veer 我非常了解Liskov等原则,但是Java默认情况下并不提供Map构建器,使用这个方法可以避免编写或查找构建器。考虑到构建一个Map<String, Integer>(我已经这样做了)- Java不支持混合变长参数,因此类型定义会更加棘手。此外,您可以定义和调用方便的方法,所有这些方法都可以正常工作。另外,HashMap是如此基本的实用程序类,以至于没有人(除了你似乎)关心它是否被“子类化”。我鼓励我的团队使用这种模式,并进行重构以将上述丑陋版本转换为它。所以 :P - Bohemian
1
@Bohemian 我犯了一个错误,Maps.<String, String>newHashMap*。但是不管怎样,它可以返回一个 Map<String, String>(尽管根据方法的约定,这样做可能没有什么意义)。我并不是要挑剔,但是在像那样非传统方式使用继承时,应该小心谨慎。 - obataku

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