Collection filtering with Pair<*,*?> objects

Hello,

I’m new to kotlin (and a novice programmer in general) and ran into a bit of a snag when iterating over a list of files and filtering them based on their “creationTime” file attribute. The main issue I ran into was dealing with the creation of a Pair<Path, FileTime?> during the collection filtering and handling the null case.

I’m trying to find the most recently created file in a directory.
This is the code I had on my first attempt:

 val mostRecentlyCreatedFile = Files.list(downloadFolder).use { path ->
             path.asSequence()
                 .filter { it.isRegularFile() }
                 .mapNotNull { it to it.getAttribute("creationTime") as? FileTime }
                 .filter { (_, creationTime) -> creationTime != null}
                 .maxByOrNull{ it.second!! }
                 ?.first

I didn’t love this because it required using the not-null assertion operator (!!) which I’d like to avoid. I’m assuming !! is required because the FileTime? type is still nullable, so the compiler assumes worst case scenario.

The second attempt was this:

val mostRecentlyCreatedFile = Files.list(downloadFolder).use { path ->
        path.asSequence()
            .filter { it.isRegularFile() }
            .mapNotNull { file ->
                val creationTime = Files.getAttribute(file, "creationTime") as? FileTime
                creationTime?.let { file to it }
            }
            .maxByOrNull { it.second }
            ?.first
    }

Similar, but this time the compiler doesn’t complain and I don’t need the !! operator. The .mapNotNull looks a little messy, though.
Question: Is there a reason the compiler understands a null check in the form of " ?.let {} " but not " != null "?

Attempt #3

val mostRecentlyCreatedFile = Files.list(downloadFolder).use { path ->
            path.asSequence()
                .filter { it.isRegularFile() }
                .map { it to it.getAttribute("creationTime") as? FileTime }
                .filterIsInstance<Pair<Path, FileTime>>()
                .maxByOrNull { it.second }
                ?.first
        }

This gives me no complaints from the compiler and simple lambdas, but the meaning of filterIsInstance is maybe a little opaque.

So my question: Is there an accepted way of filtering out Pair<*, ?> that have a null value? Also, is there something obvious that I’m missing for trying to find the most recently created file that could improve my code?

Any help would be super appreciated - thanks! :slightly_smiling_face:

Maybe this:

        val mostRecentlyCreatedFile = Files.list(downloadFolder).use { path ->
            path.asSequence()
                .filter { it.isRegularFile() }
                .sortedBy { it.getAttribute("creationTime") as? FileTime }
                .last()
        }

That worked perfectly and avoided some unnecessary complexity!

Thanks :slightly_smiling_face:

It does. Your second attempt could be:

val mostRecentlyCreatedFile = Files.list(downloadFolder).use { path ->
        path.asSequence()
            .filter { it.isRegularFile() }
            .mapNotNull { file ->
                val creationTime = Files.getAttribute(file, "creationTime") as? FileTime
                if (creationTime != null) file to creationTime else null
            }
            .maxByOrNull { it.second }
            ?.first
    }
1 Like

Please note the provided solution works differently than your initial solution. If there are no files with creation time, your solution returns null. Solution by apklaus returns a random item. If this behavior is acceptable, then yet another approach would be:

path.asSequence()
    .filter { it.isRegularFile() }
    .maxBy { it.getAttribute("creationTime") as? FileTime ?: FileTime(0) } // or whatever is the minimum FileTime
2 Likes

I think you are overthinking this. !! isn’t always that bad. I like the first solution, because it is clear and explicit on the fact we both filter and look for the max value. As said above, apklaus accidentally “lost” the filtering part by trying to skip some steps.

The reason it requires !! is that the Kotlin compiler is smart, but not that smart. To handle this case, it would have to handle two other cases it doesn’t understand:

  1. Smart casting of parameter types after checking a property type:
val foo: Pair<Int?, Int?> = 1 to 2
if (foo.first != null) {
    // foo is still Pair<Int?, Int?>, so foo.first is Int?
}
  1. Smart casting of list type when using filter, e.g.:
val list = listOf<Int?>()
    .filter { it != null }
// list is still List<Int?>

Both cases are rather complicated to implement.

Your mapNotNull example is much different. In this case the compiler doesn’t do any smart casts. You map items to another type and you provide this new type explicitly.

edit:
Also, I don’t think asSequence() do anything else here than degrading the performance. Even if there is a huge number of files, we started from a list (I assume), so we have them all in the memory and to find the max item we again need to store all items in the memory at once.

1 Like

Overthinking is a hallmark trait, so you could very well be right :grin:

Files.list() is actually a lazy-loading stream, so nothing is in memory and I’m not sure there’s a performance penalty in that case with .asSequence()?

Thanks for taking the time to respond - this is all super helpful input!

Well, I’m obviously wrong. Somehow, I was thinking about sorting. If we start from a lazy stream and we only look for a max, then yeah, it makes sense to use sequences. In that case watch out for the sort+first/last solutions - they will probably load everything at once into the memory.

edit:
Initially, I didn’t recognize the standard Java API for files :man_facepalming:

All good! I think after reading over your comments the initial solution with !! is the way to go. Potentially returning something random in the case of unreadable file attributes is a no-go in my case.

Thanks for your help!

Ahh, one more thing to add:

Don’t do this, it can’t work correctly due to type erasure. There is no direct way in JVM to check at runtime what are type parameters.

Actually, this is really bad we don’t get a warning about the unchecked cast :-/ There is even no warning about this behavior in the docs of filterIsInstance() :-/

sequenceOf(5 to 5)
    .filterIsInstance<Pair<String, String>>()
    .count() // returns 1

val foo = sequenceOf(5 to 5)
    .filterIsInstance<Pair<String, String>>()
    .first()
    .first // ClassCastException
2 Likes

Oh, wow, thanks for the heads up. I’ll put that one into Anki so I don’t forget.