2009-10-05 11 views
33

Sto imparando Scala e ho un progetto Java per migrare a Scala. Voglio migrarlo riscrivendo le classi una alla volta e controllando che la nuova classe non abbia infranto il progetto.Java <-> Scala interop: conversione elenco e mappa trasparente

Questo progetto Java utilizza molto java.util.List e java.util.Map. Nelle nuove classi di Scala mi piacerebbe usare lo List di Scala e lo Map per avere un codice Scala di bell'aspetto.

Il problema è che le nuove classi (quelle che sono scritte in Scala) non si integrano perfettamente con il codice Java esistente: Java ha bisogno di java.util.List, Scala ha bisogno del proprio scala.List.

Ecco un esempio semplificato del problema. Esistono classi Principale, Logica, Dao. Si chiamano in una riga: 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, le classi principale e Dao sono classi del framework (Non ho bisogno di migrare loro). Classe La logica è business-logic e trarrà un grande vantaggio dalle funzionalità di Scala.

Ho bisogno di riscrivere classe Logic a Scala, preservando l'integrità con le classi principale e Dao. La migliore riscrittura sarà simile (non funziona):

class Logic2 { 
    def calculate(ints: List[Integer]) : List[Integer] = { 
     val together: List[Integer] = new Dao().getSomeInts() 
     together ++ ints 
    } 
} 

comportamento ideale: liste all'interno Logic2 sono nativi Scala Liste. Tutto dentro/fuori java.util.Lists ottiene un box/unboxed automaticamente. Ma questo non funziona.

Invece, questo fa di lavoro (grazie alla 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 si ottiene la conversione magica trasparente di elenchi e mappe tra Java < -> Scala (senza necessità di eseguire toScala/toJava)?

Se non è possibile, quali sono le migliori pratiche per la migrazione di Java -> codice Scala che utilizza java.util.List e gli amici?

risposta

65

Fidati di me; non si desidera conversione trasparente avanti e indietro. Questo è esattamente ciò che le funzioni scala.collection.jcl.Conversions hanno tentato di fare. In pratica, causa un sacco di mal di testa.

La radice del problema con questo approccio è che Scala invierà automaticamente le conversioni implicite necessarie per far funzionare un metodo call. Questo può avere delle 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 luogo per chi è nuovo alle collezioni quadro Scala o anche solo il concetto di collezioni immutabili. Sfortunatamente, è completamente sbagliato. Il risultato di questa funzione è la stessa mappa. La chiamata a put attiva una conversione implicita in java.util.Map<String, Int>, che accetta felicemente i nuovi valori e viene prontamente eliminata. L'originale map non è modificato (come è, infatti, immutabile).

Jorge Ortiz mette meglio quando dice che si deve solo definire conversioni implicite per uno dei due scopi:

  • aggiunta di membri (metodi, campi, ecc). Queste conversioni dovrebbero corrispondere a un nuovo tipo non correlato a qualsiasi altra cosa nell'ambito.
  • "Correzione" di una gerarchia di classi danneggiate. Pertanto, se si dispone di alcuni tipi A e B che non sono correlati. È possibile definire una conversione A => B se e solo se si preferisce avere A <: B (<: significa "sottotipo").

Dal momento che il java.util.Map non è ovviamente un nuovo tipo non correlato a qualcosa nella nostra gerarchia, non possiamo cadere sotto la prima riserva. Pertanto, la nostra unica speranza è la nostra conversione Map[A, B] => java.util.Map[A, B] per qualificarsi per il secondo. Tuttavia, non ha assolutamente senso per lo Map di Scala ereditare da java.util.Map. Sono davvero interfacce/tratti completamente ortogonali. Come dimostrato sopra, il tentativo di ignorare queste linee guida produrrà quasi sempre un comportamento strano e inaspettato.

La verità è che i metodi javautils asScala e asJava sono stati progettati per risolvere questo problema esatto. Esiste una conversione implicita (in realtà molte di esse) in javautils da Map[A, B] => RichMap[A, B]. RichMap è un tipo nuovo di zecca definito da javautils, quindi il suo unico scopo è quello di aggiungere membri a Map. In particolare, aggiunge il metodo , che restituisce una mappa wrapper che implementa java.util.Map e delega all'istanza originale Map. Ciò rende il processo molto più esplicito e molto meno soggetto a errori.

In altre parole, l'utilizzo di asScala e asJavaè la procedura consigliata. Avendo disceso entrambe queste strade in modo indipendente in un'applicazione di produzione, posso dirti in prima persona che l'approccio di javautils è molto più sicuro e più facile da utilizzare. Non cercare di eludere le sue protezioni solo per salvare te stesso 8 personaggi!

+1

molto ben inserito, +1 – geowa4

+1

(+1) molto ben indicato. Vorrei che questo post fosse stato in giro quando ho lottato con esso qualche tempo fa. – Shaun

+1

Grazie per questo scrupolo approfondito. Trovato tramite google (ovviamente!) quindi hai già salvato un sacco di dolore a qualcuno! –

3

Ecco alcuni rapidi esempi di utilizzo di Jorge Ortiz scalaj-collection library:

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] 

progetto javautils è disponibile dal central maven repository

2

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 
Problemi correlati