Perché utilizzare Opzionale in Java 8+ anziché i tradizionali controlli puntatori nulli?

93

Recentemente siamo passati a Java 8. Ora, vedo le applicazioni inondate con Optional objects.

Prima di Java 8 (Stile 1)

Employee employee = employeeServive.getEmployee();

if(employee!=null){
    System.out.println(employee.getId());
}

Dopo Java 8 (Stile 2)

Optional<Employee> employeeOptional = Optional.ofNullable(employeeService.getEmployee());
if(employeeOptional.isPresent()){
    Employee employee = employeeOptional.get();
    System.out.println(employee.getId());
}

Non vedo alcun valore aggiunto di Optional<Employee> employeeOptional = employeeService.getEmployee(); quando il servizio stesso restituisce facoltativo:

Provenendo da uno sfondo Java 6, vedo lo stile 1 come più chiaro e con meno righe di codice. C'è qualche vantaggio reale che mi manca qui?

Consolidato dalla comprensione di tutte le risposte e ulteriori ricerche su blog

    
posta user3198603 18.01.2018 - 15:34
fonte

10 risposte

98

Lo stile 2 non sta passando a Java 8 abbastanza per vederne tutti i benefici. Non vuoi affatto if ... use . Vedi Esempi di Oracle . Seguendo il loro consiglio, otteniamo:

Stile 3

// Changed EmployeeServive to return an optional, no more nulls!
Optional<Employee> employee = employeeServive.getEmployee();
employee.ifPresent(e -> System.out.println(e.getId()));

O uno snippet più lungo

Optional<Employee> employee = employeeServive.getEmployee();
// Sometimes an Employee has forgotten to write an up-to-date timesheet
Optional<Timesheet> timesheet = employee.flatMap(Employee::askForCurrentTimesheet); 
// We don't want to do the heavyweight action of creating a new estimate if it will just be discarded
client.bill(timesheet.orElseGet(EstimatedTimesheet::new));
    
risposta data 18.01.2018 - 16:14
fonte
43

Se stai utilizzando Optional come livello di "compatibilità" tra un'API precedente che potrebbe ancora restituire null , potrebbe essere utile creare l'Opzionale (non vuoto) nell'ultima fase in cui ti trovi sicuro che tu abbia qualcosa. Ad esempio, dove hai scritto:

Optional<Employee> employeeOptional = Optional.ofNullable(employeeService.getEmployee());
if(employeeOptional.isPresent()){
    Employee employeeOptional= employeeOptional.get();
    System.out.println(employee.getId());
}

Io opterei per:

Optional.of(employeeService)                 // definitely have the service
        .map(EmployeeService::getEmployee)   // getEmployee() might return null
        .map(Employee::getId)                // get ID from employee if there is one
        .ifPresent(System.out::println);     // and if there is an ID, print it

Il punto è che conosci che esiste un servizio del dipendente non null, quindi puoi racchiuderlo in un Optional con Optional.of() . Quindi, quando chiami getEmployee() su questo, puoi o non puoi ottenere un dipendente. Quel dipendente può (o, possibilmente, non può) avere un ID. Quindi, se hai finito con un ID, devi stamparlo.

Non è necessario controllare esplicitamente qualsiasi null, presenza, ecc. in questo codice.

    
risposta data 18.01.2018 - 20:37
fonte
20

Non c'è più nessun valore aggiunto nell'avere un Optional di un singolo valore. Come hai visto, sostituisce semplicemente un controllo per null con un controllo per la presenza.

C'è un enorme valore aggiunto nell'avere un Collection di tali cose. Tutti i metodi di streaming introdotti per sostituire i loop di basso livello vecchio stile comprendono Optional cose e fanno la cosa giusta su di loro (non elaborandoli o in ultima analisi restituendo un altro% non impostato Optional ). Se gestisci articoli n più spesso che con singoli articoli (e il 99% di tutti i programmi realistici lo fanno), allora è un enorme miglioramento avere un supporto integrato per le cose presenti opzionalmente. Nel caso ideale non devi mai controllare la presenza di null o .

    
risposta data 18.01.2018 - 15:47
fonte
14

Se utilizzi Optional proprio come un'API di fantasia per isNotNull() , allora sì, non troverai differenze con il solo controllo di null .

Cose che NON dovresti usare Optional per

L'utilizzo di Optional solo per verificare la presenza di un valore è Bad Code ™:

// BAD CODE ™ --  just check getEmployee() != null  
Optional<Employee> employeeOptional =  Optional.ofNullable(employeeService.getEmployee());  
if(employeeOptional.isPresent()) {  
    Employee employee = employeeOptional.get();  
    System.out.println(employee.getId());
}  

Cose che dovresti usare Optional per

Evita che un valore non sia presente, producendone uno quando è necessario quando si utilizzano i metodi dell'API con ritorno nullo:

Employee employee = Optional.ofNullable(employeeService.getEmployee())
                            .orElseGet(Employee::new);
System.out.println(employee.getId());

Oppure, se creare un nuovo Impiegato è troppo costoso:

Optional<Employee> employee = Optional.ofNullable(employeeService.getEmployee());
System.out.println(employee.map(Employee::getId).orElse("No employee found"));

Inoltre, rendi tutti consapevoli che i tuoi metodi potrebbero non restituire un valore (se, per qualsiasi motivo, non puoi restituire un valore predefinito come sopra):

// Your code without Optional
public Employee getEmployee() {
    return someCondition ? null : someEmployee;
}
// Someone else's code
Employee employee = getEmployee(); // compiler doesn't complain
// employee.get...() -> NPE awaiting to happen, devs criticizing your code

// Your code with Optional
public Optional<Employee> getEmployee() {
    return someCondition ? Optional.empty() : Optional.of(someEmployee);
}
// Someone else's code
Employee employee = getEmployee(); // compiler complains about incompatible types
// Now devs either declare Optional<Employee>, or just do employee = getEmployee().get(), but do so consciously -- not your fault.

E infine, ci sono tutti i metodi simili a stream che sono già stati spiegati in altre risposte, anche se questi non sono in realtà tu che decidono di usare Optional ma facendo uso del Optional fornito da qualcun altro.

    
risposta data 19.01.2018 - 09:43
fonte
12

Il punto chiave per me nell'utilizzo di Opzionale è chiaro quando lo sviluppatore deve verificare se la funzione restituisce o meno il valore.

//service 1
Optional<Employee> employeeOptional = employeeService.getEmployee();
if(employeeOptional.isPresent()){
    Employee employeeOptional= employeeOptional.get();
    System.out.println(employee.getId());
}

//service 2
Employee employee = employeeService.getEmployeeXTPO();
System.out.println(employee.getId());
    
risposta data 18.01.2018 - 19:26
fonte
8

Il tuo errore principale è che stai ancora pensando in termini più procedurali. Questo non è inteso come una critica a te come persona, è solo un'osservazione. Pensare in termini più funzionali arriva con il tempo e la pratica, e quindi i metodi sono presenti e appaiono come le cose più ovvie e corrette da chiamare per te. Il tuo errore secondario secondario è la creazione del tuo opzionale all'interno del tuo metodo. L'opzione Opzionale ha lo scopo di aiutare a documentare che qualcosa può o non può restituire un valore. Potresti non ottenere nulla.

Questo ti ha portato a scrivere codice perfettamente leggibile che sembra perfettamente ragionevole, ma sei stato sedotto dalle vili tentatrici gemelle che sono state ottenute e sono presenti.

Ovviamente la domanda diventa rapidamente "perché sono isPresent e arriva anche lì?"

Una cosa che manca a molte persone qui è che isPresent () è che non è qualcosa che è fatto per un nuovo codice scritto da persone pienamente a bordo con quanto siano utili lambdas e che amano la cosa funzionale.

Tuttavia, ci concede alcuni (due) vantaggi buoni, fantastici e glamour (?):

  • Facilita la transizione del codice legacy per utilizzare le nuove funzionalità.
  • Facilita le curve di apprendimento di Facoltativo.

Il primo è piuttosto semplice.

Immagina di avere un'API simile a questa:

public interface SnickersCounter {
  /** 
   * Provides a proper count of how many snickers have been consumed in total.
   */
  public SnickersCount howManySnickersHaveBeenEaten();

