author-pic

Ferry S

An ISTJ, Type 5, Engineer, Gamer, and Thriller-Movies-Lover
Immutable Collection untuk Java 8
Sun. Aug 1st, 2021 06:54 PM5 mins read
Immutable Collection untuk Java 8
Source: Bing Image creator - funny image about code "IMMUTABLE"

Beberapa postingan terakhir gw lebih sering post tulisan di luar coding karena berbagai hal, termasuk salah satunya lagi berduka sejak ditinggal nenek😥. Sekarang gw mulai mencoba menulis tentang codingan lagi🙂.

Seperti kita ketahui, pada Java terdapat beberapa inteface Collection seperti Set, List dan Map. Pada Java 8 ke bawah, Collection tersebut biasanya mutable. Walaupun ada Arrays.asList(), tapi tetap saja itu ga terhitung Immutable karena masih bisa dimodifikasi elemennya. Pada beberapa kasus kita ingin collection tersebut menjadi immutable. Beberapa alasannya pernah gw bahas di postingan tentang Mutable Object dan Global Variable, salah satunya agar elemennya konsisten dan cocok dijadikan public constant. Pada Java 9 ke atas sudah tersedia static factory seperti List.of(), Set.of() dan Map.of() untuk Immutable Collection. Sayangnya untuk pengguna Java 8 harus melakukan tugas extra dengan membungkus Mutable Collection dengan method-method seperti Collections.unmodifableList(), Collections.unmodifiableSet(), dan Collections.unmodifiableMap() agar immutable.

Pada Java 9 bisa menggunakan static factory method of() pada masing-masing collection.

public static void main(String[] args){
	List<String> immutableList = List.of("satu", "dua");
}

Sedangkan untuk Java 8 by default kita harus setup terlebih dahulu.

public static void main(String[] args){
	List<String> immutableList = Collections.unmodifiableList(getMutableList());
	//will throws exception when mutate the collections
}

private static List<String> getMutableList(){
	List<String> mutableList = new ArrayList<>();
	mutableList.add("satu"); //can modify
	mutableList.add("dua"); //can modify
	return mutableList;
}

Agak effort sih😕.

Salah satu cara yang bisa digunakan adalah dengan melakukan Double Brace Initialization. Contohnya seperti berikut:

public static void main(String[] args){
	List<Integer> integers = Collections.unmodifiableList(new ArrayList<Integer>(){{
		add(1);
		add(5);
		add(2);
	}});
}

Code-nya lebih singkat dan cukup readable. Tapi perlu diperhatikan, dengan Double Brace Initialization ini Java akan membuat anonymous class. Fyi, pada anonymous class Java akan membuat local instance class dan dianggap subtype baru dari sebuah class. Berbeda dengan lambda operator di Java 8 yang menggunakan invokedynamic, setara static method. Itu artinya penggunaan double brace ini nantinya akan ada reference objek terselubung dari class tersebut terhadap instance yang dapat menjadi memory leaks. Secara penggunaan juga agak verbose, kita tidak bisa menggunakan diamond operator saat initialization, melainkan harus menuliskan type saat bikin instance. Oleh karena itu double brace ini dianggap bad practice dan anti-pattern, sebaiknya dihindari.

Gw punya solusinya, yaitu dengan membuat utilitas mirip static factory di Java 9. Secara behavior di belakangnya beda sih, karena di Java 9 menggunakan binary operation. Sedangkan ide gw hanya shortcut dari utilities yang udah ada, hanya mirip di bagian penggunaannya aja sih😅.

public final class CollectionUtils{
	private CollectionUtils(){
	}

	@SafeVarargs
	public static <E> List<E> listOf(E... elements){
		return Collections.unmodifiableList(Arrays.asList(elements));
	}
}

Pada code di atas, kita bikin public static method dengan return generic type of List dan parameter varargs agar value bisa di-input dengan Comma-Separated. Kita tinggal panggil method Arrays.asList() dan bungkus dengan Collections.unmodifiableList() lalu return. Tidak lupa juga tambahkan @SafeVarargs pada method agar di-optimize compiler karena kita menggunakan satu parameter varargs. Simple bukan?

Tapi kita perlu optimize sedikit lagi untuk code yang menggunakan element kosong atau satu elemen doang. Karena dengan varargs, semua elemen akan di-convert menjadi Array. Untuk itu perlu overload method listOf() dengan dua method lagi.

public static <E> List<E> listOf(E element){
	return Collections.singletonList(element);
}

public static <E> List<E> listOf(){
	return Collections.emptyList();
}

Untuk Set juga bisa melakukannya dengan code yang hampir sama dengan List di atas.

@SafeVarargs
public static <E> Set<E> setOf(E... elements){
	return Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(elements)));
}

public static <E> Set<E> setOf(E element){
	return Collections.singleton(element);
}

public static <E> Set<E> setOf(){
	return Collections.emptySet();
}

Mirip-mirip dengan List, bedanya kita harus convert Arrays.asList() menjadi Set terlebih dulu sebelum dibikin Immutable. Kita menggunakan LinkedHashSet agar by default terurut berdasarkan element yang masuk duluan.

public static void main(String[] args){
	List<String> emptyList = CollectionUtils.listOf();
	List<String> singleList = CollectionUtils.listOf("satu");
	List<String> list = CollectionUtils.listOf("satu", "dua");
	List<String> list2 = CollectionUtils.listOf("satu", "tiga", "dua");

	Set<Integer> emptySet = CollectionUtils.setOf();
	Set<Integer> singleSet = CollectionUtils.setOf(1);
	Set<Integer> set = CollectionUtils.setOf(1, 3, 2);
	Set<Integer> set2 = CollectionUtils.setOf(1, 3, 2, 5, 9);
}

Sekarang penggunaannya jadi terlihat lebih elegan😎.

Untuk Map emang agak lain implementasinya. Kalau di Java 9 ke atas bisa pakai Map.of() atau Map.ofEntries(). Di utils ini gw ingin penggunaannya lebih less error-prone, karena di Map.of() Java 9 varargs-nya harus berpasang-pasangan, dan itu cukup error-prone menurut gw penggunaannya. Disini gw punya ide menggunakan Functional Interface dan Map Builder. Untuk itu kita butuh beberapa setup.

public class MapBuilder<K, V>{
	private final Map<K, V> map;

	public MapBuilder(Map<K, V> map){
		this.map = map;
	}

	public MapBuilder<K, V> put(K key, V value){
		map.put(key, value);
		return this;
	}

	private Map<K, V> build(){
		return Collections.unmodifiableMap(map);
	}
}

Gw bikin MapBuilder agar bisa menampung objek Map yang asli sebelum objek Map dikunci menjadi Immutable. Pada method put() gw mengembalikan objek MapBuilder itu sendiri agar method-nya jadi fluent dan bisa digunakan secara berantai tanpa harus dipisah semi-colon. Pada method build() inilah kita melakukan operasi pembungkusan mutable Map menjadi immutable.

@FunctionalInterface
public interface MapBuilderFunction<K, V> extends UnaryOperator<MapBuilder<K, V>>{ }

Kita perlu membuat Functional Interface agar dapat menggunakan lambda style. Cukup extends Interface UnaryOperator, karena sebenarnya Java sudah menyediakan Functional Interface yang parameter dan return value-nya tipe yang sama. Hanya saja kita perlu membuat interface baru agar kita bisa "memaksa" user agar hanya boleh menggunakan objek MapBuilder sebagai parameter dan return value. Annotasi @FunctionalInterface ditambahkan agar di-optimize compiler.

public static <K, V> Map<K, V> mapOf(MapBuilderFunction<K, V> mapBuilderFunction){
	MapBuilder<K, V> mapBuilder = mapBuilderFunction.apply(new MapBuilder<>(new LinkedHashMap<>()));
	return mapBuilder.build();
} 

Untuk code-nya tinggal apply aja interface MapBuilderFunction dan MapBuilder tadi dan execute menggunakan LinkedHashMap. Build map dari MapBuilder, lalu return. Disini alasan gw menggunakan LinkedHashMap sebagai objek Map sama seperti contoh pada Set di atas, agar by default terurut berdasarkan element yang masuk duluan.

public static void main(String[] args){
	Map<Integer, String> immutableMap = CollectionUtils.mapOf(map -> map
			.put(1, "satu")
			.put(3, "tiga")
			.put(2, "dua")
	);
}

Khusus Immutable Map ini agak lain emang😁, kita memanfaatkan lambda style untuk construct Map-nya.

Java 8 sendiri sudah punya utils untuk immutable Collection, tapi agak beda dengan Java 9, ga ada static factory-nya pada masing-masing collection. Untuk itu kita bisa mengakalinya dengan membuat utilitas sendiri. Minimal user experience-nya mirip-mirip lah. Oh ya, sebenarnya ada juga sih library-nya, tapi gw sendiri belum pernah coba, pernah liat doang. Menurut gw kalau bisa diakali dengan utilitas sendiri, ngapain harus repot-repot import library yang belum tentu kepake keseluruhan utils-nya? Bikin gendut jar-nya aja🤪.