2
votes

Suppose there is a Java class that doesn't provide getters and setters for all its fields, and I have to extend it with :gen-class and fiddle with them.

How do I access the superclass fields?

The quickest (and perhaps cleanest...) solution that comes to my mind right now is to create a java class that extends my super class, and extend it instead, but I was wondering if there is an alternative that sounds more direct.

Thanks!

2
Are those fields public, protected, package private, or private?ez121sl
Assume "public" for simplicityRick77

2 Answers

3
votes

The methods in generated classes can access base class fields with the help of the :exposes option of gen-class. :exposes expects a map where keys are symbols matching base class field names; values are also maps like {:get getterName, :set setterName}. Clojure generates those getter and setter methods automatically. They can be used to read and modify base class fields. This is documented in the docstring for gen-class.

This approach works for public and protected fields. It does not work for private fields.

Assuming the Java base class like this:

package fields;

class Base {
    public String baseField = "base";
}

The Clojure code to generate a subclass would be:

(ns fields.core
  (:gen-class
   :extends fields.Base
   :methods [[bar [] String]
             [baz [String] Object]]
   :exposes { baseField { :get getField :set setField }}))

(defn -bar [this]
  (str (.getField this) "-sub"))

(defn -baz [this val]
  (.setField this val)
  this)

(defn -main
  [& args]
  (println (.. (fields.core.) (bar)))
  (println (.. (fields.core.) (baz "new-base") (bar))))

Assuming all this is AOT compiled and ran, the output is:

base-sub
new-base-sub
1
votes

I was having a bit of trouble understanding all of the details and decided to try out a minimal version. Here is a file listing:

> d **/*.{clj,java}
-rw-rw-r-- 1 alan alan 501 Jun 29 17:11 project.clj
-rw-rw-r-- 1 alan alan 431 Jun 29 17:10 src/demo/core.clj
-rw-rw-r-- 1 alan alan  63 Jun 29 16:57 src-java/jpak/Base.java

Here is the project.clj

(defproject demo "0.1.0-SNAPSHOT"
  :description "demo code"
  :license {:name "Eclipse Public License"
            :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [
    [org.clojure/clojure "1.8.0"]
    [tupelo "0.9.55"]
  ]
  :profiles {:dev {:dependencies [ [org.clojure/test.check "0.9.0"] ] }
             :uberjar {:aot :all} }
  :java-source-paths ["src-java"]
  :aot [ demo.core ]
  :main ^:skip-aot demo.core
  :target-path "target/%s"
  jvm-opts ["-Xms500m" "-Xmx500m" ]
)

and the Java class:

package jpak;
public class Base {
  public long answer = 41;
}

and our Clojure code:

(ns demo.core
  (:gen-class
   :extends jpak.Base
   :exposes {answer {:get getans :set setans}}
 ))

(defn -main
  [& args]
  (let [sub-obj    (demo.core.) ; default name of subclass
        old-answer (.getans sub-obj)
        >>         (.setans sub-obj (inc old-answer))
        new-answer (.getans sub-obj)  ]
    (println "old answer = " old-answer)
    (println "new answer = " new-answer)
  ))

We can run using lein run to get:

> lein run  
old answer =  41
new answer =  42

Version 2

The above continues to work if the Java variable answer is protected, but fails if it is either private or "package protected" (no qualifier). This makes sense since our subclass is in a different package.

Also, it is a little cleaner if I give the subclass a name different than the default value, which is the clojure namespace name "demo.core":

(ns demo.core
  (:gen-class
   :name demo.Sub
   :extends jpak.Base
   :exposes {answer {:get getans :set setans}}
 ))

(defn -main
  [& args]
  (let [sub-obj    (demo.Sub.) ; new name of subclass
        old-answer (.getans sub-obj)
        >>         (.setans sub-obj (inc old-answer))
        new-answer (.getans sub-obj)
  ]
    (println "old answer = " old-answer)
    (println "new answer = " new-answer)
  ))

Version 3: Access private member values

In Java, a subclass cannot normally see private member variables of a superclass; "package protected" members from a different package are also restricted. Here is the pesky Java class:

package jpak;
public class Base {
  private long answer = 41;
}

However, Java has a well-known ability to override private access restrictions, and you don't even need a subclass! All you need to do is use reflection. Here is the clojure version:

(ns demo.break
  (:import [jpak Base]))

(defn -main
  [& args]
  (let [base-obj   (Base.)
        class-obj  (.getClass base-obj)
        ans-field  (.getDeclaredField class-obj "answer")
        >>         (.setAccessible ans-field true)
        old-answer (.get ans-field base-obj)
        >>         (.set ans-field base-obj 42)
        new-answer (.get ans-field base-obj)
  ]
    (println "old answer = " old-answer)
    (println "new answer = " new-answer)))

> lein run -m demo.break
old answer =  41
new answer =  42

See the docs for AccessibleObject here. Note that Field & Method, which are the classes returned during reflection, are both included.