Perché Optional.get () senza chiamare isPresent () è cattivo, ma non iterator.next ()?


23

Quando si utilizzano le nuove API Java8 stream per trovare un elemento specifico in una raccolta, scrivo codice in questo modo:

    String theFirstString = myCollection.stream()
            .findFirst()
            .get();

Qui IntelliJ avverte che get () viene chiamato senza prima controllare isPresent ().

Tuttavia, questo codice:

    String theFirstString = myCollection.iterator().next();

... non dà alcun avvertimento.

C'è qualche profonda differenza tra le due tecniche, che rende l'approccio di streaming più "pericoloso" in qualche modo, che è fondamentale non chiamare mai get () senza prima chiamare isPresent ()? Cercando su Internet, trovo articoli che parlano di come i programmatori sono incuranti con Optional <> e presumo che possano chiamare get () ogni volta.

Il fatto è che mi piace ricevere un'eccezione il più vicino possibile al luogo in cui il mio codice è difettoso, che in questo caso sarebbe dove chiamo get () quando non è stato trovato alcun elemento. Non voglio scrivere saggi inutili come:

    Optional<String> firstStream = myCollection.stream()
            .findFirst();
    if (!firstStream.isPresent()) {
        throw new IllegalStateException("There is no first string even though I totally expected there to be! <sarcasm>This exception is much more useful than NoSuchElementException</sarcasm>");
    }
    String theFirstString = firstStream.get();

... a meno che non ci sia qualche pericolo nel provocare eccezioni da get () di cui non sono a conoscenza?


Dopo aver letto la risposta di Karl Bielefeldt e un commento di Hulk, mi sono reso conto che il mio codice di eccezione sopra era un po 'goffo, ecco qualcosa di meglio:

    String errorString = "There is no first string even though I totally expected there to be! <sarcasm>This exception is much more useful than NoSuchElementException</sarcasm>";
    String firstString = myCollection.stream()
            .findFirst()
            .orElseThrow(() -> new IllegalStateException(errorString));

Sembra più utile e probabilmente diventerà naturale in molti luoghi. Mi sento ancora come se volessi essere in grado di scegliere un elemento da un elenco senza dover affrontare tutto questo, ma potrebbe essere solo che mi devo abituare a questi tipi di costrutti.


7
La parte relativa alle eccezioni del tuo ultimo esempio potrebbe essere scritta in modo molto più conciso se usi orElseThrow e sarebbe chiaro a tutti i lettori che intendi lanciare l'eccezione.
Hulk,

Risposte:


12

Prima di tutto, considera che il controllo è complesso e probabilmente relativamente tempo da fare. Richiede alcune analisi statiche e potrebbe esserci un bel po 'di codice tra le due chiamate.

In secondo luogo, l'uso idiomatico dei due costrutti è molto diverso. Programma a tempo pieno in Scala, che usa Optionsmolto più frequentemente di quanto non faccia Java. Non riesco a ricordare una singola istanza in cui dovevo usare a .get()su un Option. Dovresti quasi sempre restituire il Optionaldalla tua funzione, o utilizzare orElse()invece. Questi sono modi molto superiori per gestire i valori mancanti rispetto al generare eccezioni.

Poiché .get()raramente dovrebbe essere chiamato durante il normale utilizzo, ha senso che un IDE avvii un controllo complesso quando vede un .get()per vedere se è stato usato correttamente. Al contrario, iterator.next()è l'uso corretto e onnipresente di un iteratore.

In altre parole, non è una questione di essere cattivi e l'altro no, è una questione di essere un caso comune che è fondamentale per il suo funzionamento ed è altamente probabile che generi un'eccezione durante i test degli sviluppatori, e l'altro è usato raramente caso che non può essere esercitato abbastanza da generare un'eccezione durante i test degli sviluppatori. Questi sono i tipi di casi in cui vuoi che gli IDE ti avvertano.


Potrei aver bisogno di saperne di più su questo business opzionale - ho visto principalmente roba java8 come un bel modo per fare la mappatura di elenchi di oggetti che facciamo molto nella nostra webapp. Quindi dovresti inviare gli <> opzionali ovunque? Ci vorrà un po 'per abituarsi, ma questo è un altro post SE! :)
Frank's TV,

Frank di @ TV È meno che si supponga che siano ovunque, e più che ti permettono di scrivere funzioni che trasformano un valore del tipo contenuto e li incateni con mapo flatMap. OptionalPer lo più sono un ottimo modo per evitare di dover affrontare tutto con i nullcontrolli.
Morgen,

15

La classe opzionale deve essere utilizzata quando non è noto se l'elemento presente sia presente o meno. Esiste un avviso per informare i programmatori che esiste un ulteriore percorso di codice che potrebbero essere in grado di gestire con grazia. L'idea è di evitare che vengano sollevate eccezioni. Il caso d'uso che presenti non è quello per cui è stato progettato, e quindi l'avvertimento è irrilevante per te - se vuoi davvero lanciare un'eccezione, vai avanti e disabilitalo (anche se solo per questa linea, non a livello globale - dovresti essere prendere la decisione se gestire o meno il caso non presente su un'istanza per istanza e l'avvertimento ti ricorderà di farlo.

