16

I am new to Kotlin. I am following along a tutorial where the GUI portion involves this code snippet:

    sampleList.addMouseListener(object: MouseAdapter() {
        override fun mouseClicked(mouseEvent: MouseEvent?) {
            if (mouseEvent?.clickCount == 2) {
                launchSelectedSample()
            }
        }
    })

mouseEvent is obviously something nullable. I am used to, in previous coding experience, changing a line like mouseEvent?.clickCount == 2 to mouseEvent?.clickCount > 1 (or maybe >=2), to ensure there is no corner case where the clicks happen so quickly that it jumps from 1 to 3, or something similar.


So, I switched this code to:

    sampleList.addMouseListener(object: MouseAdapter() {
        override fun mouseClicked(mouseEvent: MouseEvent?) {
            if (mouseEvent?.clickCount >= 2) {
                launchSelectedSample()
            }
        }
    })

Upon making the switch (changing ==2 to >=2), I received the following error from IntelliJ:

Operator call corresponds to a dot-qualified call 'mouseEvent?.clickCount.compareTo(2)' which is not allowed on a nullable receiver 'mouseEvent?.clickCount'.

This raised 2 questions for me:

  1. Why does ==2 work fine, but >=2 does not work? (I tried >1, which gave the same error as >=2.)
  2. What is the right way to handle a corner case where, what I really want is anything greater than 1?

I like the idea of ensuring nulls don't screw things up during runtime, but I kinda wish if Kotlin just got rid of null values altogether and did something like Rust or Haskell. (I do like what I've seen of Kotlin so far, though.)

Mike Williamson
  • 4,915
  • 14
  • 67
  • 104

4 Answers4

34

As you've found, Kotlin's equality operators (== and !=) can handle nulls, while the order comparison operators (<, <=, >, >=) can't.

This is probably because it's obvious what equality checks should mean for nulls — two nulls are clearly equal, and a non-null value should never equal a null — while it's not at all clear what it should mean for order comparisons.  (If null isn't < 0, does that mean null >= 0?  If not, you no longer have a well-defined ordering.)

This is reflected in the implementation: Any has an equals() method, indicating that all objects can be checked for equality.  (Kotlin's documentation makes it explicit, as does that for the underlying Java method, that non-null objects must never equal null.)  And Kotlin's implementation of the == and != operators explicitly checks for nulls.  (a == b translates to what you have to spell out in Java: a == null ? b == null : a.equals(b).)

But order comparison is handled differently.  It uses the Comparable interface: only types with a ‘natural ordering’ implement that; those that don't, can't be compared in that way.  Since null can't implement any interfaces, it can't have a natural ordering, and the compiler prevents you trying the comparison.  (Kotlin's documentation doesn't make this explicit, because the parameter is non-nullable; but that for the underlying Java interface says that such a comparison should return a NullPointerException.)

As to how you should handle this, the Elvis operator is probably the most concise solution:

if (mouseEvent?.clickCount ?: 0 >= 2)

If mouseEvent is not null, this will get its clickCount; otherwise, the safe-call ?. will give the null directly, and then the ?: will substitute 0.  (That would also happen if the clickCount held null, though that shouldn't be possible.)  In every case, you end up with a non-nullable integer that can safely be compared with 2.

Of course, in practice, nothing should ever be calling a listener method and passing a null event.  (I can't recall ever allowing for that back when I used to write Java Swing code for a living, or hitting any problems as a result.)  So a simpler alternative might be declaring the parameter as non-nullable.  But handling the null properly is just that little bit safer; and in this case, it doesn't add much extra code.  So it's up to you!

gidds
  • 16,558
  • 2
  • 19
  • 26
  • Excellent explanation! The part regarding "if null isn't <0, then is it >=0" really helped me to understand **why it couldn't be implemented**. I understood that any nullable would have trouble doing any comparison, but I felt it would not be *worse* than any "equals comparison", but your example explained why it is indeed harder to handle > or <. – Mike Williamson Mar 29 '19 at 03:20
  • Is it standard Kotlin verbiage to *not* consider an equality a comparison? This is part of what confused me with the original error I saw. In other languages, both equality and referential equality are considered comparison operators. – Mike Williamson Mar 29 '19 at 03:24
  • Thanks for mentioning the possible declaration as non-nullable. I am trying to avoid that because (1) I like that Kotlin is safer than Java, and (2) I am new and trying to avoid potential traps as much as possible. – Mike Williamson Mar 29 '19 at 03:27
  • 1
    @MikeWilliamson, I've been using ‘comparison’ to mean only comparing _for order_, in line with the `Comparable` interface.  Of course, you can use it more loosely to include equality and identity/reference checks too, but that risks further confusion!  I'll try to tweak the answer to make that clearer. – gidds Mar 29 '19 at 09:00
  • 1
    It's worth mentioning that Kotlin has a [`compareValues()`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.comparisons/compare-values.html) method which assumes null values to be lower than any other value. This allows for comparison between optional Comparable objects, but makes assumptions in order to do this. – th3ant Sep 17 '20 at 14:33
1

What behavior from mouseEvent?.clickCount >= 2 do you expect when mouseEvent == null ?

You could transform your mouseEvent?.clickCount to NotNull using elvis (?:) operator:

val clickCount = mouseEvent?.clickCount ?: 0
if (clickCount >= 2) {
    launchSelectedSample()
}

In this case mouseEvent?.clickCount will be 0 if mouseEvent is null

==2 works because mouseEvent?.clickCount considered by Kotlin as null and comparison null == 2 is correct unlike null >= 2

Taras Parshenko
  • 584
  • 1
  • 5
  • 17
1

a little bit more kotlin style can be this:

mouseEvent?.clickCount?.let{ 
  if(it >= 2) {
    launchSelectedSample()
  }
}

even more functional style:

mouseEvent?.clickCount?.takeIf { it >= 2 }?.let { launchSelectedSample() }
Kemal Cengiz
  • 133
  • 1
  • 8
0
if (compareValues(mouseEvent?.clickCount, 2) >= 0

compareValues compares two nullable Comparable values. Null is considered less than any value.

Psijic
  • 743
  • 7
  • 20