Best Practices dari “Effective Java (3rd Edition)”

Kutipan dari “Effective Java (3rd Edition) ” karya Joshua Bloch

1. Kembalikan collection atau array kosong, bukan null

Jika method mengembalikan Set, jangan return null; untuk hasil kosong – gunakan return Set.of(); sebagai gantinya.

2. Pertimbangkan static factory method alih-alih constructor

public static Boolean valueOf(boolean b) {
    return b ? Boolean.TRUE : Boolean.FALSE;
}

Tidak selalu lebih baik, tetapi dalam beberapa kasus menawarkan banyak keuntungan:

Kekurangan:

3. Pertimbangkan builder saat menghadapi banyak parameter constructor

Static factory dan constructor memiliki keterbatasan yang sama: keduanya tidak cocok untuk parameter opsional dalam jumlah besar. Masalah ini memukul cukup telak beberapa proyek yang pernah saya kerjakan.

4. Terapkan sifat singleton dengan constructor private atau tipe enum

Apa pun yang tidak membutuhkan banyak instance sebaiknya hanya diinstansiasi sekali – entah dengan @ApplicationScoped atau pola singleton lain.

5. Cegah instansiasi dengan constructor private

Sesekali Anda ingin menulis class yang hanya berisi kumpulan method static dan field static. Class semacam ini punya reputasi buruk karena sebagian orang menyalahgunakannya untuk menghindari berpikir secara berorientasi objek, tetapi class seperti ini memang punya kegunaan yang sah.

private MyStaticClass() {
    // private constructor to prevent instantiation
}

6. Hindari membuat objek yang tidak perlu

String s = new String("bikini");  // DON'T DO THIS!

Atau:

// Hideously slow! Can you spot the object creation?
private static long sum() {
    Long sum = 0L;
    for (long i = 0; i <= Integer.MAX_VALUE; i++)
        sum += i;
    return sum;
}

Solusi: Program ini memberikan jawaban yang benar, tetapi jauh lebih lambat dari seharusnya, karena salah ketik satu karakter. Variabel sum dideklarasikan sebagai Long alih-alih long, yang berarti program membuat sekitar 2^31 instance Long yang tidak perlu (kira-kira satu untuk setiap kali long i ditambahkan ke Long sum).

7. Hindari finalizer dan cleaner

Finalizer tidak dapat diprediksi, sering berbahaya, dan umumnya tidak diperlukan. Penggunaannya dapat menyebabkan perilaku yang tidak menentu, performa buruk, dan masalah portabilitas.

8. Utamakan try-with-resources daripada try-finally

// try-with-resources - the best way to close resources!
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
    return br.readLine();
}

9. Pertimbangkan untuk mengimplementasikan Comparable

Dengan mengimplementasikan Comparable, class Anda dapat bekerja sama dengan semua algoritma generik dan implementasi collection yang bergantung pada interface ini.

10. Minimalkan aksesibilitas class dan member

Satu-satunya faktor terpenting yang membedakan komponen yang dirancang dengan baik dari yang dirancang dengan buruk adalah sejauh mana komponen tersebut menyembunyikan data internal dan detail implementasi lainnya dari komponen lain. Komponen yang dirancang dengan baik menyembunyikan seluruh detail implementasinya, memisahkan API dari implementasinya secara bersih. Komponen-komponen kemudian hanya berkomunikasi melalui API masing-masing dan tidak mengetahui cara kerja internal satu sama lain. Konsep ini, yang dikenal sebagai information hiding atau enkapsulasi, adalah prinsip dasar desain perangkat lunak.

11. Di class public, gunakan method akses, bukan field public

Sesekali Anda mungkin tergoda menulis class yang “merosot” (degenerate), yang tidak punya tujuan lain selain mengelompokkan field instance:

// Degenerate classes like this should not be public!
class Point {
    public double x;
    public double y;
}

Karena field data class semacam ini diakses secara langsung, class ini tidak menawarkan manfaat enkapsulasi. Anda tidak bisa mengubah representasinya tanpa mengubah API, tidak bisa menegakkan invarian, dan tidak bisa melakukan tindakan tambahan saat sebuah field diakses. Programmer berorientasi objek garis keras berpendapat bahwa class semacam ini terlarang dan harus selalu diganti dengan class yang memiliki field private dan method akses public (getter), serta untuk class yang mutable, mutator (setter):

// Encapsulation of data by accessor methods and mutators
class Point {

    private double x;
    private double y;

    public Point(double x, double y) {
        this.x = x;
        this.y = y;
    }

    public double getX() { return x; }
    public double getY() { return y; }

    public void setX(double x) { this.x = x; }
    public void setY(double y) { this.y = y; }
}

Tentu saja, kaum garis keras itu benar dalam hal class public: jika sebuah class dapat diakses di luar package-nya, sediakan method akses untuk menjaga fleksibilitas dalam mengubah representasi internal class tersebut. Jika class public mengekspos field datanya, semua harapan untuk mengubah representasinya hilang, karena kode klien bisa sudah tersebar luas ke mana-mana.

Namun, jika sebuah class bersifat package-private atau merupakan nested class private, tidak ada yang salah secara mendasar dengan mengekspos field datanya—asalkan field tersebut cukup baik menggambarkan abstraksi yang disediakan class itu.

12. Minimalkan mutabilitas

Class immutable hanyalah class yang instance-nya tidak dapat diubah. Semua informasi yang terkandung dalam setiap instance bersifat tetap sepanjang masa hidup objek, sehingga tidak pernah ada perubahan yang dapat diamati. Library platform Java berisi banyak class immutable, termasuk String, class primitif boxed, serta BigInteger dan BigDecimal. Ada banyak alasan bagus untuk ini: Class immutable lebih mudah dirancang, diimplementasikan, dan digunakan daripada class mutable. Class ini lebih tahan terhadap kesalahan dan lebih aman. Untuk membuat class menjadi immutable, ikuti lima aturan berikut:

  1. Jangan sediakan method yang mengubah state objek (dikenal sebagai mutator).
  2. Pastikan class tidak dapat di-extend. Ini mencegah subclass yang ceroboh atau berniat jahat merusak perilaku immutable class dengan bertindak seolah-olah state objek telah berubah. Mencegah pembuatan subclass umumnya dilakukan, misalnya, dengan menjadikan class tersebut final.
  3. Jadikan semua field final. Ini menyatakan maksud Anda dengan jelas dengan cara yang ditegakkan oleh sistem. Selain itu, hal ini diperlukan untuk menjamin perilaku yang benar jika referensi ke instance yang baru dibuat dioper dari satu thread ke thread lain tanpa sinkronisasi, sebagaimana dijabarkan dalam memory model.
  4. Jadikan semua field private. Ini mencegah klien memperoleh akses ke objek mutable yang dirujuk oleh field dan mengubah objek tersebut secara langsung. Meskipun secara teknis class immutable boleh memiliki field public final yang berisi nilai primitif atau referensi ke objek immutable, hal ini tidak dianjurkan karena menutup kemungkinan mengubah representasi internal pada rilis berikutnya.
  5. Pastikan akses eksklusif ke setiap komponen mutable. Jika class Anda memiliki field yang merujuk ke objek mutable, pastikan klien class tersebut tidak dapat memperoleh referensi ke objek-objek itu. Jangan pernah menginisialisasi field semacam itu dengan referensi objek dari klien atau mengembalikan field tersebut dari method akses. Buat salinan defensif di constructor, method akses, dan method readObject.

13. Utamakan komposisi daripada pewarisan

Pewarisan (inheritance) adalah cara yang ampuh untuk menggunakan ulang kode, tetapi tidak selalu menjadi alat terbaik untuk pekerjaan tersebut. Jika digunakan secara tidak tepat, pewarisan menghasilkan perangkat lunak yang rapuh. Pewarisan aman digunakan di dalam satu package, di mana implementasi subclass dan superclass berada di bawah kendali programmer yang sama. Pewarisan juga aman digunakan saat meng-extend class yang memang dirancang dan didokumentasikan untuk di-extend. Namun, mewarisi class konkret biasa melintasi batas package itu berbahaya.

Tidak seperti pemanggilan method, pewarisan melanggar enkapsulasi. Anda mungkin mengira aman untuk meng-extend sebuah class jika Anda hanya menambahkan method baru dan tidak meng-override method yang sudah ada. Meskipun extend semacam ini jauh lebih aman, ia tetap tidak bebas risiko. Jika superclass mendapat method baru pada rilis berikutnya dan Anda sedang sial karena telah memberi subclass sebuah method dengan signature yang sama tetapi tipe kembalian berbeda, subclass Anda tidak akan bisa dikompilasi lagi. Jika Anda telah memberi subclass method dengan signature dan tipe kembalian yang sama dengan method baru superclass, berarti kini Anda meng-override-nya, sehingga Anda terkena masalah yang dijelaskan sebelumnya. Selain itu, diragukan bahwa method Anda akan memenuhi kontrak method baru superclass tersebut, karena kontrak itu belum ditulis saat Anda menulis method subclass.

Untungnya, ada cara untuk menghindari semua masalah di atas. Alih-alih meng-extend class yang sudah ada, beri class baru Anda sebuah field private yang merujuk ke instance dari class yang sudah ada itu. Desain ini disebut komposisi karena class yang sudah ada menjadi komponen dari class yang baru. Setiap method instance di class baru memanggil method yang bersesuaian pada instance class lama yang dikandungnya dan mengembalikan hasilnya. Ini dikenal sebagai forwarding, dan method di class baru disebut forwarding method. Class yang dihasilkan akan kokoh sekali, tanpa ketergantungan pada detail implementasi class yang sudah ada. Bahkan menambahkan method baru ke class lama tidak akan berdampak pada class baru.

14. Utamakan interface daripada abstract class

Java memiliki dua mekanisme untuk mendefinisikan tipe yang memungkinkan banyak implementasi: interface dan abstract class. Sejak diperkenalkannya default method untuk interface di Java 8, kedua mekanisme memungkinkan Anda menyediakan implementasi untuk sebagian method instance.

Perbedaan utamanya adalah, untuk mengimplementasikan tipe yang didefinisikan oleh abstract class, sebuah class harus menjadi subclass dari abstract class tersebut. Karena Java hanya mengizinkan pewarisan tunggal, batasan pada abstract class ini sangat membatasi penggunaannya sebagai definisi tipe. Class mana pun yang mendefinisikan semua method yang dibutuhkan dan mematuhi kontrak umumnya boleh mengimplementasikan interface, terlepas dari posisinya dalam hierarki class. Class yang sudah ada dapat dengan mudah disesuaikan untuk mengimplementasikan interface baru.

15. Generics: Jangan gunakan raw type

Anda baru menemukan error saat runtime, lama setelah error itu terjadi, dan di kode yang mungkin jauh dari kode yang mengandung error. Begitu melihat ClassCastException, Anda harus mencari di seluruh codebase pemanggilan method yang memasukkan koin ke dalam koleksi perangko. Compiler tidak bisa membantu Anda.

16. Generics: Hilangkan peringatan unchecked

Saat memprogram dengan generics, Anda akan melihat banyak peringatan compiler:

Makin banyak pengalaman Anda dengan generics, makin sedikit peringatan yang Anda dapat, tetapi jangan berharap kode yang baru ditulis bisa dikompilasi tanpa peringatan. Banyak peringatan unchecked yang mudah dihilangkan. Misalnya, anggap Anda tanpa sengaja menulis deklarasi ini:

Set<Lark> exaltation = new HashSet();

Ubah menjadi:

Set<Lark> exaltation = new HashSet<>();

Sebagian peringatan akan jauh lebih sulit dihilangkan. Jika Anda mendapat peringatan yang butuh pemikiran, tekunlah! Hilangkan setiap peringatan unchecked yang bisa Anda hilangkan.

Jika Anda menghilangkan semua peringatan, Anda terjamin bahwa kode Anda typesafe, dan itu hal yang sangat baik. Artinya Anda tidak akan mendapat ClassCastException saat runtime, dan ini menambah keyakinan Anda bahwa program akan berperilaku sesuai maksud Anda. Jika Anda tidak bisa menghilangkan sebuah peringatan, tetapi dapat membuktikan bahwa kode yang memicunya typesafe, barulah (dan hanya saat itu) redam peringatan tersebut dengan anotasi @SuppressWarnings("unchecked"). Jika Anda meredam peringatan tanpa terlebih dahulu membuktikan bahwa kodenya typesafe, Anda memberi diri sendiri rasa aman yang palsu. Kode mungkin terkompilasi tanpa satu peringatan pun, tetapi tetap bisa melempar ClassCastException saat runtime.