Probabilmente, un avviso simile dovrebbe essere generato per gli iteratori. Tuttavia, semplicemente non è mai stato fatto e aggiungerne uno ora probabilmente farebbe sì che troppi avvertimenti nei progetti esistenti siano una buona idea.

Si noti inoltre che generare la propria eccezione può essere più utile di una semplice eccezione predefinita, poiché includere una descrizione dell'elemento che si aspettava esistesse ma non può essere molto utile nel debug in un secondo momento.


6

Concordo sul fatto che non vi è alcuna differenza intrinseca in questi, tuttavia, vorrei sottolineare un difetto più grande.

In entrambi i casi, hai un tipo, che fornisce un metodo che non è garantito per funzionare e che dovresti chiamare un altro metodo prima di quello. Se il tuo IDE / compilatore / etc ti avverte di un caso o dell'altro dipende solo se supporta questo caso e / o se lo hai configurato per farlo.

Detto questo, entrambi i pezzi di codice sono odori di codice. Queste espressioni suggeriscono i difetti di progettazione sottostanti e producono un codice fragile.

Ovviamente hai ragione a voler fallire velocemente, ma la soluzione, almeno per me, non può essere semplicemente scrollarla di dosso e lanciare le mani in aria (o un'eccezione sulla pila).

Per il momento la tua raccolta potrebbe non essere vuota, ma tutto ciò di cui puoi veramente fare affidamento è il suo tipo: Collection<T>- e questo non ti garantisce la non vacuità. Anche se ora sai che non è vuoto, un requisito modificato può portare alla modifica anticipata del codice e all'improvviso il tuo codice viene colpito da una raccolta vuota.

Ora sei libero di respingere e dire agli altri che avresti dovuto ottenere un elenco non vuoto. Puoi essere felice di trovare quell'errore rapidamente. Tuttavia, hai un software rotto e devi cambiare il tuo codice.

Contrastalo con un approccio più pragmatico: poiché ottieni una raccolta e può essere vuota, ti obbligi a gestire quel caso usando l'opzione # mappa, #orElse o qualsiasi altro di questi metodi, ad eccezione di #get.

Il compilatore fa quanto più lavoro possibile per te in modo tale da poter fare affidamento il più possibile sul contratto di tipo statico di ottenere un Collection<T>. Quando il tuo codice non viola mai questo contratto (ad esempio tramite una di queste due catene di chiamate puzzolenti), il tuo codice è molto meno fragile rispetto alle mutevoli condizioni ambientali.

Nello scenario precedente, una modifica che produce una raccolta di input vuota non avrebbe alcuna conseguenza per il codice. È solo un altro input valido e quindi ancora gestito correttamente.

Il costo di questo è che devi investire tempo in progetti migliori, al contrario di a) rischiare un codice fragile o b) complicare inutilmente le cose generando eccezioni.

Un'ultima nota: poiché non tutti gli IDE / compilatori avvertono delle chiamate iterator (). Next () o optional.get () senza i controlli precedenti, tale codice viene generalmente contrassegnato nelle recensioni. Per quello che vale, non permetterei a un codice del genere di passare una recensione e richiedere invece una soluzione pulita.


Immagino di poter essere un po 'd'accordo sull'odore del codice - ho fatto l'esempio sopra per fare un breve post. Un altro caso più valido potrebbe essere quello di estrarre un elemento obbligatorio da un elenco, che non è una cosa strana apparire in un'applicazione IMO.
TV Frank

Spot on. "Scegli l'elemento con la proprietà p dall'elenco" -> list.stream (). Filter (p) .findFirst () .. e ancora una volta siamo costretti a porre la domanda "cosa succede se non è lì?" .. pane quotidiano e burro. Se è mandatoray e "solo lì" - perché l'hai nascosto in un sacco di altre cose in primo luogo?
Frank,

4
Perché forse è un elenco che viene caricato da un database. Forse l'elenco è una raccolta statistica che è stata raccolta in qualche altra funzione: sappiamo di avere oggetti A, B e C, voglio trovare la quantità di qualunque cosa per B. Molti casi validi (e nella mia applicazione, comuni).
TV Frank

Non conosco il tuo database, ma il mio non garantisce almeno un risultato per query. Lo stesso vale per le raccolte statistiche: chi può dire che saranno sempre non vuote? Concordo sul fatto che sia semplice ragionare sul fatto che ci dovrebbe essere questo o quell'elemento, eppure preferisco una corretta digitazione statica rispetto alla documentazione o, peggio, alcune intuizioni di una discussione.
Frank,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.