Ciekawy eksperyment miałem okazję przeprowadzić dziś w pracy. Generalnie problem polega na tym, że aplikacja wykonuje dużo operacji zamiany String na Double i odwrotnie. Testability, Findbug i Cobertura to bardzo przydatne narzędzia w takim wypadku. Cobertura pozwala na sprawdzenie pokrycia testami kodu i gwarantuje, że nie pominiemy czegoś ważnego. Findbug wyszuka kod, który może powodować problemy i być źródłem błędów. Testability pozwala na odszukanie kodu nietestowalnego oraz zwróci uwagę na miejsca, w których kod nie jest optymalny. Nie należy jednak bezgranicznie wierzyć tym narzędziom. Podpowiedziały mi one, że konstruktor Double(String) jest wydajniejszy od wywołania Double.parseDoube(String). Zamieniłem więc podane linie kodu na konstruktory, przetestowałem i uruchomiłem program. Chodził znacznie dłużej… jakieś 70%… Przeprowadziłem więc wspomniany eksperyment. Polegał on na sprawdzeniu ile czasu zajmie wywołanie konstruktora, a ile metody statycznej.

Listing 1. Kod testu

import org.junit.Test;

public class DoubleTest {

	private static final String NUMBER = "1";

	private static final int NUMBER_OF_STEPS100000 = 100000;

	private static final int NUMBER_OF_STEPS1000000 = 1000000;

	private static final int NUMBER_OF_STEPS10000000 = 10000000;

	private static final int NUMBER_OF_STEPS100000000 = 100000000;

	private static final int NUMBER_OF_STEPS1000000000 = 1000000000;

	@Test
	public void parse100000Test() {
		for (int i = NUMBER_OF_STEPS100000; i > 0; i--)
			Double.parseDouble(NUMBER);
	}

	@Test
	public void parse1000000Test() {
		for (int i = NUMBER_OF_STEPS1000000; i > 0; i--)
			Double.parseDouble(NUMBER);
	}

	@Test
	public void parse10000000Test() {
		for (int i = NUMBER_OF_STEPS10000000; i > 0; i--)
			Double.parseDouble(NUMBER);
	}

	@Test
	public void parse100000000Test() {
		for (int i = NUMBER_OF_STEPS100000000; i > 0; i--)
			Double.parseDouble(NUMBER);
	}

	@Test
	public void parse1000000000Test() {
		for (int i = NUMBER_OF_STEPS1000000000; i > 0; i--)
			Double.parseDouble(NUMBER);
	}

	@Test
	public void constructor100000Test() {
		for (int i = NUMBER_OF_STEPS100000; i > 0; i--)
			(new Double(NUMBER)).doubleValue();
	}

	@Test
	public void constructor1000000Test() {
		for (int i = NUMBER_OF_STEPS1000000; i > 0; i--)
			(new Double(NUMBER)).doubleValue();
	}

	@Test
	public void constructor10000000Test() {
		for (int i = NUMBER_OF_STEPS10000000; i > 0; i--)
			(new Double(NUMBER)).doubleValue();
	}

	@Test
	public void constructor100000000Test() {
		for (int i = NUMBER_OF_STEPS100000000; i > 0; i--)
			(new Double(NUMBER)).doubleValue();
	}

	@Test
	public void constructor1000000000Test() {
		for (int i = NUMBER_OF_STEPS1000000000; i > 0; i--)
			(new Double(NUMBER)).doubleValue();
	}
}

Wyniki były bardzo ciekawe:
Wyniki testów.
Jak widać wyniki są bardzo pouczające. Dlaczego? Ponieważ gdy używamy konstruktora tak naprawdę dwa razy tworzymy obiekt. Proces ten kodzie klasy Double wygląda następująco:

Listing 2. Tworzenie obiektu klasy Double ze String za pomocą konstruktora.

public final class Double extends Number implements Comparable {
// wywołujemy ouble(String)
    public Double(String s) throws NumberFormatException {
	// REMIND: this is inefficient
	this(valueOf(s).doubleValue());
    }
// następnie wywoływana jest metoda valueOf(String)
    public static Double valueOf(String s) throws NumberFormatException {
	return new Double(FloatingDecimal.readJavaFormatString(s).doubleValue());
    }
// która tworzy obiekt Double za pomocą FloatingDecimal i przekazuje go do konstruktora Double(double)
    public Double(double value) {
	this.value = value;
    }
}

Jak widać jest tu dwa razy wywoływany konstruktor Double(double). W przypadku parsowania wywoływany jest tylko kawałek:

Listing 3. Tworzenie obiektu klasy Double ze String za pomocą metody Double.parseDouble(String).

public final class Double extends Number implements Comparable {
    public static double parseDouble(String s) throws NumberFormatException {
	return FloatingDecimal.readJavaFormatString(s).doubleValue();
    }
}

I wszystko jasne 😀

Podsumowując zanim zaczniesz optymalizować coś co zgłosiło jakieś narzędzie przeprowadź testy pod kątem parametru na którym ci zależy np. prędkości czy zajętości pamięci.