Faites Option.ou Null de Scala en Java

Faites Option.ou Null de Scala en Java

Notez qu'il n'y avait aucun moyen de changer Some (a) de Scala en ʻaetNone en null`.

C'est devenu assez long, donc si vous êtes pressé, nous vous recommandons de parcourir les titres de chaque section.

Conclusion pour le moment

Comment utiliser uniquement la méthode scala.Option qui peut être utilisée depuis Java sans utiliser l'instruction ʻif`

option.fold(() -> null, some -> some)

Cible

Contexte

Lors de la réécriture d'un programme d'une échelle raisonnable écrit en Java vers Scala étape par étape, je veux changer la partie en utilisant null en ʻOption`.

<! - Voulez-vous expliquer pourquoi vous ne voulez pas utiliser null? ->

Par exemple, envisagez de porter une méthode Java qui renvoie une extension d'une chaîne de nom de fichier vers Scala. Cette méthode renvoie la chaîne après le point si le nom du fichier contient un point (.), ou null si ce n'est pas le cas.

FileName.java


public class FileName {
    public static String getExtension(final String filename) {
        final int dotIdx = filename.lastIndexOf('.');
        if (dotIdx >= 0) {
            return filename.substring(dotIdx);
        } else {
            return null;
        }
    }
}

Voici un exemple de programme qui utilise cette classe.

Program.java


public class Program {
    public static void main(final String[] args) {
        printExtension("foo.txt");
        printExtension("hosts");
    }

    public static void printExtension(final String filename) {
        final String ext = FileName.getExtension(filename);
        if (ext != null) {
            System.out.println("\"" + filename + "\"L'extension est\"" + ext + "\"est.");
        } else {
            System.out.println("\"" + filename + "\"N'a pas d'extension.");
        }
    }
}

//Résultat de l'exécution:
// "foo.txt"L'extension est"txt"est.
// "hosts"N'a pas d'extension.

Réécrivons la classe FileName ci-dessus avec Scala. Je ne veux pas utiliser null, donc le type de retour de getExtension devrait être ʻOption [String]`.

FileName.scala


object FileName {
  def getExtension(filename: String): Option[String] =
    filename.lastIndexOf('.') match {
      case dotIdx if dotIdx >= 0 =>
        Some(filename.substring(dotIdx))
      case _ =>
        None
    }
}

Puisque le type de retour de FileName.getExtension a changé, Program.java ne se compilera pas tel quel. Porter Program sur Scala est la manière la plus légitime de le faire, mais ici nous voulons gérer le cas où la quantité de code Java est énorme et il n'est pas réaliste de tout porter sur Scala, donc Program est Pensez à utiliser FileName.getExtension d'une manière ou d'une autre en Java.

politique

Le processus de conversion de «Some (a)» en «a» et de «None» en «null» est ** Java **. C'est le but de cet article.

Je pense que c'est une pratique courante de créer une méthode d'assistance côté Scala pour ce type de processus de conversion, mais je pense que cela peut ne pas être réaliste en raison du grand nombre de méthodes à porter, alors ici. Je m'en tiendrai au ** côté Java ** en quelque sorte (si vous utilisez le macro-paradis de Scala, il générera automatiquement une méthode wrapper qui retourne ʻA ou nullau lieu de ʻOption [A]. Vous pouvez peut-être faire une annotation. Veuillez créer quelqu'un).

Main sûre: classe utilitaire

Probablement la main la plus courante.

ScalaCompat.java


import scala.Option;

public class ScalaCompat {
    public static <A> A optionOrNull(final Option<A> o) {
        if (o.isDefined())
            return o.get();
        else
            return null;
    }
}

--ʻO.isDefined () retourne true si ʻo estSome (a), et renvoie false si None. --ʻO.get () retourne ʻa si ʻo est Some (a) ʻet lance une exception si None.

Donc, utilisez d'abord ʻo.isDefined () pour vérifier si ʻo a une valeur, et après avoir découvert que c'estSome (a), retirez ʻa avec ʻo.get et Si o n'a pas de valeur, il renverra "null" sans essayer de le récupérer.

C'est sûr, mais cela présente quelques inconvénients.

a. Vous devez créer une nouvelle classe.

