5

I've created a utility that combines zip file archives into a single archive. In doing so, I originally had the following method (see this question for some background on 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)));
}

Here is the code for ExceptionWrapper.wrapConsumer and ConsumerWrapper

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

This results in the compilation errors:

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

However, the following change compiles just fine and works as expected:

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

Notice that all I did was pull out the in-lined creation of the Consumer into a separate variable. Any spec experts know what changes for the compiler when the Consumer is in-lined?

EDIT: As requested, this is the signature of addEntryContent(...):

private void addEntryContent(final ZipOutputStream out, 
                             final ZipFile source, 
                             final ZipEntry entry, 
                             final Set<String> entryNames) throws IOException {
Community
  • 1
  • 1
MadConan
  • 3,749
  • 1
  • 16
  • 27
  • The type inference rules around generics and lambdas are fairly complicated, but that's the area you should be looking at in the JLS. It is even possible that it's a compiler bug. – biziclop Jul 16 '15 at 14:01
  • By the way, what is the signature of `addEntryContent()`? – biziclop Jul 16 '15 at 14:01
  • @biziclop edited to include sig of `addEntryContent`. Good suggestion. – MadConan Jul 16 '15 at 14:04

1 Answers1

6

The problem is the rather unusual signature of ZipFile.stream():

public Stream<? extends ZipEntry> stream()

it was defined this way because it allows the subclass JarFile to override it with the signature:

public Stream<JarEntry> stream()

Now when you call stream() on a ZipFile you get a Stream<? extends ZipEntry> with a forEach method with the effective signature void forEach(Consumer<? super ? extends ZipEntry> action) which is a challenge to the type inference.

Normally, for a target type of Consumer<? super T>, the functional signature T → void gets inferred and the resulting Consumer<T> is compatible to a target type of Consumer<? super T>. But when wildcards are involved, it fails as a Consumer<? extends ZipEntry> is inferred for your lambda expression which is considered not to be compatible with the target type Consumer<? super ? extends ZipEntry>.

When you use a temporary variable of type Consumer<ZipEntry>, you have explicitly defined the type of the lambda expression and that type is compatible with the target type. Alternatively you can make the type of the lambda expression more explicit by saying:

source.stream().forEach(ExceptionWrapper.wrapConsumer(
                        (ZipEntry e) -> addEntryContent(out, source, e, entryNames)));

or simply use a JarFile instead of ZipFile. The JarFile doesn’t mind if the underlying file is a plain zip file (i.e. has no manifest). Since JarFile.stream() doesn’t use wildcards, the type inference works without problems:

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

Of course, it will now infer the type Consumer<JarEntry> rather than Consumer<ZipEntry> but that difference has no consequences…

Holger
  • 285,553
  • 42
  • 434
  • 765
  • 5
    As a side note, `ZipFile.stream()` violates their [own recommendation](http://docs.oracle.com/javase/tutorial/java/generics/wildcardGuidelines.html): “*Using a wildcard as a return type should be avoided because it forces programmers using the code to deal with wildcards*”. They are so right… – Holger Jul 16 '15 at 14:44
  • Awesome explanation. When I saw `Consumer super capture#1 of ? extends java.util.zip.ZipEntry>`, I went, "WTF is that?" It's what prompted me to ask the question since my brain has a hard enough time with 1 wild card in generic statements, let alone two. – MadConan Jul 16 '15 at 14:49
  • I wonder whether it would be more appropriate to make a `ZipFile` and return `Stream`... – Tagir Valeev Jul 17 '15 at 12:57
  • I don't see why they made the `stream()` return `Stream extends ZipEntry>` instead of just `Stream`. I mean, I see how it allows the override in `JarFile`, but since they already created [`getJarEntry()`](https://docs.oracle.com/javase/8/docs/api/java/util/jar/JarFile.html#getJarEntry-java.lang.String-) along side `getEntry()`, why not do the same and create `public Stream jarEntryStream()` in `JarFile`? – MadConan Jul 17 '15 at 13:30
  • 1
    @Tagir Valeev: because then, `ZipFile` suddenly turns into a generic class causing tons of raw type warnings on legitimate use cases. – Holger Jul 17 '15 at 13:36
  • 1
    @MadConan: given the now established patterns, having `zipEntries()` and `jarEntries()` would fit better than `stream()` anyway. But don’t ask me, most probably, this pattern hadn’t established yet when they made that method… – Holger Jul 17 '15 at 13:40
  • @Holger, this did not stop Swing developers from adding generic arguments to [JComboBox](https://docs.oracle.com/javase/7/docs/api/javax/swing/JComboBox.html) and several other classes in JDK 7, while in JDK 6 they were [raw](https://docs.oracle.com/javase/6/docs/api/javax/swing/JComboBox.html). – Tagir Valeev Jul 17 '15 at 15:25
  • 1
    @Tagir Valeev: that’s justified as these classes have indeed a collection like functionality which suffered from the absence of a type parameter. It’s rather incomprehensible why this took so long, as that’s missing since Java 5. However, the case of `ZipFile` is different as the only valid type argument would be ``, so it’s not really a generic class, it’s just pretending to be generic to support that single subclass… – Holger Jul 17 '15 at 16:13