Soluzione true-way in Java: analizza 2 numeri da 2 stringhe e poi restituisci la loro somma


84

Una domanda piuttosto stupida. Dato il codice:

public static int sum(String a, String b) /* throws? WHAT? */ {
  int x = Integer.parseInt(a); // throws NumberFormatException
  int y = Integer.parseInt(b); // throws NumberFormatException
  return x + y;
}

Potresti dire se è un buon Java o no? Quello di cui sto parlando è NumberFormatExceptionun'eccezione incontrollata. Non è necessario specificarlo come parte della sum()firma. Inoltre, per quanto ho capito, l'idea di eccezioni non controllate è solo per segnalare che l'implementazione del programma non è corretta, e ancor di più, catturare eccezioni non controllate è una cattiva idea, poiché è come riparare un programma difettoso in fase di esecuzione .

Qualcuno potrebbe chiarire se:

  1. Dovrei specificare NumberFormatExceptioncome parte della firma del metodo.
  2. Dovrei definire la mia eccezione controllata ( BadDataException), gestire NumberFormatExceptionall'interno del metodo e rilanciarla come BadDataException.
  3. Dovrei definire la mia eccezione controllata ( BadDataException), convalidare entrambe le stringhe in qualche modo come espressioni regolari e lanciare la mia BadDataExceptionse non corrisponde.
  4. La tua idea?

Aggiornamento :

Immagina, non è un framework open source, che dovresti usare per qualche motivo. Guardi la firma del metodo e pensi: "OK, non genera mai". Poi, un giorno, hai un'eccezione. È normale?

Aggiornamento 2 :

Ci sono alcuni commenti che dicono che il mio sum(String, String)è un cattivo design. Sono assolutamente d'accordo, ma per coloro che credono che il problema originale non sarebbe mai apparso se avessimo un buon design, ecco una domanda in più:

La definizione del problema è questa: hai un'origine dati in cui i numeri sono memorizzati come Strings. Questa fonte può essere un file XML, una pagina web, una finestra del desktop con 2 caselle di modifica, qualunque sia.

Il tuo obiettivo è implementare la logica che prende questi 2 Strings, li converte in se intvisualizza la finestra di messaggio che dice "la somma è xxx".

Indipendentemente dall'approccio che utilizzi per progettarlo / implementarlo, avrai questi 2 punti di funzionalità interna :

  1. Un luogo dove si converte Stringaint
  2. Un luogo in cui aggiungi 2 ints

La domanda principale del mio post originale è:

Integer.parseInt()si aspetta che venga passata la stringa corretta . Ogni volta che passi una stringa errata , significa che il tuo programma non è corretto (non "il tuo utente è un idiota"). Devi implementare il pezzo di codice in cui da un lato hai Integer.parseInt () con la semantica MUST e dall'altro devi essere a posto con i casi in cui l'input non è corretto - semantica DOVREBBE .

Quindi, brevemente: come implementare la semantica DOVREBBE se ho solo le librerie MUST .


13
Non è affatto una domanda stupida. In realtà è abbastanza buono! Non sono completamente sicuro di quale opzione sceglierei.
martin

1
Secondo il tuo aggiornamento: Sì, secondo il principio del fail-fast anche questo non sarebbe necessariamente un male.
Johan Sjöberg

1
Sono d'accordo con Martin. È uno degli aspetti della programmazione Java meno compreso che richiede alcune best practice. Questo può essere visto dalla mancanza di pagine sull'argomento con pochissime oltre a O'Reilly che dice "questo è come dovrebbe essere fatto".
James P.

2
In realtà, questa è un'ottima domanda.
Chris Cudmore

1
Ottima domanda! Ha anche sollevato alcune ottime risposte: D
Kheldar

Risposte:


35

Questa è una buona domanda. Vorrei che più persone pensassero a queste cose.

IMHO, lanciare eccezioni non controllate è accettabile se ti sono stati passati parametri di spazzatura.

In generale, non dovresti lanciare BadDataExceptionperché non dovresti usare le eccezioni per controllare il flusso del programma. Le eccezioni sono per l'eccezionale. I chiamanti al tuo metodo possono sapere prima di chiamarlo se le loro stringhe sono numeri o meno, quindi il passaggio di spazzatura è evitabile e quindi può essere considerato un errore di programmazione , il che significa che è OK lanciare eccezioni non controllate.

Per quanto riguarda la dichiarazione throws NumberFormatException, questo non è così utile, perché pochi se ne accorgeranno perché NumberFormatException è deselezionato. Tuttavia, gli IDE possono utilizzarlo e offrire di avvolgere try/catchcorrettamente. Una buona opzione è usare anche javadoc, ad esempio:

/**
 * Adds two string numbers
 * @param a
 * @param b
 * @return
 * @throws NumberFormatException if either of a or b is not an integer
 */
public static int sum(String a, String b) throws NumberFormatException {
    int x = Integer.parseInt(a); 
    int y = Integer.parseInt(b); 
    return x + y;
}

MODIFICATO :
I commentatori hanno espresso punti validi. È necessario considerare come verrà utilizzato e il design generale della tua app.

Se il metodo verrà utilizzato ovunque ed è importante che tutti i chiamanti gestiscano i problemi, dichiarare che il metodo genera un'eccezione controllata (costringendo i chiamanti a gestire i problemi), ma ingombra il codice di try/catchblocchi.

Se d'altra parte stiamo usando questo metodo con dati di cui ci fidiamo, dichiaralo come sopra, perché non ci si aspetta che esploda mai e tu eviti l'ingombro di codice di try/catchblocchi essenzialmente non necessari .


6
Il problema è che le eccezioni non controllate vengono spesso ignorate e possono filtrare il tuo stack di chiamate, causando il caos in parti della tua app che non hanno idea di cosa ci sia sotto. La domanda è davvero quale bit di codice dovrebbe controllare se l'input è valido o meno.
G_H

1
@G_H ha ragione. I tutorial Java dicono questo: "Se ci si può ragionevolmente aspettare che un client si riprenda da un'eccezione, rendila un'eccezione selezionata. Se un client non può fare nulla per recuperare dall'eccezione, rendila un'eccezione non controllata". La domanda è dove dovrebbe RuntimeExceptionessere gestito un? Dovrebbe essere il chiamante o l'eccezione dovrebbe essere lasciata fluttuare? In caso affermativo come dovrebbe essere gestito?
James P.

1
Per rispondere parzialmente a questo ci sono due approcci che ho visto: il primo è quello di intercettare Exceptionsdai livelli inferiori e racchiuderli in eccezioni di livello superiore che sono più significative per l'utente finale e il secondo è usare un gestore di eccezioni non rilevato che può essere inizializzato in il programma di avvio dell'applicazione.
James P.

2
IMHO che ha NumberFormatException nel javaDoc è molto più importante. Metterlo nella throwsclausola è una bella aggiunta, ma non obbligatoria.
Dorus

3
A "I chiamanti del tuo metodo possono sapere prima di chiamarlo se le loro stringhe sono numeri o meno, quindi è possibile evitare di passare spazzatura" : Non è proprio vero. L'unico modo semplice per verificare che una stringa venga analizzata come int è provarla. Sebbene tu possa voler eseguire alcuni controlli anticipati, il controllo esatto è piuttosto un PITA.
maaartinus

49

A mio parere sarebbe preferibile gestire la logica delle eccezioni il più in alto possibile. Quindi preferirei la firma

 public static int sum(int a, int b);

Con la tua firma del metodo non cambierei nulla. O lo sei

  • A livello di codice utilizzando valori errati, dove invece potresti convalidare l'algoritmo del produttore
  • o inviando valori, ad esempio, dall'input dell'utente, nel qual caso quel modulo dovrebbe eseguire la convalida

Quindi, la gestione delle eccezioni in questo caso diventa un problema di documentazione .


4
Mi piace questo. Costringe l'utente a seguire il contratto e fa in modo che il metodo faccia solo una cosa.
Chris Cudmore

Non capisco come l'utilizzo di questo metodo sia diverso da quello dell'utente che fa a + b. Perché abbiamo bisogno di questo metodo?
Swati

4
@ Swati: Penso che sia un esempio , che è un'ottima tecnica per mostrare le cose semplicemente. Il metodo sum potrebbe anche essere myVeryComplicatedMethodWhichComputesPhoneNumberOfMyIdealMatchGirl (int myID, int myLifeExpectancy) ...
Kheldar

3
Totalmente d'accordo qui. una sumfunzione che si aspetta due numeri, dovrebbe ottenere due numeri. Il modo in cui li hai ottenuti non è correlato alla sumfunzione. Separare correttamente la logica . Quindi, lo collegherei a un problema di progettazione.
c00kiemon5ter

5

Numero 4. Come dato, questo metodo non dovrebbe prendere stringhe come parametri, dovrebbe prendere numeri interi. In tal caso (dato che java esegue il wrapping invece di overflow) non c'è possibilità di eccezione.

 x = sum(Integer.parseInt(a), Integer.parseInt(b))

è molto più chiaro su cosa si intende di x = sum(a, b)

Si desidera che l'eccezione avvenga il più vicino possibile alla sorgente (ingresso).

Per quanto riguarda le opzioni 1-3, non definisci un'eccezione perché ti aspetti che i chiamanti presumano che altrimenti il ​​tuo codice non può fallire, definisci un'eccezione per definire cosa accade in condizioni di errore note CHE SONO UNICHE AL TUO METODO. Ad esempio, se hai un metodo che è un wrapper attorno a un altro oggetto e genera un'eccezione, passala. Solo se l'eccezione è unica per il tuo metodo dovresti lanciare un'eccezione personalizzata (frex, nel tuo esempio, se la somma doveva restituire solo risultati positivi, quindi controllarlo e lanciare un'eccezione sarebbe appropriato, se d'altra parte java ha lanciato un'eccezione di overflow invece di wrapping, quindi lo si trasmette, non lo si definisce nella firma, lo si rinominerà o lo si mangerà).

Aggiornamento in risposta all'aggiornamento della domanda:

Quindi, in breve: come implementare la semantica DOVREBBE se ho solo le librerie MUST.

La soluzione a questo è racchiudere la libreria MUST e restituire un valore DOVREBBE. In questo caso, una funzione che restituisce un numero intero. Scrivi una funzione che prenda una stringa e restituisca un oggetto Integer: o funziona o restituisce null (come Ints.tryParse di guava). Fai la tua convalida separatamente dalla tua operazione, la tua operazione dovrebbe richiedere int. Se la tua operazione viene chiamata con valori predefiniti quando hai un input non valido, o fai qualcos'altro, dipenderà dalle tue specifiche - la maggior parte che posso dire a riguardo, è che è davvero improbabile che il luogo per prendere quella decisione sia nell'operazione metodo.


Qual è la tua idea qui? Avrò lo stesso problema anche se ho sum(int, int)e parseIntFromString(String). Per ottenere la stessa funzionalità, in entrambi i casi, dovrò scrivere un pezzo di codice in cui ci sono 2 chiamate a parseIntFromString()e 1 chiamata a sum(). Considera questo caso, se ha senso per te: non vedo alcuna differenza dal punto di vista del problema originale.
Andrey Agibalov

@ loki2302: il punto è che il chiamante poi decide quale comportamento è appropriato quando non è un numero. Inoltre, se non eseguono il controllo degli errori, l'errore si verifica un passo più vicino a dove è assegnato il valore: per scopi di debug, più vicino all'assegnazione, più facile sarà il debug. Ed è più probabile che ti manchi la scrittura dello unit test appropriato per il chiamante se stai facendo TDD. Fondamentalmente non vuoi avere una stringa fornita nel metodo x che dovrebbe essere un numero e quindi 5 classi e 20 chiamate al metodo lanciano un'eccezione quando provi a trattarlo come un numero.
jmoreno

Non lo capisco. Stai cercando di dire che fornire un'interfaccia di int sum(String, String)non è mai possibile nella realtà?
Andrey Agibalov

1
@ loki2302: Dato il codice mostrato, sarebbe possibile ma una cattiva idea. Se il numero nella stringa potrebbe essere una parola in cui "2", "due", "dos", "deux", "zwo" dovessero essere trattati allo stesso modo, allora quella firma sarebbe appropriata. Ma così com'è, al metodo vengono date due stringhe e le tratta come numeri interi. Perché mai avresti voluto farlo? È una cattiva idea.
jmoreno

