Gerakan Perlawanan Terhadap C++: Java

Java lahir dari rasa frustrasi beberapa karyawan Sun Microsystems terhadap C++. Mereka berpandangan bahwa C++ itu terlalu buncit, tidak aman, tidak punya threading, dan mempunyai manajemen memori manual yang rawan kesalahan. C++ terlalu rumit. Itulah sebabnya mereka menyebut Java sebagai “C++ ++ –”. Java memang terinspirasi oleh C++, tapi dia memangkas fitur-fitur C++ yang tidak perlu. Kenyataannya, dari sisi bahasa, Java 1.x jauh lebih sederhana dari C++. Banyak fitur-fitur yang ada di C++, dipangkas habis di Java.

OOP yang lebih mudah

Java menghilangkan kerumitan-kerumitan yang tidak perlu ketika harus berurusan dengan OOP:

  • Semua method pada dasarnya virtual

    Virtual method atau virtual function adalah fungsi yang ketika dipanggil sebagai klas leluhurnya, maka yang dipanggil adalah fungsi di klas turunanny. Di C++, fungsi apa saja yang bisa digantikan oleh fungsi turunanny harus dinyatakan sebagai virtual. Di Java, semua method adalah virtual. Satu kerumitan berkurang.

    #include <iostream>
    
    using namespace std;
    
    class Binatang {
    public:
        virtual void bersuara() { cout << "bersuara" << endl; }
        void bergerak() { cout << "bergerak" << endl; }
    };
    
    class Keledai: public Binatang {
    public:
        virtual void bersuara() {cout << "meringkik" << endl;  }
        void bergerak() { cout << "berjalan" << endl; }
    };
    
    int main() {
        Keledai keledai;
        Binatang& binatang = keledai;
        keledai.bersuara();
        keledai.bergerak();
        binatang.bersuara();
        binatang.bergerak();
    }
    
    //Outputnya adalah
    //meringkik
    //berjalan
    //meringkik
    //bergerak
    

    Jelas bahwa di C++, polimorfisme hanya bisa dicapai dengan adanya virtual function, sementara di Java, polimorfisme akan selalu terjadi.

    class Binatang {
        public void bersuara() { System.out.println("bersuara"); }
        public void bergerak() { System.out.println("bersuara"); }
    }
    
    class Keledai extends Binatang {
        public void bersuara() { System.out.println("meringkik");  }
        public void bergerak() { System.out.println("berjalan"); }
    }
    
    public class Main {
        public static void main(String[] args) {
            Keledai keledai = new Keledai();
            Binatang binatang = keledai;
            keledai.bersuara();
            keledai.bergerak();
            binatang.bersuara();
            binatang.bergerak();
        }
    }
    
    //Outputnya adalah:
    //meringkik
    //berjalan
    //meringkik
    //berjalan
    
  • Tidak ada public, protected, atau private inheritance

    Dengan demikian, tidak ada lagi kebingungan, karena semuanya adalah public inheritance. Setiap method di kelas leluhurnya akan selalu bisa diakses dari turunannya.

    Di C++:

    #include <iostream>
    
    using namespace std;
    
    class Ancestor {
    public:
        virtual void method1() { cout << "Ancestor method1" << endl; }
        void method2() { cout << "Ancestor method2" << endl; }
    };
    
    class Descendant: public Ancestor {
    public:
        virtual void method1() { cout << "Descendant virtual_method" << endl; }
    };
    
    int main() {
        Descendant descendant;
        descendant.method1();  // compile error kalo bukan public inheritance
        descendant.method2(); // compile error kalo bukan public inheritance
    
        return 0;
    }
    

    Di Java:

    class Ancestor {
        public void method1() {}
        public void method2() {}
    }
    
    class Descendant extends Ancestor {
    
    }
    
    class Main {
        public static void main(String[] args) {
            Descendant descendant = new Descendant();
            descendant.method1(); //Normal, karena semua inheritance adalah public inheritance
            descendant.method2(); //Normal, karena semua inheritance adalah public inheritance
        }
    }
    
  • Tidak ada multiple inheritance

    Tidak ada lagi kasus diamond inheritance. Tidak perlu lagi bingung untuk tahu method leluhur mana yang dipanggil, karena sudah pasti leluhurnya cuma satu. Namun, untuk mengakomodasi jurus-jurus OOP ala Design Patterns, klas di Java walaupun cuma punya satu leluhur, tapi bisa mengimplementasikan banyak interface.

    //C, D, dan E adalah interface
    public class B extends A implements C, D, E {/*...*/}
    
  • Adanya interface dan abstract class

    C++ mengenal yang namanya pure virtual method, di mana suatu method yang tidak diimplementasikan oleh klas leluhur harus diimplementasikan oleh klas turunannya. Para pembuat Java menyukai konsep ini, tapi tidak menyukai sintaks dan penamaannya di C++. Lahirlah interface di Java. Sebenarnya, secara informal, C++ menyebut klas yang punya pure virtual method dengan abstract class. Nah di Java, diperkenalkanlah abstract class dan interface.

    Di C++

    //interface ala "C++"
    class SomeInterface {
        virtual void method1() = 0; //pure virtual method, tidak ada implementasi
        virtual void method2() = 0; //pure virtual method, tidak ada implementasi
        virtual void method3() = 0; //pure virtual method, tidak ada implementasi
    };
    
    //abstract class ala "C++"
    class SomeAbstractClass {
        virtual void method1() = 0; //salah satu method-nya pure virtual
        virtual void method2() {/*...*/}
        void method3() {/*...*/}
    };
    
    //implementasi interface
    class AClass : SomeInterface {/*...*/};
    //turunan abstract class
    class AnotherClass : SomeAbstractClass {/*...*/};
    //implementasi interface, turunan abstract class
    class WhatClass : SomeAbstractClass, SomeInterface {/*...*/};
    

    Di Java

    interface SomeInterface {
        void method1();
        void method2();
        void method2();
    }
    
    abstract class SomeAbstractClass {
        abstract void method1();
        void method2() {/*...*/}
        void method3() {/*...*/}
    }
    
    //implementasi interface
    class AClass implements SomeInterface {/*...*/}
    //turunan abstract class
    class AnotherClass extends SomeAbstractClass {/*...*/}
    //implementasi interface, turunan abstract class
    class WhatClass extends SomeAbstractClass implements SomeInterface {/*...*/}
    

