클로저가 다른 리습 계열 언어들(Common Lisp, Scheme, Racket, …)에 비해 갖는 장점들 중의 하나는 풍부한 라이브러리들을 이용할 수 있다는 것이다. 리습 계열 언어가 자체적으로는 아무리 뛰어나다 하더라도 이용할 수 있는 라이브러리가 제한적이어서 그 활용 가능성은 현저히 떨어질 수 밖에 없었다. 하지만 클로저의 경우에는, 자바(더 정확히는, JVM 상)의 모든 방대한 라이브러리들을 그대로 아주 쉽게 가져다 쓸 수 있다는 점에서 다른 리습 계열 언어들과는 차별화된다.
이 장에서는 클로저에서 자바 코드를 어떻게 불러다 쓸 수 있는지 살펴본다.
1. 자바의 클래스, 메소드, 필드 사용하기
클로저는 자바 코드를 쉽게 호출할 수 있는 다양한 방법을 제공하는데, 다음의 표에 간결하게 정리했다.
Clojure forms | Java equivalents | |
---|---|---|
클래스의 인스턴스 생성 |
(ClassName.) (ClassName. arg1 arg2 ...) |
new ClassName() new ClassName(arg1, arg2, ...) |
객체의 인스턴스 메소드 호출 |
(.methodName object) (.methodName object arg1 arg2 ...) |
object.methodName() object.methodName(arg1, arg2, ...) |
객체의 인스턴스 필드 읽기 |
(.-field object) (.field object) |
object.field |
객체의 인스턴스 필드 쓰기 |
(set! (.-fieldName object) 5) (set! (.fieldName object) 5) |
object.fieldName = 5 |
정적 메소드 호출 |
(ClassName/staticMethod) (ClassName/staticMethod arg1 arg2 ...) |
ClassName.staticMethod() ClassName.staticMethod(arg1, arg2, ...) |
정적 필드 읽기 |
ClassName/FIELD |
ClassName.FIELD |
위의 '객체의 인스턴스 필드 읽기' 항목에서 .field
와 .-field
의 두 가지 형태가
제공되는데, 가급적이면 .-field
의 형태를 쓰기를 권한다. 그 이유는 (.field object)
의
호출 형태가 객체의 인스턴스 메소드 호출 형태인 (.methodName object)
와 겉모습이
동일해서, 실제 사용될 떄 인스턴스 필드 읽기인지 아니면 인스턴스 메소드 호출인지 구분이
가지 않기 때문이다. .-
형태는 클로저스크립트에서 먼저 도입되었는데 나중에 클로저에서도
이 표기법을 수용한 결과로 현재는 두 가지 표기법이 혼재한다.
위의 여러가지 Java Interp 형태는 일종의 syntactic sugar(구문 단축형)로, 내부적으로는 모두
클로저의 특수 형식(special form)인.
과 new
두 개로 구현되어 있다. 이 구문 단축형은
특수 형식을 이용하는 것보다 짧고 편리하므로 굳이 특수 형식을 이용할 필요는 없겠지만,
참고로 이 두 형태를 서로 비교해 보면 아래와 같다.
Clojure Syntactic Sugar |
Clojure Special Form |
|
클래스의 인스턴스 생성 |
(ClassName.) (ClassName. arg1 arg2 ...) |
(new ClassName) (new ClassName arg1 arg2 ...) |
객체의 인스턴스 메소드 호출 |
(.methodName object) (.methodName object arg1 arg2 ...) |
(. object methodName) (. object methodName arg1, arg2, ...) |
객체의 인스턴스 필드 읽기 |
(.-field object) (.field object) |
(. object field) |
객체의 인스턴스 필드 쓰기 |
(set! (.-fieldName object) 5) (set! (.fieldName object) 5) |
(set! (. object fieldName) 5) |
정적 메소드 호출 |
(ClassName/staticMethod) (ClassName/staticMethod arg1 arg2 ...) |
(. ClassName staticMethod) (. ClassName staticMethod arg1 arg2 ...) |
정적 필드 읽기 |
ClassName/FIELD |
(. ClassName FIELD) |
1.1. 자바 클래스 import하기
자바 클래스를 사용할 때는, clojure.core/import
함수를 이용해 먼저 import해 주면, 짧은
클래스 이름만을 이용해 다음과 같이 간편하게 호출할 수 있다.
(import java.util.Date)
(Date.) ;=> #inst "2015-12-07T08:07:00.231-00:00"
하지만 import
함수를 이용하지 않고, 다음과 같이 완전한 패키지명을 붙여 호출할 수도 있다.
(java.util.Date.) ;=> #inst "2015-12-07T08:08:23.918-00:00"
자바 클래스 import와 관련된 자세한 내용은, Namespaces and Libraries: import 부분을 참조하기 바란다.
1.2. 인스턴스 생성 및 인스턴스 메소드 호출
자바 클래스의 인스턴스를 생성하려먼, 클래스 이름 바로 뒤에 .
을 붙인다. 생성자의
인수들은 그 뒤에 나열하면 된다.
(import 'java.net.URL)
(def cnn (URL. "http://cnn.com"))
그리고 해당 인스턴스의 메소드를 호출하려면, 메소드 이름 바로 앞에 .
을 붙이고, 그 뒤에
인스턴스 객체가 온다. 호출하려는 메소드에 추가로 인수가 있는 경우에는 이 인스턴스 객체
뒤에 차례로 나열해 준다.
(.getHost cnn) ;=> "cnn.com"
(slurp cnn)
;=> "<html lang=\"en\"><head><title>CNN.com........."
1.3. 정적 필드 및 정적 메소드 호출
정적 필드는 클래스명 뒤에 슬래시(/
) 기호를 붙인 후 필드명을 써 준다.
Double/MAX_VALUE ;=> 1.7976931348623157E308
정적 메소드 역시 클래스명 뒤에 슬래시 기호를 붙인 후 메소드명을 써 준다. 정적 메소드의 인수들은 메소드명 뒤에 차례대로 나열해 준다.
(Double/parseDouble "3.141592653589793") ;=> 3.141592653589793
1.4. 객체의 필드 읽고 쓰기
자바에서 필드를 public으로 노출하는 경우는 드물고, 그 값의 변경을 허용하는 예는 더욱
드물지만, 클로저에서는 객체의 public 필드를 읽고 쓰는 방법을 제공한다. 객체의 필드에 값을
설정할 때는 클로저의 특수 형식(special form)인 set!
를 이용해야 한다.
(import 'java.awt.Point)
(def pt (Point. 5 10))
(.-x pt) ;=> 5
(set! (.-x pt) -42)
(.-x pt) ;=> -42
물론, 위에서 .-x
대신 .x
를 써도 된다.
2. 유용한 Interop 관련 함수들
2.1. class
class
함수는 인수로 주어진 객체의 class를 반환한다.
(class "foo") ;=> java.lang.String
2.2. instance?
instance?
함수는 두 번째 인수가 첫 번째 인수로 주어진 클래스의 인스턴스인지 여부를
반환한다.
(instance? String "foo") ;=> true
2.3. ..
다음과 같은 연속 호출(chained calls) 형태의 자바 코드를, 클로저에서는 ..
매크로를 이용해
표현할 수 있다.
import java.util.Date;
Date date = new Date();
date.getTime().toString();
(import 'java.util.Date)
(.. (Date.) getTime toString) ;=> "1449477417080"
호출하고자 하는 메소드가 인수를 필요로 하지 않으면 괄호로 둘러 싸지 않아도 된다. 즉, 다음의 두 코드는 같은 결과를 반환한다. 하지만, 코드의 간결성을 위해서는 괄호로 둘러싸지 않는 것이 바람직하다.
(.. "fooBAR" (toLowerCase) (contains "ooba")) ;=> true
(.. "fooBAR" toLowerCase (contains "ooba")) ;=> true
위의 코드는
|
2.4. doto
doto
매크로는 자바의 '동일한' 인스턴스 객체를 대상으로 여러 번의 설정 작업을 반복적으로
수행할 때 이용하면 편리하다.
예를 들면, 다음과 같은 자바 코드가 있을 때
ArrayList list = new ArrayList();
list.add(1);
list.add(2);
list.add(3);
이것을 클로저 코드로 변환하면 다음과 같다.
(import 'java.util.ArrayList)
(let [alist (ArrayList.)]
(.add alist 1)
(.add alist 2)
(.add alist 3)
alist)
;=> [1 2 3]
하지만 doto
매크로를 이용하면 다음과 같이 간결하게 표현할 수 있다. doto
는 설정을
마친 인스턴스 객체를 반환한다.
(import 'java.util.ArrayList)
(doto (ArrayList.)
(.add 1)
(.add 2)
(.add 3))
;=> [1 2 3]
예를 들어, 다음의 graphics
가 java.awt.Graphics2D
의 객체일 때, 다음과 같은 연속적인
작업을 doto
매크로를 이용해 수월하게 처리할 수 있다.
(doto graphics
(.setBackground Color/white)
(.setColor Color/black)
(.scale 2 2)
(.clearRect 0 0 500 500)
(.drawRect 100 100 300 300))
3. Exceptions and Error Handling
클로저의 예외 처리는 자바의 예외 처리 방식을 그대로 이용한다. catch
절은 여러 개 나열될
수 있고, finally
절은 선택적으로 올 수 있다.
public static Integer asInt (String s) {
try {
return Integer.parseInt(s);
} catch (NumberFormatException e) {
e.printStackTrace();
return null;
} finally {
System.out.println("Attempted to parse as integer: " + s);
}
}
(defn as-int
[s]
(try
(Integer/parseInt s)
(catch NumberFormatException e
(.printStackTrace e))
(finally
(println "Attempted to parse as integer: " s))))
자바에서는 catch 와 finally 절이 try 절과 병렬로 배치되어 있는 반면에,
클로저에서는 catch 와 finally 절이 try 절의 내부에 속해 있다는 차이점에 주의하자.
|
예외를 던질 때에는 자바에서와 마찬가지로 throw
를 이용한다. 이때 throw
의 인수는
반드시 예외 클래스의 인스턴스이어야 한다.
(throw (IllegalStateException. "I don't know what to do!"))
;>> IllegalStateException I don't know what to do!
자바에서는 다음과 같이 메소드를 정의할 때, throws
뒤에 그 메소드가 던질 예외를 미리
선언할 수 있는데, 이런 예외를 checked exception[1]이라 부른다.
public static int parseInt(String s) throws NumberFormatException { ... }
자바에서는, 이와 같은 메소드를 '호출’하는 코드에서 try/catch/finally
구문을 통해 이
예외를 반드시 명시적으로 처리해 주어야 한다. 그렇지 않으면 컴파일러시 에러가 발생한다.
하지만 클로저에서는 그럴 필요가 없다. 그 이유는 checked exception을 강제하는 것은 자바 컴파일러이지 JVM 자체의 요구 사양은 아니기 때문이다. 클로저 소스 코드는 클로저의 자체 컴파일러가 직접 컴파일을 수행하므로 자바 컴파일러의 요구 사항을 무시할 수 있다.
4. Type Hinting for Performance
클로저에서는 ^ClassName
형식으로 type hinting 정보를 줄 수 있다.
(defn length-of
[^String text]
(.length text))
위와 같이 타입 힌팅 정보를 주면 ^{:tag String} text
의 형식으로 text 인수의 metadata
맵의 :tag
키에 type 정보가 들어간다.
그런데 타입 힌팅 정보를 주더라도, Java Interop 호출을 하지 않으면 그 정보는 쓰이지 않고 컴파일러에 의해 무시된다.
(ns clj-prog.java-interop)
(defn accepts-anything-hint
[^java.util.List x]
x)
(defn accepts-anything-no-hint
[x]
x)
위의 두 함수를 컴파일 한 후에, 컴파일된 .class
파일을 다시 decompile해 보면, 다음과 같이
그 결과에 차이가 전혀 없다는 것을 확인할 수 있다.
import clojure.lang.AFunction;
public final class java_interop$accepts_anything_hint extends AFunction {
public java_interop$accepts_anything_hint() {
}
public static Object invokeStatic(Object x) {
Object var10000 = x;
x = null;
return var10000;
}
public Object invoke(Object var1) {
Object var10000 = var1;
var1 = null;
return invokeStatic(var10000);
}
}
import clojure.lang.AFunction;
public final class java_interop$accepts_anything_no_hint extends AFunction {
public java_interop$accepts_anything_no_hint() {
}
public static Object invokeStatic(Object x) {
Object var10000 = x;
x = null;
return var10000;
}
public Object invoke(Object var1) {
Object var10000 = var1;
var1 = null;
return invokeStatic(var10000);
}
}
반면에 Java Interop 호출이 있는 경우에는, type hinting 정보가 있으면 컴파일시 그 정보가 반영되어, 런타임시 reflection으로 인한 실행 시간 지연을 막을 수 있다.
(defn length-of-hint
[^String text]
(.length text))
(defn length-of-no-hint
[text]
(.length text))
import clojure.lang.AFunction;
public final class java_interop$length_of_hint extends AFunction {
public java_interop$length_of_hint() {
}
public static Object invokeStatic(Object text) {
Object var10000 = text;
text = null;
return Integer.valueOf(((String)var10000).length()); (1)
}
public Object invoke(Object var1) {
Object var10000 = var1;
var1 = null;
return invokeStatic(var10000);
}
}
1 | (String) 으로 타입 캐스팅되어 있다. |
import clojure.lang.AFunction;
import clojure.lang.Reflector;
public final class java_interop$length_of_no_hint extends AFunction {
public java_interop$length_of_no_hint() {
}
public static Object invokeStatic(Object text) {
Object var10000 = text;
text = null;
return Reflector.invokeNoArgInstanceMember(var10000, "length", false); (1)
}
public Object invoke(Object var1) {
Object var10000 = var1;
var1 = null;
return invokeStatic(var10000);
}
}
1 | 런타임에 reflection이 행해져 실행 시간의 지연을 초래하고 있다. |
타입 힌팅 정보를 주면, 실행 속도를 향상시킬 수 있다.
(defn capitalize
[s]
(-> s
(.charAt 0)
Character/toUpperCase
(str (.substring s 1))))
(time (doseq [s (repeat 100000 "foo")]
(capitalize s)))
;>> "Elapsed time: 5040.218 msecs"
(defn fast-capitalize
[^String s]
(-> s
(.charAt 0)
Character/toUpperCase
(str (.substring s 1))))
(time (doseq [s (repeat 100000 "foo")]
(fast-capitalize s)))
;>> "Elapsed time: 154.889 msecs"
위의 실행 결과를 보면, 타입 힌팅 정보가 주어졌을 때 실행 시간이 단축되는 것을 확인할 수 있다. 하지만, 실행 속도를 향상시킬 수 있다고 해서 타입 힌팅 정보를 남발하는 것은 바람직하지 못하다. 프로파일링(profiling)을 실시해서 병목 지점을 확인한 후, 그 부분을 최적화할 때 타입 힌팅 정보를 주는 것이 바람직하다.
그런데 클로저 컴파일러가 코드의 어느 부분에서 reflection 기능을 호출하고 있는지 확인할 수
있으면 코드의 어느 부분에 타입 힌팅 정보를 주어야 할지 판단하는 데 도움이 될 것이다. 이런
경우에 *warn-on-reflection*
값을 true
로 설정해 주면, 컴파일시 클로저 컴파일러가
코드의 어느 부분에서 reflection 기능을 호출하고 있는지 출력해 준다.
(set! *warn-on-reflection* true)
(defn capitalize
[s]
(-> s
(.charAt 0)
Character/toUpperCase
(str (.substring s 1))))
;>> Reflection warning, NO_SOURCE_PATH:27 - call to charAt can't be resolved.
;>> Reflection warning, NO_SOURCE_PATH:29 - call to toUpperCase can't be resolved.
;>> Reflection warning, NO_SOURCE_PATH:29 - call to substring can't be resolved.
project.clj 파일에서 :warn-on-reflection true
로 설정해도 같은 결과를 얻을 수 있다.
타입 힌팅 정보는 어느 식(expression)에나 붙일 수 있다. 다음과 같이 함수의 반환값에도 표시할 수 있다.
(defn split-name
[user]
(zipmap [:first :last]
(.split ^String (:name user) " ")))
함수를 정의할 때 반환값에도 표시할 수 있다.
(defn file-extension
^String [^java.io.File f] ; (1)
(-> (re-seq #"\.(.+)" (.getName f))
first
second))
(.toUpperCase (file-extension (java.io.File. "image.png")))
1 | ^String 부분이 함수의 반환값이 String형임을 표시한다. |
var에도 표시할 수 있다.
(def a "image.png")
(java.io.File. a)
;>> Reflection warning, NO_SOURCE_PATH:1 - call to java.io.File ctor can't be resolved.
(def ^String a "image.png")
(java.io.File. a)
;=> #<File image.png>
5. Arrays
클로저에서는 자바의 기본(primitive) 자료형의 배열도 직접 다룰 수 있는 방법을 제공한다. 그래서 필요한 경우 처리 속도를 높이는 데 사용할 수 있다. 자세한 내용은 Numerics and Mathematics에서 다룬다.
Operation | Clojure expression | Java equivalent |
---|---|---|
컬렉션으로부터 배열 생성하기 |
(into-array ["a" "b" "c"]) |
(String[]) coll.toArray(new String[list.size()]); |
빈 배열 생성하기 |
(make-array Integer 10 100) |
new Integer[10][100] |
long형의 빈 배열 생성하기 |
(long-array 10) (make-array Long/TYPE 10) |
new long[10] |
배열의 값 읽기 |
(aget some-array 4) |
some_array[4] |
배열에 값 쓰기 |
(aset some-array 4 "foo") (aset ^ints int-array 4 5) |
some_array[4] = 5.6 |
6. Defining Classes and Implementing Interfaces
proxy | gen-class | reify | deftype | defrecord | |
---|---|---|---|---|---|
무명 클래스의 인스턴스 반환 |
O |
O |
|||
이름 있는 클래스 |
O |
O |
O |
||
부모 클래스 확장 |
O |
O |
|||
implicit this |
O |
||||
새 필드 정의 |
O |
O |
|||
AOT compile |
O |
||||
Object.equals, Object.hashcode 및 여러가지 클로저 인터페이스 default 제공 |
O |
-
reify, defrecord, deftype: Clojure 내부에서 사용
-
proxy, gen-class: Java Interop을 위해 사용
6.1. 무명 클래스의 인스턴스 만들기: proxy
reify
와 proxy
둘 다 무명 클래스의 인스턴스를 생성한다. 그리고 둘 다 top level
form으로 쓰여서는 안된다. 이 무명 클래스는 컴파일할 때 한 번만 생성되고, reify
와
proxy
를 감싸고 있는 함수가 호출될 때마다 이 무명 클래스의 인스턴스가 매번 생성된다.
-
reify
는 자바의 인터페이스나 클로저의 프로토콜을 구현(implement)하고, 자바의 클래스들 중 오직java.lang.Object
클래스만을 확장(extend)할 수 있다. 그런데 실제로java.lang.Object
클래스를 확장하는 일은 거의 없으므로, 자바의 인터페이스나 클로저의 프로토콜을 구현(implement)할 때 주로 사용된다고 보면 된다. -
proxy
는reify
가 할 수 있는 일에 더해, 모든 자바의 클래스를 확장할 수 있다. 따라서 상위 클래스의 메소드를 재정의(overriding)할 필요가 있을 때 주로 사용된다. 하지만 상위 클래스에는 없는 새로운 메소드를 정의할 수는 없다. 이 일을 하려면gen-class
를 사용해야 한다.
상위 클래스를 subclassing할 일이 없으면, reify 를 사용하는 것이 좋다.
|
(proxy [super-class? interface*] [super-class-constructor-argument*] fun*) super-class := 상속할 상위 클래스를 지정하며, 맨 처음에 와야 한다. 상위 클래스가 지정되지 않으면 java.lang.Objcet를 상속하게 된다. interface := 구현하고자 하는 자바의 인터페이스 또는 클로저의 프로토콜 super-class-constructor-argument := 상위 클래스 생성자의 인수 fun := 상위 클래스의 재정의하고자 하는 함수 또는 구현하고자 하는 인터페이스의 함수 (name [params*] body) | (name ([params*] body) ([params+] body) ...) fun은 closure를 형성할 수 있다.
다음은 java.lang.Object
의 toString
메소드를 재정의하고, 클로저의 프로토콜
clojure.lang.IDeref
을 구현한 예이다.
(defn make-some-example
[msg]
(let [state (atom msg)]
(proxy [clojure.lang.IDeref] [] ; (1)
(toString [] @state) ; (2)
(deref [] state))))
(def o (make-some-example "Hello, there!"))
(.toString o) ;=> "Hello, there!"
(reset! @o "Hi, everyone!")
(.toString o) ;=> "Hi, everyone!"
1 | clojure.lang.IDeref 프로토콜을 구현하므로, deref(또는 @ )를 사용할 수 있다. |
2 | proxy 내부에서 지역 심볼 state를 closure로 참조할 수 있다. |
proxy 내에서 overloading하는 함수들 안에는 (toString [this] …) 와 같이
this 를 명시적으로 넣어줄 필요가 없다. 반면에, proxy 를 제외한 다른 reify ,
defrecord , deftype , gen-class 의 경우에는 모두 메소드의 첫 번째 인수에
this [2]를 명시적으로 넣어 주어야 한다.
|
6.2. 이름 있는 클래스(Named Classes) 정의하기: gen-class
proxy
는 컴파일 타임에 컴파일한 후, 그 결과를 클래스 파일(*.class)에 저장하지는
않는다. 반면에 gen-class
는 컴파일 타임에 클래스 파일을 디스크 상에 직접 생성해
준다[3]. 따라서 클로저로 작성된 함수를 자바 언어에서 직접 호출해 사용할 필요가
있을 때 유용하다. 예를 들어, 핵심 로직은 클로저로 작성하고 클래스 파일의 형태로 컴파일한
후 자바 프로그래머에게 건네주면, 그 자바 프로그래머는 클로저에 대한 지식이 없어도 원하는
작업을 수행할 수 있게 된다.
gen-class
로 할 수 있는 일은 proxy
가 할 수 있는 일에 더해 다음과 같은 일을 할 수 있다.
-
상위 클래스의 protected 필드에 접근할 수 있다.
-
여러 개의 생성자를 정의할 수 있다.
-
상위 클래스에는 없는 새로운 정적(static) 메소드와 인스턴스 메소드를 정의할 수 있다.
-
static main 메소드를 정의할 수 있다.
하지만 gen-class
는 가교 역할만을 담당하고 실제 일은 클로저 함수가 하게 된다는 점에서,
일반적인 자바 클래스와는 차이가 있다. 그리고 반드시 AOT(Ahead of Time) 컴파일을 수행한 후,
해당 클래스를 import해야만 그 효력이 발생한다는 점도 기억하자.
(gen-class :name class-name :prefix prefix :extends super-class :implements [interface+] :constructors {[param-type*] [super-param-type*] ...} :state state-name :init init-name :methods [[method-name [method-argument-type*] return-type]+] :main boolean ...) class-name := 생성할 클래스명을 지정해 준다. prefix := 메소드명 앞에 붙일 문자열 또는 심볼을 지정한다. default는 "-"이다. super-class := 상속할 상위 클래스를 지정한다. 지정하지 않으면 java.lang.Object를 상속한다. interface := 자바의 인터페이스 또는 클로저의 프로토콜 param-type := 이 클래스의 생성자 인수들의 타입을 지정한다. super-param-type := 상속할 상위 클래스의 생성자 인수들의 타입을 지정한다. state-name := public final 속성을 가진 멤버 변수 한 개만 지정 가능하다. 여기에 atom을 지정하면 해당 값을 변경할 수 있다. init-name := [[superclass-constructor-argument*] state] constructor가 호출될 때 이 함수를 호출한다. :methods := [[method-name [arguemnt-type*] return-type]+] 새로 추가하고자 하는 메소드를 선언한다. :main := true이면 static public main 함수가 생성된다.
6.2.1. gen-class
이용 절차
먼저 간단한 예제로 시작해 보자.
-
다음 예에서는
:extends
를 지정하지 않았으므로java.lang.Object
클래스를 상속한다. 그리고:prefix
가 지정되지 않았으므로, 메소드 이름에는 default prefix인 "-"이 붙어야 한다.java-interop.example1.clj(ns java-interop.example1) (gen-class :name java_interop.example1.MyClass) (defn -toString [this] "Hello, World!")
-
project.clj 파일의
:aot
옵션에gen-class
가 정의되어 있는 이름공간을 다음과 같이 추가해 준다.project.clj(defproject java-interop "0.1.0-SNAPSHOT" :dependencies [[org.clojure/clojure "1.7.0"]] :aot [java-interop.example1])
-
프로젝트의 루트 디렉토리에서 다음과 같이 컴파일해 준다.
lein compile 실행$ lein compile Compiling java-interop.example1
-
그러면 target/classes/java_interop/example1 디렉토리 아래에 MyClass.class로 컴파일되어 있는 것을 확인할 수 있다.
-
다음은 이 클래스 파일을 다른 이름공간에서 읽어들여 실행한 결과이다. 여기에서는 클로저에서 읽어 들여 실행했지만, 실제로는 자바 언어로 읽어 들이게 될 것이다.
java-interop.test1.clj(ns java-interop.test1 (:import java_interop.example1.MyClass)) (.toString (MyClass.)) ;=> "Hello, World!"
6.2.2. :prefix
옵션 지정하기
:prefix
옵션에 다음과 같이 자신이 원하는 prefix를 지정할 수도 있다. 그리고 하나의 이름
공간에 gen-class
를 여러 번 정의할 수도 있다.
(ns java-interop.example2)
(gen-class
:name java_interop.example1.MyClassA
:prefix classA-)
(gen-class
:name java_interop.example1.MyClassB
:prefix classB-)
(defn classA-toString
[this]
"I'm an A.")
(ns java-interop.test2
(:import (java_interop.example1 MyClassA MyClassB)))
(.toString (MyClassA.)) ;=> "I'm an A."
(.toString (MyClassB.)) ;=> "I'm an B."
6.2.3. 인터페이스 구현하기
gen-class
는 ns
안에서도 정의할 수 있다. 이 때 :name
옵션을 지정해 주지 않으면, ns
이름공간이 곧 클래스명이 된다.
(ns java-interop.Example3
(:gen-class
:implements [clojure.lang.IDeref]))
(defn -deref
[this]
"Hello, World!")
(ns java-interop.test3
(:import (java_interop.Example3)))
@(Example3.) ;=> "Hello, World!"
6.2.4. 클래스 확장하기
:extends
옵션에 확장하고자 하는 상위 클래스를 지정할 수 있다. 앞에서 정의한 Example3
클래스를 그대로 상속해 보자.
(ns java-interop.Example4
(:gen-class
:extends java_interop.Example3))
(ns java-interop.test4
(:import java_interop.Example4))
@(Example4.) ;=> "Hello, World!"
6.2.5. :state
옵션 지정하기
:constructors
옵션을 지정하게 되면 :init
옵션과 :state
옵션도 함께 지정해 주어야
한다. 이때 :state
옵션에는 public final 속성을 가진 멤버 변수 한 개만 지정 가능하지만,
여기에 atom을 지정하면 해당 값을 변경할 수도 있다.
(ns java-interop.Example5
(:gen-class
:implements [clojure.lang.IDeref]
:state state
:init init
:constructors {[String] []}))
(defn -init
[message]
[[] (atom message)]) ; (1)
(defn -deref
[this]
@(.state this))
1 | -init 함수 안에서 :state 옵션에 지정된 state 필드를 (atom message) 으로
초기화하고 있다. 따라서 이 필드의 값을 다음과 같이 변경할 수 있게 된다. |
(ns java-interop.test5
(:import java_interop.Example5))
(def o (Example5. "Hello, there!"))
@o ;=> "Hallo, there!"
(reset! (.state o) "Good morning, everybody!")
@o ;=> "Good morning, everybody!"
6.2.6. 새로운 메소드 추가하기
gen-class
는 상위 클래스에는 없는 정적 메소드나 인스턴스 메소드를 추가할 수 있다.
(ns java-interop.Example6
(:gen-class
:methods [^:static [greet [] String] ; (1)
[greetMessage [String] String]]))
(defn -greet
[]
"Hello, World!")
(defn -greetMessage
[this msg]
msg)
1 | metadata ^:static 를 추가하면 정적 메소드가 된다. |
(ns java-interop.test6
(:import java_interop.Example6))
(Example6/greet) ;=> "Hello, World!"
(.greetMessage (Example6.) "Good night!") ;=> "Good night!"