将项目迁移到JDK 8时出现奇怪的ExceptionInInitializerError

时间:2014-12-25 12:21:05

标签: java static nullpointerexception java-8

我有一些代码包含一些二进制依赖项(BioJava 3.1.0是上述问题的根源),它可以在JDK 7中正常工作,但在使用和编译JDK 8时,会发生一些奇怪的事情......这是堆栈跟踪的重要部分:

java.lang.ExceptionInInitializerError
        at org.biojava3.core.sequence.template.AbstractSequence.getSequenceAsString(AbstractSequence.java:527)
        at uk.ac.roslin.ensembl.datasourceaware.core.DADNASequence.getSequenceAsString(DADNASequence.java:465)
...
Caused by: java.lang.NullPointerException
        at java.util.Collections$UnmodifiableCollection.<init>(Collections.java:1026)
        at java.util.Collections$UnmodifiableList.<init>(Collections.java:1302)
        at java.util.Collections.unmodifiableList(Collections.java:1287)
        at org.biojava3.core.sequence.location.template.AbstractLocation.<init>(AbstractLocation.java:111)
        at org.biojava3.core.sequence.location.SimpleLocation.<init>(SimpleLocation.java:57)
        at org.biojava3.core.sequence.location.SimpleLocation.<init>(SimpleLocation.java:53)
...

以下是SimpleLocation的二进制代码(在我没有源代码的第3方链接库中)它有一个字段EMPTY_LOCS,构造就像这样:

public class SimpleLocation extends AbstractLocation {

    private static final List<Location> EMPTY_LOCS = Collections.emptyList();
    ...
    public SimpleLocation(int start, int end, Strand strand) 
        this(new SimplePoint(start), new SimplePoint(end), strand); { //line 53
    }

    public SimpleLocation(Point start, Point end, Strand strand) { 
        super(start, end, strand, false, false, EMPTY_LOCS); //line 57
    ...

似乎当EMPTY_LOCS传递给super AbstractLocation(第57行)时,会传递空值,不是空列表 (我查看了JDK 7,并且传递了一个很好的旧空列表。)

为什么?我应该深入研究第三方依赖源代码并覆盖它吗? (听起来不是很整洁)

当我自己使用emptyList()方法时,它返回一个空列表 - 但是这个隐藏在我的依赖项中的私有静态字段有一些针对Java 8的东西,只是不想被初始化。

修改

AbstractLocation依次使用null(和非空列表)调用unmodifiableList()(第111行):

public AbstractLocation(Point start, Point end, Strand strand,
        boolean circular, boolean betweenCompounds, AccessionID accession,
        List<Location> subLocations) {
    this.start = start;
    this.end = end;
    this.strand = strand;
    this.circular = circular;
    this.betweenCompounds = betweenCompounds;
    this.accession = accession;
    this.subLocations = Collections.unmodifiableList(subLocations); //line 111
    assertLocation();
}

然后构造UnmodifiableList(行:1287):

public static <T> List<T> unmodifiableList(List<? extends T> list) {
    return (list instanceof RandomAccess ?
            new UnmodifiableRandomAccessList<>(list) :
            new UnmodifiableList<>(list)); //line 1287
}

调用他的super(第1302行):

UnmodifiableList(List<? extends E> list) {
    super(list); //line 1302
    this.list = list;
}

因为null被传递给构造函数,所以抛出NullPointerException(第1026行):

   static class UnmodifiableCollection<E> implements Collection<E>, Serializable {
        private static final long serialVersionUID = 1820017752578914078L;

        final Collection<? extends E> c;

        UnmodifiableCollection(Collection<? extends E> c) {
            if (c==null)
                throw new NullPointerException(); //line 1026
            this.c = c;
        }

使用JDK 7 时不会发生这种投掷,并且发生了ExceptionInInitializerError

QUICK FIX:

这是一个Maven依赖项,所以我手动到达了源代码,将jar工件导入到我自己的源代码中以覆盖Maven依赖项,并更改了行{111中的AbstractLocation以包含以下内容:

if (subLocations == null) {
    subLocations = Collections.<Location>emptyList();
}

但是,迁移到JDK8时,未初始化的private static final(在static上使用empthasize)的谜团仍然让我感到困惑。

2 个答案:

答案 0 :(得分:0)

这看起来像是我初学阶段的循环。

类初始化的规则是在创建实例之前初始化类,并在子类之前初始化类。例外情况是,在类初始化期间,如果遇到遇到当前由此线程初始化的类的代码路径,代码将继续并且可以观察类的部分初始化或未初始化状态。

简化这里给出的例子,我们有

public class SimpleLocation extends AbstractLocation {

    private static final List<Location> EMPTY_LOCS = Collections.emptyList();

    public SimpleLocation(...) { 
        super(..., EMPTY_LOCS);
    }
}

鉴于上述规则,在构造函数在其超类上调用EMPTY_LOCS之前,必须初始化super()。但是超类获得该参数的null并且爆炸了。唯一的出现方式是在类完全初始化之前调用SimpleLocation构造函数。

类初始化按声明顺序从上到下(或从左到右,如规范所示)发生。即使EMPTY_LOCS是最终的,它的初始状态null也可以通过类中的上面声明的静态初始化器和超类的静态初始化器来观察。例如,如果在EMPTY_LOCS的声明上方插入如下所示的行,则很容易看到:

    static SimpleLocation defaultLocation = new SimpleLocation();

我查看了BioJava 3.1的来源,但我没有看到这样的东西。可能是您正在使用修改后的版本,或者还有一些其他类初始化依赖项导致我无法想到这一点。

我怀疑这里有任何Java 7与Java 8特定的行为差异。似乎更有可能的是,7和8之间的差异导致你的系统采用不同的代码路径,这最终会改变类初始化的顺序,从某些方式,幸运地发生在工作中,到导致此错误发生的方式。

如果要跟踪类加载和初始化的顺序,请使用选项-verbose:class运行系统。

答案 1 :(得分:0)

这似乎是一个静态初始化周期,如a prior answer by Stuart Marks中所述。这个循环可能是因为子类在它实现的一个接口的静态初始化器中被引用:

public interface Location extends Iterable<Location>, Accessioned {

    /**
     * Basic location which is set to the minimum and maximum bounds of
     * {@link Integer}. {@link Strand} is set to {@link Strand#UNDEFINED}.
     */
    public static final Location EMPTY =
        new SimpleLocation(Integer.MIN_VALUE, Integer.MAX_VALUE, Strand.UNDEFINED);
    ...
}

因此我们遇到一种情况,其中Location需要初始化SimpleLocation,而SimpleLocation需要初始化Location。必须以某种方式打破循环,看来JDK8正在以不同于JDK7及更早版本的方式解析它。

我能够通过一个简单的技巧打破循环:通过在子类的任何引用之前直接引用Location.EMPTY字段,它会使它以不同的顺序初始化。我不确定这个解决方案是否可靠,可能是订单仍然是任意的。但是至少它似乎在JDK8中没有修改任何第三方库:

public static void main(String[] args) {
    // This line prevents exception on the next line        
    Location l = Location.EMPTY;

    // This line will fail if the line above is commented out
    SimpleLocation s = new SimpleLocation(20,30, Strand.POSITIVE, new SimpleLocation[0]);
}