I'm glad that static typing is there to help... 13

Posted by Brett Schuchert Sat, 25 Oct 2008 10:24:00 GMT

The Background

A colleague was using FitNesse to create a general fixture for setting values in various objects rendered from a DTD. Of course you can write one per top level object, but given the number of eventual end-points, this would require a bit too much manual coding.

This sounds like a candidate for reflection, correct? Yep, but rather than do that manually, using the Jakarta Commons BeanUtils makes sense – it’s a pretty handy library to be familiar with if you’re ever doing reflective programming with attributes.

So far, so good. However, the auto-generated classes included one-to-many relationships represented with arrays. Before getting any further, let’s define this problem (at least partially) with a test:
package com.objectmentor.arraycopyexample;

import static org.junit.Assert.assertEquals;
import org.junit.Test;

public class ArrayPropertySetterTest {
   @Test
   public void assertCanAssignToArrayFieldFromArrayOfObject() {
      Object[] arrayOfBars = createArrayOfBars();
      Foo foo = new Foo();

      ArrayPropertySetter.assignToArrayFieldFromObjectArray(foo, "bars", arrayOfBars);

      assertEquals(3, foo.getBars().length);
   }

   private Object[] createArrayOfBars() {
      Object[] objectArray = new Object[3];
      for (int i = 0; i < objectArray.length; ++i)
         objectArray[i] = new Bar();

      return objectArray;
   }
}
For completeness, you’ll need to see the Foo and Bar classes:

Bar

package com.objectmentor.arraycopyexample;

public class Bar {
}

Foo

package com.objectmentor.arraycopyexample;

public class Foo {
    Bar[] bars;

    public Bar[] getBars() {
        return bars;
    }

    public void setBars(Bar[] bars) {
        this.bars = bars;
    }

}

So an instance of a Foo holds on to an array of Bar objects; and the Foo class has the standard java-bean-esque setters and getters.

With this description of how to set an array field on a Java bean, let’s get this to actually work.

First question, how do you deal with arrays in Java? Sounds trivial, right. If you don’t mind a little pain, it’s not that bad… By dealing, I mean what happens when someone has given you an array created as follows:
   Object[] arrayOfObject = new Object[3]:
Note that this is very different from this:
   Object[] arrayOfBars = new Bar[3]:

The runtime type of these two results is different. One is array of Object; the other is Array of Bar.

This will not work:
   Bar[] arrayOfBar = (Bar[])arrayOfObject;
This will generate a runtime cast exception. You cannot simply take something allocated as an array of objects and cast it to an array of a specific type. NO, you have to do something more like the following:
    Array.newInstance(typeYouWantAnArrayOf, sizeOfArray);

That’s not too bad, right? You can then either use another method on the Array class to set the values, or you can cast the result to an appropriate array.

That’s enough information to write a generic method to copy from an array of Object to an array of a subtype of Object:
   public static Object[] copyToArrayOfType(Class destinationType, Object[] fromArray) {
      Object[] result = (Object[])Array.newInstance(destinationType, fromArray.length);

      for(int i = 0; i < fromArray.length; ++i)
         result[i] = fromArray[i];

      return result;
   }
This is a bit unruly because the caller still needs to cast the result:
   Object[] arrayOfObject = new Object[] { new Foo(), new Foo(), new Foo() };
   Foo[] arrayOfFoo = (Foo[])copyToArrayOfType(Foo.class, arrayOfObject);
We can get rid of this cast if we use generics:
   public static <T> T[] copyToArrayOfType(Class<T> destinationType, Object[] fromArray) {
      T[] result = (T[])Array.newInstance(destinationType, fromArray.length);

      for(int i = 0; i < fromArray.length; ++i)
         result[i] = (T) fromArray[i];

      return result;
   }
This doesn’t quite work because of type erasure, so to get this to “compile cleanly – no warnings”, you’ll need to add the following line above the method:
   @SuppressWarnings("unchecked")

That’s just me telling the compiler I really think I know what I’m doing.

With this change, you can now write the following:
   Object[] arrayOfObject = new Object[] { new Foo(), new Foo(), new Foo() };
   Foo[] arrayOfFoo = copyToArrayOfType(Foo.class, arrayOfObject);

The original problem was to take an array of Object[] and set it into a destination object’s attribute. Now we can create an array with the correct type, what next?

There are several things still remaining:
  1. Given the name of the property, determine its underlying array type.
  2. Create the array (above)
  3. Assign the value to the underlying field
  4. Do some suficient hand-waving to handle exceptions

Here are solutions for each of those things:
Determine underlying type

   PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor( destObject, fieldName);
   Class<?> destType = pd.getPropertyType().getComponentType();

Create the array

   Object[] destArray = copyToArrayOfType.(destType, fromArray);

Assign the value

   PropertyUtils.setSimpleProperty(destObject, fieldName, destArray);
Here’s all of that put together and simply capturing all of the checked exceptions (that’s a whole other can of worms):
   public static void assignToArrayFieldFromObjectArray(Object destObject, String fieldName, Object[] fromArray) {
      try {
         PropertyDescriptor pd = PropertyUtils.getPropertyDescriptor(destObject, fieldName);
         Class<?> destType = pd.getPropertyType().getComponentType();
         Object[] destArray = copyToArrayOfType(destType, fromArray);
         PropertyUtils.setSimpleProperty(destObject, fieldName, destArray);
      } catch (Exception e) {
         throw new RuntimeException(e);
      }
   }

That’s all it takes to copy an array and then set the value in the field of a destination object.

Simple, right?

Sometimes static (and strong) typing can get in the way. This is one of those cases. Luckily, you can write this one and use it all over. Maybe it’s a part of the BeanUtils that I was unable to track down (probably).

Comments

Leave a response

  1. Avatar
    Paul Holser 1 day later:

    You can eliminate one warning and push the suppression closer to the cause in copyToArrayOfType() like so:

    public static  T[] copyToArrayOfType(Class destinationType, Object[] fromArray) {
        @SuppressWarnings("unchecked")
        T[] result = (T[]) Array.newInstance(destinationType, fromArray.length);
        for (int i = 0; i < fromArray.length; ++i)
           result[i] = destinationType.cast(fromArray[i]);
        return result;
    }
  2. Avatar
    Brett L. Schuchert 1 day later:

    Thanks Paul. For some reason, I had it in my mind that the Suppress Warnings annotation’s target was a method.

    However, adding it to a declaration line is very smart. You’re cooking with gas!

  3. Avatar
    Paul Holser 1 day later:

    Yeah, cool, that @SuppressWarnings…http://cleveralias.blogs.com/thought_spearmints/2006/01/suppresswarning.html

  4. Avatar
    Greg M 3 days later:

    Yeah, Java has a pretty sorry excuse for a static type system. The sort of thing that’d be a breeze in any modern language. (in many languages that predate Java 1.0, even)

  5. Avatar
    http://www.blacktowhiteiphone4.com over 2 years later:

    It’s sorry to hear that Some white iPhone 4 buyers have reported signal reduction when the phone is held in certain ways, especially in the left hand, as the antenna problem is in the bottom left corner of the phone’s side casing. Is that mean i have to wait longer to get the white iphone 4?

  6. Avatar
    Criminal Records over 2 years later:

    So an instance of a Foo holds on to an array of Bar objects; and the Foo class has the standard java-bean-esque setters and getters.

  7. Avatar
    Matteo over 2 years later:

    nice and good sharing

  8. Avatar
    Jones over 2 years later:

    nice and good sharing

  9. Avatar
    Backup iPhone SMS over 2 years later:

    Keep your Contacts and SMS safe! Actually, the contacts and SMS have more values than a cell phone’s own value. You can pay money to buy a new iPhone, but cannot buy your lost contacts and SMS back. So it’s important for you to backup your contacts and SMS in iPhone. And we recommend you backup contacts and SMS regularly. Our backup software can help you take a snapshot for your contacts and SMS. Your important personal information will be never lost.

  10. Avatar
    okey oyunu oyna over 3 years later:

    woww very very nice.

    internette görüntülü olarak okey oyunu oyna, gerçek kisilerle tanis, turnuva heyecanini yasa.

  11. Avatar
    funny pictures over 3 years later:

    yeah Em glad Too

    Static typing make work easy and relaible.Keep Posting Same Stuff in future

  12. Avatar
    christian louboutin shoes on sale over 3 years later:

    Your essay is good except for the spelling.

  13. Avatar
    beats by dr dre headphones over 3 years later:

    These beats by dr dre solo are just the same as selling in the franchise store.Or even better.

Comments