Setiap kali Anda memakai anotasi @SuppressWarnings("unchecked"), tambahkan komentar yang menjelaskan mengapa hal itu aman.

17. Generics: Kapan masuk akal

Aduh – sebuah class yang sangat membutuhkan generics:

Contoh class yang membutuhkan generics

Hal seperti ini paling lambat harus tertangkap saat merge request!

18. Generics: Contoh yang tidak boleh ditiru

Maaf, tak perlu banyak kata. Tolong JANGAN PERNAH merge ini:

public abstract class JpaLearningCardBaseContentService<LC extends LearningCardBaseContent<L, LC, AC>, J extends JpaLearningCardBaseContent<L, LC, AC>, L extends
        LearningCardBase<LC, A>, A extends AnswerOptionBase<L, AC>, AC extends AnswerOptionBaseContent<LC, AC, A>, AS extends AnswerOptionBaseService<A, AC, L, LC>, ACS extends
        AnswerOptionBaseContentService<AC, A, L, LC>, LS extends LearningCardBaseService<L>>
    extends JpaLightPersistenceService<LC, J, Long> implements LearningCardBaseContentService<LC, L, A, AC> {
    // ...
}

Sebuah deklarasi class saja tidak semestinya butuh 5 menit untuk dipahami.

19. Utamakan list daripada array

Array berbeda dari tipe generik dalam dua hal penting. Pertama, array bersifat kovarian. Kata yang terdengar menyeramkan ini sebenarnya hanya berarti bahwa jika Sub adalah subtipe dari Super, maka tipe array Sub[] adalah subtipe dari tipe array Super[]. Sebaliknya, generics bersifat invarian: untuk dua tipe berbeda mana pun Type1 dan Type2, List<Type1> bukan subtipe maupun supertipe dari List<Type2>.

20. Utamakan lambda daripada anonymous class

// Anonymous class instance as a function object - obsolete!
Collections.sort(words, new Comparator<String>() {
    public int compare(String s1, String s2) {
        return Integer.compare(s1.length(), s2.length());
    }
});

Sebagai gantinya, lakukan ini:

// Lambda expression as function object (replaces anonymous class)
Collections.sort(words, (s1, s2) -> Integer.compare(s1.length(), s2.length()));

21. Minimalkan cakupan variabel lokal

Teknik paling ampuh untuk meminimalkan cakupan variabel lokal adalah mendeklarasikannya di tempat ia pertama kali digunakan...

22. Utamakan loop for-each daripada loop for tradisional

// The preferred idiom for iterating over collections and arrays
for (Element e : elements) {
    ... // Do something with e
}

23. Kenali dan gunakan library

Misalkan Anda ingin menghasilkan bilangan bulat acak antara nol dan suatu batas atas. Menghadapi tugas umum ini, banyak programmer akan menulis method kecil yang kira-kira seperti ini:

// Common but deeply flawed!
static Random rnd = new Random();

static int random(int n) {
    return Math.abs(rnd.nextInt()) % n;
}

Method ini mungkin tampak bagus, tetapi memiliki tiga cacat. Pertama, jika n adalah perpangkatan dua yang kecil, deret bilangan acaknya akan berulang setelah periode yang cukup singkat. Cacat kedua adalah jika n bukan perpangkatan dua, sebagian angka rata-rata akan dikembalikan lebih sering daripada yang lain. Jika n besar, efek ini bisa cukup mencolok.

Cacat ketiga pada method random adalah, pada kesempatan yang jarang, ia bisa gagal secara fatal dengan mengembalikan angka di luar rentang yang ditentukan. Hal ini terjadi karena method tersebut mencoba memetakan nilai yang dikembalikan rnd.nextInt() ke int non-negatif dengan memanggil Math.abs. Jika nextInt() mengembalikan Integer.MIN_VALUE, maka Math.abs juga akan mengembalikan Integer.MIN_VALUE, dan operator sisa bagi (%) akan mengembalikan bilangan negatif, dengan asumsi n bukan perpangkatan dua. Ini hampir pasti akan membuat program Anda gagal, dan kegagalannya mungkin sulit direproduksi.

24. Hindari float dan double jika butuh jawaban yang eksak

Gunakan BigDecimal untuk jumlah uang. float dan double menghasilkan galat pembulatan.

Misalnya, anggap Anda punya $1.03 di saku dan membelanjakan 42¢. Berapa sisa uang Anda? Berikut potongan program naif yang mencoba menjawab pertanyaan ini:

System.out.println(1.03 - 0.42);

Sayangnya, yang tercetak adalah 0.6100000000000001. Ini bukan kasus tunggal. Misalkan Anda punya satu dolar di saku, lalu membeli sembilan ring baut seharga sepuluh sen per buah. Berapa kembalian yang Anda terima?

System.out.println(1.00 - 9 * 0.10);

Menurut potongan program ini, Anda menerima $0.09999999999999998.

Anda mungkin berpikir masalah ini bisa diselesaikan cukup dengan membulatkan hasil sebelum dicetak, tetapi sayangnya cara ini tidak selalu berhasil. Misalnya, anggap Anda punya satu dolar di saku, dan Anda melihat rak berisi sederet permen lezat seharga 10¢, 20¢, 30¢, dan seterusnya, hingga satu dolar. Anda membeli satu dari setiap permen, mulai dari yang seharga 10¢, sampai uang Anda tidak cukup lagi untuk membeli permen berikutnya di rak.

25. Utamakan tipe primitif daripada boxed primitive

// Broken comparator - can you spot the flaw?
Comparator<Integer> naturalOrder = (i, j) -> (i < j) ? -1 : (i == j ? 0 : 1);

26. Rujuk objek melalui interface-nya

Contoh yang baik:

Set<Son> sonSet = new LinkedHashSet<>();

Contoh yang buruk:

LinkedHashSet<Son> sonSet = new LinkedHashSet<>();

27. Utamakan interface daripada reflection

Jika tidak:

28. Patuhi konvensi penamaan yang berlaku umum

Platform Java memiliki serangkaian konvensi penamaan yang sudah mapan, banyak di antaranya tercantum dalam The Java Language Specification [JLS, 6.1].

29. Gunakan exception hanya untuk kondisi luar biasa

// Horrible abuse of exceptions. Don't ever do this!
try {
    int i = 0;
    while (true) {
        range[i++].climb();
    }
} catch (ArrayIndexOutOfBoundsException e) {
    // catch it
}

Apa yang dilakukan kode ini? Sama sekali tidak jelas jika hanya dilihat, dan itu sudah cukup menjadi alasan untuk tidak memakainya. Ternyata ini adalah idiom yang dirancang dengan sangat buruk untuk melakukan loop pada elemen sebuah array. Loop tanpa akhir ini berhenti dengan melempar, menangkap, dan mengabaikan ArrayIndexOutOfBoundsException ketika ia mencoba mengakses elemen pertama di luar batas array. Kode ini seharusnya setara dengan idiom standar untuk melakukan loop pada array, yang langsung dikenali oleh setiap programmer Java:

for (Mountain m : range) {
    m.climb();
}

Exception, sesuai namanya, hanya boleh digunakan untuk kondisi luar biasa; exception tidak boleh digunakan untuk alur kontrol biasa.

30. Gunakan checked exception untuk kondisi yang dapat dipulihkan dan runtime exception untuk kesalahan pemrograman

Gunakan checked exception untuk kondisi yang secara wajar dapat diharapkan bisa dipulihkan oleh pemanggil. Dengan menghadapkan pengguna pada checked exception, perancang API memberikan mandat untuk memulihkan diri dari kondisi tersebut. Pengguna bisa mengabaikan mandat ini dengan menangkap exception lalu mengabaikannya, tetapi biasanya itu ide yang buruk.

Gunakan runtime exception untuk menandakan kesalahan pemrograman. Sebagian besar runtime exception menandakan pelanggaran prasyarat (precondition). Pelanggaran prasyarat hanyalah kegagalan klien sebuah API untuk mematuhi kontrak yang ditetapkan oleh spesifikasi API tersebut. Misalnya, kontrak akses array menetapkan bahwa indeks array harus berada antara nol dan panjang array dikurangi satu, inklusif. ArrayIndexOutOfBoundsException menandakan bahwa prasyarat ini telah dilanggar.

31. Hindari penggunaan checked exception yang tidak perlu

Singkatnya, bila digunakan secukupnya, checked exception dapat meningkatkan keandalan program; bila digunakan berlebihan, checked exception membuat API menyusahkan untuk dipakai.

Pair<Boolean, LearningCardGroup> answerLearningCard(@NotNull LearningCardGroup learningCardGroup,
                                                      LearningCardAssignment learningCardAssignment,
                                                      @NotNull Collection<AnswerOption> answers)
      throws InvalidTrainingStateException, PersistenceException, LearningCardGroupAlreadyCompletedException,
             ConsecutiveCorrectCountExceededException,
             PreviousLearningCardGroupUncompletedException, LearningCardGroupCooldownException,
             LearningCardGroupHasUncompletedLearningCardsException, InvalidParticipantException, TrainingNotSignedException; // but why ???
     // this is a mistake seen a lot with junior devs
     // exceptions are exceptionally slow and should be avoided for mere validation
     // the performance gets even worse if a stacktrace is printed, since logging and printing is one of the slowest possible actions

32. Utamakan penggunaan exception standar

Salah satu ciri yang membedakan programmer ahli dari yang kurang berpengalaman adalah para ahli berusaha mencapai, dan biasanya berhasil mencapai, tingkat penggunaan ulang kode yang tinggi.

Exception bukanlah pengecualian dari aturan bahwa penggunaan ulang kode itu baik. Library Java menyediakan sekumpulan exception yang mencakup sebagian besar kebutuhan pelemparan exception pada kebanyakan API. Menggunakan ulang exception standar memiliki beberapa manfaat. Yang paling utama, API Anda menjadi lebih mudah dipelajari dan dipakai karena sesuai dengan konvensi mapan yang sudah dikenal programmer. Yang hampir sama pentingnya, program yang memakai API Anda lebih mudah dibaca karena tidak dipenuhi exception yang asing. Terakhir (dan paling tidak penting), lebih sedikit class exception berarti jejak memori yang lebih kecil dan lebih sedikit waktu untuk memuat class.

Tipe exception yang paling sering digunakan ulang adalah IllegalArgumentException. Umumnya inilah exception yang dilempar ketika pemanggil mengoper argumen dengan nilai yang tidak sesuai. Misalnya, exception ini yang dilempar jika pemanggil mengoper bilangan negatif ke parameter yang menyatakan berapa kali suatu tindakan harus diulang. Exception lain yang sering digunakan ulang adalah IllegalStateException. Umumnya inilah exception yang dilempar jika pemanggilan tidak sah karena state objek penerimanya. Misalnya, exception ini yang dilempar jika pemanggil mencoba memakai suatu objek sebelum objek itu diinisialisasi dengan benar.

Bisa dibilang, setiap pemanggilan method yang keliru pada akhirnya bermuara pada argumen atau state yang tidak sah, tetapi ada exception lain yang secara standar dipakai untuk jenis argumen dan state tidak sah tertentu. Jika pemanggil mengoper null ke parameter yang tidak mengizinkan nilai null, konvensi menetapkan bahwa yang dilempar adalah NullPointerException, bukan IllegalArgumentException. Demikian pula, jika pemanggil mengoper nilai di luar rentang ke parameter yang menyatakan indeks dalam sebuah urutan, yang seharusnya dilempar adalah IndexOutOfBoundsException, bukan IllegalArgumentException.

Jangan menggunakan ulang Exception, RuntimeException, Throwable, atau Error secara langsung. Perlakukan class-class ini seolah-olah abstract. Meski demikian, RuntimeException boleh digunakan jika Anda ingin mengubah checked exception menjadi runtime exception, dan Anda benar-benar tahu apa yang Anda lakukan.

33. Jangan abaikan exception

// Empty catch block ignores exception - Highly suspect!
try {
    ...
} catch (SomeException e) {
}

Blok catch yang kosong menggagalkan tujuan exception, yaitu memaksa Anda menangani kondisi luar biasa. Mengabaikan exception sama seperti mengabaikan alarm kebakaran – lalu mematikannya sehingga tidak ada orang lain yang sempat memeriksa apakah benar-benar ada kebakaran.