Nulo se esgueirou: quebrando a opção Scala usando Java

Saudações, Habr! Trago à sua atenção um pequeno artigo de sexta-feira sobre Java, Scala, programadores malucos e promessas quebradas.




Às vezes, observações simples levam a perguntas não muito simples.


Aqui, por exemplo, é um fato simples e externo, talvez até trivial, que afirma que em Java você pode estender qualquer classe não final e qualquer interface no escopo. E outro, também bastante simples, dizendo que o código Scala compilado para a JVM pode ser usado a partir do código Java.


A combinação desses dois fatos, no entanto, me fez pensar: como uma classe se comportará do ponto de vista do Java, que em Scala está selado, ou seja, não pode ser expandido com código externo em relação ao seu próprio arquivo?



Classe Scala descompilada na visĂŁo do artista. Fonte: https://specmahina.ru/wp-content/uploads/2018/08/razobrannaya-benzopila.jpg


Como coelho experimental, participei da aula padrão Option. Ao alimentá-lo com o descompilador incorporado no IntelliJ Idea, obtemos algo assim:


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

O código descompilado, no entanto, não será um código Java válido - um problema semelhante ao descrito aqui , por exemplo, com este método:


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.


De fato, tudo o que este artigo era necessário era um pequeno experimento para revelar uma nuance da interação de diferentes idiomas em uma JVM. A nuance, com um pouco de reflexão, é óbvia, mas - para meu gosto, ainda é interessante.


All Articles