Return-Path: Received: from po10.mit.edu (po10.mit.edu [18.7.21.66]) by po10.mit.edu (Cyrus v2.1.5) with LMTP; Tue, 08 Apr 2003 14:07:41 -0400 X-Sieve: CMU Sieve 2.2 Received: from fort-point-station.mit.edu by po10.mit.edu (8.12.4/4.7) id h38I7ZKB015911; Tue, 8 Apr 2003 14:07:35 -0400 (EDT) Received: from hermes.sun.com (hermes.sun.com [64.124.140.169]) by fort-point-station.mit.edu (8.12.4/8.9.2) with SMTP id h38I28Ve016339 for ; Tue, 8 Apr 2003 14:02:09 -0400 (EDT) Date: 8 Apr 2003 09:15:43 -0800 From: "JDC Tech Tips" To: alexp@mit.edu Message-Id: <33208391174049728@hermes.sun.com> Subject: Core Java Technologies Tech Tips, April 8, 2003 (Destroying Objects, Preprocessing) Mime-Version: 1.0 Content-Type: text/html; charset=ISO-8859-1 Content-Transfer-Encoding: 7bit X-Mailer: SunMail 1.0 X-Spam-Score: 2.3 X-Spam-Level: ** (2.3) X-Spam-Flag: NO X-Scanned-By: MIMEDefang 2.28 (www . roaringpenguin . com / mimedefang) Core Java Technologies Technical Tips
.
.
Core Java Technologies Technical Tips
.
   View this issue as simple text April 8, 2003    

In this Issue

Welcome to the Core Java Technologies Tech Tips, April 8, 2003. Here you'll get tips on using core Java technologies and APIs, such as those in Java 2 Platform, Standard Edition (J2SE).

This issue covers:

.Destroying Objects
.Preprocessing and the Java Language

These tips were developed using Java 2 SDK, Standard Edition, v 1.4.

This issue of the Core Java Technologies Tech Tips is written by Glen McCluskey.

.
.

DESTROYING OBJECTS

In Java programming, you create class objects and arrays using the new operator. Such creation involves allocation of space, followed by a constructor call (for classes) to initialize the object. This tip examines what happens at the other end, that is, when objects are destroyed.

Let's start by presenting a comparative example that shows how another object-oriented language (C++) handles object destruction:

    #include <cstdio>
    
    using namespace std;
    
    class A {
    public:
        A() {
            printf("call A::A %lx\n", (unsigned long)this);
        }
        ~A() {
            printf("call A::~A %lx\n", (unsigned long)this);
        }
    };
    
    void f() {
        A a;
    }
    
    int main() {
        A* ap = new A;
        f();
        delete ap;
    }

Class A has a destructor A::~A. It's called when a stack-based object, such as the one allocated in the f function, goes out of scope. A destructor is also called when an object is explicitly deleted through the delete operator -- this is also illustrated in the program. The destructor is responsible for any class-specific cleanup that needs to be done. After the destructor is called, the space for the object is reclaimed.

The output of the program looks like this:

    call A::A 7a32c8
    call A::A 12ff50
    call A::~A 12ff50
    call A::~A 7a32c8

The output shows the memory addresses of objects at construction and destruction time.

The Java language handles things a little differently. In the first place, there are no stack-based objects, only references to objects. In other words, objects are allocated dynamically, and references (pointers) to those objects may be stored on the stack.

Another really big difference is that there is no delete operator corresponding to the new operator that is used to allocate objects. Unreferenced objects ("garbage") are automatically reclaimed as needed, through a process known as garbage collection. For example, in this code:

    void f() {
        String str = new String();
    }

the string object becomes garbage as soon as the f method completes execution. There is no way for the program containing f to reference the string, and so the memory used for the string object can be reclaimed. The actual process of garbage collection is scheduled in a way to minimize garbage collector overhead. It will probably take place at a later time (or not at all).

When garbage collection happens, the Java virtual machine* determines which objects in the application are live, that is, which objects are reachable from running code. Objects that are not reachable are subject to reclamation, and their memory is reused. Because there is no delete operator, and no notion of stack-based objects going out of scope, it is only at garbage collection time that a call to a destructor is meaningful.

This model has several implications. One is that you don't usually need to worry about space management issues. There is no delete operator available, and it's usually a bad idea to explicitly invoke garbage collection.

Sometimes it's useful to deliberately nullify object references, to help out the garbage collector. For example, if you have a stack of object references, and you pop an element from the stack, it's important to set the just-vacated stack slot reference to null. Here's some sample code:

        public Object pop() { 
                Object obj = stack[stackptr]; 
                stack[stackptr--] = null; 
                return obj; 
        }

Popping an element from the stack means that the element is no longer on the stack, but the object reference will linger unless nullified or overwritten by a subsequent stack push. The collection classes take care of this operation for you.