Le fait de devoir créer une nouvelle classe provoque les problèmes suivants.

--Choisissez un forfait

Je pense que c'est une histoire idiote, mais du point de vue de la maintenance du code, c'est étonnamment ennuyeux.

De plus, vous avez probablement déjà une bibliothèque avec des classes similaires, même si vous ne l'avez pas écrite vous-même, mais j'hésite à augmenter la dépendance juste pour cela.

b. Ne peut pas être appelé comme méthode d'instance.

Si vous faites une chaîne de méthodes avec du code qui abuse (ou ignore, pour être exact) null, l'introduction d'une classe utilitaire comme celle ci-dessus réduira considérablement la lisibilité.

Par exemple, considérez le code suivant. Supposons que toutes les méthodes get peuvent renvoyer null. (Image comme java.util.Map <String, *>)

person.get("account").get("balance").get("USD")

Remplacez cette méthode get par une méthode qui renvoie ʻOption` et essayez d'utiliser la classe utilitaire. Afin de maintenir l'opération existante, nous osons omettre la vérification de null qui est initialement requise.

ScalaCompat.optionOrNull(ScalaCompat.optionOrNull(ScalaCompat.optionOrNull(person.get("account")).get("balance")).get("USD"))

Pouvez-vous voir à quel point il est difficile de lire et d'écrire? "Check null!", "Changer la structure du code!", "Portez-le sur Scala!", Etc., mais dans la réalité des contraintes de temps, d'autres méthodes de la même classe En raison de la situation telle qu'un grand nombre, il est nécessaire de refactoriser "en Java" "sans vérifier null (= sans changer la condition que NPE se produit), et la méthode dans une telle situation La méthode statique de la chaîne et de la classe utilitaire est la pire correspondance.

Variante 1: instruction ʻif`

Ce n'est pas difficile, c'est juste une question d'utiliser l'instruction ʻif`.

final Option<Account> account = person.get("account");
if (account.isDefined()) {
  final Option<Balance> balance = account.get().get("balance");
  if (balance.isDefined()) {
    final Option<Double> usd = balance.get().get("USD");
    if (usd.isDefined()) {
      return usd;
    } else {
      return null;
    }
  }
}
throw new NullPointerException();

Le code qui était à l'origine une ligne est passé à 13 lignes. Outre l'augmentation du nombre de lignes, il est également nécessaire de spécifier le type afin de l'affecter une seule fois à une variable locale. Scala a une compréhension pour ces occasions, mais malheureusement Java ne le fait pas.

Variante 2: instruction try

ʻOption.get lance uneNoSuchElementExceptionpourNone`, donc vous pouvez l'attraper et envoyer un NPE à la place.

try {
  person.get("account").get().get("balance").get().get("USD").get();
} catch (final NoSuchElementException e) {
  throw new NullPointerException();
}

Il peut être rédigé de manière relativement concise. Cela semble bien si vous pouvez tolérer une ligne augmentant à quatre lignes. Cependant, cette approche présente des problèmes sobres mais ennuyeux. Et si person.get (" account ") lançait lui-même une NoSuchElementException au lieu de ʻOption.get ()`? Le code d'origine lève l'exception telle quelle, mais ce code la remplace également par NPE. Il détecte les exceptions qui ne devraient pas être interceptées, ce qui peut secrètement causer des bogues.

Je pense que cela dépend du cas ... Dans cet article, tout cela est considéré comme peu pratique. Ci-dessous, examinons comment rendre ʻOption`` null "en utilisant la méthode d'instance de scala.Option`".

Échec 1: ʻOption.orNull`

ʻOption a une méthode appelée ʻouNull. C'est une méthode très personnalisée qui renvoie ʻa si ʻOption elle-même est Some (a) ʻet nullpourNone, mais malheureusement, elle est difficile à utiliser depuis Java. Vous pouvez voir pourquoi en regardant la déclaration de ʻOption.orNull. Selon [Scala API Reference] api-option-ornull, la signature est

final def orNull[A1 >: A](implicit:ev<:<[Null,A1]): A1

Le problème est cet argument «implicite». En un mot, ceci est utilisé pour empêcher que l'argument de type «A» soit utilisé pour des types qui ne peuvent pas être «null», comme «Int» (description détaillée: [StackOverflow] [so-option]. -ornull]). Vous n'avez pas à vous en soucier car le compilateur remplit automatiquement l'argument ʻimplicit` pour une utilisation dans Scala, mais en Java c'est une tâche très gênante car vous devez le remplir à la main.

