author-pic

Ferry S

An ISTJ, Type 5, Engineer, Gamer, and Thriller-Movies-Lover
Java: Jebakan Optional (The Optional Trap & Mistake)
Fri. Feb 10th, 2023 07:35 PM6 mins read
Java: Jebakan Optional (The Optional Trap & Mistake)
Source: Vecteezy - A Mouse Caught In A Mouse Trap

Sebelumnya gw pernah post tentang Jebakan Boolean, kali ini yang dibahas adalah Jebakan Optional pada Java beserta tips solusi penggunaan Optional yang tepat. Ini juga pernah gw mention pada post The Verbosity. Sekarang Optional sudah menjadi standar best practice ketika melakukan return object pada public method di Java. Optional awalnya fitur yang dipopulerkan oleh library Guava sebelum akhirnya diadopsi Java sejak Java 8. Optional jadi fitur andalan di Java untuk mempermudah maintain aplikasi dalam hal mencegah NullPointerException. Optional menggantikan peran null value untuk mengembalikan objek kosong dengan cara membungkus value dari objek tersebut dan “memaksa” kita menentukan action saat mendapatkan null value dari public method yang dipanggil agar terhindar dari NullPointer karena kita kadang lupa melakukan null-checking ketika menerima return object sehingga sering terjadi NullPointer. Kalau di bahasa lain seperti Javascript, Typescript, Kotlin, dan lainnya, menggunakan “?” sebagai Null-safe Operator, sedangkan kalau di Java menggunakan Optional. Walaupun sebenarnya menurut gw lebih simple pakai operator “?” sih😅. Entah kenapa Java malah menggunakan objek lagi😵‍💫. Kelebihannya yaitu Optional ga hanya untuk mencegah NullPointer saja, tapi juga ada fitur lain kayak mapping, filter, dan lainnya. Kekurangannya, selain karena penulisannya ga sesimple operator “?”, fitur di Optional juga seringkali menjebak dan disalahgunakan sehingga bukannya mempermudah maintain aplikasi malah memperburuk code bahkan bisa bikin bugs atau error. Kali ini gw akan share beberapa kesalahan yang sering gw temui pada code yang menggunakan Optional.

Hal pertama yang disalahgunakan adalah meng-assign null ke Optional. Ini justru kontradiktif, karena fungsi utama Optional itu untuk mencegah NullPointer, kalau di-assign null berarti bakal kena NullPointer juga dong ujung-ujungnya. Jadi ga ada bedanya sama objek biasa.

Optional<Phone> phone = null;

Untuk itu, jangan pernah meng-assign null ke Optional. Minimal di-assign empty jika memang tidak ada value yang ingin dibungkus.

Optional<Phone> phone = Optional.ofNullable(new Phone(1, "iphone", "iphone 14"));
Optional<Phone> emptyPhone = Optional.empty();

Ini hal yang paling sering disalahgunakan. Method tersebut sebenarnya anti-pattern dan menurut beberapa pendapat harusnya method ini tidak usah di-release karena sangat menjebak😡. Method get() fungsinya adalah untuk memanggil value dari objek Optional tersebut. Masalahnya, value yang dibungkus ke dalam objek Optional itu nullable. Kalau isinya null pasti bakal kena Exception yang tidak diharapkan saat di-get. Jadi percuma dong pakai Optional kalau bakal error juga. Sedangkan fungsi Optional adalah agar user memiliki opsi ketika mendapatkan objek tersebut kalau value-nya null untuk mecegah Exception yang tidak diinginkan.

Optional<String> hello = getHello();
String helloStr = hello.get();

Solusinya jangan gunakan method get(). Gunakan method lain agar kita punya opsi ketika valuenya null seperti berikut:

  • Gunakan orElse() kalau ingin menggunakan value dari constant saat valuenya kosong;
  • Gunakan orElseGet() kalau ingin menggunakan value hasil dari eksekusi sebuah method saat valuenya kosong;
  • Gunakan orElseThrow() kalau ingin melakukan spesifik Exception saat valuenya kosong;
  • Gunakan ifPresent() kalau ada action tertentu saat valuenya ada isinya;
  • Gunakan ifPresentOrElse() kalau ada action tertentu saat valuenya kosong maupun tidak (khusus Java 9 ke atas);
Optional<String> hello = getHello();

hello.ifPresent(o -> System.out.println("the value is = " + o));

hello.ifPresentOrElse(o -> System.out.println("the value is = " + o), () -> System.out.println("no value"));

String world = hello.orElse("world");

String defaultValue = hello.orElseGet(() -> constructDefaultStr());

String throwHello = hello.orElseThrow(() -> new IllegalArgumentException("no value"));

Oh ya, gw juga sering melihat penggunaan method orElse() dan orElseGet() yang sering kebalik. Perlu diperhatikan bahwa orElse() memiliki parameter constant, sedangkan orElseGet() memiliki parameter Functional Interface yang bisa ditulis menggunakan Lambda atau Method Reference. Jangan sampai tertukar penggunaannya! Kalau kita memasukkan parameter berupa method yang akan dieksekusi ketika valuenya null ke method orElse(), maka method itu akan dieksekusi langsung meskipun tidak diperlukan saat value Optional-nya ada. Ini tentu tidak efisien. Begitu juga sebaliknya, kalau kita memasukkan constant value berupa null ke paramater orElseGet(), maka akan terjadi NullPointerException saat value Optional-nya tidak ada karena Functional Interface-nya akan dieksekusi ketika valuenya tidak ada.

public static String getStr(){
	System.out.println("executed!");
	return "";
}

public static void main(String[] args){
	Optional<String> vercel = Optional.of("vercel");
	vercel.orElseGet(() -> getStr()); //getStr() won't be executed if "vercel" is not empty✅

	Optional<String> str = Optional.empty();
	str.orElse(null); //will return null if "str" is empty✅

	Optional<String> vercel = Optional.of("vercel");
	vercel.orElse(getStr()); //getStr() executed even if "vercel" is not empty😱

	Optional<String> str = Optional.empty();
	str.orElseGet(null); //will throws error if "str" is empty🤯
}

Makanya, jangan sampai kebalik antara orElse() dan orElseGet()☺️.

Seringkali gw temui banyak yang mengekstrak value dari objek Optional duluan dan menggunakan conditional logic secara manual. Justru sebenarnya ini melewatkan fitur dari Optional itu sendiri. Optional jadi malah ga terasa manfaatnya.

Optional<Phone> optional = getPhone();
Phone phone = optional.orElse(null);
if(phone != null){
	int id = phone.id();
	if(id != 0){
		String name = phone.name();
		if(name != null){
			System.out.println("name = " + name);
		}
	}
}

Solusinya manfaatkan method map() dan filter() sesuai fungsinya.

Optional<Phone> optional = getPhone();
optional.filter(phone -> phone.id() != 0)
		.map(Phone::name)
		.ifPresent(name -> System.out.println("name = " + name));

Ini juga banyak gw temukan, menggunakan Optional.of() untuk membungkus value yang nullable. Kadang orang-orang kurang aware bahwa ini dapat menghasilkan NullPointer kalau valuenya null.

void initOptional(String hello){
	Optional<String> object = Optional.of(hello);
}

Solusinya adalah gunakan Optional.ofNullable() untuk membungkus value yang nullable.

void initOptional(String hello){
	Optional<String> object = Optional.ofNullable(hello);
}

Selanjutnya adalah menggunakan Optional untuk hal sederhana. Terlalu overuse jika menggunakan Method Chaining hanya untuk menentukan value yang ingin di-return berdasarkan nullable dari sebuah value.

String getSomething(String hello){
	return Optional.ofNullable(hello).orElse("world");
}

Solusinya gunakan ternary operator biasa untuk hal sederhana. Kecuali ada pengecekan null yang sedikit kompleks.

String getSomething(String hello){
	return hello == null ? "world" : hello;
}

String getPersonAddressName(Person person){
	return Optional.ofNullable(person)
			.map(Person::getAddress)
			.map(Address::getName)
			.orElse("world");
}

Fungsi utama dari Optional adalah mencegah NullPointer ketika mendapatkan return objek dari method lain. Menggunakan Optional sebagai field sebuah class justru anti-pattern apalagi pada Java Bean. Karena Optional itu sendiri tidak Serializable, jadi ga bisa serialized dong.

public class Person implements Serializable{
	Optional<Integer> id;
	Optional<String> name;
}

Solusinya adalah tetap gunakan tipe data yang umum sebagai field.

public class Person implements Serializable{
	Integer id;
	String name;
}

Sama seperti field, penggunaan Optional pada parameter sebuah method atau constructor juga anti-pattern karena tidak sesuai fungsinya. Menggunakan parameter Optional justru membuat ribet user yang ingin menggunakan method tersebut karena harus membungkus valuenya jadi Optional setiap pemanggilan.

void print(int code, Optional<Phone> phone){
	status.map(Phone::name)
			.ifPresent(name -> System.out.println("name = " + name));
	System.out.println("code = " + code);
}

Solusinya adalah kalau memang parameternya nullable, maka valuenya dibungkus Optional di dalam method itu saja. Jadi user ga perlu membungkus sendiri menjadi Optional tiap memanggil method tersebut.

void print(int code, Phone phone){
	Optional<Phone> phoneOpt = Optional.ofNullable(phone);
	phoneOpt.map(Phone::name)
			.ifPresent(name -> System.out.println("name = " + name));
	System.out.println("code = " + code);
}

Ini juga anti-pattern karena salah satu tujuan Optional adalah menggantikan peran value null dengan objek kosong. Sedangkan pada Collection kita bisa mengakalinya menggunakan empty Collection.

Optional<Collection<Integer>> getCollection(){
	return Optional.ofNullable(new ArrayList<>());
}

Solusinya ga usah repot-repot membungkus Collection menjadi Optional. Jangan return null pada Collection, cukup return objek empty Collection kalau ingin mengembalikan Collection kosong. Ini juga berlaku pada Map.

Collection<Integer> getCollection(){
	return Collections.emptyList();
}

Jika sebelumnya membungkus Collection dengan Optional, kali ini adalah membungkus value dari Collection dengan Optional. Ini juga sama anti-pattern dan merupakan bad practice menyimpan nullable value pada Collection. Cuma bikin gendut Collection aja memasukkan value yang ga ada isinya. Code juga jadi susah dihandle saat dipakai.

Collection<Optional<String>> getCollectionValue(String value1, String value2){
	List<Optional<String>> list = new ArrayList<>();
	list.add(Optional.ofNullable(value1));
	list.add(Optional.ofNullable(value2));
	return list;
}

Solusinya adalah tetap gunakan tipe objek biasa, dan kalau value-nya null jangan ditambahkan ke dalam Collection. Ini juga berlaku pada Map.

Collection<String> getCollectionValue(String value1, String value2){
	List<String> list = new ArrayList<>();
	if(value1 != null) list.add(value1);
	if(value2 != null) list.add(value2);
	Optional<String> value3 = getValue();
	value3.ifPresent(list::add);
	return list;
}

Menggunakan Optional pada Stream Lambda juga harus dihindari. Alasannya sama dengan poin Optional pada value Collection di atas.

List<Phone> phones = getPhones();
List<Optional<String>> phoneNames = phones.stream()
		.map(p -> Optional.ofNullable(p.name()))
		.collect(Collectors.toList());

Makanya sebisa mungkin maksimalkan fitur map() dan filter() pada Stream dan simpan valuenya sebagai tipe objek biasa.

List<Phone> phones = getPhones();
List<String> phoneNames = phones.stream()
		.map(Phone::name)
		.filter(Objects::nonNull)
		.collect(Collectors.toList());

Itulah beberapa kesalahan yang umum ditemui ketika menggunakan Optional pada Java. Kesalahan-kesalahan itu yang menjadi jebakan membuat pemakaian Optional justru jadi anti-pattern karena tidak sesuai fungsinya. Kadang juga malah membuat code jadi lebih ribet. Bahkan kesalahan tersebut dapat berakibat fatal seperti Exception yang tidak diharapkan.