Высокопроизводительная оболочка для конвертации Scala в Java коллекций - PullRequest
1 голос
/ 20 января 2020

Скажем, у меня есть Java класс с некоторыми бизнес-логиками c:

package examples;

import java.util.List;
import java.util.Map;

public class Inner {
    public void consume(Map<Integer, List<Double>> map) {
        map.forEach((k, v) -> {
            System.out.println("Key: " + k);
            v.forEach(i -> System.out.println("  item: " + i));
        });
    }
}

Теперь я хочу написать высокую производительность, максимально родную Scala обертка (потому что эта обертка может быть вызвана на высокой частоте), так:

Первая попытка

package examples

class Wrapper(val asJava: Inner) extends AnyVal {
  implicit def consume(map: Map[Int, List[Double]]): Unit = asJava.consume(map)
}

Но есть ошибки:

[error]  found   : Map[Int,List[Double]]               (in scala.collection.immutable)
[error]  required: Map[Integer,java.util.List[Double]] (in java.util)
[error]   implicit def consume(map: Map[Int, List[Double]]): Unit = asJava.consume(map)
[error]                                                                            ^
[error] one error found

Вторая попытка

package examples

import scala.jdk.CollectionConverters._

class Wrapper(val asJava: Inner) extends AnyVal {
  implicit def consume(map: Map[Int, List[Double]]): Unit = asJava.consume(map.asJava)
}

По-прежнему получаются ошибки:

[error]  found   : java.util.Map[Int,List[scala.Double]]
[error]  required: java.util.Map[Integer,java.util.List[java.lang.Double]]
[error]   implicit def consume(map: Map[Int, List[Double]]): Unit = asJava.consume(map.asJava)
[error]                                                                                ^
[error] one error found

И я боюсь, что scala.jdk.CollectionConverters._ может выделить дополнительную память и потратить много времени

Вопросы

  1. Можно ли использовать scala.jdk.CollectionConverters._ в сценарии высокой производительности?
  2. В Kotlin все примитивные типы и большинство коллекций могут быть преобразованы в / из их Java альтернатива практически без потери производительности , поэтому мне интересно, как достичь цели в Scala?

Ответы [ 3 ]

5 голосов
/ 20 января 2020

ОБНОВЛЕНИЕ 1 : я расширил ответ, чтобы предоставить больше деталей и объяснений.

Используйте Integer вместо Int и java.lang.Double вместо scala.Double (это то, что Double интерпретируется как Scala) в ваших Scala определениях.

Проблема в том, что Java не позволяет использовать примитивы в коллекциях, поэтому в качестве типа используется Integer вместо int и Double вместо double. В Scala, обработка Int, Double, et c. более сложный Он будет обрабатывать их как примитивы (например, Java * int, double, et c.) В большинстве случаев, но он будет блокировать (и распаковывать) их при использовании с коллекциями.

Однако при взаимодействии с Java необходимо быть более явным. Scala неявно преобразует Integer экземпляров в Int экземпляров - и наоборот - когда это необходимо, но не может неявно преобразовывать коллекции примитивов в штучной упаковке.

Вы можете использовать Java коллекции в Scala просто отлично, с нулевыми накладными расходами; вам вообще не нужно конвертировать их в Scala эквивалентов. Тем не менее, вам, очевидно, потребуется выполнить преобразования, если вы передадите их в код, который ожидает эквивалентные коллекции Scala, или если вы хотите использовать коллекции Scala со своим наследием Java код. (Если неясно, java.util.Map - это не то же самое, что scala.collection.immutable.Map, и java.util.List - это не то же самое, что scala.collection.immutable.List. Коллекции Scala являются неизменными и предназначены для эффективного использования с использованием функционального программирования парадигмы.)

Если вы беспокоитесь о производительности, вам следует использовать набор инструментов для микро-бенчмаркинга, такой как ScalaMeter , что позволит вам точно измерить сравнение использования коллекций Java в исходном виде с преобразованием в / из Scala коллекций.

ОБНОВЛЕНИЕ 2 :

Я переписал вашу первую попытку, используя коллекции Java. Чтобы избежать путаницы с типами Scala, я переименовал конфликтующие типы Java, добавив к ним префикс J:

package examples

import java.lang.{Double => JDouble}
import java.util.{List => JList, Map => JMap}
import scala.language.implicitConversions

class Wrapper(val asJava: Inner) {
  implicit def consume(map: JMap[Integer, JList[JDouble]]): Unit = {
    asJava.consume(map)
  }
}

