Monday, July 23, 2007

Collections deepEquals()

There was some discussion on Eamonn McManus's blog (java.net) about Arrays.deepEquals() and a collections version. http://weblogs.java.net/blog/emcmanus/archive/2007/07/comparing_objec.html

I've decided to construct a first attempt at this. The idea is that two lists are deepEqual if the elements in them are equal. Of course the elements in a list can be other lists and so on. This applies to Sets and Maps also. This code attempts to be generic enough to compare two collections of the same type for deep equality. This is only a first attempt I'm sure there can be some enhancements/optimizations made.


NOTE: It does not (yet) handle collections that include themselves.


import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;


public class DeepEqualsTest {

/**
* @param args
*/
public static void main(String args[]) {

ArrayList<Object> stuff1 = new ArrayList<Object>();
stuff1.add(null);
stuff1.add("A");
stuff1.add(new Object[] { "B", "C", null });

ArrayList<Object> stuff2 = new ArrayList<Object>();
stuff2.add(null);
stuff2.add("A");
stuff2.add(new Object[] { "B", "C", null });

TreeSet<Integer> ts1 = new TreeSet<Integer>();
ts1.add(1);
ts1.add(3);
ts1.add(2);

TreeSet<Integer> ts2 = new TreeSet<Integer>();
ts2.add(1);
ts2.add(2);
ts2.add(3);
// ts2.add(4);

HashMap<String, Object> map1 = new HashMap<String, Object>();
HashMap<String, Object> map2 = new HashMap<String, Object>();

map1.put("key1", new Object[] { ts1, "hi", 123, 456 });
map2.put("key1", new Object[] { ts2, "hi", 123, 456 });

stuff1.add(map1);
stuff2.add(map2);

System.out.println("Result = " + deepEquals(stuff1, stuff2));
}

/**
* An implementation of "deep equals" for collection classes, built in the
* Arrays.deepEquals() style. It attempts to compare equality based on the
* contents of the collection.
*
* @param t1 -
* first object, most likely a collection of some sort.
* @param t2 -
* second object, most likely a collection of some sort.
* @return - true if the content of the collections are equal.
*/
public static <T> boolean deepEquals(T t1, T t2) {

if (t1 == t2) {
return true;
}

if (t1 == null || t2 == null) {
return false;
}

if (t1 instanceof Map && t2 instanceof Map) {
return mapDeepEquals((Map<?, ?>) t1, (Map<?, ?>) t2);
} else if (t1 instanceof List && t2 instanceof List) {
return linearDeepEquals((List<?>) t1, (List<?>) t2);

} else if (t1 instanceof Set && t2 instanceof Set) {
return linearDeepEquals((Set<?>) t1, (Set<?>) t2);

} else if (t1 instanceof Object[] && t2 instanceof Object[]) {
return linearDeepEquals((Object[]) t1, (Object[]) t2);

} else {
return t1.equals(t2);
}
}

/**
* Compares two maps for equality. This is based around the idea that if the
* keys are deep equal and the values the keys return are deep equal then
* the maps are equal.
*
* @param m1 -
* first map
* @param m2 -
* second map
* @return - weather the maps are deep equal
*/
private static boolean mapDeepEquals(Map<?, ?> m1, Map<?, ?> m2) {
if (m1.size() != m1.size()) {
return false;
}

Set<?> allKeys = m1.keySet();
if (!linearDeepEquals(allKeys, m2.keySet())) {
return false;
}

for (Object key : allKeys) {
if (!deepEquals(m1.get(key), m2.get(key))) {
return false;
}
}
return true;
}

/**
* Compares two Collections for deep equality.
*
* @param s1
* @param s2
* @return
*/
private static boolean linearDeepEquals(Collection<?> s1, Collection<?> s2) {
if (s1.size() != s2.size()) {
return false;
}

for (Object s1Item : s1) {
boolean found = false;
for (Object s2Item : s2) {
if (deepEquals(s2Item, s1Item)) {
found = true;
break;
}
}
if (!found) {
return false;
}
}
return true;
}

/**
* Compares two Object[] for deep equality
*
* @param s1
* @param s2
* @return
*/
private static boolean linearDeepEquals(Object[] s1, Object[] s2) {

if (s1.length != s2.length) {
return false;
}

for (Object s1Item : s1) {
boolean found = false;
for (Object s2Item : s2) {
if (deepEquals(s2Item, s1Item)) {
found = true;
break;
}
}
if (!found) {
return false;
}
}
return true;
}
}

Wednesday, July 18, 2007

Reworking the reworking of the Icon demo

Java.net posted an article about the rewriting of some of the demo code in the java tutorial.

http://blogs.sun.com/thejavatutorials/entry/reworking_the_icondemo

I was not pleased with the overall quality of this "reworked" demo code. It turns out I'm not alone. Not to be one to criticize and not contribute I am posting my own revision. I'm sure someone can pick my code apart too, but I feel it's better then what sun has provided.

As far as I'm concerned demo code should do three things:
  1. Focus on the topic
  2. Be informative
  3. Exemplify best practices
To that effect I redesigned the demo a little bit. Instead of "Next" and "Previous" buttons I build a toolbar that has thumbnails of each image. This gives us the opportunity to make more Icons, which of course is the point. I had debated removing the background SwingWorker that loads the images. It just doesn't seem to be necessary to talk about that in the icon demo. In the end the "best practice" idea won out and I built a new SwingWorker that populates the buttons and creates the images and the thumbnails. Some of you more experienced Swing programmers may notice I call getScaledInstance() to create the thumbnail. I am aware that Chris Campbell has a blog entry called The Perils of Image.getScaledInstance(). In this demo though we are not resizing an image in a paint method that will be called lots of times, we do it one per image in a background thread. Performances wise I don't think it's an issue.

I have to admit I'm new to the JDK6 version of SwingWorker. I don't really like the use of Void in the parameters because I have to return null at the end of doInBackground(). The thing is all the work is done in the calls to process.

Also I've never done a webstart application so I'm not sure if I removed anything important to webstart.

All in all I think my demo more readable and concise. With spacing and javadoc comments it weighs in around 180 lines. The original was about 350 lines of code, and had almost no comments. So here is the code in all it's glory. I would gladly accept any feedback people have on how to make it even more clear for a beginner.



import java.awt.BorderLayout;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JToolBar;
import javax.swing.SwingUtilities;
import javax.swing.SwingWorker;

/**
 * Reworking of the IconDemoApp from the java tutorial.
 *
 * IconDemoApp.java requires the following files: <br>
 * The following files are copyright 2006 spriggs.net and licenced under a
 * Creative Commons Licence (http://creativecommons.org/licenses/by-sa/3.0/)
 * <br>
 * images/sunw01.jpg <br>
 * images/sunw02.jpg <br>
 * images/sunw03.jpg <br>
 * images/sunw04.jpg <br>
 * images/sunw05.jpg <br>
 */
public class IconDemoApp extends JFrame {

    /**
     * Hashmap to store the icons used in this example. The goal is to only load
     * them from the disk once.
     */
    private HashMap<String, ImageIcon> iconCache;
    private HashMap<String, ImageIcon> thumbnailCache;

    private JLabel photographLabel;
    private JToolBar buttonBar = new JToolBar();

    private String imagedir = "images/";

    /**
     * List of all the descriptions of the image files. These correspond one to
     * one with the image file names
     */
    private String[] imageCaptions = { "Original SUNW Logo", "The Clocktower",
            "Clocktower from the West", "The Mansion", "Sun Auditorium" };

    /**
     * List of all the image files to load.
     */
    private String[] imageFileNames = { "sunw01.jpg", "sunw02.jpg",
            "sunw03.jpg", "sunw04.jpg", "sunw05.jpg" };

    /**
     * Main entry point to the demo. Loads the Swing elements on the "Event
     * Dispatch Thread".
     *
     * @param args
     */
    public static void main(String args[]) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                IconDemoApp app = new IconDemoApp();
                app.setVisible(true);
            }
        });
    }

    /**
     * Default constructor for the demo.
     */
    public IconDemoApp() {
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setTitle("Icon Demo: Please Select an Image");

        // A label for displaying the pictures
        photographLabel = new JLabel();
        photographLabel.setVerticalTextPosition(JLabel.BOTTOM);
        photographLabel.setHorizontalTextPosition(JLabel.CENTER);
        photographLabel.setHorizontalAlignment(JLabel.CENTER);
        photographLabel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));

        // we add glue on two sides so that when we add stuff in between later
        // it will be centered
        buttonBar.add(Box.createGlue());
        buttonBar.add(Box.createGlue());

        add(buttonBar, BorderLayout.SOUTH);
        add(photographLabel, BorderLayout.CENTER);

        setSize(400, 300);
        // this centers the frame on the screen
        setLocationRelativeTo(null);

        loadimages.execute();
    }

    /**
     * SwingWorker class that loads the images a background thread and published
     * when a new one is ready to be displayed.
     *
     * We use Void as the first SwingWroker param as we do not need to return anything from do in background.
     */
    private SwingWorker<Void, String> loadimages = new SwingWorker<Void, String>() {

        /**
         * Creates full size and thumbnail versions of the target image files.
         */
        @Override
        protected Void doInBackground()
                throws Exception {

            iconCache = new HashMap<String, ImageIcon>();
            thumbnailCache = new HashMap<String, ImageIcon>();

            for (int i = 0; i < imageCaptions.length; i++) {
                ImageIcon icon;
                icon = new ImageIcon(IconDemoApp.class
                        .getResource((imagedir + imageFileNames[i])));
                iconCache.put(imageCaptions[i], icon);
                thumbnailCache.put(imageCaptions[i], new ImageIcon(icon
                        .getImage().getScaledInstance(32, 32,
                                Image.SCALE_AREA_AVERAGING)));
                publish(imageCaptions[i]);
            }
            // unfortunately we must return something, and only null is valid to return when the return type is void.
            return null;
        }

        /**
         * Process all loaded images.
         */
        @Override
        protected void process(List<String> chunks) {
            for (String caption : chunks) {
                JButton thumbButton = new JButton(thumbnailCache.get(caption));
                thumbButton.setToolTipText(caption);
                thumbButton.addActionListener(new ThumbnailActionListener(
                        caption));
                // add the new button BEFORE the last glue
                // this centers the buttons in the toolbar
                buttonBar.add(thumbButton, buttonBar.getComponentCount() - 1);
            }
        }
    };

    /**
     * changes the currently displayed image
     *
     * @param caption -
     *            the caption is also the key
     */
    private void setCurrentImage(String caption) {
        photographLabel.setIcon(iconCache.get(caption));
        photographLabel.setToolTipText(caption);
        setTitle("Icon Demo: " + caption);
    }

    /**
     * Action listener that shows the image specified in it's constructor. For
     * use on the thumbnail buttons.
     */
    private class ThumbnailActionListener implements ActionListener {
        private String imageCaption;

        public ThumbnailActionListener(String caption) {
            imageCaption = caption;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            setCurrentImage(imageCaption);
        }
    };

}