Kapitel 1. Aufgaben in der Befehlszeile

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Höchstwahrscheinlich wird einer der ersten Schritte auf deiner Reise mit Scala 3 die Arbeit an der Kommandozeile sein. Nachdem du Scala wie in "Scala installieren" beschrieben installiert hast , möchtest du vielleicht die REPL - ScalasRead/Eval/Print/Loop - starten, indem du scala in die Kommandozeile deines Betriebssystems eingibst. Oder du möchtest ein kleines "Hello, world"-Projekt aus einer Datei erstellen und es dann kompilieren und ausführen. Da diese Kommandozeilenaufgaben für viele Menschen der Einstieg in die Arbeit mit Scala sind, werden sie hier zuerst behandelt.

Die REPL ist eine Kommandozeilen-Shell. Sie ist eine Spielwiese, auf der du kleine Tests durchführen kannst, um zu sehen, wie Scala und die Bibliotheken von Drittanbietern funktionieren. Wenn du mit der JShell von Java, der irb von Ruby, der Python-Shell oder IPython oder der ghci von Haskell vertraut bist, ähnelt die REPL von Scala all diesen Programmen. Wie in Abbildung 1-1 zu sehen ist, startest du die REPL, indem du scala in die Befehlszeile deines Betriebssystems eingibst und dann deine Scala-Ausdrücke eingibst.

Wann immer du Scala-Code testen willst, ist die REPL eine großartige Spielumgebung. Es ist nicht nötig, ein komplettes Projekt zu erstellen - du kannst deinen Testcode einfach in die REPL einfügen und damit experimentieren, bis du weißt, dass er funktioniert. Weil die REPL ein so wichtiges Werkzeug ist, werden ihre wichtigsten Funktionen in den ersten beiden Rezepten dieses Kapitels vorgestellt.

Abbildung 1-1. Die Scala 3 REPL läuft in einem macOS Terminal-Fenster

Die REPL ist zwar großartig, aber sie ist nicht das einzige Spiel in der Stadt. Die Ammonite REPL wurde ursprünglich für Scala 2 entwickelt und hatte viel mehr Funktionen als die Scala 2 REPL, darunter:

  • Die Möglichkeit, Code aus GitHub- und Maven-Repositories zu importieren

  • Die Möglichkeit, Sitzungen zu speichern und wiederherzustellen

  • Hübsch gedruckte Ausgabe

  • Mehrzeilige Bearbeitung

Zum Zeitpunkt der Erstellung dieses Artikels wird Ammonite noch auf Scala 3 portiert, aber viele wichtige Funktionen funktionieren bereits. In Rezept 1.3 findest du Beispiele dafür, wie du diese Funktionen nutzen kannst.

Wenn du Scala-Projekte bauen musst, verwendest du normalerweise ein Build-Tool wie sbt, das in Kapitel 17 vorgestellt wird. Wenn du jedoch eine kleine Scala-Anwendung kompilieren und ausführen möchtest, z. B. eine, die nur aus ein oder zwei Dateien besteht, kannst du deinen Code mit dem Befehl scalac kompilieren und mit scala ausführen, genau wie du es in Java mit den Befehlen javac und java machst. Dieser Prozess wird in Rezept 1.4 demonstriert. Danach zeigt Rezept 1.6, wie du Anwendungen, die du als JAR-Datei verpackst, mit den Befehlen java oder scala ausführen kannst.

1.1 Erste Schritte mit der Scala REPL

Problem

Du möchtest mit der Scala REPL beginnen und einige ihrer grundlegenden Funktionen nutzen.

Lösung

Wenn du REPL-Umgebungen in Sprachen wie Java, Python, Ruby und Haskell benutzt hast, wird dir die Scala REPL sehr vertraut sein. Um die REPL zu starten, gibst du in der Befehlszeile deines Betriebssystems scala ein. Wenn die REPL startet, siehst du möglicherweise eine erste Meldung, gefolgt von einer Eingabeaufforderung scala>:

$ scala
Welcome to Scala 3.0
Type in expressions for evaluation. Or try :help.

scala> _

Die Eingabeaufforderung zeigt an, dass du jetzt die Scala REPL verwendest. In der REPL-Umgebung kannst du alle Arten von Experimenten und Ausdrücken ausprobieren:

scala> val x = 1
x: Int = 1

scala> val y = 2
y: Int = 2

scala> x + y
res0: Int = 3

scala> val x = List(1, 2, 3)
x: List[Int] = List(1, 2, 3)

scala> x.sum
res1: Int = 6

Wie in diesen Beispielen gezeigt:

  • Nachdem du deinen Befehl eingegeben hast, zeigt die REPL-Ausgabe das Ergebnis deines Ausdrucks an, einschließlich der Informationen zum Datentyp.

  • Wenn du keinen Variablennamen angibst, wie im dritten Beispiel, erstellt das REPL eine eigene Variable, die mit res0 beginnt, dann res1 usw. Du kannst diese Variablennamen so verwenden, als ob du sie selbst erstellt hättest:

scala> res1.getClass
res2: Class[Int] = int

scala> res1 + 2
res3: Int = 8

Sowohl Anfänger als auch erfahrene Entwickler schreiben jeden Tag Code in der REPL, um schnell zu sehen, wie Scala-Funktionen und ihre eigenen Algorithmen funktionieren.

Registerkarte Abschluss

Es gibt ein paar einfache Tricks, mit denen du die REPL effektiver nutzen kannst. Ein Trick ist die Verwendung der Tabulatorvervollständigung, um die Methoden zu sehen, die für ein Objekt verfügbar sind. Um zu sehen, wie die Tabulatorvervollständigung funktioniert, gibst du die Zahl 1 ein, dann eine Dezimalzahl und drückst dann die Tabulatortaste. Die REPL zeigt daraufhin die Dutzenden von Methoden an, die für eine Instanz von Int verfügbar sind:

scala> 1.
!=                         finalize                   round
##                         floatValue                 self
%                          floor                      shortValue
&                          formatted                  sign
*                          getClass                   signum
many more here ...

Du kannst die Liste der angezeigten Methoden auch einschränken, indem du den ersten Teil eines Methodennamens eingibst und dann die Tabulatortaste drückst. Wenn du z. B. alle Methoden auf List sehen willst, gibst du List(1). ein und drückst dann die Tabulatortaste, um über zweihundert Methoden zu sehen. Wenn du dich aber nur für die Methoden auf List interessierst, die mit den Zeichen to beginnen, gibst du List(1).to ein und drückst dann die Tabulatortaste, und die Ausgabe wird auf diese Methoden reduziert:

scala> List(1).to
to              toIndexedSeq    toList          toSet           toTraversable
toArray         toIterable      toMap           toStream        toVector
toBuffer        toIterator      toSeq           toString

Diskussion

Ich benutze die REPL, um viele kleine Experimente zu erstellen, und sie hilft mir auch, einige Typkonvertierungen zu verstehen, die Scala automatisch durchführt. Als ich zum Beispiel anfing, mit Scala zu arbeiten und den folgenden Code in die REPL eintippte, wusste ich nicht, welchen Typ die Variable x hat:

scala> val x = (3, "Three", 3.0)
val x: (Int, String, Double) = (3,Three,3.0)

Mit der REPL ist es einfach, Tests wie diesen auszuführen und dann getClass für eine Variable aufzurufen, um ihren Typ zu sehen:

scala> x.getClass
val res0: Class[? <: (Int, String, Double)] = class scala.Tuple3

Obwohl ein Teil dieser Ergebniszeile schwer zu lesen ist, wenn du zum ersten Mal mit Scala arbeitest, lässt dich der Text auf der rechten Seite von = wissen, dass der Typ eine Tuple3 Klasse ist.

Du kannst auch den REPL-Befehl :type verwenden, um ähnliche Informationen zu erhalten, allerdings wird der Name Tuple3 derzeit nicht angezeigt:

scala> :type x
(Int, String, Double)

In vielen anderen Fällen ist es jedoch hilfreich:

scala> :type 1 + 1.1
Double

scala> :type List(1,2,3).map(_ * 2.5)
List[Double]

Auch wenn dies einfache Beispiele sind, wirst du feststellen, dass die REPL sehr hilfreich ist, wenn du mit komplizierterem Code und Bibliotheken arbeitest, mit denen du nicht vertraut bist.

Starten der REPL innerhalb von sbt

Du kannst eine Scala REPL-Sitzung auch aus der sbt-Shell heraus starten. Wie in Rezept 17.5, "Andere sbt-Befehle verstehen", gezeigt , startest du einfach die sbt-Shell innerhalb eines sbt-Projekts:

$ sbt
MyProject> _

Verwende dann entweder den console oder den consoleQuick Befehl von dort aus:

MyProject> console
scala> _

Der Befehl console kompiliert die Quellcodedateien im Projekt, legt sie auf den Klassenpfad und startet die REPL. Der Befehl consoleQuick startet die REPL mit den Projektabhängigkeiten auf dem Klassenpfad, aber ohne die Projektquellcodedateien zu kompilieren. Diese zweite Option ist nützlich, wenn sich dein Code nicht kompilieren lässt oder wenn du einen Testcode mit deinen Abhängigkeiten (Bibliotheken) ausprobieren möchtest.