Manajemen memori otomatis

Kalo ada perbedaan antara Java dan C++ yang paling didengung-dengungkan, itu adalah manajemen memori otomatis. Padahal ya manajemen memori manual kan bukan masalah besar. Tinggal new sama delete aja kan? Ternyata tidak semudah itu. New dan delete itu sendiri memang gampang. Tapi, menentukan kapan harus new dan kapan harus delete adalah masalah pelik yang membuat banyak programmer C++ kelimpungan. Akibatnya sering terjadi memory leak, dan debugging-nya kalo code base udah besar banget, sangat susah. Dengan adanya garbage collector yang akan membersihkan semua variabel yang tidak dipakai lagi di memori, programmer Java dibebaskan dari bug-bug memory leak yang gak penting.

Cuman, garbage collector (GC) ala Java ini punya satu kelemahan. Dia adalah suatu proses yang berjalan sendiri, dan pada suatu saat, dia butuh untuk menghentikan program secara keseluruhan (“stop the world”) untuk membersihkan variabel-variabel yang tidak dipakai. Gak lucu kalo ada drone sedang menjalankan control loop untuk menyeimbangkan diri tiba-tiba programnya ngefreeze. Yah, walaupun makin ke sini, teknologi GC makin canggih. Sehingga, walaupun tidak sepenuhnya hilang, lamanya program berhenti makin berkurang, hingga hampir tidak terasa.

OOP yang lebih “murni”

Di Java, semuanya adalah objek, kecuali tipe data primitif. Kita sebagai programmer hanya bisa mendefinisikan objek baru, tidak bisa mendefinisikan tipe data primitif baru. Karena itu Java dikatakan OOP murni. Semua fungsi harus didefinisikan di dalam klas. Saya tidak tahu alasan Java mengambil jalan ini. Mungkin, Java sedikit terpengaruh oleh Smalltalk. Mungkin, saat itu dogma OOP sedang populer, sehingga OOP murni dianggap membantu programmer menjadi umat OOP yang lebih baik.

Tidak ada pointer

