Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I call collect(Collectors.toList()) on a Java 8 Stream in Kotlin?

I have the some code:

directoryChooser.title = "Select the directory"
val file = directoryChooser.showDialog(null)
if (file != null) {
    var files = Files.list(file.toPath())
            .filter { f ->
                f.fileName.endsWith("zip") && f.fileName.endsWith("ZIP")
                        && (f.fileName.startsWith("1207") || f.fileName.startsWith("4407") || f.fileName.startsWith("1507") || f.fileName.startsWith("9007") || f.fileName.startsWith("1807"))
            }
    for (f in files) {
        textArea.appendText(f.toString() + "\n")
    }
}

If I call collect(Collectors.toList()) at the end of filter, I get:

Error:(22, 13) Kotlin: [Internal Error] org.jetbrains.kotlin.codegen.CompilationException: Back-end (JVM) Internal error: no descriptor for type constructor of ('Captured(in ('Path'..'Path?'))'..'CapturedTypeConstructor(in ('Path'..'Path?'))?')
Cause: no descriptor for type constructor of ('Captured(in ('Path'..'Path?'))'..'CapturedTypeConstructor(in ('Path'..'Path?'))?')
File being compiled and position: (22,13) in D:/My/devel/ListOfReestrs/src/Controller.kt
PsiElement: var files = Files.list(file.toPath())
                    .filter { f ->
                        f.fileName.endsWith("zip") && f.fileName.endsWith("ZIP")
                                && (f.fileName.startsWith("1207") || f.fileName.startsWith("4407") || f.fileName.startsWith("1507") || f.fileName.startsWith("9007") || f.fileName.startsWith("1807"))
                    }.collect(Collectors.toList())
The root cause was thrown at: JetTypeMapper.java:430

If I don't do this, I get the f with the type [error: Error] in my for-loop.

like image 969
NCNecros Avatar asked Mar 01 '16 11:03

NCNecros


2 Answers

UPDATE: This issue is now fixed in Kotlin 1.0.1 (previously was KT-5190). No work around is needed.


Workarounds

Workaround #1:

Create this extension function, then use it simply as .toList() on the Stream:

fun <T: Any> Stream<T>.toList(): List<T> = this.collect(Collectors.toList<T>())

usage:

Files.list(Paths.get(file)).filter { /* filter clause */ }.toList()

This adds a more explicit generic parameter to the Collectors.toList() call, preventing the bug which occurs during inference of the generics (which are somewhat convoluted for that method return type Collector<T, ?, List<T>>, eeeks!?!).

Workaround #2:

Add the correct type parameter to your call as Collectors.toList<Path>() to avoid type inference of that parameter:

Files.list(Paths.get(file)).filter { /* filter clause */ }.collect(Collectors.toList<Path>())

But the extension function in workaround #1 is more re-usable and more concise.


Staying Lazy

Another way around the bug is to not collect the Stream. You can stay lazy, and convert the Stream to a Kotlin Sequence or Iterator, here is an extension function for making a Sequence:

fun <T: Any> Stream<T>.asSequence(): Sequence<T> = this.iterator().asSequence()

Now you have forEach and many other functions available to you while still consuming the Stream lazily and only once. Using myStream.iterator() is another way but may not have as much functionality as a Sequence.

And of course at the end of some processing on the Sequence you can toList() or toSet() or use any other of the Kotlin extensions for changing collection types.

And with this, I would create an extensions for listing files to avoid the bad API design of Paths, Path, Files, File:

fun Path.list(): Sequence<Path> = Files.list(this).iterator().asSequence()

which would at least flow nicely from left to right:

File(someDir).toPath().list().forEach { println(it) }
Paths.get(dirname).list().forEach { println(it) }

Alternatives to using Java 8 Streams:

We can change your code slightly to get the file list from File instead, and you would just use toList() at the end:

file.listFiles().filter { /* filter clause */ }.toList()

or

file.listFiles { file, name ->  /* filter clause */ }.toList()

Unfortunately the Files.list(...) that you originally used returns a Stream and doesn't give you the opportunity to use a traditional collection. This change avoids that by starting with a function that returns an Array or collection.

In General:

In most cases you can avoid Java 8 streams, and use native Kotlin stdlib functions and extensions to Java collections. Kotlin does indeed use Java collections, via compile-time readonly and mutable interfaces. But then it adds extension functions to provide more functionality. Therefore you have the same performance but with many more capabilities.

See Also:

  • What Java 8 Stream.collect equivalents are available in the standard Kotlin library? - You will see it is more concise to use Kotlin stdlib functions and extensions.
  • Kotlin Collections, and Extension Functions API docs
  • Kotlin Sequences API docs
  • Kotlin idioms

You should review the API reference for knowing what is available in the stdlib.

like image 72
16 revs, 2 users 99% Avatar answered Nov 17 '22 08:11

16 revs, 2 users 99%


This is another work around if you are for some reason stuck on older beta version of Kotlin which requires a more brutal workaround...

Workaround #3: (ugly, an old workaround ONLY for old versions of Kotlin)

Add this Java class to your project:

import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

public class CollectFix {
    public static <T> List<T> streamToList(Stream<T> s) {
        return s.collect(Collectors.toList());
    }
}

And this one Kotlin extension function:

fun <T: Any> Stream<T>.toList(): List<T> = CollectFix.streamToList(this)

And then any time you have this case, use this new extension:

Files.list(Paths.get(file)).filter { /* filter clause */ }.toList()
like image 33
Jayson Minard Avatar answered Nov 17 '22 09:11

Jayson Minard