Tbpgr Blog

Employee Experience Engineer tbpgr(てぃーびー) のブログ

Javaで同名フィールドのコピーとパフォーマンスの計測。DRYを選ぶか高速化を選ぶか

概要

Javaで同名フィールドのコピーとパフォーマンスの計測

内容

Javaで同名フィールドのコピーを行う手法とそのパフォーマンス検証について。
ある2つのデータクラスがあり、同名のフィールドは同じ型であるとします。
この2つのデータクラスの同名フィールドのみコピーしたい場合に

・この処理を汎用化し、1回コーディングすれば以降はその機能を呼び出せばいいようにした場合

・ベタ書きで似たようなケースが登場するたびにデータのsetter、getterのコードを書く場合

の2つのパフォーマンスを比較します。

処理で共通利用するクラス

データクラスの内部で利用するEnum

package gr.java_conf.tb.java_sample_code.copyutil_blog;

public enum CountEnum {
	ONE("1"),TWO("2"),THREE("3");

	private String type;
	private CountEnum(final String type) {
		this.type = type;
	}
	public String toValue() {
		return this.type;
	}
	 public CountEnum fromValue(String value) throws Exception {
	    if (value.equals("1")) {
	      return ONE;
	    } else if (value.equals("2")) {
	      return TWO;
	    } else if (value.equals("3")) {
	      return THREE;
	    } else {
	      throw new Exception();
	    }
	  }
}

データクラスの内部で使用するデータ型

package gr.java_conf.tb.java_sample_code.copyutil_blog;

import java.math.BigDecimal;

public class Line {
  private String str1 = null;
  private String str2 = null;
  private BigDecimal bigDecimal1 = null;
  private BigDecimal bigDecimal2 = null;
  private CountEnum hogeCls1 = null;
  private CountEnum hogeCls2 = null;

  public CountEnum getHogeCls1() {
    return hogeCls1;
  }

  public void setHogeCls1(CountEnum hogeCls1) {
    this.hogeCls1 = hogeCls1;
  }

  public CountEnum getHogeCls2() {
    return hogeCls2;
  }

  public void setHogeCls2(CountEnum hogeCls2) {
    this.hogeCls2 = hogeCls2;
  }

  public String getStr1() {
    return str1;
  }

  public void setStr1(String str1) {
    this.str1 = str1;
  }

  public String getStr2() {
    return str2;
  }

  public void setStr2(String str2) {
    this.str2 = str2;
  }

  public BigDecimal getBigDecimal1() {
    return bigDecimal1;
  }

  public void setBigDecimal1(BigDecimal bigDecimal1) {
    this.bigDecimal1 = bigDecimal1;
  }

  public BigDecimal getBigDecimal2() {
    return bigDecimal2;
  }

  public void setBigDecimal2(BigDecimal bigDecimal2) {
    this.bigDecimal2 = bigDecimal2;
  }
}

コピー元データクラス

package gr.java_conf.tb.java_sample_code.copyutil_blog;

import java.math.BigDecimal;
import java.util.List;

public class From {
  private String str1 = null;
  private String str2 = null;
  private BigDecimal bigDecimal1 = null;
  private BigDecimal bigDecimal2 = null;
  private CountEnum hogeCls1 = null;
  private CountEnum hogeCls2 = null;
  private List<Line> lineList = null;

  public CountEnum getHogeCls1() {
    return hogeCls1;
  }
  public void setHogeCls1(CountEnum hogeCls1) {
    this.hogeCls1 = hogeCls1;
  }
  public CountEnum getHogeCls2() {
    return hogeCls2;
  }
  public void setHogeCls2(CountEnum hogeCls2) {
    this.hogeCls2 = hogeCls2;
  }
  public String getStr1() {
    return str1;
  }
  public void setStr1(String str1) {
    this.str1 = str1;
  }
  public String getStr2() {
    return str2;
  }
  public void setStr2(String str2) {
    this.str2 = str2;
  }
  public BigDecimal getBigDecimal1() {
    return bigDecimal1;
  }
  public void setBigDecimal1(BigDecimal bigDecimal1) {
    this.bigDecimal1 = bigDecimal1;
  }
  public BigDecimal getBigDecimal2() {
    return bigDecimal2;
  }
  public void setBigDecimal2(BigDecimal bigDecimal2) {
    this.bigDecimal2 = bigDecimal2;
  }
  public List<Line> getLineList() {
    return lineList;
  }
  public void setLineList(List<Line> lineList) {
    this.lineList = lineList;
  }

}

コピー先データクラス

package gr.java_conf.tb.java_sample_code.copyutil_blog;

