Java的垃圾收集器如何工作引起了困惑(节点+队列)

4

我一直在尝试在Java中实现LinkedList、Stack和Queue。

对于每个数据结构,我都使用了一个节点类。虽然我知道有更好的实现方法,但这不是我的问题重点,我只想关注我的问题。

public class Node<E> {
    private E data;
    private Node<E> next;
    private Node<E> prev;

    public Node(E data) {
        this.data = data;
        this.next = null;
        this.prev = null;
    }

    public E getData() {
        return this.data;
    }

    public Node<E> getNext() {
        return this.next;
    }

    public Node<E> getPrev() {
        return this.prev;
    }

    public void setPrev(Node<E> prev) {
        this.prev = prev;
    }

    public void setData(E data) {
        this.data = data;
    }

    public void setNext(Node<E> next) {
        this.next = next;
    }
}

现在有了node类,我对垃圾回收器的工作方式感到困惑,所以假设这是我的队列类

public class Queue<E> {
    private int size;
    private Node<E> head, tail;

    public Queue() {
        this.size = 0;
        this.head = this.tail = null;
    }

    public Queue(E data) {
        Node<E> temp = new Node<E>(data);
        this.tail = this.head = temp;
        this.size = 0;
    }

    public boolean enqueue(E data) {
        Node<E> temp = new Node<E>(data);

        if (this.head == null) {
            this.tail = temp;
            this.head = temp;
        } else {
            temp.setNext(this.head);
            this.head.setPrev(temp);
            this.head = temp;
        }
        this.size++;
        return true;
    }

    public E dequeue() {
        if (this.tail == null)
            throw new IndexOutOfBoundsException();
        else {
            E data = this.tail.getData();
            this.tail.setPrev(null);
            this.tail = temp;
            this.tail.setNext(null);
            this.size--;
            return data;
        }
    }

    public int getSize() {
        return this.size;
    }

    public E peak() {
        if (this.tail == null)
            throw new IndexOutOfBoundsException();
        else
            return this.tail.getData();
    }

    public boolean contains(E data) {
        if (this.head == null)
            return false;
        else {
            for (Node<E> cursor = this.head; cursor != null; cursor = cursor
                    .getNext()) {
                if (cursor.getData().equals(data))
                    return true;
            }
        }
        return false;
    }
}

现在我有点混淆垃圾回收器的工作原理了。我听说,它会清理任何没有被指向的引用。所以我在我的出队类中一直得到空指针异常,在执行

部分时。
 this.tail.setNext(null);

现在,我听说垃圾回收器需要工作,没有任何引用它的东西,所以我想我的节点设置如下:

       head          tail
 null<-[1]-><-[2]-><-[3]->null

每个节点都可以指向下一个节点和前一个节点,所以对于我的双端队列,我认为需要做一些事情:

1)获取数据(这很容易)

2)获取一个指向前一个节点的临时节点

 Node<E> temp = this.tail.getPrev()

3) 现在我开始有些迷惑了,为了使每个节点不再被引用,我必须摆脱所有指向它的指针,这意味着我必须将其设置为 null。

this.tail.setPrev(null);

自从我删除节点之后,就无法向后退回去擦除该引用了。

       head               tail
 null<-[1]-><-[2]-> null<-[3]->null
 <-[temp]-> ( equals node [2])

4) 将尾部设置为指向临时节点temp,这是前一个节点所指向的节点

this.tail = temp;

不,应该是这样的

       head   tail    
 null<-[1]-><-[2]->(this still points to [3])    null<-[3]->null

5) 但第二个节点仍然指向 [3] 的内存地址,所以我继续执行。

this.tail.setNext(null); 

为了使任何内容都不再引用我们的任何内存区域,需要进行以下操作:
       head   tail         will be deleted by GC
 null<-[1]-><-[2]->null      null<-[3]->null

然而,在队列中只剩下一个节点时,NullPointerException错误会出现在此部分

现在,我知道我的做法可能有很多错误,因为我还在学习,但是我不确定我需要对每个节点做多少事情才能确保垃圾回收器将其清除,我需要将prevnext都设置为null吗?还是只需要设置其中一个?请帮忙解决问题,谢谢;)


