Man lernt durch den Vergleich mit dem, was man bereits weiß. Ich wurde kürzlich davon überrascht, dass ich annahm, Rust würde wie Java bei der transitiven Abhängigkeitsversionsauflösung funktionieren. In diesem Beitrag möchte ich die beiden vergleichen.
Bevor wir auf die Besonderheiten jedes Stacks eingehen, beschreiben wir die Domäne und die damit verbundenen Probleme.
\ Bei der Entwicklung eines Projekts über das Hello-World-Niveau hinaus werden Sie wahrscheinlich auf Probleme stoßen, mit denen andere bereits konfrontiert waren. Wenn das Problem weit verbreitet ist, ist die Wahrscheinlichkeit hoch, dass jemand freundlich und bürgerlich genug war, den Code, der es löst, für andere zur Wiederverwendung zu verpacken. Jetzt können Sie das Paket verwenden und sich auf die Lösung Ihres Kernproblems konzentrieren. So baut die Industrie heute die meisten Projekte auf, auch wenn es andere Probleme mit sich bringt: Sie stehen auf den Schultern von Riesen.
\ Sprachen kommen mit Build-Tools, die solche Pakete zu Ihrem Projekt hinzufügen können. Die meisten von ihnen bezeichnen Pakete, die Sie zu Ihrem Projekt hinzufügen, als Abhängigkeiten. Wiederum können Projektabhängigkeiten ihre eigenen Abhängigkeiten haben: Letztere werden als transitive Abhängigkeiten bezeichnet.

Im obigen Diagramm sind C und D transitive Abhängigkeiten.
\ Transitive Abhängigkeiten haben ihre eigenen Probleme. Das größte Problem tritt auf, wenn eine transitive Abhängigkeit von verschiedenen Pfaden benötigt wird, aber in unterschiedlichen Versionen. Im Diagramm unten hängen sowohl A als auch B von C ab, aber von unterschiedlichen Versionen davon.

Welche Version von C sollte das Build-Tool in Ihr Projekt aufnehmen? Java und Rust haben unterschiedliche Antworten. Lassen Sie uns sie der Reihe nach beschreiben.
Zur Erinnerung: Java-Code wird zu Bytecode kompiliert, der dann zur Laufzeit interpretiert wird (und manchmal zu nativem Code kompiliert wird, aber das liegt außerhalb unseres aktuellen Problembereichs). Ich werde zuerst die Laufzeit-Abhängigkeitsauflösung und die Build-Zeit-Abhängigkeitsauflösung beschreiben.
\ Zur Laufzeit bietet die Java Virtual Machine das Konzept eines Klassenpfads. Wenn eine Klasse geladen werden muss, durchsucht die Laufzeit den konfigurierten Klassenpfad der Reihe nach. Stellen Sie sich die folgende Klasse vor:
public static Main { public static void main(String[] args) { Class.forName("ch.frankel.Dep"); } }
\ Lassen Sie uns sie kompilieren und ausführen:
java -cp ./foo.jar:./bar.jar Main
\ Das obige Beispiel sucht zuerst in der foo.jar nach der Klasse ch.frankel.Dep. Wenn gefunden, stoppt es dort und lädt die Klasse, unabhängig davon, ob sie auch in der bar.jar vorhanden sein könnte; wenn nicht, sucht es weiter in der bar.jar-Klasse. Wenn sie immer noch nicht gefunden wird, schlägt es mit einer ClassNotFoundException fehl.
\ Javas Laufzeit-Abhängigkeitsauflösungsmechanismus ist geordnet und hat eine pro-Klasse-Granularität. Es gilt, ob Sie eine Java-Klasse ausführen und den Klassenpfad wie oben auf der Befehlszeile definieren oder ob Sie ein JAR ausführen, das den Klassenpfad in seinem Manifest definiert.
\ Ändern wir den obigen Code wie folgt:
public static Main { public static void main(String[] args) { var dep = new ch.frankel.Dep(); } }
\ Da der neue Code direkt auf Dep verweist, erfordert neuer Code die Klassenauflösung zur Kompilierzeit. Die Klassenpfadauflösung funktioniert auf die gleiche Weise:
javac -cp ./foo.jar:./bar.jar Main
\ Der Compiler sucht nach Dep in foo.jar, dann in bar.jar, wenn nicht gefunden. Das oben Genannte ist das, was Sie am Anfang Ihrer Java-Lernreise lernen.
\ Danach ist Ihre Arbeitseinheit das Java-Archiv, bekannt als JAR, anstelle der Klasse. Ein JAR ist ein verbessertes ZIP-Archiv mit einem internen Manifest, das seine Version angibt.
\ Stellen Sie sich nun vor, dass Sie ein Benutzer von foo.jar sind. Entwickler von foo.jar setzen beim Kompilieren einen bestimmten Klassenpfad, der möglicherweise andere JARs enthält. Sie benötigen diese Informationen, um Ihren eigenen Befehl auszuführen. Wie gibt ein Bibliotheksentwickler dieses Wissen an nachgelagerte Benutzer weiter?
\ Die Community kam mit einigen Ideen auf, um diese Frage zu beantworten: Die erste Antwort, die haften blieb, war Maven. Maven hat das Konzept des Project Object Model, in dem Sie die Metadaten Ihres Projekts sowie Abhängigkeiten festlegen. Maven kann transitive Abhängigkeiten leicht auflösen, da sie auch ihr POM mit ihren eigenen Abhängigkeiten veröffentlichen. Daher kann Maven die Abhängigkeiten jeder Abhängigkeit bis zu den Blattabhängigkeiten verfolgen.
\ Nun zurück zur Problemstellung: Wie löst Maven Versionskonflikte auf? Welche Abhängigkeitsversion wird Maven für C auflösen, 1.0 oder 2.0?
\ Die Dokumentation ist klar: die nächstgelegene.

