Null hat sich eingeschlichen: Scala Option mit Java brechen

Grüße, Habr! Ich mache Sie auf einen kurzen Artikel über Java, Scala, verrückte Programmierer und gebrochene Versprechen aufmerksam.




Einfache Beobachtungen führen manchmal zu nicht sehr einfachen Fragen.


Hier ist zum Beispiel eine einfache und äußerliche, vielleicht sogar triviale Tatsache, die besagt, dass Sie in Java jede nicht endgültige Klasse und jede Schnittstelle im Umfang erweitern können. Und eine andere, ebenfalls recht einfache Aussage, dass Scala-Code, der für die JVM kompiliert wurde, aus Java-Code verwendet werden kann.


Die Kombination dieser beiden Tatsachen ließ mich jedoch fragen: Wie wird sich eine Klasse unter dem Gesichtspunkt von Java verhalten, das in Scala versiegelt ist, d. H. kann nicht mit externem Code relativ zu seiner eigenen Datei erweitert werden?



Dekompilierte Scala-Klasse aus Sicht des Künstlers. Quelle: https://specmahina.ru/wp-content/uploads/2018/08/razobrannaya-benzopila.jpg


Als experimentelles Kaninchen nahm ich die Standardklasse Option. Wenn wir es dem in IntelliJ Idea integrierten Dekompiler zuführen, erhalten wir ungefähr Folgendes:


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

Dekompilierter Code ist jedoch kein gültiger Java-Code - ein Problem, das dem hier beschriebenen ähnlich ist , beispielsweise mit dieser Methode:


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.


Tatsächlich war alles, wofür dieser Artikel benötigt wurde, ein kleines Experiment, um eine Nuance der Interaktion zwischen verschiedenen Sprachen auf einer JVM aufzudecken. Die Nuance ist mit ein wenig Nachdenken offensichtlich, aber - für meinen Geschmack immer noch interessant.


All Articles