Есть ли оператор return только для устранения плохой практики синтаксиса?

82

Рассмотрим следующий код:

public Object getClone(Cloneable a) throws TotallyFooException {

    if (a == null) {
        throw new TotallyFooException();
    }
    else {
        try {
            return a.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
    }
    //cant be reached, in for syntax
    return null;
}

Это return null;необходимо, поскольку исключение может быть перехвачено, однако в таком случае, поскольку мы уже проверили, было ли оно нулевым (и предположим, что мы знаем, что вызываемый нами класс поддерживает клонирование), поэтому мы знаем, что оператор try никогда не завершится ошибкой.

Является ли плохой практикой помещать дополнительный оператор возврата в конце, чтобы удовлетворить синтаксис и избежать ошибок компиляции (с пояснением, что он не будет достигнут), или есть лучший способ закодировать что-то вроде этого, чтобы дополнительный оператор возврата не нужен?

ицих
источник
25
Ваш метод принимает Objectпараметр. Если aэто не класс, поддерживающий cloneметод (или это определено в Object?), Или если во время cloneметода возникает ошибка (или любая другая ошибка, о которой я не могу сейчас вспомнить), может быть сгенерировано исключение, и вы достигнете заявление о возврате.
Arc676
26
Ваше предположение, что окончательный возврат невозможен, ошибочно. Это полностью допустимо для класса, который реализует Cloneableсоздание CloneNotSupportedException. Источник: javadocs
Cephalopod
20
Код, который вы отметили как «недоступен», доступен. Как упоминалось выше, есть кодовый путь от CloneNotSupportedException до этой строки.
Дэвид Уотеруорт
7
Основной вопрос здесь: если вы предполагаете, что вызываемый класс поддерживает клонирование, почему вы вообще перехватываете исключение? Исключения должны быть брошены на необычайное поведение.
deworde
3
@wero Я бы выбрал AssertionErrorвместо InternalError. Последний, кажется, предназначен для специального использования, если что-то в JVM пойдет не так. А вы в основном утверждаете, что код не дошел.
kap

Ответы:

135

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

if (a != null) {
    try {
        return a.clone();
    } catch (CloneNotSupportedException e) {
        e.printStackTrace();
    }
}
throw new TotallyFooException();

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

Каяман
источник
28
Это иллюстрирует действительно хороший общий момент. Если вы обнаруживаете, что боретесь с требованиями Java, это обычно означает, что вы пытаетесь что-то сделать неоптимальным способом. Обычно, если мне неудобно, я отступаю, смотрю на всю картину и пытаюсь понять, можно ли ее закодировать другим способом, чтобы сделать ее более ясной - обычно это так. Большинство мелких придирок Java становятся удивительно полезными, когда вы их принимаете.
Bill K
3
@BillK: Этот код имеет другую семантику, как указал Марун Марун.
Дедупликатор
5
AssertionError- это встроенный класс с аналогичным намерением TotallyFooException(которого на самом деле не существует).
user253751
5
@BillK: «Если вы боретесь с требованиями java, это обычно означает, что вы пытаетесь сделать что-то неоптимальным способом». или что Java решила не поддерживать функцию, которая облегчает вашу жизнь.
user541686
5
@Mehrdad, ... но такой взгляд на него не принесет вам практически никакой пользы, если только вы не работаете над RFE для будущих версий Java или альтернативным языком. Изучение местных идиом имеет реальное практическое преимущество, в то время как обвинение языка (любого языка, если он не имеет поддержки метапрограммирования a-la-LISP, позволяющей добавлять новый синтаксис самостоятельно) в том, что он не поддерживает тот способ, которым вы изначально хотели что-то сделать, - нет.
Чарльз Даффи,
101

Это определенно может быть достигнуто. Обратите внимание, что вы печатаете только трассировку стека вcatch предложении.

В сценарии, где a != nullи будет исключение, return null будет достигнуто. Вы можете удалить этот оператор и заменить его наthrow new TotallyFooException(); .

В общем * , если nullэто действительный результат метода (т.е. пользователь ожидает его и это что-то значит), то возвращать его как сигнал «данные не найдены» или возникло исключение - не лучшая идея. В противном случае я не вижу проблем, почему ты не должен возвращатьсяnull .

Возьмем, к примеру, Scanner#ioExceptionметод:

Возвращает IOExceptionпоследнее значение, брошенное базовым элементом Readable этого Scanner. Этот метод возвращает значение null, если такого исключения не существует .

В этом случае возвращаемое значение nullимеет ясный смысл, когда я использую метод, я могу быть уверен, что получил nullтолько потому, что не было такого исключения, а не потому, что метод пытался что-то сделать, но он не удался.

* Обратите внимание, что иногда вы все же хотите вернуться, nullдаже если значение неоднозначно. Например HashMap#get:

Возвращаемое значение null не обязательно означает, что карта не содержит сопоставления для ключа; также возможно, что карта явно отображает ключ в null . containsKeyОперация может быть использована , чтобы отличить эти два случая.

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

Марун
источник
4
Хотя ваши аргументы справедливы, я хотел бы отметить, что это просто дрянной дизайн API. Оба метода должны возвращать Optional<Throwable>или Optional<TValue>соответственно. Примечание: это критика не вашего ответа, а скорее конструкции API этих двух методов. (Однако это довольно распространенная ошибка дизайна, которая распространяется не только на весь Java API, но и, например, на .NET.)
Йорг В. Миттаг,
4
«дерьмовый» - это довольно жестко, если вы не знаете, можно ли использовать средства Java 8 или нет.
Thorbjørn Ravn Andersen
2
Но когда nullэто не является допустимым значением возврата, возвращение nullявляется еще хуже идея. Другими словами, возвращение nullв неожиданной ситуации - плохая идея.
Holger
1
@Holger Что я имею в виду не действует , например , когда ожидает пользователь , чтобы получить значение из структуры данных , которые не могут содержать null, то nullникогда не может быть «действителен» возвращаемое значение в том смысле , что он должен указать что - то другое , а не нормальный возвращаемое значение. В этом случае его возврат имеет особое значение, и я не вижу ничего плохого в этом дизайне (конечно, пользователь должен знать, как справиться с этой ситуацией).
Maroun
1
В этом суть: «пользователь должен знать, как справиться с этой ситуацией» подразумевает, что вы должны указать, что nullэто возможное возвращаемое значение, с которым должен иметь дело вызывающий. Это делает его допустимым значением, и у нас здесь то же самое понимание «действительного», поскольку вы описываете «действительный» как «пользователь этого ожидает, и это что-то значит». Но здесь ситуация иная. OP заявляет для каждого комментария кода, что он утверждает, что return null;утверждение «не может быть достигнуто», что означает, что возврат nullневерен. throw new AssertionError();Вместо этого он должен быть …
Хольгер
26

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

Я считаю, что return nullэто плохая практика для завершения недоступной ветви. Лучше закинуть RuntimeException(AssertionError также было бы приемлемо), чтобы добраться до этой строки, что-то пошло не так, и приложение находится в неизвестном состоянии. Скорее всего, это (как указано выше), потому что разработчик что-то упустил (объекты могут быть непустыми и неклонируемыми).

Скорее всего, я не буду использовать, InternalErrorесли не очень уверен, что код недоступен (например, послеSystem.exit() ), так как более вероятно, что я сделаю ошибку, чем виртуальная машина.

Я бы использовал настраиваемое исключение (например, TotallyFooException), только если переход к этой «недоступной строке» означает то же самое, что и везде, где вы вызываете это исключение.

Майкл Ллойд Ли млк
источник
3
Как отмечалось в другом месте в комментариях, также AssertionErrorможет быть разумным выбором добавить событие «не может произойти» .
Ilmari Karonen
2
Лично я бы бросил SomeJerkImplementedClonableAndStillThrewACloneNotSupportedExceptionException . Очевидно, это расширится RuntimeException.
w25r
@ w25r Я не использую приведенный выше код, а отвечаю на основной вопрос. Данный Cloneableне реализует общедоступный clone()метод иclone() в объекте приведенный protectedвыше код фактически не компилируется.
Michael Lloyd Lee mlk
Я бы не стал использовать пассивно-агрессивное исключение на случай, если оно ускользнет (как ни забавно было бы получить a для одного из наших клиентов JerkException, я не думаю, что это будет оценено по достоинству).
Майкл Ллойд Ли mlk
14

Вы поймали, CloneNotSupportedExceptionчто означает, что ваш код может с этим справиться. Но после того, как вы его поймаете, вы буквально не знаете, что делать, когда дойдете до конца функции, что означает, что вы не можете справиться с этим. Так что вы правы, что в данном случае это запах кода и, на мой взгляд, означает, что вы не должны были его поймать CloneNotSupportedException.

Джехлин
источник
12

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

И чтобы избежать отмеченных исключений, я бы снова бросил CloneNotSupportedException как файл RuntimeException.

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

public Object getClone(Object a) {

    Objects.requireNonNull(a);

    try {
        return a.clone();
    } catch (CloneNotSupportedException e) {
        throw new IllegalArgumentException(e);
    }

}
Кучи
источник
7

В такой ситуации я бы написал

public Object getClone(SomeInterface a) throws TotallyFooException {
    // Precondition: "a" should be null or should have a someMethod method that
    // does not throw a SomeException.
    if (a == null) {
        throw new TotallyFooException() ; }
    else {
        try {
            return a.someMethod(); }
        catch (SomeException e) {
            throw new IllegalArgumentException(e) ; } }
}

Интересно, что вы говорите, что «оператор try никогда не потерпит неудачу», но вы все же потрудились написать оператор, e.printStackTrace();который, по вашему мнению, никогда не будет выполнен. Почему?

Возможно, ваша вера не так прочна. Это хорошо (на мой взгляд), поскольку ваша вера основана не на написанном вами коде, а на ожидании того, что ваш клиент не нарушит предварительное условие. Лучше программировать публичные методы в защитных целях.

Кстати, ваш код для меня не компилируется. Вы не можете позвонить, a.clone()даже если тип aтакой Cloneable. По крайней мере, так говорит компилятор Eclipse. Выражение a.clone()дает ошибку

Метод clone () не определен для типа Cloneable

Что бы я сделал для вашего конкретного случая, так это

public Object getClone(PubliclyCloneable a) throws TotallyFooException {
    if (a == null) {
        throw new TotallyFooException(); }
    else {
        return a.clone(); }
}

Где PubliclyCloneableопределяется

interface PubliclyCloneable {
    public Object clone() ;
}

Или, если вам абсолютно необходим тип параметра Cloneable, по крайней мере компилируется следующий.

public static Object getClone(Cloneable a) throws TotallyFooException {
//  Precondition: "a" should be null or point to an object that can be cloned without
// throwing any checked exception.
    if (a == null) {
        throw new TotallyFooException(); }
    else {
        try {
            return a.getClass().getMethod("clone").invoke(a) ; }
        catch( IllegalAccessException e ) {
            throw new AssertionError(null, e) ; }
        catch( InvocationTargetException e ) {
            Throwable t = e.getTargetException() ;
            if( t instanceof Error ) {
                // Unchecked exceptions are bubbled
                throw (Error) t ; }
            else if( t instanceof RuntimeException ) {
                // Unchecked exceptions are bubbled
                throw (RuntimeException) t ; }
            else {
                // Checked exceptions indicate a precondition violation.
                throw new IllegalArgumentException(t) ; } }
        catch( NoSuchMethodException e ) {
            throw new AssertionError(null, e) ; } }
}
Теодор Норвелл
источник
Ошибка времени компиляции заставила меня задуматься, почему Cloneableне объявляется cloneобщедоступным методом, который не генерирует никаких проверенных исключений. На bugs.java.com/view_bug.do?bug_id=4098033
Теодор Норвелл,
2
Я хотел бы упомянуть, что подавляющее большинство кода Java не использует фигурные скобки в стиле Лиспа, и вы получите непристойный вид, если попытаетесь использовать это в рабочей среде.
Иск Фонда Моники
1
Спасибо за это. На самом деле я не был очень последовательным. Я отредактировал ответ, чтобы постоянно использовать предпочитаемый мной стиль скобок.
Теодор Норвелл,
6

Приведенные выше примеры действительны и очень Java. Однако вот как я бы ответил на вопрос OP о том, как обрабатывать этот возврат:

public Object getClone(Cloneable a) throws CloneNotSupportedException {
    return a.clone();
}

Нет никакой пользы от проверки, aявляется ли он нулевым. Это будет NPE. Печать трассировки стека также бесполезна. Трассировка стека одинакова независимо от того, где она обрабатывается.

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

(Обратите внимание, что OP включил ошибку в обработку исключений; вот почему был необходим возврат. OP не ошиблась бы в методе, который я предлагаю.)

Тони Эннис
источник
5

Есть ли оператор return только для устранения плохой практики синтаксиса?

Как уже упоминали другие, в вашем случае это фактически не применяется.

Однако, чтобы ответить на вопрос, программы типа Lint наверняка этого не поняли! Я видел, как два разных человека боролись из-за этого в инструкции switch.

    switch (var)
   {
     case A:
       break;
     default:
       return;
       break;    // Unreachable code.  Coding standard violation?
   }

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

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

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

Jos
источник
5

Это не «просто для удовлетворения синтаксиса». Семантическое требование языка состоит в том, что каждый путь кода приводит к возврату или отклонению. Этот код не соответствует. Если исключение обнаружено, требуется следующий возврат.

Никакой «плохой практики» по этому поводу или по поводу удовлетворения компилятора в целом.

В любом случае, будь то синтаксис или семантика, у вас нет выбора.

user207421
источник
2

Я бы переписал это, чтобы вернуть в конце. Псевдокод:

if a == null throw ...
// else not needed, if this is reached, a is not null
Object b
try {
  b = a.clone
}
catch ...

return b
Пабло Пасос
источник
В качестве дополнительного примечания: вы почти всегда можете рефакторировать свой код, чтобы в конце был оператор return. Фактически, это хороший стандарт кодирования, поскольку он немного упрощает поддержку кода и исправление ошибок, поскольку вам не нужно учитывать множество точек выхода. По той же причине было бы неплохо объявить все переменные в начале метода. Также учтите, что для возврата в конце может потребоваться объявить больше переменных, как и в моем решении, мне нужна была дополнительная переменная «Object b».
Пабло Пазос
2

Никто еще не упомянул об этом, так что вот оно:

public static final Object ERROR_OBJECT = ...

//...

public Object getClone(Cloneable a) throws TotallyFooException {
Object ret;

if (a == null) 
    throw new TotallyFooException();

//no need for else here
try {
    ret = a.clone();
} catch (CloneNotSupportedException e) {
    e.printStackTrace();
    //something went wrong! ERROR_OBJECT could also be null
    ret = ERROR_OBJECT; 
}

return ret;

}

Я не люблю returnвнутренние tryблоки именно по этой причине.

рат
источник
2

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

Если вы знаете подробности о вводимых данных таким образом, чтобы вы знали, что tryутверждение никогда не может потерпеть неудачу, какой в ​​нем смысл? Избегайтеtry если вы точно знаете, что всегда все будет успешно (хотя редко можно быть абсолютно уверенным на протяжении всего срока службы вашей кодовой базы).

В любом случае компилятор, к сожалению, не умеет читать мысли. Он видит функцию и ее входные данные, и, учитывая имеющуюся информацию, ему нужен этот returnоператор внизу, как и у вас.

Является ли плохой практикой помещать дополнительный оператор возврата в конце, чтобы удовлетворить синтаксис и избежать ошибок компиляции (с пояснением, что он не будет достигнут), или есть лучший способ закодировать что-то вроде этого, чтобы дополнительный оператор возврата не нужен?

Напротив, я бы посоветовал избегать любых предупреждений компилятора, например, даже если это стоит еще одной строчки кода. Не беспокойтесь здесь о количестве строк. Определите надежность функции с помощью тестирования, а затем двигайтесь дальше. Просто представьте, что вы можете пропустить это returnутверждение, представьте, что вы вернетесь к этому коду через год, а затем попытайтесь решить, returnне вызовет ли этот оператор внизу больше путаницы, чем какой-то комментарий с подробным описанием того, почему он был опущен из-за предположений, которые вы можете сделать о входных параметрах. Скорее всего, с returnзаявлением будет легче разобраться.

Тем не менее, конкретно об этой части:

try {
    return a.clone();
} catch (CloneNotSupportedException e) {
   e.printStackTrace();
}
...
//cant be reached, in for syntax
return null;

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

Вы можете думать о try/catchмеханизме транзакции. tryвнесение этих изменений, если они потерпят неудачу и мы перейдем в catchблок, сделайте это (что бы ни было в catchблоке) в ответ как часть процесса отката и восстановления.

В этом случае простая печать трассировки стека с последующим принудительным возвратом null - это не совсем подход к транзакции / восстановлению. Код передает ответственность за обработку ошибок всему коду, вызывающему getCloneвручную проверку на наличие сбоев. Вы могли бы предпочесть пойматьCloneNotSupportedException и преобразовать его в другую, более значимую форму исключения и выбросить это, но вы не хотите просто проглатывать исключение и возвращать нуль в этом случае, поскольку это не похоже на сайт восстановления транзакций.

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

Это как если вы загружаете файл, это транзакция высокого уровня. У вас может быть try/catchтам. В процессе tryingзагрузки файла вы можете клонировать объекты. Если где-то в этой высокоуровневой операции (загрузка файла) происходит сбой, вы обычно хотите, чтобы исключения полностью возвращались в этот try/catchблок транзакции верхнего уровня, чтобы вы могли корректно восстановиться после сбоя при загрузке файла (будь то из-за ошибки клонирования или чего-то еще). Таким образом, мы обычно не хотим просто проглотить исключение в таком детализированном месте, как это, а затем вернуть null, например, поскольку это нарушит большую часть значения и цели исключений. Вместо этого мы хотим полностью распространить исключения на сайт, где мы сможем с ними справиться.


источник
0

Ваш пример не идеален для иллюстрации вашего вопроса, как указано в последнем абзаце:

Является ли плохой практикой помещать дополнительный оператор возврата в конце, чтобы удовлетворить синтаксис и избежать ошибок компиляции (с пояснением, что он не будет достигнут), или есть лучший способ закодировать что-то вроде этого, чтобы дополнительный оператор возврата не нужен?

Лучшим примером может быть реализация самого клона:

 public class A implements Cloneable {
      public Object clone() {
           try {
               return super.clone() ;
           } catch (CloneNotSupportedException e) {
               throw new InternalError(e) ; // vm bug.
           }
      }
 }

Здесь никогда не следует вводить оговорку о перехвате . Тем не менее, синтаксис требует либо выбросить что-то, либо вернуть значение. Поскольку возвращение чего-либо не имеет смысла, InternalErrorиспользуется для обозначения серьезного состояния виртуальной машины.

Wero
источник
5
Суперкласс бросает CloneNotSupportedExceptionэто не «VM ошибки» - это совершенно законно для Cloneableбросить его. Это может представлять собой ошибку в вашем коде и / или в суперклассе, и в этом случае может быть целесообразным заключить ее в RuntimeExceptionили, возможно, AssertionError(но не InternalError). Или вы можете просто уклониться от исключения - если ваш суперкласс не поддерживает клонирование, то вы тоже, поэтому CloneNotSupportedExceptionсемантически приемлемо.
Илмари Каронен
@IlmariKaronen, вы можете отправить этот совет в Oracle, чтобы они могли исправить все методы клонирования в JDK, которые вызывают InternalError
wero
@wero Кто-то, несомненно, уже сделал это, но это попахивает устаревшим кодом.
deworde