Thứ Ba, 17 tháng 8, 2010

Java Map.get and Map.containsKey

When using Java's Map implementations, it is sometimes common to invoke the Map's get(Object) method and to react differently based on whether the value returned is null or not.  A common assumption might be made that a null returned from Map.get(Object) indicates there is no entry with the provided key in the map, but this is not always the case.  Indeed, if a Java Map implementation allows for null values, then it is possible for the Map to return its value for the given key, but that value might be a null.  Often this doesn't matter, but if it does, one can use Map.containsKey() to determine if the Map entry has a key entry.  If it does and the Map returns null on a get call for that same key, then it is likely that the key maps to a null value.  In other words, that Map might return "true" for containsKey(Object) while at the same time returning "null" for get(Object).  There are some Map implementations that do not allow null values.  In those cases, a null from a "get" call should consistently match a "false" return from the "containsKey" method.

In this blog post, I demonstrate these aspects of Map.get(Object) and Map.containsKey(Object).  Before going into that demonstration, I will first point out that the Javadoc documentation for Map.get(Object) does explicitly warn about the subtle differences between Map.get(Object) and Map.containsKey(Object):

If this map permits null values, then a return value of null does not necessarily indicate that the map contains no mapping for the key; it's also possible that the map explicitly maps the key to null. The containsKey operation may be used to distinguish these two cases.

For the post's examples, I will be using the States enum defined next:

States.java
package dustin.examples;

/**
* Enum representing select western states in the United Sates.
*/
public enum States
{
ARIZONA("Arizona"),
CALIFORNIA("California"),
COLORADO("Colorado"),
IDAHO("Idaho"),
KANSAS("Kansas"),
MONTANA("Montana"),
NEVADA("Nevada"),
NEW_MEXICO("New Mexico"),
NORTH_DAKOTA("North Dakota"),
OREGON("Oregon"),
SOUTH_DAKOTA("South Dakota"),
UTAH("Utah"),
WASHINGTON("Washington"),
WYOMING("Wyoming");

/** State name. */
private String stateName;

/**
* Parameterized enum constructor accepting a state name.
*
* @param newStateName Name of the state.
*/
States(final String newStateName)
{
this.stateName = newStateName;
}

/**
* Provide the name of the state.
*
* @return Name of the state
*/
public String getStateName()
{
return this.stateName;
}
}

The next code listing uses the enum above and populates a map of states to their capital cities.  The method accepts a Class that should be the specific implementation of Map to be generated and populated.

generateStatesMap(Class)
/**
* Generate and populate a Map of states to capitals with provided Map type.
* This method also logs any Map implementations for which null values are
* not allowed.
*
* @param mapClass Type of Map to be generated.
* @return Map of states to capitals.
*/
private static Map<States, String> generateStatesMap(Class mapClass)
{
Map<States,String> mapToPopulate = null;
if (Map.class.isAssignableFrom(mapClass))
{
try
{
mapToPopulate =
mapClass != EnumMap.class
? (Map<States, String>) mapClass.newInstance()
: getEnumMap();
mapToPopulate.put(States.ARIZONA, "Phoenix");
mapToPopulate.put(States.CALIFORNIA, "Sacramento");
mapToPopulate.put(States.COLORADO, "Denver");
mapToPopulate.put(States.IDAHO, "Boise");
mapToPopulate.put(States.NEVADA, "Carson City");
mapToPopulate.put(States.NEW_MEXICO, "Sante Fe");
mapToPopulate.put(States.NORTH_DAKOTA, "Bismark");
mapToPopulate.put(States.OREGON, "Salem");
mapToPopulate.put(States.SOUTH_DAKOTA, "Pierre");
mapToPopulate.put(States.UTAH, "Salt Lake City");
mapToPopulate.put(States.WASHINGTON, "Olympia");
mapToPopulate.put(States.WYOMING, "Cheyenne");
try
{
mapToPopulate.put(States.MONTANA, null);
}
catch (NullPointerException npe)
{
LOGGER.severe(
mapToPopulate.getClass().getCanonicalName()
+ " does not allow for null values - "
+ npe.toString());
}
}
catch (InstantiationException instantiationException)
{
LOGGER.log(
Level.SEVERE,
"Unable to instantiate Map of type " + mapClass.getName()
+ instantiationException.toString(),
instantiationException);
}
catch (IllegalAccessException illegalAccessException)
{
LOGGER.log(
Level.SEVERE,
"Unable to access Map of type " + mapClass.getName()
+ illegalAccessException.toString(),
illegalAccessException);
}
}
else
{
LOGGER.warning("Provided data type " + mapClass.getName() + " is not a Map.");
}
return mapToPopulate;
}

The method above can be used to generate Maps of various sorts.  I don't show the code right now, but my example creates these Maps with four specific implementations: HashMap, LinkedHashMap, ConcurrentHashMap, and EnumMap.  Each of these four implementations is then run through the method demonstrateGetAndContains(Map), which is shown next.

demonstrateGetAndContains(Map<states,string>)
/**
* Demonstrate Map.get(States) and Map.containsKey(States).
*
* @param map Map upon which demonstration should be conducted.
*/
private static void demonstrateGetAndContains(final Map<States, String> map)
{
final StringBuilder demoResults = new StringBuilder();
final String mapType = map.getClass().getCanonicalName();

final States montana = States.MONTANA;
demoResults.append(NEW_LINE);
demoResults.append(
"Map of type " + mapType + " returns "
+ (map.get(montana)) + " for Map.get() using " + montana.getStateName());
demoResults.append(NEW_LINE);
demoResults.append(
"Map of type " + mapType + " returns "
+ (map.containsKey(montana)) + " for Map.containsKey() using "
+ montana.getStateName());
demoResults.append(NEW_LINE);

final States kansas = States.KANSAS;
demoResults.append(
"Map of type " + mapType + " returns "
+ (map.get(kansas)) + " for Map.get() using " + kansas.getStateName());
demoResults.append(NEW_LINE);
demoResults.append(
"Map of type " + mapType + " returns "
+ (map.containsKey(kansas)) + " for Map.containsKey() using "
+ kansas.getStateName());
demoResults.append(NEW_LINE);
LOGGER.info(demoResults.toString());
}

For this demonstration, I intentionally set up the Maps to have null capital values for Montana to have no entry at all for Kansas.  This helps to demonstrate the differences in Map.get(Object) and Map.containsKey(Object).  Because not every Map implementation type allows for null values, I surrounded the portion that puts Montana without a capital inside a try/catch block.

The results of running the four types of Maps through the code appears next.