Another point concerns management of system resources. When garbage collection takes place, unused object and array space is reclaimed. But what happens if an object makes reference to other system resources, such as file descriptors? How are such resources reclaimed? One way of handling this situation is to define an explicit close() or cleanup() method, which an application must call when done with an object. This method will take care of freeing system resources.

For more information about garbage collection, see section 12.1, Garbage Collection, section 12.2, A Simple Model, section 12.3, Finalization, and section 12.4, Interacting With the Garbage Collector, in "The Java Programming Language Third Edition" by Arnold, Gosling, and Holmes.

.
.

PREPROCESSING AND THE JAVA LANGUAGE

The widely-used programming languages C and C++ make heavy use of what is called a preprocessor. The preprocessor makes an initial scan over the source code, and certain preprocessor directives are treated in a special way. For example, the directive:

    #define X 37

results in the token X being replaced with 37 throughout the code. This feature is used to specify global constants in an application.

The preprocessor is used to define constants, to include the contents of other source files (header files), to specify conditional compilation, and so on. Preprocessing is a useful mechanism, but it does have some problems associated with it. The Java language has no preprocessor, and because of this, it takes a different approach in these areas.

Let's start by looking at the preprocessor #include directive in C and C++. This directive is used to include the contents of one file in another, like this:

    #include <stdio.h>

Typically you use a directive like this to gain access to constants and function declarations found in a header file such as stdio.h. The Java approach uses import declarations instead of #include:

    import java.util.*;

    import java.io.FileInputStream;

and there is also an implicit declaration:

    import java.lang.*;

supplied for you at the top of every Java source file. Information such as constants, typically found in header files, is folded into Java source code.

Import declarations do not use textual inclusion, but simply make the types in the specified package available. This approach tends to be much cleaner than actually importing huge masses of source code from a large number of header files.

Another way that a C/C++ preprocessor is used is to define global constants and macros, for example:

    #define X 100

    #define perc(a,b) ((a) * 100.0 / (b))

The Java equivalent of these definitions might look like this:

    class Globals {
    
        // define private constructor so that no
        // objects of the Globals class can be created
    
        private Globals() {}
    
        // global constants
    
        public static final int X = 100;
        public static final int Y = 200;
    
        // global "macro"
    
        public static double perc(double a, double b) {
            return a * 100.0 / b;
        }
    }
    
    public class PPDemo1 {
        public static void main(String[] args) {
            double p = Globals.perc(57.2958, Globals.Y);
            System.out.println("p = " + p);
        }
    }

Globals is a class used to hold public constants and static methods. It's got a private constructor so that no instances of the Globals class can be created. In other words, Globals is a packaging vehicle rather than a conventional class from which you create objects. Note that in this example there are other ways to define a group of constants. For example, you could use an enum pattern. In general, you should define constants in as local of a context as possible. You should also make constants private if possible. This example assumes that you do, in fact, need to define some constants that are pervasive across your program.

This Java language approach illustrated in the Globals class tends to work better for defining globals than the C/C++ approach that uses the #define directive. For example, if you have some code like this:

    #define X 100

    ...

    #define X 150

various C++ compilers will produce a warning message. However, this usage is probably an outright error, typically triggered by defining the same constant with different values across multiple header files.

Note in this example that static methods have guaranteed function semantics. For example, method arguments are evaluated only once. Macros fall short in this regard.

One issue with this example is whether the perc method is inlined (for performance). If you use the C/C++ preprocessor, and write a macro as illustrated in the C/C++ example, the macro will be expanded textually, and by definition is inline. There's no direct Java equivalent to this textual expansion. However, it's usually possible to achieve the same end by doing the following in some combination: making methods final (static methods are implicitly final), turning on compiler optimization (javac -O), and using Java Virtual Machines that do dynamic method inlining. Understand that expanding a macro inline is not always a good thing -- it makes debugging difficult or impossible.

Another area to look at is modifying the operation of a program by the use of compiler or run time directives. For example, if a C program is compiled like this:

    $ cc -DABC file.c

and file.c has in it the preprocessor directive:

    #ifdef ABC
    ...
    #endif

the behavior of the program is modified based on the presence of -DABC on the compile line. The code in the #ifdef ... #endif block is compiled only if -DABC is specified.

This approach is also useful with Java programs, although there is less need for it. One classic use of -D directives is targeting an application for particular hardware and software architectures. The Java system partially solves this problem by offering a uniform abstraction, no matter what the underlying hardware and operating system. For example, a long data type is 64 bits in the Java language. The size is independent of the hardware you use.

The following code illustrates what is sometimes called "conditional compilation":

    class Debug {
        public static final boolean DEBUG = false;
    }
    
    public class PPDemo2 {
        public static void main(String[] args) {
            if (Debug.DEBUG) {
                System.out.println("DEBUG is true");
            }
        }
    }

