Java ↔ Scala interop: прозрачный список и преобразование карты

Я изучаю Scala, и у меня есть проект Java для перехода на Scala. Я хочу перенести его путем перезаписи классов один за другим и проверки того, что новый класс не нарушил проект.

В этом Java-проекте используются много java.util.List и java.util.Map. В новых классах Scala я хотел бы использовать Scala s List и Map, чтобы иметь красивый код Scala.

Проблема в том, что новые классы (те, что написаны в Scala) не интегрируются без использования существующего Java-кода: Java нуждается в java.util.List, Scala нуждается в собственном scala.List.

Вот упрощенный пример проблемы. Есть классы Main, Logic, 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);
    }
}

В моей ситуации классы Main и Dao являются рамковыми классами (мне не нужно их переносить). Class Logic является бизнес-логикой и принесет много пользы от Scala интересных функций.

Мне нужно переписать класс Logic в Scala, сохраняя целостность с классами Main и Dao. Лучшая переписка будет выглядеть (не работает):

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

Идеальное поведение: списки внутри Logic2 являются родными Scala списками. Все входы/выходы java.util.Lists автоматически загружаются в коробку/распаковываются. Но это не работает.

Вместо этого это работает (спасибо 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
  }
}

Но это выглядит уродливо.

Как добиться прозрачного магического преобразования списков и карт между Java ↔ Scala (без необходимости делать toScala/toJava)?

Если это невозможно, каковы наилучшие методы переноса кода Java → Scala, который использует java.util.List и друзей?

Ответы

Ответ 1

Поверь мне; вы не хотите прозрачного преобразования взад и вперед. Это то, что пытались выполнить функции scala.collection.jcl.Conversions. На практике это вызывает много головных болей.

Корень проблемы с этим подходом Scala будет автоматически вводить неявные преобразования, если необходимо, чтобы заставить работать вызов метода. Это может иметь некоторые действительно неудачные последствия. Например:

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
}

Этот код не будет полностью не соответствовать характеру для тех, кто не знаком с картой коллекций Scala или даже просто с концепцией неизменных коллекций. К сожалению, это совершенно неправильно. Результатом этой функции является одна и та же карта. Вызов put вызывает неявное преобразование в java.util.Map<String, Int>, которое с радостью принимает новые значения и немедленно отбрасывается. Оригинал map немодифицирован (так как он действительно является неизменным).

Хорхе Ортис говорит, что лучше всего использовать неявные преобразования для одной из двух целей:

  • Добавление членов (методы, поля и т.д.). Эти преобразования должны быть связаны с новым типом, не связанным ни с чем другим в области видимости.
  • "Фиксирование" сломанной иерархии классов. Таким образом, если у вас есть некоторые типы A и B, которые не связаны друг с другом. Вы можете определить преобразование A => B тогда и только тогда, когда вы предпочли бы иметь A <: B (<: означает "подтип" ).

Так как java.util.Map, очевидно, не является новым типом, не связанным ни с чем в нашей иерархии, мы не можем подпадать под первую оговорку. Таким образом, наша единственная надежда состоит в том, чтобы наше преобразование Map[A, B] => java.util.Map[A, B] получило право на второе. Однако для Scala map нет смысла наследовать от java.util.Map. Они действительно полностью ортогональные интерфейсы/черты. Как показано выше, попытка игнорировать эти рекомендации почти всегда приводит к странному и неожиданному поведению.

По правде говоря, методы javautils asScala и asJava были разработаны для решения этой точной проблемы. Существует неявное преобразование (на самом деле это число) в javautils из Map[A, B] => RichMap[A, B]. RichMap - это совершенно новый тип, определенный javautils, поэтому его единственная цель - добавить участников в map. В частности, он добавляет метод asJava, который возвращает карту-оболочку, которая реализует java.util.Map и делегирует ваш исходный экземпляр map. Это делает процесс гораздо более явным и гораздо менее подверженным ошибкам.

Другими словами, использование asScala и asJava является наилучшей практикой. Спустившись по обеим этим дорогам самостоятельно в производственном приложении, я могу сказать вам из первых рук, что подход javautils намного безопаснее и проще в работе. Не пытайтесь обойти его защиту только ради спасения 8 символов!

Ответ 2

Вот несколько быстрых примеров с использованием Jorge Ortiz библиотека коллекции scalaj:

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]

проект javautils доступен из центрального репозитория maven

Ответ 3

С Scala 2.8 это можно сделать следующим образом:

import scala.collection.JavaConversions._

val list = new java.util.ArrayList[String]()
list.add("test")
val scalaList = list.toList