Et si je devais le remplir à la main? Pensons-y parce que c'est un gros problème.

Selon javap, depuis Scala-2.12.6, ʻOption.orNull` semble être compilé dans la méthode suivante:

$ javap scala/Option.class | grep orNull
  public final <A1> A1 orNull(scala.Predef$$less$colon$less<scala.runtime.Null$, A1>)
...

Vous pouvez voir que l'argument «implicite» est compilé en un simple argument. Le type est <: <, qui est une classe membre de scala.Predef $, une bibliothèque standard de Scala, et est une sous-classe de Function1.

[Bibliothèque standard Scala scala / Predef.scala] [api-predef- <: <] ne cite que les points principaux

scala/Predef.scala


object Predef {
  //Type de paramètre mystère(1)
  sealed abstract class <:<[-From, +To] extends (From => To) with Serializable
  // <:<Seule instance de(2)
  private[this] final val singleton_<:< = new <:<[Any,Any] { def apply(x: Any): Any = x }
  //Source de valeur implicite(3)
  implicit def $conforms[A]: A <:< A = singleton_<:<.asInstanceOf[A <:< A]

Je vais expliquer chaque point.

--(1): Définition du type de paramètre mystère (identité de l'argument implicite). Ce n'est pas différent de Function1 [From, To] sauf qu'il se mélange dans Serializable. --(2):(1)est la classe scellée, c'est donc la seule instance. Si vous regardez attentivement le contenu de «def apply», c'est pratiquement la même chose que «identité». --(3): fonction ʻimplicit def qui ne prend pas de paramètres de valeur. Par conséquent, comme la valeur ʻimplicit val, cette valeur est utilisée implicitement lors du passage du paramètre ʻimplicit`.

Pris ensemble, le paramètre implicite transmis implicitement par le compilateur Scala lors de l'appel de ʻOption.orNull est (3) , et son contenu est (2)` (cast), qui est effectivement En fin de compte, c'est «identité».

Pour être un peu plus précis, traçons un programme qui appelle # orNull pour None: Option [X] avec n'importe quel type X.

val o = Option.empty[X]

val x: X
  = o.orNull
  //Étant donné que le type de l'expression est X, X est sélectionné pour l'argument de type A1 de orNull.
  // <:<Est une inversion pour les arguments de type 1, Null<:Parce que c'est X(Null <:< X) >: (X <:< X)Est.
  // (X <:<X est nul<:<C'est une sous-classe de X.)
  //Par conséquent Predef.$conforms[X]: X <:<X est sélectionné pour l'argument implicite.(☆)
  = o.orNull(Predef.$conforms[X])
  = o.orNull(singleton_<:<.asInstanceOf[X <:< X])
  // singleton_<:<Notez qu'il s'agit essentiellement d'identité,
  ~= o.orNull(identity: X => X)

Cependant, singleton_ <: < est private [this], donc si vous voulez le spécifier explicitement, vous pouvez spécifier Predef. $ Conforms [X]. Si vous écrivez ceci en Java

scala.Predef.<X>$conforms()

Cela signifie que.

