Java实例变量与局部变量

use*_*251 23 java variables instance-variables

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

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

chu*_*ubs 34

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

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

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++;
         }
      }
   }
}
Run Code Online (Sandbox Code Playgroud)

这是一个我知道的愚蠢程序,但是我们可以用它来说明使用实例变量来做这样的事情的概念是一个非常糟糕的主意.你会发现最重要的是这些方法使用我们拥有的所有实例变量.它每次调用时都会修改索引,计数器和索引.您将发现的第一个问题是,一个接一个地调用这些方法可以修改先前运行的答案.例如,如果您编写了以下代码:

BadIdea idea = new BadIdea();
idea.findColor( Color.RED );
idea.findColor( Color.GREEN );  // whoops we just lost the results from finding all Color.RED
Run Code Online (Sandbox Code Playgroud)

由于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;
Run Code Online (Sandbox Code Playgroud)

在第二个例子中,我们在第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;
Run Code Online (Sandbox Code Playgroud)

好吧最后我们有两个单独的结果.红色和绿色的结果现在是分开的.但是,我们必须了解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;
Run Code Online (Sandbox Code Playgroud)

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

idea.findColor( Color.RED )
idea.findOppositeColors( Color.RED )
Run Code Online (Sandbox Code Playgroud)

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

idea.findOppositeColors( Color.RED )
idea.findColor( Color.RED )
Run Code Online (Sandbox Code Playgroud)

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

这只关注单线程问题.如果在多线程情况下使用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;
   }
 }
Run Code Online (Sandbox Code Playgroud)

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

public class Pair<K,T> {
    public K first;
    public T second;

    public Pair( K first, T second ) {
       this.first = first;
       this.second = second;
    }
}
Run Code Online (Sandbox Code Playgroud)

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


Ste*_*hen 8

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

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

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

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


Mar*_*urz 3

简而言之:当且仅当一个变量需要由多个方法(或类外部)访问时,才将其创建为实例变量。如果您仅在本地单个方法中需要它,则它必须是局部变量。

实例变量比局部变量更昂贵。

请记住:实例变量被初始化为默认值,而局部变量则不然。