2
@ loki2302: hai accettato la risposta dicendo che era consentito lanciare un'eccezione non controllata quando si passava spazzatura, ma lo scopo di un linguaggio fortemente digitato è IMPEDIRE che la spazzatura venga trasmessa. Avere un metodo che si aspetta che una stringa sia sempre un valore intero è solo chiedere problemi. Anche Integer.parseInt non si occupa di questo bene (un'eccezione su ciò che DOVREBBE essere previsto input è negativo, il metodo integer.TryParse di .Net è molto meglio, sebbene la mancanza di un parametro out da parte di Java lo renda alquanto impraticabile).
jmoreno

3

1. Dovrei specificare NumberFormatException come parte della firma del metodo.

Credo di si. È una bella documentazione.

2. Dovrei definire la mia eccezione controllata (BadDataException), gestire NumberFormatException all'interno del metodo e lanciarla nuovamente come BadDataException.

A Volte si. Le eccezioni selezionate sono considerate migliori in alcuni casi, ma lavorare con esse è piuttosto PITA. Ecco perché molti framework (ad esempio, Hibernate) utilizzano solo eccezioni di runtime.

3. Dovrei definire la mia eccezione controllata (BadDataException), convalidare entrambe le stringhe in qualche modo come le espressioni regolari e lanciare la mia BadDataException se non corrisponde.