Pour résumer ce qui précède, lorsque vous appelez ʻOption.ouNull` en Java, c'est comme suit.

final scala.Option<A> option = ...;
final A aOrNull = option.orNull(scala.Predef.<A>$conforms());

Je vois. Mais cela ne compile pas. ʻA <: <B est rebelle à propos de ʻA car il est uniquement dans Scala et est traité comme immuable en Java. Le type de scala.Predef. <A> $ conforms () est scala.Predef $$ less $ common $ less <A, A> lorsqu'il est écrit en Java, mais le type d'argument implicite est <A1> scala Puisque .Predef $$ less $ common $ less <scala.runtime.Null $, A1> , ʻA et ʻA1 doivent êtrescala.runtime.Null $.

Par conséquent, cela devrait être comme suit.

final scala.Option<A> option = ...;
final A aOrNull = (A)(Object) option.orNull(scala.Predef.<scala.runtime.Null$>$conforms());

Si vous réécrivez Program.java en utilisant ceci, ce sera comme suit.

Program.java


public class Program {
    public static void main(final String[] args) {
        printExtension("foo.txt");
        printExtension("hosts");
    }

    public static void printExtension(final String filename) {
        final String ext = (String)(Object) FileName.getExtension(filename).orNull(scala.Predef.<scala.runtime.Null$>$conforms());
        if (ext != null) {
            System.out.println("\"" + filename + "\"L'extension est\"" + ext + "\"est.");
        } else {
            System.out.println("\"" + filename + "\"N'a pas d'extension.");
        }
    }
}

//Résultat de l'exécution:
// "foo.txt"L'extension est"txt"est.
// "hosts"N'a pas d'extension.

Je l'ai essayé avec une demi-blague, mais le résultat était plus piraté que ce à quoi je m'attendais. La raison pour laquelle ce programme fonctionne (devient) comme prévu est que Predef.singleton_ <: < est en fait une "identité" et ne connaît absolument pas les types à la compilation et à l'exécution.

En premier lieu, l'argument implicite de ʻOption.ouNull n'a pas de signification particulière dans la valeur elle-même (c'est juste ʻidentité), mais dans "si la valeur existe ou non". Par exemple, comme contre-exemple, si vous essayez d'appeler ʻOption [Int] .ouNull, le compilateur recherchera une valeur de type Null <: <Int. Cependant, la seule valeur implicite, ʻimplicit def $ conforms [A]: A <: <Si vous remplacez ʻInt par ʻA de A, vous obtenez ʻInt <: <Int, mais Null <: IntNe tient pas, donc(Null <: <Int)>: (Int <: <Int)ne tient pas (ce qui précède(☆) échoue). Par conséquent, l'argument implicite correspondant à ʻOption [Int] .orNull ne peut pas être trouvé, et il est possible de faire une erreur ** à la compilation **.

En Java (non expliqué dans Scala), vous pouvez forcer l'argument implicite à être passé manuellement en abusant du cast, mais le contrôle à la compilation ne fonctionnera pas.

Si, pour une raison quelconque, vous devez absolument appeler ʻOption.ouNull` depuis Java, il peut être judicieux de l'essayer en dernier recours. Je ne le recommande pas du tout.

Échec 2: ʻOption.getOrElse (null) `

Il existe plusieurs méthodes de type ʻOption [A] => A ou proches de celle-ci (ʻor Null est l'une d'entre elles), mais la méthode typique est getOrElse.

En regardant la déclaration de ʻOption.getOrElse` dans [API Reference] api-option-getorelse:

final def getOrElse[B >: A](default:=>B): B

Contrairement à ʻou Null, il n'y a pas d'argument implicite. L'argument default est la valeur renvoyée à la place si ʻOption est None, il semble donc que vous deviez spécifier null.

TooBad_NPE.java


public class TooBad_NPE {
    public static void main(final String[] args) {
        final scala.Option<String> some = scala.Option.apply("foo");  // Some("foo")
        final String foo = some.getOrElse(null);
        System.out.println(foo);                                      // foo
        final scala.Option<String> none = scala.Option.apply(null);   // None
        final String shouldBeNull = none.getOrElse(null);             // !! throw NullPointerException !!
        System.out.println(shouldBeNull);
    }
}

La compilation passera. Cela fonctionne également comme prévu lorsque «Option» est «Some». Cependant, si ʻOption est ʻAucune, NPE sera envoyé.

La cause est toujours dans la signature getOrElse. Si vous regardez attentivement la déclaration, elle dit default:=>B. Le type de «default» est «B», mais il est passé par nom («=>»). Les arguments passés par nom sont des arguments spéciaux, et l'évaluation (exécution) de l'argument se fait dans la méthode appelée, pas dans l'appelant. Java n'a pas cette fonctionnalité, donc il doit avoir été compilé dans quelque chose de spécial! Jetons un œil à javap:

$ javap scala/Option.class | grep getOrElse
  public final <B> B getOrElse(scala.Function0<B>)