Aug 17, 2010 11:23:26 PM dustin.examples.MapContainsGet logMapInfo
INFO: HashMap: {MONTANA=null, WASHINGTON=Olympia, ARIZONA=Phoenix, CALIFORNIA=Sacramento, WYOMING=Cheyenne, SOUTH_DAKOTA=Pierre, COLORADO=Denver, NEW_MEXICO=Sante Fe, NORTH_DAKOTA=Bismark, NEVADA=Carson City, OREGON=Salem, UTAH=Salt Lake City, IDAHO=Boise}
Aug 17, 2010 11:23:26 PM dustin.examples.MapContainsGet demonstrateGetAndContains
INFO:
Map of type java.util.HashMap returns null for Map.get() using Montana
Map of type java.util.HashMap returns true for Map.containsKey() using Montana
Map of type java.util.HashMap returns null for Map.get() using Kansas
Map of type java.util.HashMap returns false for Map.containsKey() using Kansas

Aug 17, 2010 11:23:26 PM dustin.examples.MapContainsGet logMapInfo
INFO: LinkedHashMap: {ARIZONA=Phoenix, CALIFORNIA=Sacramento, COLORADO=Denver, IDAHO=Boise, NEVADA=Carson City, NEW_MEXICO=Sante Fe, NORTH_DAKOTA=Bismark, OREGON=Salem, SOUTH_DAKOTA=Pierre, UTAH=Salt Lake City, WASHINGTON=Olympia, WYOMING=Cheyenne, MONTANA=null}
Aug 17, 2010 11:23:26 PM dustin.examples.MapContainsGet demonstrateGetAndContains
INFO:
Map of type java.util.LinkedHashMap returns null for Map.get() using Montana
Map of type java.util.LinkedHashMap returns true for Map.containsKey() using Montana
Map of type java.util.LinkedHashMap returns null for Map.get() using Kansas
Map of type java.util.LinkedHashMap returns false for Map.containsKey() using Kansas

Aug 17, 2010 11:23:26 PM dustin.examples.MapContainsGet generateStatesMap
SEVERE: java.util.concurrent.ConcurrentHashMap does not allow for null values - java.lang.NullPointerException
Aug 17, 2010 11:23:26 PM dustin.examples.MapContainsGet logMapInfo
INFO: ConcurrentHashMap: {SOUTH_DAKOTA=Pierre, ARIZONA=Phoenix, WYOMING=Cheyenne, UTAH=Salt Lake City, OREGON=Salem, CALIFORNIA=Sacramento, IDAHO=Boise, NEW_MEXICO=Sante Fe, COLORADO=Denver, NORTH_DAKOTA=Bismark, WASHINGTON=Olympia, NEVADA=Carson City}
Aug 17, 2010 11:23:26 PM dustin.examples.MapContainsGet demonstrateGetAndContains
INFO:
Map of type java.util.concurrent.ConcurrentHashMap returns null for Map.get() using Montana
Map of type java.util.concurrent.ConcurrentHashMap returns false for Map.containsKey() using Montana
Map of type java.util.concurrent.ConcurrentHashMap returns null for Map.get() using Kansas
Map of type java.util.concurrent.ConcurrentHashMap returns false for Map.containsKey() using Kansas

Aug 17, 2010 11:23:26 PM dustin.examples.MapContainsGet logMapInfo
INFO: EnumMap: {ARIZONA=Phoenix, CALIFORNIA=Sacramento, COLORADO=Denver, IDAHO=Boise, MONTANA=null, NEVADA=Carson City, NEW_MEXICO=Sante Fe, NORTH_DAKOTA=Bismark, OREGON=Salem, SOUTH_DAKOTA=Pierre, UTAH=Salt Lake City, WASHINGTON=Olympia, WYOMING=Cheyenne}
Aug 17, 2010 11:23:26 PM dustin.examples.MapContainsGet demonstrateGetAndContains
INFO:
Map of type java.util.EnumMap returns null for Map.get() using Montana
Map of type java.util.EnumMap returns true for Map.containsKey() using Montana
Map of type java.util.EnumMap returns null for Map.get() using Kansas
Map of type java.util.EnumMap returns false for Map.containsKey() using Kansas



For the three Map types for which I was able to input null values, the Map.get(Object) call returns null even when the containsKey(Object) method returns "true" for Montana because I did put that key in the map without a value.  For Kansas, the results are consistently Map.get() returns null and Map.containsKey() returns "false" because there is no entry whatsoever in the Maps for Kansas.

The output above also demonstrates that I could not put a null value for Montana's capital into the ConcurrentHashMap implementation (an NullPointerException was thrown).


Aug 17, 2010 11:23:26 PM dustin.examples.MapContainsGet generateStatesMap
SEVERE: java.util.concurrent.ConcurrentHashMap does not allow for null values - java.lang.NullPointerException


This had the side effect of keeping Map.get(Object) and Map.containsKey(Object) a more consistent respective null and false return values.  In other words, it was impossible to have a key be in the map without having a corresponding non-null value.

In many cases, use of Map.get(Object) works as needed for the particular needs at hand, but it is best to remember that there are differences between Map.get(Object) and Map.containsKey(Object) to make sure the appropriate one is always used.  It is also interesting to note that Map features a similar containsValue(Object) method as well.

I list the entire code listing for the MapContainsGet class here for completeness:

MapContainsGet.java
package dustin.examples;

import java.util.EnumMap;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
* Simple example of using Map.get(key) versus Map.containsKey(key).
*/
public class MapContainsGet
{
/** Handle to java.util.logging Logger. */
private static Logger LOGGER = Logger.getLogger("dustin.examples.MapContainsGet");

/** New line. */
private static final String NEW_LINE = System.getProperty("line.separator");

/**
* Generate and populate a Map of states to capitals with provided Map type.
* This method also logs any Map implementations for which null values are
* not allowed.
*
* @param mapClass Type of Map to be generated.
* @return Map of states to capitals.
*/
private static Map<States, String> generateStatesMap(Class mapClass)
{
Map<States,String> mapToPopulate = null;
if (Map.class.isAssignableFrom(mapClass))
{
try
{
mapToPopulate =
mapClass != EnumMap.class
? (Map<States, String>) mapClass.newInstance()
: getEnumMap();
mapToPopulate.put(States.ARIZONA, "Phoenix");
mapToPopulate.put(States.CALIFORNIA, "Sacramento");
mapToPopulate.put(States.COLORADO, "Denver");
mapToPopulate.put(States.IDAHO, "Boise");
mapToPopulate.put(States.NEVADA, "Carson City");
mapToPopulate.put(States.NEW_MEXICO, "Sante Fe");
mapToPopulate.put(States.NORTH_DAKOTA, "Bismark");
mapToPopulate.put(States.OREGON, "Salem");
mapToPopulate.put(States.SOUTH_DAKOTA, "Pierre");
mapToPopulate.put(States.UTAH, "Salt Lake City");
mapToPopulate.put(States.WASHINGTON, "Olympia");
mapToPopulate.put(States.WYOMING, "Cheyenne");
try
{
mapToPopulate.put(States.MONTANA, null);
}
catch (NullPointerException npe)
{
LOGGER.severe(
mapToPopulate.getClass().getCanonicalName()
+ " does not allow for null values - "
+ npe.toString());
}
}
catch (InstantiationException instantiationException)
{
LOGGER.log(
Level.SEVERE,
"Unable to instantiate Map of type " + mapClass.getName()
+ instantiationException.toString(),
instantiationException);
}
catch (IllegalAccessException illegalAccessException)
{
LOGGER.log(
Level.SEVERE,
"Unable to access Map of type " + mapClass.getName()
+ illegalAccessException.toString(),
illegalAccessException);
}
}
else
{
LOGGER.warning("Provided data type " + mapClass.getName() + " is not a Map.");
}
return mapToPopulate;
}

/**
* Provide the {@code Map<States, String>} as an EnumMap.
*
* @return EnumMap of States to String.
*/
private static EnumMap<States, String> getEnumMap()
{
return new EnumMap<States, String>(States.class);
}

/**
* Log the provided Map and its type.
*
* @param mapToLog Map to have its type and content logged.
*/
private static void logMapInfo(final Map<States, String> mapToLog)
{
LOGGER.info(
mapToLog != null
? (mapToLog.getClass().getSimpleName() + ": " + mapToLog.toString())
: "null Map");
}

/**
* Demonstrate Map.get(States) and Map.containsKey(States).
*
* @param map Map upon which demonstration should be conducted.
*/
private static void demonstrateGetAndContains(final Map<States, String> map)
{
final StringBuilder demoResults = new StringBuilder();
final String mapType = map.getClass().getCanonicalName();

final States montana = States.MONTANA;
demoResults.append(NEW_LINE);
demoResults.append(
"Map of type " + mapType + " returns "
+ (map.get(montana)) + " for Map.get() using " + montana.getStateName());
demoResults.append(NEW_LINE);
demoResults.append(
"Map of type " + mapType + " returns "
+ (map.containsKey(montana)) + " for Map.containsKey() using "
+ montana.getStateName());
demoResults.append(NEW_LINE);

final States kansas = States.KANSAS;
demoResults.append(
"Map of type " + mapType + " returns "
+ (map.get(kansas)) + " for Map.get() using " + kansas.getStateName());
demoResults.append(NEW_LINE);
demoResults.append(
"Map of type " + mapType + " returns "
+ (map.containsKey(kansas)) + " for Map.containsKey() using "
+ kansas.getStateName());
demoResults.append(NEW_LINE);
LOGGER.info(demoResults.toString());
}

/**
* Main executable function.
*
* @param arguments Command-line arguments; none expected.
*/
public static void main(final String[] arguments)
{
final Map<States, String> hashMap = generateStatesMap(HashMap.class);
logMapInfo(hashMap);
demonstrateGetAndContains(hashMap);
final Map<States, String> linkedHashMap = generateStatesMap(LinkedHashMap.class);
logMapInfo(linkedHashMap);
demonstrateGetAndContains(linkedHashMap);
final Map<States, String> concurrentHashMap = generateStatesMap(ConcurrentHashMap.class);
logMapInfo(concurrentHashMap);
demonstrateGetAndContains(concurrentHashMap);
final Map<States, String> enumMap = generateStatesMap(EnumMap.class);
logMapInfo(enumMap);
demonstrateGetAndContains(enumMap);
}
}

I used an enum as the key for this demonstration which was nice because of its satisfactory equals and hashCode implementations available immediately with no extra effort on my part.  Objects used as keys without these set properly can misbehave in general and cause other differences between Map.get(Object) and Map.containsKey(Object) in certain cases.

Thứ Hai, 16 tháng 8, 2010

Most Valuable Type of Conference Session

The Java.net poll question this past week has been, "What type of technical conference sessions are most valuable?"  The options that can be selected for this poll question are Keynote Addresses, Panel Sessions, Technical Sessions, Birds of a Feather (BOF) Sessions, Other, and "I Don't Know."  As of this writing, there have been just over one hundred responses with 60% favoring Technical Sessions followed by the "I Don't Know" option being in second place with 20% of the responses.  I don't recall seeing many poll questions where this option is so high.  The next highest type of conference session after Technical Sessions's 60% is Birds of a Feather Sessions with 10%.  There are potential advantages associated with each of these session types as well as risks and drawbacks for each session.  In this post, I look at each of these types of sessions based on my previous experiences with them.


Keynote Sessions


I have attended keynote presentations at conferences that have been the highlight of the conference (especially in terms of long-term perspective), but I have also attended conference keynote sessions that have felt like barely more than a waste of time.  A keynote session has great potential if the subject is relevant to what I'm interested in because the keynote is often given by people with position, influence, and depth and breadth of experience.  For example, I enjoyed the Simon Phipps keynote presentations at Colorado Software Summit because of Phipps's obvious experience in the industry (especially open source) and because of his position and influence at Sun Microsystems (Chief Open Source Officer).

In many ways the opportunity cost of keynote sessions is often low because many conferences don't have any other sessions held during the keynotes.  This means that nothing potentially more valuable can be missed to attend the keynote.  There was a time when I thought that detailed technical sessions were always more valuable than keynote presentations and so resented keynotes precluding the holding of additional technical sessions.  However, looking back on things, select keynote presentations have had a profound impact on my development career and I now appreciate a thought-provoking and relevant keynote session.

While a technical session may provide more direct and specific details on how to do something, the keynote's nature is such that it typically has more breadth, is more far-reaching, and is future-looking.  Keynotes can give us insight into what a product's steward plans to do with their product in the future.  For example, the JavaOne 2010 keynote sessions have huge potential because they are likely to provide indication of the future of Java under Oracle.


Panel Sessions

The strength of panel sessions tends to be the potential diversity of opinion and the opportunity to see multiple sides of an issue.  While keynotes are often very one-sided (single speaker) and technical sessions are nearly always evangelistic in tone (one speaker cares enough about the topic to take the time and effort to prepare for and present), panels can bring out real controversy.  I have learned significant new concepts and formed or strengthened opinions based at least partially on observations in panel sessions.  It is particularly interesting to see where panelists with different agendas and different biases can find common ground.  I'm a strong believe that no single tool, language, or framework is right for all situations and no session is better equipped to help determine where certain things best fit than a panel session.

There are risks with panel sessions.  They can be significantly less effective if there is very little real diversity of opinion on the panel.  Second, panel sessions can easily go off on tangents and get lost in the weeds without a strong and knowledgeable moderator.  I've been in some truly awesome panel sessions where significant new ideas and concepts were energetically discussed and defended and I have been in some truly awful wastes of time with a bunch of like-minded individuals rehashing the same old opinions and masquerading as a panel session.

