Singletons and Garbage collection.

I've heard that a singleton in java may
be garbage collected. To get around this you are supposed to put a sleeping thread in it, so that it won't be collected. First of all, is this true? Anyway, I wrote some code to test this, it uses finalize to tell when the garbage collector has run. I can't seem to get the garbage collector to run though. Can anyone tell me how to fix this code, to prove that the singleton is getting garbage collected, or if it isn't getting collected to give me some proof as to why? (It is two classes, the driver and the singleton.)


public class Singleton {
    static Singleton ref = null;
    int count = 0;

    protected Singleton() {}; // Don't use this outside of instance.
   
    public static Singleton instance() {
        if (ref == null) {
            ref = new Singleton();
        }
       
        ref.count++;
        System.out.println("Initializing #" + ref.count);
        return ref;
    }

    public static int getNumber() {
        return ref.count;
    }

    // This is how we tell if the Garbage collector has run
    // finalize() is run when an obj is GB collected.
   protected void finalize() {
        System.out.println("Garbage Collector running.");
    }
   
 
}

public class SingletonDriver {
   static void main (String argv[]) {
        boolean collected = false;
        Singleton sa = Singleton.instance();
        Singleton sb = Singleton.instance();
        String dummy; // to take up space.


        System.out.println("There are " + sa.getNumber() + " objects.");

        sa = null;
        sb = null;

        // Wait for GB collector
        for(int i =0; i < 1000000000; i++) {
            // Just busy wait.
            dummy = new String("Just wasting space" +i);
        }

        System.gc();
      //        System.out.println("collecteed == " + Singleton.collected);
        sa = Singleton.instance();
        System.out.println("There are " + sa.getNumber() + " objects.");
    }
}


Thanks.-



ArtimageAsked:
Who is Participating?
I wear a lot of hats...

"The solutions and answers provided on Experts Exchange have been extremely helpful to me over the last few years. I wear a lot of hats - Developer, Database Administrator, Help Desk, etc., so I know a lot of things but not a lot about one thing. Experts Exchange gives me answers from people who do know a lot about one thing, in a easy to use platform." -Todd S.

imladrisCommented:
I'm not sure what your original information was referring to. Certainly not a singleton of the kind you are describing above. Nothing gets garbage collected that still has a valid reference to it. The static in your class will certainly be found as a valid reference.

As some additional comfort I have used this construct:

      private static PosEventQueue obj = new PosEventQueue();

// Don't let anyone instantiate this class
      private PosEventQueue()
      {      scron=true;
            return;
      }

      public static PosEventQueue getPosEventQueue()
      {      return(obj);
      }

This has been running in a production system for over a year without trouble. Note that the class can be instantiated right in an initializer.

0
mbormannCommented:
>>>if it isn't getting collected to give me some proof as to why?

continuing in the same line as imladris ,the JVM will still have a valid reference to the static Singleton class ,why you will ask? Because the Memory Layout on ALL systems is such that there is a Heap, Stack and a special Heap for static objects (& variables ? i am not sure abt the last though),I got this answer from Scott Meyer's excellent book 'More Effective C++'

Also there is a special memory reserved for Strings so your string
"Just wasting space" is cached by the JVM and not allocated again unless you forcibly use a new String("Just wasting space"),if your code is like this I think the gc() will be forced to kick in and run.

new String (new String ("Just wasting space") + i)

what actually happens is that the JVM checks if this particular string is in Memory ,if it is then it reuses it else new String("that string") is called.
try and see a good site on gc
http://www.daimi.au.dk/~beta/Papers/Train/train.html
0
heyhey_Commented:
but why
>> // Don't let anyone instantiate this class
>> scron=true;

:)
0
Cloud Class® Course: Ruby Fundamentals

This course will introduce you to Ruby, as well as teach you about classes, methods, variables, data structures, loops, enumerable methods, and finishing touches.

mbormannCommented:
Now that you point it out, in a static method creating singleton as suggested by GoF the constructor is privatized,but need not have any content.
:(
0
heyhey_Commented:
> :(
mbormann, but why are you so sad today ?
0
mbormannCommented:
there's a reason for that ,shall i mail you abt it? If u say so here then will do so else forget it...
0
imladrisCommented:
Is there anything more you need clarified before grading the answer?

(scron stands for screen on, and relates to other things going on in the class ;) )
0
heyhey_Commented:
mbormann: e-mail me, maybe I can cheer you :)
but its Friday, 19:00, I am going out for a  beer ... talk to you tomorrow friend :)

