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

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

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

Вот упрощенный пример проблемы. Есть классы Main , Logic , Dao . Звонят друг другу в строчку: 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 являются классами фреймворка (мне не нужно их переносить). Класс 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 и его друзей?

33
oshyshko 5 Окт 2009 в 16:40

3 ответа

Лучший ответ

Поверьте мне; вы не хотите прозрачного преобразования взад и вперед. Именно это и пытались сделать функции 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 персонажей!

65
Daniel Spiewak 5 Окт 2009 в 17:47
1
(+1) очень хорошо сказано. Я бы хотел, чтобы этот пост был рядом, когда я боролся с ним некоторое время назад.
 – 
Shaun
5 Окт 2009 в 19:46
1
Спасибо за такое подробное описание. Нашел это через Google (конечно!), Так что уже избавил кого-то от боли!
 – 
Michael Neale
15 Окт 2009 в 10:59
2
Обратите внимание, javautils заменяется. В наши дни вам нужен github.com/scalaj/scalaj-collection
 – 
Synesso
4 Фев 2011 в 09:17
1
Думаю, теперь вместо jcl.Conversions следует использовать JavaConversions.
 – 
Vlad Patryshev
8 Авг 2012 в 22:02
По состоянию на 2.12 это import scala.collection.JavaConverters._
 – 
Alex Abdugafarov
20 Ноя 2017 в 14:18

Вот несколько быстрых примеров использования библиотеки scalaj-collection Хорхе Ортиса:

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
David Carlson 16 Ноя 2010 в 01:37

В Scala 2.8 это можно было сделать так:

import scala.collection.JavaConversions._

val list = new java.util.ArrayList[String]()
list.add("test")
val scalaList = list.toList
2
Somatik 28 Май 2012 в 22:04