Переменные экземпляра Java против локальных переменных
Я в своем первом классе программирования в средней школе. Мы заканчиваем первый семестр проекта. Этот проект включает только один класс, но много методов. Мой вопрос - лучшая практика с переменными экземпляра и локальными переменными. Кажется, мне было бы намного проще кодировать, используя почти только переменные экземпляра. Но я не уверен, должен ли я так поступать или мне следует больше использовать локальные переменные (мне просто нужно, чтобы методы принимали значения локальных переменных гораздо больше).
Я также объясняю это тем, что во многих случаях я хочу, чтобы метод возвращал два или три значения, но это, конечно, невозможно. Thus it just seems easier to simply use instance variables and never having to worry since they are universal in the class.
10 ответов
Я не видел, чтобы кто-нибудь обсуждал это, поэтому я добавлю больше пищи для размышлений. Краткий ответ / совет: не используйте переменные экземпляра над локальными переменными только потому, что вы думаете, что им легче возвращать значения. Вы очень сильно усложните работу с кодом, если не будете правильно использовать локальные переменные и переменные экземпляра. Вы получите серьезные ошибки, которые действительно трудно отследить. Если вы хотите понять, что я имею в виду под серьезными ошибками, и как это может выглядеть, читайте дальше.
Давайте попробуем использовать только переменные экземпляра, так как вы предлагаете писать в функции. Я создам очень простой класс:
public class BadIdea {
public Enum Color { GREEN, RED, BLUE, PURPLE };
public Color[] map = new Colors[] {
Color.GREEN,
Color.GREEN,
Color.RED,
Color.BLUE,
Color.PURPLE,
Color.RED,
Color.PURPLE };
List<Integer> indexes = new ArrayList<Integer>();
public int counter = 0;
public int index = 0;
public void findColor( Color value ) {
indexes.clear();
for( index = 0; index < map.length; index++ ) {
if( map[index] == value ) {
indexes.add( index );
counter++;
}
}
}
public void findOppositeColors( Color value ) {
indexes.clear();
for( index = 0; i < index < map.length; index++ ) {
if( map[index] != value ) {
indexes.add( index );
counter++;
}
}
}
}
Я знаю, что это глупая программа, но мы можем использовать ее, чтобы проиллюстрировать концепцию, согласно которой использование переменных экземпляра для подобных вещей является чрезвычайно плохой идеей. Самое важное, что вы обнаружите, - это то, что эти методы используют все переменные экземпляра, которые у нас есть. И он изменяет индексы, счетчик и индекс каждый раз, когда они вызываются. Первая проблема, которую вы обнаружите, заключается в том, что вызов этих методов один за другим может изменить ответы предыдущих запусков. Так, например, если вы написали следующий код:
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
idea.findColor( Color.GREEN ); // whoops we just lost the results from finding all Color.RED
Поскольку findColor использует переменные экземпляра для отслеживания возвращаемых значений, мы можем возвращать только один результат за раз. Давайте попробуем сохранить ссылку на эти результаты, прежде чем мы вызовем ее снова:
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes;
int redCount = idea.counter;
idea.findColor( Color.GREEN ); // this causes red positions to be lost! (i.e. idea.indexes.clear()
List<Integer> greenPositions = idea.indexes;
int greenCount = idea.counter;
Во втором примере мы сохранили красные позиции на 3-й строке, но произошло то же самое! Почему мы их потеряли?! Потому что idea.indexes был очищен, а не распределен, поэтому за один раз может использоваться только один ответ. Вы должны полностью закончить использование этого результата, прежде чем вызывать его снова. После повторного вызова метода результаты очищаются, и вы теряете все. Чтобы это исправить, вам нужно будет каждый раз назначать новый результат, чтобы красный и зеленый ответы были отдельными. Итак, давайте клонируем наши ответы для создания новых копий вещей:
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes.clone();
int redCount = idea.counter;
idea.findColor( Color.GREEN );
List<Integer> greenPositions = idea.indexes.clone();
int greenCount = idea.counter;
Хорошо, наконец, у нас есть два отдельных результата. Результаты красного и зеленого теперь раздельные. Но мы должны были много знать о том, как BadIdea работала внутри, прежде чем программа работала, не так ли? Мы должны помнить, чтобы клонировать возвраты каждый раз, когда мы вызывали его, чтобы убедиться, что наши результаты не были засорены. Почему звонящий вынужден помнить эти детали? Разве не было бы легче, если бы нам не пришлось это делать?
Также обратите внимание, что вызывающий должен использовать локальные переменные для запоминания результатов, поэтому, хотя вы не использовали локальные переменные в методах BadIdea, вызывающий должен использовать их для запоминания результатов. Так чего же вы действительно добились? Вы действительно просто перенесли проблему на вызывающую сторону, заставляя их делать больше. И работа, которую вы выдвинули на вызывающего, не является простым правилом, потому что есть несколько исключений из этого правила.
Теперь давайте попробуем сделать это двумя разными методами. Обратите внимание, как я был "умным", и я использовал те же самые переменные экземпляра, чтобы "сохранить память", и сохранил код компактным.;-)
BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
List<Integer> redPositions = idea.indexes;
int redCount = idea.counter;
idea.findOppositeColors( Color.RED ); // this causes red positions to be lost again!!
List<Integer> greenPositions = idea.indexes;
int greenCount = idea.counter;
То же самое случилось! Блин, но я был таким "умным" и экономил память, а код использует меньше ресурсов!!! Это реальная опасность использования переменных экземпляра, так как вызов методов теперь зависит от порядка. Если я изменю порядок вызовов методов, результаты будут другими, даже если я не изменил базовое состояние BadIdea. Я не менял содержимое карты. Почему программа дает разные результаты, когда я вызываю методы в другом порядке?
idea.findColor( Color.RED )
idea.findOppositeColors( Color.RED )
Результат будет другим, чем если бы я поменял местами эти два метода:
idea.findOppositeColors( Color.RED )
idea.findColor( Color.RED )
Эти типы ошибок действительно трудно отследить, особенно когда эти строки не находятся рядом друг с другом. Вы можете полностью сломать свою программу, просто добавив новый вызов в любом месте между этими двумя строками, и получите совершенно разные результаты. Конечно, когда мы имеем дело с небольшим количеством строк, легко обнаружить ошибки. Но в более крупной программе вы можете тратить дни, пытаясь воспроизвести их, даже если данные в программе не изменились.
И это только смотрит на однопоточные проблемы. Если BadIdea использовался в многопоточной ситуации, ошибки могут быть очень странными. Что произойдет, если findColors() и findOppositeColors() будут вызваны одновременно? Крах, все твои волосы выпадают, Смерть, пространство и время рушатся в сингулярность, и вселенная глотает? Вероятно, по крайней мере, два из них. Потоки, вероятно, сейчас у вас над головой, но, надеюсь, мы сможем отвлечь вас от совершения плохих поступков сейчас, поэтому, когда вы попадаете в потоки, эти плохие практики не вызывают у вас настоящей боли в сердце.
Заметили ли вы, как осторожно вы должны быть при вызове методов? Они перезаписывали друг друга, они разделяли память, возможно, случайным образом, вы должны были помнить детали того, как она работала внутри, чтобы заставить ее работать снаружи, изменяя порядок, в котором вещи назывались, производили очень большие изменения в следующих строках вниз, и это только может работать только в одной ситуации потока. Подобные действия приведут к созданию действительно хрупкого кода, который, кажется, разваливается, когда вы к нему прикасаетесь. Эти практики, которые я показал, способствовали тому, что код был хрупким.
Хотя это может выглядеть как инкапсуляция, это полная противоположность, потому что технические детали того, как вы написали это, должны быть известны вызывающей стороне. Вызывающий должен написать свой код очень особым образом, чтобы заставить его работать, и они не могут сделать это, не зная о технических деталях вашего кода. Это часто называют Leaky Abstraction, потому что предполагается, что класс скрывает технические детали за абстракцией / интерфейсом, но технические детали просачиваются, заставляя вызывающего пользователя изменить свое поведение. Каждое решение имеет некоторую степень утечки, но использование любого из вышеперечисленных методов, подобных этим, гарантирует, что независимо от того, какую проблему вы пытаетесь решить, оно будет ужасно протекающим, если вы его примените. Итак, давайте посмотрим на GoodIdea сейчас.
Давайте перепишем, используя локальные переменные:
public class GoodIdea {
...
public List<Integer> findColor( Color value ) {
List<Integer> results = new ArrayList<Integer>();
for( int i = 0; i < map.length; i++ ) {
if( map[index] == value ) {
results.add( i );
}
}
return results;
}
public List<Integer> findOppositeColors( Color value ) {
List<Integer> results = new ArrayList<Integer>();
for( int i = 0; i < map.length; i++ ) {
if( map[index] != value ) {
results.add( i );
}
}
return results;
}
}
Это решает все проблемы, которые мы обсуждали выше. Я знаю, что я не отслеживаю счетчик и не возвращаю его, но если я это сделаю, я могу создать новый класс и вернуть его вместо List. Иногда я использую следующий объект для быстрого возврата нескольких результатов:
public class Pair<K,T> {
public K first;
public T second;
public Pair( K first, T second ) {
this.first = first;
this.second = second;
}
}
Длинный ответ, но очень важная тема.
Используйте переменные экземпляра, когда это основная концепция вашего класса. Если вы выполняете итерацию, повторяете или выполняете какую-то обработку, используйте локальные переменные.
Когда вам нужно использовать две (или более) переменные в одних и тех же местах, самое время создать новый класс с этими атрибутами (и соответствующими средствами для их установки). Это сделает ваш код чище и поможет вам думать о проблемах (каждый класс - новый термин в вашем словаре).
Одна переменная может быть сделана классом, когда это основная концепция. Например, реальные идентификаторы: они могут быть представлены в виде строк, но часто, если вы инкапсулируете их в их собственный объект, они внезапно начинают "привлекать" функциональность (проверка, ассоциация с другими объектами и т. Д.)
Также (не полностью связана) согласованность объектов - объект может гарантировать, что его состояние имеет смысл. Установка одного свойства может изменить другое. Это также значительно облегчает изменение вашей программы, чтобы она была поточно-ориентированной позднее (если требуется).
Простой способ: если переменная должна использоваться несколькими методами, используйте переменную экземпляра, в противном случае используйте локальную переменную.
Однако рекомендуется использовать как можно больше локальных переменных. Зачем? Для вашего простого проекта с одним классом разницы нет. Для проекта, который включает в себя много классов, есть большая разница. Переменная экземпляра указывает состояние вашего класса. Чем больше переменных экземпляра в вашем классе, тем больше состояний может иметь этот класс, а затем, чем сложнее этот класс, тем сложнее поддерживается класс или тем более вероятным может быть ваш проект. Поэтому хорошей практикой является использование как можно большего числа локальных переменных, чтобы сохранить состояние класса как можно более простым.
Короткая история: если и только если переменная должна быть доступна более чем одному методу (или вне класса), создайте ее как переменные экземпляра. Если вам это нужно только локально, в одном методе это должна быть локальная переменная. Переменные экземпляра являются более дорогостоящими, чем локальные переменные.
Помните: переменные экземпляра инициализируются значениями по умолчанию, а локальные переменные - нет.
Локальные переменные, внутренние для методов, всегда предпочтительнее, так как вы хотите, чтобы область действия каждой переменной была как можно меньше. Но если для доступа к переменной требуется более одного метода, он должен быть переменной экземпляра.
Локальные переменные больше похожи на промежуточные значения, используемые для достижения результата или вычисления чего-либо на лету. Переменные экземпляра больше похожи на атрибуты класса, такие как ваш возраст или имя.
Объявите переменные как можно более узкие. Сначала объявите локальные переменные. Если этого недостаточно, используйте переменные экземпляра. Если этого недостаточно, используйте переменные класса (статические).
Если вам нужно вернуть более одного значения, верните составную структуру, например, массив или объект.
Попытайтесь думать о своей проблеме с точки зрения объектов. Каждый класс представляет отдельный тип объекта. Переменные экземпляра - это фрагменты данных, которые необходимо запомнить классу для работы с самим собой или с другими объектами. Локальные переменные следует использовать только для промежуточных вычислений, данных, которые вам не нужно сохранять после выхода из метода.
Старайтесь не возвращать более одного значения из ваших методов. Если вы не можете, а в некоторых случаях вы действительно не можете, то я бы рекомендовал инкапсулировать это в классе. Просто в последнем случае я бы порекомендовал изменить другую переменную внутри вашего класса (переменную экземпляра). Проблема с подходом переменных экземпляра состоит в том, что он увеличивает побочные эффекты - например, вы вызываете метод A в своей программе, и он изменяет некоторые переменные экземпляра. Со временем это приводит к увеличению сложности вашего кода, и обслуживание становится все сложнее и сложнее.
Когда мне нужно использовать переменные экземпляра, я пытаюсь сделать потом final и затем инициализировать в конструкторах классов, чтобы побочные эффекты были сведены к минимуму. Этот стиль программирования (минимизация изменений состояния в вашем приложении) должен привести к лучшему коду, который легче поддерживать.
Используйте переменную экземпляра, когда
- Если 2 функции в классе нуждаются в одинаковом значении, сделайте его переменной экземпляра ИЛИ
- Если состояние не изменится, сделайте его переменной экземпляра. Например: неизменяемый объект, DTO, LinkedList, объекты с конечными переменными ИЛИ
- Если это базовые данные, над которыми выполняются действия. например: final в arr[] в исходном коде PriorityQueue.java ИЛИ
- Даже если он используется только один раз, когда ожидается изменение && состояния, сделайте его экземпляром, если он используется только один раз функцией, список параметров которой должен быть пустым. Например: HTTPCookie.java Строка: 860 Функция hashcode() использует "переменную пути".
Точно так же используйте локальную переменную, когда ни одно из этих условий не совпадает, в частности, роль переменной заканчивается после удаления стека. например: Comparator.compare(o1, o2);
Обычно переменные должны иметь минимальную область видимости.
К сожалению, для создания классов с минимальной областью действия переменных часто требуется много передачи параметров метода.
Но если вы будете следовать этому совету все время, полностью сводя к минимуму область видимости переменных, вы можете получить большую избыточность и негибкость методов со всеми необходимыми объектами, передаваемыми в и из методов.
Представьте себе кодовую базу с тысячами таких методов:
private ClassThatHoldsReturnInfo foo(OneReallyBigClassThatHoldsCertainThings big,
AnotherClassThatDoesLittle little) {
LocalClassObjectJustUsedHere here;
...
}
private ClassThatHoldsReturnInfo bar(OneMediumSizedClassThatHoldsCertainThings medium,
AnotherClassThatDoesLittle little) {
...
}
И, с другой стороны, представьте кодовую базу с множеством переменных экземпляра, например:
private OneReallyBigClassThatHoldsCertainThings big;
private OneMediumSizedClassThatHoldsCertainThings medium;
private AnotherClassThatDoesLittle little;
private ClassThatHoldsReturnInfo ret;
private void foo() {
LocalClassObjectJustUsedHere here;
....
}
private void bar() {
....
}
По мере увеличения кода первый способ может минимизировать переменную область видимости, но может легко привести к передаче большого количества параметров метода. Код, как правило, будет более подробным, и это может привести к усложнению, так как все эти методы рефакторизируются.
Использование большего количества переменных экземпляра может снизить сложность множества передаваемых параметров методов и может обеспечить гибкость методов, когда вы часто реорганизуете методы для ясности. Но это создает больше состояния объекта, которое вы должны поддерживать. Обычно совет состоит в том, чтобы делать первое и воздерживаться от второго.
Однако очень часто, и это может зависеть от человека, можно легче управлять сложностью состояний по сравнению с тысячами дополнительных ссылок на объекты первого случая. Это можно заметить, когда бизнес-логика в методах увеличивается, и организация должна измениться, чтобы сохранить порядок и ясность.
Не только это. Когда вы реорганизуете свои методы, чтобы сохранить ясность и вносить множество изменений параметров метода в процесс, вы получите множество различий в управлении версиями, что не очень хорошо для стабильного кода качества производства. Есть баланс. Один путь вызывает один вид сложности. Другой способ вызывает другой вид сложности.
Используйте способ, который лучше всего подходит для вас. Вы найдете этот баланс со временем.
Я думаю, что у этого молодого программиста есть первые глубокие впечатления от кода, требующего минимального обслуживания.