|
Tech Tips Archive
June 12, 2001
WELCOME to the Java Developer Connection (JDC) Tech Tips, June 12, 2001. This issue covers:
These tips were developed using Java 2 SDK, Standard Edition,
v 1.3.
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 Programming Language Third Edition" by Arnold, Gosling, and Holmes.
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 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 synchronized 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 getValue and destroy methods are synchronized
to avoid race conditions. The create method is called from the constructor,
which can execute in only one thread for a given object.
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 Operating Environment, using the Sun WorkShop 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 Native Interface" by Sheng Liang.
- NOTE
Sun respects your online time and privacy. The Java Developer Connection mailing lists are used for internal Sun Microsystems purposes only. You have received this email because you elected to subscribe. To unsubscribe, go to the Subscriptions page, uncheck the appropriate checkbox, and click the Update button.
- SUBSCRIBE
To subscribe to a JDC newsletter mailing list, go to the Subscriptions page, 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.
|