Справжнє рішення в Java: проаналізуйте 2 числа з 2 рядків, а потім поверніть їх суму


84

Досить дурне запитання. Даний код:

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

Не могли б ви сказати, хороша це Java чи ні? Те, про що я кажу, NumberFormatException- це неперевірений виняток. Вам не потрібно вказувати це як частину sum()підпису. Більше того, наскільки я розумію, ідея неперевірених винятків полягає лише у сигналізації про те, що реалізація програми є неправильною, і навіть більше, ловити неперевірені винятки - погана ідея, оскільки це як виправлення поганої програми під час виконання .

Хтось, будь ласка, пояснить, чи:

  1. Я повинен вказати NumberFormatExceptionяк частину підпису методу.
  2. Мені слід визначити власний перевірений виняток ( BadDataException), обробити NumberFormatExceptionвсередині методу і перекинути його як BadDataException.
  3. Я повинен визначити власний перевірений виняток ( BadDataException), перевірити обидва рядки якимось чином, як регулярні вирази, і викинути мій, BadDataExceptionякщо він не збігається.
  4. Ваша ідея?

Оновлення :

Уявіть, це не фреймворк з відкритим кодом, який вам слід використовувати з якихось причин. Ви дивитесь на підпис методу і думаєте - "Добре, це ніколи не кидає". Потім, одного дня, ви отримали виняток. Це нормально?

Оновлення 2 :

Є деякі коментарі, які говорять, що мій sum(String, String)поганий дизайн. Я абсолютно згоден, але для тих, хто вважає, що оригінальна проблема просто не з’явилася б, якби ми мали гарний дизайн, ось додаткове запитання:

Визначення проблеми таке: у вас є джерело даних, де числа зберігаються як Strings. Це джерело може бути XML-файлом, веб-сторінкою, вікном робочого столу з 2 полями для редагування.

Ваша мета полягає в тому, щоб реалізувати логіку, яка приймає ці 2 Stringс, перетворює їх на ints і відображає вікно повідомлення "Сума xxx".

Незалежно від того, який підхід ви використовуєте для проектування / реалізації цього, ви будете мати ці 2 пункти внутрішньої функціональності :

  1. Місце , де ви перетворити Stringвint
  2. Місце, де ви додаєте 2 intс

Основне питання мого оригінального допису:

Integer.parseInt()очікує, що буде передано правильний рядок. Всякий раз, коли ви передаєте неправильний рядок , це означає, що ваша програма неправильна (а не " ваш користувач ідіот"). Вам потрібно реалізувати шматок коду там, де з одного боку ви маєте Integer.parseInt () із семантикою MUST, а з іншого боку, вам потрібно бути в порядку з випадками, коли введення неправильне - СЛІД семантики .

Отже, коротко: як мені реалізувати СЛІД семантику, якщо у мене є лише ОБОВ’ЯЗКОВІ бібліотеки .


13
Це аж ніяк не дурне питання. Насправді це досить непоганий! Я не зовсім впевнений, який варіант я вибрав би.
Мартін

1
До вашого оновлення: Так, згідно з принципом швидкої роботи, це також не обов'язково буде поганим.
Йоган Шеберг,

1
Я згоден з Мартіном. Це один із аспектів програмування на Java, який є найменш зрозумілим і вимагає кращих практик. Це видно з відсутності сторінок на цю тему, і крім О'Райлі дуже мало, окрім того, що вони кажуть "так треба робити".
Джеймс П.

2
Насправді, це чудове питання.
Chris Cudmore

1
Чудове питання! Також підняв кілька чудових відповідей: D
Kheldar

Відповіді:


35

Це гарне запитання. Я хотів би, щоб більше людей думало про такі речі.

ІМХО, кидання неперевірених винятків є прийнятним, якщо вам передали параметри сміття.