The Panel Session often has a higher opportunity cost than the Keynote Session because there often are other session types held simultaneously with the Panel Session.  It's bad enough to go to a poor Panel Session (either because of the poor Panelists or because of the poor moderator or both), but it seems even worse when you realize you could have attended a possibly more useful technical session, BOF, or unconference session.


Technical Sessions

Technical Sessions are the bread and butter of most software development conferences.  It is the technical sessions that I am most excited about when I build my preliminary agenda for which sessions I'm going to attend at a given conference.  One of the main advantages of the technical sessions is their typical narrow focus on a practical subject.  Panel Sessions and Keynote Sessions often focus on bigger picture concepts, trends, and "softer" things like politics and other issues.  In other words, focused detailed technical details are often an advantage of a good technical session.

Just as the value of the Panel Session is largely dependent on the quality of the panelists and moderator and the value of a Keynote Session is largely dependent on the experience and position of the person giving the keynote, a big part of a technical session's quality is grounded in the speaker's experience, speaking abilities, and preparation time.  Technical sessions have great potential to be aligned with a participant's interests because of their typical narrow focus and because many of them are offered at once so that participants can choose the most relevant session.

Not everyone likes the technical session format.  In the (currently) sole comment to the Java.net poll question, Olivier Allouch wrote, "IMHO, technical sessions are useless for me. I prefer a good doc or written tutorial."  I think he brings up a good point.  I generally prefer technical sessions that either introduce me to a new library or language (to decide if it's worth my time to look at the tutorials and documentation) or technical sessions that provide best practices, tips, and tricks that may not yet be readily collected and available in documentation.  Specifically, hard-earned lessons learned can make valuable technical sessions.  In other words, for me, the technical session is typically most valuable when it either provides an easy and soft introduction to something I have little familiarity with or when it provides the kind of specific details that can be difficult to glean from general documentation.


Birds of a Feather Sessions

The major advantage of Birds of a Feather (BOF) sessions lies in its nature: because these by definition are held by and for people with enough interest in the subject to flock together, they tend to be people either with deep and current experience or people truly interested in learning more about the subject.  Having like-minded participants can be a great advantage.

A second potential advantage of the BOF format is that these are often less formal than technical sessions.  The BOF can share some of the advantages of the panel sessions (different opinions and perspectives) as well as some of its disadvantages (potential for conversation to wander aimlessly).  Like the panel, the success of the BOF often depends on having one or a few strong moderators.  The key to a successful BOF seems to lie in its reduced formality, but without giving up formality altogether.  Many successful BOF sessions I have seen have even started with a small number of slides or notes on a projector to steer the conversation.  The best BOFs, in my experience, have been those held by someone with a prepared agenda, but with the ability to quickly adapt as the conversation goes without letting that conversation go too far off topic.

A great advantage of the BOF often comes outside of the session itself.  If nothing else, the BOF is often a great opportunity to share contact information with like-minded individuals for future collaboration and discussion.  The after-the-BOF in-the-hall sessions can often be worth more than the BOF itself.


Unconference Sessions

I'm not going to get into "Other" because I have no way of knowing what that entails.  However, one could argue that the recent popularity of Unconference Sessions has earned them a category of their own.  This is the type of session that I have the least experience with of the session types on this list, but its concept is pretty straightforward.  The Unconference Session is arguably often even less formal than the BOFs.  The main advantage of the Unconference Session is the opportunity to get sessions on topics that the conference organizer may not have covered as adequately as participants would have hoped.  Unconference sessions provide an opportunity for conference participants to set up their own presentations.  This is particularly useful for niche topics or for topics that might be a little outside of the conference's main charter.

JavaOne 2010 already has several Unconference Sessions scheduled.  An example is the recent announcement that Duchess will be holding an unconference session on "the role of women in Java and IT in general."

Like the BOFs, the Unconference Sessions enjoy advantages associated with reduced formality and the gathering of like-minded individuals.  However, like BOFs, they also have greater potential for drifting off topic if the organizers are not well prepared and adaptive.


Conclusion

I typically attempt to attend at least one of each type of session at a conference like JavaOne.  This approach reminds me of a diversified investing approach.  If I attend several different types of sessions, I'm more likely to receive tremendous benefit overall and to reduce my risk of wasted time.  For JavaOne 2010, I plan to attend the JavaOne-specific keynotes (especially the one on JDK7 and the road ahead) and will likely attend technical sessions most of the time when not in keynotes.  However, I will try to find one or two BOFs and one or two Unconference Sessions to break up the formality and to gain some of the benefits for which those types of sessions are better suited.


Thứ Bảy, 14 tháng 8, 2010

The Subtle Nuance of the new Keyword with Reference Types in Java

One of the trickier aspects of "general Java" development is related to comparing Java reference types for equality.  Fortunately, most of us learn early in our Java development experience that we can generally use the reference types' overridden versions of Object.equals to safely check the content of the objects, which is almost always what we want.  Object identity equality comparison with == is not what we want as frequently, but it can sometimes be mistakenly added to Java code and not discovered immediately because often even == between two seemingly different reference type objects can evaluate to true.  This is demonstrated in this blog post.


The following simple class demonstrates how == can appear to behave erratically.

LongValue.java
package dustin.examples;

import java.util.HashMap;
import java.util.Map;
import static java.lang.System.out;

public class LongValue
{
/**
* Print descriptive text followed by the resultant equality.
*
* @param descriptiveText Descriptive text explaining which equality is being
* shown.
* @param equality Equality being printed.
*/
private static void printEqualsResults(
final String descriptiveText, final boolean equality)
{
out.println(descriptiveText + " : " + equality);
}

/**
* Demonstrate use of == and .equals with reference types obtained in
* different ways (such as via instantiation with {@code new} keyword,
* {@code Long.valueOf(String)}, {@code Long.valueOf(long)}, and autoboxing)
* and with primitives.
*/
private static void demonstrateEquality()
{
final long primitiveLong = 1L;

final Long referenceLong1 = new Long(primitiveLong);
final Long referenceLong2 = primitiveLong;
final Long referenceLong3 = Long.valueOf("1");
final Long referenceLong4 = Long.valueOf(1L);
final Long referenceLong5 = new Long(primitiveLong);
final Long referenceLong6 = referenceLong1;

printEqualsResults("Primitive/Reference New", primitiveLong == referenceLong1);
printEqualsResults("Primitive/Reference Autobox", primitiveLong == referenceLong2);
printEqualsResults("Primitive/Reference Long.valueOf(String)", primitiveLong == referenceLong3);
printEqualsResults("Primitive/Reference Long.valueOf(long)", primitiveLong == referenceLong4);

out.println(" ---");

printEqualsResults("Reference New/Reference Autobox", referenceLong1 == referenceLong2);
printEqualsResults("Reference Autobox/Reference Long.valueOf(String)", referenceLong2 == referenceLong3);
printEqualsResults("Reference Long.valueOf(String)/Long.valueOf(long)", referenceLong3 == referenceLong4);
printEqualsResults("Reference New/Reference Long.valueOf(String)", referenceLong1 == referenceLong3);
printEqualsResults("Reference New/Reference Long.valueOf(long)", referenceLong1 == referenceLong4);
printEqualsResults("Reference Autobox/Reference Long.valueOf(long)", referenceLong2 == referenceLong4);
printEqualsResults("Reference New1/Reference New5", referenceLong1 == referenceLong5);
printEqualsResults("Reference New1/Reference New6", referenceLong1 == referenceLong6);
}

/**
* Compare object references to object references stored in a Map.
*/
private static void demonstrateWithinMaps()
{
final Map<String, Long> longs = new HashMap<String, Long>();
longs.put("1_Literal", 1L);
longs.put("2_New Reference", new Long(1L));
longs.put("3_LongValueOfLong Reference", Long.valueOf(1L));
longs.put("4_LongValueOfString Reference", Long.valueOf("1"));
for (final Map.Entry<String,Long> longReference : longs.entrySet())
{
printEqualsResults(longReference.getKey(), longReference.getValue() == 1L);
printEqualsResults(longReference.getKey(), longReference.getValue() == new Long(1L));
}
}

/**
* Main executable function.
*
* @param arguments Command-line arguments; none anticipated.
*/
public static void main(final String[] arguments)
{
demonstrateEquality();
out.println(" ---");
demonstrateWithinMaps();
}
}

The output from running the above code is shown next.


This output demonstrates a few interesting things about using == to compare references types to each other and reference types to primitive types.  There are actually several cases in the examples where two reference types instantiated in different ways with the same underlying long value actually evaluate to true even when compared for equality with the == operator.  Indeed, in the first set of examples, the only reference types compared for equality with == that do NOT evaluate to true are those with an instance of the Long obtained using the new keyword to instantiate the instance.  This same observation holds true in the collections set as well.

As the above examples demonstrate, using the new keyword to explicitly get an instance of the Long reference type results in a truly unique instance whose identity is not the same as any other Long instances no matter how those Long instances are obtained.  However, instances of Long obtained in other ways (autoboxing from primitive to reference type, Long.valueOf(String), and Long.valueOf(long)) all have the same identity.  Speaking of autoboxing, all instances of Long reference type evaluated to true when compared with == to the primitive long.

With all of this in mind, I now move to a related, but slightly different, nuance in Java identity comparisons for Integer.  The code example below (see The Terrible Dangers of Autoboxing, Part 2) shows a simple class called Autoboxing:

Autoboxing.java
package dustin.examples;

import static java.lang.System.out;

/**
* Simple class demonstrating a nuance of Java's Integer identity comparisons.
* Two Integers separately instantiated via autoboxing from primitive 10 will
* evaluate as identical via == while two integers instantiated via autoboxing
* from primitive 1000 will not evaluate as identical using same == operator.
*/
public class Autoboxing
{
public static void main(String[] args)
{
Integer a = 10;
Integer b = 10;
Integer c = 1000;
Integer d = 1000;
out.println("a == b: " + (a == b)); //true
out.println("c == d: " + (c == d)); //false
}
};

It can be somewhat surprising the first time the output of the above is seen.  It is shown in the next screen snapshot.


That's awkward.  The two reference type Integer instances based on autoboxing of the primitive ten are considered identical (== returns true when comparing the two) but two reference type Integer instances based on autoboxing of the primitive one thousand are considered not identical.  This is the case even with no "new" keyword in sight.

The next code snippet is Groovy code (script called generateAutoboxClass.groovy) that generates a simple Java class called GeneratedAutoboxing that will repeat the above experiment for many more primitives than simply 10 and 1000.

generateAutoboxClass.groovy
#!/usr/bin/env groovy
NEW_LINE = System.getProperty("line.separator")
newClass = new File("src/dustin/examples/GeneratedAutobox.java")
newClass << "package dustin.examples;${NEW_LINE}${NEW_LINE}"
newClass << "import static java.lang.System.out;${NEW_LINE}${NEW_LINE}"
newClass << "public class GeneratedAutobox${NEW_LINE}{${NEW_LINE}"
newClass << " public static void main(final String[] args)${NEW_LINE}"
newClass << " {${NEW_LINE}"
for (i in 0..250)
{
newClass << " final Integer a${i} = ${i};${NEW_LINE}"
newClass << " final Integer b${i} = ${i};${NEW_LINE}"
newClass << " final Integer c${i} = new Integer(${i});${NEW_LINE}"
newClass << " final Integer d${i} = new Integer(${i});${NEW_LINE}"
newClass << " out.println(\"a${i} = b${i}: \" + (a${i} == b${i}));${NEW_LINE}"
newClass << " out.println(\"c${i} = d${i}: \" + (c${i} == d${i}));${NEW_LINE}"
}
newClass << " }${NEW_LINE}"
newClass << "}"

This simple Groovy script generates the Java class GeneratedAutobox.java as shown below (with some of the monotonous middle portion removed):

GeneratedAutobox.java
package dustin.examples;

import static java.lang.System.out;

public class GeneratedAutobox
{
public static void main(final String[] args)
{
final Integer a0 = 0;
final Integer b0 = 0;
final Integer c0 = new Integer(0);
final Integer d0 = new Integer(0);
out.println("a0 = b0: " + (a0 == b0));
out.println("c0 = d0: " + (c0 == d0));
final Integer a1 = 1;
final Integer b1 = 1;
final Integer c1 = new Integer(1);
final Integer d1 = new Integer(1);
out.println("a1 = b1: " + (a1 == b1));
out.println("c1 = d1: " + (c1 == d1));
final Integer a2 = 2;
final Integer b2 = 2;
final Integer c2 = new Integer(2);
final Integer d2 = new Integer(2);
out.println("a2 = b2: " + (a2 == b2));
out.println("c2 = d2: " + (c2 == d2));
final Integer a3 = 3;
final Integer b3 = 3;
final Integer c3 = new Integer(3);
final Integer d3 = new Integer(3);
out.println("a3 = b3: " + (a3 == b3));
out.println("c3 = d3: " + (c3 == d3));
final Integer a4 = 4;
final Integer b4 = 4;
final Integer c4 = new Integer(4);
final Integer d4 = new Integer(4);
out.println("a4 = b4: " + (a4 == b4));
out.println("c4 = d4: " + (c4 == d4));
final Integer a5 = 5;
final Integer b5 = 5;
final Integer c5 = new Integer(5);
final Integer d5 = new Integer(5);
out.println("a5 = b5: " + (a5 == b5));
out.println("c5 = d5: " + (c5 == d5));
final Integer a6 = 6;
final Integer b6 = 6;
final Integer c6 = new Integer(6);
final Integer d6 = new Integer(6);
out.println("a6 = b6: " + (a6 == b6));
out.println("c6 = d6: " + (c6 == d6));
final Integer a7 = 7;
final Integer b7 = 7;
final Integer c7 = new Integer(7);
final Integer d7 = new Integer(7);
out.println("a7 = b7: " + (a7 == b7));
out.println("c7 = d7: " + (c7 == d7));
final Integer a8 = 8;
final Integer b8 = 8;
final Integer c8 = new Integer(8);
final Integer d8 = new Integer(8);
out.println("a8 = b8: " + (a8 == b8));
out.println("c8 = d8: " + (c8 == d8));
final Integer a9 = 9;
final Integer b9 = 9;
final Integer c9 = new Integer(9);
final Integer d9 = new Integer(9);
out.println("a9 = b9: " + (a9 == b9));
out.println("c9 = d9: " + (c9 == d9));
final Integer a10 = 10;
final Integer b10 = 10;
final Integer c10 = new Integer(10);
final Integer d10 = new Integer(10);
out.println("a10 = b10: " + (a10 == b10));
out.println("c10 = d10: " + (c10 == d10));
final Integer a11 = 11;
final Integer b11 = 11;
final Integer c11 = new Integer(11);
final Integer d11 = new Integer(11);
out.println("a11 = b11: " + (a11 == b11));
out.println("c11 = d11: " + (c11 == d11));
final Integer a12 = 12;
final Integer b12 = 12;
final Integer c12 = new Integer(12);
final Integer d12 = new Integer(12);
out.println("a12 = b12: " + (a12 == b12));
out.println("c12 = d12: " + (c12 == d12));

//
// . . . several lines omitted here . . .
//

final Integer a246 = 246;
final Integer b246 = 246;
final Integer c246 = new Integer(246);
final Integer d246 = new Integer(246);
out.println("a246 = b246: " + (a246 == b246));
out.println("c246 = d246: " + (c246 == d246));
final Integer a247 = 247;
final Integer b247 = 247;
final Integer c247 = new Integer(247);
final Integer d247 = new Integer(247);
out.println("a247 = b247: " + (a247 == b247));
out.println("c247 = d247: " + (c247 == d247));
final Integer a248 = 248;
final Integer b248 = 248;
final Integer c248 = new Integer(248);
final Integer d248 = new Integer(248);
out.println("a248 = b248: " + (a248 == b248));
out.println("c248 = d248: " + (c248 == d248));
final Integer a249 = 249;
final Integer b249 = 249;
final Integer c249 = new Integer(249);
final Integer d249 = new Integer(249);
out.println("a249 = b249: " + (a249 == b249));
out.println("c249 = d249: " + (c249 == d249));
final Integer a250 = 250;
final Integer b250 = 250;
final Integer c250 = new Integer(250);
final Integer d250 = new Integer(250);
out.println("a250 = b250: " + (a250 == b250));
out.println("c250 = d250: " + (c250 == d250));
}
}

The output from this generated class is interesting. A small part of that is shown in the next image.

The output of the generated Java class demonstrates a couple things.  First, the "new" approach to instantiating the integers consistently resulted in them being considered not identical when compared with the == operator.  It did not matter what primitive was used in the instantiation of the reference type Integer when the "new" operator was used: they were never identical.  The second observation is related to the previous example where autoboxing 10 resulted in two identical Integer reference types, but autoboxing 1000 did NOT result in two identical Integer reference types.  This example demonstrates where the break-off is: integers less than 128 are considered identical and integers 128 and greater are not considered identical.

Of course, there is nothing "magic" about that 127/128 break.  Indeed, the Java Language Specification does spell out this behavior.  Specifically, Section 5.1.7 ("Boxing Conversions") of the Third Edition of the JLS prescribes this:
If the value p being boxed is truefalse, a byte, a char in the range \u0000 to \u007f, or an int or short number between -128 and 127, then let r1 and r2 be the results of any two boxing conversions of p. It is always the case that r1 == r2.
This is intentional.

Other resources on this nuance include Java 1.5 Autoboxing Wackyness, Confused About == to Compare Java Wrapper Objects, and EXP03-J: Do not use the equal and not equal operators to compare boxed primitives.

In this post, I have demonstrated some nuances and potentially surprising behaviors related to Java's treatment of primitives, reference types, and autoboxing/unboxing.  These nuances, when not understood or realized, can lead to subtle errors and logic problems.  Most importantly, they serve as reminder of the importance of carefully considering handling of primitives and reference types and especially the mixing of the two.  The good news is that in many cases, only logical equality (.equals) [and not identity equality (==)] is required.

Thứ Năm, 12 tháng 8, 2010

JavaOne 2010 Technical Session on Scala and Clojure Canceled

With any conference the size of JavaOne, it is not unusual to have some presentations canceled.  As I blogged previously, a session I had signed up for ("Java and HTML5: Boldly Combine") has been canceled.  I have since learned that another session that I had signed up for ("Scala and Clojure: Java Virtual Machine Languages") has also been canceled.  Here are the details of this now canceled session:


Session ID: S313852
Title: Scala and Clojure Java Virtual Machine Languages
Date: Monday, Sept 20
Start Time: 4:00pm
Location: Parc 55, Powell I / II

I was looking forward to this single presentation comparing the two JVM-based languages Scala and Clojure. Although it is disappointing to learn of this cancellation, there are several other presentations in the same time slot that look really interesting. These include (but are not limited to) "Java Puzzlers: Scraping the Bottom of the Barrel" (S314408), "Java Persistence API (JPA) 2.0 with EclipseLink" (S314492), "Best Practices for Signing Code" (S314345), "Top 10 Oracle Features to Supercharge Your SQL" (S314673), "Building Software with Rich Client Platforms (NetBeans RCP and Eclipse RCP)" (S314711), and even another presentation on Clojure ["Clojure's Approach to Identity, State, and Concurrency" (S313914)].

In addition, there are other presentations at different times on Scala, on Clojure, and on JVM languages.  For example, Stephen Colebourne will be presenting on "Next Big Java Virtual Machine Language" (S314355) and Hamlet D'Arcy will be presenting on "Code Generation on the JVM."  Other related examples include "Polyglot Programming in the Java Virtual Machine (JVM)" (S314424), "Speedy Scripting: Productivity and Performance" (S314094), and "Multiple Languages, One Virtual Machine" (S314432).

There are numerous talks and other events at JavaOne 2010 focused on the growing set of languages other than Java itself that run on the JVM.  Although I use Groovy quite a bit, I look forward to learning more about Groovy and more about some of the other JVM languages.

Thứ Tư, 11 tháng 8, 2010

RMOUG Training Days 2010/2011 and Canceled JavaOne Presentation

The Rocky Mountain Oracle Users Group (RMOUG) has made the slides from Training Days 2010 available online.  Unfortunately, I made many changes and additions to the slides from my two presentations after the date for submitting them to the conference and neglected to send the updated versions in to be posted.  However, there are numerous other presentations from other presenters available on the presentations summary page.

RMOUG has also sent out an e-mail message calling for abstracts for presenting at RMOUG Training Days 2011.  I snapped a screenshot from that e-mail message to post here:


As this image from the e-mail message indicates, abstracts are due September 22, 2010.  RMOUG Training Days 2011 will be held 15-17 February, 2010, at the Colorado Convention Center.  The call for abstracts states that the conference organizers are "looking for presentations that celebrate the best of the old and the best of the new and emerging technologies."

The page linked to from the Submit link in that e-mail snippet is to the page Call For Papers


JavaOne 2010 Session Canceled

I was somewhat disappointed to learn via e-mail last week that a session I was looking forward to at JavaOne 2010 (and that was on my Schedule Builder) had been canceled.  Here is the most relevant snippet from the e-mail message I received:

Dear Oracle OpenWorld, JavaOne, and Oracle Develop Attendee,

We regret to inform you that the following session you are currently
enrolled in has been cancelled and removed from your schedule.

Session ID: S314089
Title: Java and HTML5: Boldly Combine
Date: 9/21/2010
Start Time: 9:30:00 AM

I was really looking forward to this presentation on using Java and HTML5 together.   The good news is that there are numerous sessions that interest me in every time slot.  Also, there are still sessions scheduled for JavaOne related to HTML5 and Java:
  • A Lean, RESTful Java Architecture for Building Rich HTML5 Web Applications (S314404)
  • The JSF 2.0 and HTML5 Version of Parleys.com  (S313804)
It is not surprising for a conference this size with this many presentations to have a few cancellations, but I always hope that it's presentations I was not going to attend anyway.  In this case, I was definitely planning on attending "Java and HTML5: Boldly Combine."


    Thứ Hai, 2 tháng 8, 2010

    Unconference at JavaOne 2010

    The Unconference concept has become very popular in recent years (Oracle offered its first OpenWorld Unconference at Oracle OpenWorld 2007).  The Unconferences for JavaOne 2010 and Oracle Develop 2010 are featured on this Wiki page.  There are numerous slots still available for anyone interested in organizing an Unconference topic, but several topics are already scheduled.  The Scheduled Sessions Descriptions page provides descriptions of a few of these.

    The Unconference sessions are being held in conjunction with JavaOne and Oracle Develop.  They will be held 20-23 September 2010 in the Hotel Parc 55 from 9 am to 5 pm on Monday through Wednesday (20-22 September) and from 9 am to noon on Thursday, 23 September.

    I blogged previously on how difficult it is to decide which presentations to attend at JavaOne.  It becomes even more difficult to decide what to attend during the conference when Oracle Develop sessions and the currently scheduled and potentially scheduled Unconference sessions are added into the mix.  The Oracle Wiki page What to Expect at the Unconference provides an idea of what to expect at an unconference.

    Screen Snapshots with Java's Robot

    One of the Java programming language's advantages that I really appreciate is its rich set of standard libraries in the SDK.  I use some of these all the time and use others less frequently, but still appreciate these less used ones when they are needed.  In this blog post, I look at a feature that I don't use often, but really appreciate when I do need it: the ability to take screen snapshots with Java's Robot class (this is the first post in which I'm using the nicer URLs with Oracle in the URL: http://download.oracle.com/javase/6/docs/api/java/awt/Robot.html).

    There are numerous online resources covering the Java Robot: Introduction to the Java Robot Class in Java, How to Use Robot Class in Java, Capture the Screen, Full Screen Capture with Java, How to Take Screen Shots in Java, and many, many more.  The main code behind the screen capture is fairly simple and is shown here without try/catch blocks and other "overhead":

    Main Java Code for Capturing Screen Snapshot
    final Dimension screenDimension = Toolkit.getDefaultToolkit().getScreenSize();
    final Rectangle screenRectangle = new Rectangle(screenDimension);
    final Robot robot = new Robot();
    final BufferedImage screenImage = robot.createScreenCapture(screenRectangle);
    ImageIO.write(screenImage, newFileFormat, new File(newFileName));

    I'll include a much lengthier code listing shortly that wraps this code with a "try" block, allows a delay to be set before taking the screen snapshot, and provides basic command-line user interaction. However, the "meat" of this is all available in the five lines shown above.

    I have named the full class JavaRobotExample. Its code listing is shown next.

    JavaRobotExample.java
    package dustin.examples;

    import java.awt.AWTException;
    import java.awt.Dimension;
    import java.awt.Rectangle;
    import java.awt.Robot;
    import java.awt.Toolkit;
    import java.awt.image.BufferedImage;
    import java.io.File;
    import java.io.IOException;
    import java.util.Arrays;
    import java.util.List;
    import javax.imageio.ImageIO;

    import static java.lang.System.err;
    import static java.lang.System.out;


    /**
    * Simple class demonstrating the general utility of the java.awt.Robot class
    * in capturing screen snapshots.
    *
    * @author Dustin
    */
    public class JavaRobotExample
    {
    /** Collection of informal file format names understood by registered readers. */
    private final static List<String> INFORMAL_FILE_FORMAT_NAMES;

    /** Operating system independent new line. */
    private final static String NEW_LINE = System.getProperty("line.separator");

    /** Default delay in milliseconds. */
    private final static int DEFAULT_DELAY_MS = 2500;

    /** Default filename base when no filename base is provided. */
    private final static String DEFAULT_FILE_NAME_BASE = "screenshot";

    /** Default file format for generated image file used when none is provided. */
    private final static String DEFAULT_INFORMAL_FILE_FORMAT_NAME;

    static
    {
    INFORMAL_FILE_FORMAT_NAMES = Arrays.asList(ImageIO.getReaderFormatNames());
    DEFAULT_INFORMAL_FILE_FORMAT_NAME =
    isFormatRecognized("png")
    ? "png"
    : provideSingleInformalFileFormatName();
    }

    /**
    * Capture snaphot of the current screen and write it to an output file with
    * the provided file name.
    *
    * @param newFileName Name of file (without prefix) to which screen snapshot
    * should be written.
    * @param newFileFormat The informal file format name to be used to specify
    * the format of the file the screen snapshot is written to.
    * @param newDelayInMs Delay (measured in milliseconds) that should be employed
    * before taking screen snapshot to allow screen adjustment (such as
    * closing the terminal in which this application is executed).
    */
    public static void captureScreenShot(
    final String newFileName, final String newFileFormat, final int newDelayInMs)
    {
    final Dimension screenDimension = Toolkit.getDefaultToolkit().getScreenSize();
    final Rectangle screenRectangle = new Rectangle(screenDimension);
    try
    {
    final Robot robot = new Robot();
    robot.delay(newDelayInMs);
    final BufferedImage screenImage = robot.createScreenCapture(screenRectangle);
    ImageIO.write(screenImage, newFileFormat, new File(newFileName));
    }
    catch (AWTException awtEx)
    {
    if (System.console() == null)
    {
    err.println("Not supported for headless console - " + awtEx.toString());
    }
    else
    {
    err.println("Not supported for this environment - " + awtEx.toString());
    }
    }
    catch (IOException ioEx)
    {
    err.println(
    "Unable to write screen shot to file " + newFileName + " - "
    + ioEx.toString());
    }
    }

    /** Print the supported informal file format names to standard output. */
    public static void printInformalFileFormatNames()
    {
    for (final String formatName : INFORMAL_FILE_FORMAT_NAMES)
    {
    out.println(formatName);
    }
    }

    /**
    * Provide a single informal file format name that is supported by my
    * readers.
    *
    * @return Single informal file format name support by my readers; may be
    * empty String if I don't have any valid informal file format names.
    */
    public static String provideSingleInformalFileFormatName()
    {
    return INFORMAL_FILE_FORMAT_NAMES != null && !INFORMAL_FILE_FORMAT_NAMES.isEmpty()
    ? INFORMAL_FILE_FORMAT_NAMES.get(0)
    : "";
    }

    /**
    * Indicates whether the provided String format represents a valid informal
    * file format name for my registered readers.
    *
    * @param candidateInformalFormatName String of possible file format.
    * @return {@code true} if the provided String represents a valid informal
    * file format name for my registered readers.
    */
    public static boolean isFormatRecognized(final String candidateInformalFormatName)
    {
    return INFORMAL_FILE_FORMAT_NAMES.contains(candidateInformalFormatName);
    }

    /**
    * Extracts file name and format information from the provided arguments,
    * assumed to have come from the command-line or other source. It is also
    * assumed that the provided array of Strings has two elements with the first
    * String representing the base of the file name (no extension) and the
    * second representing the file format. If either the base file name or the
    * format is not specified, defaults are used for both file name and format.
    *
    * @param arguments Strings with first String representing the base file name
    * (no extension) and the second String representing the file format.
    * @return Two Strings with the first String representing the file name to be
    * written to (without extension and may be the same as provided) and the
    * second String representing a valid informal file format name.
    */
    public static String[] extractFileNameAndFileFormat(final String[] arguments)
    {
    String fileNameBase;
    String fileFormat;
    if (arguments.length < 2)
    {
    out.println(
    "If specified arguments are to be used, both must be specified."
    + NEW_LINE);
    out.println(
    "Because either file name or file format was not specified, "
    + "defaults are used for both ('" + DEFAULT_FILE_NAME_BASE
    + "' for the generated base filename of file format "
    + DEFAULT_INFORMAL_FILE_FORMAT_NAME + " with delay of "
    + DEFAULT_DELAY_MS + ")." + NEW_LINE + NEW_LINE
    + "To explicitly specify them, provide them as command-line arguments:"
    + NEW_LINE
    + " dustin.examples.JavaRobotExample <<file_name_base>> <<file_format>> <<delay_ms>>"
    + NEW_LINE + NEW_LINE
    + "where file_name_base is name of generated image file without its suffix "
    + "and file_format is the image format ('PNG', 'GIF', 'JPG', etc.).");
    fileNameBase = DEFAULT_FILE_NAME_BASE;
    fileFormat = DEFAULT_INFORMAL_FILE_FORMAT_NAME;
    }
    else
    {
    fileNameBase = arguments[0];
    final String candidateFileFormat = arguments[1];
    fileFormat = isFormatRecognized(candidateFileFormat)
    ? candidateFileFormat
    : DEFAULT_INFORMAL_FILE_FORMAT_NAME;
    }
    return new String[] {fileNameBase, fileFormat};
    }

    /**
    * Extract the delay in milliseconds from the provided arguments.
    *
    * @param arguments Arguments, most likely from the command line, that are
    * expected to include the number of milliseconds of delay before screen
    * capture as the third argument.
    * @return The extract delay in milliseconds before screen capture should
    * take place.
    */
    public static int extractDelayInMilliseconds(final String[] arguments)
    {
    final int defaultDelayInMs = DEFAULT_DELAY_MS;
    int requestedDelayInMs;
    if (arguments.length > 2)
    {
    try
    {
    requestedDelayInMs = Integer.valueOf(arguments[2]);
    if (requestedDelayInMs < 0 || requestedDelayInMs > 60000)
    {
    requestedDelayInMs = defaultDelayInMs;
    err.println(
    "Specified delay of " + requestedDelayInMs
    + " is NOT between 0 and 60000 ms; setting to " + defaultDelayInMs);
    }
    }
    catch (NumberFormatException nfe)
    {
    requestedDelayInMs = defaultDelayInMs;
    err.println(arguments[2] + " is not a valid number of milliseconds for a delay.");
    }
    }
    else
    {
    requestedDelayInMs = defaultDelayInMs;
    }
    return requestedDelayInMs;
    }

    /**
    * Main executable method.
    *
    * @param arguments Command-line arguments: none expected.
    */
    public static void main(final String[] arguments)
    {
    final String[] fileNameAndFormat = extractFileNameAndFileFormat(arguments);
    final String fileFormat = fileNameAndFormat[1];
    final String fileName = fileNameAndFormat[0] + "." + fileFormat.toLowerCase();
    final int delayInMs = extractDelayInMilliseconds(arguments);
    captureScreenShot(fileName, fileFormat, delayInMs);
    }
    }

    The above code listing is for the complete class. When this class is built, its main() function can be run to take a screen snapshot. This is demonstrated in the next two images. The first image is a snapshot of the DOS terminal in which the Java class is being executed. The second image is the actual screenshot snapped by the Java application when I left the terminal open until the screen snapshot was taken.


    The image above shows that the defaults were used and the screen snapshot was captured as a file called screenshot.png.  The next image shows the actual captured screen shot:


    Without the delay, or if the console was not closed before the delay, the screen snapshot includes the console.  Because this is seldom desirable, the delay was added to give a sufficient amount of time to close the console and get it out of the picture.  When this is done, the screen snapshot taken appears like the example shown next (run with the format of JPG rather than PNG).


    With a 3 second delay (not shown here) I had plenty of time to minimize the console before the screen snapshot took place.

    Taking screen shots with Java is pretty cool, but the Robot class can do much more than that.  The Javadoc for the Robot class states: "The primary purpose of Robot is to facilitate automated testing of Java platform implementations."  The class supports this via screen snapshot and delay functionality as demonstrated here along with support for pressing a key (and releasing) and handling a mouse (move, press, release).