合成访问器方法警告

22

我在Eclipse中设置了一些新的警告设置。在使用这些新设置时,我遇到了一个奇怪的警告。经过阅读,我知道了这是什么,但找不到删除它的方法。

以下是我的示例代码问题

public class Test {
    private String testString;

    public void performAction() {

        new Thread( new Runnable() {
            @Override
            public void run() {
                testString = "initialize"; // **
            }
        });
    }
}

在 Eclipse 中,带有 ** 的那一行让我收到了警告。

Read access to enclosing field Test.testString is emulated by a synthetic accessor method. 
Increasing its visibility will improve your performance.

问题在于,我不想改变testString的访问修饰符。同时也不想为它创建一个getter。

应该做出什么样的改变?


More descriptive example 

public class Synthetic
{
    private JButton testButton;

    public Synthetic()
    {
        testButton = new JButton("Run");
        testButton.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent ae)
                {
                    /* Sample code */
                    if( testButton.getText().equals("Pause") ) {
                        resetButton(); // **    
                    } else if( testButton.getText().equals("Run") ) {
                        testButton.setText("Pause"); // **  
                    }

                }
            }
        );
    }

    public void reset() {
        //Some operations
        resetButton();
    }

    private void resetButton() {
        testButton.setText("Run");
    }
}

使用**的行会给我相同的警告。


在网站上搜索,有很多关于这个警告的帖子。 - khachik
5个回答

50

什么是“合成”方法?

Method类(及其父类Member)开始,我们了解到合成成员是“由编译器引入的”,并且JLS §13.1会告诉我们更多信息。它指出:

如果Java编译器发出的构造不对应于源代码中明确或隐式声明的构造,则必须将其标记为合成的

由于本节正在讨论二进制兼容性,因此还值得引用JVMS,并且JVMS §4.7.8添加了一些上下文:

没有在源代码中出现的类成员必须使用Synthetic属性进行标记,否则必须设置其ACC_SYNTHETIC标志。唯一不受此要求限制的是不被视为实现工件的编译器生成方法...

Synthetic属性在JDK1.1中引入,以支持嵌套类和接口。

换句话说,“合成”方法是Java编译器为了支持JVM本身不支持的语言特性而引入的实现工件。

问题出在哪里?

您遇到了这样一种情况:您正在尝试从匿名内部类访问类的private字段。Java语言允许这样做,但JVM不支持它,因此Java编译器会生成一个合成方法来将private字段暴露给内部类。这是安全的,因为编译器不允许任何其他类调用此方法,但它确实引入了两个(小)问题:

  1. 声明了额外的方法。对于绝大多数用例而言,这不应该是问题,但如果您在像Android这样的受限环境下工作且生成了大量这些合成方法,则可能会遇到问题。
  2. 访问该字段是通过合成方法间接进行的,而不是直接进行的。这对于高度注重性能的用例来说也不应该是问题。如果出于性能原因不想在此处使用getter方法,则也不想使用合成的getter方法。这在实践中很少会成为问题。

简而言之,它们真的不错。除非您有充分的理由避免使用合成方法(即您已确定它们是应用程序中的瓶颈),否则应该让编译器根据需要生成它们。如果Eclipse的警告会让您感到困扰,请考虑关闭它。

我应该怎么办?

如果您确实想防止编译器生成合成方法,有几个选项:

选项1:更改权限

包私有或protected字段可直接访问内

public Synthetic() {
  // Create final local instance - will be reachable by the inner class
  final JButton testButton = new JButton("Run");
  testButton.addActionListener(
      new ActionListener() {
        public void actionPerformed(ActionEvent ae) {
          /* Sample code */
          if( testButton.getText().equals("Pause") ) {
            resetButton();
          } else if( testButton.getText().equals("Run") ) {
            testButton.setText("Pause");
          }
        }
      });
  // associate same instance with outer class - this.testButton can be final too
  this.testButton = testButton;
}

这并不总是你想要做的。例如,如果testButton以后可以更改指向另一个对象,那么你需要重新构建你的ActionListener(尽管这样更显式和明确,所以可以说这是一种特性),但我认为它是最能清晰地展示其意图的选项。


关于线程安全的注意事项

你的例子Test类不是线程安全的 - testString在另一个Thread中被设置,但你没有在该赋值上同步。将testString标记为volatile就足以确保所有线程都能看到更新。 Synthetic的例子没有这个问题,因为testButton只在构造函数中设置,但既然如此,建议将testButton标记为final


1
在Java 11中添加了JEP 181:基于嵌套的访问控制,不再生成访问器方法。 - Marcono1234

3

在您的第二个示例中,不必直接访问testButton;您可以通过检索操作事件的源来访问它。

对于resetButton()方法,您可以添加一个参数以传递要操作的对象,如果您已经这样做了,那么降低其访问限制就不是一个很大的问题。


0

这是Java默认可见性(也称为“包私有”)非常有用的罕见情况之一。

public class Test {
    /* no private */ String testString;

    public void performAction() {

        new Thread( new Runnable() {
            @Override
            public void run() {
                testString = "initialize"; // **
            }
        });
    }
}

这将执行以下操作:

  • testString现在对于与外部类(Test)在同一包中的所有类都可用。
  • 由于内部类实际上是生成为OuterClassPackage.OuterClassName$InnerClassName,它们也驻留在同一个包中。因此,它们可以直接访问此字段。
  • 与使此字段protected不同,默认可见性不会使此字段对子类可用(当然,除非它们在同一包中)。因此,您不会为外部用户污染API。

使用private时,javac将生成一个合成访问器,它本身只是具有Java默认可见性的getter方法。因此,它基本上做相同的事情,只是多了一个额外方法的最小开销。


0

考虑到上下文(您将其作为相当昂贵的操作的一部分分配给变量),我认为您不需要做任何事情。


1
这只是一个例子,我并没有执行这个操作。 - Rites
如果您正在执行大量操作,现代JVM将对其进行内联处理。 - Tom Hawtin - tackline

0
我认为问题在于您正在将一个字符串设置在父类上。这会影响性能,因为线程需要查找变量所在的位置。我认为更清晰的方法是使用Callable返回一个字符串,然后进行.get()或其他返回结果的操作。获得结果后,您可以将数据设置回父类。
这样做的想法是确保线程只做一件事情,而且只做一件事情,而不是在其他类上设置变量。这是一种更清洁的方法,可能更快,因为内部线程不会访问其外部的任何东西。这意味着锁定较少。 :)

谢谢Amir。但这只是一个例子。我将编辑我的问题,使其更加详细。 - Rites

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