Почему реализация этого универсального интерфейса создает неоднозначную ссылку? - PullRequest
7 голосов
/ 23 января 2012

Допустим, у меня есть следующее:

public interface Filter<E> {
     public boolean accept(E obj);
}

и

import java.io.File;
import java.io.FilenameFilter;

public abstract class CombiningFileFilter extends javax.swing.filechooser.FileFilter
        implements java.io.FileFilter, FilenameFilter {

    @Override
    public boolean accept(File dir, String name) {
        return accept(new File(dir, name));
    }
}

В таком виде вы можете использовать javac для компиляции CombiningFileFilter.Но, если вы также решите реализовать Filter<File> в CombiningFileFilter, вы получите следующую ошибку:

CombiningFileFilter.java:9: error: reference to accept is ambiguous, 
both method accept(File) in FileFilter and method accept(E) in Filter match
                return accept(new File(dir, name));
                       ^
  where E is a type-variable:
    E extends Object declared in interface Filter
1 error

Однако, если я сделаю третий класс:

import java.io.File;

public abstract class AnotherFileFilter extends CombiningFileFilter implements
        Filter<File> {
}

Тамбольше не является ошибкой компиляции.Ошибка компиляции также исчезает, если Filter не является универсальным:

public interface Filter {
    public boolean accept(File obj);
}

Почему компилятор не может понять, что, поскольку класс реализует Filter<File>, метод accept на самом деле должен быть accept(File) а что тут двусмысленного нет?Кроме того, почему эта ошибка происходит только с Javac?(Он прекрасно работает с компилятором Eclipse.)

/ edit
Более простой способ решения этой проблемы компилятора, чем создание третьего класса, заключается в добавлении метода public abstract boolean accept(File) в CombiningFileFilter.Это стирает неоднозначность.

/ e2
Я использую JDK 1.7.0_02.

Ответы [ 2 ]

9 голосов
/ 23 января 2012

Насколько я могу судить, ошибка компиляции определяется спецификацией языка Java, которая записывает :

Пусть C будет объявлением класса или интерфейса с формальными параметрами типа A1,...,An, и пусть C<T1,...,Tn> будет вызовом C, где для 1in Ti являются типами (а не подстановочными знаками). Тогда:

  • Пусть m будет объявлением члена или конструктора в C, чей тип объявлен как T. Тогда тип m (§8.2, §8.8.6) в типе C<T1,...,Tn> будет T[A1 := T1, ..., An := Tn].
  • Пусть m будет объявлением члена или конструктора в D, где D - это класс, расширенный на C, или интерфейс, реализованный на C. Пусть D<U1,...,Uk> будет супертипом C<T1,...,Tn>, который соответствует D. Тогда тип m в C<T1,...,Tn> тип m в D<U1,...,Uk>.

Если любой из аргументов типа параметризованного типа является подстановочным знаком, тип его членов и конструкторов не определен.

То есть метод, объявленный Filter<File>, имеет тип boolean accept(File). FileFilter также объявляет метод boolean accept(File).

CombiningFilterFilter наследует оба этих метода.

Что это значит? Спецификация языка Java пишет :

Класс может наследовать несколько методов с эквивалентными по переопределению (§8.4.2) сигнатурами.

Ошибка времени компиляции, если класс C наследует конкретный метод, сигнатуры которого являются подписью другого конкретного метода, унаследованного C.

(Это не относится, поскольку ни один из методов не является конкретным.)

В противном случае возможны два случая:

  • Если один из унаследованных методов не является абстрактным, то есть два случая:
    • Если метод, который не является абстрактным, является статическим, возникает ошибка времени компиляции.
    • В противном случае считается, что метод, который не является абстрактным, переопределяет и, следовательно, реализует все остальные методы от имени класса, который его наследует. Если подпись неабстрактного метода не является подписью каждого из других унаследованных методов, должно быть выдано непроверенное предупреждение (если не подавлено (§9.6.1.5)). Ошибка времени компиляции также происходит, если возвращаемый тип неабстрактного метода не является возвращаемым типом, заменяемым (§8.4.5) для каждого из других унаследованных методов. Если возвращаемый тип неабстрактного метода не является подтипом возвращаемого типа любого из других унаследованных методов, должно быть выдано непроверенное предупреждение. Кроме того, ошибка времени компиляции происходит, если унаследованный метод, который не является абстрактным, имеет предложение throws, которое конфликтует (§8.4.6) с тем из любого другого из унаследованных методов.
  • Если все унаследованные методы являются абстрактными, то класс обязательно является абстрактным классом и считается наследующим все абстрактные методы. Ошибка времени компиляции возникает, если для любых двух таких унаследованных методов один из методов не является возвращаемым типом, заменяемым другим (предложения throws в этом случае не вызывают ошибок.)

Таким образом, «объединение» переопределенных наследуемых методов в один метод происходит только в том случае, если один из них является конкретным, если все они абстрактные, они остаются отдельными, поэтому все они доступны и применимы к вызову метода.

Спецификация языка Java определяет , что должно произойти затем следующим образом:

Если более одного метода-члена доступны и применимы к вызову метода, необходимо выбрать один, чтобы предоставить дескриптор для отправки метода во время выполнения. Язык программирования Java использует правило, согласно которому выбирается наиболее специфичный метод .

Неформальная интуиция заключается в том, что один метод более специфичен, чем другой, если любой вызов, обработанный первым методом, может быть передан другому без ошибки типа во время компиляции.

Затем он определяет более конкретно формально. Я избавлю вас от определения, но стоит отметить, что более конкретный не является частичным порядком, так как каждый метод более конкретен , чем он сам. Затем он пишет:

Метод m1 строго более специфичен , чем другой метод m2, если и только если m1 более специфичен , чем м2, а м2 не более специфичен , чем m1 .

Таким образом, в нашем случае, когда у нас есть несколько методов с одинаковыми сигнатурами, каждый более специфичен , чем другой, но ни один не является строго более специфичным , чем другой.

Считается, что метод максимально специфичен для вызова метода, если он доступен и применим, и нет другого применимого и доступного метода, который был бы более конкретным.

Таким образом, в нашем случае все унаследованные accept методы максимально специфичны .

Если существует ровно один максимально специфический метод, то этот метод на самом деле является наиболее специфичным методом; он обязательно более конкретен, чем любой другой доступный метод, который применим. Затем он подвергается некоторым дополнительным проверкам во время компиляции, как описано в §15.12.3.

К сожалению, это не тот случай.

Возможно, что ни один из методов не является наиболее специфичным, поскольку существует два или более методов, которые являются максимально специфичными. В этом случае:

  • Если все максимально определенные методы имеют сигнатуры, эквивалентные переопределению (§8.4.2), то:
    • Если точно один из максимально специфических методов не объявлен абстрактным, это самый специфический метод.
    • В противном случае, если все максимально специфические методы объявлены абстрактными, а сигнатуры всех максимально специфических методов имеют одинаковое стирание (§4.6), то наиболее специфический метод выбирается произвольно среди подмножества максимально специфических методов. которые имеют наиболее конкретный тип возвращаемого значения. Однако считается, что наиболее конкретный метод вызывает выброшенное исключение, если и только если это исключение или его удаление объявлено в предложениях throws каждого из максимально определенных методов.
  • В противном случае мы говорим, что вызов метода неоднозначен, и возникает ошибка времени компиляции.

И это, наконец, важный момент: все унаследованные методы имеют идентичные и, следовательно, эквивалентные по переопределению сигнатуры. Однако метод, унаследованный от универсального интерфейса Filter, не имеет такого же стирания, как другие.

Следовательно, * +1136 *

  1. Первый пример будет скомпилирован, потому что все методы абстрактны, эквивалентны переопределению и имеют одинаковое стирание.
  2. Второй пример не скомпилируется, поскольку все методы абстрактны, эквивалентны переопределению, но их удаление не одинаково.
  3. Третий пример будет компилироваться, потому что все методы-кандидаты являются абстрактными, эквивалентными переопределению и имеют одинаковое стирание. (Метод с другим стиранием объявляется в подклассе и, следовательно, не является кандидатом)
  4. Четвертый пример скомпилируется, потому что все методы абстрактны, эквивалентны переопределению и имеют одинаковое стирание.
  5. Последний пример (повторение абстрактного метода в CombiningFileFilter) будет скомпилирован, поскольку этот метод эквивалентен переопределению со всеми унаследованными accept методами и поэтому переопределяет их (обратите внимание, что для переопределения не требуется одно и то же стирание!). Таким образом, существует только один применимый и доступный метод, который поэтому является наиболее специфичным один.

Я могу только предположить, почему спецификация требует таких же стираний в дополнение к переопределенной эквивалентности.Возможно, это связано с тем, что для обеспечения обратной совместимости с неуниверсальным кодом компилятору необходимо создавать синтетический метод со стертой сигнатурой, когда объявление метода ссылается на параметры типа.В этом стертом мире, какой метод может использовать компилятор в качестве цели для выражения вызова метода?Спецификация языка Java обходит эту проблему, требуя наличия подходящего, общего, стертого объявления метода.

В заключение, поведение javac, хотя и далеко не интуитивное, определяется спецификацией языка Java, и затмениене проходит тест на совместимость.

1 голос
/ 23 января 2012

В интерфейсе FileFilter есть метод с такой же сигнатурой, как у вашего конкретного интерфейса Filter<File>. Они оба имеют подпись accept(File f).

Это неоднозначная ссылка, потому что у компилятора нет возможности узнать, какой из этих методов вызывать в переопределенном вызове метода accept(File f, String name ).

...