Null sneaked up: Breaking Scala Option using Java

Salutations, Habr! J'attire votre attention sur un court article du vendredi sur Java, Scala, les programmeurs fous et les promesses non tenues.




Des observations simples conduisent parfois Ă  des questions pas trĂšs simples.


Voici, par exemple, un fait simple et extĂ©rieur, peut-ĂȘtre mĂȘme trivial, qui stipule qu'en Java, vous pouvez Ă©tendre n'importe quelle classe non finale et n'importe quelle interface de portĂ©e. Et un autre, Ă©galement assez simple, disant que le code Scala compilĂ© pour la JVM peut ĂȘtre utilisĂ© Ă  partir du code Java.


La combinaison de ces deux faits, cependant, m'a fait me demander: comment une classe se comportera-t-elle du point de vue de Java, qui dans Scala est scellĂ©, c'est-Ă -dire ne peut pas ĂȘtre dĂ©veloppĂ© avec du code externe par rapport Ă  son propre fichier?



DĂ©compilation de la classe Scala selon l'artiste. Source: https://specmahina.ru/wp-content/uploads/2018/08/razobrannaya-benzopila.jpg


En tant que lapin expérimental, j'ai suivi le cours standard Option. En l'introduisant dans le décompilateur intégré à IntelliJ Idea, nous obtenons quelque chose comme ceci:


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

Le code décompilé, cependant, ne sera pas un code Java valide - un problÚme similaire à celui décrit ici , par exemple, avec cette méthode:


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.


En fait, tout ce dont cet article avait besoin, c'était d'une petite expérience pour révéler une nuance de l'interaction de différents langages sur une seule machine virtuelle Java. La nuance, avec un peu de réflexion, est évidente, mais - à mon goût, toujours intéressante.


All Articles