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

时间:2015-07-16 13:12:03

标签: java generics lambda java-8 java-stream

我创建了一个将zip文件存档合并到一个存档中的实用程序。在这样做时,我最初使用以下方法(有关ExceptionWrapper的某些背景,请参阅this question):

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

以下是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;
}

这会导致编译错误:

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

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

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

的签名
private void addEntryContent(final ZipOutputStream out, 
                             final ZipFile source, 
                             final ZipEntry entry, 
                             final Set<String> entryNames) throws IOException {

1 个答案:

答案 0 :(得分:6)

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

public Stream<? extends ZipEntry> stream()

它是以这种方式定义的,因为它允许子类JarFileoverride签名:

public Stream<JarEntry> stream()

现在,当您在stream()上致电ZipFile时,您会获得Stream<? extends ZipEntry> forEach方法,并使用有效签名void forEach(Consumer<? super ? extends ZipEntry> action),这是对类型推断。

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

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

使lambda表达式的类型更明确
source.stream().forEach(ExceptionWrapper.wrapConsumer(
                        (ZipEntry e) -> addEntryContent(out, source, e, entryNames)));

或仅使用JarFile代替ZipFile。如果基础文件是普通的zip文件(即没有清单),则JarFile不介意。由于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)));

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