Java实例变量与局部变量 - 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<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()会发生什么?崩溃,你所有的头发掉下来,死亡,空间和时间崩溃成一个奇异点,宇宙被吞噬了?可能至少其中两个。线程现在可能已经超出您的头了,但希望我们现在可以引导您远离做坏事,因此当您使用线程时,这些不良做法不会导致您真正的头痛。

您是否注意到调用这些方法时必须非常小心?他们彼此改写,可能随机共享内存,您必须记住它在内部工作的细节,以使其在外部工作,改变顺序的产生在接下来的几行中产生很大的变化,而且它只能在单线程情况下工作。这样做会产生非常脆弱的代码,只要您触摸它,它就会崩溃。我展示的这些做法直接导致了代码脆弱。

虽然这看起来像封装,但正好相反,因为调用者必须知道如何编写它的技术细节。调用者必须以非常特殊的方式编写代码才能使其代码正常工作,并且他们不知道代码的技术细节就无法做到这一点。这通常被称为“泄漏抽象”,因为该类被认为是将技术细节隐藏在抽象/接口后面,但是技术细节泄漏出去,迫使调用者改变其行为。每个解决方案都有一定程度的泄漏性,但是使用上述任何一种这样的技术,无论您想解决什么问题,都可以保证,如果您应用它们,则将非常泄漏。现在让我们来看一下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;
    }
}

答案很长,但很重要。

Java中的“ <<”运算符 - java

最喜欢的语句来自Java的Character类:(1 << Character.PARAGRAPH_SEPARATOR)) >> type PARAGRAPH_SEPARATOR是字节,type是整数。这句话中的操作员,他们做什么?如何以及在哪里可以使用这些运算符?这是oracles java.lang.Character文档。该类中…

JAVA:字节码和二进制有什么区别? - java

java字节代码(已编译的语言,也称为目标代码)与机器代码(当前计算机的本机代码)之间有什么区别?我读过一些书,他们将字节码称为二进制指令,但我不知道为什么。 参考方案 字节码是独立于平台的,在Windows中运行的编译器编译的字节码仍将在linux / unix / mac中运行。机器代码是特定于平台的,如果在Windows x86中编译,则它将仅在Win…

java:继承 - java

有哪些替代继承的方法? java大神给出的解决方案 有效的Java:偏重于继承而不是继承。 (这实际上也来自“四人帮”)。他提出的理由是,如果扩展类未明确设计为继承,则继承会引起很多不正常的副作用。例如,对super.someMethod()的任何调用都可以引导您通过未知代码的意外路径。取而代之的是,持有对本来应该扩展的类的引用,然后委托给它。这是与Eric…

Java:BigInteger,如何通过OutputStream编写它 - java

我想将BigInteger写入文件。做这个的最好方式是什么。当然,我想从输入流中读取(使用程序,而不是人工)。我必须使用ObjectOutputStream还是有更好的方法?目的是使用尽可能少的字节。谢谢马丁 参考方案 Java序列化(ObjectOutputStream / ObjectInputStream)是将对象序列化为八位字节序列的一种通用方法。但…

Java-如何将此字符串转换为日期? - java

我从服务器收到此消息,我不明白T和Z的含义,2012-08-24T09:59:59Z将此字符串转换为Date对象的正确SimpleDateFormat模式是什么? java大神给出的解决方案 这是ISO 8601标准。您可以使用SimpleDateFormat simpleFormat = new SimpleDateFormat("yyyy-MM…