Java Interop

Clojure läuft auf der JVM

  • Cross Platform
  • Zugriff auf Java Ökosystem

Für viele Funktionalitäten gibt es bereits gute Java Bibliotheken:

  • Parsen von speziellen Dateiformaten
  • Kryptographie
  • Netzwerkkommunikation
  • Datenbankzugriffe
  • ...

Wir können jede Java Klasse direkt verwenden.

Die Clojure Datentypen sind unter der Haube als Java Klassen implementiert.

Die Member Methoden einer Klasse können wir direkt aufrufen:


          (.toUpperCase "hello world")
        

Statische Methoden und Felder einer Java Klasse können wir auch direkt aufrufen:


          (println (java.lang.Math/abs -3))

          (println (java.lang.Math/PI))
        

Wir können neue Objekte auf zwei Arten erzeugen:


          (def d (java.util.Date. 1685016589000))

          (def d (new java.util.Date 1685016589000))
        

Java Objekte bringen üblicherweise Mutability mit:


          (def stack (java.util.Stack.))

          (.push stack "a")
          (.push stack 1)
          (.push stack :foo)

          (println (.pop stack))
        

Java Container sind nicht wirklich stark typisiert.

Mit doto können wir eine ähnliche Semantik wie Threading darstellen:


          (def stack (doto (java.util.Stack.)
                       (.push "a")
                       (.push 1)
                       (.push :foo)))

          (println (.pop stack))
        

Für Java Klassen gibt es ein import statement, welches analog zu dem Java Äquivalent funktioniert:


            (import [java.util Date Stack])

            (def stack (doto (Stack.)
                         (.push "a")))
          

Meistens wollen wir aber (analog zu require) die Java Imports als Teil der Namespace Definition haben:


          (ns my-namespace
            (:import [java.util Date Stack]))

          (def stack (doto (Stack.)
                       (.push "a")))
        

Wir können Java Libraries analog zu Clojure Libraries verwenden.

Maven Central gibt uns auch direkt das passende Leiningen Snippet.

Es ist auch möglich in Clojure Klassen zu erstellen, die dann in Java genutzt werden können.

    Stichworte:

  • gen-class
  • defrecord

Der übliche Weg ist eher, dass wir Java von Clojure aus aufrufen

  • Weil die Funktionalität schon in Java implementiert ist.
  • Weil wir ein Performance Bottleneck haben.
  • Weil sich ein bestimmter Aspekt Objekt orientiert besser implementieren lässt.

Einige fundamentale Java Klassen finden recht häufig Verwendung

  • System
  • java.util.Date
  • java.io.File, java.io.BufferedReader, etc.

System


          (System/getenv)
          ; => Map of environment variables

          (System/getProperties)
          ; => Map of system properties

          (Sytstem/nanoTime)
          ; => Current JVM time

          (System/exit)
          ; => quit process
        

Wenn wir Dateien lesen oder schreiben wollen, brauchen wir oft die Java Reader oder Writer Klassen

  • java.io.File
  • java.io.BufferedReader
  • java.io.FileReader
  • java.io.BufferedWriter
  • java.io.FileWriter

Lesen von Dateieigenschaften:


          (let [file (java.io.File. "/")]
            (println (.exists file))
            (println (.canWrite file))
            (println (.getPath file)))
        

String $\Rightarrow$ File und File $\Rightarrow$ String sind einfach:


          (spit "/tmp/foo.txt" "Some text")

          (slurp "/tmp/foo.txt")
        

spit und slurp nehmen als Input eine beliebige schreib- oder lesbare Ressource (oder konvertieren zu einer).


          (let [s (java.io.StringWriter.)]
            (spit s "Lorem ipsum")
            (.toString s))
        

          (let [s (java.io.StringReader. "Lorem ipsum")]
            (slurp s))
        

Mit dem with-open Macro stellen wir sicher, dass die Ressource wieder geschlossen wird.


          (with-open [file (-> "/tmp/foo.txt"
                                java.io.FileReader.
                                java.io.BufferedReader.)]
            (println (first (line-seq file))))
        

Einen einfachen BufferedReader aus einer Datei können wir auch mit clojure.java.io/reader erstellen:


          (with-open [file (clojure.java.io/reader "/tmp/foo.txt")]
            (println (first (line-seq file))))
        

line-seq erstellt eine lazy sequence der Zeilen der Datei.

Exkurs: Lazy Sequences

Eine lazy sequence ist eine Liste, deren Werte noch nicht realisiert sind.

Idee: Berechne die Werte erst, wenn sie auch gebraucht werden.

Eine Lazy Sequence können wir erzeugen, indem wir angeben, wie der nächste Wert rekursiv erzeugt werden kann:


          (defn fib
            ([]
              (fib 1 1))
            ([a b]
              (println "Generating fib" a)
              (lazy-seq (cons a (fib b (+ a b))))))

          (take 30 (fib))
        

Eine Lazy Sequence kann unendlich lang sein.

Die Ergebnisse werden nur einmal berechnet und dann gespeichert.


          (defn fib
            ([]
             (fib 1 1))
            ([a b]
             (lazy-seq (cons (do (println "Generating fib" a) a)
                             (fib b (+ a b))))))

          (def fib-10 (take 10 (fib)))
          (println fib-10)
          (println fib-10)
        

Viele Standardfunktionen arbeiten mit Lazy Sequences


          (->> (range)
               (map #(* % %))
               (filter #(= 1 (mod % 2)))
               (drop 10)
               (take 20))
        

Analog zu Streams oder Generators in anderen Sprachen

Eine sehr große Lazy Sequence ist Memory effizient, wenn wir sie nur einmal verwenden.


          (let [s (map #(* % 1234567) (range 1e8))]
            (println (count s))
            (println (count s)))
        

vs


          (let [s (map #(* % 1234567) (range 1e8))]
            (println (count s)))
        

Wenn wir keine Lazy Seq haben wollen können wir sie umwandeln


          (->> (range 10)
               (map #(* % %))
               (vec))
        

oder eine Funktion nehmen, die direkt einen Vektor erzeugt


          (mapv #(* % %) (range 10))