import java.math.BigDecimal;
import java.util.List;

public class To {
  private String str1 = null;
  private String str3 = null;
  private BigDecimal bigDecimal1 = null;
  private BigDecimal bigDecimal3 = null;
  private CountEnum hogeCls1 = null;
  private CountEnum hogeCls3 = null;
  private List<Line> lineList = null;

  public String getStr1() {
    return str1;
  }

  public void setStr1(String str1) {
    this.str1 = str1;
  }

  public String getStr3() {
    return str3;
  }

  public void setStr3(String str3) {
    this.str3 = str3;
  }

  public BigDecimal getBigDecimal1() {
    return bigDecimal1;
  }

  public void setBigDecimal1(BigDecimal bigDecimal1) {
    this.bigDecimal1 = bigDecimal1;
  }

  public BigDecimal getBigDecimal3() {
    return bigDecimal3;
  }

  public void setBigDecimal3(BigDecimal bigDecimal3) {
    this.bigDecimal3 = bigDecimal3;
  }

  public CountEnum getHogeCls1() {
    return hogeCls1;
  }

  public void setHogeCls1(CountEnum hogeCls1) {
    this.hogeCls1 = hogeCls1;
  }

  public CountEnum getHogeCls3() {
    return hogeCls3;
  }

  public void setHogeCls3(CountEnum hogeCls3) {
    this.hogeCls3 = hogeCls3;
  }

  public List<Line> getLineList() {
    return lineList;
  }

  public void setLineList(List<Line> lineList) {
    this.lineList = lineList;
  }

  public String toString() {
    String out = "";
    out += "str1:" + getStr1() + "\n";
    out += "str3:" + getStr3() + "\n";;
    out += "bigDecimal1:" + getBigDecimal1() + "\n";;
    out += "bigDecimal3:" + getBigDecimal3() + "\n";;
    out += "HogeCls1:" + getHogeCls1() + "\n";;
    out += "HogeCls3:" + getHogeCls3() + "\n";;
    out += "LineList:" + getLineList() + "\n";;
    return out;
  }
}

汎用版データコピー機能

package gr.java_conf.tb.java_sample_code.copyutil_blog;

import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;

/**
 * フィールドコピーユーティリティー。
 *
 * @author tbpgr
 *
 */
public class FieldCopyUtility {
  /**
   * copyFromに持っているフィールドの内容をcopyToにコピーする。
   *
   * <pre>
   * 前提条件:copyFromとcopyToで同名かつ同じデータ型をコピー
   * </pre>
   *
   * @param copyFrom
   * @param copyTo
   * @return copyFromから値をコピーしたcopyTo
   */
  public static Object copyFields(Object copyFrom, Object copyTo) {
    try {
      PropertyDescriptor[] propertiesFrom = Introspector.getBeanInfo(copyFrom.getClass()).getPropertyDescriptors();
      PropertyDescriptor[] propertiesTo = Introspector.getBeanInfo(copyTo.getClass()).getPropertyDescriptors();
      for (PropertyDescriptor propertyFrom : propertiesFrom) {
        for (PropertyDescriptor propertyTo : propertiesTo) {
          if (hasSameName(propertyFrom, propertyTo)) {
            if (canWriteField(propertyTo)) {
              copyField(copyFrom, copyTo, propertyFrom, propertyTo);
            }
            break;
          }
        }
      }
    } catch (IntrospectionException e) {
      // TODO 自動生成された catch ブロック
      e.printStackTrace();
    } catch (IllegalArgumentException e) {
      // TODO 自動生成された catch ブロック
      e.printStackTrace();
    } catch (IllegalAccessException e) {
      // TODO 自動生成された catch ブロック
      e.printStackTrace();
    } catch (InvocationTargetException e) {
      // TODO 自動生成された catch ブロック
      e.printStackTrace();
    }
    return copyTo;
  }

  /**
   * Fieldのコピーを行う。
   *
   * @param copyFrom
   *          コピー元
   * @param copyTo
   *          コピー先
   * @param propertyFrom
   *          コピー元PropertyDescriptor
   * @param propertyTo
   *          コピー先PropertyDescriptor
   * @throws IllegalAccessException
   * @throws InvocationTargetException
   */
  private static void copyField(Object copyFrom, Object copyTo, PropertyDescriptor propertyFrom,
      PropertyDescriptor propertyTo) throws IllegalAccessException, InvocationTargetException {
    Object copyFromValue = propertyFrom.getReadMethod().invoke(copyFrom);
    propertyTo.getWriteMethod().invoke(copyTo, copyFromValue);
  }

  /**
   * 同名のフィールドを持っているか判定する。
   *
   * @param propertyFrom
   *          判定元プロパティ
   * @param propertyTo
   *          判定先プロパティ
   * @return 同名であれば、true。同名でなければfalse
   */
  private static boolean hasSameName(PropertyDescriptor propertyFrom, PropertyDescriptor propertyTo) {
    return propertyFrom.getName().equals(propertyTo.getName());
  }

  /**
   * 書き込み可能か判定する。
   *
   * @param propertyTo
   * @return 書き込み可能ならTrue,不可能ならfalse
   */
  private static boolean canWriteField(PropertyDescriptor propertyTo) {
    return propertyTo.getWriteMethod() != null;
  }
}

ベタ書き版データコピー機能

package gr.java_conf.tb.java_sample_code.copyutil_blog;

import java.util.ArrayList;
import java.util.List;

/**
 * フィールドコピーユーティリティー。
 *
 * @author tbpgr
 *
 */
public class FieldCopyBetaCoding {
  /**
   * ObjectFromに持っているフィールドの内容をObjectToにコピーする。
   *
   * <pre>
   * 前提条件:ObjectFromとObjectToで同名かつ同じデータ型をコピー
   * </pre>
   *
   * @param copyFrom
   * @param copyTo
   * @return copyFromから値をコピーしたcopyTo
   */
  public static To copyFields(From copyFrom, To copyTo) {
    copyTo.setBigDecimal1(copyFrom.getBigDecimal1());
    copyTo.setBigDecimal3(null);
    copyTo.setHogeCls1(copyFrom.getHogeCls1());
    copyTo.setHogeCls3(null);
    copyTo.setStr1(copyFrom.getStr1());
    copyTo.setStr3(null);

    List<Line> lineList = new ArrayList<Line>();

    for (int i = 0; i < copyFrom.getLineList().size(); i++) {
      Line dataLineFrom = copyFrom.getLineList().get(i);
      Line dataLineTo = new Line();
      dataLineTo.setBigDecimal1(dataLineFrom.getBigDecimal1());
      dataLineTo.setBigDecimal2(dataLineFrom.getBigDecimal2());
      dataLineTo.setHogeCls1(dataLineFrom.getHogeCls1());
      dataLineTo.setHogeCls2(dataLineFrom.getHogeCls2());
      dataLineTo.setStr1(dataLineFrom.getStr1());
      dataLineTo.setStr2(dataLineFrom.getStr2());
      lineList.add(dataLineTo);
    }

    copyTo.setLineList(lineList);
    return copyTo;
  }
}

パフォーマンス計測コード

package gr.java_conf.tb.java_sample_code.copyutil_blog;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

import org.junit.Test;

public class FieldCopyUtilityTest {

  private static final int MAX = 10;
  private static final int TEST_COUNT = 5;

  @Test
  public void testCopyFields_UseBeanInfo() {
    // Mockデータの取得
    From dataFrom = getDataFromMock();

    for (int count = 0; count < TEST_COUNT; count++) {
      long startTime = System.currentTimeMillis();
      for (int i = 0; i < MAX; i++) {
        To dataTo = new To();
        dataTo = (To) FieldCopyUtility.copyFields(dataFrom, dataTo);
      }
      long endTime = System.currentTimeMillis();
      printResultTime("一回書けばいいDRYな共通処理", count, startTime, endTime);
    }
  }

  @Test
  public void testCopyFields_BetaCoding() {
    // Mockデータの取得
    From dataFrom = getDataFromMock();

    for (int count = 0; count < TEST_COUNT; count++) {
      long startTime = System.currentTimeMillis();
      for (int i = 0; i < MAX; i++) {
        To dataTo = new To();
        dataTo = (To) FieldCopyBetaCoding.copyFields(dataFrom, dataTo);
      }
      long endTime = System.currentTimeMillis();
      printResultTime("気合でベタ書き", count, startTime, endTime);
    }
  }

  private void printResultTime(String title, int count, long startTime, long endTime) {
    System.out.println(title + "版" + (count + 1) + "回目:" + (endTime - startTime) + "ミリ秒");
  }

  @SuppressWarnings("unused")
  private void checkAfterCopyData(To dataTo) {
    System.out.println("--------------処理結果--------------");
    System.out.println(dataTo);
    System.out.println(dataTo.getLineList().get(0).getStr1());
    System.out.println(dataTo.getLineList().get(0).getStr2());
    System.out.println(dataTo.getLineList().get(0).getBigDecimal1());
    System.out.println(dataTo.getLineList().get(0).getBigDecimal2());
    System.out.println(dataTo.getLineList().get(0).getHogeCls1());
    System.out.println(dataTo.getLineList().get(0).getHogeCls2());
    System.out.println(dataTo.getLineList().get(1).getStr1());
    System.out.println(dataTo.getLineList().get(1).getStr2());
    System.out.println(dataTo.getLineList().get(1).getBigDecimal1());
    System.out.println(dataTo.getLineList().get(1).getBigDecimal2());
    System.out.println(dataTo.getLineList().get(1).getHogeCls1());
    System.out.println(dataTo.getLineList().get(1).getHogeCls2());
  }

  private From getDataFromMock() {
    From dataFrom = new From();

    dataFrom.setStr1("str1");
    dataFrom.setStr2("str2");
    dataFrom.setBigDecimal1(new BigDecimal(1));
    dataFrom.setBigDecimal2(new BigDecimal(2));
    dataFrom.setHogeCls1(CountEnum.ONE);
    dataFrom.setHogeCls2(CountEnum.TWO);
    List<Line> dataLineFrom = new ArrayList<Line>();
    dataLineFrom = appendDataLine(dataLineFrom, "str1_1", "str2_1", new BigDecimal(1), new BigDecimal(2),
        CountEnum.ONE, CountEnum.TWO);
    dataLineFrom = appendDataLine(dataLineFrom, "str1_2", "str2_2", new BigDecimal(11), new BigDecimal(12),
        CountEnum.TWO, CountEnum.THREE);
    dataFrom.setLineList(dataLineFrom);
    return dataFrom;
  }

  private List<Line> appendDataLine(List<Line> dataLineFrom, String string1, String string2,
      BigDecimal bigDecimal1, BigDecimal bigDecimal2, CountEnum hogeEnum1, CountEnum hogeEnum2) {
    Line line1 = new Line();
    line1.setStr1(string1);
    line1.setStr2(string2);
    line1.setBigDecimal1(bigDecimal1);
    line1.setBigDecimal2(bigDecimal2);
    line1.setHogeCls1(hogeEnum1);
    line1.setHogeCls2(hogeEnum2);
    dataLineFrom.add(line1);
    return dataLineFrom;
  }

}

計測結果

■100万回ループ
一回書けばいいDRYな共通処理版1回目:1312ミリ秒
一回書けばいいDRYな共通処理版2回目:1291ミリ秒
一回書けばいいDRYな共通処理版3回目:1292ミリ秒
一回書けばいいDRYな共通処理版4回目:1292ミリ秒
一回書けばいいDRYな共通処理版5回目:1283ミリ秒
気合でベタ書き版1回目:87ミリ秒
気合でベタ書き版2回目:84ミリ秒
気合でベタ書き版3回目:83ミリ秒
気合でベタ書き版4回目:84ミリ秒
気合でベタ書き版5回目:83ミリ秒

■10万回ループ
一回書けばいいDRYな共通処理版1回目:152ミリ秒
一回書けばいいDRYな共通処理版2回目:133ミリ秒
一回書けばいいDRYな共通処理版3回目:135ミリ秒
一回書けばいいDRYな共通処理版4回目:132ミリ秒
一回書けばいいDRYな共通処理版5回目:129ミリ秒
気合でベタ書き版1回目:12ミリ秒
気合でベタ書き版2回目:8ミリ秒
気合でベタ書き版3回目:9ミリ秒
気合でベタ書き版4回目:8ミリ秒
気合でベタ書き版5回目:8ミリ秒

■1万回ループ
一回書けばいいDRYな共通処理版1回目:34ミリ秒
一回書けばいいDRYな共通処理版2回目:14ミリ秒
一回書けばいいDRYな共通処理版3回目:13ミリ秒
一回書けばいいDRYな共通処理版4回目:15ミリ秒
一回書けばいいDRYな共通処理版5回目:13ミリ秒
気合でベタ書き版1回目:4ミリ秒
気合でベタ書き版2回目:1ミリ秒
気合でベタ書き版3回目:1ミリ秒
気合でベタ書き版4回目:1ミリ秒
気合でベタ書き版5回目:1ミリ秒

総括

コピー機能を大量に呼び出せば呼び出すほど、パフォーマンス差は顕著に。
また、このサンプルの場合はフィールド数が7個ですが現実のシステム開発では1クラスに
大量のフィールドがある場合も多いため差はより顕著になるでしょう。

コピー機能をあまり呼び出さない場合はパフォーマンス差も小さくなり、
処理時間自体も1万回ループで10-30ミリ秒程度である。

汎用版を利用する基準は、
・同様の処理が多くあること
・フィールド数が大量ではないこと
・大量のループなどがない箇所
になる。