8. Using Java from Jess
8.1. Java reflection
Among the list of functions above are a set that let you create and manipulate Java objects directly from Jess. Using them, you can do virtually anything you can do from Java code, except for defining new classes. Here is an example in which I create a Java HashMap and add a few String objects to it, then lookup one object and display it.Jess> (bind ?ht (new java.util.HashMap)) <Java-Object:java.util.HashMap> Jess> (call ?ht put "key1" "element1") Jess> (call ?ht put "key2" "element2") Jess> (call ?ht get "key1") "element1"
Jess> (bind ?ht (new java.util.HashMap)) <Java-Object:java.util.HashMap> Jess> (?ht put "key1" "element1") Jess> (?ht put "key2" "element2") Jess> (?ht get "key1") "element1"
Jess> (bind ?pt (new java.awt.Point)) <Java-Object:java.awt.Point> Jess> (set-member ?pt x 37) 37 Jess> (set-member ?pt y 42) 42 Jess> (get-member ?pt x) 37
Jess> (get-member System out) <Java-Object:java.io.PrintStream>
Java type | Jess type |
---|---|
A null reference | The symbol 'nil' |
A void return value | The symbol 'nil' |
String | RU.STRING |
An array | A Jess list |
boolean or java.lang.Boolean | The symbols 'TRUE' and 'FALSE' |
byte, short, int, or their wrappers | RU.INTEGER |
long or Long | RU.LONG |
double, float or their wrappers | RU.FLOAT |
char or java.lang.Character | RU.SYMBOL |
anything else | RU.JAVA_OBJECT |
Jess type | Possible Java types |
---|---|
RU.JAVA_OBJECT | The wrapped object |
The symbol 'nil' | A null reference |
The symbols 'TRUE' or 'FALSE' | java.lang.Boolean or boolean |
RU.SYMBOL, RU.STRING | String, char, java.lang.Character |
RU.FLOAT | float, double, and their wrappers |
RU.INTEGER | long, short, int, byte, char, and their wrappers |
RU.LONG | long, short, int, byte, char, and their wrappers |
RU.LIST | A Java array |
8.2. Definstance facts
You may have noticed that unordered facts look a bit like Java objects, or specifically, like Java Beans. The similarity is that both have a list of slots (for Java Beans, they're called properties) which contains values that might change over time. Jess has a mechanism for automatically generating templates that represent specific types of Java Beans. Jess can then use these templates to store a representation of a Java Bean's properties in the working memory. The working memory representation of the Bean can be static (changing infrequently, like a snapshot of the properties at one point in time) or dynamic (changing automatically whenever the Bean's properties change.) The Jess commands that make this possible are defclass and definstance. defclass tells Jess to generate a special template to represent a category of Beans, while definstance puts a representation of one specific Bean into working memory. An example will probably help at this point. Let's say you have the following Java Bean classimport java.io.Serializable; public class ExampleBean implements Serializable { private String name = "Bob"; public String getName() { return name; } public void setName(String s) { name = s; } }
Jess> (defclass simple ExampleBean) ExampleBean Jess> (ppdeftemplate simple) "(deftemplate MAIN::simple \"$JAVA-OBJECT$ ExampleBean\" (declare (from-class ExampleBean)) (slot class) (slot name) (slot OBJECT))"
Note that if you want your Java Beans to work with Jess's bload and bsave commands, the individual classes need to implement the java.io.Serializable tag interface.Now let's say we want an actual ExampleBean in our working memory. Here we'll create one from Jess code, but it could come from anywhere. We will use the definstance function to add the object to the working memory.
Jess> (bind ?sb (new ExampleBean)) <Java-Object:ExampleBean> Jess> (definstance simple ?sb static) <Fact-0> Jess> (facts) f-0 (MAIN::simple (class <Java-Object:java.lang.Class>) (name "Bob") (OBJECT <Java-Object:ExampleBean>)) For a total of 1 facts in module MAIN.
Jess> (call ?sb setName "Fred") Jess> (facts) f-0 (MAIN::simple (class <Java-Object:java.lang.Class>) (name "Bob") (OBJECT <Java-Object:ExampleBean>)) For a total of 1 facts in module MAIN.
Jess> (reset) TRUE Jess> (facts) f-0 (MAIN::initial-fact) f-1 (MAIN::simple (class <Java-Object:java.lang.Class>) (name "Fred") (OBJECT <Java-Object:ExampleBean>)) For a total of 2 facts in module MAIN.
If your objects will be changed often from outside of Jess, then it would be nice to have a better way to do this. If you want to have your definstance facts stay continuously up to date, Jess needs to be notified whenever a Bean property changes. For this to happen, the Bean has to support the use of java.beans.PropertyChangeListeners. For Beans that fulfill this requirement Jess will automatically arrange for working memory to be updated every time a property of the Bean changes. A simple JavaBean that correctly implements this interface looks like this:
import java.io.Serializable; import java.beans.*; public class ExampleAutomaticBean implements Serializable { private PropertyChangeSupport pcs = new PropertyChangeSupport(this); private String name = "Bob"; public String getName() { return name; } public void setName(String s) { String temp = name; name = s; pcs.firePropertyChange("name", temp, s); } public void addPropertyChangeListener(PropertyChangeListener pcl) { pcs.addPropertyChangeListener(pcl); } public void removePropertyChangeListener(PropertyChangeListener pcl) { pcs.removePropertyChangeListener(pcl); } }
You can also actively tell Jess to refresh its knowledge of the properties of individual objects using the jess.Rete.updateObject() method. This is useful if you want to update objects manually or if you want to use your own automated change mechanism instead of PropertyChangeEvents.
defclasses, like deftemplates, can extend one another. In fact, deftemplates can extend defclasses, and defclasses can extend deftemplates. Of course, for a defclass to extend a deftemplate, the corresponding Bean class must have property names that match the template's slot names. Note, also, that just because two Java classes have an inheritance relationship doesn't mean that if both are defclassed the two defclasses will. You must explicitly declare all such relationships using extends. See the full documenation for defclass for details.Instead of using the defclass function to create templates from Java classes, you can accomplish the same thing using the special from-class declaration in a deftemplate construct. This provides a different syntax for defining defclasses, and more importantly, lets you apply "slot-specific" to defclasses. The syntax looks like:
(deftemplate simple (declare (from-class ExampleBean) (slot-specific TRUE)))
This is equivalent to calling (defclass simple ExampleBean) , plus it adds the slot-specific property.
One final note about Java Beans used with Jess: Beans are often operating in a multithreaded environment, and so it's important to protect their data with synchronized blocks or synchronized methods. However, sending PropertyChangeEvents while holding a lock on the Bean itself can be dangerous, as the Java Beans Specification points out:
"In order to reduce the risk of deadlocks, we strongly recommend that event sources should avoid holding their own internal locks when they call event listener methods. Specifically, as in the example code in Section 6.5.1, we recommend they should avoid using a synchronized method to fire an event and should instead merely use a synchronized block to locate the target listeners and then call the event listeners from unsynchronized code." -- JavaBean Specification, v 1.0.1, p.31.Failing to heed this advice can indeed cause deadlocks in Jess.
8.3. Transferring values between Jess and Java code
This section describes a very easy-to-use mechanism for communicating inputs and results between Jess and Java code. These methods are available in the class jess.Rete:public Value store(String name, Value val); public Value store(String name, Object val); public Value fetch(String name); public void clearStorage();
(store <name> <value>) (fetch <name>) (clear-storage)
import jess.*; public class ExFetch { public static void main(String[] unused) throws JessException { Rete r = new Rete(); r.store("DIMENSION", new java.awt.Dimension(10, 10)); r.eval("(bind ?list (list dimension (fetch DIMENSION)))"); r.eval("(printout t ?list)"); } } C:\> java ExFetch (dimension <Java-Object:java.awt.Dimension>)
8.4. Implementing Java interfaces with Jess
Many Java libraries expect you to use callbacks. A callback is an object that implements a specific interface; you pass it to a library method, and the methods of the callback are invoked at some expected time. GUIs work this way -- the callbacks are called event handlers. But Java Threads work this way too -- a Runnable is a callback that] gets invoked on a new thread. So being able to implement an interface is an important part of programming in Java.
Jess supports creating callbacks with the implement function. This function is simple to use: you tell it the name of an interface, and the name of a deffunction, and it returns an object that implements that interface by calling that function. When any method of that interface is invoked, the deffunction will be called. The first argument will be the name of the interface function, and all the arguments of the interface function will follow.
As an example, here is an implementation of java.util.Comparator which sorts Strings in case-insensitive order:
Jess> (import java.util.Comparator) Jess> (deffunction compare(?name ?s1 ?s2) (return ((?s1 toUpperCase) compareTo (?s2 toUpperCase)))) TRUE Jess> (bind ?c (implement Comparator using compare))
8.4.1. Lambda expressions
There's a nice shortcut you can use when calling implement. Instead of defining a separate deffunction, you can define one in-line using the lambda facility. The lambda function lets you define a deffunction without naming it, and without adding it to a Rete engine. We can redo the example above using lambda like this:
Jess> (import java.util.Comparator) Jess> (bind ?c (implement Comparator using (lambda (?name ?s1 ?s2) (return ((?s1 toUpperCase) compareTo (?s2 toUpperCase))))))
8.5. Java Objects in working memory
You can let Jess pattern-match on Java objects using definstance. You can also easily put Java objects into the slots of other Jess facts, as described elsewhere in this document. This section describes some minimal requirements for objects used in either of these ways. Jess may call the equals and hashCode methods of any objects in working memory. As such, it is very important that these methods be implemented properly. The Java API documentation lists some important properties of equals and hashCode, but I will reiterate the most important (and most often overlooked) one here: if you write equals, you probably must write hashCode too. For any pair of instances of a class for which equals returns true, hashCode must return the same value for both instances. If this rule is not observed, Jess will appear to malfunction when processing facts containing these improperly defined objects in their slots. In particular, rules that should fire may not do so.A value object is an instance of a class that represents a specific value. They are often immutable like Integer, Double, and String. For Jess's purposes, a value object is one whose hashCode() method returns a constant -- i.e., whose hash code doesn't change during normal operation of the class. Integer, Double, and all the other wrapper classes qualify, as does String, and generally all immutable classes. Any class that doesn't override the default hashCode() method also qualifies as a value object by the definition. Java's Collection classes (Lists, Maps, Sets, etc.) are classic examples of classes that are not value objects, because their hash codes depend on the collection's contents.
As far as Jess is concerned, an object is a value object as long as its hash code won't change while the object is in working memory. This includes the case where the object is contained in a slot of any fact. If the hash code will only change during calls to modify, then the object is still a value object.
Jess can make certain assumptions about value objects that lead to large performance increases during pattern matching. Because many classes are actually value classes by Jess's broad definition, Jess now assumes that all objects (except for Collections) are value objects by default. If you're working with a class that is not a value class, it's very important that you tell Jess about it by using the set-nonvalue-class function or the static method jess.HashCodeComputer.setNonValueClass(). Failure to do so will lead to undefined (bad) behavior.
8.6. Setting and Reading Java Bean Properties
As mentioned previously, Java objects can be explicitly pattern-matched on the LHS of rules, but only to the extent that they are Java Beans. A Java Bean is really just a Java object that has a number of methods that obey a simple naming convention for Java Bean properties. A class has a Bean property if, for some string X and type T it has either or both of:- A method named getX which returns T and accepts no arguments; or, if T is boolean, named isX which accepts no arguments;
- A method named setX which returns void and accepts a single argument of type T.
Jess> (defglobal ?*frame* = (new java.awt.Frame "Frame Demo")) TRUE Jess> ;; Directly call 'isVisible', or... (printout t (call ?*frame* isVisible) crlf) FALSE Jess> ;; ... equivalently, query the Bean property (printout t (get ?*frame* visible) crlf) FALSE