Programming Notes(ArrayList中的值传递)

之前的程序改进过程中出现了一点问题,发现跟对于ArrayList的值传递方式的理解有关。

具体情况是我准备在之前的naive机器学习程序中把训练集扩大到100万句话的程度,而运行之后出现了这个错误:java.lang.OutOfMemoryError: Java heap space。应该是因为new了过多的新对象导致的。通过增加jvm的内存上限或者改程序都能解决,当然,重点不在这里。
重点是后来发现问题是在一个地方需要将数量跟训练集大小成正比例的List对象vec,用ArrayList的add()方法,加入到一个大的List对象trainVector中。我想因为jvm的垃圾回收会自动清除没有被引用的对象,所以我每次都直接new一个List对象给vec,当然,既然崩了,那自然是没有被回收的。
于是我决定不每次新建List对象,而是在循环外面new了这个对象之后,每次循环之后clear()掉,操作之后在add()进大List,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 List<Integer> vec = new ArrayList<>();
for (int j = 0; j < trainData.size(); j++) {
vec.clear();
for (int i = 0; i < vocabulary.size(); i++) {
vec.add(0);
}
for (int i = 0; i < trainData.get(j).size(); i++) {
if (vocabulary.contains(trainData.get(j).get(i))) {
vec.set(vocabulary.indexOf(trainData.get(j).get(i)),
vec.get(vocabulary.indexOf(trainData.get(j).get(i)))+1);
}
}
trainVector.add(vec);
}

但是我发现,这样操作之后,最后trainVector中的List居然全是同一个List,而且都跟最后一遍得到的vec一样。那么问题已经很明显了,add()方法并没有将List的值传进trainVector,而是传的引用。之所以之前的写法jvm没有进行垃圾回收也是这个原因。大的List中存有的全是小的List的引用。但是java不是值传递的吗?带着这个问题我看了一下ArrayList对List的add()方法的实现:

1
2
3
4
5
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}

没错,的确是直接传的引用。这样的话,如果每次new一个元素出来,因为在新的地址自然不会影响前面的元素;如果只new一次,trainVector中只有一个vec的引用,当然是后者怎么变前者就怎么变。java也的确是传了值,

只是对于对象它传的是地址的值,不是地址里面的内容的值。

再试验一下,对于对象:

1
2
3
4
5
6
7
8
class Test1{
public static void main(String[] args){
StringBuffer s1= new StringBuffer("hello");
StringBuffer s2=s1;
s2.append(" world");
System.out.println(s1);
}
}

程序输出是 hello world.
对于原始数据类型:

1
2
3
4
5
6
7
8
class Test2{
public static void main(String[] args){
int i1=5;
int i2=i;
i2=6;
System.out.println(i1);
}
}

程序输出还是5,传递的是值。

后记

于是情况就比较尴尬,如果按照原方法,似乎必须得保存这一百万句话转换出的向量在堆中(因为他们的引用还在大的List中);如果不想新建这么多堆对象,就得避免List中存的引用全指向一个地址。暂时来说我想的方法是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
List<Integer> vec;
for (int j = 0; j < trainData.size(); j++) {
List<Integer> temp = new ArrayList<>();
vec = temp;
for (int i = 0; i < vocabulary.size(); i++) {
vec.add(0);
}
for (int i = 0; i < trainData.get(j).size(); i++) {
if (vocabulary.contains(trainData.get(j).get(i))) {
vec.set(vocabulary.indexOf(trainData.get(j).get(i)),
vec.get(vocabulary.indexOf(trainData.get(j).get(i)))+1);
}
}
trainVector.add(vec);
}

这样每次new的对象用temp来引用,马上又将temp传给vec,真正在trainVector中引用的是vec,因此下次new的对象赋给temp后,之前的对象因为没有引用指向它,就会被回收,而trainVector中的引用全是不同地址的,不会再出现都指向一个地址的问题。
但是还是有堆栈溢出,似乎是由vec.add(0)引起的。后续再更新一下解决方法。

如果有别的办法,恳请不吝赐教,谢谢!