【问题标题】:super() causes NullPointerExceptionsuper() 导致 NullPointerException
【发布时间】:2015-11-07 01:46:12
【问题描述】:

当我在研究梁的书时,我被困在一个点上,我不明白发生了什么。错误的原因是 MyArrayList 类的构造函数。作者警告我们不要调用 super(object),但他们没有解释原因。现在,当我尝试运行代码时,作者是正确的——当我们调用 super(object) 时,我得到一个错误。这个错误的原因是什么?

MyArrayList.java

public class MyArrayList<E> extends MyAbstractList<E> {
  public static final int INITIAL_CAPACITY = 16;
  private E[] data = (E[])new Object[INITIAL_CAPACITY];

  /** Create a default list */
  public MyArrayList() {
  }

  /** Create a list from an array of objects */
  public MyArrayList(E[] objects) {
    /*for (int i = 0; i < objects.length; i++)
     add(objects[i]); // Warning: don't use super(objects)! */
     super(objects);  //!!! AUTHOR WARNS US ABOUT NOT INVOKING THIS LINE !!!
  }

  /** Add a new element at the specified index in this list */
  public void add(int index, E e) {
    ensureCapacity();

    // Move the elements to the right after the specified index
    for (int i = size - 1; i >= index; i--)
      data[i + 1] = data[i];

    // Insert new element to data[index]
    data[index] = e;

    // Increase size by 1
    size++;
  }

  /** Create a new larger array, double the current size */
  private void ensureCapacity() {
    if (size >= data.length) {
      E[] newData = (E[])(new Object[size * 2 + 1]);
      System.arraycopy(data, 0, newData, 0, size);
      data = newData;
    }
  }

  /** Clear the list */
  public void clear() {
    data = (E[])new Object[INITIAL_CAPACITY];
    size = 0;
  }

  /** Return true if this list contains the element */
  public boolean contains(E e) {
    for (int i = 0; i < size; i++)
      if (e.equals(data[i])) return true;

    return false;
  }

  /** Return the element from this list at the specified index */
  public E get(int index) {
    return data[index];
  }

  /** Return the index of the first matching element in this list.
   *  Return -1 if no match. */
  public int indexOf(E e) {
    for (int i = 0; i < size; i++)
      if (e.equals(data[i])) return i;

    return -1;
  }

  /** Return the index of the last matching element in this list
   *  Return -1 if no match. */
  public int lastIndexOf(E e) {
    for (int i = size - 1; i >= 0; i--)
      if (e.equals(data[i])) return i;

    return -1;
  }

  /** Remove the element at the specified position in this list
   *  Shift any subsequent elements to the left.
   *  Return the element that was removed from the list. */
  public E remove(int index) {
    E e = data[index];

    // Shift data to the left
    for (int j = index; j < size - 1; j++)
      data[j] = data[j + 1];

    data[size - 1] = null; // This element is now null

    // Decrement size
    size--;

    return e;
  }

  /** Replace the element at the specified position in this list
   *  with the specified element. */
  public E set(int index, E e) {
    E old = data[index];
    data[index] = e;
    return old;
  }

  /** Override toString() to return elements in the list */
  public String toString() {
    StringBuilder result = new StringBuilder("[");

    for (int i = 0; i < size; i++) {
      result.append(data[i]);
      if (i < size - 1) result.append(", ");
    }

    return result.toString() + "]";
  }

  /** Trims the capacity to current size */
  public void trimToSize() {
    if (size != data.length) { // If size == capacity, no need to trim
      E[] newData = (E[])(new Object[size]);
      System.arraycopy(data, 0, newData, 0, size);
      data = newData;
    }
  }
}

MyList.java

public interface MyList<E> {
  /** Add a new element at the end of this list */
  public void add(E e);

  /** Add a new element at the specified index in this list */
  public void add(int index, E e);

  /** Clear the list */
  public void clear();

  /** Return true if this list contains the element */
  public boolean contains(E e);

  /** Return the element from this list at the specified index */
  public E get(int index);

  /** Return the index of the first matching element in this list.
   *  Return -1 if no match. */
  public int indexOf(E e);

  /** Return true if this list contains no elements */
  public boolean isEmpty();

  /** Return the index of the last matching element in this list
   *  Return -1 if no match. */
  public int lastIndexOf(E e);

  /** Remove the first occurrence of the element o from this list.
   *  Shift any subsequent elements to the left.
   *  Return true if the element is removed. */
  public boolean remove(E e);

  /** Remove the element at the specified position in this list
   *  Shift any subsequent elements to the left.
   *  Return the element that was removed from the list. */
  public E remove(int index);

  /** Replace the element at the specified position in this list
   *  with the specified element and returns the new set. */
  public Object set(int index, E e);

  /** Return the number of elements in this list */
  public int size();
}

MyAbstractList.java

public abstract class MyAbstractList<E> implements MyList<E> {
  protected int size = 0; // The size of the list