Sintaks pointer itu sebenarnya sederhana saja. Cuma ada &, *, dan ->. Dah, itu saja. Tapi, implikasi yang ditimbulkannya luar biasa. Berapa banyak programmer pemula yang tersandung oleh pointer ketika belajar pemrograman? Bahkan sampai hari ini, saya masih sering salah sintaks ketika berhadapan dengan pointer. Dengan menghilangkan pointer, para perancang Java berharap bahwa Java akan lebih mudah dikuasai oleh programmer-programmer baru.

Di C++

Dog* d = new Dog();
d->bark();
(*d).run();
Cat c;
chaseAway(&c);

Di Java

Dog d = new Dog();
d.bark();
d.run();
Cat c = new Cat();
chaseAway(c);

Sudah kebayang kan, ribetnya pointer? Karena itulah Java gak punya pointer. Nganu, sebenarnya Java punya pointer. Semua turunan object adalah pointer. Akan tetapi, anda bisa jadi programmer Java profesional tanpa perlu tahu fakta ini.

Dukungan array yang lebih baik

Di C++ dan C, array adalah tipe data setengah hati. Kalo pas di dalam fungsi, maka dia bisa jadi array. Tapi, begitu dimasukkan jadi parameter atau jadi nilai kembalian fungsi, maka dia akan jadi pointer. Di Java, tipe data array itu sungguh-sungguh ada. Jadi, ketika dimasukkan jadi parameter atau dikembalikan dari fungsi, dia tetep berupa array.

Di C++


//int_array sebenarnya sama saja dengan "int* int_array", tidak punya size. 
//Makanya, size harus dimasukkan terpisah
void sort(int int_array[], int size) {
    //katakanlah ini bubble sort
}

void func() {
    int  arr[5] = {3, 4, 1, 2, 5};
    sort(arr, sizeof(arr)/sizeof(arr[0]))
}

Di Java

public class Main {
    //Fungsi sort bisa menerima array yang berbeda-beda panjangnya saat runtime
    public static void sort(int[] int_array) {
        //katakanlah ini bubble sort
    }

    public static void main(String[] args) {
        int[]  arr = {3, 4, 1, 2, 5};
        sort(arr);
        System.out.println(arr.length);//array punya length
    }
}

Tidak ada generics

Walaupun pada akhirnya muncul di Java 5 karena tekanan dari C#, ketika muncul pertama kali, Java tidak mempunyai generics. Sebenarnya, tanpa adanya generics, Java tidak terlalu masalah. Secara umum memang generics keliatan sama saja dengan inheritance. Akan tetapi, ada hal-hal yang bisa dilakukan dengan generics yang tidak bisa dilakukan dengan inheritance. Salah satunya adalah soal static typing. Misalnya ketika mengimplementasikan List:

//List diimplementasikan dengan inheritance
class InheritanceList {
    public void add(Object item) {/*...*/}
    public void remove(Object item) {/*...*/}
    public Object get(int index) {/*...*/}
}

class IntList extends InheritanceList {
    public void add(Object item) {/*...*/}
    public void remove(Object item) {/*...*/}
    public Object get(int index) {/*...*/}
}

//List diimplementasikakn dengan generics, di Java 5
class GenericList<T> {
    public void add(T item) {/*...*/}
    public void remove(T item) {/*...*/}
    public T get(int index) {/*...*/}
}

Implementasi generics hanya bisa memaksa add, remove dan get hanya berurusan dengan tipe data T, bukan tipe data yang lain. Sementara implementasi dengan inheritance membuat add, remove, dan get seolah-olah bisa berurusan dengan tipe data apapun, padahal IntList hanya bisa menerima int.

Berkaca dari pengalaman buruk C++ yang implementasi template-nya yang terlalu canggih tapi juga masih belum sempurna (di antaranya karena tidak adanya generic constraint), tidak mengimplementasikan generics kayaknya ide bagus. Bahkan ketika akhirnya generics muncul di Java, implementasinya mempunya sintaks yang jauh lebih sederhana dari sintaks di C++.

Tidak ada header file

Kalo anda baca soal header file di artikel sebelumnya, anda akan tahu betapa ribetnya harus nulis deklarasi kemudian harus nulis definisi secara terpisah. Sudah demikian, masih sering pula programmer salah tulis. Java menghapus keribetan soal tetek bengek header file dan menggantinya dengan sedikit keribetan ala java. Nama source file Java harus sama dengan nama class-nya, dan file tersebut harus ditaruh di folder yang bersesuaian path-nya dengan nama package dia.