垃圾回收(除非有严重错误)不能导致空指针异常。 - Has QUIT--Anony-Mousse
同意,NullPointerException 是一个逻辑错误,而不是垃圾回收问题。垃圾收集器只清理您不能再使用(没有引用的)对象,仅此而已。 - dimo414
2个回答

5
你不需要关心垃圾回收器的工作原理。如果你的列表实现正确,垃圾回收器将正常运行。
你的NullPointerException将由逻辑错误引起。与垃圾收集无关。
队列中的头和尾引用应该引用第一个和最后一个元素。
每个节点应正确指向前一个和后一个元素。你的逻辑应该识别列表的开头和结尾,并正确处理节点的插入和删除。
如果你从功能上做得对,那么删除的节点就不会被任何东西引用,垃圾回收器将清除它。
专注于编写边缘情况(空列表、一个节点列表)的单元测试,并测试插入和删除操作。
一旦功能正确,垃圾回收将正常工作。
编辑:
在长列表中,内部节点将有前一个和最后一个元素,但头和尾没有,因此你需要特殊逻辑来处理它们的删除。
如果列表只有一个元素,则头和尾是相同的,因此头部和尾部的特殊逻辑将适用于该节点。

我明白了,我已经在纸上做了一些测试,一切似乎都是正确的。但是在进行一些打印屏幕测试时,我注意到当删除最后一个节点时,在这个步骤this.tail=temp时,某种原因导致temp=null,所以我设置tail=null,但我不知道为什么temp在之前的时候没有成为节点...但我问GC的原因是因为我不确定需要付出多少工作,就像之前说的,我的this.tail.setNext(null)崩溃了,我不知道为什么,这让我开始怀疑我是否需要这样做?或者GC会替我完成它? - Tanner Summers
我会说,完全没有。唯一可能破坏垃圾回收的方式是如果您的类中有一些节点的临时引用,但您似乎没有这样的情况。 - rghome
好的,我只是太习惯使用C++了。在C++中,您必须尽力确保没有内存泄漏,而在Java中,我不确定我有多少自由以及GC到底做了多少工作。如果可能的话,我不想留下一个有引用但无法返回和清除的节点。 - Tanner Summers
"你唯一可能破坏GC的方式是...如果你调用(已损坏的)本地代码,或者错误使用java.misc.Unsafe API。你无法通过纯Java代码破坏GC,而你的代码就是纯Java代码。" - Stephen C

0

你的代码有一个bug。这与垃圾回收器无关。 当队列中只有一个节点时,在你的示例中,this.tailnull,因此你会得到NullPointerException。你赋值temp = this.tail.getPrev();,但对于仅有一个节点的情况,tempnull。然后你将this.tail = temp;。下面是正确实现dequeue()的方法。 虽然不是必须的,但也许有些人认为在删除节点时将所有内容设置为null是一种好习惯。

public E dequeue() {
        if (this.tail == null)
            throw new IndexOutOfBoundsException();
        else {
            E data = this.tail.getData();
            Node<E> temp = this.tail;

            this.tail = temp.getPrev();
            if ( this.tail == null ) { // if that was last node
                this.head = null;
                return data;
            }
            this.tail.setNext(null);

            temp.setPrev(null);
            temp.setNext(null);

            this.size--;
            return data;
        }
    }

enqueue()方法中,你检查head是否为空队列。但在dequeue()方法中,你检查tail是否为空队列。这可能有点令人困惑。你应该检查两个都是否为空。null。这是对程序的额外测试。
构造函数中还有一个错误。应将this.size设置为1而不是0。
public Queue(E data) {
        Node<E> temp = new Node<E>(data);
        this.tail = this.head = temp;
        this.size = 1;
    }

好的,我现在明白了。看到某些东西有一个良好的夜间休息总是很好的,谢谢你的时间 :)。将来,如果我需要将prev和next对象设置为null以擦除引用,我需要吗? - Tanner Summers
是的,您可以通过将prev和next对象置空来删除引用。 - Maciej

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接