Почему локальные переменные не инициализируются в Java? - PullRequest
93 голосов
/ 06 января 2009

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

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

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

Очень расстраивает.

Ответы [ 14 ]

55 голосов
/ 06 января 2009

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

22 голосов
/ 06 января 2009

Похоже, что "проблема", на которую вы ссылаетесь , описывает эту ситуацию:

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 существует only для защиты экземпляра SomeObject, чтобы обеспечить его очистку независимо от того, что еще произойдет. Если есть другие вещи, которые нужно запустить, но они не связаны с тем, был ли экземпляр SomeObject выделен для свойства, тогда они должны войти в другой try - finally блок, вероятно, тот, который оборачивает тот, который я показал.

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

Если бы локальные переменные имели значения по умолчанию, то so в первом примере было бы null. Это действительно не решило бы ничего. Вместо того, чтобы получить ошибку времени компиляции в блоке finally, у вас будет скрываться NullPointerException, которая может скрыть любое другое исключение, которое может произойти в разделе «Сделай здесь что-нибудь» код. (Или исключения в секциях finally автоматически соединяются с предыдущим исключением? Я не помню. Несмотря на это, у вас будет дополнительное исключение в виде реального.)

12 голосов
/ 06 января 2009

Более того, в приведенном ниже примере исключение могло быть сгенерировано внутри конструкции 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
  }
}
10 голосов
/ 06 января 2009

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

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

8 голосов
/ 21 мая 2011

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

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

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

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

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

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

3 голосов
/ 25 марта 2010

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

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

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

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

3 голосов
/ 06 января 2009

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

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

3 голосов
/ 06 января 2009

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

1 голос
/ 12 сентября 2018

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

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

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 , этот написанный код будет работать правильно. Однако что, если мы забыли выражение else?

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

Enter grade
43
Your grade is

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

Это правда, что блоки try-catch-finally могут немного запутаться (но на самом деле это не catch-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];, как только вы инициализируете этот массив, объект существует в памяти в данном месте. Давайте на минутку предположим, что значений по умолчанию нет, но вместо этого начальное значение - это любая серия из 1 и 0, которая в данный момент находится в этой ячейке памяти. Это может привести к недетерминированному поведению в ряде случаев.

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

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. Таким образом, хотя значения по умолчанию для локальных переменных в лучшем случае могут рассматриваться как необязательные, наличие значений по умолчанию для переменных экземпляра ближе к необходимость .

0 голосов
/ 30 августа 2017

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

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