2015-05-14 5 views
8

Ho questo codice:Come evitare problemi derivanti dall'accesso ai campi statici prima dell'inizializzazione della classe?

public abstract class Person { 

    public static final class Guy extends Person { 

     public static final Guy TOM = new Guy(); 
     public static final Guy DICK = new Guy(); 
     public static final Guy HARRY = new Guy(); 
    } 

    public static final List<Guy> GUYS = ImmutableList.of(Guy.TOM, Guy.DICK, Guy.HARRY); 
} 

Lo so, sembra che dovrebbe essere un enum, e sarebbe, ma ha bisogno di ereditare da una persona, una classe astratta.

Il mio problema è: se provo ad accedere all'elenco dei ragazzi, sto bene. Ma provo ad accedere a qualsiasi Guy in particolare, ho un problema: Guy viene caricato prima dello Person. Tuttavia, dal momento che Guy eredita Person, Person viene caricato e tenta di accedere a TOM, ma dal momento che Guy è già stato caricato, non può iniziare a caricare di nuovo, e quindi abbiamo un riferimento null. (E ImmutableList non accetta nulla, così abbiamo ottenere un'eccezione.)

quindi so perché questo sta accadendo, ma non sono sicuro come evitarlo. Potrei spostare l'elenco nella classe Guy, ma avrò più di un'implementazione di Person e mi piacerebbe avere un elenco principale di tutti i Person s nella classe Person.

Mi sembra strano che sia possibile caricare una classe interna statica senza caricarne la classe contenente, ma suppongo che abbia senso. C'è un modo per risolvere questo problema?

+1

Non tenere i ragazzi in "Ragazzo" o "Persona". Conservalo in una classe 'PersonManager' o 'People' o come vuoi chiamarlo. –

+0

La ragione per cui mi piace in questo modo è che posso fare "Guy.TOM", che credo abbia una migliore leggibilità. – codebreaker

+0

Se il ragazzo non fosse statico, potresti semplicemente fare qualcosa del tipo: 'Guy tom = new Guy()' 'Lista guys = new ArrayList <>()' 'guys.add (tom)' – EricF

risposta

3

Si potrebbe mantenere la vostra lista immutabile in una classe separata, come People.GUYS:

public class People { 
    public static final List<Guy> GUYS = ImmutableList.of(Guy.TOM, Guy.DICK, Guy.HARRY); 
} 

questo modo si può ancora mantenere i singoli ragazzi nella classe Guy:

public abstract class Person { 

    public static final class Guy extends Person { 

     public static final Guy TOM = new Guy(); 
     public static final Guy DICK = new Guy(); 
     public static final Guy HARRY = new Guy(); 
    } 
} 
2

Una soluzione per motivi di soluzione, questo sembra lavorare (per singolo thread) disegno

public class Person 
{ 
    static Lazy tom = new Lazy(); 
    static Lazy cat = new Lazy(); 

    public static final List<Guy> GUYS = ImmutableList.of(tom.get(), cat.get()); 

    public static final class Guy extends Person 
    { 
     public static final Guy TOM = tom.get(); 
     public static final Guy CAT = cat.get(); 
    } 

    static class Lazy 
    { 
     Guy v; 
     Guy get() 
     { 
      if(v==null) 
      { 
       Object x = Guy.TOM; // just to trigger Guy initialization 
       if(v==null) 
        v = new Guy(); 
      } 
      return v; 
     } 
    } 

OP è intrinsecamente ricorsivo. Sebbene Java abbia una procedura di inizializzazione ben definita (che si arresta quando viene rilevata la ricorsione), il comportamento varia a seconda di quale classe viene inizializzata per prima.

Il problema è aggravato dal fatto che i campi sono final, che limita fortemente il modo in cui è possibile assegnarli. Altrimenti, possiamo provare if(null) assign in più punti per risolvere il problema.

La soluzione pubblicata sopra utilizza essenzialmente una variabile temporanea per risolvere il problema del vincolo final.


una versione modificata utilizzando una mappa, adatto a più numero di campi:

public class Person 
{ 
    static Map<String,Guy> cache = new HashMap<>(); // remove after both class are initialized 
    static Guy get(String name) 
    { 
     if(!cache.containsKey(name)) 
     { 
      Object x = Guy.TOM; // just to trigger Guy initialization 
      if(!cache.containsKey(name)) 
       cache.put(name, new Guy()); 
     } 
     return cache.get(name); 
    } 