  /** Create a default list */
  protected MyAbstractList() {
  }

  /** Create a list from an array of objects */
  protected MyAbstractList(E[] objects) {
    for (int i = 0; i < objects.length; i++)
      add(objects[i]);
  }

  /** Add a new element at the end of this list */
  public void add(E e) {
    add(size, e);
  }

  /** Return true if this list contains no elements */
  public boolean isEmpty() {
    return size == 0;
  }

  /** Return the number of elements in this list */
  public int size() {
    return size;
  }

  /** Remove the first occurrence of the element o from this list.
   *  Shift any subsequent elements to the left.
   *  Return true if the element is removed. */
  public boolean remove(E e) {
    if (indexOf(e) >= 0) {
      remove(indexOf(e));
      return true;
    }
    else
      return false;
  }
}

TestMyArrayList.java

public class TestMyArrayList {
    public static void main(String[] args)
    {

        String[] str = {"manisa","turkey","germany"};

        MyList<String> list = new MyArrayList<String>(str);

        list.add("America");
        list.add(0,"Canada");
        list.add(1,"England");
        System.out.println(list);
    }
}

这是错误代码:

Exception in thread "main" java.lang.NullPointerException
    at MyArrayList.ensureCapacity(MyArrayList.java:36)
    at MyArrayList.add(MyArrayList.java:21)
    at MyAbstractList.add(MyAbstractList.java:16)
    at MyAbstractList.<init>(MyAbstractList.java:11)
    at MyArrayList.<init>(MyArrayList.java:16)
    at TestMyArrayList.main(TestMyArrayList.java:8)

【问题讨论】:

  • 如果你读过 NullPointerExceptions,你就会知道抛出它的那一行是关键。那么它们是哪些? MyArrayList 的第 36 和 21 行,例如
  • @JeroenVannevel 有一个关于对象初始化顺序的合法问题,这不是基本的 npe 问题。
  • @njzk2: 并阅读 NPE 是什么以及如何调试它,将展示手头的问题,然后他们知道要搜索什么。规范问题旨在学习如何研究它,而不是列出 NPE 的所有可能原因。
  • 我必须同意@njzk2。导致此错误的原因是子类中的ensureCapacity 在子类的初始化程序尚未执行的状态下被调用。这不是微不足道的 NPE。
  • @HovercraftFullOfEels 然后你看到data 为空,除了data 在声明时定义。 OP还提到另一种方法有效,当人们直观地期望两者的行为相同时,因为两者归结为大致同时调用add

标签: java


【解决方案1】:

这里的问题是MyAbstractList中的构造函数在data被初始化之前调用了add

data 字段在MyArrayList 类中声明和初始化。但是直到超类初始化完成后才会进行初始化,并且在超类初始化期间进行add调用......当data仍然是null时。


这里的一般问题是构造函数调用一个可能被子类覆盖的方法是很危险的。 override 方法很可能在子类初始化发生之前被调用。

在这种情况下,add(E) 可以被覆盖。更糟糕的是,add(E) 调用 add(E, int) 肯定会被覆盖,因为它在超类中是抽象的。

【讨论】:

    【解决方案2】:

    让我们将您的代码简化为最基本的:

    public abstract class MyAbstractList<E> {
      protected int size = 0; // The size of the list
    
      protected MyAbstractList() {}
    
      protected MyAbstractList(E[] objects) {
        for (int i = 0; i < objects.length; i++)
          add(objects[i]);
    }
    

    public class MyArrayList<E> extends MyAbstractList<E> {
      public static final int INITIAL_CAPACITY = 16;
      private E[] data = (E[])new Object[INITIAL_CAPACITY];
    
      public MyArrayList(E[] objects) {
         super(objects); // this call to super() executes before data is initialized
      }
    }
    

    public static void main(String[] args) {
      String[] str = {"manisa","turkey","germany"};
      MyList<String> list = new MyArrayList<String>(str);
    }
    

    要理解的重要一点是父类的构造函数在子类初始化之前被调用(这就是为什么super()总是必须是构造函数中的第一个调用),这意味着当MyAbstractList的构造函数正在运行,data 仍然是 null

    super() 调用替换为其内容意味着for 循环在MyArrayList 初始化时执行,data 已正确设置。

    本质上,问题在于MyAbstractList 提供了一个构造函数,该构造函数调用将被子类覆盖的方法,这是一种严重的反模式。 MyAbstractList 不应提供全加式构造函数。

    有关更多信息,请参阅 Effective Java Item 17,其中说明:

    构造函数不得直接或间接调用可覆盖的方法。如果您违反此规则,将导致程序失败。超类构造函数在子类构造函数之前运行,因此子类中的覆盖方法将在子类构造函数运行之前被调用。如果覆盖方法依赖于子类构造函数执行的任何初始化,则该方法将不会按预期运行。

    【讨论】: