Почему я должен использовать ключевое слово «final» для параметра метода в Java?


381

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

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

Обеспечение того, что некоторые данные остаются постоянными, не так сильно, как кажется.

  • Если параметр является примитивом, то он не будет иметь никакого эффекта, так как параметр передается методу в качестве значения, и его изменение не будет иметь никакого эффекта вне области действия.

  • Если мы передаем параметр по ссылке, то сама ссылка является локальной переменной, и если ссылка изменяется внутри метода, это не будет иметь никакого эффекта за пределами области действия метода.

Рассмотрим простой пример теста ниже. Этот тест проходит, хотя метод изменил значение заданной ссылки, но не имеет никакого эффекта.

public void testNullify() {
    Collection<Integer> c  = new ArrayList<Integer>();      
    nullify(c);
    assertNotNull(c);       
    final Collection<Integer> c1 = c;
    assertTrue(c1.equals(c));
    change(c);
    assertTrue(c1.equals(c));
}

private void change(Collection<Integer> c) {
    c = new ArrayList<Integer>();
}

public void nullify(Collection<?> t) {
    t = null;
}

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

6
В чем разница между передачей по ссылке и передачей по значению?
NobleUplift

Проще описать эту разницу в контексте Си (по крайней мере, для меня). Если я передам указатель на метод типа: <code> int foo (int bar) </ code>, то этот указатель будет передан по значению. Это означает, что он скопирован, поэтому если я сделаю что-то внутри этого метода, как <code> free (bar); bar = malloc (...); </ code> тогда я только что сделал очень плохую вещь. Бесплатный вызов фактически освободит часть памяти, на которую указывает (так что любой указатель, который я передал, теперь висят). Однако <code> int foo (int & bar) </ bar> означает, что код действителен, и значение переданного указателя будет изменено.
Джерслан

1
Первый должен быть int foo(int* bar)последним int foo(int* &bar). Последний передает указатель по ссылке, первый передает ссылку по значению.
Джерслан

2
@ Мартин, на мой взгляд, это хороший вопрос ; см. заголовок вопроса и содержание сообщения в качестве объяснения, почему задается вопрос. Может быть, я неправильно понимаю правила здесь, но это именно тот вопрос, который я хотел найти при поиске «использования конечных параметров в методах» .
Виктор Заманян

Ответы:


239

Остановить переназначение переменной

Хотя эти ответы интеллектуально интересны, я не прочитал короткий простой ответ:

Используйте ключевое слово final, если вы хотите, чтобы компилятор предотвращал переназначение переменной другому объекту.

Независимо от того, является ли переменная статической переменной, переменной-членом, локальной переменной или переменной аргумента / параметра, эффект полностью одинаков.

пример

Давайте посмотрим эффект в действии.

Рассмотрим этот простой метод, в котором обеим переменным ( arg и x ) могут быть назначены разные объекты.

// Example use of this method: 
//   this.doSomething( "tiger" );
void doSomething( String arg ) {
  String x = arg;   // Both variables now point to the same String object.
  x = "elephant";   // This variable now points to a different String object.
  arg = "giraffe";  // Ditto. Now neither variable points to the original passed String.
}

Пометьте локальную переменную как окончательную . Это приводит к ошибке компилятора.

void doSomething( String arg ) {
  final String x = arg;  // Mark variable as 'final'.
  x = "elephant";  // Compiler error: The final local variable x cannot be assigned. 
  arg = "giraffe";  
}

Вместо этого давайте пометим переменную параметра как окончательную . Это также приводит к ошибке компилятора.

void doSomething( final String arg ) {  // Mark argument as 'final'.
  String x = arg;   
  x = "elephant"; 
  arg = "giraffe";  // Compiler error: The passed argument variable arg cannot be re-assigned to another object.
}

Мораль истории:

Если вы хотите, чтобы переменная всегда указывала на один и тот же объект, отметьте переменную final .

Никогда не переназначать аргументы

Как хорошая практика программирования (на любом языке), вы никогда не должны повторно назначать переменную параметра / аргумента объекту, отличному от объекта, переданного вызывающим методом. В приведенных выше примерах никогда не следует писать строкуarg = . Поскольку люди делают ошибки, а программисты - люди, давайте попросим компилятор помочь нам. Пометьте каждую переменную параметра / аргумента как 'final', чтобы компилятор мог найти и пометить любое такое переназначение.

Ретроспективно

Как отмечалось в других ответах… Учитывая первоначальную цель разработки Java, заключающуюся в том, чтобы помочь программистам избежать глупых ошибок, таких как чтение за концом массива, Java должна была быть спроектирована так, чтобы автоматически приводить в действие все переменные параметра / аргумента как «final». Другими словами, Аргументы не должны быть переменными . Но задним числом является видение 20/20, и дизайнеры Java были заняты в то время.

Итак, всегда добавлять finalко всем аргументам?

Должны ли мы добавить finalк каждому объявленному параметру метода?

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

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

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

Еще один случай, добавленный для полноты

public class MyClass {
    private int x;
    //getters and setters
}

void doSomething( final MyClass arg ) {  // Mark argument as 'final'.

   arg =  new MyClass();  // Compiler error: The passed argument variable arg  cannot be re-assigned to another object.

   arg.setX(20); // allowed
  // We can re-assign properties of argument which is marked as final
 }

23
«Как хорошая практика программирования (на любом языке), вы никогда не должны повторно назначать переменную параметра / аргумента [..]» Извините, я действительно должен вызывать вас по этому вопросу. Перераспределение аргументов является стандартной практикой в ​​таких языках, как Javascript, где количество передаваемых аргументов (или даже если они есть) не определяется сигнатурой метода. Например, с такой подписью, как «function say (msg)», люди позаботятся о том, чтобы был назначен аргумент «msg», например: «msg = msg || 'Hello World!';". Лучшие программисты Javascript в мире нарушают вашу хорошую практику. Просто прочитайте источник jQuery.
Стейн де Витт

37
@StijndeWitt Ваш пример показывает саму проблему переназначения аргумента переменной. Вы теряете информацию, ничего не получая взамен: (a) Вы потеряли переданное первоначальное значение, (b) Вы потеряли намерение вызывающего метода (Прошел ли вызывающий объект «Hello World!» Или мы по умолчанию). Оба a & b полезны для тестирования, длинного кода, а также для дальнейшего изменения значения. Я придерживаюсь своего утверждения: аргументы arg никогда не должны быть переназначены. Ваш код должен быть: message = ( msg || 'Hello World"' ). Просто нет причин не использовать отдельную переменную. Единственная стоимость - несколько байтов памяти.
Василий Бурк

9
@Basil: это больше кода (в байтах) и в Javascript, который действительно имеет значение. Крупный. Как и во многих вещах, это основано на мнении. Вполне возможно полностью игнорировать эту практику программирования и при этом писать отличный код. Практика программирования одного человека не делает это практикой каждого. Стой на этом все, что хочешь, я все равно решил написать по-другому. Это делает меня плохим программистом, или мой код плохим кодом?
Стейн де Витт

14
Используя message = ( msg || 'Hello World"' )риск, я позже случайно использую msg. Когда контракт, который я намереваюсь, звучит так: «поведение с аргументом no / null / undefined arg неотличимо от прохождения "Hello World"», хорошей практикой программирования является принятие его в начале функции. [Это может быть достигнуто без переназначения, начиная с, if (!msg) return myfunc("Hello World");но это становится громоздким с несколькими аргументами.] В тех редких случаях, когда логика в функции должна заботиться о том, использовалось ли значение по умолчанию, я бы предпочел назначить специальное значение для стража (предпочтительно общедоступное) ,
Бени Чернявский-Паскин

6
@ BeniCherniavsky-Paskin риск, который вы описываете, только из-за сходства между messageи msg. Но если бы он назвал это чем-то похожим processedMsgили что-то еще, что обеспечивает дополнительный контекст - вероятность ошибки намного ниже. Сосредоточьтесь на том, что он говорит, а не на том, «как» он это говорит. ;)
alfasin

230

Иногда приятно явно (для удобства чтения), что переменная не меняется. Вот простой пример, где использование finalможет спасти некоторые возможные головные боли:

public void setTest(String test) {
    test = test;
}

Если вы забыли ключевое слово this в установщике, то переменная, которую вы хотите установить, не будет установлена. Однако если вы используете finalключевое слово в параметре, то ошибка будет обнаружена во время компиляции.


65
Кстати, вы все равно увидите предупреждение «Назначение переменной test не имеет никакого эффекта»
AvrDragon

12
@AvrDragon Но мы можем игнорировать и предупреждение. Таким образом, всегда лучше иметь что-то, что будет мешать нам идти дальше, например, ошибку компиляции, которую мы получим, используя ключевое слово final.
Сумит Десаи

10
@AvrDragon Это зависит от среды разработки. Вы все равно не должны полагаться на IDE, чтобы поймать подобные вещи для вас, если только вы не хотите развить вредные привычки.
b1nary.atr0phy

25
@ b1naryatr0phy на самом деле это предупреждение компилятора, а не просто IDE-подсказка
AvrDragon

8
@SumitDesai «Но мы могли бы также проигнорировать предупреждение. Поэтому всегда лучше иметь что-то, что не даст нам двигаться дальше, например, ошибку компиляции, которую мы получим, используя ключевое слово final». Я понимаю вашу точку зрения, но это очень сильное утверждение, с которым, я думаю, многие Java-разработчики не согласятся. Предупреждения компилятора существуют по какой-то причине, и компетентному разработчику не нужна ошибка, чтобы «заставить» их задуматься о ее последствиях.
Стюарт Росситер

127

Да, исключая анонимные классы, читаемость и декларацию намерений, это почти бесполезно. Эти три вещи бесполезны, хотя?

Лично я склонен не использовать final локальные переменные и параметры, если я не использую переменную в анонимном внутреннем классе, но я, безусловно, вижу смысл тех, кто хочет прояснить, что само значение параметра не изменится (даже если объект, на который он ссылается, изменяет свое содержимое). Для тех, кто считает, что это улучшает читабельность, я думаю, что это вполне разумно.

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

РЕДАКТИРОВАТЬ: Я действительно должен был подвести итог всего этого с ссылкой на Монти Пайтон; вопрос кажется чем-то похожим на вопрос: «Что римляне когда-либо делали для нас?»


17
Но если перефразировать Красти с его датским, что они сделали для нас в последнее время ? =)
Джеймс Шек

Юваль. Забавно! Я полагаю, что мир может произойти, даже если он поддерживается мечом!
gonzobrains

1
Вопрос , кажется , больше похоже на вопрос: «Что не римляне сделали для нас?», Потому что это больше критики о том , что окончательное ключевое слово не делать.
NobleUplift

«Вы предполагаете, что есть значительная часть разработчиков, предполагающих, что финал имеет больший эффект, чем на самом деле?» Для меня это главная проблема: я сильно подозреваю , что значительная часть разработчиков , используя его думать , что он навязывает неизменность вызывающих переданных товаров , когда это не так. Разумеется, тогда ввязываются в дебаты о том, должны ли стандарты кодирования «защищать» от концептуальных недоразумений (о которых должен знать «компетентный» разработчик) или нет (и это затем ведет к выводам, выходящим за рамки SO). вопрос)
Стюарт Росситер

1
@SarthakMittal: значение не будет скопировано, если вы его не используете, если вам интересно.
Джон Скит

76

Позвольте мне объяснить немного об одном случае, когда у вас есть использовать final, о котором Джон уже упоминал:

Если вы создаете анонимный внутренний класс в своем методе и используете локальную переменную (например, параметр метода) внутри этого класса, то компилятор заставит вас сделать параметр окончательным:

public Iterator<Integer> createIntegerIterator(final int from, final int to)
{
    return new Iterator<Integer>(){
        int index = from;
        public Integer next()
        {
            return index++;
        }
        public boolean hasNext()
        {
            return index <= to;
        }
        // remove method omitted
    };
}

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

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

Поэтому вместо этого Java помещает копии этих локальных переменных в качестве скрытых переменных экземпляра в анонимный класс (вы можете увидеть их, если изучите байт-код). Но если они не были окончательными, можно ожидать, что анонимный класс и метод увидят изменения, которые другой вносит в переменную. Чтобы сохранить иллюзию, что существует только одна переменная, а не две копии, она должна быть окончательной.


1
Вы потеряли меня из «Но если они не окончательные ....» Можете ли вы попытаться перефразировать это, Может быть, мне не хватило кофе.
hhafez

1
У вас есть локальные переменные из - вопрос в том, что произойдет, если вы используете экземпляр класса anon внутри метода, и он меняет значения из - люди ожидают, что изменение будет видимым в методе, поскольку они видят только одну переменную. Чтобы избежать этой путаницы, она должна быть окончательной.
Майкл Боргвардт

Он не делает копию, это просто ссылка на любой объект, на который была ссылка.
Викирк

1
@vickirk: уверен, что делает копию - ссылки, в случае ссылочных типов.
Майкл Боргвардт

Кстати, если у нас нет анонимных классов, ссылающихся на эти переменные, знаете ли вы, есть ли какая-либо разница между finalпараметром функции и параметром нефинала в глазах HotSpot?
Pacerier

25

Я использую final все время по параметрам.

Это добавляет так много? На самом деле, нет.

Буду ли я выключить это? Нет.

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

Я хотел бы видеть это сделанным по умолчанию в будущей версии Java. Передача по значению / справке запутывает огромное количество начинающих программистов.

Еще одна вещь ... мои методы, как правило, имеют небольшое количество параметров, поэтому дополнительный текст в объявлении метода не является проблемой.


4
Я также собирался предложить это, что final будет значением по умолчанию в будущих версиях, и что вы должны указать «изменяемое» или лучшее ключевое слово, которое задумано. Вот хорошая статья об этом: lpar.ath0.com/2008/08/26/java-annoyance-final-parameters
Джефф Аксельрод

Прошло много времени, но не могли бы вы привести пример обнаруженной ошибки?
user949300

Смотрите наиболее проголосовавших за ответ. Это действительно хороший пример, когда переменная-член не установлена, а параметр изменен.
Fortyrunner

18

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


2
Точно, это не часть интерфейса функции, а только реализация. Это сбивает с толку, что java допускает (но не учитывает) final параметры в интерфейсах / объявлениях абстрактных методов.
Бени Чернявский-Паскин

8

Лично я не использую final для параметров метода, потому что это добавляет слишком много беспорядка в списки параметров. Я предпочитаю следить за тем, чтобы параметры метода не изменялись через что-то вроде Checkstyle.

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

Я, конечно, хотел бы что-то более сильное, как C / C ++ const.


Не уверен, что ссылки на IDE и инструменты применимы к публикации OP или теме. А именно, «final» - это проверка во время компиляции, что ссылка не была изменена или разграблена. Кроме того, для того, чтобы действительно обеспечить такие вещи, смотрите ответ об отсутствии защиты для детей-членов окончательных ссылок. Например, при создании API, использование IDE или инструмента не поможет внешним сторонам в использовании / расширении такого кода.
Даррелл Тиг

4

Поскольку Java передает копии аргументов, я чувствую, что актуальность finalдовольно ограничена. Я полагаю, что эта привычка возникла в эпоху C ++, когда вы могли запретить изменение ссылочного содержимого, выполнив a const char const *. Я чувствую, что такого рода вещи заставляют вас верить, что разработчик по своей сути глуп, как f ***, и должен быть защищен от действительно каждого символа, который он печатает. При всей скромности могу сказать, что я пишу очень мало ошибок, даже если я их опускаю final(если я не хочу, чтобы кто-то переопределял мои методы и классы). Может быть, я просто разработчик старой школы.


1

Я никогда не использую final в списке параметров, он просто добавляет беспорядок, как говорили предыдущие респонденты. Также в Eclipse вы можете установить назначение параметров для генерации ошибки, поэтому использование final в списке параметров мне кажется излишним. Интересно, что когда я включил настройку Eclipse для назначения параметров, генерирующую ошибку, он поймал этот код (это только то, как я помню поток, а не фактический код.): -

private String getString(String A, int i, String B, String C)
{
    if (i > 0)
        A += B;

    if (i > 100)
        A += C;

    return A;
}

Играя адвоката дьявола, что именно не так с этим делать?


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

1

Краткий ответ: finalнемного помогает, но ... вместо этого используйте защитное программирование на стороне клиента.

В самом деле, проблема в finalтом, что он только гарантирует, что ссылка неизменна, и радостно разрешает мутировать ссылочные элементы объекта без уведомления вызывающей стороны. Следовательно, наилучшей практикой в ​​этом отношении является защитное программирование на стороне вызывающего, создание глубоко неизменных экземпляров или глубоких копий объектов, которые могут быть ограблены недобросовестными API.


2
«проблема с final заключается в том, что он только обеспечивает неизменность ссылки», - не соответствует действительности, сама Java предотвращает это. Переменная, переданная в метод, не может изменить свою ссылку этим методом.
Madbreaks

Пожалуйста, изучите, прежде чем публиковать ... stackoverflow.com/questions/40480/…
Даррелл Тиг

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

Либо ты меня неправильно понимаешь, либо ошибаешься. Если я передаю ссылку на объект методу, и этот метод переназначает его, исходная ссылка остается неизменной для (меня) вызывающей стороны, когда метод завершает свое выполнение. Java строго передается по значению. И ты нагло самоуверен, чтобы утверждать, что я не провел никаких исследований.
Madbreaks

Downvoting, потому что op спросил, зачем использовать final, а вы указали только одну неверную причину.
Madbreaks

0

Еще одна причина для добавления final к объявлениям параметров заключается в том, что он помогает идентифицировать переменные, которые должны быть переименованы как часть рефакторинга «Extract Method». Я обнаружил, что добавление final к каждому параметру перед запуском рефакторинга большого метода быстро говорит мне, есть ли какие-либо проблемы, которые мне нужно решить перед продолжением.

Однако я обычно удаляю их как лишние в конце рефакторинга.


-1

Следите за тем, что написал Мишель. Я сам сделал другой пример, чтобы объяснить это. Я надеюсь, что это может помочь.

public static void main(String[] args){
    MyParam myParam = thisIsWhy(new MyObj());
    myParam.setArgNewName();

    System.out.println(myParam.showObjName());
}

public static MyParam thisIsWhy(final MyObj obj){
    MyParam myParam = new MyParam() {
        @Override
        public void setArgNewName() {
            obj.name = "afterSet";
        }

        @Override
        public String showObjName(){
            return obj.name;
        }
    };

    return myParam;
}

public static class MyObj{
    String name = "beforeSet";
    public MyObj() {
    }
}

public abstract static class MyParam{
    public abstract void setArgNewName();
    public abstract String showObjName();
}

Из кода выше, в методе thisIsWhy () , мы на самом деле не назначали в [аргументе MyObj OBJ] к реальной ссылке в MyParam. Вместо этого мы просто используем [аргумент MyObj obj] в методе внутри MyParam.

Но после того, как мы закончим метод thisIsWhy () , должен ли существовать аргумент (объект) MyObj?

Похоже, что так и должно быть, потому что мы можем видеть в main, что мы все еще вызываем метод showObjName (), и ему нужно достичь obj . MyParam по-прежнему использует / достигает аргумента метода, даже если метод уже был возвращен!

Как Java на самом деле этого добивается, так это генерация копии - это скрытая ссылка на аргумент MyObj obj внутри объекта MyParam (но это не формальное поле в MyParam, поэтому мы его не видим)

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

Но если мы не поставили аргумент final, что приводит к ситуации, мы можем переназначить новую память (объект) на аргумент MyObj obj .

Технически нет никакого столкновения! Если нам позволят это сделать, ниже будет ситуация:

  1. Теперь у нас есть скрытая [MyObj obj] точка для [Памяти A в куче], теперь живущей в объекте MyParam.
  2. У нас также есть еще один [MyObj obj], который является аргументом для [Памяти B в куче], теперь живущей в методе thisIsWhy.

Никакого столкновения, но "ОГРОМНОЕ !!" Потому что все они используют одно и то же «ссылочное имя», которое «obj» .

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.