Il s'avère donc que l'argument de passage par nom => B est compilé dans scala.Function0 <B> (c'est-à-dire la fonction d'argument zéro () => B). Cela signifie que passer null à l'argument default degetOrElse [B](par défaut: => B)signifie que ** null n'est pas B, mais `Function0 [ B] ʻest **. «B» et «Function0 » sont des sous-classes de «Object» pour Java, donc ils seront compilés. Puisque "un objet qui devrait vous dire ce qu'il faut retourner dans le cas de" Aucun "" est "nul", NPE se produira dans le cas de "Aucun".

… Il semble donc que vous ayez juste à écrire l'objet correspondant à () => null: Function0 [B] en Java.

Échec 3: ʻOption.getOrElse (() -> null) `

Grâce à la "conversion SAM (Single Abstract Method)" introduite dans Java8, les expressions Java lambda sont directement converties en scala.Function1. Vous pouvez donc écrire en Java:

final scala.Option<String> some = scala.Option.apply("foo");  // Some("foo")
System.out.println(some.getOrElse(() -> null));               // foo
final scala.Option<String> none = scala.Option.apply(null);   // None
System.out.println(none.getOrElse(() -> null));               // null

Vous pouvez appeler getOrElse presque comme Scala. Je vous remercie. Il semble que nous ayons enfin réalisé ce que nous attendions, mais malheureusement, il reste des pièges. En fait, ʻOption.getOrElse` n'est pas * de type sécurisé en * Java.

TooBad_CCE.java


import java.util.Date;

public class TooBad_CCE {
    public static void main(final String[] args) {
        final scala.Option<String> some = scala.Option.apply("foo");  // Some("foo")
        final String foo = some.getOrElse(() -> null);
        System.out.println(foo);                                      // foo
        final scala.Option<String> none = scala.Option.apply(null);   // None
        final String shouldBeNull = none.getOrElse(() -> null);
        System.out.println(shouldBeNull);                             // null
        //Jusqu'à ce point est OK

        //Le contenu d'Option est Date, mais le destinataire est String
        final scala.Option<Date> now = scala.Option.apply(new Date());
        final String nowstr = now.getOrElse(() -> null);              // !! throw ClassCastException !!
    }
}

Puisque now est de type ʻOption , le type de now.getOrElse (...)devrait êtreDate, mais il se compilera même s'il est reçu par un autre type comme décrit ci-dessus. Je vais finir. J'obtiens une ClassCastException` au moment de l'exécution.

Pourquoi cela arrive-t-il? Examinons maintenant de plus près la référence API de Scala et la sortie de javap.

apidoc.scala


final def getOrElse[B >: A](default:=>B): B

javap.java


public final <B> B getOrElse(scala.Function0<B>)

Il y a une différence dans les contraintes imposées à «B». Dans Scala [B>: A], c'est-à-dire que B doit être une superclasse de ʻA, mais en Java c'est juste , c'est-à-dire que B est ʻA. Vous pouvez choisir un type non pertinent. Dans l'exemple TooBad_CCE.java, String est sélectionné pour B dans le type de retour, mais ce n'est pas un problème pour le compilateur Java. Par conséquent, il compile et le type est en fait différent, donc une exception se produit au moment de l'exécution.

Pour le moment, il est possible de générer une erreur de compilation en spécifiant explicitement l'argument génériques.

final scala.Option<Date> now = scala.Option.apply(new Date());
final String nowstr_ct = now.<Date>getOrElse(() -> null);    // compilation error
final String nowstr_rt = now.<String>getOrElse(() -> null);  // !! throw ClassCastException !!

Cependant, c'est beaucoup de travail, et si vous spécifiez par erreur le type de destinataire au lieu du type d'objet ʻOption`, vous obtiendrez toujours une erreur d'exécution, ce qui provoquera des erreurs imprudentes.

Il s'avère que getOrElse fonctionne comme prévu, mais perd la sécurité de type. Cela provoque des bogues difficiles à trouver et doit être évité si possible.

Bonne réponse: ʻOption.fold (() -> null, some-> some) `