. коллекции Java Map и List, а также коробочные примитивы. Он компилируется, но абсолютно ничего не покупает (подпись Wrapper.consume точно такая же, как Inner.consume). Тем не менее, он иллюстрирует, как использовать Java коллекции in situ in Scala.

Если вы хотите использовать Scala коллекций и конвертируем в Java эквивалентов, которые, как я думаю, является целью вашей второй попытки, тогда это будет выглядеть так:

package examples

import java.lang.{Double => JDouble}
import scala.jdk.CollectionConverters._
import scala.language.implicitConversions

class Wrapper(val asJava: Inner) {
  implicit def consume(map: Map[Int, List[Double]]): Unit = {
    val jmap = map.asInstanceOf[Map[Integer, List[JDouble]]]
    asJava.consume(jmap.map(p => p._1 -> p._2.asJava).asJava)
  }
}

В этом случае сначала нам нужно привести коллекцию к использованию упакованных типов (определения эквивалентны под капотом), у которых нет накладных расходов как таковых.

Затем мы должны преобразовать Scala структур данных, эквивалентных Java структур данных. Совершенно очевидно, что это требует дополнительных усилий, если только обернуть структуры данных Scala в интерфейс Java.

ОБНОВЛЕНИЕ 3

Я также должен отметить, что использование метода Wrapper.consume implicit является странным выбором. Я думаю, что вы, возможно, намеревались создать класс implicit, чтобы (во второй попытке) вы могли неявно потреблять Scala структур данных с экземпляром Inner. (По сути, Inner оформляется функциями, предоставленными в Wrapper.) * Экземпляры Inner будут неявно преобразованы в Wrapper экземпляры без дополнительных затрат (это даже не создаст экземпляр Wrapper в большинстве случаи, из-за предложения extending AnyVal):

import java.lang.{Double => JDouble}
import scala.jdk.CollectionConverters._
import scala.language.implicitConversions

package object examples {
  implicit class Wrapper(val asJava: Inner) extends AnyVal {
    def consume(map: Map[Int, List[Double]]): Unit = {
      val jmap = map.asInstanceOf[Map[Integer, List[JDouble]]]
      asJava.consume(jmap.map(p => p._1 -> p._2.asJava).asJava)
    }
  }
}

Обратите внимание, что Wrapper теперь необходимо определить внутри package object. (Это потому, что неявные классы могут быть определены только внутри object некоторого вида, который должен быть введен в область видимости, и package object, как правило, является наиболее удобным способом достижения этого.)

1 голос
/ 20 января 2020

Вот эталонный тест jmh, показывающий, что scala.jdk.CollectionConverters имеют незначительную стоимость

@State(Scope.Benchmark)
@BenchmarkMode(Array(Mode.Throughput))
class So59827649 {
  def consumeScala(inner: Inner, map: Map[Int, List[Double]]): Unit = {
    val jmap = map.asInstanceOf[Map[Integer, List[java.lang.Double]]];
    inner.consume(jmap.map(p => p._1 -> p._2.asJava).asJava)
  }

  def consumeJava(inner: Inner, map: java.util.Map[Integer, java.util.List[java.lang.Double]]): Unit = {
    inner.consume(map)
  }

  val inner = new Inner
  val size = 1000
  val scalaMap = (1 to size).map(i => i ->  List.fill(size)(math.random)).toMap
  val javaMap = (1 to size).map(i => int2Integer(i) -> List.fill(size)(double2Double(math.random)).asJava).toMap.asJava

  @Benchmark def _consumeScala(): Unit = consumeScala(inner, scalaMap)
  @Benchmark def _consumeJava(): Unit = consumeJava(inner, javaMap)
}

и

public class Inner {
    public void consume(java.util.Map<Integer, java.util.List<Double>> map) {
        map.forEach((k, v) -> v.stream().mapToDouble(Double::doubleValue).sum());
    }
}

, где sbt "jmh:run -i 10 -wi 5 -f 2 -t 1 bench.So59827649" дает

[info] Benchmark                  Mode  Cnt   Score   Error  Units
[info] So59827649._consumeJava   thrpt   20  97.963 ± 0.980  ops/s
[info] So59827649._consumeScala  thrpt   20  96.350 ± 2.326  ops/s
1 голос
/ 20 января 2020

asJava / asScala создание простых оболочек. Они выделяют один дополнительный объект, и большинство методов просто делегируют базовую коллекцию. Но дело в том, что классы коллекций Java уже очень далеки от оптимальных для примитивов; вместо этого есть довольно много примитивных коллекционных библиотек вокруг. В этой статье 2015 года перечислены FastUtil, Goldman Sachs, HPP C, Koloboke, Trove. Есть также Eclipse Collections и, вероятно, больше, о которых я не знаю.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...