Siehe auch

Wenn dir die Idee einer REPL-Umgebung gefällt, du aber Alternativen zur Standard-REPL ausprobieren möchtest, gibt es einige tolle kostenlose Alternativen:

  • Die Ammonite REPL hat mehr Funktionen als die REPL und wird in Rezept 1.3 demonstriert.

  • Scastie ist eine webbasierte Alternative zur REPL, die sbt-Optionen unterstützt und mit der du externe Bibliotheken zu deiner Umgebung hinzufügen kannst.

  • ScalaFiddle ist auch eine webbasierte Alternative.

  • Die IDEs IntelliJ IDEA und Visual Studio Code (VS Code) haben beide Arbeitsblätter, die der REPL ähnlich sind.

1.2 Quellcode und JAR-Dateien in die REPL laden

Problem

Du hast Scala-Code in einer Quellcode-Datei und möchtest diesen Code in der REPL verwenden.

Lösung

Verwende den Befehl :load, um Quellcodedateien in die REPL-Umgebung einzulesen. Ein Beispiel: Dieser Code befindet sich in einer Datei namens Person.scala in einem Unterverzeichnis namensmodels:

class Person(val name: String):
    override def toString = name

kannst du den Quellcode wie folgt in die laufende REPL-Umgebung laden:

scala> :load models/Person.scala
// defined class Person

Nachdem der Code in die REPL geladen wurde, kannst du eine neue Person Instanz erstellen:

scala> val p = Person("Kenny")
val p: Person = Kenny

Beachte jedoch, dass, wenn dein Quellcode eine package Deklaration enthält:

// Dog.scala file
package animals
class Dog(val name: String)

wird der Befehl :load fehlschlagen:

scala> :load Dog.scala
1 |package foo
  |^^^
  |Illegal start of statement

Quellcodedateien können in der REPL keine Pakete verwenden. Daher musst du sie in Situationen wie dieser in eine JAR-Datei kompilieren und sie dann in den Klassenpfad aufnehmen, wenn du die REPL startest. So verwende ich zum Beispiel die Version 0.2.0 meiner Simple Test Library mit der Scala 3 REPL:

// start the repl like this
$ scala -cp simpletest_3.0.0-0.2.0.jar

scala> import com.alvinalexander.simpletest.SimpleTest.* 

scala> isTrue(1 == 1)
true

Zum Zeitpunkt der Erstellung dieses Artikels kannst du einer bereits laufenden REPL-Sitzung keine JARs hinzufügen, aber diese Funktion könnte in Zukunft hinzugefügt werden.

Diskussion

Gut zu wissen ist auch, dass kompilierte Klassendateien im aktuellen Verzeichnis automatisch in die REPL geladen werden. Wenn du zum Beispiel diesen Code in eine Datei mit dem Namen Cat.scala schreibst und ihn dann mit scalac kompilierst, wird eine Cat.class-Datei erstellt:

case class Cat(name: String)

Wenn du die REPL in demselben Verzeichnis wie die Klassendatei startest, kannst du eine neue Cat erstellen:

scala> Cat("Morris")
val res0: Cat = Cat(Morris)

Auf Unix-Systemen kannst du diese Technik verwenden, um deine REPL-Umgebung anzupassen. Befolge dazu die folgenden Schritte:

  1. Erstelle in deinem Home-Verzeichnis ein Unterverzeichnis namens repl. In meinem Fall lege ich dieses Verzeichnis als /Users/al/repl an. (Du kannst dieses Verzeichnis beliebig benennen.)

  2. Lege alle *.class-Dateien, die du willst, in diesem Verzeichnis ab.

  3. Erstelle einen Alias oder ein Shell-Skript, mit dem du die REPL in diesem Verzeichnis starten kannst.

Auf meinem System habe ich eine Datei mit dem Namen Repl.scala in meinem ~/repl-Verzeichnis angelegt, die diesenInhalt hat:

import sys.process.*

def clear = "clear".!
def cmd(cmd: String) = cmd.!!
def ls(dir: String) = println(cmd(s"ls -al $dir"))
def help =
    println("\n=== MY CONFIG ===")
    "cat /Users/Al/repl/Repl.scala".!

case class Person(name: String)
val nums = List(1, 2, 3)

Dann kompiliere ich diesen Code mit scalac, um die Klassendateien in diesem Verzeichnis zu erstellen. Dann erstelle und verwende ich diesen Alias, um die REPL zu starten:

alias repl="cd ~/repl; scala; cd -"

Dieser Alias verschiebt mich in das Verzeichnis ~/repl, startet die REPL und kehrt dann in mein aktuelles Verzeichnis zurück, wenn ich die REPL verlasse.

Eine andere Möglichkeit ist, ein Shell-Skript mit dem Namen repl zu erstellen, es ausführbar zu machen und es in deinem ~/bin-Verzeichnis (oder irgendwo anders auf deinem PATH) abzulegen:

#!/bin/sh

cd ~/repl
scala

Da ein Shell-Skript in einem Unterprozess ausgeführt wird, kehrst du zu deinem ursprünglichen Verzeichnis zurück, wenn du die REPL verlässt.

Auf diese Weise werden deine benutzerdefinierten Methoden in die REPL geladen, wenn sie gestartet wird, sodass du sie innerhalb der scala Shell verwenden kannst:

clear       // clear the screen
cmd("ps")   // run the 'ps' command
ls(".")     // run 'ls' in the current directory
help        // displays my Repl.scala file as a form of help

Nutze diese Technik, um andere benutzerdefinierte Definitionen, die du in der REPL verwenden möchtest, vorzuladen.

1.3 Erste Schritte mit der Ammonite REPL

Problem

Du möchtest mit der Ammonite REPL arbeiten und einige ihrer grundlegenden Funktionen verstehen.

Lösung

Die Ammonite REPL funktioniert genau wie die Scala REPL: Lade sie einfach herunter, installiere sie und starte sie dann mit dem Befehl amm. Wie die standardmäßige Scala REPL wertet sie Scala-Ausdrücke aus und weist einen Variablennamen zu, wenn du keinen angibst:

@ val x = 1 + 1
x: Int = 2

@ 2 + 2
res0: Int = 4

Aber Ammonite hat viele zusätzliche Funktionen. Du kannst die Eingabeaufforderung der Shell mit diesem Befehl ändern:

@ repl.prompt() = "yo: "

yo: _

Wenn du diese Scala-Ausdrücke in einer Datei namens Repl.scala in einem Unterverzeichnis namens foo:

import sys.process.*

def clear = "clear".!
def cmd(cmd: String) = cmd.!!
def ls(dir: String) = println(cmd(s"ls -al $dir"))

kannst du sie mit diesem Befehl in deine Ammonite REPL importieren:

@ import $file.foo.Repl, Repl.* 

Dann kannst du diese Methoden in Ammonite verwenden:

clear        // clear the screen
cmd("ps")    // run the 'ps' command
ls("/tmp")   // use 'ls' to list files in /tmp

Auf ähnliche Weise kannst du eine JAR-Datei mit dem Namen simpletest_3.0.0-0.2.0.jar in einem Unterverzeichnis namens foo in deine amm REPL-Sitzung importieren, indem du die Variable Ammonite $cp verwendest:

// import the jar file
import $cp.foo.`simpletest_3.0.0-0.2.0.jar`

// use the library you imported
import com.alvinalexander.simpletest.SimpleTest.*
isTrue(1 == 1)

Mit dem Befehl import ivy kannst du Abhängigkeiten aus Maven Central (und anderen Repositories) importieren und sie in deiner aktuellen Shell verwenden:

yo: import $ivy.`org.jsoup:jsoup:1.13.1`
import $ivy.$

yo: import org.jsoup.Jsoup, org.jsoup.nodes.{Document, Element}
import org.jsoup.Jsoup

yo: val html = "<p>Hi!</p>"
html: String = "<p>Hi!</p>"

yo: val doc: Document = Jsoup.parse(html)
doc: Document = <html> ...

yo: doc.body.text
res2: String = "Hi!"

Mit dem in Ammonite eingebauten Befehl time kannst du die Zeit messen, die du für die Ausführung deines Codes benötigst:

@ time(Thread.sleep(1_000))
res2: (Unit, FiniteDuration) = ((), 1003788992 nanoseconds)

Die Autovervollständigungsfunktion von Ammonite ist beeindruckend. Gib einfach einen Ausdruck wie diesen ein und drücke nach dem Komma die Tabulatortaste:

@ Seq("a").map(x => x.

Wenn du das tust, zeigt Ammonite eine lange Liste von Methoden an, die auf x-einer String- verfügbar sind, beginnend mit diesen Methoden:

def intern(): String
def charAt(x$0: Int): Char
def concat(x$0: String): String
much more output here ...

Das ist gut, denn es zeigt dir nicht nur die Methodennamen, sondern auch ihre Eingabeparameter und den Rückgabetyp.

Diskussion

Die Liste der Funktionen von Ammonite ist lang. Eine weitere großartige Funktion ist, dass du eine Startkonfigurationsdatei verwenden kannst, genau wie eine Unix .bashrc oder .bash_profile Startdatei. Füge einfach ein paar Ausdrücke in eine Datei ~/.ammonite/predef.sc ein:

import sys.process.*

repl.prompt() = "yo: "
def clear = "clear".!
def cmd(cmd: String) = cmd.!!
def ls(dir: String) = println(cmd(s"ls -al $dir"))
def reset = repl.sess.load()  // similar to the scala repl ':reset' command

Wenn du dann die Ammonite REPL startest, ändert sich deine Eingabeaufforderung in yo: und diese anderen Methoden stehen dir zur Verfügung.

Eine weitere großartige Funktion ist, dass du eine REPL-Sitzung speichern kannst und alles, was du bis zu diesem Punkt gemacht hast, gespeichert wird. Um das zu testen, erstelle eine Variable in der REPL und speichere dann deine Sitzung:

val remember = 42
repl.sess.save()

Dann erstelle eine weitere Variable:

val forget = 0

Wenn du jetzt die Sitzung neu lädst, siehst du, dass die Variable remember immer noch verfügbar ist, aber die Variable forget wie gewünscht vergessen wurde:

@ repl.sess.load()
res3: SessionChanged = SessionChanged(removedImports = Set('forget),
addedImports = Set(), removedJars = Set(), addedJars = Set())

@ remember
res4: Int = 42

@ forget
   |val res5 = forget
   |           ^^
   |           Not found: forget

Du kannst auch mehrere Sitzungen speichern und laden, indem du ihnen unterschiedliche Namen gibst, z.B. so:

// do some work
val x = 1
repl.sess.save("step 1")

// do some more work
val y = 2
repl.sess.save("step 2")

// reload the first session
repl.sess.load("step 1")

x   // this will be found
y   // this will not be found

In der Ammonite-Dokumentation findest du Details zu weiterenFunktionen.

1.4 Kompilieren mit scalac und Ausführen mit scala

Problem

Obwohl du in der Regel ein Build-Tool wie sbt oder Mill verwenden wirst, um Scala-Anwendungen zu erstellen, möchtest du vielleicht gelegentlich auch einfachere Tools verwenden, um kleine Testprogramme zu kompilieren und auszuführen, so wie du vielleicht javac und java für kleine Java-Anwendungen verwendest.

Lösung

Kompiliere kleine Programme mit scalac, und führe sie mit scala aus. Nehmen wir zum Beispiel diese Scala-Quellcodedatei namens Hello.scala:

@main def hello = println("Hello, world")

kompiliere es auf der Kommandozeile mit scalac:

$ scalac Hello.scala

Dann führe sie mit scala aus und gib dem Befehl scala den Namen der Methode @main, die du erstellt hast:

$ scala hello
Hello, world

Diskussion

Das Kompilieren und Ausführen von Klassen ist dasselbe wie in Java, einschließlich Konzepten wie dem Klassenpfad. Stell dir zum Beispiel vor, du hast eine Klasse namens Pizza in einer Datei namens Pizza.scala, die von einem Topping Typ abhängt:

class Pizza(val toppings: Topping*):
    override def toString = toppings.toString

Angenommen, Topping ist wie folgt definiert:

enum Topping:
    case Cheese, Mushrooms

und dass sie sich in einer Datei namens Topping.scala befindet und zu Topping.class in einem Unterverzeichnis namens classes kompiliert wurde, kompiliere Pizza.scala wie folgt:

$ scalac -classpath classes Pizza.scala

Beachte, dass der Befehl scalac viele zusätzliche Optionen hat, die du verwenden kannst. Wenn du zum Beispiel die Option -verbose zum vorherigen Befehl hinzufügst, siehst du Hunderte von Zeilen zusätzlicher Ausgaben, die zeigen, wie scalac funktioniert. Diese Optionen können sich im Laufe der Zeit ändern, daher solltest du die Option -help verwenden, um zusätzliche Informationen zu erhalten:

$ scalac -help

Usage: scalac <options> <source files>
where possible standard options include:
-P               Pass an option to a plugin, e.g. -P:<plugin>:<opt>
-X               Print a synopsis of advanced options.
-Y               Print a synopsis of private options.
-bootclasspath   Override location of bootstrap class files.
-classpath       Specify where to find user class files.

much more output here ...

Wichtigste Methoden

Da wir gerade über die Kompilierung von main Methoden sprechen, ist es hilfreich zu wissen, dass sie in Scala 3 auf zwei Arten deklariert werden können:

  • Verwendung der @main Annotation für eine Methode

  • Die Deklaration einer main Methode mit der richtigen Signatur in einer object

Wie in der Lösung gezeigt, kann eine einfache @main Methode, die keine Eingabeparameter benötigt, wie folgt deklariert werden:

@main def hello = println("Hello, world")

Du kannst auch eine Methode @main deklarieren, die beliebige Parameter aus der Befehlszeile entgegennimmt, wie in diesem Beispiel String und Int:

@main def hello(name: String, age: Int): Unit =
    println(s"Hello, $name, I think you are $age years old.")

Nachdem der Code mit scalac kompiliert wurde, kann er wie folgt ausgeführt werden:

$ scala hello "Lori" 44
Hello, Lori, I think you are 44 years old.

Beim zweiten Ansatz ist die Deklaration einer main Methode innerhalb einer object genauso wie die Deklaration einer main Methode in Java, und die Signatur für die Scala main Methode muss wie folgt aussehen:

object YourObjectName:
    // the method must take `Array[String]` and return `Unit`
    def main(args: Array[String]): Unit =
        // your code here

Wenn du mit Java vertraut bist, ist dieser Scala-Code analog zu diesem Java-Code:

public class YourObjectName {
    public static void main(String[] args) {
        // your code here
    }
}

1.5 Disassemblieren und Dekompilieren von Scala-Code

Problem

Wenn du lernen willst, wie Scala-Code in Klassendateien kompiliert wird, oder wenn du versuchst, ein bestimmtes Problem zu verstehen, möchtest du den Bytecode untersuchen, den der Scala-Compiler aus deinem Quellcode erzeugt.

Lösung

Die wichtigste Methode, um Scala-Code zu disassemblieren, ist der Befehl javap. Du kannst auch einen Decompiler verwenden, um deine Klassendateien wieder in Java-Quellcode umzuwandeln. Diese Option wird in der Diskussion vorgestellt.

Mit javap

Da deine Scala-Quellcodedateien in reguläre JVM-Klassendateien kompiliert werden, kannst du den Befehl javap verwenden, um sie zu disassemblieren. Nimm zum Beispiel an, dass du eine Datei mit dem Namen Person.scala erstellt hast, die diesen Quellcode enthält:

class Person(var name: String, var age: Int)

Als nächstes kompilierst du diese Datei mit scalac:

$ scalac Person.scala

Jetzt kannst du die resultierende Person.class -Datei mit javap in ihre Signatur zerlegen:

$ javap -public Person
Compiled from "Person.scala"
public class Person {
  public Person(java.lang.String, int);
  public java.lang.String name();
  public void name_$eq(java.lang.String);
  public int age();
  public void age_$eq(int);
}

Dies zeigt die öffentliche Signatur der Klasse Person, die ihre öffentliche API oder Schnittstelle darstellt. Selbst in einem einfachen Beispiel wie diesem kannst du sehen, wie der Scala-Compiler seine Arbeit für dich macht und Methoden wie name(), name_$eq, age() und age_$eq erstellt. Die Diskussion zeigt ausführlichere Beispiele.

Wenn du möchtest, kannst du mit der Option javap -private zusätzliche Informationen anzeigen:

$ javap -private Person
Compiled from "Person.scala"
public class Person {
  private java.lang.String name;   // new
  private int age;                 // new
  public Person(java.lang.String, int);
  public java.lang.String name();
  public void name_$eq(java.lang.String);
  public int age();
  public void age_$eq(int);
}

javap hat noch einige weitere nützliche Optionen. Verwende die Option -c, um die tatsächlichen Befehle zu sehen, aus denen der Java-Bytecode besteht, und füge die Option -verbose hinzu, um viele weitere Details zu sehen. Führe javap -help aus, um Details zu allen Optionen zu erhalten.

Diskussion

Das Disassemblieren von Klassendateien mit javap kann eine hilfreiche Methode sein, um zu verstehen, wie Scala funktioniert. Wie du im ersten Beispiel mit der Klasse Person gesehen hast, erzeugt die Definition der Konstruktorparameter name und age als var Felder eine ganze Reihe von Methoden für dich.

Als zweites Beispiel nimmst du das Attribut var aus diesen beiden Feldern heraus, so dass du diese Klassendefinition erhältst:

class Person(name: String, age: Int)

Kompiliere diese Klasse mit scalac und führe dann javap auf die resultierende Klassendatei aus. Du wirst sehen, dass dies zu einer viel kürzeren Klassensignatur führt:

$ javap -public Person
Compiled from "Person.scala"
public class Person {
  public Person(java.lang.String, int);
}

Wenn du hingegen var für beide Felder belässt und die Klasse in eine Fallklasse umwandelst, wird die Menge an Code, die Scala für dich generiert, deutlich größer. Um das zu sehen, ändere den Code in Person.scala so, dass du diese Fallklasse hast:

case class Person(var name: String, var age: Int)

Wenn du diesen Code kompilierst, werden zwei Ausgabedateien erstellt, Person.class und Person$.class. Disassembliere diese beiden Dateien mit javap:

$ javap -public Person
Compiled from "Person.scala"
public class Person implements scala.Product,java.io.Serializable {
  public static Person apply(java.lang.String, int);
  public static Person fromProduct(scala.Product);
  public static Person unapply(Person);
  public Person(java.lang.String, int);
  public scala.collection.Iterator productIterator();
  public scala.collection.Iterator productElementNames();
  public int hashCode();
  public boolean equals(java.lang.Object);
  public java.lang.String toString();
  public boolean canEqual(java.lang.Object);
  public int productArity();
  public java.lang.String productPrefix();
  public java.lang.Object productElement(int);
  public java.lang.String productElementName(int);
  public java.lang.String name();
  public void name_$eq(java.lang.String);
  public int age();
  public void age_$eq(int);
  public Person copy(java.lang.String, int);
  public java.lang.String copy$default$1();
  public int copy$default$2();
  public java.lang.String _1();
  public int _2();
}

$ javap -public Person$
Compiled from "Person.scala"
public final class Person$ implements scala.deriving.Mirror$Product,
java.io.Serializable {
  public static final Person$ MODULE$;
  public static {};
  public Person apply(java.lang.String, int);
  public Person unapply(Person);
  public java.lang.String toString();
  public Person fromProduct(scala.Product);
  public java.lang.Object fromProduct(scala.Product);
}

Wenn du eine Klasse als Fallklasse definierst, generiert Scala eine Menge Code für dich, und diese Ausgabe zeigt die öffentliche Signatur für diesen Code. In Rezept 5.14, "Erzeugen von Boilerplate-Code mit Case-Klassen", findest du eine detaillierte Beschreibung dieses Codes.

Über diese .tasty-Dateien

Du hast vielleicht schon bemerkt, dass Scala 3 während des Kompilierungsprozesses nicht nur .class-Dateien, sondern auch .tasty-Dateien erzeugt. Diese Dateien werden im so genannten TASTy-Format erzeugt, wobei das Akronym TASTy von dem Begriff typisierte abstrakte Syntaxbäume kommt.

In der TASTy-Inspektionsdokumentation heißt es dazu: "TASTy-Dateien enthalten den vollständigen Typisierungsbaum einer Klasse einschließlich der Quellpositionen und der Dokumentation. Dies ist ideal für Werkzeuge, die semantische Informationen aus dem Code analysieren oder extrahieren."

Sie werden unter anderem für die Integration zwischen Scala 3 und Scala 2.13+ verwendet. Auf der Scala-Seite zur Vorwärtskompatibilität heißt es: "Scala 2.13 kann diese (TASTy)-Dateien lesen, um zum Beispiel zu erfahren, welche Terme, Typen und Implikits in einer bestimmten Abhängigkeit definiert sind und welcher Code generiert werden muss, um sie korrekt zu verwenden. Der Teil des Compilers, der dies verwaltet, wird als Tasty Reader bezeichnet."

Siehe auch

  • In meinem Blogbeitrag "How to Create Inline Methods in Scala 3" zeige ich, wie man diese Technik verwendet, um inline Methoden zu verstehen.

  • Vielleicht kannst du auch Decompiler verwenden, um .class-Dateien in Java-Code umzuwandeln. Ich verwende gelegentlich ein Tool namens JAD, das 2001 eingestellt wurde, aber erstaunlicherweise auch zwanzig Jahre später noch in der Lage ist, Klassendateien zumindest teilweise zu dekompilieren. Ein viel modernerer Decompiler namens CFR wurde auch im Scala Gitter Channel erwähnt.

Weitere Informationen über TASTy und .tasty-Dateien findest du in diesen Ressourcen:

1.6 JAR-Dateien mit Scala und Java ausführen

Problem

Du hast eine JAR-Datei aus einer Scala-Anwendung erstellt und möchtest sie mit den Befehlen scala oder java ausführen.

Lösung

Erstelle zunächst ein grundlegendes sbt-Projekt, wie in Rezept 17.1, "Erstellen einer Projektverzeichnisstruktur für sbt" gezeigt . Füge dann sbt-assembly in die Projektkonfiguration ein, indem du diese Zeile in die Datei project/plugins.sbt einfügst:

// note: this version number changes several times a year
addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.15.0")

Dann lege diese Hello.scala-Quellcodedatei in das Stammverzeichnis des Projekts:

@main def hello = println("Hello, world")

Als Nächstes erstellst du eine JAR-Datei entweder mit dem Befehl assembly oder show assembly in der sbt-Shell:

// option 1
sbt:RunJarFile> assembly

// option 2: shows the output file location
sbt:RunJarFile> show assembly
[info] target/scala-3.0.0/RunJarFile-assembly-0.1.0.jar

Wie gezeigt, gibt die Ausgabe des Befehls show assembly den Ort aus, an den die JAR-Ausgabedatei geschrieben wird. Diese Datei beginnt mit dem Namen RunJarFile, weil das der Wert des Feldes name in meiner build.sbt-Datei ist. Ebenso stammt der Teil 0.1.0 des Dateinamens aus dem Feld version in dieser Datei:

lazy val root = project
   .in(file("."))
   .settings(
      name := "RunJarFile",
      version := "0.1.0",
      scalaVersion := "3.0.0"
  )

Als Nächstes erstellst du ein Unterverzeichnis Example, wechselst in dieses Verzeichnis und kopierst die JAR-Datei in dieses Verzeichnis:

$ mkdir Example
$ cd Example
$ cp ../target/scala-3.0.0/RunJarFile-assembly-0.1.0.jar .

Da das sbt-assembly Plugin alles, was du brauchst, in die JAR-Datei packt, kannst du die Methode hello main mit diesem scala Befehl ausführen:

$ scala -cp "RunJarFile-assembly-0.1.0.jar" hello
Hello, world

Wenn deine JAR-Datei mehrere @main Methoden in Paketen enthält, kannst du sie mit ähnlichen Befehlen ausführen, indem du den vollständigen Pfad zu den Methoden am Ende des Befehls angibst:

scala -cp "RunJarFile-assembly-0.1.0.jar" com.alvinalexander.foo.mainMethod1
scala -cp "RunJarFile-assembly-0.1.0.jar" com.alvinalexander.bar.mainMethod2

Diskussion

Wenn du (a) versuchst, deine JAR-Datei mit dem Befehl java auszuführen, oder (b) die JAR-Datei mit sbt package anstelle von sbt assembly erstellst, musst du die Abhängigkeiten deiner JAR-Datei manuell zu deinem Klassenpfad hinzufügen. Wenn du zum Beispiel eine JAR-Datei mit dem Befehl java ausführst, musst du einen Befehl wie den folgenden verwenden:

$ java -cp "~/bin/scala3/lib/scala-library.jar:my-packaged-jar-file.jar"foo.bar.Hello
Hello, world

Beachte, dass der gesamte java Befehl in einer Zeile stehen sollte, einschließlich des foo.bar.Hello Teils am Ende der Zeile.

Für diesen Ansatz musst du die Datei scala-library.jar finden. Da ich die Scala 3-Distribution manuell verwalte, habe ich sie in dem angegebenen Verzeichnis gefunden. Wenn du ein Tool wie Coursier verwendest, um deine Scala-Installation zu verwalten, findest du die Dateien, die es herunterlädt, in diesen Verzeichnissen:

  • Unter macOS: ~/Library/Caches/Coursier/v1

  • Unter Linux: ~/.cache/coursier/v1

  • Unter Windows: %LOCALAPPDATA%\Coursier\Cache\v1, was für einen Benutzer namens Alvin normalerweise C:\Benutzer\Alvin\AppData\Local\Coursier\Cache\v1 entspricht

Auf der Seite Coursier Cache findest du aktuelle Informationen zu diesen Verzeichnissen.

Warum sbt-assembly verwenden?

Wenn deine Anwendung verwaltete oder nicht verwaltete Abhängigkeiten verwendet und du sbt package anstelle von sbt assembly verwendest, musst du alle diese Abhängigkeiten und ihre transitiven Abhängigkeiten verstehen, diese JAR-Dateien finden und sie dann in die Klassenpfadeinstellungen aufnehmen. Aus diesem Grund wird die Verwendung von sbt assembly oder einem ähnlichen Tool dringend empfohlen.

Siehe auch

Get Scala Kochbuch, 2. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.