Impostazione della variabile esterna dalla class interna anonima

C’è un modo per accedere alle variabili con scope del chiamante da una class interna anonima in Java?

Ecco il codice di esempio per capire di cosa ho bisogno:

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

Il codice è in una class di servizio DAO. Ovviamente non si compila, perché chiede che il result sia definitivo, se lo è – non si compila perché provo a modificare una variabile finale. Sono legato a JDK5. Oltre a eliminare completamente doWork() , esiste un modo per impostare il valore del risultato all’interno di doWork() ?

Java non sa che doWork sta per essere sincrono e che il frame dello stack in cui si trova sarà ancora lì. Devi modificare qualcosa che non è nella pila.

Penso che funzionerebbe

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

e poi

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

in execute() . Alla fine, devi return result[0];

Questa situazione si presenta molto in Java e il modo più semplice per gestirlo è con una class contenitore di valori semplice. È lo stesso tipo di approccio alla matrice, ma è IMO più pulito.

 public class ValContainer { 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; } } 

Long è immutabile. Se si utilizza una class mutabile, con un valore lungo, è ansible modificare il valore. Per esempio:

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

Se la class che contiene è MyClass ->

 MyClass.this.variable = value; 

Non ricordo se questo funzionerebbe con una variabile privata (penso che funzionerebbe).

Funziona solo per gli attributi della class (variabile di class). Non funziona per le variabili locali del metodo. In JSE 7 probabilmente ci saranno chiusure per fare quel genere di cose.

La soluzione standard a questo è restituire un valore. Vedi, ad esempio, ye olde java.security.AccessController.doPrivileged .

Quindi il codice sarebbe simile a questo:

 public Long getNumber( final String type, final String refNumber, final Long year ) throws ServiceException { try { Session session = PersistenceHelper.getSession(); return session.doWork(new Work() { 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); } } 

(Fissata anche la potenziale perdita di risorse e restituendo null per qualsiasi errore.)

Aggiornamento: Quindi apparentemente il Work proviene da una libreria di terze parti e non può essere modificato. Quindi suggerisco di non usarlo, almeno isolare la tua applicazione in modo da non utilizzarla direttamente. Qualcosa di simile a:

 public interface WithConnection { T execute(Connection connnection) throws SQLException; } public class SessionWrapper { private final Session session; public SessionWrapper(Session session) { session = nonnull(session); } public  T withConnection(final WithConnection 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; } } 

Il modo più semplice (e più pulito) per farlo è utilizzare AtomicLong disponibile da 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; } 

Sono disponibili altre varianti AtomicXXX nel pacchetto java.util.concurrent.atomic : AtomicInteger , AtomicBoolean , AtomicReference (for your POJOs) ecc.

Le classi / metodi anonimi non sono chiusure: questa è esattamente la differenza.

Il problema è che doWork() potrebbe creare un nuovo thread per chiamare execute() e getNumber() potrebbe tornare prima che il risultato sia impostato – e ancora più problematico: dove dovrebbe execute() scrivere il risultato quando lo stack frame che contiene la variabile è andato? Le lingue con chiusure devono introdurre un meccanismo per mantenere tali variabili vivi al di fuori del loro ambito originale (o assicurare che la chiusura non sia eseguita in un thread separato).

Una soluzione:

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

A partire da Hibernate 4, il metodo Session#doReturningWork(ReturningWork work) restituirà la val di ritorno dal metodo interno:

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

(Pulito usando un lambda Java 8)