【问题标题】:Anonymous code blocks in JavaJava中的匿名代码块
【发布时间】:2025-12-24 20:00:09
【问题描述】:

在 Java 中有匿名代码块的实际用途吗?

public static void main(String[] args) {
    // in
    {
        // out
    }
}

请注意,这与命名块无关,即

name: { 
     if ( /* something */ ) 
         break name;
}

.

【问题讨论】:

  • 顺便说一句,我也是。但话又说回来,我决定不排除那些(对我来说)似乎很明显的用途,就像你提到的那些。

标签: java


【解决方案1】:

它们限制了变量范围。

public void foo()
{
    {
        int i = 10;
    }
    System.out.println(i); // Won't compile.
}

但在实践中,如果您发现自己使用这样的代码块,这可能表明您想将该块重构为方法。

【讨论】:

  • 好答案,完全同意方法评论。
  • +1:正确答案,这是迫切需要重构的标志,也就是 Martin Fowler 所说的“代码味道”:)
  • 您不想将其分解为私有方法的唯一情况是该代码块需要大量上下文(以其他局部变量的形式)。但这可能是另一种代码味道......
  • @Eugene 是的,命名块也可以这样使用。但它们通常用作 break 语句的目标;如果我看到一个没有 break 语句的命名块,我想知道我是否正在查看一个糟糕的合并或半完成的重构。我的建议:使用未命名的块和评论。更多讨论,see this SO answer
  • 我经常在测试类中使用这些,所以我不必担心一遍又一遍地使用相同的变量名。相当整洁。
【解决方案2】:

@David Seiler 的回答是正确的,但我认为代码块非常有用,应该经常使用,并不一定表明需要将因素分解到方法中。我发现它们对于构建 Swing 组件树特别有用,例如:

JPanel mainPanel = new JPanel(new BorderLayout());
{
    JLabel centerLabel = new JLabel();
    centerLabel.setText("Hello World");
    mainPanel.add(centerLabel, BorderLayout.CENTER);
}
{
    JPanel southPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 0,0));
    {
        JLabel label1 = new JLabel();
        label1.setText("Hello");
        southPanel.add(label1);
    }
    {
        JLabel label2 = new JLabel();
        label2.setText("World");
        southPanel.add(label2);
    }
    mainPanel.add(southPanel, BorderLayout.SOUTH);
}

代码块不仅尽可能严格地限制变量的范围(这总是好的,尤其是在处理可变状态和非最终变量时),而且它们还以 XML 的方式说明了组件层次结构/HTML 使代码更易于阅读、编写和维护。

我将每个组件实例化分解为一个方法的问题是

  1. 即使是私有实例方法,该方法也只会使用一次,但会向更广泛的受众公开。
  2. 很难阅读,想象一个更深更复杂的组件树,您必须深入查找您感兴趣的代码,然后松散视觉上下文。

在这个 Swing 示例中,我发现当复杂性确实超出了可管理性时,它表明是时候将树的一个分支分解为一个新类,而不是一堆小方法。

