19

I was making a class similar to java.util.LinkedList, and got a completely unexpected ClassFormatError. My IDE shows no warnings. FYI, I am using Java 8u20. Update: fixed in Java 8u60.

T̶h̶i̶s̶ ̶i̶n̶c̶l̶u̶d̶e̶s̶ ̶a̶l̶l̶ ̶r̶e̶l̶e̶v̶a̶n̶t̶ ̶m̶e̶t̶h̶o̶d̶s̶:̶ Updated example as fully compilable:

import java.io.Serializable;
import java.util.*;
import java.util.function.Function;

public class Foo<E> implements Deque<E>, Serializable {
    private static final long serialVersionUID = 0L;

    private final Node sentinel = sentinelInit();
    private final Iterable<Node> nodes = (Iterable<Node> & Serializable) () -> new Iterator<Node>() {
        @SuppressWarnings("UnusedDeclaration")
        private static final long serialVersionUID = 0L;

        private Node next = sentinel.next;

        @Override
        public boolean hasNext() {
            return next != sentinel;
        }

        @Override
        public Node next() {
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            Node old = next;
            next = next.next;
            return old;
        }

        @Override
        public void remove() {
             if (next.previous == sentinel) {
                throw new IllegalStateException();
            }
            removeNode(next.previous);
        }
    };

    @Override
    public boolean isEmpty() {
        return false;
    }

    @Override
    public Object[] toArray() {
        return new Object[0];
    }

    @Override
    public <T> T[] toArray(T[] a) {
        return null;
    }

    @Override
    public boolean containsAll(Collection<?> c) {
        return false;
    }

    @Override
    public boolean removeAll(Collection<?> c) {
        return false;
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        return false;
    }

    @Override
    public void clear() {

    }

    @Override
    public void addFirst(E e) {

    }

    @Override
    public void addLast(E e) {

    }

    @Override
    public boolean offerLast(E e) {
        return false;
    }

    @Override
    public E removeFirst() {
        return null;
    }

    @Override
    public E removeLast() {
        return null;
    }

    @Override
    public E pollFirst() {
        return null;
    }

    @Override
    public E getFirst() {
        return null;
    }

    @Override
    public E getLast() {
        return null;
    }

    @Override
    public E peekFirst() {
        return null;
    }

    @Override
    public boolean removeFirstOccurrence(Object o) {
        return false;
    }

    @Override
    public boolean removeLastOccurrence(Object o) {
        return false;
    }

    @Override
    public E remove() {
        return null;
    }

    @Override
    public E element() {
       return null;
    }

    @Override
    public void push(E e) {

    }

    @Override
    public E pop() {
        return null;
    }

    @Override
    public boolean contains(Object o) {
        return false;
    }

    @Override
    public boolean offerFirst(E e) {
        return false;
    }

    @Override
    public E pollLast() {
        return null;
    }

    @Override
    public E peekLast() {
        return null;
    }

    @Override
    public boolean offer(E e) {
        Node node = new Node(e);
        sentinel.previous.next = node;
        node.previous = sentinel.previous;
        sentinel.previous = node;
        node.next = sentinel;
        return true;
    }

    @Override
    public E poll() {
        return null;
    }

    @Override
    public E peek() {
        return null;
    }

    @Override
    public boolean remove(Object o) {
        for (Node node : nodes) {
            if (node.value.equals(o)) {
                removeNode(node);
                return true;
            }
        }
        return false;
    }

    @Override
    public int size() {
        return 0;
    }

    @Override
    public Iterator<E> descendingIterator() {
        return null;
    }

    @Override
    public Iterator<E> iterator() {
        return new Iterator<E>() {
            private final Iterator<Node> backingIter = nodes.iterator();

            @Override
            public boolean hasNext() {
                return backingIter.hasNext();
            }

            @Override
            public E next() {
                return backingIter.next().value;
            }

            @Override
            public void remove() {
                backingIter.remove();
            }
        };
    }

    private Node sentinelInit() {
        Node sentinel = new Node();
        sentinel.next = sentinel;
        sentinel.previous = sentinel;
        return sentinel;
    }

    private void removeNode(Node node) {
        node.previous.next = node.next;
        node.next.previous = node.previous;
    }

    private class Node implements Serializable {
        private static final long serialVersionUID = 0L;
        public E value;
        public Node next;
        public Node previous;

        public Node(E value) {
            this.value = value;
        }

        public Node() {
            this(null);
        }
    }

    public static <I, O> List<O> map(Function<? super I, O> function, Iterable<I> objects) {
        ArrayList<O> returned = new ArrayList<>();
        for (I obj : objects) {
            returned.add(function.apply(obj));
        }
        return returned;
    }

    @Override
    public boolean addAll(Collection<? extends E> c) {
        boolean ret = false;
        for (boolean changed : map(this::add, c)) {
            if (changed) {
                ret = true;
            }
        }
        return ret;
    }

    @Override
    public boolean add(E e) {
        if (!offer(e)) {
            throw new IllegalStateException();
        }
        return true;
    }

    public static void main(String[] args) {
        Foo<String> list = new Foo<>();
        System.out.println("Constructed list");
        list.addAll(Arrays.asList("a", "B", "c"));
        System.out.println("Added a, B and c.");
        list.forEach(System.out::println);
        list.remove("B");
        list.forEach(System.out::println);
    }
}

Here is the output:

Constructed list
Added a, B and c.
Exception in thread "main" java.lang.ClassFormatError: Duplicate field name&signature in class file uk/org/me/Foo$1
    at java.lang.ClassLoader.defineClass1(Native Method)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:760)
    at java.security.SecureClassLoader.defineClass(SecureClassLoader.java:142)
    at java.net.URLClassLoader.defineClass(URLClassLoader.java:455)
    at java.net.URLClassLoader.access$100(URLClassLoader.java:73)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:367)
    at java.net.URLClassLoader$1.run(URLClassLoader.java:361)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.net.URLClassLoader.findClass(URLClassLoader.java:360)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:308)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at uk.org.me.Foo.lambda$new$c83cc381$1(Foo.java:18)
    at uk.org.me.Foo$$Lambda$1/1392838282.iterator(Unknown Source)
    at uk.org.me.Foo$2.<init>(Foo.java:222)
    at uk.org.me.Foo.iterator(Foo.java:221)
    at java.lang.Iterable.forEach(Iterable.java:74)
    at uk.org.me.Foo.main(Foo.java:300)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:483)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
bcsb1001
  • 2,834
  • 3
  • 24
  • 35
  • Your runtime doesn't match with your compile time java version – jmj Jul 04 '14 at 20:19
  • @JigarJoshi It does; the IDE, compiler and runtime are all using the same java version. – bcsb1001 Jul 04 '14 at 20:23
  • 1
    Seems like it's complaining about you having a field `Node next` and a method `Node next()`. But I'm not sure why that's a problem, and if it is, why the compile would not fail. – Mark Peters Jul 04 '14 at 20:39
  • 1
    @MarkPeters Thanks. I tried changing the field `next` to `nextNode`, and this seemed to fix the problem. I'm not sure why a method and field with the same signature would conflict, though. – bcsb1001 Jul 04 '14 at 20:48
  • 5
    I'd suggest creating a bug report to Oracle – Tassos Bassoukos Jul 04 '14 at 21:05
  • Yes, unless you've been mucking with the class file innards or doing other nefarious things, this sounds like a JDK bug. – Hot Licks Jul 04 '14 at 22:31
  • Can you include the code of the MyAbstractDeque ? Problem might be there by inheritance. – Rafael Jul 16 '14 at 13:37
  • @Rafael I have included every method this test executed, as you can see from the stack trace. Also, I fixed the problem now without changing `MyAbstractDeque`, so the inheritance is fine. – bcsb1001 Jul 16 '14 at 14:57
  • Are you using Eclipse? The Eclipse compiler sometimes has weird compilation bugs. – Mark Rotteveel Jul 16 '14 at 15:01
  • @MarkRotteveel No, I'm using Intellij IDEA. Also, a command line test threw the same exception. – bcsb1001 Jul 20 '14 at 19:58
  • Per my updated answer below, this is a JDK bug that will be fixed in update 1.8.0_60. – skomisa Jun 01 '15 at 02:25
  • 1
    Per my update to my updated answer below, Oracle have now fixed this bug from release 8u60 onwards, and I have verified their fix on 8u60. Bug Report 8080842 refers. – skomisa Aug 28 '15 at 06:21
  • @Downvoter please explain? – bcsb1001 Jun 28 '16 at 20:02

4 Answers4

14