and have a nice weekend !
0
ArtimageAuthor Commented:
Sorry, it has been a busy few days. I'll try to be more prompt from here on out.

So I found the book, "Patterns in Java volume 1", which states the error that I'm trying to test.

Here is the line. Now I see that it has somthing to do with class loadings, but I'm still not sure how to write code to prove it.

"There is a rather subtle bug than [sic] can occur in implementation of the Singleton pattern. It can cause a Singleton class to create and instatiate more than one insance of itself. The problem occurs in programs that refer to a singleton class only through other classes that are dynamically loaded..."

" ... When a program stops using using classes, the classes that it is finished with may be garbaged collected."


So apperently I need a test that uses the singleton in a dynamically loaded class, then allows that class to be gc'd.

Finally...
Mbormann: I understand that the string is cached, but since I'm adding I to it in the new().. and i is different each time, shouldn't that force a new string to be created?

Artimage.-
0
heyhey_Commented:
1. you have one Class object per name per classloader, so you may have more than one class with the same name, but these are different OBJECTS, and usually you won't find them inside the same namespace (in fact this would be a serious BUG).
think of it this way - each classloader has its own classspace, you may have classes with the same name in different namespaces, but EVERY object (usually) works only with classes from its namespace. for example in browser enviroment, you usually have one classloader per applet codebase, so you'll have one classobject / singleton per applet codebase, but every applet will see only ONE singleton.
the statement that you've quoted is quite vague - what does that mean
'...that refer to a singleton class only through other classes that are dynamically loaded... '
in fact in Java all CLASSES are dynamically loaded ...
2.
'... When a program stops using using classes, the classes that it is finished with may be garbaged collected.'
as far as I remember in Java 1.1 (bug or not) the class object won't be garbage collected even when all the class instances are already garbage collected. in Java 2.0 the class objects are garbage collected too.

my 0.05$ :)
0
mbormannCommented:
continuing what heyhey said

a few clarifications
(1)At runtime, a class is uniquely identified both by its fully-qualified class name (package and class name) as well as the class loader that loaded it.

(2)The name spaces created by different class loaders can overlap, have some names in common, and have others quite separate.

>>>>>for example in browser enviroment, you usually have one classloader per applet codebase, so you'll have one classobject / singleton per applet codebase, but every applet will see only ONE singleton.

Generally what happnes is that when asked to load a class, a class loader will first delegate the request to a parent calss loader. In this way, all the class loaders in a VM form a tree, and at the root of this tree is the system class loader (which loads class from the VMs classpath). The classes recognized by a class loader include those classes loaded explicitly by the class loader itself, as well as all those classes recognized by the loaders parent class loader. So, the classes recognized by a particular class loader are a superset of the classes actually loaded by that loader. Its like a inheritance tree.

yes there maybe several applet calss loaders running in browser art same time and they may all have to load class java.applet.Applet. It is essential that all of them return the exact same Class object when asked to load this class. Otherwise, the container (in this particluar case the browser) will not recognize an object generated from one of the loaded applet classes as an object derived from java.applet.Applet. You wouldn't want the container's ( browsers) namespace to be different than any of the individual applet class loader's namespaces, at least not where the class java.applet.Applet is concerned.

So, instead of actually loading the Applet class when asked, an applet class loader must delegate to the system class loader (which loads all of the java.* classes).

Similarly we can argue that your singleton class which maybe loaded over the network will be a 'Singleton' as it's been loaded by your VM's Class Loader

>>>'...that refer to a singleton class only through other classes that are dynamically loaded... '

Probably Mark Grand ,author of 'Patterns in Java' meant to say 'dynamically loaded over the network'

>>>" ... When a program stops using using classes, the classes that it is finished with may be garbaged collected."

Not true of static data ,methods they are kept in a special memory which is I think different from the Heap & stack.So I think I agree with
>>>the class object won't be garbage collected even when all the class instances are already garbage collected.

heyhey,can u give evidence abt this one?
>>>in Java 2.0 the class objects are garbage collected too.
0
heyhey_Commented:
heyhey,can u give evidence abt this one?
>>>in Java 2.0 the class objects are garbage collected too.

not at the moment - but if you have time, you can search JavaWorld for 'garbage collection' ... :)

after all Class objects are just normal Java objects - in some specail enviroment - for example if you have some small (embeded ?) device that continuously downloads new application classes and executes them, you really need its JavaVM to garbage collect all the 'old' classes :)