Mai. Più lavoro, meno velocità (a meno che non ti aspetti che l'eccezione sia una regola) e nessun guadagno.

4. La tua idea?

Proprio nessuno.


2

Nr 4.

Penso che non cambierei affatto il metodo. Vorrei mettere un tentativo di cattura intorno al metodo di chiamata o superiore nello stack-trace in cui sono in un contesto in cui posso recuperare con grazia con la logica aziendale dall'eccezione.

Non avrei la certezza di fare il n. 3 perché lo ritengo eccessivo.


2

Supponendo che ciò che stai scrivendo verrà consumato (come un'API) da qualcun altro, allora dovresti andare con 1, NumberFormatException è specificamente allo scopo di comunicare tali eccezioni e dovrebbe essere usato.


2
  1. Per prima cosa devi chiederti se l'utente del mio metodo deve preoccuparsi di inserire dati errati o ci si aspetta che inserisca dati corretti (in questo caso String). Questa aspettativa è anche conosciuta come design by contract .

  2. e 3. Sì, probabilmente dovresti definire BadDataException o ancora meglio usare alcune di quelle asportate come NumberFormatException ma piuttosto lasciare il messaggio standard da mostrare. Cattura NumberFormatException nel metodo e rilanciala con il tuo messaggio, senza dimenticare di includere la traccia dello stack originale.

  3. Dipende dalla situazione ma probabilmente andrei a rilanciare NumberFormatException con alcune informazioni aggiuntive. E inoltre deve esserci una spiegazione javadoc di quali sono i valori attesiString a, String b


1

Dipende molto dallo scenario in cui ti trovi.

Caso 1. Sei sempre tu a eseguire il debug del codice e nessun altro e l'eccezione non causerà una cattiva esperienza utente

Lancia il NumberFormatException predefinito

Caso 2: il codice dovrebbe essere estremamente gestibile e comprensibile

Definisci la tua eccezione e aggiungi molti più dati per il debug durante il lancio.

Non hai bisogno di controlli regex perché andrà comunque in eccezione per un input errato.

Se fosse un codice a livello di produzione, la mia idea sarebbe quella di definire più di un'eccezione personalizzata, come

  1. Eccezione nel formato del numero
  2. Eccezione di overflow
  3. Eccezione nulla ecc ...

e trattare tutti questi separatamente


1
  1. Puoi farlo per chiarire che ciò può accadere per un input errato. Potrebbe aiutare qualcuno che usa il tuo codice a ricordare di aver gestito questa situazione. Più specificamente, stai chiarendo che non lo gestisci da solo nel codice o restituisci invece un valore specifico. Ovviamente, JavaDoc dovrebbe chiarire anche questo.
  2. Solo se si desidera forzare il chiamante a gestire un'eccezione verificata.
  3. Sembra eccessivo. Affidati all'analisi per rilevare input errati.

In generale, un'eccezione NumberFormaException è deselezionata perché è previsto che venga fornito un input analizzabile correttamente. La convalida dell'input è qualcosa che dovresti gestire. Tuttavia, in realtà l'analisi dell'input è il modo più semplice per farlo. Puoi semplicemente lasciare il tuo metodo così com'è e avvertire nella documentazione che è previsto un input corretto e chiunque chiami la tua funzione dovrebbe convalidare entrambi gli input prima di usarlo.


1

Qualsiasi comportamento eccezionale dovrebbe essere chiarito nella documentazione. O dovrebbe affermare che questo metodo restituisce un valore speciale in caso di errore (comenull , cambiando il tipo di ritorno in Integer) o dovrebbe essere usato il caso 1. Averlo esplicito nella firma del metodo consente all'utente di ignorarlo se garantisce stringhe corrette con altri mezzi, ma è comunque ovvio che il metodo non gestisce questo tipo di errore da solo.


1

Rispondi alla tua domanda aggiornata.

Sì, è perfettamente normale ricevere eccezioni "a sorpresa". Pensa a tutti gli errori di runtime che si ottengono quando si è nuovi alla programmazione.

e.g ArrayIndexOutofBound

Anche una comune eccezione a sorpresa dal ciclo for each.

ConcurrentModificationException or something like that

1

Anche se sono d'accordo con la risposta secondo cui dovrebbe essere consentito filtrare l'eccezione di runtime, dal punto di vista del design e dell'usabilità, sarebbe una buona idea inserirla in un'eccezione IllegalArgumentException piuttosto che lanciarla come NumberFormatException. Questo quindi rende più chiaro il contratto del tuo metodo in base al quale dichiara che gli è stato passato un argomento illegale a causa del quale ha lanciato un'eccezione.

Per quanto riguarda l'aggiornamento alla domanda "Immagina, non è un framework open source, che dovresti usare per qualche motivo. Guardi la firma del metodo e pensi:" OK, non genera mai ". Poi, un giorno, hai avuto un'eccezione . È normale?" il javadoc del tuo metodo dovrebbe sempre riversare il comportamento del tuo metodo (vincoli pre e post). Pensa sulle linee di say collection interfaces dove in se un null non è consentito, javadoc dice che verrà lanciata un'eccezione di puntatore null sebbene non faccia mai parte della firma del metodo.


2
NumberFormatException è una sottoclasse di IllegalArgumentException, quindi questo wrapping non aggiunge informazioni.
Don Roby

cosa significa che l'eccezione può essere percolata così com'è (poiché in questo caso nfe è già un'eccezione di argomento illegale) e può esserci una gestione generica per trattare argomenti illegali da qualche parte nella gerarchia delle chiamate. quindi se questo fosse un esempio in cui gli argomenti sono stati passati dall'utente, potrebbe esserci un codice generico che avvolgerebbe tutta la gestione degli argomenti illegali e informerebbe l'utente al riguardo.
Scorpion

1

Dato che stai parlando di buona pratica Java, secondo me è sempre meglio

  • Per gestire l'eccezione non controllata, analizzala e tramite un'eccezione non selezionata personalizzata.

  • Inoltre, mentre lanci un'eccezione personalizzata non selezionata puoi aggiungere il messaggio di eccezione che il tuo client potrebbe comprendere e anche stampare la traccia dello stack dell'eccezione originale

  • Non è necessario dichiarare l'eccezione personalizzata come "genera" in quanto è deselezionata.

  • In questo modo non stai violando l'uso di ciò per cui sono fatte eccezioni non verificate, allo stesso tempo il client del codice capirebbe facilmente il motivo e la soluzione dell'eccezione.

  • Anche documentare adeguatamente in java-doc è una buona pratica e aiuta molto.


1

Penso che dipenda dal tuo scopo, ma lo documenterei come minimo:

/**
 * @return the sum (as an int) of the two strings
 * @throws NumberFormatException if either string can't be converted to an Integer
 */
public static int sum(String a, String b)
  int x = Integer.parseInt(a);
  int y = Integer.parseInt(b);
  return x + y;
}

Oppure prendi una pagina dal codice sorgente Java per la classe java.lang.Integer:

public static int parseInt(java.lang.String string) throws java.lang.NumberFormatException;

1

Che ne dici del modello di convalida dell'input implementato dalla libreria "Guava" di Google o dalla libreria "Validator" di Apache ( confronto )?

Nella mia esperienza, è considerata buona pratica convalidare i parametri di una funzione all'inizio della funzione e lanciare eccezioni dove appropriato.

Inoltre, considererei questa domanda in gran parte indipendente dalla lingua. La "buona pratica" qui si applicherebbe a tutti i linguaggi che hanno funzioni che possono assumere parametri che possono essere o non essere validi.


1

Penso che la tua prima frase di "Una domanda abbastanza stupida" sia molto rilevante. Perché mai scrivere un metodo con quella firma in primo luogo? Ha senso anche sommare due stringhe? Se il metodo chiamante vuole sommare due stringhe, è responsabilità del metodo chiamante assicurarsi che siano int validi e convertirli prima di chiamare il metodo.

In questo esempio, se il metodo chiamante non può convertire le due stringhe in un int, potrebbe fare diverse cose. Dipende davvero a quale livello si verifica questa somma. Suppongo che la conversione della stringa sarebbe molto vicina al codice front-end (se è stata eseguita correttamente), in questo caso 1. sarebbe il più probabile:

  1. Imposta un messaggio di errore e interrompi l'elaborazione o reindirizza a una pagina di errore
  2. Restituire falso (ovvero, metterebbe la somma in qualche altro oggetto e non sarebbe richiesto di restituirlo)
  3. Lancia un po 'di BadDataException come suggerisci, ma a meno che la somma di questi due numeri non sia molto importante, questo è eccessivo e, come accennato in precedenza, questo è probabilmente un cattivo design poiché implica che la conversione viene eseguita nel posto sbagliato

1

Ci sono molte risposte interessanti a questa domanda. Ma voglio ancora aggiungere questo:

Per l'analisi delle stringhe, preferisco sempre utilizzare "espressioni regolari". Il pacchetto java.util.regex è lì per aiutarci. Quindi finirò con qualcosa del genere, che non genera mai alcuna eccezione. Spetta a me restituire un valore speciale se voglio rilevare qualche errore:

import java.util.regex.Pattern;
import java.util.regex.Matcher;

public static int sum(String a, String b) {
  final String REGEX = "\\d"; // a single digit
  Pattern pattern = Pattern.compile(REGEX);
  Matcher matcher = pattern.matcher(a);
  if (matcher.find()) { x = Integer.matcher.group(); }
  Matcher matcher = pattern.matcher(b);
  if (matcher.find()) { y = Integer.matcher.group(); }
  return x + y;
}

Come si può vedere, il codice è solo un po 'più lungo, ma possiamo gestire ciò che vogliamo (e impostare i valori di default per x e y, controllare cosa succede con le clausole else, ecc ...) Potremmo anche scrivere una trasformazione più generale routine, a cui possiamo passare stringhe, valori di ritorno predefiniti, codice REGEX da compilare, messaggi di errore da lanciare, ...

Spero sia stato utile.

Avviso: non sono stato in grado di testare questo codice, quindi scusa eventuali problemi di sintassi.


1
stiamo parlando di Java? Che cos'è Integer.matcher? privatevariabile all'interno del metodo? Manca ()per se e un sacco di dispersi ;, x, ynon dichiarate, matcherha dichiarato due volte, ...
user85421

Infatti Carlos, l'ho fatto in fretta e, come ho detto, non ho potuto provarlo subito. Scusate. Volevo mostrare il modo di fare regex.
Louis

OK, ma questo non risolve il problema con NumberFormatException - la domanda principale IMO - (supponendo che Integer.matcher.group()debba essere Integer.parseInt(matcher.group()))
user85421

0

Affronti questo problema perché lasci che gli errori dell'utente si propagino troppo in profondità nel nucleo dell'applicazione e in parte anche perché abusi dei tipi di dati Java.

Dovresti avere una separazione più chiara tra la convalida dell'input dell'utente e la logica aziendale, utilizzare una corretta tipizzazione dei dati e questo problema scomparirà da solo.

Il fatto è che la semantica di Integer.parseInt()are known - il suo scopo principale è analizzare interi validi . Ti manca un passaggio esplicito di convalida / analisi dell'input dell'utente.

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.