Thursday, January 5, 2012

Pair Class Coming to Java via JavaFX?

The pair class is familiar to those of us who have used C++ for any considerable length of time. Although there has been talk of adding it to Java as a standard part of the SDK, it is a somewhat controversial topic. Several folks have formally requested it and bugs have even been filed (4947273, 4983155, and 6229146) to get it in Java. In a post asking the question Do we want a java.util.Pair?, Alex Miller does a nice job of covering both sides of the issue.

There are already implementations of Pair or a Pair-like equivalent out there for Java. Besides the unknown number of custom ones in local code bases, there are publicly available examples such as those provided by the post Java Pair Class, examples provided in a StackOverflow thread, Ideograph's Generic Pair, and (no surprise) Java Tuples's Pair. The Android SDK also features a Pair class. The one that has surprised me the most is the existence of JavaFX 2.0's javafx.util.Pair class.

The package and class name most often proposed for an SDK version of the Pair class has been java.util.Pair and the JavaFX version is similar in package name: javafx.util.Pair. Running javap against this class in the JavaFX SDK, leads to the following output.

Compiled from "Pair.java"
public class javafx.util.Pair<K, V> {
  public K getKey();
  public V getValue();
  public javafx.util.Pair(K, V);
  public java.lang.String toString();
  public int hashCode();
  public boolean equals(java.lang.Object);
}

As the above javap output indicates, this is a relatively simple class with a basic parameterized constructor, "get" methods for the key and value portions of the Pair, and "common" methods toString(), equals(Object), and hashCode(). The next code listing demonstrates using the parameterized constructor to provide the key and value to each instance of Pair that is instantiated.

   /**
    * Provide a collection of famous pairs.
    * 
    * @return Collection of famous pairs.
    */
   private static Collection<Pair<String,String>> createFamousPairs()
   {
      final Collection<Pair<String,String>> pairs =
         new ArrayList<Pair<String,String>>();
      pairs.add(new Pair("Yin", "Yang"));
      pairs.add(new Pair("Action", "Reaction"));
      pairs.add(new Pair("Salt", "Pepper"));
      pairs.add(new Pair("Starsky", "Hutch"));
      pairs.add(new Pair("Fox", "Mulder"));
      pairs.add(new Pair("Batman", "Robin"));
      pairs.add(new Pair("Fred Astaire", "Ginger Rogers"));
      pairs.add(new Pair("Flotsam", "Jetsam"));
      pairs.add(new Pair("Brutus", "Nero"));
      pairs.add(new Pair("Tom", "Jerry"));
      pairs.add(new Pair("Jekyll", "Hyde"));
      pairs.add(new Pair("Holmes", "Watson"));
      pairs.add(new Pair("Mario", "Luigi"));
      pairs.add(new Pair("Pinky", "The Brain"));
      pairs.add(new Pair("Wallace", "Gromit"));
      return pairs;
   }

Accessing the key and value of each Pair is also easy as shown in the next code sample.

   /**
    * Write provided collection of pairs to standard output.
    * 
    * @param title Title for output written to standard output.
    * @param pairsToPrint Pairs to be written to standard output.
    */
   private static void writeCollectionOfPairs(
      final String title,
      final Collection<Pair<String,String>> pairsToPrint)
   {
      out.println(title + ":");
      for (final Pair<String,String> pair : pairsToPrint)
      {
         out.println("\t" + pair.getKey() + " and " + pair.getValue());
      }
   }

The above example is relatively contrived, but could be argued to be a most effective use of Pair because, in that particular example, it really is a "pair" concept being represented. One of the biggest complaints about adding Pair to the SDK or using it in general is that it is not named specific enough to cover the business purpose for an object's existence. I actually had thought about using the JavaFX Pair class when I wrote my Christmas Tree example for the post JavaFX 2.0 Christmas Tree (JavaFX 2.0 Shapes). I ended up deciding against this and used a more appropriately-named nested Coordinate class. However, I could have easily used Pair in that example. The next code listing contains that very example with the nested Coordinate class removed and references to it replaced by Pair.

package dustin.examples;

import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.effect.Glow;
import javafx.scene.input.MouseEvent;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.*;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.Pair;

/**
 * Simple example of using JavaFX 2.0's Path to create a simple Christmas tree.
 * 
 * @author Dustin
 */