ʻOption a une méthode appelée fold`. Dans [API Reference] api-option-fold:

final def fold[B](ifEmpty:=>B)(f:(A)=>B): B

Si ʻOption vaut None, ʻifEmpty est évalué, et siSome (a), f (a) ʻest renvoyé. Si vous spécifiez ʻidentity pour f, il se comporte de la même manière que getOrElse. Le fait est que ʻA et B apparaissent dans la deuxième section de paramètre, donc si vous donnez une fonction avec le type ʻA => A, vous pouvez spécifier B sans spécifier explicitement le paramètre de type. Il peut être fixé à «A».

Cette méthode se compile en une méthode à deux arguments. Depuis javap:

$ javap scala/Option.class | grep fold
  public final <B> B fold(scala.Function0<B>, scala.Function1<A, B>);

Donc, il semble bon de spécifier une fonction qui renvoie null comme premier argument et une fonction qui renvoie l'argument tel quel comme deuxième argument. Faisons le.

WorksAsIntended.java


import java.util.Date;

public class WorksAsIntended {
    public static void main(final String[] args) {
        final scala.Option<Date> now = scala.Option.apply(new Date());
        // final String nowstr = now.fold(() -> null, some -> some);  // compilation error
        final Date nowdate  = now.fold(() -> null, some -> some);  // ok, now
        final scala.Option<Date> none = scala.Option.apply(null);
        final Date nulldate = none.fold(() -> null, some -> some); // ok, null
        System.out.println(nowdate);   //Heure actuelle
        System.out.println(nulldate);  // null
    }
}

(Vous pouvez utiliser n'importe quel nom au lieu de some, mais j'ai essayé ce nom pour préciser qu'il gère le cas de Some.)

C'est un peu long, mais j'ai pu obtenir une méthode plus sûre de type "en utilisant la méthode d'instance scala.Option".

A part: si l'inférence de type échoue

Si le compilateur Java ne peut pas effectuer d'inférence de type, par exemple lors de sa transmission en tant qu'argument à une méthode surchargée, vous devez spécifier explicitement l'argument génériques.

Cependant, même dans ce cas, la sécurité de type est maintenue, contrairement au cas de ʻOption.getOrElse (() -> null) `, car si le mauvais argument générique est spécifié, une erreur de compilation se produira.

ExplicitGenParam.java


import java.util.Date;

public class ExplicitGenParam {
    public static void main(final String[] args) {
        final scala.Option<Date> now = scala.Option.apply(new Date());
        println(now.fold(() -> null, some -> some));          // compilation error, ambiguous
        println(now.<Date>fold(() -> null, some -> some));    // OK,Heure actuelle
        println(now.<String>fold(() -> null, some -> some));  // compilation error, incompatible types
    }

    public static void println(final Date x) {
        System.out.println(x);
    }

    public static void println(final String x) {
        System.out.println(x);
    }
}

Conclusion

Comment changer l '«Option [A]» de Scala en «A» ou «null» en Java

option.fold(() -> null, some -> some)

Est recommandé.

Utilisons ceci pour réécrire Program.java dans la section" Arrière-plan "de FileName.scala.

Program.java


public class Program {
    public static void main(final String[] args) {
        printExtension("foo.txt");
        printExtension("hosts");
    }

    public static void printExtension(final String filename) {
        final String ext = FileName.getExtension(filename).fold(() -> null, some -> some); //Cela a changé
        if (ext != null) {
            System.out.println("\"" + filename + "\"L'extension est\"" + ext + "\"est.");
        } else {
            System.out.println("\"" + filename + "\"N'a pas d'extension.");
        }
    }
}

//Résultat de l'exécution:
// "foo.txt"L'extension est"txt"est.
// "hosts"N'a pas d'extension.

Je suis heureux.

Leçon

J'en ai tiré quelques leçons, je vais donc les résumer.

--ʻOption.orNull est pratiquement inutilisable depuis Java. --L'argument de passage de nom (=> B) est compilé dans scala.Function0 , donc lorsque vous passez null, passez l'expression lambda () -> null`.

  • La sécurité de type peut ne pas être maintenue lors de l'appel d'une méthode avec des paramètres de type de limite supérieure et inférieure ([B>: A]) de Scala à partir de Java.

Cependant, si vous écrivez le programme entier dans Scala, tout le travail ci-dessus sera inutile, donc tout le monde, veuillez considérer Scala lors du développement d'un nouveau.

Recommended Posts