КлассFormatError в java 8?

Я делал урок, похожий на java.util.LinkedList, и получил совершенно неожиданный ClassFormatError. Моя IDE не показывает предупреждений. К вашему сведению, я использую Java 8u20. Обновление: исправлено в 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̶: ̶ Обновлен пример как полностью компилируемый:

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);
    }
}

Вот вывод:

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)

Ответы

Ответ 1

Ваш код действителен, и ClassFormatError из-за ошибки в компиляторе Java 8. Я думаю, что вы раскрыли крайний случай: загрузка файла класса, сгенерированного для тела лямбда-выражения, содержащего переменную с тем же именем, что и метод, который переопределяется в этом теле, может завершиться неудачей.

Я вставил ваш код в последние версии Intellij Idea (13.1.5), NetBeans (8.0.1) и Eclipse (Kepler SR2). Код скомпилирован во всех случаях. При запуске он не справился с этим ClassFormatError в NetBeans и Idea, но в Eclipse он работал нормально.

Трассировка стека показывает, что ошибка возникает из-за неудачной попытки загрузить класс, сгенерированный выражением lamba, назначенным переменной экземпляра Foo.nodes. Основная причина в том, что в лямбда-теле, присвоенном Foo.nodes, есть переменная next с тем же именем, что и переопределенный метод next().

Как вы уже заметили, если вы переименуете переменную в объявлении private Node next = sentinel.next; в какое-то уникальное значение (например, next2), проблема исчезнет. Аналогично, переименование его в другой переопределенный метод (например, remove или hasNext) вызывает проблему снова. Сообщение, сопровождающее ClassFormatError ("Дублирующееся имя поля и подпись"), точно описывает проблему, но не объясняет, почему это вообще должно быть точно определенной ошибкой времени выполнения; код, скомпилированный и имеющий одно и то же имя для метода и переменной, является допустимым (если неразумно). Далее с префиксом "это". не решает проблему.

Запуск javap для файла класса Idea (в моем случае foo/Foo$1) показывает это:

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();
} 

Тем не менее, запуск javap в соответствующем классе Eclipse, который работал нормально, показывает дополнительную запись:

public java.lang.Object next();

Эта дополнительная запись метода также волшебным образом появляется в файле класса Idea, если переменная next в теле лямбды переименована во что-то уникальное, что позволяет запускать код. Я предлагаю вам сообщить об этом как об ошибке компилятора в JetBrains.

Кроме того, в качестве несвязанной проблемы интерфейс Iterator реализует remove() в качестве метода по умолчанию в Java 8. Поскольку вы никогда не вызываете Iterator.remove(), вы можете удалить две свои реализации.

UPDATE: Я поднял ошибку (JDK-8080842) для этой проблемы с Oracle, и в выпуске 1.8.0_60 будет исправлено.

ОБНОВЛЕНИЕ К ОБНОВЛЕНИЮ (27.08.15): Oracle исправила эту проблему во всех выпусках начиная с 8u60. Я проверил исправление против 8u60. Ошибка JDK-8080842 относится.

Ответ 2

Обе переменные внутри nodes iterable и private class Node, называемые next, имеют один и тот же тип (Node) и имеют одно и то же имя: внутри этого частного класса они оба в области видимости, но не могут быть снято неоднозначность.

Итак, проблема заключается не в поле next и next(), а в переменных next с именами как в Iterable<Node> nodes, так и в private class Node.

Если вы реорганизуете sentinel.next на другое имя:

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`
    ...

Он будет компилироваться и запускаться.

Как личное замечание, я бы отговорил вас написать такой код: это невероятно сложно прочитать и выяснить, что он делает.

Надеюсь, это поможет!

Ответ 3

Попробуйте компилировать файл .java в командной строке с установкой jdk.

После компиляции с помощью JDK вы можете отключить новые файлы .class. Если ваш загрузчик классов работает в этих условиях, ваша среда IDE не использует JQK-копию javac (или настроена для компиляции на другой уровень уровня).

Eclipse используется для отправки собственного компилятора (и я считаю, что он по-прежнему работает). Netbeans всегда отказывается от компилятора JDK. В последних двух выпусках JDK-компилятор работал над внедрением интегрированной среды IDE; но я не думаю, что Eclipse переработала свою среду IDE, чтобы воспользоваться этими усилиями.

В любом случае вы скоро сможете узнать, является ли ваша проблема вашим компилятором JDK или компилятором IDE.