public class ChristmasTreePathWithPair extends Application
{
   /** Number of branches on Christmas tree. */
   private final static int NUMBER_BRANCHES = 4;
   /** X-coordinate of very top of Christmas tree. */
   private final static int TOP_CENTER_X = 400;
   /** Y-coordinate of very top of Christmas tree. */
   private final static int TOP_CENTER_Y = 25;
   /** Horizontal distance to end of each branch. */
   private final static int DELTA_X = 125;
   /** Vertical distance to end of each branch. */
   private final static int DELTA_Y = 100;
   /** Length of each branch as measured on bottom of branch. */
   private final static int BRANCH_LENGTH = 75;
   /** Width of tree stump. */
   private final static int STUMP_WIDTH = 100;
   /** Height of tree stump. */
   private final static int STUMP_HEIGHT = 150;
   /** X-coordinate of top left corner of tree stump. */
   private final static int LEFT_STUMP_X = TOP_CENTER_X - STUMP_WIDTH/2;
   /** Y-coordinate of top left corner of tree stump. */
   private final static int LEFT_STUMP_Y = TOP_CENTER_Y + DELTA_Y * NUMBER_BRANCHES;
   /** Width of Christmas tree bottom. */
   private final static int TREE_BOTTOM_WIDTH = (DELTA_X-BRANCH_LENGTH) * NUMBER_BRANCHES * 2;

   /**
    * Draw left side of the Christmas tree from top to bottom.
    * 
    * @param path Path for left side of Christmas tree to be added to.
    * @param startingX X portion of the starting coordinate.
    * @param startingY Y portion of the starting coordinate.
    * @return Coordinate with x and y values.
    */
   private Pair<Integer, Integer> drawLeftSide(
      final Path path, final int startingX, final int startingY)
   {
      int coordX = startingX - DELTA_X;
      int coordY = startingY + DELTA_Y;
      final LineTo topLeft = new LineTo(coordX, coordY);
      path.getElements().add(topLeft);

      coordX += BRANCH_LENGTH;
      final LineTo topLeftHorizontal = new LineTo(coordX, coordY);
      path.getElements().add(topLeftHorizontal);

      coordX -= DELTA_X;
      coordY += DELTA_Y;
      final LineTo secondLeft = new LineTo(coordX, coordY);
      path.getElements().add(secondLeft);

      coordX += BRANCH_LENGTH;
      final LineTo secondLeftHorizontal = new LineTo(coordX, coordY);
      path.getElements().add(secondLeftHorizontal);

      coordX -= DELTA_X;
      coordY += DELTA_Y;
      final LineTo thirdLeft = new LineTo(coordX, coordY);
      path.getElements().add(thirdLeft);

      coordX += BRANCH_LENGTH;
      final LineTo thirdLeftHorizontal = new LineTo(coordX, coordY);
      path.getElements().add(thirdLeftHorizontal);

      coordX -= DELTA_X;
      coordY += DELTA_Y;
      final LineTo fourthLeft = new LineTo(coordX, coordY);
      path.getElements().add(fourthLeft);

      coordX += BRANCH_LENGTH;
      final LineTo fourthLeftHorizontal = new LineTo(coordX, coordY);
      path.getElements().add(fourthLeftHorizontal);

      return new Pair(coordX, coordY);
   }

