web-dev-qa-db-ja.com

匿名内部クラスから外部変数を設定する

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が最終的なものであることを要求するため、コンパイルしません-最終的な変数を変更しようとするためコンパイルしません。私はJDK5に縛られています。 doWork()を完全に削除する以外に、doWork()内から結果値を設定する方法はありますか?

51
TC1

Javaは、doWorkが同期すること、および結果のスタックフレームがまだ存在することを知りません。スタックにないものを変更する必要があります。

これはうまくいくと思う

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

その後

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

execute()で。最後に、_return result[0];_する必要があります

62
Lou Franco

この状況はJavaで多く発生します。それを処理する最もクリーンな方法は、単純な値コンテナークラスを使用することです。これは、配列アプローチと同じタイプですが、IMOはよりクリーンです。

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;
    }
}
15
user2080225

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
Oded Peer

含むクラスがMyClassの場合->

MyClass.this.variable = value;

これがプライベート変数で機能するかどうか覚えていない(それが機能すると思う)。

クラス(クラス変数)の属性に対してのみ機能します。メソッドのローカル変数では機能しません。 JSE 7では、おそらくそのようなことを行うためのクロージャーが存在するでしょう。

8
SJuan76

これを行う最も簡単な(そして最もクリーンな)方法は、Java 1.5以降で利用可能なAtomicLongを使用することです

_public Long getNumber(final String type, final String refNumber, final Long year) throws ServiceException {
   final AtomicLong result = new AtomicLong;
   try {
       Session session = PersistenceHelper.getSession();
       session.doWork(new Work() {
               public void execute(Connection conn) throws SQLException {
                   //...
                   result.set(4);
                   //...
               }
           });
   } catch (Exception e) {
       log.error(e);
   }
   return result.get;
}
_

_Java.util.concurrent.atomic_パッケージで利用可能な他のAtomicXXXバリアントがあります:AtomicIntegerAtomicBooleanAtomicReference<V> (for your POJOs) e.t.c

5
Emmanuel

これに対する標準的な解決策は、値を返すことです。例えば、あなたがたの古いものを見る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);
    }
}

(潜在的なリソースリークも修正し、エラーに対してnullを返しました。)

更新:したがって、明らかにWorkはサードパーティライブラリのものであり、変更できません。したがって、少なくともアプリケーションを分離して、直接使用しないようにすることをお勧めします。何かのようなもの:

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;
    }
}

匿名クラス/メソッドはクロージャーではありません-これはまさに違いです。

問題は、doWork()が新しいスレッドを作成してexecute()を呼び出すことができ、getNumber()が結果が設定される前に戻る可能性があり、さらに問題がある:変数を含むスタックフレームがなくなったときにexecute()が結果を書き込む必要があるか?クロージャーを使用する言語では、そのような変数を元のスコープ外で存続させるメカニズムを導入する必要があります(またはクロージャーが別のスレッドで実行されないようにします)。

回避策:

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

Hibernate 4以降、メソッド Session#doReturningWork(ReturningWork<T> work) は内部メソッドから戻り値valを返します。

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ラムダ)を使用してクリーンアップしました)

1
stickyShift