if you don't want your class to be garbage collected, you'll have to keep some global reference to it (or some of its objects).
0
mbormannCommented:
read this http://www.metamech.com/wiki/view/Main/PrimordialClassLoader

About static

A system class is based on CLASSPATH. CLASSPATH cannot be changed during the lifetime of a JVM. System classes are loaded once and only once during the lifetime of a JVM. Because of these things, a static variable is really static for system classes.

Static varibles are initialized once and only once during the lifetime of a JVM.
A non-system class is based on CODEBASE or a custom class loader.
CODEBASE changes from one HTML page to another. A custom class loader can reused many times. Because of these things, a static variable isn't really static for non-system classes.

Static variables are initialized less often than other variables. It may be once
per restart of an applet. It may be once per instance of a custom class loader.

So Artimage ,my answer to your question is that put your Singleton class in the primordial JVM's classpath so it will be loaded only once and yes have a global reference to it for safety sake's as heyhey said.
0
heyhey_Commented:
Singleton per classloader is enough for me - classloader namespaces are different universies inside the same JavaVM
0
ArtimageAuthor Commented:
Actually, I'm writing this code to see it not work. ie the singleton's class being gc'd and losing state. But I'm pretty sure I know how I need to do about that now. I need to make a custom class loader that loads my singleton class. Then allow all references to be garbage collected. Then get a new instance. This should show me that it is possible for a Singleton to lose state.

I still don't understand one thing though, why does the finalize in the above code never print anything?

Thanks.-
0
heyhey_Commented:
you don't keep references to your Strings, so they won't fill the memory quickly enough ...

about the Singleton stuff - as I already said - Singleton per Classloader is single enough - after all your next step is singleton per VM which is not much better ... classloader space is THE UNIVERSE - not the Java VM space (which is system classloader space)

all your current objects live in the same namespace. if you can access two different version of the 'same' class (loaded from two different classloaders) - you already have a problem to FIX :)

regarding class GC, my JavaVM (-noclassgc option) says that it CAN garbage collect classes, but I doubt that this is true :)

usage: java [-options] class

where options include:
    -help             print out this message
    -version          print out the build version
    -v -verbose       turn on verbose mode
    -debug            enable remote JAVA debugging
    -noasyncgc        don't allow asynchronous garbage collection
    -verbosegc        print a message when garbage collection occurs
    -noclassgc        disable class garbage collection

best regards
  heyhey
0
mbormannCommented:
Perhaps this helps?

/*****************************************************************************
 * Copyright (c) 1999, KL GROUP INC.  All Rights Reserved.
 * http://www.klgroup.com
 *
 *****************************************************************************/


import java.lang.reflect.*;
import java.util.*;

/*************************************************************
 *
 * A utility class for identifying loitering objects.  Objects
 * are tracked by calling <code>ObjectTracker.add()</code> when instantiated,
 * and calling </code>ObjectTracker.remove()</code> when finalized.  Only
 * classes that implement </code>ObjectTracker.Tracked</code> can be tracked.
 * As instances are created and destroyed, they are reported
 * to the stdout.  Summaries by class can also be reported on demand.
 *
 * <P>
 * In order to enable this functionality, add <code>-DObjectTracker</code>
 * when running your program.  This will track all classes that implement
 * <code>ObjectTracker.Tracked</code> and call add/remove as indicated in the
 * previous paragraph.
 *
 * <P>
 * For a finer degree of control, specify a list of filters
 * when setting the <code>ObjectTracker</code> property.  For instance,
 * <code>-DObjectTracker=+MySpecialClass,-ClassFoo</code> will only report
 * on instances of classes whose name contains <code>MySpecialClass</code>
 * but not <code>ClassFoo</code>.  Hence <code>MySpecialClassBar</code>
 * will be tracked, while <code>MySpecialClassFoo</code> will not be.
 * See <A HREF="ObjectTracker.html#start()"> start()</A> for more details.
 *
 * <P>
 * <B>Limitations</B>
 *
 * <P>
 * Since you must add instrumentation to all the classes you want to
 * track, this is not nearly as useful as a Memory Profiler/Debugger
 * like <I>JProbe Profiler</I>.  Also, since it cannot tell you which references
 * are causing the object to loiter, it doesn't help you remove clean
 * up the loiterers.  If you want to solve the problem, you really need
 * to use a Memory Profiler/Debugger like <I>JProbe Profiler</I>.  The only
 * thing that ObjectTracker can help with is testing whether an instance
 * of a known class goes away.
 * <P>
 * <B>Implementation Notes</B>
 *
 * <P>
 * The current implementation assumes that every object has a unique
 * hashcode.  This is a false assumption in general, but does work
 * in JavaSoft's Win32 VM for JDK1.1.
 *
 * <P>
 * This implementation will definitely not work in JavaSoft's
 * implementation of the Java 2 VM, including the HotSpot VM.
 *
 *************************************************************
 */
