【问题标题】:State of an ArrayList in a recursive method递归方法中 ArrayList 的状态
【发布时间】:2014-08-03 22:39:13
【问题描述】:

我有一个这样的递归方法:

class MyClass {
ArrayList<ArrayList<MyNode> allSeriesOfNodes = new ArrayList<ArrayList<MyNode>();

public void myMethod(MyNode currNode, ArrayList<MyNode> nodes) {
    // make sure currNode and nodes aren't null, blah....
    nodes.add(currNode);
    for(MyNode n: otherNodeList) {
        for(listItem in n.getList()) {
            if(...) {
                myMethod(n, nodes);
        }
    }
if(currNode is a leaf and nodes is > 1) {
    allSeriesOfNodes.add(nodes);
}

调试它我注意到当堆栈帧弹出并且前一个堆栈帧恢复时,节点列表仍然具有 currNode 的值,尽管它弹出了。

nodeslist 和 currNode 一样是一个局部变量,递归应该记住每个堆栈帧的 nodelist 的状态。回到之前的stackframe,为什么不是nodelist A 而不是A, B (myMethod(B, nodes) was popped)。

【问题讨论】:

  • 你在哪里从nodes“弹出”值?
  • 我认为您的代码示例太不完整,我们无法回答。虽然我会指出 currNode NOT 是一个局部变量,而是一个参数,因此不是真正的 local ,因为它是从其他地方获得的。但是尝试生成一个可以运行的示例,生成您所询问的条件,并且足够短,我们可以追踪正在发生的事情。
  • 我认为ArrayList&lt;ArrayList&lt;MyNode&gt; allSeriesOfNodes = new ArrayList&lt;ArrayList&lt;MyNode&gt;();MyNode 之后缺少&gt;
  • @BrettOkken:没有弹出节点。如果我错了,请纠正我,但假设我们用 A 调用这个方法,然后用 B 调用 B。B 调用被弹出。返回到 A 堆栈帧,我将节点视为 A,B 而不是 A。我认为递归会记得回到堆栈帧 A 中停止的位置,节点是 A。就树而言,它看起来像 A 是根, B 是左孩子,而与 C 的另一个递归调用将使 C 成为右孩子
  • 同一个ArrayList 实例正在被传递。有关更完整的描述,请参阅下面的@GaryDrocella 答案。

标签: java recursion


【解决方案1】:

序幕

编辑:和题主聊天后,答案还是不清楚,所以我认为有必要把答案表达得更好。

确实,Java 中的一切都是pass-by-value,也就是说,栈帧上的值是不能改变的。问题是,当将对象作为参数传递给方法时,引用(内存地址)作为值传递给函数。由于引用只是一个值,因此无法更改引用本身,但这当然并不意味着您不能更改正在被引用的内存中对象的实例。

这可能会让初学者甚至有经验的 C/C++ 程序员感到困惑。 C/C++ 中的经典指针可以更改与指针关联的引用,并且您可以更改引用指向的对象的值。因此,我声明 Java 使用我所谓的准指针,准含义(“它是,但不是”)然后是指针,它是经典的 C 指针。

考虑以下 Non Immutable 类的示例(对象不变性很重要,可能会让您感到困惑)。 Non Immutable 基本上只是意味着对象的状态可以改变。

public static void main(String[] args) {
    NonImmutable ni = new NonImmutable(0);
    mystery1(ni);
    System.out.println(ni.value);
}

public static void mystery1(NonImmutable ni) {
    ni = new NonImmutable(7);
    System.out.println(ni.value);
}

public static class NonImmutable {
    public int value;

    public NonImmutable(int value) {
        this.value =  value;
    }
}

在上面的代码中sn-p。你会注意到输出是。

7
0

现在让我们假设当我们调用应用程序时,main 方法在地址 0x123456 的堆上创建了一个 NonImmutable 对象。当我们调用神秘1时,它真的是这样的

mystery1(0x123456) //passing the reference to Non Immutable instance as a value!

所以在函数内部我们有 ni = 0x123456,但请记住它只是引用的值,而不是经典的 C 指针。所以在神秘1中,我们先在堆上的另一个内存地址0x123abc创建另一个非不可变对象,然后我们将其设置为参数ni的值,因此我们有ni = 0x123abc。现在,让我们停下来想一想。由于在 java 中 everythingpass-by-value,因此设置 ni=0x123abc 与将其设置为该值(如果它是原始 int 数据类型)没有什么不同。因此传入的对象的值没有改变,因为内存地址只是一个值,与一些 int、double、float、boolean 等的值没有什么不同。

现在考虑下一个例子……

public static void main(String[] args) {
    NonImmutable ni = new NonImmutable(0);
    mystery2(ni);
    System.out.println(ni.value);
}

public static void mystery2(NonImmutable ni) {
    ni.value = 7;
    System.out.println(ni.value);
}

public static class NonImmutable {
    public int value;

    public NonImmutable(int value) {
        this.value =  value;
    }
}

您会注意到上面示例的输出将是。

7
7

我们将做与第一个示例相同的假设,即初始的 Non Immutable 对象在内存地址 0x123456 的堆上初始化。现在,当执行神秘 2 时,会发生一些完全不同的事情。在执行 ni.value 时,我们实际上是在执行以下操作

(0x123456).value = 7 // Set the value of instance variable Non Immutable object at memory address 0x123456 to 7

上面的代码确实会改变对象的内部状态。因此,当我们从神秘 2 中返回时,我们拥有指向将打印 7 的同一对象的指针。

现在我们了解了 Immutable 对象的工作原理,您可能会注意到传递 Objects 时有一些不寻常的情况,但上面的 准指针 似乎并不一致。嗯,它实际上是一致的,但是在面向对象语言中有一种不同的对象可以让你产生不一致的错觉,但不要被愚弄了。 Java中还有一些对象叫做不可变对象,比如这里指定的String对象(http://docs.oracle.com/javase/7/docs/api/java/lang/String.html)。

不可变对象只是意味着对象的内部状态不能改变。对,那是正确的。每次你做如下的事情

String foo = "foo";
String moo = foo + " bar";

moo 没有引用 foo,因为字符串 foo 是不可变的,因此连接操作在执行 foo + "bar" 时会在堆上创建一个全新的 String 对象。这意味着当传递 String 对象(或任何 Immutable 对象)作为方法的参数时,您不能更改 Immutable 对象的状态。因此,您对 Immutable 对象执行的任何操作实际上都会在堆上创建新对象,因此将引用新的堆对象,如上面的神秘 1 示例所示。

这是传递不可变对象的递归示例。

public static void main(String[] args) {
    foo(4, "");
}

public static void foo(int i, String bar) {
    if(i==0)
        return;

    bar += Integer.toString(i);
    foo(i-1, bar);
    System.out.println(bar);
}

你会注意到输出看起来像这样

4321
432
43
4

请注意,我们不会像您预期的那样四次得到 4321。这是因为在每次递归调用中,都会在堆上创建一个新字符串来表示由串联创建的新字符串。

希望这能消除提问者的任何困惑,这对其他人有帮助。

另一个很好的参考 http://www.yoda.arachsys.com/java/passing.html.

手头的问题

因为在java中通过函数的参数传递变量时,是传值。在您第一次调用我的方法时,您传入了一个数组列表对象,该对象在整个递归算法中都是相同的指针。如果您希望节点成为堆栈帧上的局部变量,则必须创建一个局部变量,即不指定为方法签名中的参数

这里有一个教程,它解释了 java 对象是如何通过复制值引用传递的。 http://www.javaworld.com/article/2077424/learn-java/does-java-pass-by-reference-or-pass-by-value.html

是的,您想使用什么术语。事实是它是同一个指针。

class MyClass {
ArrayList<ArrayList<MyNode> allSeriesOfNodes = new ArrayList<ArrayList<MyNode>();

public void myMethod(MyNode currNode) {
    List<MyNode> nodes = new ArrayList<MyNode>();  //nodes list as a local variable
    // make sure currNode and nodes aren't null, blah....
    nodes.add(currNode);
    for(MyNode n: otherNodeList) {
        for(listItem in n.getList()) {
            if(...) {
                myMethod(n);
        }
    }

    if(currNode is a leaf and nodes is > 1) {
        allSeriesOfNodes.add(nodes);
    }
}

【讨论】:

  • @HukeLau_DABA 为什么你投了反对票,因为我给了你对你问题的正确答案?
  • java 是按值传递的,尽管值本身有时是引用。
  • @HukeLau_DABA 好吧,当您发现您的算法不起作用时,您不会感到惊讶,因为它的同一个指针“引用”了整个算法中的同一个数组列表,即因为对象是通过引用传递的。
  • @HukeLau_DABA 这是一个很棒的教程供您阅读。 javaworld.com/article/2077424/learn-java/…
  • 如果是这种情况,弹出堆栈帧应该始终让 currNode 成为叶节点。
猜你喜欢
  • 1970-01-01
  • 2017-08-22
  • 1970-01-01
  • 2014-12-17
  • 2017-08-07
  • 1970-01-01
  • 1970-01-01
  • 2015-11-15
  • 2020-10-27
相关资源
最近更新 更多