【讨论】:

  • 我只是在处理大量使用此“功能”的代码,而且很难理解。我认为这是构造代码的一种不好的方式
  • @Jan - 有很多方法可以很好地使用不好的功能,我可以想象这个很容易失控(深度嵌套,长时间滚动,大量突变......)。
  • @Jan “相当难以理解”和“难以理解的混乱”是主观意见。他们总结的只是“我不习惯这样做,所以它一定很糟糕。” Stephen 解释了为什么这样做更好的 客观 原因。 (我碰巧同意。)所以,要么提出客观的反对意见,要么保持沉默。
  • 在我看来,这种编码风格对于已经难以理解且难以理解的代码非常有用。它只会让它更容易跟随。我认为一个类最好包含 50 个匿名代码黑人,而不是 50 个小型的一次性函数。如果有不同意见,我想听听。
  • 我更喜欢:mainPanel.add(label("Hello World",...);southPanel.add(label("Hello"));southPanel.add(label("World"));,没有多余的大括号。我无法理解它是如何难以阅读的,因为适当命名的方法应该提供有关其意图的线索。此外,IDE 通过将鼠标悬停在方法上将 Javadocs 显示为工具提示,这意味着视觉上下文不会丢失。重构通常会减少(或引起人们注意)复制/粘贴错误的可能性。
【解决方案3】:

通常最好是make the scope of local variables as small as possible。匿名代码块可以帮助解决这个问题。

我发现这对switch 语句特别有用。考虑以下示例,没有匿名代码块:

public String manipulate(Mode mode) {
    switch(mode) {
    case FOO: 
        String result = foo();
        tweak(result);
        return result;
    case BAR: 
        String result = bar();  // Compiler error
        twiddle(result);
        return result;
    case BAZ: 
        String rsult = bar();   // Whoops, typo!
        twang(result);  // No compiler error
        return result;
    }
}

还有匿名代码块:

public String manipulate(Mode mode) {
    switch(mode) {
        case FOO: {
            String result = foo();
            tweak(result);
            return result;
        }
        case BAR: {
            String result = bar();  // No compiler error
            twiddle(result);
            return result;
        }
        case BAZ: {
            String rsult = bar();   // Whoops, typo!
            twang(result);  // Compiler error
            return result;
        }
    }
}

我认为第二个版本更简洁,更易于阅读。而且,它将在 switch 中声明的变量的范围缩小到声明它们的情况,根据我的经验,无论如何,这在 99% 的情况下都是你想要的。

但是请注意,它确实不会改变案例失败的行为 - 您仍然需要记住包含 breakreturn 以防止它!

【讨论】:

    【解决方案4】:

    我认为您和/或其他答案混淆了两种不同的句法结构;即实例初始化器和块。 (顺便说一句,“命名块”实际上是一个带标签的语句,其中语句恰好是一个块。)

    实例初始化器用于类成员的语法级别;例如

    public class Test {
        final int foo;
    
        {
             // Some complicated initialization sequence; e.g.
             int tmp;
             if (...) {
                 ...
                 tmp = ...
             } else {
                 ...
                 tmp = ...
             }
             foo = tmp;
        }
    }
    

    根据@dfa 的示例,Initializer 构造最常用于匿名类。另一个用例是对“最终”属性进行复杂的初始化;例如见上面的例子。 (但是,更常见的是使用常规构造函数来执行此操作。上面的模式更常用于静态初始化器。)

    另一个构造是一个普通的块,出现在一个代码块中,比如方法;例如

    public void test() {
        int i = 1;
        {
           int j = 2;
           ...
        }
        {
           int j = 3;
           ...
        }
    }
    

    块最常用作控制语句的一部分,用于对语句序列进行分组。但是当你在上面使用它们时,它们(只是)允许你限制声明的可见性;例如j 在上面。

    这通常表明您需要重构代码,但并不总是一目了然。例如,您有时会在用 Java 编码的解释器中看到这种情况。 switch 臂中的语句可以分解为单独的方法,但这可能会对解释器的“内部循环”造成重大的性能损失;例如

        switch (op) {
        case OP1: {
                 int tmp = ...;
                 // do something
                 break;
             }
        case OP2: {
                 int tmp = ...;
                 // do something else
                 break;
             }
        ...
        };
    

    【讨论】:

    • 谢谢!您能否扩展更多内容或指向一些关于“解释器内部循环的性能影响”部分的文章?
    • 调用一个方法(不能被内联)需要额外的 CPU 周期——这会影响性能。这可能很重要
    【解决方案5】:

    您可以将它用作匿名内部类的构造函数。

    像这样:

    这样你就可以初始化你的对象,因为空闲块是在对象构造过程中执行的。

    它不仅限于匿名内部类,它也适用于常规类。

    public class SomeClass {
        public List data;{
            data = new ArrayList();
            data.add(1);
            data.add(1);
            data.add(1);
        }
    }
    

    【讨论】:

    • 这实际上是一个实例初始化器,这是另一回事。
    • 不同于...?构造函数?哦,是的,当提到构造函数时,我的意思是“作为......的替代或补充”。
    • 不同于问题示例代码中演示的方法内的代码块。
    • 现在这是一个非常酷的技巧,但是使用自动格式化它会变成一个丑陋的烂摊子。仍然很有趣的想法。
    【解决方案6】:

    匿名块可用于限制变量的范围以及double brace initialization

    比较

    Set<String> validCodes = new HashSet<String>();
    validCodes.add("XZ13s");
    validCodes.add("AB21/X");
    validCodes.add("YYLEX");
    validCodes.add("AR2D");
    

    Set<String> validCodes = new HashSet<String>() {{
      add("XZ13s");
      add("AB21/X");
      add("YYLEX");
      add("AR5E");
    }};
    

    【讨论】:

    • 哇,那个双括号初始化的东西很可爱。
    • 请注意,在第二种情况下,validCodes 现在不是 HashSet,而是 HashSet 的子类。对于更复杂的类,这可能会导致一些意外行为。
    • 再一次,实例初始化器与块不同。
    • 我知道双括号初始化技巧,但它并不甜蜜,这是一个肮脏的技巧,会让不知道它的人彻底迷惑。不要使用它!
    【解决方案7】:

    实例初始化块:

    class Test {
        // this line of code is executed whenever a new instance of Test is created
        { System.out.println("Instance created!"); }
    
        public static void main() {
            new Test(); // prints "Instance created!"
            new Test(); // prints "Instance created!"
        }
    }
    

    匿名初始化块:

    class Test {
    
        class Main {
            public void method() {
                System.out.println("Test method");
            }
        }
    
        public static void main(String[] args) {
            new Test().new Main() {
                {
                    method(); // prints "Test method"
                }
            };
    
            {
                //=========================================================================
                // which means you can even create a List using double brace
                List<String> list = new ArrayList<>() {
                    {
                        add("el1");
                        add("el2");
                    }
                };
                System.out.println(list); // prints [el1, el2]
            }
    
            {
                //==========================================================================
                // you can even create your own methods for your anonymous class and use them
                List<String> list = new ArrayList<String>() {
                    private void myCustomMethod(String s1, String s2) {
                        add(s1);
                        add(s2);
                    }
    
                    {
                        myCustomMethod("el3", "el4");
                    }
                };
    
                System.out.println(list); // prints [el3, el4]
            }
        }
    }
    

    变量范围限制:

    class Test {
        public static void main() {
            { int i = 20; }
            System.out.println(i); // error
        }
    }
    

    【讨论】:

      【解决方案8】:

      您可以使用块从父范围初始化最终变量。这是一个很好的方法来限制一些仅用于初始化单个变量的变量的范围。

      public void test(final int x) {
          final ClassA a;
          final ClassB b;
          {
              final ClassC parmC = getC(x);
              a = parmC.getA();
              b = parmC.getB();
          }
          //... a and b are initialized
      }
      

      一般来说,最好将块移动到一个方法中,但是当需要返回多个变量并且您不想创建包装类时,这种语法可能会很好。

      【讨论】:

      • 我看不出有什么理由在这种情况下使用大括号块,它没用。
      【解决方案9】:

      描述一项任务,无论是使用注释还是由于代码结构和所选标识符而固有的,然后使用代码块在其中创建语言本身不强制执行的层次关系。例如:

      public void sendAdminMessage(String msg) throws IOException {
          MessageService service; {
              String senderKey = properties.get("admin-message-server");
              service = MessageService.of(senderKey);
              if (!ms.available()) {
                throw new MessageServiceException("Not available: " + senderKey);
              }
          }
      
          /* workaround for issue 1298: Stop sending passwords. */ {
              final Pattern p = Pattern.compile("^(.*?)\"pass\":.*(\"stamp\".*)$");
              Matcher m = p.matcher(msg);
              if (m.matches()) msg = m.group(1) + m.group(2);
          }
          ...
      }
      

      以上只是一些解释这个概念的示例代码。第一个块由紧接在其前面的内容“记录”:该块用于初始化service 变量。第二个块由注释记录。在这两种情况下,该块都为注释/变量声明提供了“范围”:它们解释了该特定过程的结束位置。它是这种更常见的风格的替代品:

      public void sendAdminMessage(String msg) throws IOException {
          // START: initialize service
          String senderKey = properties.get("admin-message-server");
          MessageService service = MessageService.of(senderKey);
          if (!ms.available()) {
            throw new MessageServiceException("Not available: " + senderKey);
          }
          // END: initialize service
      
          // START: workaround for issue 1298: Stop sending passwords.
          final Pattern p = Pattern.compile("^(.*?)\"pass\":.*(\"stamp\".*)$");
          Matcher m = p.matcher(msg);
          if (m.matches()) msg = m.group(1) + m.group(2);
          // END: workaround for issue 1298: Stop sending passwords.
      
          ...
      }
      

      不过,这些块更好:它们让您可以使用编辑器工具更有效地导航(“转到块的末尾”),它们限定了块中使用的局部变量,因此它们无法逃脱,最重要的是,它们对齐了包含的概念:作为java程序员,您已经熟悉包含的概念:for块,if块,方法块:它们都是代码流中层次结构的表达。出于文档原因而非技术原因的代码收容仍然是收容。为什么要使用不同的机制?一致性很有用。减少精神负担。

      注意:最好的设计很可能是将 MessageService 对象的初始化隔离到一个单独的方法中。然而,这确实导致了意大利面化:在某些时候,将一个简单且易于理解的任务隔离到一个方法中,使得对方法结构的推理变得更加困难:通过隔离它,您已经将初始化消息服务的工作变成了一个黑匣子(在至少,在您查看辅助方法之前),并且要按照代码的流动顺序完全阅读代码,您需要在源文件中跳来跳去。这通常是更好的选择(替代方法是很难测试或重用部分的非常长的方法),但有时并非如此。例如,如果您的块包含对大量局部变量的引用:如果您创建一个辅助方法,则必须传递所有这些变量。方法也不是控制流和局部变量透明的(辅助方法不能从主方法中跳出循环,辅助方法不能从主方法中查看或修改局部变量)。有时这是一个障碍。

      【讨论】: