Почему локальные переменные не инициализируются в Java?


104

Была ли какая-то причина, по которой разработчики Java считали, что локальным переменным не следует давать значение по умолчанию? Серьезно, если переменным экземпляра можно присвоить значение по умолчанию, то почему мы не можем сделать то же самое для локальных переменных?

И это также приводит к проблемам, как описано в этом комментарии к сообщению в блоге :

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

Очень неприятно.



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 (so! = null) ... в блоке finally?
izb

это по-прежнему вызовет предупреждение / ошибку компилятора - я не думаю, что компилятор понимает это, если проверяется (но я просто делаю это из памяти, а не проверяю).
Chii

6
Я бы поставил просто SomeObject so = null перед попыткой поставить нулевую проверку в предложение finally. Таким образом не будет предупреждений компилятора.
Юха Сюрьяля,

Зачем все усложнять? Напишите таким образом блок try-finally, и вы ЗНАЕТЕ, что переменная имеет допустимое значение. Никакой нулевой проверки не требуется.
Роб Кеннеди,

1
Роб, ваш пример «new SomeObject ()» прост, и здесь не должно генерироваться никаких исключений, но если вызов может генерировать исключения, тогда было бы лучше, чтобы это происходило внутри блока try, чтобы его можно было обработать.
Сарел Бота

12

Более того, в приведенном ниже примере внутри конструкции SomeObject могло возникнуть исключение, и в этом случае переменная so будет иметь значение null, а вызов 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
Что бы ты предпочел сделать?
Electric Monk,

2
Ага, некрасиво. Ага, я тоже этим занимаюсь.
SMBiggs

@ElectricMonk Какая форма, по вашему мнению, лучше, та, которую вы показали, или показанная здесь в методе getContents (..): javapractices.com/topic/TopicAction.do?Id=126
Atom

11

Обратите внимание, что последние переменные экземпляра / члена не инициализируются по умолчанию. Потому что они окончательные и впоследствии не могут быть изменены в программе. Это причина того, что Java не дает им значения по умолчанию и заставляет программиста инициализировать его.

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


9

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

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

Вы никогда не должны инициализировать свою переменную нулем или нулем на первом проходе (когда вы ее впервые кодируете). Либо присвойте ему фактическое значение, либо не присваивайте его вообще, потому что в противном случае Java может сказать вам, когда вы действительно облажаетесь. Возьмите ответ Electric Monk в качестве отличного примера. В первом случае на самом деле удивительно полезно, когда он сообщает вам, что если try () завершится неудачно из-за того, что конструктор SomeObject сгенерировал исключение, вы получите NPE в файле finally. Если конструктор не может создать исключение, его не должно быть в попытке.

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

Вдобавок ко всему, не лучше ли явно сказать «int size = 0», а не «int size», и заставить следующего программиста понять, что вы хотите, чтобы он был равен нулю?

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


1
Да, и есть другие, где его более или менее необходимо инициализировать нулевым значением из-за того, как течет код - я не должен был говорить, что «никогда» не обновлял ответ, чтобы отразить это.
Bill K

4

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


1
Возможно, компилятор мог определить, всегда ли вы присваиваете переменную, прежде чем делать что-либо с ней, и подавить автоматическое присвоение значения по умолчанию в таком случае. Если компилятор не может определить, происходит ли присвоение до доступа, он сгенерирует назначение по умолчанию.
Грег Хьюгилл, 06

2
Да, но кто-то может возразить, что это позволяет программисту узнать, если он или она по ошибке оставили переменную неинициализированной.
Мехрдад Афшари, 06

1
Компилятор тоже может это сделать. :) Лично я бы предпочел, чтобы компилятор рассматривал неинициализированную переменную как ошибку. Значит, я мог где-то ошибиться.
Грег Хьюгилл, 06

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

Предупреждает ли он и о переменных-членах?
Adeel Ansari 06

4

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

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

Тем не менее, я мог полностью отказаться от требования, чтобы переменные экземпляра всегда явно инициализировались; ошибка может возникнуть в любом конструкторе, в котором результат допускает неинициализированную переменную экземпляра (например, не инициализированную при объявлении и не в конструкторе). Но это не решение Gosling et. др., сняли в начале 90-х, так вот и мы. (И я не говорю, что они сделали неправильный звонок.)

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


«Тем не менее, я мог бы полностью отказаться от требования, чтобы переменные экземпляра всегда явно инициализировались ...», что, FWIW, является направлением, которое они выбрали в TypeScript.
TJ Crowder

3

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

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


3

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

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

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);

Когда все сказано и сделано, предполагая, что компилятор присвоил letterGrade значение по умолчанию '\ 0' , этот код в том виде, в каком он написан, будет работать правильно. Однако что, если мы забыли инструкцию else?

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

Enter grade
43
Your grade is

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

Это правда, что блоки try-catch-finally могут стать немного беспорядочными (но на самом деле это не уловка-22, как кажется в цитате), когда, например, объект выдает проверенное исключение в своем конструкторе, но для одного по какой-либо причине, что-то должно быть сделано с этим объектом в конце блока в finally. Прекрасный пример этого - работа с ресурсами, которые необходимо закрыть.

Один из способов справиться с этим в прошлом мог быть таким ...

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, этот блок finally больше не нужен с использованием try-with-resources, например.

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

Тем не менее (как следует из названия) это работает только с ресурсами.

И хотя первый пример немного неприятен, это, возможно, больше говорит о способе реализации try-catch-finally или этих классов, чем о локальных переменных и о том, как они реализованы.

Это правда, что поля инициализируются значением по умолчанию, но это немного другое. Когда вы говорите, например, int[] arr = new int[10];как только вы инициализировали этот массив, объект существует в памяти в заданном месте. Предположим на мгновение, что нет значений по умолчанию, но вместо этого начальное значение - это любая последовательность единиц и нулей, которая в данный момент находится в этой ячейке памяти. В ряде случаев это может привести к недетерминированному поведению.

Предположим, у нас есть ...

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 должен указывать на строку (или иметь значение NULL). Однако, если начальным значением является любая последовательность нулей и единиц, встречающаяся в этой ячейке памяти, не только нет гарантии, что вы будете каждый раз получать одни и те же результаты, но также нет гарантии, что объект s [0] указывает to (при условии, что он указывает на что-нибудь значимое) даже является String (возможно, это Rabbit,: p )! Это отсутствие заботы о типе противоречит почти всему, что делает Java Java. Таким образом, хотя значения по умолчанию для локальных переменных можно рассматривать в лучшем случае как необязательные, наличие значений по умолчанию для переменных экземпляра ближе к необходимости .


1

Если я не ошибаюсь, другой причиной может быть

Предоставление значения по умолчанию для переменных-членов является частью загрузки класса

Загрузка класса - это вещь времени выполнения в java, это означает, что когда вы создаете объект, тогда класс загружается с загрузкой класса, только переменные-члены инициализируются значением по умолчанию JVM не требует времени, чтобы присвоить значение по умолчанию вашим локальным переменным, потому что некоторые методы никогда не будут вызывается, потому что вызов метода может быть условным, так зачем тратить время на то, чтобы присвоить им значение по умолчанию и снизить производительность, если эти значения по умолчанию никогда не будут использоваться.


0

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


0

Локальные переменные хранятся в стеке, но переменные экземпляра хранятся в куче, поэтому есть некоторые шансы, что предыдущее значение в стеке будет прочитано вместо значения по умолчанию, как это происходит в куче. По этой причине jvm не позволяет использовать локальную переменную без ее инициализации.


2
Совершенно неправильно ... все непримитивы Java хранятся в куче, независимо от того, когда и как они созданы
gshauger

До Java 7 переменные экземпляра хранятся в куче, а локальные переменные находятся в стеке. Однако любой объект, на который ссылается локальная переменная, будет найден в куче. Начиная с Java 7, «Java Hotspot Server Compiler» может выполнять «escape-анализ» и решать размещать некоторые объекты в стеке, а не в куче.
mamills

0

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


-1

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


-2

Я мог придумать следующие 2 причины

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

Поля не могут быть переопределены. В лучшем случае их можно скрыть, и я не вижу, как скрытие может помешать проверке инициализации?
meriton

Справа скрыто. Если кто-то решит создать локальную переменную с тем же именем, что и экземпляр, из-за этого ограничения локальная переменная будет инициализирована целенаправленно выбранным значением (отличным от значения переменной экземпляра)
Митра
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.