public class ObjectTracker {

// Property ObjectTracker turns this on when set
private final static boolean ENABLED =
      System.getProperty("ObjectTracker") != null;

// Classes are hashed by name into this table.
private static Hashtable classReg;
private static Vector       patterns;

/** Record info about an object.  Class and ordinal number are stored. */
private static class ObjectEntry {
      int            ordinal;            // distinguishes between mult. instances
      String      clazz;                  // classname
      String  name;                  // name (may be null)

      public ObjectEntry(int ordinal, String clazz, String name) {
            this.ordinal = ordinal;
            this.clazz   = clazz;
            this.name    = name;
      }

      public String toString() {
            return clazz + ":#" + ordinal + " ("+name+")";
      }
} // ObjectEntry

/**
 * Records info about a class.  Within each class, a table of objects
 * is maintained, along with the next ordinal to use to stamp the
 * next object of this class.
 */
private static class ClassEntry {
      String            clazz;                  // class name
      Hashtable      objects;            // list of ObjectEntry
      int                  ordinal;            // last instance of this class created

      public ClassEntry(String clazz) {
            this.clazz = clazz;
            objects    = new Hashtable();
            ordinal    = 1;
      }

      public String toString() {
            return clazz;
      }

      /**
       * Get the name of the object by invoking getName().
       * Uses reflection to find the method.
       */
      private String getName(Object o) {
            String name = null;
            try {
                  Class cl = o.getClass();
                  Method m = cl.getMethod("getName", null);
                  name = (m.invoke(o, null)).toString();
            }
            catch (Exception e) { }

            return name;
      }

      public void addObject(Object obj) {
            // Store this object in the object table
            Integer id = new Integer(System.identityHashCode(obj));
            ObjectEntry entry = new ObjectEntry(ordinal, clazz, getName(obj));
            objects.put(id, entry);
            ordinal++;

            System.out.println("    added: " +entry);
      }

      public void removeObject(Object obj) {
            // Removes this object from the object table
            Integer id = new Integer(System.identityHashCode(obj));
            ObjectEntry entry = (ObjectEntry) objects.get(id);
            objects.remove(id);

            System.out.println("    removed: " +entry);
      }