Your code is valid, and the ClassFormatError is due to a bug in the Java 8 compiler. I think you have uncovered an edge case: loading the class file generated for a lambda expression body which contains a variable with the same name as a method that is being overriden within that body may fail.

I pasted your code into the latest versions of Intellij Idea (13.1.5), NetBeans (8.0.1) and Eclipse (Kepler SR2). The code compiled in all cases. When run, it failed with that ClassFormatError in NetBeans and Idea, yet it ran fine in Eclipse.

The stack trace shows the Error is arising from a failed attempt to load the class generated by the lamba expression assigned to instance variable Foo.nodes. The root cause is that within the lambda body assigned to Foo.nodes you have a variable next with the same name as the overriden method next().

As you have already noted, if you rename the variable in the declaration private Node next = sentinel.next; to some unique value (e.g. next2) the problem goes away. Similarly, renaming it to a different overriden method (i.e. remove or hasNext) causes the problem to reappear. The message accompanying the ClassFormatError ("Duplicate field name&signature") exactly describes the problem, but does not explain why this should be such a precisely defined runtime error at all; the code compiled, and having the same name for a method and a variable is legal (if unwise). Prefixing next with "this." doesn't solve the problem.

Running javap on the failing Idea class file (foo/Foo$1 in my case) shows this:

C:\Idea\Java8\Foo\out\production\Foo\foo>"C:\Program Files\Java\jdk1.8.0_20\bin\javap.exe" Foo$1.class
Compiled from "Foo.java"
class foo.Foo$1 implements java.util.Iterator<foo.Foo<E>.Node> {
  final foo.Foo this$0;
  foo.Foo$1(foo.Foo);
  public boolean hasNext();
  public foo.Foo<E>.Node next();
  public void remove();
} 

However, running javap on the corresponding Eclipse class which ran fine shows an extra entry:

public java.lang.Object next();

That extra method entry also magically appears in the Idea class file if the variable next within the lambda body is renamed to something unique, allowing the code to run. I suggest you report this as a compiler bug to JetBrains.

Also, as an unrelated issue, the Iterator interface implements remove() as a default method in Java 8. Since you never call Iterator.remove() you can delete your two implementations.

UPDATE: I raised a bug (JDK-8080842) for this issue with Oracle, and there will be a fix for it in release 1.8.0_60.

UPDATE TO THE UPDATE (8/27/15): Oracle have fixed this issue in all releases from 8u60 onwards. I verified the fix against 8u60. Bug JDK-8080842 refers.

Junaid
  • 2,572
  • 6
  • 41
  • 77
skomisa
  • 16,436
  • 7
  • 61
  • 102
  • Report it to JetBrains? IDEA uses javac, and this occurs with raw command-line javac. It only works in Eclipse because of their custom compiler. – bcsb1001 Oct 07 '14 at 16:05
  • Since it's failing in Idea it seems reasonable to report it to JetBrains, but report it to Oracle if you think that is more appropriate. Or both. – skomisa Oct 07 '14 at 16:22
  • @bcsb1001 I wrote a much simpler version of the app you posted in NetBeans, reproduced the error, and reported it to Oracle as a compiler bug. See https://netbeans.org/bugzilla/show_bug.cgi?id=247843 for details. – skomisa Oct 09 '14 at 23:57
  • hmm? bug 8080842 still stands. – ZhongYu Aug 28 '15 at 03:03
  • @ bayou.io Actually 8080842 is now closed, and the problem has been fixed in 8u60 onwards. My update above was hopelessly wrong, but I have corrected it now. Thanks for keeping me honest. – skomisa Aug 28 '15 at 06:18
2

Both variables inside the nodes iterable and the private class Node called next are of the same type (Node) and have the same name: inside that private class, they are both in scope, but cannot be disambiguated.

So, the issue is not the next field and next() method, but the next named variables both in Iterable<Node> nodes and your private class Node.

If you refactor sentinel.next to some other name:

private final Iterable<Node> nodes = (Iterable<Node> & Serializable) () -> new Iterator<Node>() {
    @SuppressWarnings("UnusedDeclaration")
    private static final long serialVersionUID = 0L;

    private Node snext = sentinel.next;  // refactor `next` to `snext`
    ...

It will compile and run.

As a personal note, I would discourage you writing code such as this: it's incredibly difficult to read and figure out what it does.

Hope this helps!

Marco Massenzio
  • 2,822
  • 1
  • 25
  • 37
  • I already knew that I can fix the issue by renaming `next` (see comments), but thanks anyway. If you want a working example, see my edit. – bcsb1001 Sep 25 '14 at 15:51
  • 1
    Your comment says: "I'm not sure why a method and field with the same signature would conflict, though" -- it's *not* the method's name that conflicts with the instance variable name: it's the inner Node class's member named `node` that conflicts. As you also mentioned this as a possible "Java compiler bug" I was just pointing out that it wasn't so. – Marco Massenzio Sep 26 '14 at 17:17
  • BTW - if you feel that my answer addresses the issue, upvoting would be appreciated :) (as it would also help others looking for the same exception/issue). Thanks! – Marco Massenzio Sep 26 '14 at 17:19
  • I don't see how this isn't a Java compiler bug, and your answer does **not** explain why not. An instance of `Iterable` should be able to have a field with the same signature as one in its generic type (`Node`). – bcsb1001 Sep 26 '14 at 17:43
  • They are both instances of type `Node`, they are both called `next` and inside the `private class Node` they cannot be disambiguated. Yes, the `Iterable` (which is called `nodes`) does not enter into the equation here. Anyways, never mind - I've tried to help, but have it your way: feel free to file a bug to the JCP. – Marco Massenzio Sep 28 '14 at 06:17
  • @Marco I have upvoted your correct answer. The first line of the error message says the same thing: `Duplicate field name&signature`. – Mark Brown Oct 07 '14 at 05:23
  • @Marco "the issue is not the next field and next() method, but the next named variables both in Iterable nodes and your private class Node". The opposite is true; see my separate answer where I actually ran the code. If the issue was simply a naming clash between two variables then renaming Node.next to Node.next2 would fix the problem, but it doesn't. – skomisa Oct 07 '14 at 16:07
  • @MarkBrown "I have upvoted your correct answer". This answer is incorrect. Running the code shows that the error is due to a clash between a method name and a variable name, not a clash between two variable names. – skomisa Oct 07 '14 at 16:16
  • @skomi Thanks for the analysis. Can you explain the significance of the extra method entry? Also, note that the message is Duplicate *field*, which is not accurate if you are right about only one field being involved. – Mark Brown Oct 08 '14 at 01:17
  • @MarkBrown Re: "Can you explain the significance of the extra method entry?" => Sadly no - I'm just naively reporting the class file difference shown by javap when that naming clash between overriden method next() and variable next is resolved. Maybe someone familiar with how Java implements lambda expressions can explain? – skomisa Oct 08 '14 at 04:58
  • @MarkBrown Re: "the message is Duplicate field, which is not accurate if you are right about only one field being involved" => Well if two variables were clashing I'd expect the message to be "Duplicate field names" rather than "Duplicate field name&signature". But regardless, if both variables named 'next' are renamed to 'next2' then all is well, so the problem cannot be due to a name clash between those two variables alone. – skomisa Oct 08 '14 at 05:02
0

Try compiling the '.java' file on the command line with your jdk installation.

After you compile using the JDK, see if you can run off the new '.class' files. If your class loader is working under those conditions, your IDE is not using the JDK copy of javac (or is configured to compile to a different --target level).

Eclipse used to ship it's own compiler (and I believe it still does). Netbeans always defers to the JDK compiler. In the last couple of releases, the JDK compiler was worked on quite a bit for IDE integration; but, I don't think Eclipse has reworked their IDE to take advantage of that effort.

In any event, you will soon be able to tell if your issue is your JDK compiler or IDE compiler.

Edwin Buck
  • 69,361
  • 7
  • 100
  • 138
  • I use IntelliJ IDEA and I haven't changed from the default compiler. This default compiler is javac. Source: http://www.jetbrains.com/idea/webhelp/compiler-java-compiler.html – bcsb1001 Jul 16 '14 at 15:20
  • So did you try the steps above? There is a world of difference between reading how something should work or fail and observing it working or failing. – Edwin Buck Jul 16 '14 at 16:40
  • I tried compiling and running from the command line, and the same error was reproduced. So it's definitely an issue with the JDK. – bcsb1001 Jul 20 '14 at 19:56
0

As per java jdk documentation : Thrown when the Java Virtual Machine attempts to read a class file and determines that the file is malformed or otherwise cannot be interpreted as a class file.

You could try to delete the contents of the out compiled classes folder then have a full rebuild.

That's what worked for me.

Roy Doron
  • 585
  • 9
  • 23