Java: Tweaking Non-Optimized Code
Sun. Aug 23rd, 2020 07:45 PM10 mins read
Java: Tweaking Non-Optimized Code
Source: Shutterstock - skeleton using cobwebbed computer

Sebenarnya ini catatan-catatan kecil gw aja yang selama ini gw simpan. Gw emang suka nyari-nyari Best Practice gitu di internet. Biasanya sih gw simpan di notepad aja, tapi kali ini gw coba share kali aja berguna. Beberapa catatan diantaranya gw pelajari dari Intellij Inspector dan lainnya berasal dari pencarian di internet dan blog dari Java Expert. Sebenarnya ada lumayan banyak sih catatannya, tapi yang menurut gw jarang diketahui orang-orang aja yang gw post. Langsung aja deh berikut beberapa code yang kurang optimal dan bisa dioptimalkan:

1.String Concatenation on Loop

Code berikut akan menghasilkan string dari 0 hingga 9:

String result = "";
for(int i = 0; i < 10; i++){
	result += i;
}

String merupakan Immutable Object. Menggunakan Concatenation saat melakukan perulangan akan membuat Heap baru di memory sebanyak n kali. Dari code di atas bisa dioptimalkan menggunakan StringBuilder.

StringBuilder builder = new StringBuilder();
for(int i = 0; i < 10; i++){
	builder.append(i);
}
String result = builder.toString();

Dengan StringBuilder yang merupakan Mutable Object, penggunaan Heap di memory lebih optimal tanpa harus menciptakan object baru sebanyak n kali. Sebenarnya selain StringBuilder juga ada StringBuffer, tapi StringBuffer ini Synchronized. Jarang banget ada case yang membutuhkan Synchronized saat melakukan mutasi String.

Rule of thumb:

  • Gunakan StringBuilder jika String Concatenation-nya terlalu banyak atau saat looping

2.Map containsKey, get & put Method

Misalkan kita ingin mendapatkan value dari key tertentu pada sebuah Map, jika key tersebut tidak ditemukan, ambil default value-nya.

String name = nameByCode.containsKey(123) ? nameByCode.get(123) : "Unknown";

FYI, method containsKey() dan get() pada Map mempunyai algoritma yang sama. Bedanya containsKey() menghasilkan true jika ada, dan false jika tidak ada. Sedangkan get() menghasilkan value jika ada, dan null jika tidak ada. Dengan code seperti di atas, jika key 123 itu ada, maka akan mengeksekusi algoritma yang sama sebanyak 2ร—. Sebenarnya di Java 8 ada method baru pada Map, yaitu getOrDefault(), yang punya 2nd parameter sebagai default value jika key yang dicari tidak ada.

String name = nameByCode.getOrDefault(123, "Unknown"); 

Hal yang sama juga terjadi saat mengeksekusi put(). Misalkan hanya akan menambahkan sebuah entry jika key-nya belum terdaftar pada map.

If(!nameByCode.containsKey(123)){
	nameByCode.put(123, "one two three");
}

Code di atas bisa dioptimalkan dengan putIfAbsent()

nameByCode.putIfAbsent(123, "one two three");

Rule of thumb:

  • Gunakan getOrDefault() jika ada default value seandainya key yang digunakan belum tersedia pada map
  • Gunakan putIfAbsent() hanya jika ingin menambahkan entry saat key yang di-assign belum tersedia

3.Map putIfAbsent Method with Generated Value

Misalkan ingin menambahkan entry dengan value yang dihasilkan dari sebuah method hanya jika key tersebut belum ada pada map.

private String getDefault(Integer integer){
	return RandomStringUtils.random(10) + integer;
}

private void computeTest(Map<Integer, String> nameByCode){
	nameByCode.putIfAbsent(123, getDefault(123));
}

Pada code di atas, method getDefault(123) selalu dieksekusi terlebih dahulu untuk mendapatkan value-nya, entah itu key-nya sudah ada pada map atau belum. Baru setelah itu method putIfAbsent() dieksekusi. Pada kasus seperti ini eksekusi method getDefault() akan redundant jika key-nya sudah ada pada map, karena putIfAbsent() tidak akan melakukan put() jika key-nya sudah ada pada map. Solusinya bisa menggunakan computeIfAbsent(). Ini mirip dengan putIfAbsent(). Bedanya putIfAbsent() 2nd parameternya merupakan Constant. Sedangkan computeIfAbsent() 2nd parameternya merupakan Functional Interface. Dengan Functional Interface, method tersebut hanya dieksekusi jika key-nya belum terdaftar pada map.

private void computeTest(Map<Integer, String> nameByCode){
	nameByCode.computeIfAbsent(123, key -> getDefault(key));
}

Selain computeIfAbsent() juga ada computeIfPresent() dan compute(). Bedanya computeIfPresent() hanya dieksekusi jika key-nya ada pada map dan value sebelumnya akan diganti dengan value hasil dari 2nd parameter. Kalau compute() lebih mirip dengan put(), tidak ada pengecekan key tersebut sudah ada atau belum.

Rule of thumb:

  • Gunakan putIfAbsent() jika value yang di-assign adalah Constant value
  • Gunakan computeIfAbsent() jika value yang di-assign hasil dari sebuah method

4.Return null Object

Pada kondisi tertentu terkadang kita mengembalikan null value.

public City buildCity(String cityName){
	if(cityName == null){
		return null;
	}
	City city = new City();
	city.setCityName(cityName);
	return city;
}

public void compute(){
	City city = buildCity(null);
	String cityName = "Unknown";
	if(city!= null){
		cityName = city.getCityName() != null ? city.getCityName() : "Unknown";
	}
}

Masalahnya, code yang memanggil method di atas harus melakukan null-checking saat akan menggunakan properties yang ada pada City. Tambah masalah jika City mempunyai Nested Object seperti yang pernah gw tulis di postingan sebelumnya, makin verbose. Solusinya menggunakan Optional. Objek akan dibungkus menjadi sebuah Optional dan akan mengembalikan Optional.empty() jika objek yang diinginkan bernilai null.

public Optional<City> buildCity(String cityName){
	if(cityName == null){
		return Optional.empty();
	}
	City city = new City();
	city.setCityName(cityName);
	return Optional.of(city);
}

public void compute(){
	Optional<City> city = buildCity(null);
	String cityName = city.map(City::getCityName).orElse("Unknown");
	String provinceName = city.map(City::getProvince).map(Province::getProvinceName).orElse("Unknown");
}

Dengan Optional, code di atas jadi lebih readable tanpa perlu melakukan null-check, terutama jika Object tersebut memiliki return value berupa object lainnya. Bisa makin panjang null-check-nya ntar. Ini sangat berguna ketika membuat method yang bersifat public, sehingga developer lain yang ingin memakai method tersebut tidak perlu mengecek sumber dari code tersebut apakah nilai yang di-return nullable atau tidak. Lain kali mungkin gw akan bikin catatan khusus Optional juga deh. Ada beberapa sih.

Rule of thumb:

  • Berdasarkan Intellij Code Inspector, Optional digunakan untuk return method yang dapat menghasilkan null Object

5.Return null Collection

Mirip dengan kasus sebelumnya, pada kasus tertentu tidak ada Collections yang dikembalikan. Kasus berikut menggunakan List:

public List<String> getCodes(String codes){
	if(codes == null){
		return null;
	}
	return Arrays.asList(codes.split(","));
}

Sama dengan kasus sebelumnya, mengembalikan null pada Collection apapun akan membuat code menjadi rentan terhadap NullPointerException. Solusinya bisa seperti berikut:

public List<String> getCodes(String s){
	if(s == null){
		return Collections.emptyList();
	}
	return Arrays.asList(s.split(","));
}

Dengan begitu, code akan lebih aman dari NullPointerException. Oh ya, disarankan menggunakan Collections.emptyList() daripada new ArrayList<>() kosong, karena Collections.emptyList() merupakan Constant value dari java.util.Collections dengan tipe Generic. Jadi karena dia Constant dan Shareable, tidak perlu menuh-menuhin Heap Memory menggunakan new ArrayList<>() lagi. Perlu diingat juga Collections.emptyList() ini merupakan Immutable List, tidak bisa diubah, ditambah atau dikurangi elemennnya. Umumnya ketika kita mendapatkan return value berupa List, jarang banget kita melakukan perubahan pada List tersebut. Untuk Set bisa menggunakan Collections.emptySet() dan Collections.emptyMap() untuk Map.

Rule of thumb:

  • By Default gunakan Collections.emptyList() pada List, Collections.emptySet() pada Set dan Collections.emptyMap() pada Map
  • Gunakan ArrayList<> kosong hanya jika akan ada perubahan element pada List yang di-get, hal yang sama juga berlaku pada Set dengan HashSet dan Map dengan HashMap.

6.Single Element Collection

Masih tentang Collections. Ada kasus ketika membutuhkan sebuah Collection yang isinya hanya satu element saja. Kali ini gw pakai Set sebagai contoh:

public Set<Integer> getSingleId(int id){
	Set<Integer> singleId = new HashSet<>();
	singleId.add(id);
	return singleId;
}

Pertama, code di atas verbose hanya untuk menghasilkan Set dengan satu elemen saja. Kedua, objeknya Mutable. Dalam kasus ini kita memang hanya butuh satu elemen saja, namanya juga single artinya ya single, ga mungkin lebih dari satu, sudah pasti ini harus Immutable. Ketiga, HashSet ini cukup expensive jika hanya digunakan untuk satu element saja. Solusinya menggunakan java.util.Collections method:

public Set<Integer> getSingleId(int id){
	return Collections.singleton(id);
}

Pertama, ini sangat simple. Kedua, ini Immutable, akan throws UnsupportedOperationException jika terjadi perubahan. Ketiga, less expensive dibandingkan dengan HashSet dengan satu elemen, karena Collections.singleton() hanya sebuah objek yang mengimplementasi Set dengan satu final object pada field-nya. Hal yang sama juga berlaku pada List dengan Collections.singletonList() dan Map dengan Collections.singletonMap(). Oh ya, untuk List sebenarnya ada Arrays.asList(), bedanya ini menyimpan object dengan Object[] dan bentuknya semi-mutable karena bisa melakukan perubahan pada element menggunakan set() walaupun add() dan remove()-nya forbidden.

Rule of thumb:

  • By default gunakan Collections.singleton() untuk Set, Collections.singletonList() untuk List dan Collections.singletonMap() untuk Map saat berurusan dengan Single Element
  • Gunakan Arrays.asList() hanya jika elemennya lebih dari satu

7.Undecided ArrayList Capacity

Trick ini gw dapat dari blog salah satu Java Champions. Seperti kita ketahui ArrayList ini dinamis dan Object[] ukurannya fixed. Ada kondisi ketika ingin membuat sebuah List baru dengan menggunakan value dari sebuah List lainnya. Ribet banget kata-katanya ๐Ÿ˜…. Langsung liat code di bawah deh:

public List<CityResponse> buildResponse(List<City> cities){
	List<CityResponse> responses = new ArrayList<>();
	for(City city : cities){
		CityResponse response = new CityResponse();
		response.setCityName(city.getCityName());
		responses.add(response);
	}
	return responses;
}

Pada code di atas bisa dipastikan jumlah element dari List<CityResponse> dan List<City> pasti sama. Masalahnya terletak pada responses.add(response); yaitu saat menambahkan element yang sedang di-loop. FYI, pada ArrayList itu sebenarnya dia menyimpan Object[]. Saat add() pertama kali, default ukurannya adalah 10. Jadi, by default setiap pemanggilan method add() pada ArrayList dia akan bikin Object[] baru dengan ukuran baru setelah jumlah elemennya lebih dari 10 sebanyak n kali. Cukup menguras Garbage Collector nantinya. Solusinya bisa dengan menambahkan capacity pada Constructor-nya:

public List<CityResponse> buildResponse(List<City> cities){
	List<CityResponse> responses = new ArrayList<>(cities.size());
	for(City city : cities){
		CityResponse response = new CityResponse();
		response.setCityName(city.getCityName());
		responses.add(response);
	}
	return responses;
}

Dengan menambahkan capacity pada Constructor, maka otomatis Object[] pada ArrayList akan dimuat dengan ukuran sesuai capacity yang tertera pada Constructor dari awal. Sehingga saat memakai method add() tidak perlu lagi bikin objek baru, ArrayList tinggal mengalokasikan element pada Object[] tersebut. Tapi ingat, ini hanya berlaku jika ukurannya memang bakal sama. Pengecualian jika ukurannya tidak menentu seperti ada kondisi saat looping, sehingga belum tentu ukuran pada List<City> akan sama dengan List<CityResponse>, seperti berikut:

public List<CityResponse> buildResponse(List<City> cities){
	List<CityResponse> responses = new ArrayList<>();
	for(City city : cities){
		if(city.getCityName() == null) continue;
		CityResponse response = new CityResponse();
		response.setCityName(city.getCityName());
		responses.add(response);
	}
	return responses;
}

Rule of thumb:

  • Pasang capacity pada Constructor di ArrayList jika ukurannya fixed, selain itu tidak perlu

8.Overuse Stream.forEach Method

Sejak Functional Interface jadi hype, semuanya jadi serba Lambda. Termasuk melakukan looping menggunakan forEach() seperti code berikut:

public void lambda(List<City> cities){
	cities.stream().forEach(System.out::println);
}

Penggunaan stream() pada code di atas redundant. Tanpa harus dikonversi menjadi Stream pun Collection by default sudah punya method forEach() sendiri.

public void lambda(List<City> cities){
	cities.forEach(System.out::println);
}

Kecuali jika ada operasi khusus menggunakan Stream seperti filter(), map(), dan lainnya.

public void lambda(List<City> cities){
	cities.stream().map(City::getCityName).forEach(System.out::println);
}

Ini juga berlaku pada Set tentunya.

