Interoperabilità Java <-> Scala:conversione trasparente di elenchi e mappe
-
19-09-2019 - |
Domanda
Sto imparando Scala e ho un progetto Java da migrare a Scala.Voglio migrarlo riscrivendo le classi una per una e verificando che la nuova classe non abbia interrotto il progetto.
Questo progetto Java ne utilizza molti java.util.List
E java.util.Map
.Nelle nuove lezioni di Scala vorrei usare quelle di Scala List
E Map
per avere un codice Scala di bell'aspetto.
Il problema è che le nuove classi (quelle scritte in Scala) non si integrano perfettamente con il codice Java esistente:Java ha bisogno java.util.List
, Scala ha bisogno del suo scala.List
.
Ecco un esempio semplificato del problema.Ci sono lezioni Principale, Logica, Dao.Si chiamano in fila: Principale -> Logica -> Dao.
public class Main {
public void a() {
List<Integer> res = new Logic().calculate(Arrays.asList(1, 2, 3, 4, 5));
}
}
public class Logic {
public List<Integer> calculate(List<Integer> ints) {
List<Integer> together = new Dao().getSomeInts();
together.addAll(ints);
return together;
}
}
public class Dao {
public List<Integer> getSomeInts() {
return Arrays.asList(1, 2, 3);
}
}
Nella mia situazione, lezioni Principale E Dao sono classi framework (non ho bisogno di migrarle).Classe Logica è una logica aziendale e trarrà grandi benefici dalle interessanti funzionalità di Scala.
Devo riscrivere la lezione Logica in Scala preservando l'integrità delle classi Principale E Dao.La migliore riscrittura sarebbe simile (non funziona):
class Logic2 {
def calculate(ints: List[Integer]) : List[Integer] = {
val together: List[Integer] = new Dao().getSomeInts()
together ++ ints
}
}
Comportamento ideale:Elenchi all'interno Logica2 sono liste Scala native.Tutto dentro/fuori java.util.Lists
ottenere boxed/unboxed automaticamente.Ma questo non funziona.
Invece, questo funziona (grazie a scala-javautils (GitHub)):
import org.scala_tools.javautils.Implicits._
class Logic3 {
def calculate(ints: java.util.List[Integer]) : java.util.List[Integer] = {
val together: List[Integer] = new Dao().getSomeInts().toScala
(together ++ ints.toScala).toJava
}
}
Ma sembra brutto.
Come posso ottenere una conversione magica trasparente di elenchi e mappe tra Java <-> Scala (senza bisogno di fare toScala/toJava)?
Se non è possibile, quali sono le migliori pratiche per la migrazione del codice Java -> Scala che utilizza java.util.List
e amici?
Soluzione
Fidati di me;tu no Volere conversione trasparente avanti e indietro.Questo è esattamente ciò che scala.collection.jcl.Conversions
funzioni tentate di eseguire.In pratica provoca non pochi mal di testa.
La radice del problema con questo approccio è che Scala inserirà automaticamente le conversioni implicite necessarie per far funzionare una chiamata al metodo.Ciò può avere conseguenze davvero sfortunate.Per esempio:
import scala.collection.jcl.Conversions._
// adds a key/value pair and returns the new map (not!)
def process(map: Map[String, Int]) = {
map.put("one", 1)
map
}
Questo codice non sarebbe del tutto fuori dal comune per chi è nuovo al framework delle raccolte Scala o anche solo al concetto di raccolte immutabili.Sfortunatamente, è completamente sbagliato.Il risultato di questa funzione è il Stesso carta geografica.La chiamata a put
attiva una conversione implicita in java.util.Map<String, Int>
, che accetta volentieri i nuovi valori e viene prontamente scartata.L'originale map
non è modificato (poiché è, infatti, immutabile).
Jorge Ortiz lo spiega meglio quando dice che dovresti definire le conversioni implicite solo per uno dei due scopi:
- Aggiunta di membri (metodi, campi, ecc.).Queste conversioni dovrebbero essere in un nuovo tipo non correlato a qualsiasi altra cosa nell'ambito.
- "Riparare" una gerarchia di classi rotta.Quindi, se hai alcuni tipi
A
EB
che non sono correlati.Puoi definire una conversioneA => B
se e soltanto se avresti preferito averloA <: B
(<:
significa "sottotipo").
Da java.util.Map
ovviamente non è un nuovo tipo estraneo a qualsiasi cosa nella nostra gerarchia, non possiamo rientrare nella prima condizione.Pertanto, la nostra unica speranza è la nostra conversione Map[A, B] => java.util.Map[A, B]
qualificarsi per la seconda.Tuttavia, per Scala non ha assolutamente senso Map
ereditare da java.util.Map
.Sono davvero interfacce/tratti completamente ortogonali.Come dimostrato sopra, tentare di ignorare queste linee guida si tradurrà quasi sempre in comportamenti strani e inaspettati.
La verità è che javautils asScala
E asJava
metodi sono stati progettati per risolvere questo problema esatto.Esiste una conversione implicita (alcune in realtà) in javautils from Map[A, B] => RichMap[A, B]
. RichMap
è un tipo completamente nuovo definito da javautils, quindi il suo unico scopo è aggiungere membri a Map
.In particolare, aggiunge il asJava
metodo, che restituisce una mappa wrapper che implementa java.util.Map
e delega al tuo originale Map
esempio.Ciò rende il processo molto più esplicito e molto meno soggetto a errori.
In altre parole, utilizzando asScala
E asJava
È la migliore pratica.Avendo percorso entrambe queste strade in modo indipendente in un'applicazione di produzione, posso dirti in prima persona che l'approccio Javautils è molto più sicuro e più facile da utilizzare.Non cercare di aggirare le sue protezioni solo per il gusto di risparmiare 8 personaggi!
Altri suggerimenti
Ecco alcuni rapidi esempi utilizzando scalaj-collezione della biblioteca di Jorge Ortiz:
import org.scala_tools.javautils.Implicits._
val sSeq = java.util.Collections.singletonList("entry") asScala
// sSeq: Seq[String]
val sList = sSeq toList // pulls the entire sequence into memory
// sList: List[String]
val sMap = java.util.Collections.singletonMap("key", "value") asScala
// sMap: scala.collection.Map[String, String]
val jList = List("entry") asJava
// jList: java.util.List[String]
val jMap = Map("key" -> "value") asJava
// jMap: java.util.Map[String, String]
il progetto javautils è disponibile dal Maven centrale repository
Con Scala 2.8, potrebbe essere fatto in questo modo:
import scala.collection.JavaConversions._
val list = new java.util.ArrayList[String]()
list.add("test")
val scalaList = list.toList