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

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

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

Well this rule is most frustrating when trying to close a resource in a finally block. If I instantiate the resource inside a try, but try to close it within the finally, I get this error. If I move the instantiation outside the try, I get another error stating that a it must be within a try.

Very frustrating.

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

Shivasubramanian A 08.01.2009 09:20

См. Также этот связанный вопрос C#: stackoverflow.com/questions/1542824/…

Raedwald 11.03.2014 17:06

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

rustyx 18.12.2015 00:11

Аналогичный вопрос для C# (несмотря на название - смотрите ответы): Опасны ли неинициализированные переменные C#?

Peter Mortensen 17.01.2021 05:15
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
107
5
70 054
15

Ответы 15

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

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

Greg Hewgill 06.01.2009 10:13

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

mmx 06.01.2009 10:15

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

Greg Hewgill 06.01.2009 10:16

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

mmx 06.01.2009 10:29

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

Adeel Ansari 06.01.2009 10:32

Нет, переменные-члены будут инициализированы компилятором как на Java, так и на C#, если вы ничего не укажете.

mmx 06.01.2009 11:37

Re "стало немного быстрее объявлять неинициализированные переменные": Разве среда выполнения (JVM) все равно не делает этого (не риторический вопрос)?

Peter Mortensen 17.01.2021 05:17

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

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

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

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

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

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 автоматически связываются с предыдущим исключением? Я не помню. Даже в этом случае у вас было бы дополнительное исключение на пути к настоящему.)

почему бы просто не иметь if (so! = null) ... в блоке finally?

izb 06.01.2009 12:42

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

Chii 06.01.2009 12:52

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

Juha Syrjälä 06.01.2009 14:00

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

Rob Kennedy 06.01.2009 19:09

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

Sarel Botha 28.01.2009 19:58

Вы всегда можете просто использовать дополнительный блок try-catch для всего этого.

Outlaw Programmer 30.01.2009 18:31

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

Rob Kennedy 30.01.2009 20:03

Во-вторых, НЕ было бы лучше иметь вызов конструктора внутри этого блока try. Это блок try-finally, который НИЧЕГО не делает для обработки исключений. Если возникнет исключение, мы вызовем CleanUp для неинициализированной или нулевой переменной и вернемся к тому же состоянию, что и в моем первом блоке кода.

Rob Kennedy 30.01.2009 20:05

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

Re «Eclipse даже выдает предупреждения»: Разве это не компилятор (не специфичный для Eclipse)? - «Доступ к неинициализированной локальной переменной приведет к ошибке времени компиляции.»

Peter Mortensen 07.01.2021 19:31

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

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

Более того, в приведенном ниже примере внутри конструкции 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
  }
}

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

SMBiggs 29.03.2013 00:38

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

Atom 22.06.2016 19:57

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

По этой причине JVM не позволяет использовать локальную переменную без ее инициализации.

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

gshauger 25.03.2010 18:54

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

mamills 11.04.2013 20:57

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

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

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

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

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

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

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

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

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

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

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

Bill K 13.01.2012 00:35

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

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

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

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

meriton 31.12.2014 01:17

Справа скрыто.Если кто-то решит создать локальную переменную с тем же именем, что и экземпляр, из-за этого ограничения локальная переменная будет инициализирована специально выбранным значением (отличным от значения переменной экземпляра)

Mitra 31.12.2014 14:19

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

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

Например, рассмотрим следующий простой код ... (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);

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

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

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

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

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

Re «некоторые методы никогда не будут вызваны, потому что вызов метода может быть условным, так зачем тратить время на то, чтобы присвоить им значение по умолчанию»: Но нет можно инициализировать локальные переменные (в стеке) до вызова метода (например, нет никакого способа предсказать, какое место в памяти нужно изменить), поэтому я считаю этот момент спорным.

Peter Mortensen 17.01.2021 06:27

не стесняйтесь редактировать, у меня жирным шрифтом если я не ошибаюсь это была просто моя идея

Abhinav Chauhan 17.01.2021 08:03

Другие вопросы по теме