   /**
    * Draw right side of the Christmas tree from bottom to top.
    * 
    * @param path Path for right side of Christmas tree to be added to.
    * @param startingX X portion of the starting coordinate.
    * @param startingY Y portion of the starting coordinate.
    * @return Coordinate with x and y values.
    */
   private Pair<Integer, Integer> drawRightSide(
      final Path path, final int startingX, final int startingY)
   {
      int coordX = startingX + BRANCH_LENGTH;
      int coordY = startingY;
      final LineTo bottomHorizontal = new LineTo(coordX, coordY);
      path.getElements().add(bottomHorizontal);

      coordX -= DELTA_X;
      coordY -= DELTA_Y;
      final LineTo bottomBranch = new LineTo(coordX, coordY);
      path.getElements().add(bottomBranch);

      coordX += BRANCH_LENGTH;
      final LineTo secondHorizontal = new LineTo(coordX, coordY);
      path.getElements().add(secondHorizontal);

      coordX -= DELTA_X;
      coordY -= DELTA_Y;
      final LineTo secondBottomBranch = new LineTo(coordX, coordY);
      path.getElements().add(secondBottomBranch);

      coordX += BRANCH_LENGTH;
      final LineTo thirdHorizontal = new LineTo(coordX, coordY);
      path.getElements().add(thirdHorizontal);

      coordX -= DELTA_X;
      coordY -= DELTA_Y;
      final LineTo thirdBottomBranch = new LineTo(coordX, coordY);
      path.getElements().add(thirdBottomBranch);

      coordX += BRANCH_LENGTH;
      final LineTo fourthHorizontal = new LineTo(coordX, coordY);
      path.getElements().add(fourthHorizontal);

      coordX -= DELTA_X;
      coordY -= DELTA_Y;
      final LineTo fourthBottomBranch = new LineTo(coordX, coordY);
      path.getElements().add(fourthBottomBranch);

      return new Pair(coordX, coordY);
   }

   /**
    * Draw stump of tree.
    * 
    * @return Path representing Christmas tree stump.
    */
   private Path buildStumpPath()
   {
      final Path path = new Path();

      int coordX = LEFT_STUMP_X;
      int coordY = LEFT_STUMP_Y;
      final MoveTo startingPoint = new MoveTo(coordX, coordY);
      path.getElements().add(startingPoint);

      coordY += STUMP_HEIGHT;
      final LineTo leftStumpSide = new LineTo(coordX, coordY);
      path.getElements().add(leftStumpSide);

      coordX += STUMP_WIDTH;
      final LineTo stumpBottom = new LineTo(coordX, coordY);
      path.getElements().add(stumpBottom);

      coordY -= STUMP_HEIGHT;
      final LineTo rightStumpSide = new LineTo(coordX, coordY);
      path.getElements().add(rightStumpSide);

      coordX -= STUMP_WIDTH;
      final LineTo topStump = new LineTo(coordX, coordY);
      path.getElements().add(topStump);

      path.setFill(Color.BROWN);

      return path;
   }

   /**
    * Build the exterior path of a Christmas Tree.
    * 
    * @return Path representing the exterior of a simple Christmas tree drawing.
    */
   private Path buildChristmasTreePath()
   {
      int coordX = TOP_CENTER_X;
      int coordY = TOP_CENTER_Y;
      final Path path = new Path();
      final MoveTo startingPoint = new MoveTo(coordX, coordY);
      path.getElements().add(startingPoint);

      final Pair<Integer, Integer> bottomLeft = drawLeftSide(path, coordX, coordY);
      coordX = bottomLeft.getKey() + TREE_BOTTOM_WIDTH;
      coordY = bottomLeft.getValue();

      final LineTo treeBottom = new LineTo(coordX, coordY);
      path.getElements().add(treeBottom);

      drawRightSide(path, coordX, coordY);

      path.setFill(Color.GREEN);

      return path;
   }

   /**
    * Create a bulb based on provided parameters and associate a MouseEvent to
    * it such that clicking on a bulb will increase its size and enable the glow
    * effect.
    * 
    * @param centerX X-coordinate of center of bulb.
    * @param centerY Y-coordinate of center of bulb.
    * @param radius Radius of bulb.
    * @param paint Paint/color instance to be used for bulb.
    * @return Christmas tree bulb with interactive support.
    */
   private Circle createInteractiveBulb(
      final int centerX, final int centerY, final int radius, final Paint paint)
   {
      final Circle bulb = new Circle(centerX, centerY, radius, paint);
      bulb.setOnMouseClicked(
         new EventHandler<MouseEvent>()  
         {
            @Override
            public void handle(MouseEvent mouseEvent)
            {
               bulb.setEffect(new Glow(1.0));
               bulb.setRadius(bulb.getRadius() + 5);
            }
         });
      return bulb;
   }

