Чому я повинен використовувати ключове слово "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> означає, що код є дійсним і значення вказуваного вказівника буде змінено.
jerslan

1
Перший повинен бути int foo(int* bar)і останній int foo(int* &bar). Останній передає вказівник за посиланням, перший передає посилання за значенням.
jerslan

2
@Martin, на мій погляд, це гарне питання ; побачити назву для питання, а після змісту в якості пояснення , чому питання задають. Можливо, я тут нерозумію правила, але це саме те питання, яке я хотів шукати при "використанні кінцевих параметрів у методах" .
Віктор Заманян

Відповіді:


239

Зупиніть перепризначення змінної

Хоча ці відповіді інтелектуально цікаві, я не читав короткої простої відповіді:

Використовуйте ключове слово остаточне, коли ви хочете, щоб компілятор не дозволяв зміні перепризначати інший об'єкт.

Незалежно від того, чи є змінна статичною змінною, змінною члена, локальною змінною або змінною аргументу / параметра, ефект абсолютно однаковий.

Приклад

Подивимося ефект в дії.

Розглянемо цей простий метод, де обом змінним ( 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.
}

Мораль історії:

Якщо ви хочете, щоб змінна завжди вказувала на один і той же об'єкт, позначте змінну остаточною .

Ніколи не переназначайте аргументи

Як хороша практика програмування (будь-якою мовою), ви ніколи не повинні призначати змінну параметра / аргументу об'єкту, відмінному від об'єкта, переданого методом виклику. У наведених вище прикладах рядок ніколи не слід писати arg =. Оскільки люди роблять помилки, а програмісти - люди, давайте попросимо компілятора допомогти нам. Позначте кожну змінну параметрів / аргументів як "остаточну", щоб компілятор міг знайти та позначити будь-які подібні повторні призначення.

В ретроспективі

Як зазначається в інших відповідях ... Враховуючи оригінальну дизайнерську мету Java допомогти програмістам уникнути німих помилок, таких як читання минулого кінця масиву, Java повинна була бути розроблена для автоматичного застосування всіх змінних параметрів / аргументів як "остаточних". Іншими словами, Аргументи не повинні бути змінними . Але задній погляд - це 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 Ваш приклад показує саму проблему перепризначення змінної аргументу. Ви втрачаєте інформацію, нічого не отримавши натомість: (а) Ви втратили початкове передане значення, (б) Ви втратили намір методу виклику (Чи пройшов абонент "Hello World!" Чи ми за замовчуванням). І а & b корисні для тестування, довгого коду та коли значення буде додатково змінено пізніше. Я стою за своє твердження: аргументи не повинні ніколи перепризначатися. Ваш код повинен бути: message = ( msg || 'Hello World"' ). Просто немає причин не використовувати окремий вар. Єдина вартість - кілька байт пам'яті.
Василь Бурк

9
@Basil: Більше код (у байтах) і Javascript налічується. Сильно. Як і у багатьох речах, це засноване на думці. Цілком можливо повністю ігнорувати цю практику програмування і все-таки написати відмінний код. Практика програмування однієї людини не робить це практикою кожного. Встаньте за все, що ви хочете, я все одно вирішу писати його. Це робить мене поганим програмістом, або мій код поганим кодом?
Штійн де Вітт

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

6
@ БеніЧернявський-Паскін ризик, який ви описуєте, є лише через подібність між messageі msg. Але якщо він би назвав це чимось подібним processedMsgчи чимось іншим, що забезпечує додатковий контекст - шанс помилки набагато нижчий. Зосередьтеся на тому, що він говорить, а не на тому, як він це говорить. ;)
альфасін

230

Іноді приємно бути явним (для читабельності), що змінна не змінюється. Ось простий приклад, коли використання finalможе врятувати деякі можливі головні болі:

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

Якщо ви забудете ключове слово "це" на сетері, змінна, яку ви хочете встановити, не встановлюється. Однак якщо ви використали finalключове слово в параметрі, помилка буде схоплена під час компіляції.


65
btw ви побачите попередження "Призначення тесту змінної не має ефекту"
AvrDragon

12
@AvrDragon Але ми можемо також ігнорувати попередження. Отже, завжди краще мати щось, що заважатиме нам йти далі, як помилка компіляції, яку ми отримаємо, використовуючи остаточне ключове слово.
Суміт Дезай

10
@AvrDragon Це залежить від середовища розробки. Ви не повинні покладатися на IDE, щоб в будь-якому разі виловлювати подібні речі для вас, якщо ви не хочете розвивати шкідливі звички.
b1nary.atr0phy

25
@ b1naryatr0phy насправді це попередження компілятора, а не лише IDE-підказка
AvrDragon

8
@SumitDesai "Але ми можемо також ігнорувати попередження. Отже, завжди краще мати щось, що перешкоджає нам йти далі, як помилка компіляції, яку ми отримаємо, використовуючи остаточне ключове слово." Думаю, але це дуже сильне твердження, з яким я думаю, що багато розробників Java не погоджуються. Попередження компілятора існують з причини, і грамотному розробнику не потрібно мати помилку, щоб «змусити» їх врахувати його наслідки.
Стюарт Россітер

127

Так, виключаючи анонімні класи, читабельність та декларацію про наміри, це майже нічого не варто. Невже ці три речі нічого не варті?

Особисто я схильний не використовувати finalдля локальних змінних та параметрів, якщо я не використовую змінну в анонімному внутрішньому класі, але я, безумовно, бачу сенс тих, хто хоче зрозуміти, що саме значення параметра не зміниться (навіть якщо об’єкт, на який він посилається, змінює його вміст). Для тих, хто вважає, що це додає читабельності, я думаю, що це цілком розумна справа.

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

EDIT: Я дійсно повинен був узагальнити все це під посиланням Monty Python; питання здається дещо схожим на запитання "Що робили для нас колись римляни?"


17
Але якщо перефразовувати Крусті з його датською мовою , що вони зробили для нас ПОСЛІДНО ? =)
Джеймс Шек

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

1
Питання здається більш схожим на запитання: "Що не зробили для нас римляни?", Тому що це більше критика щодо того, що остаточне ключове слово не робить.
NobleUplift

"Ви припускаєте, що значна частина розробників припускає, що фінал має більший ефект, ніж насправді?" Для мене це головна проблема: я сильно підозрюю, що значна частина розробників, які використовують його, вважають, що це застосовує незмінність переданих позицій абоненту, коли цього немає. Звичайно, потім втягується в дискусію щодо того, чи повинні стандарти кодування "захищати" від концептуальних непорозумінь (про що повинен знати "компетентний" розробник) чи ні (і це потім спрямовується на думку, що не мають рамки СО -тип питання)!
Стюарт Россітер

1
@SarthakMittal: Значення не буде скопійовано, якщо ви його фактично не використовуєте, якщо це вам цікаво.
Джон Скіт

76

Дозвольте мені пояснити трохи про один випадок, коли вам доведеться скористатись фіналом, про який Джон уже згадував:

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

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 всередині методу, і він змінює значення з - люди очікують, що зміни будуть видимі в методі, оскільки вони бачать лише одну змінну. Щоб уникнути цієї плутанини, вона повинна бути остаточною.
Майкл Боргвардт

Це не робить копію, це просто посилання на будь-який об'єкт, на який посилався.
vickirk

1
@vickirk: переконайтеся, що вона робить копію - довідки, у випадку посилань.
Майкл Боргвардт

Btw припускаючи, що у нас немає анонімних класів, що посилаються на ці змінні, чи знаєте ви, чи є різниця між finalпараметром функції та не кінцевим параметром функції в очах HotSpot?
Pacerier

25

Я використовую остаточний весь час за параметрами.

Це так додає? Не зовсім.

Я б його відключив? Ні.

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

Я хотів би, щоб це було за замовчуванням у майбутній версії Java. Пропуск за цінністю / довідковою справою обтяжує дуже багато молодих програмістів.

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


4
Я збирався також запропонувати це, що в наступних версіях цей фінал буде за замовчуванням, і вам доведеться вказати "мутабельне" або краще ключове слово, яке задумано. Ось приємна стаття з цього приводу: lpar.ath0.com/2008/08/26/java-annoyance-final-parameters
Джефф Аксельрод

Минуло давно, але ви могли б навести приклад помилки, яку ви спіймали?
user949300

Дивіться, хто найбільше проголосував за відповідь. Це справді хороший приклад, коли змінна члена не встановлена, а замість цього параметр мутований.
Fortyrunner

18

Використання final у параметрі методу не має нічого спільного з тим, що відбувається з аргументом на стороні виклику. Мається на увазі лише позначити його як те, що не змінюється всередині цього методу. Коли я намагаюся прийняти більш функціональний стиль програмування, я якось бачу цінність у цьому.


2
Точно це не частина функціонального інтерфейсу, а лише реалізація. Заплутано те, що java дозволяє (але dirsregards) final параметри в інтерфейсах / абстрактних методах декларацій.
Бені Чернявський-Паскін

8

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

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

Я, звичайно, хотів би щось сильніше, як C / C ++ const.


Не впевнені, що IDE та посилання на інструменти застосовні для публікації в ОП або теми. А саме "остаточний" - це перевірка часу компіляції, щоб посилання не було змінено / заглушено. Далі, щоб реально застосовувати такі речі, дивіться відповідь про відсутність захисту членів дитини в остаточних посиланнях. Наприклад, будуючи API, використання IDE або інструменту не допоможе зовнішнім сторонам використовувати / розширити такий код.
Darrell Teague

4

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


1

Я ніколи не використовую фінал у списку параметрів, він просто додає безладу, як казали попередні респонденти. Також у програмі Eclipse ви можете встановити призначення параметрів, щоб генерувати помилку, тому використання фіналу у списку параметрів мені здається непотрібним. Цікаво, що коли я включив налаштування 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
"Проблема з остаточним полягає в тому, що вона лише примушує примусити посилання незмінною" - Неправда, сама Java запобігає цьому. Змінна, передана методу, не може змінити його посилання на цей метод.
Madbreaks

Будь ласка, досліджуйте перед публікацією ... stackoverflow.com/questions/40480/…
Darrell Teague

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

Або ти мене не розумієш, або ти помиляєшся. Якщо я передаю посилання на об'єкт методу, і цей метод перепризначає його, оригінальна посилання залишається недоторканою для (мене) абонента, коли метод завершує його виконання. Java суворо проходить через значення. І ви нахабно самосуд стверджуєте, що я не проводив жодних досліджень.
Madbreaks

Відмовляючись від того, що оп поцікавився, навіщо використовувати остаточний, і ви дали лише одну, невірну причину.
Madbreaks

0

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

Однак я, як правило, видаляю їх як зайві в кінці рефакторингу.


-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 ще існувати?

Здається, це має бути, тому що ми бачимо в основному, ми все ще називаємо метод showObjName (), і йому потрібно досягти obj . MyParam все ще використовує / досягає аргументу методу, навіть метод вже повертається!

Як Java дійсно цього домагається, це також генерувати копію - це приховане посилання на аргумент MyObj obj всередині об’єкта MyParam (але це не формальне поле в MyParam, щоб ми не могли його побачити)

Як ми називаємо "showObjName", вона використовуватиме це посилання, щоб отримати відповідне значення.

Але якщо ми не ставимо аргумент остаточним, що призводить до ситуації, ми можемо призначити нову пам'ять (об’єкт) аргументу MyObj obj .

Технічно сутичок взагалі немає! Якщо нам це дозволять, нижче буде викладена ситуація:

  1. Тепер у нас є прихована точка [MyObj obj] на [Пам'ять A в купі], зараз живемо в об'єкті MyParam.
  2. У нас також є інший [MyObj obj], який є аргументом до [пам'яті B в купі], тепер живе в цьому методі.

Ніяких зіткнень, але "ПОВЕРХНЕ !!" Тому що всі вони використовують одне і те ж "довідкове ім'я", яке "obj" .

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

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