Java 实例变量与局部变量

2022-09-01 22:00:22

我在高中的第一堂编程课上。我们正在完成第一学期的期末项目。

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

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


答案 1

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

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

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;

在第二个例子中,我们保存了第三行的红色位置,但同样的事情发生了!?我们为什么失去他们?!因为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;
    }
}

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


答案 2

当实例变量是类的核心概念时,请使用实例变量。如果要迭代、递归或执行某些处理,请使用局部变量。

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

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

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