Exception yang lebih gampang dan sederhana

Di C++, kita bisa throw tipe data apapun sebagai exception. Mau integer? bisa. Mau pointer? bisa juga. Gimana dengan pointer ke stack? Bisa! Tapi kalo anda melakukan itu saya akan lempar anda dari jembatan dekat tempat tinggal saya. Begitulah, apa-apa bisa, tapi hanya ada satu yang betul, yaitu “throw by value, catch by reference”.

Nah, begini kalau di C++

try {
    throw std::runtime_error("Error!");
} catch (const std::runtime_error& e) {
    std::cout << e.what() << std::endl;
}

Sebenarnya masih banyak contoh cara ngelempar exception yang salah di C++, tapi saya lagi males nunjukin. Saya tunjukin aja yang Java. Di Java, yang bisa dilempar cuma klas turunan Exception. Turunan Exception ada dua jenis, yaitu RuntimeException dan turunannya dan selainnya. Exception turunan dari RuntimeException adalah unchecked exception, artinya, boleh tidak ditangkap, tapi pada saat dijalankan dia akan crash. Berbeda dengan checked exception, di mana setiap kode yang berpotensi untuk melempar exception harus dipagari dengan try/catch, atau fungsi tersebut dideklarasikan melempar exception.

try {
    doSomething("A message");
} catch (FileNotFoundException e) {
    e.printStackTrace();
} finally {
    //do cleanup things
}

Pada kode di atas, C++ lebih unggul sedikit karena tidak perlu finally block. C++ memakai konsep RAII secara menyeluruh. Sementara di Java, untuk cleanup resource harus dilakukan di blok finally.

public void doSomething(String input) throws MyBusinessException {
    // do something useful ...
    // if it fails
}

Nah, kode di atas artinya MyBusinessException itu adalah checked exception. Jadi, kalo penulis kode nggak mau menangani exception di dalam method, harus ditambahkan throws MyBusinessException.

Tidak ada fitur-fitur yang aneh-aneh

  • Tidak ada custom operator overloading

    Artinya, kita tidak bisa punya sintaks penjumlahan matriks a + b di Java. Kita harus bikin method sehingga jadi a.add(b). Ngeselin, tapi lebih sederhana dan nggak bikin bingung orang.

  • Tidak ada user defined conversion

    Saya juga sering dibikin bingung di C++, contohnya ketika terjadi konversi otomatis cari char* ke string. Ternyata, itu karena ada implicit single parameter constructor yang parameternya adalah char*. Dengan adanya user defined conversion, lagi-lagi programmer Java harus manggil method. Tapi, bagusnya adalah sederhana dan tidak ada magic.

  • Tidak ada default parameter

    C++ Selain memiliki method overloading, ada juga default parameter. Di sinilah kita sering bingung, apakah method yang dipanggil itu overload-nya, ataukah dia dipanggil dengan default parameter? Java tidak punya masalah seperti itu. Sederhana dan tanpa masalah.

  • Tidak ada macro

    Java memang banyak yang tidak ada. Tidak ada macro magic yang bisa mengubah source code jadi aneh-aneh. Tidak ada compile time switch.

Java adalah C++ ++ –

Java memang terinspirasi dari C++. Tapi, bukannya mengambil mentah-mentah semua fitur-nya, Java (sebelum Java 5) justru memangkas habis fitur-fitur C++, dan menyisakan hanya bagian-bagian penting dari OOP-nya saja. Java juga membuang semua warisan dari bahasa C, membuatnya jadi bahasa OOP yang mudah dipelajari bagi pemula. Walau ketika pertama kali muncul Java terkenal sangat lamban karena ketergantungannya terhadap JVM, saat ini performa JVM sudah sangat bersaing, sehingga banyak yang berpendapat memprogram dalam C++ adalah perbuatan yang buang-buang tenaga karena peningkatan performanya tidak sepadan dengan tingkat kesulitan yang dihadapi ketika memprogramnya. Kapan-kapan, saya akan lanjutkan seri sejarah C++ ini.