      /** Dump out a list of all object in this table */
      public void listObjects() {
            if (objects.size() == 0)  {
                  // skip empty tables
                  return;
            }

            System.out.println("For class: " + clazz);
            Enumeration objs = objects.elements();
            while (objs.hasMoreElements()) {
                  ObjectEntry entry = (ObjectEntry) objs.nextElement();
                  System.out.println("    " +entry);
            }
      }
} // ClassEntry

/** No constructor */
private ObjectTracker() {}

/**
 * Determine is this class name should be tracked.
 * @return true if this class should be tracked.
 * @see start
 */
private static boolean isIncluded(String clazz) {
      int i=0, size = patterns.size();

      if (size == 0) {
            // always match if list is empty
            return true;
      }

      boolean flag = false;
      for (; i<size; i++) {
            String pat = (String) patterns.elementAt(i);
            String op = pat.substring(0, 1);            // + or -
            String name = pat.substring(1);
            if (name.equals("all")) {
                  if (op.equals("+"))
                        flag = true;            // match all, unless told otherwise
                  else if (op.equals("-"))
                        flag = false;            // match nothing, unless told otherwise
            }
            else if (clazz.indexOf(name) != -1) {
                  // match if any of the filter names is a substring of
                  // the class name
                  if (op.equals("+"))
                        return true;
                  else if (op.equals("-"))
                        return false;
            }
      }

      return flag;
}

/**
 * Must be called before any objects can be tracked.  Turns on object tracking
 * if property <code>ObjectTracker</code> is set.  In addition, the list
 * of patterns assigned to this property is stored for future pattern
 * matching by <code>isIncluded()</code>.  This list of patterns must
 * be supplied as a comma-separated list, each one preceded by a
 * <code>+</code> or <code>-</code>, which indicates whether or not
 * the pattern should cause matching classes to be tracked or not.  
 * If the property <code>ObjectTracker</code> has no values, it is
 * equivalent to <code>+all</code>.
 */
public static void start() {
      if (ENABLED) {
            classReg = new Hashtable();
            patterns = new Vector();

            String targets = System.getProperty("ObjectTracker");
            StringTokenizer parser = new StringTokenizer(targets, ",");
            while (parser.hasMoreTokens()) {
                  String token = parser.nextToken();
                  patterns.addElement(token);
            }
      }
}

/**
 * Add object to the tracked list.  Will only be added if the
 * object's class has not been filtered out.
 *
 * @param obj object to be added to tracking list
 */
public static void add(Tracked obj) {
      if (ENABLED) {
            String clazz = obj.getClass().getName();
            if (isIncluded(clazz)) {
                  ClassEntry entry = (ClassEntry) classReg.get(clazz);
                  if (entry == null) {
                        // first one for this class
                        entry = new ClassEntry(clazz);
                        classReg.put(clazz, entry);
                  }
                  entry.addObject(obj);
            }
      }
}

/**
 * Removes object from tracked list.  This method should be called
 * from the finalizer.
 *
 * @param obj object to be removed from tracking list
 */
public static void remove(Tracked obj) {
      if (ENABLED) {
            String clazz = obj.getClass().getName();
            if (isIncluded(clazz)) {
                  ClassEntry entry = (ClassEntry) classReg.get(clazz);
                  entry.removeObject(obj);
            }
      }
}

/**
 * Print tracked objects, summarized by class.  Also prints a
 * summary of free/total memory.
 */
public static void dump() {
      if (ENABLED) {
            Enumeration e = classReg.elements();
            while (e.hasMoreElements()) {
                  ClassEntry entry = (ClassEntry) e.nextElement();
                  entry.listObjects();
            }

            System.out.println("==================================");
            System.out.println("Total Memory: " +
                  Runtime.getRuntime().totalMemory());
            System.out.println("Free  Memory: " +
                  Runtime.getRuntime().freeMemory());
            System.out.println("==================================");
            System.out.println("");
      }
}

/**
 * All classes that want to use this service must implement this
 * interface.  This forces this class to implement Object's finalize
 * method, which should call <code>ObjectTracker.remove()</code>.
 */
public interface Tracked {
      /**
       * All classes that use ObjectTracker must implement a finalizer.
       */
      void finalize();
}

}


/*****************************************************************************
 * Copyright (c) 1999, KL GROUP INC.  All Rights Reserved.
 * http://www.klgroup.com
 *
 *****************************************************************************/

public class tester implements ObjectTracker.Tracked {
      private int[] junk = new int[5000];

      public static void main(String args[]) {
            ObjectTracker.start();

            for (int i=0; i<1000; i++) {
                  tester t = new tester();
                  t.doNothing();
                  if (i%100 == 0) {
                        System.gc();
                  }
            }
            ObjectTracker.dump();
      }

      public tester() {
            ObjectTracker.add(this);
      }

      public void finalize() {
            ObjectTracker.remove(this);
      }

      public void doNothing() {
      }
}
0
ArtimageAuthor Commented:
heyhey: "you don't keep references to your Strings, so they won't fill the memory quickly enough ... "

How does that work, I would assume that new would create the string, and then to get rid of it the garbage collector would have to be called. How is it that my not keeping the reference outside the loop scope is making it not fill up memory? TIA.
0
heyhey_Commented:
GC will be called, but it will probably release the String objects (which are much more and much simpler objects :) it is not 'obligated' to release all the objects.

and one thing that I didn't notice - you still have reference to this Object from a static variable, so it won't be garbage collected at all. (GC of classes is tricky :)

and onr thing more - if Java VM is your OS, that different classloader spaces are your processes. singleton per process is what you'll get when you work with Windows and app written in C++ too.
0
ArtimageAuthor Commented:
Are you sure that the static reference will prevent class garbage collection? The book doesn't seem to think so. They have a static reference to the class, but they still create a sleeping thread to guard against GC.

Maybe you have a good pointer where I can read about the rules for GC in the java VM? Tia.

Artimage.-
0
heyhey_Commented:
>> Are you sure that the static reference will prevent class garbage collection?

sounds quite sensible to me :)
can you post the example code from the book ?
0
ArtimageAuthor Commented:

Sure. This is the code he uses to make sure the singleton is not GC'd, and the class below it is his Singleton example.

:)


