Истинное решение в 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 Stringсекунды, преобразует их в ints и отображает окно сообщения с сообщением «сумма равна xxx».

Независимо от того, какой подход вы используете для разработки / реализации этого, у вас будут две точки внутренней функциональности :

  1. Место, где вы конвертируетесь Stringвint
  2. Место, где вы добавляете 2 intс

Основной вопрос моего исходного сообщения:

Integer.parseInt()ожидает передачи правильной строки. Всякий раз, когда вы передаете неверную строку , это означает, что ваша программа неверна (а не « ваш пользователь идиот»). Вам необходимо реализовать фрагмент кода, в котором, с одной стороны, у вас есть Integer.parseInt () с ОБЯЗАТЕЛЬНОЙ семантикой, а с другой стороны, вам нужно быть в порядке со случаями, когда ввод неверен - семантика ДОЛЖНА .

Итак, вкратце: как мне реализовать семантику СЛЕДУЕТ, если у меня есть только ОБЯЗАТЕЛЬНЫЕ библиотеки .

Андрей Агибалов
источник
13
Это отнюдь не глупый вопрос. Вообще-то неплохой! Я не совсем уверен, какой вариант я бы выбрал.
Мартин
1
К вашему сведению: да, согласно принципу отказоустойчивости, это тоже не обязательно плохо.
Johan Sjöberg
1
Я согласен с Мартином. Это один из наименее понятных аспектов программирования на Java, требующий некоторых передовых практик. Об этом можно судить по нехватке страниц по этой теме, и очень немногие, кроме О'Рейли, говорят: «Вот как это должно быть сделано».
Джеймс П.
2
Собственно, это отличный вопрос.
Крис Кадмор
1
Отличный вопрос! Также поднял несколько отличных ответов: D
Хелдар

Ответы:

35

Это хороший вопрос. Я бы хотел, чтобы больше людей думали о таких вещах.

IMHO, выброс непроверенных исключений приемлем, если вам были переданы параметры мусора.

Вообще говоря, вы не должны бросать, 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обращаться? Должен ли это быть вызывающий объект, или следует оставить исключение всплывающим? Если да, то как с этим обращаться?
Джеймс П.
1
Чтобы частично ответить на этот вопрос, я видел два подхода: первый - перехватить Exceptionsнижние уровни и заключить их в исключения более высокого уровня, которые более значимы для конечного пользователя, а второй - использовать обработчик неперехваченных исключений, который можно инициализировать в средство запуска приложений.
Джеймс П.
2
IMHO наличие NumberFormatException в javaDoc гораздо важнее. Помещение его в throwsпредложение - хорошее дополнение, но не обязательное.
Дорус
3
To «Вызывающие ваш метод могут знать, прежде чем они вызовут его, являются ли их строки числами или нет, поэтому передачи мусора можно избежать» : Это не совсем так. Единственный простой способ убедиться, что строка разбирается как int, - это попробовать. Хотя вы можете захотеть сделать несколько предварительных проверок, точная проверка - это вполне PITA.
maaartinus
49

На мой взгляд, было бы предпочтительнее обрабатывать логику исключений как можно глубже. Следовательно, я бы предпочел подпись

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

С вашей сигнатурой метода я бы ничего не менял. Либо ты

  • Программно с использованием неверных значений, вместо этого вы можете проверить свой алгоритм производителя
  • или отправка значений, например, из пользовательского ввода, и в этом случае этот модуль должен выполнить проверку

Следовательно, обработка исключений в этом случае становится проблемой документации .

Йохан Сьёберг
источник
4
Мне это нравится. Он заставляет пользователя следовать контракту и заставляет метод делать только одно.
Крис Кадмор
Я не понимаю, чем использование этого метода отличается от того, как пользователь делает a + b. Зачем нужен этот метод?
Swati
4
@Swati: Я думаю, это пример , отличный способ показать вещи просто. Метод суммы также может быть myVeryComplicatedMethodWhichComputesPhoneNumberOfMyIdealMatchGirl (int myID, int myLifeExpectancy) ...
Хелдар
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 guava). Делайте вашу проверку отдельно от вашей операции, ваша операция должна занимать int. Будет ли ваша операция вызвана со значениями по умолчанию, когда у вас недопустимый ввод, или вы сделаете что-то еще, будет зависеть от ваших спецификаций - больше всего я могу сказать об этом, что действительно маловероятно, что место для принятия этого решения находится в операции метод.

Jmoreno
источник
Какая у вас здесь идея? У меня будет такая же проблема, даже если у меня будет sum(int, int)и parseIntFromString(String). Чтобы получить такую ​​же функциональность в любом случае, мне придется написать фрагмент кода, в котором есть 2 вызова parseIntFromString()и 1 вызов sum(). Рассмотрим этот случай, если он имеет для вас смысл - я не вижу разницы с точки зрения исходной задачи.
Андрей Агибалов
@ loki2302: дело в том, что вызывающий абонент решает, какое поведение уместно, если это не число. Кроме того, если они не выполняют проверку ошибок, сбой происходит на один шаг ближе к месту назначения значения - для целей отладки, чем ближе к назначению, тем проще отладка. И вы с большей вероятностью пропустите написание соответствующего модульного теста для вызывающего, если вы выполняете TDD. По сути, вы не хотите, чтобы в методе x передавалась строка, которая должна быть числом, а затем 5 классов и 20 вызовов методов вызывают исключение, когда вы пытаетесь обработать его числом.
jmoreno
Я не понимаю. Вы хотите сказать, что создание интерфейса int sum(String, String)в реальности невозможно?
Андрей Агибалов
1
@ loki2302: Учитывая показанный код, это было бы возможно, но плохая идея. Если число в строке могло бы быть словом, где «2», «two», «dos», «deux», «zwo» все должны обрабатываться одинаково, тогда эта подпись была бы подходящей. Но на самом деле методу даны две строки и они рассматриваются как целые числа. Зачем тебе это нужно? Это плохая идея.
jmoreno
2
@ loki2302: Вы приняли ответ, в котором говорилось, что можно генерировать непроверенное исключение при передаче мусора, но суть строго типизированного языка - ПРЕДОТВРАЩАТЬ передачу мусора. Наличие метода, который ожидает, что строка всегда будет целым числом, просто вызывает проблемы. Даже Integer.parseInt не справляется с этим хорошо (исключение в отношении того, что ДОЛЖНО ожидать ввода - это плохо, метод .Net integer.TryParse намного лучше, хотя отсутствие параметра out в Java делает его несколько непрактичным).
jmoreno
3

1. Я должен указать NumberFormatException как часть сигнатуры метода.

Я думаю так. Хорошая документация.

2. Я должен определить свое собственное проверенное исключение (BadDataException), обработать NumberFormatException внутри метода и повторно выбросить его как BadDataException.

Иногда да. Проверенные исключения в некоторых случаях считаются лучше, но работа с ними - это настоящий PITA. Вот почему многие фреймворки (например, Hibernate) используют только исключения времени выполнения.

3. Я должен определить свое собственное проверенное исключение (BadDataException), проверить обе строки каким-то образом, как регулярные выражения, и выбросить свое исключение BadDataException, если оно не совпадает.

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

4. Ваша идея?

Вовсе нет.

Maaartinus
источник
2

№ 4.

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

Я не уверен, что сделаю №3, поскольку считаю это излишним.

Farmor
источник
2

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

Ашкан Ариан
источник
2
  1. Сначала вам нужно спросить себя, нужно ли пользователю моего метода беспокоиться о вводе неправильных данных или от него ожидается ввод правильных данных (в данном случае String). Это ожидание также известно как дизайн по контракту .

  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 не отмечено, поскольку ожидается, что будет предоставлен правильно анализируемый ввод. Вам следует заняться проверкой ввода. Однако на самом деле анализ ввода - самый простой способ сделать это. Вы можете просто оставить свой метод как есть и предупредить в документации, что ожидается правильный ввод, и любой, кто вызывает вашу функцию, должен проверить оба ввода перед ее использованием.

G_H
источник
1

Любое исключительное поведение должно быть разъяснено в документации. Либо он должен указывать, что этот метод возвращает специальное значение в случае сбоя (например null, при изменении типа возврата на Integer), либо следует использовать случай 1. Явное указание этого параметра в сигнатуре метода позволяет пользователю игнорировать его, если он обеспечивает правильные строки другими способами, но все же очевидно, что метод не справляется с подобными сбоями сам по себе.

капекс
источник
1

Ответьте на ваш обновленный вопрос.

Да, это нормально, когда возникают «неожиданные» исключения. Подумайте обо всех ошибках времени выполнения, которые возникают, когда вы только начинаете программировать.

e.g ArrayIndexOutofBound

Также частое неожиданное исключение из цикла for.

ConcurrentModificationException or something like that
Farmor
источник
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 ( сравнение )?

По моему опыту, хорошей практикой считается проверка параметров функции в начале функции и выдача исключений при необходимости.

Кроме того, я считаю, что этот вопрос в значительной степени не зависит от языка. «Хорошая практика» здесь применима ко всем языкам, в которых есть функции, которые могут принимать параметры, которые могут быть или недействительными.

Spycho
источник
1

Я думаю, что ваше самое первое предложение «Довольно глупый вопрос» очень актуально. Зачем вообще вообще писать метод с такой подписью? Есть ли вообще смысл суммировать две строки? Если вызывающий метод хочет суммировать две строки, вызывающий метод должен убедиться, что они являются действительными целыми числами, и преобразовать их перед вызовом метода.

В этом примере, если вызывающий метод не может преобразовать две строки в int, он может делать несколько вещей. Это действительно зависит от того, на каком слое происходит это суммирование. Я предполагаю, что преобразование String было бы очень близко к внешнему коду (если бы оно было выполнено правильно), поэтому случай 1. был бы наиболее вероятным:

  1. Установите сообщение об ошибке и прекратите обработку или перенаправьте на страницу с ошибкой
  2. Вернуть false (т.е. он поместил бы сумму в какой-то другой объект и не должен был бы возвращать ее)
  3. Вызовите какое-нибудь исключение BadDataException, как вы предлагаете, но если суммирование этих двух чисел не очень важно, это перебор, и, как упоминалось выше, это, вероятно, плохой дизайн, поскольку он подразумевает, что преобразование выполняется в неправильном месте
GreenieMeanie
источник
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 01
0

Вы сталкиваетесь с этой проблемой, потому что позволяете ошибкам пользователей слишком глубоко распространяться в ядро ​​приложения, а также частично из-за злоупотребления типами данных Java.

У вас должно быть более четкое разделение между проверкой пользовательского ввода и бизнес-логикой, используйте правильный набор данных, и эта проблема исчезнет сама собой.

Дело в том, что семантика Integer.parseInt()известных - это основная цель - анализировать действительные целые числа. Вам не хватает явного шага проверки / синтаксического анализа пользовательского ввода.

ржавый
источник