Im obigen Diagramm hat der Pfad zu v1 eine Distanz von zwei, eine zu B, dann eine zu C; währenddessen hat der Pfad zu v2 eine Distanz von drei, eine zu A, dann eine zu D, dann schließlich eine zu C. Somit zeigt der kürzeste Pfad auf v1.
\ In dem ursprünglichen Diagramm sind jedoch beide C-Versionen in der gleichen Entfernung vom Wurzelartefakt. Die Dokumentation bietet keine Antwort. Wenn Sie daran interessiert sind, hängt es von der Reihenfolge der Deklaration von A und B im POM ab! Zusammenfassend gibt Maven eine einzelne Version einer duplizierten Abhängigkeit zurück, um sie in den Kompilierklassenpfad aufzunehmen.
\ Wenn A mit C v2.0 oder B mit C 1.0 arbeiten kann, großartig! Wenn nicht, müssen Sie wahrscheinlich Ihre Version von A aktualisieren oder Ihre Version von B herabstufen, damit die aufgelöste C-Version mit beiden funktioniert. Es ist ein manueller Prozess, der schmerzhaft ist – fragen Sie mich, woher ich das weiß. Schlimmer noch, Sie könnten feststellen, dass es keine C-Version gibt, die mit sowohl A als auch B funktioniert. Zeit, A oder B zu ersetzen.
Rust unterscheidet sich in mehreren Aspekten von Java, aber ich denke, die folgenden sind für unsere Diskussion am relevantesten:
\ Lassen Sie uns sie einzeln untersuchen.
\ Java kompiliert zu Bytecode, dann führen Sie letzteren aus. Sie müssen den Klassenpfad sowohl zur Kompilierzeit als auch zur Laufzeit festlegen. Das Kompilieren mit einem bestimmten Klassenpfad und das Ausführen mit einem anderen kann zu Fehlern führen. Stellen Sie sich zum Beispiel vor, Sie kompilieren mit einer Klasse, von der Sie abhängig sind, aber die Klasse ist zur Laufzeit nicht vorhanden. Oder alternativ ist sie vorhanden, aber in einer inkompatiblen Version.
\ Im Gegensatz zu diesem modularen Ansatz kompiliert Rust zu einem einzigartigen nativen Paket den Code der Kiste und jede Abhängigkeit. Darüber hinaus bietet Rust auch sein eigenes Build-Tool, wodurch vermieden wird, sich die Eigenheiten verschiedener Tools merken zu müssen. Ich erwähnte Maven, aber andere Build-Tools haben wahrscheinlich unterschiedliche Regeln, um die Version im obigen Anwendungsfall aufzulösen.
\ Schließlich löst Java Abhängigkeiten aus Binärdateien auf: JARs. Im Gegensatz dazu löst Rust Abhängigkeiten aus Quellen auf. Zur Build-Zeit löst Cargo den gesamten Abhängigkeitsbaum auf, lädt alle erforderlichen Quellen herunter und kompiliert sie in der richtigen Reihenfolge.
\ Mit diesem Wissen, wie löst Rust die Version der C-Abhängigkeit im ursprünglichen Problem auf? Die Antwort mag seltsam erscheinen, wenn Sie aus einem Java-Hintergrund kommen, aber Rust schließt beide ein. Tatsächlich wird Rust im obigen Diagramm A mit C v1.0 und B mit C v2.0 kompilieren. Problem gelöst.
JVM-Sprachen, und insbesondere Java, bieten sowohl einen Kompilierzeit-Klassenpfad als auch einen Laufzeit-Klassenpfad. Es ermöglicht Modularität und Wiederverwendbarkeit, öffnet aber die Tür zu Problemen bezüglich der Klassenpfadauflösung. Auf der anderen Seite baut Rust Ihre Kiste zu einer einzigen eigenständigen Binärdatei, sei es eine Bibliothek oder eine ausführbare Datei.
\ Um weiterzugehen:
Ursprünglich veröffentlicht bei A Java Geek am 14.09.2025

