繁体   English   中英

Java 实例变量与局部变量

[英]Java instance variables vs. local variables

我在高中的第一个编程课上。 我们正在做我们第一学期末的项目。

本项目只涉及一个类,方法很多。 我的问题是关于实例变量和局部变量的最佳实践。 对我来说,几乎只使用实例变量进行编码似乎要容易得多。 但我不确定这是否是我应该做的,或者我是否应该更多地使用局部变量(我只需要让方法更多地接收局部变量的值)。

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

我还没有看到任何人讨论这个,所以我会提出更多的思考。 简短的回答/建议是不要仅仅因为您认为它们更容易返回值而在局部变量上使用实例变量。 如果您没有适当地使用局部变量和实例变量,您将很难使用您的代码。 你会产生一些很难追踪的严重错误。 如果您想了解我所说的严重错误是什么意思,以及它可能是什么样子,请继续阅读。

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

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() 会发生什么? 崩溃,你的头发都掉光了,死亡,时空坍缩成一个奇点,宇宙被吞没? 可能至少有两个。 线程现在可能在您的头顶之上,但希望我们现在可以引导您远离做坏事,这样当您进入线程时,这些不良做法不会让您真正心痛。

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

虽然这可能看起来像封装,但恰恰相反,因为调用者必须知道如何编写它的技术细节 调用者必须以非常特殊的方式编写他们的代码才能使他们的代码工作,如果不了解代码的技术细节,他们就无法做到。 这通常被称为泄漏抽象,因为该类假设隐藏抽象/接口背后的技术细节,但技术细节泄漏迫使调用者改变他们的行为。 每个解决方案都有一定程度的泄漏,但是使用上述任何一种技术,无论您尝试解决什么问题,都可以保证如果您应用它们,就会非常泄漏。 现在让我们来看看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 并且它修改了一些实例变量。 随着时间的推移,这会导致代码的复杂性增加,并且维护变得越来越困难。

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

通常变量应该有最小的范围。

不幸的是,为了构建具有最小变量范围的类,通常需要进行大量的方法参数传递。

但是如果你一直遵循这个建议,完美地最小化变量范围,你最终可能会因为所有必需的对象传入和传出方法而导致大量冗余和方法不灵活。

想象一个包含数千种方法的代码库,如下所示:

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() { 
    .... 
}

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

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

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

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

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

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

使用实例变量时

  1. 如果类中的两个函数需要相同的值,则将其设为实例变量
  2. 如果预期状态不会改变,请将其设为实例变量。 例如:不可变对象、 DTO 、LinkedList、带有最终变量的对象
  3. 如果它是执行操作的基础数据。 例如: PriorityQueue.java源代码文件中的arr[]中的final
  4. 即使它只被使用一次并且状态预计会改变,如果它只被一个参数列表应该为空的函数使用一次,那么让它成为一个实例。 例如: HTTPCookie.java 行:860 hashcode() 函数使用“路径变量”。

类似地,当这些条件都不匹配时,使用局部变量,特别是当堆栈弹出后变量的作用将结束时。 例如: Comparator.compare(o1, o2);

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM