Home | FAQ | Contact me

String stuff in Java

Here's rambling on about String stuff in Java.

String concatenation

There are some real problems with string concatenation in Java despite the oh-so-attractive += operator.

For one, the following code:

String s = null; s += "The cat in the hat"; System.out.println( s );

...yields the output:

null The cat in the hat

This is (or might be to some of us) a surprising by-product when we really expected "The cat in the hat".

Next, using the += operator to concatenate two strings creates a large number of temporary objects, since String is immutable. This can lead to poor performance (higher CPU utilization) since the garbage collector finds it has additional objects to collect. Use StringBuffer or StringBuilder to concatenate strings because they are more efficient.

String s = "Jimmy crack corn"; String s2 = "and I don't care!"; StringBuffer buf = new StringBuffer(); buf.append( s ); buf.append( "..." ); buf.append( s2 ); System.out.println( buf.toString() );
StringBuilder builder = new StringBuilder(); builder.append( "Jimmy crack corn" ); builder.append( "..." ); builder.append( "and I don't care!" ); System.out.println( builder.toString() );

Both examples reach the output below. The reason to use StringBuilder instead of StringBuffer is that it's faster and more efficient in its operations (although, unlike StringBuffer, it isn't also thread-safe).

Jimmy crack corn...and I don't care!

In addition, both these classes present numerous useful interfaces for inserting, deleting, indexing and other operations on a string.

String.format()

Here's a little utility for storing and making use of SQL statements stored in a properties file, e.g.:.

mysql.query.basic.select = SELECT * FROM %1$s mysql.query.basic.select2 = SELECT * FROM %1$s %3$s %2$s %4$s

It illustrates using String.format().

package com.etretatlogiciels.strings.util;

import java.util.MissingFormatArgumentException;
import java.util.MissingResourceException;
import java.util.ResourceBundle;

import org.apache.log4j.Logger;

/**
 * Manages SQL statements or fragments in a properties file, usually corresponding
 * to the consuming class. Use Java 1.5 String.format() conversion specifiers.
 *
 * This utility is a little bit over-kill for what we usually need, but it's an
 * interesting exercise. There are ways using fake locale to enhance the properties
 * filename to carry dialect, but we opted instead to put the dialect information as
 * a prefix on each statement. Thus, there is only one properties file and all
 * statements are repeated therein once for each dialect handled.
 */
public class JdbcStatementUtil
{
	private static Logger	log = Logger.getLogger( JdbcStatementUtil.class );

	String			dialect    = null;
	String			bundlePath = null;
	ResourceBundle	bundle     = null;

	// dialects...
	public static final String	MYSQL     = "mysql";
	public static final String	POSTGRES  = "postgres";
	public static final String	ORACLE    = "oracle";
	public static final String	MICROSOFT = "mssql";

	public JdbcStatementUtil() { /* no-argument constructor for testing... */ }

	/**
	 * Call this to set up getting queries from the appropriate dialect properties
	 * file. The statement to get the resource bundle should be examined as a guide
	 * for creating dialects and properties filenames.
	 *
	 * @param dialect "mysql", "postgres", "oracle", etc.
	 */
	public JdbcStatementUtil( Class< ? > clazz, String dialect )
	{
		String	path = clazz.getName();

		this.dialect    = canonicalizeDialect( dialect );
		this.bundlePath = path;

		log.debug( "New " + this.bundlePath + " speaking " + this.dialect );
	}

	/**
	 * Call this to set up getting queries from the appropriate dialect properties
	 * file. The statement to get the resource bundle should be examined as a guide
	 * for creating dialects and properties filenames.
	 *
	 * Use preferably JdbcUtil( Class< ? > class, String dialect ).
	 *
	 * @param dialect "mysql", "postgres", "oracle", etc.
	 */
	public JdbcStatementUtil( String classname, String dialect )
	{
		this.dialect    = canonicalizeDialect( dialect );
		this.bundlePath = classname;

		log.debug( "New " + this.bundlePath + " speaking " + this.dialect );
	}

