为什么编译失败内联Consumer <ZipEntry>但在外部工作?

Mad*_*nan 5 java generics lambda java-8 java-stream

我创建了一个将zip文件存档合并到一个存档中的实用程序.在这样做时,我最初有以下方法(请参阅此问题以获取一些背景信息ExceptionWrapper):

private void addFile(File f, final ZipOutputStream out, final Set<String> entryNames){
    ZipFile source = getZipFileFromFile(f);
    source.stream().forEach(ExceptionWrapper.wrapConsumer(e -> addEntryContent(out, source, e, entryNames)));
}
Run Code Online (Sandbox Code Playgroud)

下面是代码ExceptionWrapper.wrapConsumerConsumerWrapper

public static <T> Consumer<T> wrapConsumer(ConsumerWrapper<T> consumer){
    return t -> {
        try {
            consumer.accept(t);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    };
}
public interface ConsumerWrapper<T>{
    void accept(T t) throws Exception;
}
Run Code Online (Sandbox Code Playgroud)

这会导致编译错误:

Error:(128, 62) java: incompatible types: 
java.util.function.Consumer<capture#1 of ? extends java.util.zip.ZipEntry> 
cannot be converted to 
java.util.function.Consumer<? super capture#1 of ? extends java.util.zip.ZipEntry>

Error:(128, 97) java: incompatible types: java.lang.Object cannot be converted to java.util.zip.ZipEntry

但是,以下更改编译正常并按预期工作:

private void addFile(File f, final ZipOutputStream out, final Set<String> entryNames){
    ZipFile source = getZipFileFromFile(f);
    Consumer<ZipEntry> consumer = ExceptionWrapper.wrapConsumer(e -> addEntryContent(out, source, e, entryNames));
    source.stream().forEach(consumer);
}
Run Code Online (Sandbox Code Playgroud)

请注意,我所做的就是将内联创建的内容Consumer转换为单独的变量.任何规范专家都知道在内联时编译器会发生什么变化Consumer

编辑:根据要求,这是签名addEntryContent(...):

private void addEntryContent(final ZipOutputStream out, 
                             final ZipFile source, 
                             final ZipEntry entry, 
                             final Set<String> entryNames) throws IOException {
Run Code Online (Sandbox Code Playgroud)

Hol*_*ger 6

问题是相当不寻常的签名ZipFile.stream():

public Stream<? extends ZipEntry> stream()
Run Code Online (Sandbox Code Playgroud)

它是这样定义的,因为它允许子类JarFile覆盖与签名吧:

public Stream<JarEntry> stream()
Run Code Online (Sandbox Code Playgroud)

现在当你打电话stream()给a时,ZipFile你会得到一个Stream<? extends ZipEntry>forEach有效签名的方法,void forEach(Consumer<? super ? extends ZipEntry> action)这对类型推断是一个挑战.

通常,对于目标类型Consumer<? super T>,T ? void推断出功能签名,并且结果Consumer<T>与目标类型兼容Consumer<? super T>.但是当涉及到通配符时,它Consumer<? extends ZipEntry>会因为你的lambda表达式被推断而失败,因为lambda表达式被认为与目标类型不兼容Consumer<? super ? extends ZipEntry>.

使用类型的临时变量时Consumer<ZipEntry>,您已明确定义了lambda表达式的类型,并且该类型与目标类型兼容.或者,您可以通过以下方式使lambda表达式的类型更明确:

source.stream().forEach(ExceptionWrapper.wrapConsumer(
                        (ZipEntry e) -> addEntryContent(out, source, e, entryNames)));
Run Code Online (Sandbox Code Playgroud)

或者只是使用JarFile而不是ZipFile.该JarFile不介意底层文件是一个普通的zip文件(即没有症状).由于JarFile.stream()不使用通配符,因此类型推断可以正常工作:

JarFile source = getZipFileFromFile(f);// have to adapt the return type of that method
source.stream().forEach(ExceptionWrapper.wrapConsumer(
                        e -> addEntryContent(out, source, e, entryNames)));
Run Code Online (Sandbox Code Playgroud)

当然,它现在会推断出类型,Consumer<JarEntry>而不是Consumer<ZipEntry>差异没有后果......

  • 作为旁注,`ZipFile.stream()`违反了他们[自己的推荐](http://docs.oracle.com/javase/tutorial/java/generics/wildcardGuidelines.html):"*使用通配符作为返回应避免使用类型,因为它强制程序员使用代码来处理通配符*".他们是对的...... (5认同)