Rule of thumb:

  • gunakan List.forEach() atau Set.forEach() tanpa dikonversi jadi Stream jika hanya ingin melakukan perulangan
  • Kecuali ada operasi pada Stream yang dibutuhkan sebelum melakukan perulangan seperti filter(), map(), dll.

9.Map.entrySet() for Loop

Biasanya orang-orang mengkonversi Map menjadi Set<Entry> ketika melakukan perulangan pada Map:

public void map(Map<Integer, String> nameById){
	for(Map.Entry<Integer, String> entry : nameById.entrySet()){
		System.out.println("id = " + entry.getKey());
		System.out.println("name = " + entry.getValue());
	}
}

Sebenarnya tidak ada yang salah dengan cara seperti itu. Hanya saja itu cukup verbose dan entry.getKey() serta entry.getValue() kurang menjelaskan itu variable apa. Solusinya bisa menggunakan Map.forEach()

public void map(Map<Integer, String> nameById){
	nameById.forEach((id, name) -> {
		System.out.println("id = " + id);
		System.out.println("name = " + name);
	});
}

Dengan code di atas penulisannya lebih simple dan nama variable key serta value-nya cukup menjelaskan isinya apa. Oh ya, karena ini Functional Interface, ini tidak cocok diterapkan jika ada non-final value dari luar scope Lambda yang di-assign ulang di dalamnya, dan akan Compile Error:

public void map(Map<Integer, String> nameById){
	int count = 0;
	nameById.forEach((id, name) -> {
		System.out.println("id = " + id);
		System.out.println("name = " + name);
		count += 1;
	});
}

Rule of thumb:

  • Gunakan Map.forEach() untuk readability yang lebih baik;
  • Kecuali ada operasi khusus menggunakan non-final value dari luar scope Lambda, maka gunakan Map.entrySet(), atau tetap gunakan forEach tapi value-nya disimpan menggunakan objek Atomic seperti AtomicInteger;
public void map(Map<Integer, String> nameById){
	AtomicInteger count = new AtomicInteger(0);
	nameById.forEach((id, name) -> {
		System.out.println("id = " + id);
		System.out.println("name = " + name);
		count.incrementAndGet();
	});
}

10.Sorting Collection

Ada kalanya kita menginginkan sebuah Collection itu berurutan sesuai yang diinginkan seperti berikut:

List<City> cities = new ArrayList<>();
//add some cities...
cities.sort(Comparator.comparing(City::getCityName));

Code di atas akan melakukan sorting data City berdasarkan cityName. Jika memang tujuannya dari awal adalah sebuah Collection dengan data yang berurutan sesuai cityName, alangkah baiknya menggunakan TreeSet dari awal daripada menggunakan ArrayList, LinkedList, HashSet ataupun LinkedHashSet.

Set<City> cities = new TreeSet<>(Comparator.comparing(City::getCityName));
//add some cities...

Selain karena TreeSet memang tercipta untuk itu, penggunaan TreeSet lebih efisien karena mengimplementasi Tree Data Structure dibanding List Sort yang mengimplementasi Merge Sort. Perlu diperhatikan juga bahwa Set ini bentuknya unik, jadi dalam kasus ini data City yang memiliki cityName sama hanya satu yang masuk. Penambahan yang kedua dan seterusnya akan diabaikan. Jika memang Collection yang diinginkan memang membolehkan duplikat, tetap pakai List dan sort kalau begitu. Ingat juga, Object yang tidak mengimplementasi Comparable Interface wajib menggunakan Comparator pada Constructor, dan yang bisa digunakan pada Comparator.comparing() adalah Object yang mengimplementasi Comparable Interface(seperti String, Integer, Double, dll). Dalam kasus di atas City::getCityName adalah String. Object yang mengimplementasi Comparable Interface tidak perlu dipasang Comparator-nya pada TreeSet Constructor:

Set<Integer> numbers = new TreeSet<>();
//add some numbers...

Rule of thumb:

  • Gunakan TreeSet jika datanya unique dan membutuhkan urutan
  • Gunakan Comparator pada Constructor jika objeknya tidak mengimplementasi Comparable Interface, dan tidak perlu gunakan Comparator jika objeknya mengimplementasi Comparable seperti String, Integer, Double, dll.
  • Function yang bisa digunakan pada Comparator.comparing() parameter adalah Function dengan Object yang mengimplementasi Comparable Interface

Baru 10 tapi lumayan banyak juga ya ๐Ÿ˜ƒ. Sebenarnya masih ada beberapa lagi, tapi ini udah lumyan panjang sih. Part 2 kapan-kapan menyusul ya ๐Ÿ˜.

ยฉ 2024 ยท Ferry Suhandri