Null sneaked up: breaking Scala Option using Java

Greetings, Habr! I bring to your attention a short Friday article about Java, Scala, crazy programmers and broken promises.




Simple observations sometimes lead to not very simple questions.


Here, for example, is a simple and outwardly, perhaps even trivial fact that states that in Java you can extend any non-final class and any interface in scope. And another, also quite simple, saying that Scala code compiled for the JVM can be used from Java code.


The combination of these two facts, however, made me wonder: how will any class behave from the point of view of Java, which in Scala is sealed, i.e. cannot be expanded with external code relative to its own file?



Decompiled Scala-class in the artist's view. Source: https://specmahina.ru/wp-content/uploads/2018/08/razobrannaya-benzopila.jpg


As an experimental rabbit, I took the standard class Option. Feeding it to the decompiler built into IntelliJ Idea, we get something like this:


//  ,     
public abstract class Option 
implements IterableOnce, Product, Serializable {
    //   
    public abstract Object get();
    //    
}

Decompiled code, however, will not be valid Java code - a problem similar to that described here , for example, with this method:


public List toList() {
    return (List)(
        this.isEmpty() 
            ? scala.collection.immutable.Nil..MODULE$  
            : new colon(this.get(), scala.collection.immutable.Nil..MODULE$)
    );
}

MODULE$ , package object. , , Java , ?


, , ...


Java ( Maven), Scala provided- β€” , , , :


<dependency>
    <groupId>org.scala-lang</groupId>
    <artifactId>scala-library</artifactId>
    <version>2.13.1</version>
    <scope>provided</scope>
</dependency>

, scala.Option. Idea, , Scala , sealed-, , , :


package hack;

public class Hacking<T> extends scala.Option<T> {
    @Override
    public T get() {
        return null;
    }

    public int productArity() {
        return 0;
    }

    public Object productElement(int n) {
        return null;
    }

    public boolean canEqual(Object that) {
        return false;
    }
}

- , Option Product.


, β€” . mvn package β€” , , jar-, , Java, , , .


, Scala.


… scala-


Scala (, SBT) β€” lib , , , ; build.sbt , Idea. ( ) :


import hack.Hacking

object Main {
  def main(args: Array[String]): Unit = {
    implicit val opt: Option[String] = new Hacking()
    //    
  }

  private def tryDo[T](action: => T): Unit = {
    try {
      println(action)
    } catch {
      case e: Throwable => println(e.toString)
    }
  }
}

implicit var .


tryDo β€” , : , , . call-by-name tryDo , , .


, match β€” , sealed class- ( , sealed-, ?)


object Main {
  def main(args: Array[String]): Unit = {
    // snip
    tryMatch
  }

  private def tryDo[T](action: => T): Unit = {
    // snip
  }

  private def tryMatch(implicit opt: Option[String]): Unit = tryDo {
    opt match {
      case Some(inner) => inner
      case None => "None"
    }
  }
}

:


scala.MatchError: hack.Hacking

, : Option β€” sealed class, ( case Some case None), :


[warn] $PATH/Main.scala:22:5: match may not be exhaustive.
[warn] It would fail on the following input: None
[warn]     opt match {
[warn]     ^

, .


, - Option:


object Main {
  def main(args: Array[String]): Unit = {
    // snip
    tryMap
  }

  private def tryDo[T](action: => T): Unit = {
    // snip
  }

  private def tryMap(implicit opt: Option[String]): Unit = 
    tryDo(opt.map(_.length))
}

:


java.lang.NullPointerException

, map:


sealed abstract class Option[+A] /* extends ... */ {
  //   . ,  None -  .
  final def isEmpty: Boolean = this eq None
  //  ,       null.
  def get: A
  // , ,  map.
  @inline final def map[B](f: A => B): Option[B] =
    if (isEmpty) None else Some(f(this.get))
}

, :


  • β€” None. , isEmpty false.
  • , this.get, null.
  • null , β€” length.
  • length null NPE.

, NPE , Java ? (, , , ...)


:


object Main {
  def main(args: Array[String]): Unit = {
    // snip
    tryContainsNull
  }

  private def tryDo[T](action: => T): Unit = {
    // snip
  }

  private def tryContainsNull(implicit opt: Option[String]): Unit =
    tryDo(opt.contains(null))
}

(, , null) , contains -Nullable . , , , Option false β€” null. ?


:


true

, , , contains map: !isEmpty && this.get == elem.



, . , null , Option ( , , ) else match.


In fact, all that this article was needed for was a small experiment to reveal one nuance of the interaction of different languages ​​on one JVM. The nuance, with a little thought, is obvious, but - for my taste, still interesting.


All Articles