Взагалі кажучи, ви не повинні кидати, BadDataExceptionтому що ви не повинні використовувати винятки для управління потоком програм. Виняток становлять виключні. Викличники вашого методу можуть знати, перш ніж вони його викличуть, чи є їх рядки цифрами чи ні, тому передавання сміття можна уникнути, і тому його можна вважати помилкою програмування , а це означає, що викидати непровірені винятки можна.

Щодо декларування throws NumberFormatException- це не настільки корисно, тому що мало хто помітить через те, що NumberFormatException не позначено. Однак IDE можуть цим скористатися і запропонувати правильно обернутися try/catch. Хороший варіант - використовувати також javadoc, наприклад:

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

ВИДАВАНО : Коментатори
зробили дійсні бали. Вам потрібно врахувати, як це буде використано, та загальний дизайн вашого додатка.

Якщо метод буде використовуватися повсюдно, і важливо, щоб усі абоненти обробляли проблеми, оголосіть метод як викидання перевіреного винятку (змушуючи абонентів вирішувати проблеми), але захаращуючи код try/catchблоками.

Якщо, з іншого боку, ми використовуємо цей метод з даними, яким ми довіряємо, тоді оголосіть його, як зазначено вище, тому що він, як очікується, ніколи не вибухне, і ви уникаєте безладу коду по суті непотрібних try/catchблоків.


6
Проблема полягає в тому, що неперевірені винятки часто ігноруються і можуть спричинити пошкодження стека викликів, спричиняючи хаос у частинах вашого додатку, які навіть не уявляють, що лежить внизу. Питання справді полягає в тому, який біт коду повинен перевірити, чи є введення дійсним чи ні.
G_H

1
@G_H має сенс. У підручниках Java сказано так: "Якщо з розумним розумом можна очікувати, що клієнт відновиться після винятку, зробіть це перевіреним винятком. Якщо клієнт не може нічого відновити з винятку, зробіть це неперевіреним винятком". Питання в тому, де слід RuntimeExceptionобробляти? Чи повинен це бути той, хто телефонує, чи виняток потрібно залишити для спливання? Якщо так, то як з цим слід поводитися?
James P.,

1
Щоб частково відповісти на це, я бачив два підходи: перший - перехоплювати Exceptionsнижчі шари та обертати їх винятками вищого рівня, які є більш значущими для кінцевого користувача, а другим - використовувати незахоплений обробник винятків, який можна ініціалізувати в запуск програми.
James P.,

2
IMHO, який має NumberFormatException у javaDoc, набагато важливіший. Помістити його в throwsпункт - гарне доповнення, але не обов’язкове.
Дорус,

3
Для того, щоб «Викликають на ваш метод може знати , перш ніж вони його називають , якщо їх рядки номера чи ні, тому проходження сміття в перебороти» : Це не зовсім вірно. Єдиний простий спосіб перевірити, чи рядок аналізується як int - це спробувати його. Хоча вам може знадобитися провести попередні перевірки, точна перевірка - це цілком ПДФА.
maaartinus

49

На мій погляд, було б бажано обробляти логіку винятків якомога вище. Тому я віддав би перевагу підпису

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

З вашим підписом методу я б нічого не міняв. Або ти

  • Програмно використовуючи неправильні значення, де замість цього ви можете перевірити свій алгоритм виробника
  • або надсилання значень з, наприклад, вводу користувача, і в цьому випадку модуль повинен виконувати перевірку

Отже, обробка винятків у цьому випадку стає проблемою документації .


4
Мені подобається це. Це змушує користувача виконувати договір і змушує метод робити лише одне.
Chris Cudmore

Я не розумію, як використання цього методу відрізняється від того, як користувач робить a + b. Навіщо нам цей метод?
Swati

4
@ Сваті: Я думаю, що це приклад , який є чудовою технікою, щоб просто показати речі. Методом сум може бути також myVeryComplicatedMethodWhichComputesPhoneNumberOfMyIdealMatchGirl (int myID, int myLifeExpectancy) ...
Kheldar

3
Тут повністю згодні. sumфункція , яка приймає два числа, повинна отримати два числа. Те, як ви їх отримали, не пов’язане з sumфункцією. Правильно відокремте свою логіку . Отже, я б пов’язав це з проблемою дизайну.
c00kiemon5ter

5

Число 4. Як зазначено, цей метод не повинен приймати рядки як параметри, він повинен приймати цілі числа. У цьому випадку (оскільки Java обгортається, а не переповнюється), немає можливості виключення.

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

набагато зрозуміліше, що мається на увазі, ніж x = sum(a, b)

Ви хочете, щоб виняток стався якомога ближче до джерела (введення).

Що стосується варіантів 1-3, ви не визначаєте виняток, оскільки ви очікуєте, що ваші абоненти вважатимуть, що в іншому випадку ваш код не може вийти з ладу, ви визначаєте виняток, щоб визначити, що відбувається за відомих умов відмови, ЯКІ УНІКАЛЬНІ ДЛЯ ВАШОГО МЕТОДУ. Тобто якщо у вас є метод, який є обгорткою навколо іншого об’єкта, і він видає виняток, потім передайте його. Тільки якщо виняток є унікальним для вашого методу, ви повинні кидати спеціальний виняток (frex, у вашому прикладі, якщо сума повинна була повертати лише позитивні результати, тоді перевірка на це та додавання винятку буде доречним, якщо з іншого боку java викинув виняток із переповнення замість обгортання, тоді ви передали б його, а не визначали у своєму підписі, перейменовували або з'їдали)

Оновлення у відповідь на оновлення питання:

Отже, коротко: як мені реалізувати СЛІД семантику, якщо у мене є лише ОБОВ’ЯЗКОВІ бібліотеки.

Рішенням цього є обгортання бібліотеки MUST та повернення значення SHOULD. У цьому випадку функція, яка повертає ціле число. Напишіть функцію, яка приймає рядок і повертає об’єкт Integer - або вона працює, або повертає null (як Ints.tryParse гуави). Якщо ваша перевірка відокремлена від вашої операції, ваша операція повинна приймати інт. Чи буде ваша операція викликатися зі значеннями за замовчуванням, якщо у вас невірний ввід, або ви робите щось інше, буде залежати від ваших специфікацій - найбільше, що я можу сказати про це, це те, що насправді малоймовірно, що місце для прийняття такого рішення є в операції метод.


Яка ваша ідея тут? Я отримаю те саме питання, навіть якщо маю sum(int, int)і parseIntFromString(String). Щоб отримати однакову функціональність, в будь-якому випадку мені доведеться написати фрагмент коду, де є 2 дзвінки parseIntFromString()та 1 дзвінок sum(). Розгляньте цей випадок, якщо для вас це має сенс - я не бачу різниці з точки зору вихідної проблеми.
Андрій Агібалов

@ loki2302: справа в тому, що той, хто телефонує, вирішує, яка поведінка є доречною, якщо це не номер. Крім того, якщо вони не роблять перевірку помилок, то помилка відбувається на крок ближче до місця, де присвоюється значення - для цілей налагодження, чим ближче до призначення, тим простіше налагодження. І ви, швидше за все, пропустите написання відповідного модульного тесту для абонента, якщо ви робите TDD. В основному, ви не хочете, щоб у методі x був вказаний рядок, який повинен бути числом, а потім 5 класів та 20 викликів методів видають виняток, коли ви намагаєтеся обробити це число.
jmoreno

Я не розумію. Ви намагаєтесь сказати, що надання інтерфейсу в int sum(String, String)насправді ніколи не можливо?
Андрій Агібалов

1
@ loki2302: Враховуючи показаний код, це було б можливо, але погана ідея. Якби число у рядку могло бути словом, де "2", "два", "dos", "deux", "zwo" оброблялися однаково, тоді такий підпис був би доречним. Але так, як воно є, методу дано два рядки і трактується як ціле число. Чому ти коли-небудь захочеш це зробити? Це погана ідея.
jmoreno

2
@ loki2302: Ви прийняли відповідь, сказавши, що нормально кидати неперевірений виняток при передачі сміття, але сенс сильно набраної мови полягає в тому, щоб ПОПЕРЕДЖИТИ сміття передаватись. Наявність методу, який очікує, що рядок завжди буде цілим числом, - це просто запитання про проблеми. Навіть Integer.parseInt не справляється настільки добре (виняток із того, що СЛІД очікувати введення, є поганим, ціле число .Net. Метод TryParse набагато кращий, хоча відсутність у Java параметра out робить це дещо непрактичним).
jmoreno

3

1. Я повинен вказати NumberFormatException як частину підпису методу.

Я думаю так. Це гарна документація.

2. Мені слід визначити власний перевірений виняток (BadDataException), обробити NumberFormatException усередині методу і повернути його як BadDataException.

Іноді так. У деяких випадках перевірені винятки вважаються кращими, але робота з ними - це цілком ПІТА. Ось чому багато фреймворків (наприклад, Hibernate) використовують лише винятки під час виконання.

3. Я повинен визначити власний перевірений виняток (BadDataException), перевірити обидва рядки якимось чином, як регулярні вирази, і кинути мій BadDataException, якщо він не збігається.

Ніколи. Більше роботи, менша швидкість (за винятком випадків, коли викидання винятку буде правилом), і ніякого виграшу зовсім.

4. Ваша ідея?

Зовсім ні.


2

№ 4.

Думаю, я б зовсім не міняв метод. Я б поставив спробу ловити навколо методу виклику або вище в стеці-трасування, де я перебуваю в контексті, де я можу витончено відновити бізнес-логіку з винятку.

Я б не впевнено зробив No3, оскільки вважаю це надмірним.


2

Якщо припустити, що те, що ви пишете, буде спожито (наприклад, як API) кимось іншим, тоді вам слід вибрати 1, NumberFormatException призначений спеціально для передачі таких винятків і повинен використовуватися.


2
  1. Спочатку потрібно запитати у себе, чи потрібно користувачеві мого методу турбуватися про введення неправильних даних, чи від нього очікується введення належних даних (у цьому випадку рядка). Це очікування також відоме як дизайн за контрактом .

  2. та 3. Так, ви, мабуть, повинні визначити BadDataException або навіть краще використовувати деякі із вирізаних, таких як NumberFormatException, а залишити стандартне повідомлення для показу. Зловлюйте NumberFormatException у методі та повторно додайте його разом із вашим повідомленням, не забуваючи включити оригінальну трасування стека.

  3. Це залежить від ситуації, але я б, мабуть, пішов із повторним викидом NumberFormatException із деякою додатковою інформацією. А також має бути роз'яснення javadoc щодо того, для чого очікуються значенняString a, String b


1

Багато залежить від сценарію, в якому ви знаходитесь.

Випадок 1. Завжди ви, хто налагоджує код, і ніхто інший, ані виняток, не спричинятимуть поганого користувацького досвіду

Киньте за замовчуванням NumberFormatException

Випадок 2: Кодекс повинен бути надзвичайно ремонтопридатним та зрозумілим

Визначте власний виняток і додайте набагато більше даних для налагодження під час його викидання.

Вам не потрібні перевірки регулярних виразів, оскільки вони все одно перейдуть на виняток із поганим введенням.

Якби це був код виробничого рівня, моя ідея полягала б у визначенні декількох спеціальних винятків, наприклад

  1. Виняток формату чисел
  2. Виняток із переповнення
  3. Нульовий виняток тощо ...

і мати справу з усім цим окремо


1
  1. Ви можете зробити це, щоб зрозуміти, що це може статися за неправильне введення. Хтось, хто використовує ваш код, може допомогти запам’ятати цю ситуацію. Більш конкретно, ви чітко даєте зрозуміти, що самі не обробляєте це в коді або замість цього повертаєте якесь конкретне значення. Звичайно, JavaDoc також повинен це чітко пояснити.
  2. Тільки якщо ви хочете змусити абонента мати справу з перевіреним винятком.
  3. Це здається надмірним. Покладайтеся на синтаксичний аналіз, щоб виявити неправильний вхід.

Взагалі, NumberFormaException не позначено, оскільки передбачається, що надано правильно розбірний вхід. Перевірка вводу - це те, з чим вам слід поводитися. Однак насправді розбір вхідних даних - це найпростіший спосіб зробити це. Ви можете просто залишити свій метод таким, як він є, і попередити в документації, що очікується правильне введення, і кожен, хто викликає вашу функцію, повинен перевірити обидва входи перед використанням.


1

Будь-яка виняткова поведінка повинна бути роз'яснена в документації. Або слід зазначити, що цей метод повертає спеціальне значення у випадку відмови (наприклад null, шляхом зміни типу повернення на Integer) або випадку 1. Визначення його в підписі методу дозволяє користувачеві ігнорувати його, якщо він забезпечує правильні рядки іншими засобами, але все одно очевидно, що метод не справляється з таким видом відмов сам по собі.


1

Відповідь на ваше оновлене запитання.

Так, цілком нормально отримувати "сюрпризні" винятки. Подумайте про всі помилки часу виконання, які виникають, коли з’являються нові програми.

e.g ArrayIndexOutofBound

Також поширений сюрприз виняток з для кожного циклу.

ConcurrentModificationException or something like that

1

Незважаючи на те, що я погоджуюсь з відповіддю на те, що виняток виконання має бути дозволено виконувати, з точки зору дизайну та зручності використання, було б непогано обернути його в IllegalArgumentException, а не кидати як NumberFormatException. Потім це робить контракт вашого методу більш чітким, де він оголошує, що йому передано незаконний аргумент, через який він створив виняток.

Щодо оновлення запитання "Уявіть, це не фреймворк з відкритим кодом, який вам слід використовувати з якихось причин. Ви дивитесь на підпис методу і думаєте -" Добре, це ніколи не кидає ". Потім, одного дня, ви отримали виняток Це нормально? " javadoc вашого методу завжди повинен розкривати поведінку вашого методу (обмеження до і після). Подумайте про рядки інтерфейсів збірки сказати, де, якщо нуль не дозволений, javadoc каже, що буде викинуто виняток нульового вказівника, хоча це ніколи не є частиною підпису методу.


2
NumberFormatException - це підклас IllegalArgumentException, тому це перенесення не додає жодної інформації.
Дон Робі

що це означає, що виняток може бути виконаний таким, яким він є (оскільки в цьому випадку nfe вже є винятком незаконного аргументу), і може бути загальна обробка для обробки незаконних аргументів десь вгорі в ієрархії викликів. отже, якщо це був приклад, коли аргументи передавав користувач, міг би бути загальний код, який би обернув усі обробки незаконних аргументів та повідомив про це користувача.
Scorpion

1

Оскільки ви говорите про хорошу Java-практику, на мій погляд, це завжди краще

  • Щоб обробити неперевірений виняток, проаналізуйте його та за допомогою спеціального неперевіреного винятку.

  • Також під час викиду нестандартного виключення ви можете додати повідомлення про виняток, яке ваш клієнт міг зрозуміти, а також надрукувати трасування стека вихідного винятку

  • Не потрібно оголошувати спеціальний виняток як "кидає", оскільки він не встановлений.

  • Таким чином ви не порушуєте використання того, для чого створені неперевірені винятки, в той же час клієнт коду легко зрозуміє причину та рішення для винятку.

  • Також належне документування у java-doc є гарною практикою і дуже допомагає.


1

Я думаю, це залежить від вашої мети, але я б задокументував це як мінімум:

/**
 * @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;
}

Або візьміть сторінку з вихідного коду Java для класу java.lang.Integer:

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

1

Як щодо шаблону перевірки вводу, реалізованого бібліотекою Google „Guava“ або бібліотекою Apache „Validator“ ( порівняння )?

З мого досвіду вважається гарною практикою перевіряти параметри функції на початку функції та викидати винятки, де це доречно.

Крім того, я вважав би це питання в основному незалежним від мови. Тут “хороша практика” стосуватиметься всіх мов, які мають функції, які можуть приймати параметри, які можуть бути або не бути дійсними.


1

Я думаю, що ваше перше речення "Досить дурне питання" є дуже актуальним. Чому б вам коли-небудь писати метод із таким підписом? Чи є сенс взагалі підсумовувати два рядки? Якщо метод, що викликає, хоче підсумувати два рядки, відповідальність методу, що викликає, полягає в тому, щоб переконатися, що вони є дійсними ints, і перетворити їх перед викликом методу.

У цьому прикладі, якщо метод виклику не може перетворити два рядки в int, він може зробити кілька речей. Це насправді залежить, на якому шарі відбувається це підсумовування. Я припускаю, що перетворення рядка було б дуже близьким до інтерфейсного коду (якби це було зроблено належним чином), такий випадок 1. був би найімовірнішим:

  1. Встановіть повідомлення про помилку та припиніть обробку або переспрямування на сторінку помилки
  2. Повернути false (тобто, це містило б суму в якийсь інший об'єкт і не вимагало б її повернення)
  3. Викиньте BadDataException, як ви пропонуєте, але якщо підсумовування цих двох чисел не є дуже важливим, це надмірне, і, як уже згадувалося вище, це, мабуть, поганий дизайн, оскільки це означає, що перетворення здійснюється не в тому місці

1

На це питання є багато цікавих відповідей. Але я все одно хочу додати це:

Для розбору рядків я завжди волію використовувати "регулярні вирази". Нам допоможе пакет java.util.regex. Тож я закінчу щось подібним, що ніколи не створює жодних винятків. Мені потрібно повернути спеціальне значення, якщо я хочу виявити якусь помилку:

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

Як бачимо, код трохи довший, але ми можемо обробляти те, що хочемо (і встановлювати значення за замовчуванням для x та y, контролювати те, що відбувається з реченнями else тощо). Ми могли б навіть написати більш загальне перетворення підпрограма, до якої ми можемо передавати рядки, стандартні значення повернення, код REGEX для компіляції, повідомлення про помилки для викидання, ...

Сподіваюся, це було корисно.

Попередження: Я не зміг протестувати цей код, тому, будь ласка, вибачте за можливі проблеми із синтаксисом.


1
ми говоримо про Java? Що це таке Integer.matcher? privateметод змінної всередині? Відсутня ()для МФС, пропускаючи багато ;, x, yне заявив, matcherдвічі заявив, ...
user85421

Дійсно, Карлосе, я зробив це поспіхом, і, як я вже сказав, не зміг перевірити це відразу. Вибачте. Я хотів показати регулярний вираз.
Луї

Добре, але це не вирішує проблему з NumberFormatException - головне питання IMO - (припускаючи, що Integer.matcher.group()це має бути Integer.parseInt(matcher.group()))
user85421

0

Ви стикаєтеся з цією проблемою, оскільки дозволяєте помилкам користувача поширюватися занадто глибоко в ядро ​​програми, а частково також через зловживання типами даних Java.

Ви повинні чіткіше відокремити перевірку введення користувачем від бізнес-логіки, використовувати належний набір даних, і ця проблема зникне сама собою.

Справа в тому, що семантика Integer.parseInt()відома - це основна мета аналізу дійсних цілих чисел. Вам не вистачає явного кроку перевірки / аналізу вводу користувача.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.