   /**
    * Add colored circles (bulbs) to the provided Group.
    * 
    * @param group Group to which 'bulbs' are to be added.
    */
   private void addBulbs(final Group group)
   {
      final Circle bulbOne = createInteractiveBulb(350,100,10, Color.RED);
      group.getChildren().add(bulbOne);
      final Circle bulbTwo = createInteractiveBulb(285,210,10, Color.YELLOW);
      group.getChildren().add(bulbTwo);
      final Circle bulbThree = createInteractiveBulb(325,300,10, Color.WHITE);
      group.getChildren().add(bulbThree);
      final Circle bulbFour = createInteractiveBulb(475,290,10, Color.BLUE);
      group.getChildren().add(bulbFour);
      final Circle bulbFive = createInteractiveBulb(380,150,10, Color.CADETBLUE);
      group.getChildren().add(bulbFive);
      final Circle bulbSix = createInteractiveBulb(550,390,10, Color.VIOLET);
      group.getChildren().add(bulbSix);
      final Circle bulbSeven = createInteractiveBulb(375,400,10, Color.GOLD);
      group.getChildren().add(bulbSeven);
      final Circle bulbEight = createInteractiveBulb(445,195,10, Color.SILVER);
      group.getChildren().add(bulbEight);
      final Circle bulbNine = createInteractiveBulb(220,385,10, Color.DARKSALMON);
      group.getChildren().add(bulbNine);
   }

   /**
    * Add text portions to Christmas Tree group.
    * 
    * @param group Group for text to be added to.
    */
   private void addText(final Group group)
   {
      final Text text1 = new Text(25, 125, "Merry\nChristmas!");  
      text1.setFill(Color.RED);  
      text1.setFont(Font.font(java.awt.Font.SERIF, 50));  
      group.getChildren().add(text1);
 
      final Text text2 = new Text(600, 150, "2011");
      text2.setFill(Color.DARKGREEN);
      text2.setFont(Font.font(java.awt.Font.SERIF, 75));
      group.getChildren().add(text2);
   }

   /**
    * Starting method of JavaFX application.
    * 
    * @param stage Primary stage.
    * @throws Exception Thrown for exceptional circumstances.
    */
   @Override
   public void start(final Stage stage) throws Exception
   {
      stage.setTitle("JavaFX 2.0: Christmas Tree 2011 (Pair)");
      final Group rootGroup = new Group();
      final Scene scene = new Scene(rootGroup, 800, 600, Color.WHITE);
      stage.setScene(scene);
      rootGroup.getChildren().add(buildChristmasTreePath());
      rootGroup.getChildren().add(buildStumpPath());
      addBulbs(rootGroup);
      addText(rootGroup);
      stage.show();
   }

   /**
    * Main function that kicks off this JavaFX demonstrative application.
    * 
    * @param arguments Command-line arguments; none expected.
    */
   public static void main(final String[] arguments)
   {
      Application.launch(arguments);
   }
}

Removing the nested Coordinate class reduces the overall lines of code for the application, but argument against this is that Pair is not as readable or specific as Coordinate was. This example exemplifies what I typically do: make simple custom classes rather than using a generic Pair.

What's perhaps most interesting to me about JavaFX having a Pair class is the implication of this when one considers that JavaFX will likely be standardized and made part of Java SE. This Pair class could very well end up in the Java SDK as-is. Other options would be to include a standard java.util.Pair class as a replacement for the JavaFX version, to have both in the SDK (having more than one in the SDK itself), or to not add the new one and remove the JavaFX version. There are ramifications on maintenance of existing applications either way. Several people have commented that it is a code smell or feels dirty to use a Pair rather than a custom object pairing two attributes. It would probably only make things feel even dirtier to use an implied JavaFX-specific class (javafx.util.Pair) in code that has nothing to do with JavaFX or even with presentation or user interface.

3 comments:

Laird Nelson said...

http://docs.oracle.com/javase/7/docs/api/java/util/AbstractMap.SimpleImmutableEntry.html

@DustinMarx said...

Laird,

Thanks for taking the time to post a comment.

Alex Miller, in the post I linked to above (Do we want a java.util.Pair?), briefly discusses the java.util.AbstractMap.SimpleImmutableEntry class as well as its nested mutable sibling java.util.AbstractMap.SimpleEntry as pointed out in a separate post, but he also reiterates the original post's assertion that they are "somewhat buried" and adds "are probably even less usefully named than Pair."

These two nested classes are definitely worth discussing in relationship to Java Pair implementations discussion, so thanks for highlighting them in this comment.

Dustin

@DustinMarx said...

It is interesting to see that the Ceylon language is getting Tuples support.