	private final String canonicalizeDialect( String dialect )
	{
		if( dialect.equalsIgnoreCase( MYSQL ) )
			return MYSQL;
		else if( dialect.equalsIgnoreCase( POSTGRES ) )
			return POSTGRES;
		else if( dialect.equalsIgnoreCase( ORACLE ) )
			return ORACLE;
		else if( dialect.equalsIgnoreCase( MICROSOFT ) )
			return MICROSOFT;
		return "unknown";
	}

	/**
	 * Funnel all the calls to ResourceBundle through here.
	 *
	 * @return true or false that the bundle (properties file) could be opened.
	 * @exception MissingResourceException if no resource bundle for the specified base
	 * 				name can be found
	 */
	private final boolean gotBundle()
	{
		if( this.bundle == null )
			this.bundle = ResourceBundle.getBundle( this.bundlePath );
		return( this.bundle != null );
	}

	// these aren't too compelling, but were useful in debugging...
	public String			getDialect()	{ return this.dialect; }
	public String			getBundlePath()	{ return this.bundlePath; }
	public ResourceBundle	getBundle()		{ return this.bundle; }

	/**
	 * Prefixes appropriately for type of database (dialect) and fetches the
	 * statement corresponding to key in the bundle.
	 *
	 * @param key - key to the desired SQL statement.
	 * @return the SQL statement if it exists (or null).
	 * @exception MissingResourceException if no object for the given key can be found
	 * @throws NullPointerException if key is null
	 */
	public final String getStatement( String key ) throws NullPointerException
	{
		if( !gotBundle() )
			return null;

		if( key == null )
			throw new NullPointerException( "Key cannot be null" + this.getClass().getName() );

		String	k = this.dialect + "." + key;

		log.debug( "Fetching statement \"" + k + "\" from " + this.bundlePath );

		return this.bundle.getString( this.dialect + "." + key );
	}

	/**
	 * Prefixes appropriately for type of database (dialect), fetches the
	 * statement corresponding to key in the bundle, and directly
	 * builds the statement doing conversion specifier replacement with the
	 * passed in arguments.
	 *
	 * @param key - key to the desired SQL statement.
	 * @param objects - arguments for each presumed conversion specifier.
	 * @return the SQL statement if it exists (or null).
	 * @throws NullPointerException if key is null
	 * @exception MissingResourceException if no object for the given key can be found
	 * @exception MissingFormatArgumentException if objects do not correspond to
	 * 				conversion specifiers in the statement corresponding to key
	 */
	public final String buildStatement( String key, Object...objects )
	{
		return String.format( this.getStatement( key ), objects );
	}

	public static void main( String[] args )
	{
		JdbcStatementUtil	jdbcUtil = new JdbcStatementUtil();

		jdbcUtil = new JdbcStatementUtil( jdbcUtil.getClass(), MYSQL );

		if( jdbcUtil != null )
		{
			try
			{
				String	key  = "query.basic.select";
				String	stmt = jdbcUtil.getStatement( key );

				System.out.println( "Fetching statement \"" + key + "\" from " + jdbcUtil.bundlePath );
				System.out.println( stmt );

				String	TABLE_NAME = "titles";
				String	query      = String.format( stmt, TABLE_NAME );
				System.out.println( "Resulting query is \"" + query + "\"" );
				// output should be "SELECT * FROM titles"

				System.out.println( "Round 2: some advanced stuff using buildStatement()..." );

				key   = "query.basic.select2";
				query = jdbcUtil.buildStatement( key, "This", "a", "is", "test" );
				System.out.println( "Resulting query is \"" + query + "\"" );
				// output should be "SELECT * FROM This is a test"
			}
			catch( MissingResourceException e )
			{
				String	path = ( jdbcUtil != null ) ? jdbcUtil.getBundlePath() : "(unknown)";
				System.out.println( "MissingResourceException: The properties file " + path + " is missing!" );
			}
		}
	}
}