Java实例变量与局部变量

我是高中时的第一个编程课。 我们正在完成第一学期的项目。 这个项目只涉及一个类,但有很多方法。 我的问题是实例变量和局部变量的最佳实践。 看起来我几乎只使用实例变量进行编码会容易得多。 但我不确定这是不是我应该这样做,或者我是否应该更多地使用局部变量(我可能只需要让方法更多地考虑局部变量的值)。

我的理由也是因为很多时候我想要一个方法返回两个或三个值,但这当然是不可能的。 因此,简单地使用实例变量似乎更容易,因为它们在类中是通用的,所以不必担心。

如果我以一种令人困惑的方式写作,感谢和抱歉。

我没见过有人在讨论这个问题,所以我会提出更多的思考。 简短的回答/建议是不要使用实例变量而不是局部变量,因为您认为它们更容易返回值。 如果不适当地使用局部变量和实例变量,那么您将非常努力地使用代码。 你会产生一些非常难以追查的严重错误。 如果你想了解我的意思是严重的错误,以及可能看起来像什么。

让我们尝试仅使用实例变量,因为您建议写入函数。 我将创建一个非常简单的类:

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 indexes = new ArrayList(); 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 redPositions = idea.indexes; int redCount = idea.counter; idea.findColor( Color.GREEN ); // this causes red positions to be lost! (ie idea.indexes.clear() List greenPositions = idea.indexes; int greenCount = idea.counter; 

在第二个例子中,我们在第3行保存了红色位置,但同样的事情发生了!?为什么我们失去了它们?! 因为idea.indexes已被清除而不是已分配,因此一次只能使用一个答案。 在再次调用之前,您必须完全使用该结果。 再次调用方法后,结果将被清除,您将丢失所有内容。 为了解决这个问题,每次都必须分配一个新的结果,因此红色和绿色的答案是分开的。 那么让我们克隆我们的答案来创建新的东西副本:

 BadIdea idea = new BadIdea(); idea.findColor( Color.RED ); List redPositions = idea.indexes.clone(); int redCount = idea.counter; idea.findColor( Color.GREEN ); List greenPositions = idea.indexes.clone(); int greenCount = idea.counter; 

好的,最后我们有两个单独的结果。 红色和绿色的结果现在是分开的。 但是,我们必须知道很多关于BadIdea在项目工作之前如何在内部运作的事情我们不是吗? 每次我们调用它时,我们都需要记住克隆返回值,以确保我们的结果不被破坏。 为什么呼叫者被迫记住这些细节? 如果我们不必这样做会不会更容易?

还要注意调用者必须使用局部变量来记住结果,所以当你在BadIdea的方法中没有使用局部变量时,调用者必须使用它们来记住结果。 那么你真正完成了什么? 你真的只是把问题转移给调用者,迫使他们做更多事情。 你推送到调用者的工作并不是一个容易遵循的规则,因为规则有很多例外。

现在让我们尝试使用两种不同的方法。 注意我是如何“聪明”的,我重复使用那些相同的实例变量来“节省内存”并保持代码紧凑。 😉

 BadIdea idea = new BadIdea(); idea.findColor( Color.RED ); List redPositions = idea.indexes; int redCount = idea.counter; idea.findOppositeColors( Color.RED ); // this causes red positions to be lost again!! List greenPositions = idea.indexes; int greenCount = idea.counter; 

发生了同样的事! 该死,但我是如此“聪明”,节省内存,代码使用更少的资源! 这是使用实例变量的真正危险,因为调用方法现在依赖于顺序。 如果我改变方法调用的顺序,结果是不同的,即使我没有真正改变BadIdea的基础状态。 我没有改变地图的内容。 当我以不同的顺序调用方法时,为什么程序会产生不同的结果?

 idea.findColor( Color.RED ) idea.findOppositeColors( Color.RED ) 

产生与我交换这两种方法不同的结果:

 idea.findOppositeColors( Color.RED ) idea.findColor( Color.RED ) 

这些类型的错误实际上很难追踪,特别是当这些错误不是紧挨着彼此时。 您可以通过在这两行之间的任何位置添加新调用来完全破坏您的程序,并获得截然不同的结果。 当我们处理少量线路时,很容易发现错误。 但是,在较大的程序中,即使程序中的数据没有改变,您也可能浪费数天来重现它们。

这只关注单线程问题。 如果在multithreading情况下使用BadIdea,错误可能会变得非常奇怪。 如果同时调用findColors()和findOppositeColors()会发生什么? 崩溃,你所有的头发都掉了,死亡,空间和时间崩溃成一个奇点,宇宙吞没了? 可能至少有两个。 线程可能现在已经超出了你的头脑,但希望我们现在可以引导你远离做坏事,所以当你进入线程时,这些不良做法不会让你真正心痛。

您是否注意到在调用方法时您需要多么小心? 他们相互覆盖,他们可能随机共享内存,你必须记住它在内部如何工作的细节,使其在外部工作,改变事件被调用的顺序在下一行中产生非常大的变化,它只能在单线程情况下工作。 做这样的事情会产生非常脆弱的代码,只要你触摸它就会崩溃。 我展示的这些实践直接导致代码脆弱。

虽然这可能看起来像封装,但它恰恰相反,因为调用者必须知道如何编写它的技术细节 。 调用者必须以非常特殊的方式编写代码才能使代码正常工作,如果不了解代码的技术细节,他们就无法完成。 这通常被称为Leaky Abstraction,因为该类假设隐藏抽象/接口背后的技术细节,但技术细节泄漏,迫使调用者改变他们的行为。 每个解决方案都有一定程度的漏洞,但是使用上述任何一种技术都可以保证无论你试图解决什么问题,如果你应用它们都会非常漏洞。 那么现在让我们来看看GoodIdea吧。