Here you have a "global" boolean variable Debug.DEBUG, and the execution of the code is dependent on the value of that variable. This usage takes advantage of a specific feature of the Java language specification that says:

    int x = 0;

    while (false)
        x = 3;

is illegal ("x = 3" is not reachable), but:

    int x = 0;

    if (false)
        x = 3;

is legal.

Note that a compiler can throw away the block of code inside of "if(Debug.DEBUG)" if the value is false, and javac 1.4 does exactly that. If you say:

    $ javap -c -classpath . PPDemo2

the result is:

    Compiled from PPDemo2.java
    public class PPDemo2 extends java.lang.Object {
        public PPDemo2();
        public static void main(java.lang.String[]);
    }
    
    Method PPDemo2()
        0 aload_0
        1 invokespecial #1 <Method java.lang.Object()>
        4 return
    
    Method void main(java.lang.String[])
        0 return

Notice that there's no trace of the System.out.println method call.

You can also use the Java launcher and libraries to specify the value of a system property. You can then use this value to modify the logic of your application. Here's an example:

    public class PPDemo3 {
        public static void main(String[] args) {
            String prop = System.getProperty(
                "property1");

            if (prop.equals("123")) {
                System.out.println("property1 is 123");
            }
        }
    }

If you compile this code, and then say:

    $ java -Dproperty1=123 PPDemo3

the effect will be to set the value of the system property for use within the program, that is:

    property1 is 123

Assertions are another area where you might want to alter the behavior of a program from outside. In C/C++, an assertion is specified by saying:

    assert(x >= 100);

assert is a macro, and its effect can be nullified using -DNDEBUG.

The Java language uses the assert keyword to specify assertions. Here's an example:

    public class PPDemo4 {
        static void f(int i) {
            assert i >= 0;
    
            // ... do other processing ...
        }
    
        public static void main(String[] args) {
            try {
                f(-1);
            }
            catch (AssertionError e) {
                System.err.println(
                    "*** assertion failed ***");
            }
        }
    }

You need to compile this code with:

    $ javac -source 1.4 PPDemo4.java

and then run it by saying:

    $ java -ea PPDemo4

You should see the following result:

    *** assertion failed ***

Normally you wouldn't catch an AssertionError. It's done in this example for demonstration purposes.

If you don't specify -ea to the Java launcher, assertions are not checked -- the assertions remain in your code and are ignored.

For more information about Java language approaches to preprocessing, see section 1.4, Named Constants, section 13.2, Type Imports, and section 18.1.2, System Properties, in "The Java Programming Language Third Edition" by Arnold, Gosling, and Holmes. Also see section 14.20, "Unreachable Statements" in "The Java Language Specification Second Edition" by Gosling, Joy, Steele, and Bracha. And see item 21 "Replace enum constructs with classes" in "Effective Java Programming Language Guide" by Joshua Bloch.

.
.
.

Reader Feedback

  Very worth reading    Worth reading    Not worth reading 

If you have other comments or ideas for future technical tips, please type them here:

 

Have a question about Java programming? Use Java Online Support.

.
.

IMPORTANT: Please read our Terms of Use, Privacy, and Licensing policies:
http://www.sun.com/share/text/termsofuse.html
http://www.sun.com/privacy/
http://developer.java.sun.com/berkeley_license.html


Comments? Send your feedback on the Core Java Technologies Tech Tips to: jdc-webmaster@sun.com

Subscribe to other Java developer Tech Tips:

- Enterprise Java Technologies Tech Tips. Get tips on using enterprise Java technologies and APIs, such as those in the Java 2 Platform, Enterprise Edition (J2EE).
- Wireless Developer Tech Tips. Get tips on using wireless Java technologies and APIs, such as those in the Java 2 Platform, Micro Edition (J2ME).

To subscribe to these and other JDC publications:
- Go to the JDC Newsletters and Publications page, choose the newsletters you want to subscribe to and click "Update".
- To unsubscribe, go to the subscriptions page, uncheck the appropriate checkbox, and click "Update".


ARCHIVES: You'll find the Core Java Technologies Tech Tips archives at:
http://java.sun.com/jdc/TechTips/


Copyright 2003 Sun Microsystems, Inc. All rights reserved.
4150 Network Circle, Santa Clara, CA 95054 USA.


This document is protected by copyright. For more information, see:
http://java.sun.com/jdc/copyright.html


Trademark Information: http://www.sun.com/suntrademarks/
Java, J2EE, J2SE, J2ME, and all Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries.


* As used in this document, the terms "Java virtual machine" or "JVM" mean a virtual machine for the Java platform.

Sun Microsystems, Inc.
.
.
Please unsubscribe me from this newsletter.