One day, I felt an urge to create this data structure, which is missing from Java.
This is the fundamental, cellular unit on which the tuple list is based.
package com.etretatlogiciels.collections; import java.io.Serializable; public class Tuple< K, V > implements Serializable { K key; V value; public Tuple( K key, V value ) { this.key = key; this.value = value; } public Tuple( Tuple< K, V > tuple ) { this.key = tuple.getKey(); this.value = tuple.getValue(); } public K getKey() { return this.key; } public V getValue() { return this.value; } /** * The importance of this method is to ensure that tuples, which are key-value * pairs, are examined for equality not by their object reference, but by their * content. Of course, for optimization sake, if the object references match, * then there's no need to compare the strings character by character. */ public boolean equals( Object o ) { if( !( o instanceof Tuple ) ) return false; @SuppressWarnings( "unchecked" ) Tuple< K, V > tuple = ( Tuple< K, V > ) o; K key = tuple.getKey(); V value = tuple.getValue(); return ( ( key == this.key && value == this.value ) || key.equals( this.key ) && value.equals( this.value ) ); } }
This is the implementation.
package com.etretatlogiciels.collections; import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import javax.xml.bind.annotation.adapters.XmlAdapter; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * This class sits atop Collection and uses ArrayList to create a collection of Tuples. * The motivation behind this is that we were using a Map to implement this, but it's * totally legitimate to have tuples with the same key, in fact, we were doing that * ourselves. That Map couldn't support this didn't really scream at us until testing * demonstrated it, a "Doh!" sort of moment. * * Therefore, this class is nothing more than a list of Tuples. There * may be duplicates, even duplicate identical by both their key and their value. * * @author Thomas Greger and Russell Bateman * @since August 2012 */ public class TupleList extends XmlAdapter< Element, TupleList > implements Collection< Tuple< String, String > >, Serializable { private ArrayList< Tuple< String, String > > list = new ArrayList< Tuple< String, String > >(); public TupleList() { } /** * This constructor names the field when serializing. Typically, it's called by a subclass and * the whole TupleList class is never used directly. * @param tagname */ public TupleList( String tagname ) { this.tagname = tagname; } /** * This is a special constructor created as shorthand for the JaxB serializer to use. * @param list array of tuples. */ public TupleList( ArrayList< Tuple< String, String > > list ) { addAll( list ); } @Override public int size() { return this.list.size(); } @Override public boolean isEmpty() { return this.list.size() == 0; } @Override public boolean contains( Object o ) { return list.contains( o ); } /** * Determine whether the [ key, value ] pair are present in the list. * * @param key the key string. * @param value the value string. * @return true/false that the tuple is present. */ public boolean contains( String key, String value ) { for( Tuple< String, String > v : list ) { if( v.getKey().equals( key ) && v.getValue().equals( value ) ) return true; } return false; } /** * Determine whether a tuple with the specified key is present in the list. * * @param key the key string. * @return true/false that the key is present. */ public boolean contains( String key ) { for( Tuple< String, String > v : list ) { if( v.getKey().equals( key ) ) return true; } return false; } /** * Determine whether the [ key, value ] pair in the passed tuple are present * in the list. * * @param tuple containing the key and value to match. * @return true/false that the tuple is present. */ public boolean contains( Tuple< String, String > tuple ) { String key = tuple.getKey(); String value = tuple.getValue(); return contains( key, value ); } @Override public Iterator< Tuple< String, String > > iterator() { return list.iterator(); } @Override public Object[] toArray() { return list.toArray(); } @Override public < T > T[] toArray( T[] a ) { return list.toArray( a ); } /** * This is to overcome the difficulty I have working with the two preceding methods. * @return a copy of our array of tuples. */ public ArrayList< Tuple< String, String > > toTupleArray() { ArrayList< Tuple< String, String > > array = new ArrayList< Tuple< String, String > >( this.size() ); for( Tuple< String, String > tuple : list ) array.add( new Tuple< String, String >( tuple ) ); return array; } /** * Add a new tuple to the list. * @param [ key, value ] pair to add. * @return true/false that it was added. */ @Override public boolean add( Tuple< String, String > e ) { return list.add( e ); } /** * Add a new tuple [ key, value ] pair to the list. * @param key the key string. * @param value the value string. * @return true/false that it was added. */ public boolean add( String key, String value ) { Tuple< String, String > tuple = new Tuple< String, String >( key, value ); return add( tuple ); } /** * Remove an existing tuple [ key, value ] pair to the list. * @param key the key string. * @param value the value string. * @return true/false that it was added. */ @Override public boolean remove( Object o ) { if( !( o instanceof Tuple ) ) return false; @SuppressWarnings( "unchecked" ) Tuple< String, String > t = ( Tuple< String, String > ) o; return list.remove( t ); } /** * Remove an existing tuple [ key, value ] pair to the list. * @param key the key string. * @param value the value string. * @return true/false that it was added. */ public boolean remove( String key, String value ) { Tuple< String, String > tuple = new Tuple< String, String >( key, value ); return list.remove( tuple ); } @Override public boolean containsAll( Collection< ? > c ) { return list.containsAll( c ); } @Override public boolean addAll( Collection< ? extends Tuple< String, String > > c ) { return list.addAll( c ); } @Override public boolean removeAll( Collection< ? > c ) { return list.removeAll( c ); } @Override public boolean retainAll( Collection< ? > c ) { return list.retainAll( c ); } @Override public void clear() { list.clear(); } public boolean equals( TupleList tuplelist ) { if( tuplelist == this ) return true; if( tuplelist.size() != this.size() ) return false; // find every tuple in this object in the tuplelist passed or else! for( Tuple< String, String > tuple : list ) { boolean found = false; ArrayList< Tuple< String, String > > thatlist = tuplelist.toTupleArray(); for( Tuple< String, String > that : thatlist ) { if( that.equals( tuple ) ) found = true; } if( !found ) return false; } return true; } public String toString() { if( this.size() < 1 ) return ""; StringBuffer buffer = new StringBuffer( this.size() * 7 * 3 ); buffer.append( "{<n" ); int count = this.size(); for( Tuple< String, String > tuple : list ) { buffer.append( " \"" + tuple.getKey() + "\"" ); buffer.append( ":\"" + tuple.getValue() + "\"" ); if( count -- > 1 ) buffer.append( "," ); buffer.append( "\n" ); } buffer.append( "}" ); return buffer.toString(); } /** * Returns all tuples that with the indicated key. The point to this class in * the first place is that there can be any tuples including tuples with the same * key or even the same key and value. * @param key to search for. * @return list of matching tuples. */ public ArrayList< Tuple< String, String > > getAll( String key ) { ArrayList< Tuple< String, String > > list = new ArrayList< Tuple< String, String > >(); for( Tuple< String, String > tuple : this.list ) { if( tuple.key.equals( key ) ) list.add( tuple ); } return list; } /** * This returns the first tuple found with the specified key. There may be others, * but there is no order maintained so which value comes back cannot be predicted. * @param key to search for. * @return value associated with the key found. */ public String getFirst( String key ) { ArrayList< Tuple< String, String > > list = getAll( key ); if( list == null || list.size() < 1 ) return null; return list.get( 0 ).getValue(); } /* ========== J A X B s e r i a l i z a t i o n c o d e ======================================== * This enables us to make use of TupleList through Jersey. Originally, I got this code * for using Map/HashMap from Blaise Doughan, team lead for EclipseLink; see * http://stackoverflow.com/questions/11353790/serializer-for-hashmaps-for-jersey-use. */ private static boolean initDocumentBuilder = false; private DocumentBuilder builder = null; private String tagname = "tuplelist"; // generic name; subclass this public final String getTagname() { return this.tagname; } private void initDocumentBuilder() { try { builder = DocumentBuilderFactory.newInstance().newDocumentBuilder(); initDocumentBuilder = true; } catch( Exception e ) { e.printStackTrace(); } } @Override public Element marshal( TupleList tuplelist ) throws Exception { if( !initDocumentBuilder ) initDocumentBuilder(); Document document = builder.newDocument(); Element rootElement = document.createElement( tagname ); document.appendChild( rootElement ); ArrayList< Tuple< String, String > > list = tuplelist.toTupleArray(); for( Tuple< String, String > tuple : list ) { Element childElement = document.createElement( tuple.getKey() ); childElement.setTextContent( tuple.getValue() ); rootElement.appendChild( childElement ); } return rootElement; } @Override public TupleList unmarshal( Element rootElement ) throws Exception { NodeList nodeList = rootElement.getChildNodes(); ArrayList< Tuple< String, String > > list = new ArrayList< Tuple< String, String > >( nodeList.getLength() ); /* Pre-allocate the array we'll use in the TupleList; fill it using the child nodes * of the in-coming Element. */ for( int x = 0; x < nodeList.getLength(); x++ ) { Node node = nodeList.item( x ); if( node.getNodeType() == Node.ELEMENT_NODE ) list.add( new Tuple< String, String >( node.getNodeName(), node.getTextContent() ) ); } this.list = list; return this; } }
...and a test for it.
package com.etretatlogiciels.collections; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertNotNull; import java.util.ArrayList; import java.util.Iterator; import org.junit.Test; public class TupleListTest { private static final String SMACK_NAME = "Smackaroonie"; private static final String SHIZL_NAME = "Schizzle"; private static final String SHICK_NAME = "Schnikies"; private static TupleList SMACK = new TupleList( "smackaroonie" ); private static TupleList SHICK = new TupleList( "smackaroonie" ); static { SMACK.add( "companyname", SMACK_NAME ); SMACK.add( SHIZL_NAME, "write" ); SMACK.add( SHIZL_NAME, "password" ); SMACK.add( SHICK_NAME, "read" ); SMACK.add( SHICK_NAME, "password" ); SHICK.add( SHICK_NAME, "Schnikies" ); SHICK.add( SMACK_NAME, "read" ); SHICK.add( SMACK_NAME, "password" ); } @Test public void testTupleList_arrayconstructor() { ArrayList< Tuple< String, String > > list = new ArrayList< Tuple< String, String > >(); list.add( new Tuple< String, String >( "companyname", SMACK_NAME ) ); list.add( new Tuple< String, String >( SHIZL_NAME, "write" ) ); list.add( new Tuple< String, String >( SHIZL_NAME, "password" ) ); list.add( new Tuple< String, String >( SHICK_NAME, "read" ) ); list.add( new Tuple< String, String >( SHICK_NAME, "password" ) ); TupleList s = new TupleList( list ); assertTrue( SMACK.containsAll( s.toTupleArray() ) ); } @Test public void testTupleList_tagnameconstructor() { TupleList s = new TupleList( "smackaroonie" ); assertTrue( s.getTagname().equals( "smackaroonie" ) ); } @Test public void testSize() { assertEquals( SMACK.size(), 5 ); } @Test public void testIsEmpty() { TupleList s = new TupleList(); assertTrue( "This one is not empty", SMACK.size() != 0 ); assertFalse( "This one is empty", s.size() != 0 ); } @Test public void testContains_object() { Tuple< String, String > tuple = new Tuple< String, String >( "companyname", SMACK_NAME ); assertTrue( SMACK.contains( ( Object ) tuple ) ); } @Test public void testContains_keyvaluepair() { assertTrue( SMACK.contains( "companyname", SMACK_NAME ) ); } @Test public void testContains_key() { assertTrue( SMACK.contains( "companyname" ) ); } @Test public void testContains_tuple() { Tuple< String, String > tuple = new Tuple< String, String >( "companyname", SMACK_NAME ); assertTrue( SMACK.contains( tuple ) ); } @Test public void testIterator() { Iterator< Tuple< String, String > > iterator = SMACK.iterator(); assertNotNull( iterator ); while( iterator.hasNext() ) { Tuple< String, String > tuple = iterator.next(); assertNotNull( tuple ); } } @Test public void testToArray() { Object[] object = SMACK.toArray(); assertTrue( object.length == SMACK.size() ); } @Test public void testToArray_tarray() { @SuppressWarnings( "unchecked" ) Tuple< String, String >[] list = new Tuple[ SMACK.size() ]; list = SMACK.toArray( list ); int length = list.length; assertTrue( length == SMACK.size() ); TupleList s = new TupleList( SMACK.toTupleArray() ); ArrayList< Tuple< String, String > > tuples = new ArrayList< Tuple< String, String > >( length ); for( int i = 0; i < length; i++ ) tuples.add( new Tuple< String, String >( list[ i ].key, list[ i ].value ) ); assertTrue( s.removeAll( tuples ) ); assertTrue( s.size() == 0 ); } @Test public void testToTupleArray() { ArrayList< Tuple< String, String > > list = SMACK.toTupleArray(); assertTrue( list.size() == SMACK.size() ); } @Test public void testAdd_tuple() { Tuple< String, String > tuple = new Tuple< String, String >( "this", "and that" ); TupleList s = new TupleList(); assertTrue( s.add( tuple ) ); assertTrue( s.size() == 1 ); } @Test public void testAdd_keyvaluepair() { TupleList s = new TupleList(); assertTrue( s.add( "this", "and that" ) ); assertTrue( s.size() == 1 ); } @Test public void testRemove_object() { Tuple< String, String > tuple = new Tuple< String, String >( "companyname", SMACK_NAME ); TupleList s = new TupleList( SMACK.toTupleArray() ); s.remove( ( Object ) tuple ); assertTrue( s.size() == SMACK.size() - 1 ); } @Test public void testRemove_keyvaluepair() { TupleList s = new TupleList( SMACK.toTupleArray() ); s.remove( "companyname", SMACK_NAME ); assertTrue( s.size() == SMACK.size() - 1 ); } @Test public void testContainsAll() { TupleList s = new TupleList( SMACK.toTupleArray() ); assertTrue( SMACK.containsAll( s.toTupleArray() ) ); } @Test public void testAddAll() { TupleList s = new TupleList( SMACK.toTupleArray() ); assertTrue( s.addAll( SHICK.toTupleArray() ) ); } @Test public void testRemoveAll() { TupleList s = new TupleList( SMACK.toTupleArray() ); s.removeAll( SMACK.toTupleArray() ); assertTrue( s.isEmpty() ); } @Test public void testRetainAll() { Tuple< String, String > tuple = new Tuple< String, String >( "companyname", SMACK_NAME ); ArrayList< Tuple< String, String > > list = SMACK.toTupleArray(); // remove one of the tuples... list.remove( tuple ); TupleList s = new TupleList( SMACK.toTupleArray() ); // retain from the origin SMACK list only the tuples present in the list (from which we removed one)... s.retainAll( list ); assertTrue( SMACK.size() - 1 == s.size() ); } @Test public void testClear() { TupleList s = new TupleList( SMACK.toTupleArray() ); s.clear(); assertTrue( s.size() == 0 ); } @Test public void testEquals() { TupleList s = new TupleList( SMACK.toTupleArray() ); assertTrue( s.equals( SMACK ) ); } @Test public void testToString() { System.out.println( SMACK.toString() ); } @Test public void testGetAll() { TupleList s = new TupleList( SMACK.toTupleArray() ); ArrayList< Tuple< String, String > > list = s.getAll( SHICK_NAME ); int length = s.size(); assertTrue( list.size() == 2 ); // should have found 2 tuples... assertTrue( s.removeAll( list ) ); // should modify the internal list... assertTrue( s.size() == length - 2 ); // should have matched (and removed) the 2 tuples... } @Test public void testGetFirst() { String value = SMACK.getFirst( "companyname" ); assertNotNull( value ); assertTrue( value.equals( SMACK_NAME ) ); } }