web-dev-qa-db-fra.com

Définition d'une variable externe à partir d'une classe interne anonyme

Existe-t-il un moyen d'accéder aux variables de l'appelant à partir d'une classe interne anonyme en Java?

Voici l'exemple de code pour comprendre ce dont j'ai besoin:

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

Le code est dans une classe de service DAO. Évidemment, il ne compile pas, car il demande que result soit final, si c'est le cas - il ne compile pas car j'essaie de modifier une variable finale Je suis lié à JDK5. En dehors de la suppression complète de doWork(), existe-t-il un moyen de définir la valeur du résultat à partir de doWork()?

50
TC1

Java ne sait pas que doWork va être synchrone et que le cadre de pile dans lequel le résultat se trouve sera toujours là. Vous devez modifier quelque chose qui n'est pas dans la pile.

Je pense que cela fonctionnerait

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

et alors

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

dans execute(). À la fin, vous devez return result[0];

61
Lou Franco

Cette situation se présente souvent en Java et le moyen le plus propre de la gérer consiste à utiliser une simple classe de conteneur de valeurs. C'est la même chose que l'approche par tableaux, mais c'est plus propre à l'OMI.

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 est immuable. Si vous utilisez une classe mutable contenant une valeur longue, vous pouvez modifier la valeur. Par exemple:

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

Si la classe contenante est MyClass ->

MyClass.this.variable = value;

Ne vous souvenez pas si cela fonctionnerait avec une variable privée (je pense que cela fonctionnerait).

Ne fonctionne que pour les attributs de la classe (variable de classe). Ne fonctionne pas pour les variables locales de la méthode. Dans JSE 7, il y aura probablement des fermetures pour faire ce genre de chose.

7
SJuan76

Pour ce faire, le moyen le plus simple (et le plus propre) consiste à utiliser AtomicLong disponible depuis Java 1.5.

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

D'autres versions d'AtomicXXX sont disponibles dans le package Java.util.concurrent.atomic: AtomicInteger, AtomicBoolean, AtomicReference<V> (for your POJOs) etc.

4
Emmanuel

La solution standard consiste à renvoyer une valeur. Voir, par exemple, olde Java.security.AccessController.doPrivileged.

Donc, le code ressemblerait à quelque chose comme ça:

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

(Correction également de la fuite potentielle des ressources et retourne null pour toute erreur.)

Mise à jour: Donc, apparemment, Work provient d'une bibliothèque tierce et ne peut pas être modifié. Donc, je suggère de ne pas l'utiliser, au moins isoler votre application de sorte que vous ne l'utilisez pas directement. Quelque chose comme:

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

Les classes/méthodes anonymes ne sont pas des fermetures - c'est exactement la différence. 

Le problème est que doWork() pourrait créer un nouveau thread pour appeler execute() et que getNumber() pourrait revenir avant que le résultat ne soit défini - et encore plus problématique: où execute() devrait-il écrire le résultat lorsque le cadre de pile contenant la variable a disparu? Les langues avec des fermetures doivent introduire un mécanisme permettant de garder ces variables actives en dehors de leur portée d'origine (ou de s'assurer que la fermeture n'est pas exécutée dans un thread séparé).

Une solution de contournement:

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

A partir de Hibernate 4, la méthode Session#doReturningWork(ReturningWork<T> work) renverra la valeur renvoyée par la méthode interne:

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

(Nettoyé avec un Java 8 lambda)

1
stickyShift