从匿名内部类中设置外部变量

57

在Java中,有没有办法从匿名内部类中访问调用者作用域的变量?

以下是示例代码,以了解我需要什么:

public Long getNumber(final String type, final String refNumber, final Long year) throws ServiceException {
    Long result = null;
    try {
        Session session = PersistenceHelper.getSession();
        session.doWork(new Work() {
                public void execute(Connection conn) throws SQLException {
                    CallableStatement st = conn.prepareCall("{ CALL PACKAGE.procedure(?, ?, ?, ?) }");
                    st.setString(1, type);
                    st.setString(2, refNumber);
                    st.setLong(3, year);
                    st.registerOutParameter(4, OracleTypes.NUMBER);
                    st.execute();
                    result = st.getLong(4) ;
                }
            });
    } catch (Exception e) {
        log.error(e);
    }
    return result;
}

这段代码位于DAO服务类中。显然它无法编译,因为它要求result是final的,如果是这样的话--它不会编译,因为我试图修改一个final变量。我必须使用JDK5。除了完全放弃doWork()之外,有没有一种方法可以在doWork()内设置结果值?

9个回答

67

Java不知道doWork将会是同步的,也不知道result所在的堆栈框架仍然存在。你需要修改不在堆栈中的东西。

我认为这样做会起作用。

 final Long[] result = new Long[1];

然后

 result[0] = st.getLong(4);

execute()函数中。最后,你需要返回result[0];

你可能希望创建一个类,因为你不喜欢在这里使用数组,但这是基本的想法。


5
尽管这样做可以运行,但有点像丑陋的hack。如果你想返回这种值,我认为你应该使用一个命名内部类而不是匿名内部类。在我看来,这种方式更加“清晰”。 - Robin
67
太脏了,我现在想洗个澡,只因为我读了它 :) - chzbrgla
9
我刚刚在键盘上呕吐了,因为懒得创建一个支持类,所以我只是这样做了。 - Piwaf
3
在我淋浴后,以下解决方案适用于我。 public static final AtomicLong RESULT = new AtomicLong(0); RESULT.set(st.getLong(4););(注意:代码需要手动翻译,不要使用机器翻译) - M Faisal Hameed
好的,即使IntelliJ建议这样做,但它似乎不是标准解决方案... - caligula
非常好的答案,即使它有点粗鲁,但是它解释了原因。谢谢。例如在单元测试中,这个解决方案对我来说绝对没问题。 - Mi-La

17

在Java中,这种情况经常出现,处理它的最干净的方式是使用一个简单的值容器类。这与数组方法相同,但在我看来更加清晰。

public class ValContainer<T> {
    private T val;

    public ValContainer() {
    }

    public ValContainer(T v) {
        this.val = v;
    }

    public T getVal() {
        return val;
    }

    public void setVal(T val) {
        this.val = val;
    }
}

有任何参考资料吗?我需要找一些关于这种方法的文档,有吗? - Aritra Chakraborty
3
@Aritra,要找到这样的参考资料可能很困难,但我已经从事Java开发14年了。实际上,当你需要从一个方法中获取返回值(除了它的实际返回值),你必须传递一个对象,并让该对象具有可检查的属性以便在返回后进行检查。我的代码片段只是一种通用的方法,比使用数组或专门用于获取“输出参数”的类更简洁。 - user2080225
那个帮助了...+1 - Parnab Sanyal

10
你需要一个“容器”来保存你的值。但是,你不必创建一个容器类。你可以使用java.util.concurrent.atomic包中的类。它们提供了一个不可变的值包装器,以及一个setget方法。你有AtomicIntegerAtomicBooleanAtomicReference<V>(用于对象)等等。
在外部方法中:
final AtomicLong resultHolder = new AtomicLong();

在匿名内部类方法中。
long result = getMyLongValue();
resultHolder.set(result);

稍后在您的外部方法中

return resultHolder.get();

这里是一个例子。

public Long getNumber() {
   final AtomicLong resultHolder = new AtomicLong();
   Session session = new Session();
   session.doWork(new Work() {
       public void execute() {
           //Inside anonymous inner class
           long result = getMyLongValue();
           resultHolder.set(result);
       }
   });
   return resultHolder.get(); //Returns the value of result
}

9

Long是不可变的。如果你使用一个可变类来保存long值,那么你可以改变这个值。例如:

public class Main {

public static void main( String[] args ) throws Exception {
    Main a = new Main();
    System.out.println( a.getNumber() );
}

public void doWork( Work work ) {
    work.doWork();
}


public Long getNumber() {
    final LongHolder result = new LongHolder();
    doWork( new Work() {
        public void doWork() {
            result.value = 1L;
        }
    } );
    return result.value;
}

private static class LongHolder { 
    public Long value; 
}

private static abstract class Work {
    public abstract void doWork();
}

}

8
如果包含的类是MyClass -->
MyClass.this.variable = value;

我不确定这个方法是否适用于私有变量(但我认为是可以的)。

只适用于类属性(类变量),不适用于方法局部变量。在JSE 7中可能会引入闭包来实现这种功能。


Java 7 不会有闭包。即使在Java 8中引入了它们,可能也无法写入局部变量(即“访问的局部变量必须是final”规则可能仍然适用)。 - Joachim Sauer
有点晚了,但这似乎在Java 8中有效。 - Govind Parmar

3

匿名类/方法不是闭包-这正是区别所在。

问题在于doWork()可能创建一个新线程来调用execute(),而getNumber()可能在结果设置之前返回-更为麻烦的是:execute()应该在哪里写入结果,当包含变量的堆栈帧消失时?具有闭包的语言必须引入一种机制来保持这些变量在其原始范围之外存活(或确保闭包不在单独的线程中执行)。

解决方法:

Long[] result = new Long[1];
...
result[0] = st.getLong(4) ;
...
return result[0];

2

解决这个问题的标准方法是返回一个值。例如,可以参考古老的 java.security.AccessController.doPrivileged

因此,代码应该类似于以下内容:

public Long getNumber(
    final String type, final String refNumber, final Long year
) throws ServiceException {
    try {
        Session session = PersistenceHelper.getSession();
        return session.doWork(new Work<Long>() {
            public Long execute(Connection conn) throws SQLException {
                CallableStatement st = conn.prepareCall("{ CALL PACKAGE.procedure(?, ?, ?, ?) }");
                try {
                    st.setString(1, type);
                    st.setString(2, refNumber);
                    st.setLong(3, year);
                    st.registerOutParameter(4, OracleTypes.NUMBER);
                    st.execute();
                    return st.getLong(4);
                } finally {
                    st.close();
                }
            }
        });
    } catch (Exception e) {
        throw ServiceException(e);
    }
}

更新:显然,Work来自第三方库,无法更改。因此,我建议不要使用它,至少要将您的应用程序与之隔离,以便您不会直接使用它。可以考虑以下方式:

(还修复了潜在的资源泄漏,并对任何错误返回null。)

public interface WithConnection<T> {
    T execute(Connection connnection) throws SQLException;
}
public class SessionWrapper {
    private final Session session;
    public SessionWrapper(Session session) {
        session = nonnull(session);
    }
    public <T> T withConnection(final WithConnection<T> task) throws Service Exception {
        nonnull(task);
        return new Work() {
            T result;
            {
                session.doWork(this);
            }
            public void execute(Connection connection) throws SQLException {
                result = task.execute(connection);
            }
        }.result;
    }
}

Work是org.hibernate.jdbc中的一个接口,execute的签名为public void execute(java.sql.Connection)。它真的会吞掉那个返回类型并工作吗? - TC1
很抱歉,我并不是很理解你回答中的第二个(更新后的)部分。我遇到了侦听器内部匿名类的问题 (因为它是一个覆盖方法,所以无法返回值)。你是否基本上使用了这个?或者还有其他概念需要我理解吗?或者我应该直接问一个全新的问题? - caligula

1

从Hibernate 4开始,方法Session#doReturningWork(ReturningWork<T> work)将返回内部方法的返回值:

public Long getNumber(final String type, final String refNumber, final Long year) throws ServiceException {
    try {
        Session session = PersistenceHelper.getSession();
        return session.doReturningWork(conn -> {
            CallableStatement st = conn.prepareCall("{ CALL PACKAGE.procedure(?, ?, ?, ?) }");
            st.setString(1, type);
            st.setString(2, refNumber);
            st.setLong(3, year);
            st.registerOutParameter(4, OracleTypes.NUMBER);
            st.execute();
            return st.getLong(4);
        });
    } catch (Exception e) {
        log.error(e);
    }
    return null;
}

(使用Java 8 lambda进行清理)

0

使用AtomicLong在一个非常相似的情况下帮助了我,代码看起来很干净。

// Create a new final AtomicLong variable with the initial value 0.
final AtomicLong YOUR_VARIABLE = new AtomicLong(0);
...
// set long value to the variable within inner class
YOUR_VARIABLE.set(LONG_VALUE);
...
// get the value even outside the inner class
YOUR_VARIABLE.get();

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