Null menyelinap: melanggar Opsi Scala menggunakan Java

Salam, Habr! Saya membawa Anda perhatian pada artikel Jumat pendek tentang Java, Scala, programmer gila dan janji-janji rusak.




Pengamatan sederhana terkadang menghasilkan pertanyaan yang tidak terlalu sederhana.


Di sini, misalnya, adalah fakta sederhana dan lahiriah, bahkan mungkin sepele yang menyatakan bahwa di Jawa Anda dapat memperluas kelas non-final dan antarmuka apa pun dalam cakupan. Dan yang lain, juga cukup sederhana, mengatakan bahwa kode Scala yang dikompilasi untuk JVM dapat digunakan dari kode Java.


Namun, kombinasi dari dua fakta ini membuat saya bertanya-tanya: bagaimana kelas mana pun akan berperilaku dari sudut pandang Jawa, yang dalam Scala disegel, yaitu: tidak dapat diperluas dengan kode eksternal relatif terhadap file sendiri?



Kelas Scala yang didekompilasi dalam pandangan artis. Sumber: https://specmahina.ru/wp-content/uploads/2018/08/razobrannaya-benzopila.jpg


Sebagai kelinci percobaan, saya mengambil kelas standar Option. Dengan memasukkannya ke dekompiler yang ada di IntelliJ Idea, kita mendapatkan sesuatu seperti ini:


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

Namun, kode yang didekompilasi tidak akan menjadi kode Java yang valid - masalah yang mirip dengan yang dijelaskan di sini , misalnya, dengan metode ini:


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.


Faktanya, semua yang dibutuhkan oleh artikel ini adalah percobaan kecil untuk mengungkapkan satu nuansa interaksi berbagai bahasa pada satu JVM. Nuansa, dengan sedikit pemikiran, jelas, tetapi - untuk selera saya, masih menarik.


All Articles