Return-Path: Received: from fort-point-station.mit.edu by po10.mit.edu (8.9.2/4.7) id XAA09901; Tue, 12 Jun 2001 23:15:32 -0400 (EDT) Received: from hermes.java.sun.com (hermes.java.sun.com [204.160.241.85]) by fort-point-station.mit.edu (8.9.2/8.9.2) with SMTP id XAA06327 for ; Tue, 12 Jun 2001 23:15:32 -0400 (EDT) Message-Id: <200106130315.XAA06327@fort-point-station.mit.edu> Date: Tue, 12 Jun 2001 20:15:32 PDT From: "JDC Tech Tips" To: alexp@mit.edu Subject: JDC Tech Tips June 12, 2001 Precedence: junk Mime-Version: 1.0 Content-Type: text/plain; charset=us-ascii Content-Transfer-Encoding: 7bit X-Mailer: Beyond Email 2.2 J D C T E C H T I P S TIPS, TECHNIQUES, AND SAMPLE CODE WELCOME to the Java Developer Connection(sm) (JDC) Tech Tips, June 12, 2001. This issue covers: * Abstract Classes * Using Peer Classes With the Java(tm) Native Interface These tips were developed using Java 2 SDK, Standard Edition, v 1.3. You can view this issue of the Tech Tips on the Web at http://java.sun.com/jdc/JDCTechTips/2001/tt0612.html - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ABSTRACT CLASSES If you've done much work in the Java programming language, you've probably seen programs that use the "abstract" keyword to declare classes and methods. What is an abstract class, and when would you want to use one? This tip discusses some of the basics of these classes, and then presents a couple of examples. The key idea with an abstract class is that it's useful when (a) there is common functionality that you'd like to implement in a superclass, and (b) some behavior is unique to specific classes and cannot be factored into the superclass. So you implement the superclass as an abstract class, and define methods that subclasses have in common. Then you implement each subclass by extending the abstract class, and add in the methods unique to that class. The abstract class provides a "contract" of sorts that specifies behavior that must be implemented in a subclass. Let's consider an example of an abstract class: abstract class AbstractClass1 { protected AbstractClass1() { System.out.println( "AbstractClass1 constructor called"); } public abstract void distinct_method(); public void common_method() { System.out.println("common_method called"); distinct_method(); } }; class ConcreteClass extends AbstractClass1 { public void distinct_method() { System.out.println("distinct_method called"); } } public class AbstractDemo1 { public static void main(String args[]) { AbstractClass1 ref; //ref = new AbstractClass1(); ref = new ConcreteClass(); ref.common_method(); } } AbstractClass1 is an abstract class, with a constructor and two methods. One of the methods, common_method, is defined in the class, and represents shared functionality applicable to subclasses of AbstractClass1. The other method, distinct_method, is not defined in the class, and must be implemented in a subclass. Note that common_method calls distinct_method. It's not possible to create a new instance of an abstract class. If you uncomment "new AbstractClass1()" above, you get a compile error. Since some of the methods in the abstract class are not defined, there would be no implementation to rely on if you had an object of such a class. The same consideration applies if you try to use Class.newInstance to create a new instance of an abstract class represented by a Class object. You can, however, use the abstract class type to hold a reference to an object of the subclass type, similar to the way you use an interface type. When you define an abstract class, you need to use "abstract" for the class declaration and each abstract method. Also, notice that in the AbstractDemo1 example, the constructor in the abstract class is explicit. Otherwise, the usual rules would apply about implicit and generated constructors up through the class hierarchy. Here's another example of an abstract class: abstract class AbstractClass2 { public abstract void runtest(); public void driver() { System.out.println("driver called"); runtest(); } } public class AbstractDemo2 extends AbstractClass2 { public void runtest() { System.out.println("runtest called"); } public static void main(String args[]) { new AbstractDemo2().driver(); } } This example shows how you can use abstract classes to build a software testing framework. You have an abstract class with a driver method that handles issues such as test case timing and error reporting. The user of your class extends it to actually provide the method that runs the test. Let's look at one final example that's a little more involved. Suppose you're working on an application where you need to represent geometric rectangles, and you're using legacy data with several different data representations. One approach to representing a rectangle uses the X,Y origin plus the width and height. Another approach uses two X,Y pairs for the origin and the maximum value at the opposite corner of the rectangle. You want to to have a common interface to these various representations. How can you do that? One approach to this problem is similar to that taken by the Java collection classes such as List and ArrayList. The approach is to define an interface, define an abstract class that implements the interface and has common functionality, and then code (all methods implemented) subclasses of the abstract class that define representation-specific behavior. The code looks like this: // Rect interface, describing methods that must be // implemented interface Rect { int getXLo(); int getXHi(); int getYLo(); int getYHi(); int getWidth(); int getHeight(); boolean contains(int x, int y); String toString(); }; // abstract class that implements common methods and // leaves other methods to be implemented in subclasses abstract class AbstractRect implements Rect { // these methods are implemented in subclasses abstract public int getXLo(); abstract public int getYLo(); abstract public int getXHi(); abstract public int getYHi(); // get the width of the rectangle public int getWidth() { return getXHi() - getXLo(); } // get the height of the rectangle public int getHeight() { return getYHi() - getYLo(); } // see if rectangle contains a particular point public boolean contains(int x, int y) { return x >= getXLo() && x <= getXHi() && y >= getYLo() && y <= getYHi(); } // convert to [xlo,ylo,xhi,yhi] public String toString() { StringBuffer sb = new StringBuffer(); sb.append("["); sb.append(getXLo()); sb.append(","); sb.append(getYLo()); sb.append(","); sb.append(getXHi()); sb.append(","); sb.append(getYHi()); sb.append("]"); return sb.toString(); } } // implementation of AbstractRect that represents // a rectangle as an upper left origin plus width/height class RectWidthHeight extends AbstractRect { private int x; private int y; private int w; private int h; public RectWidthHeight(int x, int y, int w, int h) { this.x = x; this.y = y; this.w = w; this.h = h; } public int getXLo() { return x; } public int getYLo() { return y; } public int getXHi() { return x + w; } public int getYHi() { return y + h; } } // implementation of AbstractRect that represents // a rectangle as low and high X,Y points class RectTwoPoints extends AbstractRect { private int xlo; private int ylo; private int xhi; private int yhi; public RectTwoPoints(int xlo, int ylo, int xhi, int yhi) { this.xlo = xlo; this.ylo = ylo; this.xhi = xhi; this.yhi = yhi; } public int getXLo() { return xlo; } public int getYLo() { return ylo; } public int getXHi() { return xhi; } public int getYHi() { return yhi; } } // driver public class AbstractDemo3 { public static void main(String args[]) { Rect r1 = new RectWidthHeight(10, 10, 10, 15); System.out.println(r1); System.out.println(r1.getWidth() + " " + r1.getHeight()); System.out.println(r1.contains(10, 10)); System.out.println(r1.contains(20, 25)); System.out.println(r1.contains(9, 9)); System.out.println(r1.contains(21, 26)); Rect r2 = new RectTwoPoints(10, 10, 20, 25); System.out.println(r2); System.out.println(r2.getWidth() + " " + r2.getHeight()); System.out.println(r2.contains(10, 10)); System.out.println(r2.contains(20, 25)); System.out.println(r2.contains(9, 9)); System.out.println(r2.contains(21, 26)); } } The abstract class implements shared functionality. Subclasses implement the four methods, such as getXLo, that are used to get the low and high X,Y values for the corners of the rectangle. Using these four methods, it's possible to implement common methods such as getWidth and toString in the abstract class. When you run this program, the output is: [10,10,20,25] 10 15 true true false false [10,10,20,25] 10 15 true true false false In the driver program, the Rect interface type is used to hold references to the concrete subclasses. One reason why you want to do this is that additional representation classes might be added at a later time. These additional classes might extend the AbstractRect class, as already done above, or implement the Rect interface directly -- and not use the abstract class at all. In either case, programming with interface types makes it easy to change and mix representation classes. Both abstract classes and interfaces allow you to specify a "contract" as a type. In other words, you can require that a subclass of an abstract class, or the class that implements an interface, define particular methods. So there is some overlap between these two mechanisms. Interfaces allow for a form of multiple inheritance, because you can implement more than one interface when you define a class. Interfaces allow you to specify only public methods and constants, with no implementation allowed within the interface. By comparison, abstract classes do not provide multiple inheritance, but they do allow you to build up a partial implementation of a class (as illustrated above). Also, you have more flexibility than interfaces in areas such as protected members. Using abstract classes and interfaces together often makes sense. For example, suppose that you're extending a class, and you also want to use the AbstractRect class defined above. You can't extend from two classes at one time (that is, multiple inheritance). However, in such a case you can implement the Rect interface, possibly by forwarding the methods of the interface to a concrete subclass of AbstractRect. Or you might want to reimplement the interface completely, perhaps for correctness or performance reasons. For more information about Abstract classes, see Section 3.7, Abstract Classes and Methods, and Section 4.6, When to Use Interfaces, in "The Java(tm) Programming Language Third Edition" by Arnold, Gosling, and Holmes http://java.sun.com/docs/books/javaprog/thirdedition/ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - USING PEER CLASSES WITH THE JAVA NATIVE INTERFACE Suppose that you're developing an application in the Java Programming Language, and you find that you need to make use of a C++ class that you previously developed. You've weighed the various pros and cons of this decision, such as complexity issues and portability, and have decided that you really need to use the C++ class. So how can you use C++ classes from a Java program, and keep C++ objects around while the program is running? This tip shows a simple example. The basic idea is that you define a Java class, called a "peer class," that corresponds to the C++ class. Each instance of the peer class corresponds to a C++ object, tracking the state of the C++ object. The peer class and driver program look like this: // PeerDemo.java class Peer { // used to hold the C++ object pointer private long peerobj; // create native object private native long create(int i); // destroy native object private native synchronized void destroy(long p); // get value of native object private native int getvalue(long p); // constructor - call create() to create native object public Peer(int i) { peerobj = create(i); System.out.println("create peerobj = " + peerobj); } // destroy native object if not already done public synchronized void destroy() { if (peerobj != 0) { System.out.println("destroy peerobj = " + peerobj); destroy(peerobj); peerobj = 0; } } // get value from native object public int getValue() { if (peerobj == 0) { throw new IllegalStateException( "getValue called on destroyed object"); } return getvalue(peerobj); } // destroy native object if it's still around // when finalize called from garbage collection public void finalize() { destroy(); } } public class PeerDemo { // load the native library static { System.loadLibrary("PeerLib"); } // driver public static void main(String args[]) { Peer p1 = new Peer(37); Peer p2 = new Peer(47); System.out.println("p1 value = " + p1.getValue()); System.out.println("p2 value = " + p2.getValue()); p1.destroy(); p2.destroy(); p1.destroy(); } } Before explaining this code, let's examine the C++ class. This is a simple class that saves with each created object an integer value passed to the constructor. This allows the value to be retrieved at a later point from the object. There's also a destructor for the class that is invoked when objects of the class go out of scope, or are explicitly deleted (remember that C++ has no garbage collection). The class is defined in a single header file, with inline functions: // PeerClass.h #ifndef _PEERCLASS_ #define _PEERCLASS_ #include using namespace std; // C++ class that stores an integer value in an object class PeerClass { int val; public: // constructor PeerClass(int i) { val = i; cout << "PeerClass::PeerClass called" << endl; } // destructor ~PeerClass() { cout << "PeerClass::~PeerClass called" << endl; } // return value stored in object int getValue() { cout << "PeerClass::getValue called" << endl; return val; } }; #endif Now let's explain the peer class and driver program, PeerDemo.java. Notice that there are three native methods in the program, create, destroy, and getvalue, that correspond to the constructor, destructor, and getValue functions in the C++ class. These native methods are called within the peer class, and are the link between the peer class and the C++ class. For example, the constructor in the peer class calls the native method create. The constructor passes as an argument the integer value to be stored in the C++ object. In a moment you'll learn how to actually connect the native methods with the C++ class functions. Suppose that an instance of a C++ class is created. How is the pointer (reference) to the instance saved so that it can be manipulated through the peer class? This is done by defining a long (64-bit) field in the peer class; the field is used to save the pointer. In other words, the create native method returns this value, and the value is saved in objects of the peer class. When other native methods, such as destroy, are called, this value is retrieved and passed as an argument to the method. The value is then cast into a C++ pointer value. Another important point about peer classes concerns finalization and destroying objects. As stated earlier, C++ has no garbage collection, so it's necessary to worry about how objects are reclaimed when they're no longer in use. When objects are dynamically created with the new operator, you must explicitly call delete. In the peer class, this behavior is modeled using the destroy method. PeerDemo also defines a finalize method that calls destroy. But you can't rely on destroy being called in a timely way. So you must explicitly call destroy to invoke the destructor for the C++ class, and avoid memory leaks. Instances of the Java peer class are garbage collected, but not instances of the C++ class. The destroy method is synchronized to avoid race conditions. The create method is called from the constructor, which can execute in only one thread for a given object. Also the getvalue method is read-only, so there are no issues with synchronization for these two methods. Given all of this discussion, how do you actually turn the code above into a running application? The first step is to compile the Java code: javac PeerDemo.java Then run the "javah" tool that is part of the JDK distribution: javah -jni -classpath . -o PeerLib.h Peer This step creates a header file that declares C++ function prototypes for the native methods. The created header file looks like this: // PeerLib.h /* DO NOT EDIT THIS FILE - it is machine generated */ #include /* Header for class Peer */ #ifndef _Included_Peer #define _Included_Peer #ifdef __cplusplus extern "C" { #endif /* * Class: Peer * Method: create * Signature: (I)J */ JNIEXPORT jlong JNICALL Java_Peer_create (JNIEnv *, jobject, jint); /* * Class: Peer * Method: destroy * Signature: (J)V */ JNIEXPORT void JNICALL Java_Peer_destroy (JNIEnv *, jobject, jlong); /* * Class: Peer * Method: getvalue * Signature: (J)I */ JNIEXPORT jint JNICALL Java_Peer_getvalue (JNIEnv *, jobject, jlong); #ifdef __cplusplus } #endif #endif Note that 'extern "C"' is used to declare the function prototypes. This means that the C++ functions will have their external names encoded using C rules, instead of C++ ones. C++ name mangling gets complicated, for example, to represent overloaded function types, and it's not at all portable, so C-style names are used. Given the header file, PeerLib.h, you can define PeerLib.cpp, the C++ implementation of the native functions: // PeerLib.cpp #include "PeerLib.h" #include "PeerClass.h" // create an object by calling constructor of C++ class JNIEXPORT jlong JNICALL Java_Peer_create(JNIEnv *, jobject, jint i) { PeerClass* ptr = new PeerClass(i); return (jlong)ptr; } // destroy an object by calling destructor of C++ class JNIEXPORT void JNICALL Java_Peer_destroy(JNIEnv *, jobject, jlong objptr) { PeerClass* ptr = (PeerClass*)objptr; delete ptr; } // get the value of an object JNIEXPORT jint JNICALL Java_Peer_getvalue(JNIEnv *, jobject, jlong objptr) { PeerClass* ptr = (PeerClass*)objptr; return ptr->getValue(); } If you study this code, you will see how the native methods are implemented. For example, the destroy function has passed to it a parameter called "objptr" which is the 64-bit value of the "peerobj" field in the peer class. This value is cast to type "PeerClass*". Then the delete operator is called on the pointer. The next step is to compile PeerLib.cpp, and create a shared library or DLL from it. The exact steps to do this will vary, but here's an example using Borland C++ on Win32: bcc32 -c -I/jdkbase/include -I/jdkbase/include/win32 PeerLib.cpp bcc32 -tWD PeerLib.obj In the Solaris(tm) Operating Environment, using the Sun WorkShop(tm) Compiler C++ 5.0, CC -G -o libPeerLib.so -Ijdkbase/include \ -Ijdkbase/include/solaris PeerLib.cpp -lCstd -lCrun Or using the GNU C++ Compiler: g++ -G -o libPeerLib.so -Ijdkbase/include \ -Ijdkbase/include/solaris PeerLib.cpp "/jdkbase" should be replaced with the path of your JDK distribution, and "win32" replaced as appropriate. The result of this step is a shared library with a name like "PeerLib.dll" or "PeerLib.so". Note that for the Solaris Operating Environment, you need to set the environment variable LD_LIBRARY_PATH to the directory containing libPeerLib.so. Otherwise you might get an UnsatisfiedLinkError exception. Once you've created the shared library, you invoke the demo program by saying: java PeerDemo Typical output will look like this: PeerClass::PeerClass called create peerobj = 149302092 PeerClass::PeerClass called create peerobj = 149302136 PeerClass::getValue called p1 value = 37 PeerClass::getValue called p2 value = 47 destroy peerobj = 149302092 PeerClass::~PeerClass called destroy peerobj = 149302136 PeerClass::~PeerClass called The big numbers are 64-bit values stored in the "peerobj" field in objects of the Java peer class, and represent C++ pointers. The Java Native Interface has other mechanisms you can use to combine Java and C/C++ code, for example, if you're trying to use C functions from an existing DLL. For more information about using peer classes with JNI, see Section 9.5, Peer Classes, in "The Java(tm) Native Interface" by Sheng Liang http://java.sun.com/docs/books/jni/index.html . . . . . . . . . . . . . . . . . . . . . . . - NOTE Sun respects your online time and privacy. The Java Developer Connection mailing lists are used for internal Sun Microsystems(tm) purposes only. You have received this email because you elected to subscribe. To unsubscribe, go to the Subscriptions page http://developer.java.sun.com/subscription/ uncheck the appropriate checkbox, and click the Update button. - SUBSCRIBE To subscribe to a JDC newsletter mailing list, go to the Subscriptions page http://developer.java.sun.com/subscription/ choose the newsletters you want to subscribe to, and click Update. - FEEDBACK Comments? Send your feedback on the JDC Tech Tips to: jdc-webmaster@sun.com - ARCHIVES You'll find the JDC Tech Tips archives at: http://java.sun.com/jdc/TechTips/index.html - COPYRIGHT Copyright 2001 Sun Microsystems, Inc. All rights reserved. 901 San Antonio Road, Palo Alto, California 94303 USA. This document is protected by copyright. For more information, see: http://java.sun.com/jdc/copyright.html - LINKS TO NON-SUN SITES The JDC Tech Tips may provide, or third parties may provide, links to other Internet sites or resources. Because Sun has no control over such sites and resources, You acknowledge and agree that Sun is not responsible for the availability of such external sites or resources, and does not endorse and is not responsible or liable for any Content, advertising, products, or other materials on or available from such sites or resources. Sun will not be responsible or liable, directly or indirectly, for any damage or loss caused or alleged to be caused by or in connection with use of or reliance on any such Content, goods or services available on or through any such site or resource. This issue of the JDC Tech Tips is written by Glen McCluskey. JDC Tech Tips June 12, 2001 Sun, Sun Microsystems, Sun Workshop, Java, Java Developer Connection, and Solaris are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. To use our one-click unsubscribe facility, select the following URL: http://hermes.java.sun.com/unsubscribe?-5521432384639049912