Чому локальні змінні не ініціалізуються на Java?


103

Чи була якась причина, чому дизайнери Java вважали, що локальним змінним не слід задавати значення за замовчуванням? Серйозно, якщо змінним екземпляра можна надати значення за замовчуванням, то чому ми не можемо зробити те ж саме для локальних змінних?

І це також призводить до проблем, як пояснено в цьому коментарі до публікації в блозі :

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

Дуже засмучує.



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

Дивіться також цей пов'язаний C # питання: stackoverflow.com/questions/1542824 / ...
Raedwald

Просто - тому що компілятору легко відстежувати неініціалізовані локальні змінні. Якби це могло б збігатися з іншими змінними, було б. Компілятор просто намагається вам допомогти.
rustyx

Відповіді:


62

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


23

Здається, "проблема", з якою ви посилаєтесь, описує цю ситуацію:

SomeObject so;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  so.CleanUp(); // Compiler error here
}

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

// Do some work here ...
SomeObject so = new SomeObject();
try {
  so.DoUsefulThings();
} finally {
  so.CleanUp();
}

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

У будь-якому разі, ця друга версія коду - це правильний спосіб його написання. У першій версії повідомлення про помилку компілятора було правильним. soЗмінна може бути инициализирован. Зокрема, якщо SomeObjectконструктор виходить з ладу, soйого не буде ініціалізовано, і тому буде спроба викликати помилку so.CleanUp. Завжди заходьте в tryрозділ після придбання ресурсу, який finallyрозділ доопрацьовує.

Блок try- finallyпісля soініціалізації існує лише для захисту SomeObjectекземпляра, щоб гарантувати його очищення незалежно від того, що ще відбувається. Якщо є інші речі, які потрібно запустити, але вони не пов’язані з тим, чи SomeObjectбув екземпляр виділений властивістю, то вони повинні перейти в інший try - finallyблок, ймовірно, той, який обгортає той, який я показав.

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

Якби локальні змінні мали значення за замовчуванням, то soв першому прикладі це було б null. Це насправді нічого б не вирішило. Замість того, щоб отримати помилку часу компіляції в finallyблоці, ви хочете NullPointerExceptionтам ховатися, що може приховати будь-який інший виняток, що може статися в розділі "Виконати якусь роботу тут". (Або винятки в finallyрозділах автоматично пов'язуються з попереднім винятком? Я не пам’ятаю. Навіть так, у вас буде додатковий виняток на шляху до реального.)


2
чому б не просто мати if (так! = null) ... нарешті блок?
izb

це все ще спричинить попередження / помилку компілятора - я не думаю, що компілятор розуміє, що якщо перевірити (але я просто роблю це з пам'яті, не перевіряється).
Chii

6
Я поставив би просто SomeObject so = null перед тим, як спробувати поставити null check, щоб остаточно поставити пункт. Таким чином, попереджень компілятора не буде.
Juha Syrjälä

Навіщо ускладнювати речі? Напишіть блокування спробувати остаточно таким чином, і ви ЗНАЄте, що змінна має дійсне значення. Не потрібно перевіряти нуль.
Роб Кеннеді

1
Роб, ваш приклад "новий SomeObject ()" простий, і винятки там не повинні створюватися, але якщо виклик може створювати винятки, то краще, щоб він відбувався всередині пробного блоку, щоб його можна було обробляти.
Сарел Бота

12

Більше того, у наведеному нижче прикладі, можливо, було викинуто виключення всередині конструкції SomeObject, і в цьому випадку змінна 'так' буде нульовою, а виклик CleanUp видасть NullPointerException

SomeObject so;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  so.CleanUp(); // Compiler error here
}

Я схильний робити це:

SomeObject so = null;
try {
  // Do some work here ...
  so = new SomeObject();
  so.DoUsefulThings();
} finally {
  if (so != null) {
     so.CleanUp(); // safe
  }
}

12
Що б ви краще зробили?
Електромонах

2
Так, некрасиво. Так, це я і роблю.
SMBiggs

@ElectricMonk Яку форму ви вважаєте кращою, ту, яку ви показали, або ту, що показана тут у методі getContents (..): javapractices.com/topic/TopicAction.do?Id=126
Atom,

11

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

З іншого боку, змінні, що не є кінцевими членами, можуть бути змінені пізніше. Отже, компілятор не дозволяє їм залишатися неініціалізованими саме тому, що вони можуть бути змінені пізніше. Що стосується локальних змінних, то область локальних змінних значно вужча. Компілятор знає, коли його звикає. Отже, змушувати програміста ініціалізувати змінну має сенс.


9

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

Чому б не зробити зайвий крок? Зробіть крок назад - Ніхто не згадав, що "попередження" в цьому випадку - дуже гарна річ.

Ніколи не слід ініціалізувати свою змінну до нуля чи нуля під час першого проходу (коли ви вперше кодуєте її). Або призначте його фактичному значенню, або не привласнюйте його взагалі, тому що якщо ви цього не зробите, то Java може сказати вам, коли ви насправді закрутите Візьміть відповідь Electric Monk як чудовий приклад. У першому випадку насправді надзвичайно корисно те, що воно говорить вам про те, що якщо спроба () провалиться через те, що конструктор SomeObject кинув виняток, то ви нарешті отримаєте NPE. Якщо конструктор не може кинути виняток, він не повинен бути в спробі.

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

На додаток до цього, чи не краще чітко сказати "int size = 0", а не "розмір int", а наступний програміст зрозуміє, що ви маєте намір нульовий?

З іншого боку, я не можу придумати єдиної поважної причини, щоб компілятор ініціалізував усі неініціалізовані змінні до 0.


1
Так, і є інші, де це більш-менш потрібно ініціалізувати на нуль через те, як протікає код - я не повинен був сказати, що "ніколи" оновив відповідь, щоб це відобразити.
Білл К

4

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


1
Можливо, компілятор може визначити, чи завжди ви призначаєте змінну, перш ніж робити що-небудь з нею, і придушити автоматичне призначення значення за замовчуванням у такому випадку. Якщо компілятор не може визначити, чи присвоєння відбувається перед доступом, він створить призначення за замовчуванням.
Грег Х'югілл

2
Так, але можна стверджувати, що це дає можливість програмісту знати, чи залишив змінну неініціалізованою помилково.
Мехрдад Афшарі

1
Компілятор міг би це зробити і в будь-якому випадку. :) Особисто я вважаю за краще, щоб компілятор розглядав неініціалізовану змінну як помилку. Це означає, що я десь помилився.
Грег Хьюгілл

Я не хлопець на Java, але мені подобається спосіб керування C #. Різниця полягає в тому, що компілятору довелося винести попередження, яке може змусити вас отримати пару сотень попереджень за правильну програму;)
Мехрдад Афшарі

Це попереджає і для змінних членів?
Адель Ансарі

4

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

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

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

Але я не міг відстати від дефолтів локальних змінних. Так, ми не повинні покладатися на компілятори, щоб двічі перевірити нашу логіку, і ні, але це все ще зручно, коли компілятор виловив її. :-)


"З цього приводу я можу повністю відстати, вимагаючи, щоб змінні екземпляра завжди були явно ініціалізовані ...", що, FWIW, - це напрямок, який вони взяли в TypeScript.
TJ Crowder

3

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

У випадках, коли вам потрібна ініціалізація змінної, ви завжди можете це зробити самостійно, тому це не проблема.


3

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

Наприклад, розглянемо наступний простий код ... ( Зверніть увагу, для демонстраційних цілей місцевим змінним присвоюється значення за замовчуванням, як зазначено, якщо явно не ініціалізовано )

System.out.println("Enter grade");
int grade = new Scanner(System.in).nextInt(); //I won't bother with exception handling here, to cut down on lines.
char letterGrade; //let us assume the default value for a char is '\0'
if (grade >= 90)
    letterGrade = 'A';
else if (grade >= 80)
    letterGrade = 'B';
else if (grade >= 70)
    letterGrade = 'C';
else if (grade >= 60)
    letterGrade = 'D';
else
    letterGrade = 'F';
System.out.println("Your grade is " + letterGrade);

Коли все буде сказано і зроблено, якщо припустити, що компілятор призначив значення "\ 0" за замовчуванням для letterGrade , цей код як написаний буде працювати належним чином. Однак що робити, якщо ми забули інше твердження?

Тестовий запуск нашого коду може призвести до наступного

Enter grade
43
Your grade is

Цей результат, хоча і слід було очікувати, безумовно, не був наміром кодера. Дійсно, напевно, у переважній більшості випадків (або, принаймні, значної їх кількості) значення за замовчуванням не було б бажаним значенням, тому в переважній більшості випадків значення за замовчуванням призведе до помилки. Більше сенсу змусити кодер присвоїти початкове значення локальній змінній перед його використанням, оскільки горе налагодження, спричинене забуттям = 1в for(int i = 1; i < 10; i++)значно переважає зручність у тому, що не потрібно включати = 0в for(int i; i < 10; i++).

Це правда, що блоки try-catch-нарешті можуть стати трохи безладним (але це насправді не привід-22, як, мабуть, підказує цитата), коли, наприклад, об’єкт кидає перевірений виняток у своєму конструкторі, але для одного Причина чи інша, щось потрібно зробити цьому об’єкту в кінці блоку, нарешті. Прекрасним прикладом цього є робота з ресурсами, які необхідно закрити.

Один із способів вирішити це в минулому може бути таким ...

Scanner s = null; //declared and initialized to null outside the block. This gives us the needed scope, and an initial value.
try {
    s = new Scanner(new FileInputStream(new File("filename.txt")));
    int someInt = s.nextInt();
} catch (InputMismatchException e) {
    System.out.println("Some error message");
} catch (IOException e) {
    System.out.println("different error message"); 
} finally {
    if (s != null) //in case exception during initialization prevents assignment of new non-null value to s.
        s.close();
}

Однак, що стосується Java 7, цей остаточний блок більше не потрібен, використовуючи «пробні ресурси».

try (Scanner s = new Scanner(new FileInputStream(new File("filename.txt")))) {
...
...
} catch(IOException e) {
    System.out.println("different error message");
}

Однак, як випливає з назви, це працює лише з ресурсами.

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

Це правда, що поля ініціалізуються до значення за замовчуванням, але це трохи інакше. Якщо ви скажете, наприклад, int[] arr = new int[10];щойно ви ініціалізували цей масив, об’єкт існує в пам'яті в заданому місці. Припустимо на мить, що немає типових значень, але натомість початкове значення є будь-яким рядом 1s та 0s, що знаходиться в цьому місці пам'яті в даний момент. Це може призвести до недетермінованої поведінки у ряді випадків.

Припустимо, у нас ...

int[] arr = new int[10];
if(arr[0] == 0)
    System.out.println("Same.");
else
    System.out.println("Not same.");

Цілком можливо, що вони Same.можуть відображатися в одному циклі, а Not same.можуть бути відображені в іншому. Проблема може стати ще більш гострою, як тільки ви почнете говорити про змінні.

String[] s = new String[5];

Відповідно до визначення, кожен елемент s повинен вказувати на рядок (або нульовий). Однак, якщо початкове значення, яке б число серій 0 і 1 не траплялося в цьому місці пам'яті, не тільки не має гарантії, що ви будете отримувати однакові результати кожен раз, але також немає гарантії, що об'єкт s [0] балів до (припускаючи, що це вказує на щось значуще) навіть є Рядок (можливо, це Кролик,: p )! Ця відсутність турботи про тип летить перед обличчям майже всього, що робить Java Java. Отже, хоча значення за замовчуванням для локальних змінних в кращому випадку можна розглядати як необов’язкове, але значення за замовчуванням для змінних, наприклад, ближче до необхідності .


1

якщо я не помиляюся, могла бути ще одна причина

Надання за замовчуванням змінних членів є частиною завантаження класу

Завантаження класів - це річ часу запуску в java, коли ви створюєте об'єкт, тоді клас завантажується завантаженням класу, лише змінні учасники ініціалізуються зі значенням за замовчуванням. JVM не потребує часу, щоб дати значення за замовчуванням для ваших локальних змінних, оскільки деякі методи ніколи не будуть викликається тому, що виклик методу може бути умовним, тому навіщо потрібен час, щоб надати їм значення за замовчуванням та знизити продуктивність, якщо ці типові параметри ніколи не використовуватимуться.


0

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


0

Локальні змінні зберігаються в стеці, але змінні екземплярів зберігаються в купі, тому є певні шанси, що попереднє значення в стеку буде прочитане замість значення за замовчуванням, як це відбувається в купі. З цієї причини jvm не дозволяє використовувати локальну змінну без ініціалізації.


2
Виправдайте неправильно ... всі
непримітивні

Перед Java 7 змінні екземпляра зберігаються у купі, а локальні змінні знаходяться у стеку. Однак будь-який об’єкт, на який посилаються локальні змінні, знайдеться в купі. Як і в Java 7, "компілятор сервера Java Hotspot" може виконати "аналіз втечі" і вирішити виділити деякі об'єкти в стеку замість купи.
mamills

0

Змінна екземпляра матиме значення за замовчуванням, але локальні змінні не можуть мати значення за замовчуванням. Оскільки локальні змінні в основному є методами / поведінкою, його основна мета полягає в виконанні деяких операцій або розрахунків. Тому встановити значення за замовчуванням для локальних змінних не годиться. Інакше перевірити причини несподіваних відповідей дуже важко і трудомістко.


-1

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


-2

Я міг би подумати про дві причини

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

Поля неможливо змінити. Щонайбільше, їх можна приховати, і я не бачу, як приховування заважатиме чеку на ініціалізацію?
meriton

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