1

我偶然发现了 Java 的一个非常令人费解的特性(?)。

似乎使用“new”关键字替换方法参数类型将该对象转移到不同的范围:

import java.util.ArrayList;

public class Puzzle {
    public static void main(String[] args) {
        ArrayList<Integer> outer = new ArrayList<Integer>();
        outer.add(17);
        Puzzle.change(outer);
        outer.add(6);
        System.out.println(outer);

        // excpected output:
        // [23]
        // [23, 6]
        //
        // actual output:
        // [23]
        // [17, 7, 6]
    }

    public static void change(ArrayList<Integer> inner) {
        inner.add(7);
        inner = new ArrayList<Integer>();
        inner.add(23);
        System.out.println(inner);
    }
}

谁能解释这个奇怪的地方?我注意到与分配相同的行为。

4

9 回答 9

8

这是 Java 初学者的经典之作。部分问题在于我们缺乏一种通用语言来识别这一点。参数是按值传递的吗?对象是通过引用传递的?根据您与谁交谈,即使每个人都想说同样的话,也会对按价值传递和按引用传递这两个词有不同的反应。

可视化这一点的方法是了解 java 变量可以是两件事之一。它可以包含原语或对对象的引用。将该引用视为指向内存中某个位置的数字。它不是 C++ 中的指针,因为它不指向特定的内存位置,它更像是一个句柄,因为它让 JVM 查找可以找到对象的特定内存位置。但是您可以通过以下方式对其进行可视化:

   ArrayList<Integer> outer = @1234; //The first reference where the ArrayList was created.

然后,您使用以下参数调用内部:

  Puzzle.change(@1234);

请注意,您不传递外部变量,而是传递@1234 的值。external 不能通过作为方法的参数以任何方式更改。这就是我们所说的按值传递的意思。该值被传递,但它与外部变量断开连接。

谜题内部:

public static void change(ArrayList<Integer> inner) { // a new reference inner is created.
    //inner starts out as @1234
    inner.add(7);
    //now inner becomes @5678
    inner = new ArrayList<Integer>();
    //The object @5678 is changed.
    inner.add(23);
    //And printed.
    System.out.println(inner);
}

但是外部仍然指向@1234,因为该方法无法改变它,它从来没有外部变量,它只有它的内容。但是,由于 change 方法以对 @1234 的引用开始,因此该方法确实可以更改该位置的对象,并且结果对任何其他对 @1234 的引用都是可见的。

更改方法完成后,没有任何内容引用对象@5678,因此它可以进行垃圾回收。

于 2009-09-24T14:53:12.883 回答
6

这是初学者的经典java问题之一。

内部参数按值传递(对于非原始对象,它是引用)。如果您对其进行操作,它会影响与外部代码中相同的对象,因此您会看到外部方法中的影响。

如果将对象替换为新对象,则不一样。当你在新的方法上工作时,你不会改变以前的方法,你也看不到你的外部方法的影响。


更新:很抱歉造成许多评论的词汇错误。我现在更正了我的答案。我相信这一点已经提出,但现在更清楚了。

于 2009-09-24T14:08:01.573 回答
6

在java中,您可以将所有变量视为指向真实对象的指针。

会发生什么:

  1. 您创建一个列表。
  2. 添加17。
  3. 将列表发送到另一个拥有指向列表的指针的函数。
  4. 将 7 添加到第一个列表中。
  5. 创建一个新列表并将“内部”指针指向该列表。
  6. 将 23 添加到新列表中。
  7. 打印新列表并返回。
  8. 在原始函数中,您仍然拥有指向与以前相同的对象的指针。
  9. 您将 6 添加到第一个列表中。
  10. 并打印第一个列表。
于 2009-09-24T14:13:26.183 回答
2

outer引用仍然指向在您的 main 方法中创建的原始 ArrayList。只有inner引用指向在change方法内部创建的新 ArrayList,而这个 ArrayList 永远不会在外部引用change

这是预期的行为。您需要返回对内部 ArrayList 的引用才能从调用范围访问它。

于 2009-09-24T14:08:14.677 回答
1

您的“实际输出”非常有意义。您已在 change() 方法中将 inner 重新分配给一个新值,而 outer 不再受到影响。尝试使用调试器跟踪执行,您将了解发生了什么。

于 2009-09-24T14:08:31.277 回答
1

您没有转移范围,您只是将新的 ArrayList 分配给了inner参数。尝试制作inner一个final参数来防止这种情况。

于 2009-09-24T14:08:49.260 回答
1

据我了解,这不是一个谜,Java 只支持按值传递,我的意思是总是复制参数。更多在这里

于 2009-09-24T14:08:59.870 回答
1

内联该方法,您将获得以下等效代码:

import java.util.ArrayList;

public class Puzzle {
    public static void main(String[] args) {
        ArrayList<Integer> outer = new ArrayList<Integer>();
        outer.add(17);

        ArrayList<Integer> inner = outer;
        inner.add(7);  // inner refers to same array list as outer

        inner = new ArrayList<Integer>();  // inner refers to new array list
        inner.add(23);
        System.out.println(inner);  // new list is printed

        outer.add(6);
        System.out.println(outer);  // outer list is printed
}

}

于 2009-09-24T14:37:30.670 回答
0

“inner”是对“outer”最初引用的同一对象的引用(因为Java对方法上的对象参数使用按引用传递,因此inner和outer都指向同一个对象),然后在您制作“inner”的方法内部引用一个新对象。

Java 的重要之处在于,这不会改变 main 方法中“外部”引用的内容。一旦方法调用完成,'inner' 将停止在作用域内,最终将被垃圾回收。“外部”指向的对象仍在范围内(因为外部仍然处于活动状态)。

于 2009-09-24T14:08:54.740 回答