  /**
    * returns the last snickers eaten.<br>
    * If no snickers have been eaten null is returned for contrived reasons.
    */
  public Snickers lastConsumedSnickers();
}

E avevi una classe legacy che usava questo (riempire gli spazi vuoti):

Snickers lastSnickers = snickersCounter.lastConsumedSnickers();
if(null == lastSnickers) {
  throw new NoSuchSnickersException();
}
else {
  consumer.giveDiabetes(lastSnickers);
}

Un esempio forzato per essere sicuro. Ma portami con me qui.

Java 8 è ora disponibile e stiamo cercando di salire a bordo. Quindi, una delle cose che facciamo è che vogliamo sostituire la nostra vecchia interfaccia con qualcosa che restituisce Opzionale. Perché? Perché come qualcun altro ha già gentilmente menzionato: Ciò consente di capire se qualcosa può essere nullo Questo è già stato sottolineato da altri. Ma ora abbiamo un problema. Immaginiamo di avere (mi scusi mentre ho colpito alt + F7 su un metodo innocente), 46 luoghi in cui questo metodo è chiamato in codice legacy ben collaudato che fa un lavoro eccellente altrimenti. Ora devi aggiornare tutti questi elementi.

QUESTO è dove splende isPresent.

Perché ora:     Snickers lastSnickers = snickersCounter.lastConsumedSnickers ();     if (null == lastSnickers) {       lanciare nuovi NoSuchSnickersException ();     }     altro {       consumer.giveDiabetes (lastSnickers);     }

diventa:

Optional<Snickers> lastSnickers = snickersCounter.lastConsumedSnickers();
if(!lastSnickers.isPresent()) {
  throw new NoSuchSnickersException();
}
else {
  consumer.giveDiabetes(lastSnickers.get());
}

E questo è un semplice cambiamento che puoi dare al nuovo junior: può fare qualcosa di utile, e allo stesso tempo esplorerà il codice base. win-win. Dopotutto, qualcosa di simile a questo schema è abbastanza diffuso. E ora non devi riscrivere il codice per usare lambda o altro. (In questo caso particolare sarebbe banale, ma lascio pensare a degli esempi in cui sarebbe difficile come esercizio per il lettore.)

Si noti che questo significa che il modo in cui lo si è fatto è essenzialmente un modo per gestire il codice legacy senza fare costose riscritture. Che dire del nuovo codice?

Bene, nel tuo caso, dove vuoi solo stampare qualcosa, devi semplicemente fare:

snickersCounter.lastConsumedSnickers () ifPresent (System.out :: println);.

Che è piuttosto semplice e perfettamente chiaro. Il punto che sta lentamente ribollendo in superficie allora, è che esistono casi d'uso per get () e isPresent (). Sono lì per farti modificare il codice esistente meccanicamente per usare i nuovi tipi senza pensarci troppo. Quello che stai facendo è quindi fuorviato nei seguenti modi:

  • Stai chiamando un metodo che potrebbe restituire null. L'idea corretta sarebbe che il metodo restituisce null.
  • Stai utilizzando i metodi legacy bandaid per gestire questa opzione, invece di utilizzare i nuovi e gustosi metodi che contengono lambda fanciness.

Se vuoi usare Opzionale come semplice controllo di sicurezza nulla, ciò che avresti dovuto fare è semplicemente questo:

new Optional.ofNullable(employeeServive.getEmployee())
    .map(Employee::getId)
    .ifPresent(System.out::println);

Naturalmente, la bella versione di questo aspetto è:

employeeService.getEmployee()
    .map(Employee::getId)
    .ifPresent(System.out::println);

A proposito, anche se non è assolutamente necessario, raccomando di usare una nuova riga per operazione, in modo che sia più facile da leggere. Facile da leggere e capire la concisione di battiti in qualsiasi giorno della settimana.

Questo è ovviamente un esempio molto semplice in cui è facile capire tutto ciò che stiamo cercando di fare. Non è sempre così semplice nella vita reale. Ma notate come in questo esempio, ciò che stiamo esprimendo sono le nostre intenzioni. Vogliamo ottenere il dipendente, ottenere il suo ID e, se possibile, stamparlo. Questa è la seconda grande vittoria con Opzionale. Ci consente di creare un codice più chiaro. Penso anche che fare cose come fare un metodo che faccia un sacco di cose in modo che tu possa darle da mangiare a una mappa è in generale una buona idea.

    
risposta data 19.01.2018 - 14:49
fonte
0

Non vedo benefici in Style 2. Hai ancora bisogno di capire che hai bisogno del controllo nulla e ora è ancora più grande, quindi meno leggibile.

Lo stile migliore sarebbe, se employeeServive.getEmployee () restituisse Optional e quindi il codice diventerebbe:

Optional<Employee> employeeOptional = employeeServive.getEmployee();
if(employeeOptional.isPresent()){
    Employee employee= employeeOptional.get();
    System.out.println(employee.getId());
}

In questo modo non puoi callemployeeServive.getEmployee (). getId () e noterai che dovresti gestire in qualche modo il valoue opzionale. E se nel tuo intero codicebase i metodi non restituiscono mai null (o quasi mai con ben note eccezioni del tuo team a questa regola), aumenteranno la sicurezza contro NPE.

    
risposta data 18.01.2018 - 15:46
fonte
0

Penso che il più grande vantaggio sia che se la tua firma del metodo restituisce un Employee tu non controlli nullo. Sapete da quella firma che è garantito restituire un dipendente. Non è necessario gestire un caso di fallimento. Con un optional sai che lo fai.

Nel codice ho visto che ci sono controlli nulli che non falliranno mai dal momento che le persone non vogliono tracciare il codice per determinare se un nulla è possibile, quindi lanciano controlli nulli ovunque in modo difensivo. Questo rende il codice un po 'più lento, ma soprattutto rende il codice molto più rumoroso.

Tuttavia, affinché funzioni, è necessario applicare questo modello in modo coerente.

    
risposta data 18.01.2018 - 22:50
fonte
0

La mia risposta è: Semplicemente no. Almeno pensaci due volte se è davvero un miglioramento.

Un'espressione (presa da un'altra risposta) come

Optional.of(employeeService)                 // definitely have the service
        .map(EmployeeService::getEmployee)   // getEmployee() might return null
        .map(Employee::getId)                // get ID from employee if there is one
        .ifPresent(System.out::println);     // and if there is an ID, print it

sembra il modo corretto di trattare i metodi di ritorno originariamente annullabili. Sembra bello, non getta mai e non ti fa mai pensare di gestire il caso "assente".

Sembra migliore del vecchio stile

Employee employee = employeeService.getEmployee();
if (employee != null) {
    ID id = employee.getId();
    if (id != null) {
        System.out.println(id);
    }
}

che rende ovvio che potrebbero esserci clausole else .

Lo stile funzionale

  • sembra completamente diverso (ed è illeggibile per le persone non abituate)
  • è un problema di debug
  • non può essere facilmente esteso per gestire il caso "assente" ( orElse non è sempre sufficiente)
  • induce in errore a ignorare il caso "assente"

In nessun caso dovrebbe essere usato come panacea. Se fosse applicabile universalmente, allora la semantica dell'operatore . dovrebbe essere sostituita dalla semantica di ?. operator , l'NPE dimenticato e tutti i problemi ignorati.

Pur essendo un grande fan di Java, devo dire che lo stile funzionale è solo un povero sostituto di Java per l'affermazione

employeeService
.getEmployee()
?.getId()
?.apply(id => System.out.println(id));

ben noto da altre lingue. Siamo d'accordo che questa affermazione

  • non è (davvero) funzionale?
  • è molto simile al codice vecchio stile, molto più semplice?
  • è molto più leggibile rispetto allo stile funzionale?
  • è adatto al debugger?
risposta data 24.01.2018 - 00:17
fonte
0

Opzionale è un concetto (un tipo di ordine superiore) proveniente dalla programmazione funzionale. Usarlo elimina il controllo nidificato annidato e ti salva dalla piramide del destino.

    
risposta data 24.01.2018 - 10:33
fonte

Leggi altre domande sui tag