    public static final List<Guy> GUYS = ImmutableList.of(get("tom"), get("cat")); 

    static{ if(Guy.TOM!=null) cache=null; } 

    public static final class Guy extends Person 
    { 
     public static final Guy TOM = get("tom"); 
     public static final Guy CAT = get("cat"); 

     static{ if(Person.GUYS!=null) cache=null; } 
    } 

Multi-Threading

E 'possibile che un thread si avvia l'inizializzazione persona mentre un altro thread si avvia inizializzando Guy. Deadlock è possibile, e succede nel mondo reale. Pertanto questo design è intrinsecamente imperfetto.

In generale, due classi (non necessariamente padre-figlio) che hanno dipendenza l'una dall'altra durante l'inizializzazione statica sono intrinsecamente in pericolo di deadlock.

L'applicazione può farla franca con questo tipo di cose, attivando con attenzione l'inizializzazione in modo prevedibile all'avvio dell'applicazione. Tuttavia, questo è abbastanza pericoloso, e potrebbe mandare qualcuno in giorni di inferno di debugging.

+0

Se hai refactored l'inizializzazione dell'istanza per sfruttare l'idioma Lazy Double Check (efficace Java, 2nd Ed.), Non risolverebbe i potenziali problemi di liveness ? – scottb

+0

@scottb - il deadlock è causato dalla procedura di inizializzazione di JVM e non penso ci sia nulla che possiamo fare - a parte il non progettare una dipendenza ricorsiva al primo posto. – ZhongYu

+0

Avevo pensato che questo potesse essere solo il genere di cose di cui parlava Bloch quando menzionava "circolarità dell'inizializzazione dannose" nel contesto del pigro idioma a doppio controllo. – scottb

1

è l'obbligo di utilizzare la classe astratta astratta Person (cioè è già una parte del codice legacy)?

In caso contrario, come suggerimento, suggerisco di poter refactoring il design del modello di dati in una classe Guy enum che implementa un'interfaccia Persona. Ci sono molti vantaggi a questo design rispetto a quello basato sulla classe.

public enum Guy implements Person { 

    TOM { 
     @Override 
     public void personMethod1(...) {...} // constant-specific 
      : 
      : 
    }, 
    DICK { 
     @Override 
     public void personMethod1(...) {...} // constant-specific 
      : 
      : 
    }; 

    @Override 
    public int personMethod2(...) {...} // general to the enum class 
     : 
     : 
} 

noti che in questo caso non v'è alcun motivo per definire un'istanza List con i riferimenti costanti come API enum fornisce il metodo GUY.values().

Risolve tutti i problemi di inizializzazione pur consentendo più implementazioni di Person. Il rovescio della medaglia è nella duplicazione del codice. avranno bisogno i metodi in Person da attuare in ogni classe enum (ma se ci sono molti metodi si può sempre creare una classe di supporto private static e il metodo di inoltrare le chiamate ad esso.)

+0

Potrei refactoring per usare un'interfaccia, ma dovrei duplicare il codice nella classe astratta ogni volta che voglio fare un enum per ereditare dall'interfaccia. Ci ho pensato, e penso che sia più pulito usare classi astratte e "pseudo-enumi". – codebreaker

1

cambiamento

public static final List<Guy> GUYS = ImmutableList.of(Guy.TOM, Guy.DICK, Guy.HARRY); 

a

public static final List<Guy> GUYS = Collections.unmodifiableList(Arrays.asList(new Guy[] {Guy.TOM, Guy.DICK, Guy.HARRY})); 

dovrebbe funzionare (ho provato utilizzando JDK 8), ma è ancora soddisfare le vostre esigenze?

public abstract class Person { 

    public static final class Guy extends Person { 

     public static final Guy TOM = new Guy(); 
     public static final Guy DICK = new Guy(); 
     public static final Guy HARRY = new Guy(); 
    } 

    public static final List<Guy> GUYS = Collections.unmodifiableList(Arrays.asList(new Guy[] {Guy.TOM, Guy.DICK, Guy.HARRY})); 
} 
+0

Questo non funziona. Non lancia un'eccezione, ma se accedo a Guy.TOM e quindi a Persona.GUYS, ottengo un elenco completo di null. – codebreaker

Problemi correlati