让我们使用局部变量重写:

  public class GoodIdea { ... public List findColor( Color value ) { List results = new ArrayList(); for( int i = 0; i < map.length; i++ ) { if( map[index] == value ) { results.add( i ); } } return results; } public List findOppositeColors( Color value ) { List results = new ArrayList(); for( int i = 0; i < map.length; i++ ) { if( map[index] != value ) { results.add( i ); } } return results; } } 

这解决了我们上面讨论的每个问题。 我知道我没有跟踪计数器或返回它,但如果我这样做,我可以创建一个新类并返回而不是List。 有时我使用以下对象快速返回多个结果:

 public class Pair { public K first; public T second; public Pair( K first, T second ) { this.first = first; this.second = second; } } 

答案很长,但是一个非常重要的话题。

当它是您的类的核心概念时使用实例变量。 如果您正在迭代,递归或进行某些处理,那么请使用局部变量。

当您需要在相同位置使用两个(或更多)变量时,是时候创建一个具有这些属性的新类(以及设置它们的适当方法)。 这将使您的代码更清晰,并帮助您思考问题(每个类都是您词汇表中的新术语)。

当一个变量是核心概念时,它可以成为一个类。 例如真实世界的标识符:这些标识符可以表示为字符串,但通常,如果将它们封装到自己的对象中,它们会突然开始“吸引”function(validation,与其他对象的关联等)

另外(不完全相关)是对象一致性 – 对象能够确保它的状态是有意义的。 设置一个属性可能会改变另一个 它还可以更容易地将程序更改为以后的线程安全(如果需要)。

简短的故事:当且仅当一个变量需要被多个方法(或类外)访问时,才将其创建为实例变量。 如果只需要在本地使用它,在单个方法中,它必须是局部变量。 实例变量比局部变量更昂贵。
请记住:实例变量初始化为默认值,而局部变量则不是。

方法内部的局部变量总是首选,因为您希望保持每个变量的范围尽可能小。 但是,如果多个方法需要访问变量,那么它必须是一个实例变量。

局部变量更像是用于达到结果或动态计算某些内容的中间值。 实例变量更像是类的属性,例如您的年龄或名称。

声明变量的范围尽可能窄。 首先声明局部变量。 如果这还不够,请使用实例变量。 如果这还不够,请使用class(s​​tatic)变量。

我需要返回多个值,返回复合结构,如数组或对象。

简单方法:如果变量必须由多个方法共享,请使用实例变量,否则使用局部变量。

但是,良好的做法是使用尽可能多的局部变量。 为什么? 对于只有一个类的简单项目,没有区别。 对于包含大量课程的项目,存在很大差异。 实例变量指示您的类的状态。 类中的实例变量越多,该类可以拥有的状态就越多,然后,这个类就越复杂,维护类的难度就越大,或者项目可能出错的程度就越大。 因此,良好的做法是尽可能使用更多的局部变量来保持类的状态尽可能简单。

尝试从对象的角度考虑您的问题。 每个类代表不同类型的对象。 实例变量是类需要记住以便与其自身或与其他对象一起工作的数据片段。 局部变量应该只用于中间计算,一旦离开方法就不需要保存的数据。

尽量不要从方法中返回多个值。 如果你不能,在某些情况下你真的不能,那么我建议将它封装在一个类中。 在最后一种情况下,我建议更改类中的另一个变量(实例变量)。 实例变量方法的问题在于它增加了副作用 – 例如,您在程序中调用方法A并且它修改了一些实例变量(s)。 随着时间的推移,这会导致代码的复杂性增加,维护变得越来越难。

当我必须使用实例变量时,我尝试在类构造函数中进行final然后初始化,因此最小化了副作用。 这种编程风格(最小化应用程序中的状态更改)应该会产生更易于维护的更好的代码。

使用实例变量时

  1. 如果类中的2个函数需要相同的值,则使其成为实例变量OR
  2. 如果预期状态不会改变,则使其成为实例变量。 例如:不可变对象,DTO,LinkedList,具有最终变量OR的那些
  3. 如果是执行操作的基础数据。 例如:在PriorityQueue.java源代码中的arr []中的final
  4. 即使只使用了一次&& state会发生变化,如果它的参数列表应为空的函数只使用一次,则将其设为实例。 例如:HTTPCookie.java行:860 hashcode()函数使用’path variable’。

类似地,当这些条件都不匹配时使用局部变量,特别是在弹出堆栈后变量的角色将结束。 例如: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() { .... } 

随着代码的增加,第一种方法可能最大限度地减少变量范围,但很容易导致传递大量方法参数。 代码通常会更冗长,这会导致复杂性,因为重构所有这些方法。

使用更多的实例变量可以降低传递的大量方法参数的复杂性,并且当您为了清晰而经常重新组织方法时,可以为方法提供灵活性。 但它会创建更多必须维护的对象状态。 一般来说,建议是做前者而不是后者。

然而,通常,并且它可能取决于人,与第一种情况的数千个额外对象引用相比,可以更容易地管理状态复杂性。 当方法中的业务逻辑增加并且组织需要改变以保持秩序和清晰度时,人们可能会注意到这一点。

不仅。 当您重新组织方法以保持清晰度并在过程中进行大量方法参数更改时,最终会出现大量版本控制差异,这对于稳定的生产质量代码来说并不是那么好。 有一个平衡。 一种方式导致一种复杂性。 另一种方式导致另一种复杂性。

使用最适合您的方式。 你会发现这种平衡随着时间的推移。

我认为这位年轻的程序员对低维护代码有一些富有洞察力的第一印象。