Blog

Holen Sie das Java aus Ihrem Scala heraus, Teil 2

Jan Vermeir

Jan Vermeir

Aktualisiert Oktober 22, 2025
6 Minuten

Java aus Ihrem Scala herausholen, Teil 2 Ich versuche immer noch, alte Gewohnheiten loszuwerden, sozusagen mein Winterfell abzuschütteln und statt ScaVa (d.h. Java mit einer Scala-Syntax) ein echtes Scala zu entwickeln. Wenn Sie daran interessiert sind, können Sie Zeuge meines Kampfes auf GitHub werden(ShoppingList auf GitHub). Diese Geschichte kam zustande, weil ich einige Kollegen um Hilfe gebeten habe. Am Ende haben wir Schleifen auf verschiedene Arten umgeschrieben. Ich werde Ihnen einige Alternativen zu klassischen Schleifen über Sammlungen zeigen.

Der Code ist hier angehängt. Das Beispiel zeigt, wie Sie Elemente in einer Liste zusammenfassen können. Die Objekte in der Liste sind alle Instanzen der Klasse Stuff, einem einfachen Wertcontainer für einen String und einen Int. Die Idee ist, Stuffs mit demselben Schlüssel zusammenzufassen, um eine neue Liste zu erstellen: [scala] val items = List(Stuff("A", 1), Stuff("A", 2), Stuff("B", 3), Stuff("B", 4)) [/scala] sollte zu [scala] val expectedResult = List(Stuff("A", 3), Stuff("B", 7)) [/scala] wobei Stuff eine einfache Fallklasse mit zwei Attributen ist: [scala] case class Stuff(val label: String, val number: Int) [/scala] Da ich ein langjähriger Java-Programmierer bin, sieht meine erste Lösung so aus: [scala] def classicSum = { var result: List[Stuff] = List() for (item <- items) { if (result.size > 0 && item.label == result.head.label) { result = Stuff(result.head.label, result.head.number + item.number) :: result.drop(1) } else { result = item :: result } } assertEquals(expectedResult, result.reverse) } [/scala] Gute alte Schleifenbildung und eine var zum Sammeln des Ergebnisses. Das funktioniert, aber es sagt nicht, was passiert. Alles, was Sie sehen, ist eine Schleife und eine ausgeklügelte Listenverarbeitung. Beachten Sie den Aufruf von drop(1), mit dem Sie den Kopf der Liste durch eine neue Instanz ersetzen können. Ich mag die Drop-Funktion und habe sie auch schon in anderen Programmen verwendet, aber hier vernebelt sie die Dinge nur. Die nächste Lösung ist eine rekursive Vorgehensweise. Wenn Sie sich die Version von ShoppingList vom Oktober/November 2011 ansehen, werden Sie viele Rekursionen finden. Mein Ziel war es damals, mir die Rekursion in den Kopf zu setzen, indem ich alle anderen Formen von Schleifen verbot. Auf die Liste der Stuff-Instanzen und das vorliegende Problem angewendet, ist das Ergebnis tragisch: [scala] def recursiveSum = { @tailrec def sum(listOfPairs: List[Stuff], result: List[Stuff]): List[Stuff] = { listOfPairs match { case Nil => result case head :: tail => { val aktuellKopf = if (result.size == 0) { Stuff(head.label, 0) } else { result.head } val newResult = if (aktuellKopf.label == head.label) { Stuff(aktuellKopf.label, aktuellKopf.number + head.number) :: result.drop(1) } else { head :: result } sum(tail, newResult) } } } val result = sum(items, List()) assertEquals(expectedResult, result.reverse) } [/scala] Meine nächste Version nutzt die Sammlungen von Scala, um die Menge des Codes drastisch zu reduzieren. Der erste Versuch besteht den Test, aber er ist kryptisch. Im Geiste des Rot-Grün-Refactor-Mantras zeige ich ihn Ihnen trotzdem: [scala] def sumByGroupSolution1 = { val groupedByLabel = items.groupBy(_.label) val result = groupedByLabel map { t => Stuff(t._1, t.2 map { .number } sum) } assertEquals(expectedResult, result) } [/scala] So! Parsen Sie das, wenn Sie diesen Code irgendwann im nächsten Jahr ändern müssen (Anmerkung: Nachdem ich nun einige Zeit mit diesem Code gearbeitet habe, während ich diesen Blog schrieb, muss ich zugeben, dass er einem ans Herz wächst; nach einer Weile tut er nicht mehr so weh, ähnlich wie ein neues Paar Schuhe). Dieses kryptische Stück Malware veranschaulicht die Verwendung von drei leistungsstarken Methoden namens groupBy, map und sum. groupBy ist in gewisser Weise mit SQLs group by Klausel vergleichbar. Die Scala-Version nimmt eine Sammlung und gibt eine Map zurück. Der Schlüssel der Map ist das Element, nach dem gruppiert werden soll (in meinem Fall Stuff.label). Der Wert ist eine Liste von Elementen, die denselben Schlüssel haben. In diesem Fall enthält die items-Sammlung Stuff-Instanzen. Ich habe nach dem Feld label gruppiert, also ist der Typ von groupedByLabel Map[String, List[Stuff]]. Auf diese Map müssen wir eine Summenfunktion anwenden, um alle Stuff-Instanzen in der List of Stuffs zu addieren. Sum funktioniert jedoch auf Int's, so dass wir vor der Anwendung von Sum das Zahlenfeld jeder Stuff-Instanz extrahieren müssen. Dies geschieht über eine Map: map { .Zahl}. Die Zuordnung wird auf jede Stuff-Instanz in der Liste der Stuffs angewendet, die von groupedByLabel zurückgegeben wurde. Indem wir einen Teil des Codes in Zeile 3 des obigen Beispiels ausklammern, können wir die Lesbarkeit verbessern: [scala] def sumByGroupSolution2 = { val stuffsGroupedByLabel = items.groupBy(.label) def sumOfStuffsWithTheSameLabel(stuffs: List[Stuff]): Int = stuffs map { _.number } sum val result = stuffsGroupedByLabel map { t => Stuff(t._1, sumOfStuffsWithTheSameLabel(t.2)) } assertEquals(expectedResult, result) } [/scala] Ich halte dies für besser, weil jetzt das Ergebnis eines Teils der Berechnung benannt ist. Wenn man etwas benennt, ist der Algorithmus leichter zu erkennen. Die nächste Lösung verwendet mehr Zwischenergebnisse. Die Ergebnisse jeder Map- oder Summenoperation werden in einer Variablen gespeichert, die einen sinnvollen Namen erhält. Dadurch wird die Bedeutung der Zwischenergebnisse klarer, aber der Umfang des Codes nimmt zu. [scala] @Test def sumByGroupSolution5 = { val calcSumOfStuff = (stuffs: Seq[Stuff]) => stuffs map { .number } sum val groupedByLabel = items.groupBy { _.label } val resultGroupedByLabel = groupedByLabel mapValues { calcSumOfStuff } val result = resultGroupedByLabel map { t => Stuff(t._1, t.2) } assertEquals(expectedResult, result) } [/scala] Der Code zeigt eine weitere Map-Funktion namens mapValues. [scala] val resultGroupedByLabel = groupedByLabel mapValues { calcSumOfStuff } [/scala] mapValues arbeitet nur mit den Werten einer map und ignoriert die Schlüssel. In meinem Fall spielen die Schlüssel keine Rolle (sie sind ein Artefakt des groupBy-Aufrufs), so dass ich mit mapValues und nicht mit map arbeiten kann. Die Namensgebung war auch die Inspiration für die nächste Lösung. Jetzt extrahiere ich keine Zwischenergebnisse, aber in diesem Fall habe ich die Variablen, die manipuliert werden, benannt: [scala] def sumByGroupSolution4 = { val groupedByLabel = items.groupBy(.label) val result = groupedByLabel map { case (label, stuffs) => Stuff(label, stuffs map { _.number } sum) } assertEquals(expectedResult, result) } [/scala] Die case-Anweisung als Argument für die Map ermöglicht es, zwei Variablen einzuführen, um die Dinge zu identifizieren, die wir manipulieren. Label und stuffs bedeuten mir mehr als _1 und _2. Rückblickend auf meinen Kampf gefällt mir diese Lösung am besten. Ich denke, sie ist sowohl prägnant als auch leicht zu lesen.

Verfasst von

Jan Vermeir

Developing software and infrastructure in teams, doing whatever it takes to get stable, safe and efficient systems in production.

Contact

Let’s discuss how we can support your journey.