/**
 * This class has methods to ensure that an object is never garbage
 * collected.
 */
public class ObjectPreserver implements Runnable {
    // This keeps this class and everything it references from being
    // garbage collected
    private static ObjectPreserver lifeLine = new ObjectPreserver();

    // Since this class won't be garbage collected, neither will this
    // HashSet or the object that it references.
    private static HashSet protectedSet = new HashSet();

    /**
     * Constructor.
     */
    private ObjectPreserver() {
        new Thread(this).start();
    } // constructor()

    public void run() {
        try {
            wait();
        } catch (InterruptedException e) {
        } // try
    } // run()

    /**
     * Garbage collection of objects passed to this method will be
     * prevented until they are passed to the unpreserveObject method.
     */
    public static void preserveObject(Object o) {
        protectedSet.add(o);
    } // preserveObject()

    /**
     * Objects passed to this method lose the protection that the
     * preserveObject method gave them from garbage collection.
     */
    public static void unpreserveObject(Object o) {
        protectedSet.remove(o);
    } // unpreserveObject(Object)
} // class ObjectPreserver


import java.applet.AudioClip;

/**
 * This class can be used to avoid playing two audio clips at the same
 * time.  The class has only one instance that can be accessed through
 * its getInstance method.  When you play audio clips through that
 * object, it stops the last audio clip it was playing before
 * it starts the newly requested one.  If all audio clips are played
 * through the AudioClipManager object then there will never be more
 * than one audio clip playing at the same time.
 */
public class AudioClipManager implements AudioClip{
    private static AudioClipManager myInstance
      = new AudioClipManager();
    private AudioClip prevClip; // previously requested audio clip

    /**
     * This private constructor is defined so the compiler won't
     * generate a default public constructor.
     */
    private AudioClipManager() { }

    /**
     * Return a reference to the only instance of this class.
     */
    public static AudioClipManager getInstance() {
        return myInstance;
    } // getInstance()

    /**
     * Start playing this audio clip. Each time this method is called,
     * the clip is restarted from the beginning.
     */
    public void play() {
        if (prevClip != null)
          prevClip.play();
    } // play()

    /**
     * Stop the previously requested audio clip and play the given audio
     * clip.
     * @param clip the new audio clip to play.
     */
    public void play(AudioClip clip) {
        if (prevClip != null)
          prevClip.stop();
        prevClip = clip;
        clip.play();
    } // play(AudioClip)

    /**
     * Starts playing this audio clip in a loop.
     */
    public void loop() {
        if (prevClip != null)
          prevClip.loop();
    } // loop()

    /**
     * Stop the previously requested audio clip and play the given audio
     * clip in a loop.
     * @param clip the new audio clip to play.
     */
    public void loop(AudioClip clip) {
        if (prevClip != null)
          prevClip.stop();
        prevClip = clip;
        clip.loop();
    } // play(AudioClip)

    /**
     * Stops playing this audio clip.
     */
    public void stop() {
        if (prevClip != null)
          prevClip.stop();
    } // stop()
} // class AudioClipManager
0
heyhey_Commented:
have you tried this code ? it seems to me that you'll get java.lang.IllegalMonitorStateException the first time you try to 'ensure that an object is never garbage collected.' :)
(not sure though)

this class may keep some objects from from being garbage collected, but I still don't see why static references won't work :)
0

Experts Exchange Solution brought to you by

Your issues matter to us.

Facing a tech roadblock? Get the help and guidance you need from experienced professionals who care. Ask your question anytime, anywhere, with no hassle.

Start your 7-day free trial
ArtimageAuthor Commented:
No I haven't tried the code, I just read the example from the book. :(  I don't see why your reference won't work either, which is why I posted.... I guess it may be one of those things where I'll end up emailing the author. :)

Art.-

PS. Since your the last man standing I figure you deserver the points.
0
mbormannCommented:
>>>I guess it may be one of those things where I'll end up emailing the author. :)

Do post it here too and let us know ur results.

>>>PS. Since your the last man standing I figure you deserver the points.

:)
0
heyhey_Commented:
Thanks :)
0
mbormannCommented:
0
It's more than this solution.Get answers and train to solve all your tech problems - anytime, anywhere.Try it for free Edge Out The Competitionfor your dream job with proven skills and certifications.Get started today Stand Outas the employee with proven skills.Start learning today for free Move Your Career Forwardwith certification training in the latest technologies.Start your trial today
Java

From novice to tech pro — start learning today.