Pengalaman pribadi: beralih dari pengembangan C tingkat rendah ke pemrograman Java



Artikel tersebut mencerminkan pengalaman pribadi penulis, seorang programmer mikrokontroler yang keranjingan, yang, setelah bertahun-tahun pengalaman dalam pengembangan mikrokontroler di C (dan sedikit dalam C ++), berkesempatan untuk berpartisipasi dalam proyek besar di Jawa untuk mengembangkan perangkat lunak untuk set-top box TV yang menjalankan Android. Selama proyek ini, saya dapat mengumpulkan catatan tentang perbedaan menarik antara bahasa Java dan C / C ++, mengevaluasi berbagai pendekatan untuk program penulisan. Artikel ini tidak berpura-pura menjadi referensi, tetapi tidak memeriksa efisiensi dan produktivitas program-program Java. Ini lebih merupakan kumpulan pengamatan pribadi. Kecuali ditentukan lain, ini adalah rilis Java SE 7.

Perbedaan sintaks dan konstruk kontrol


Singkatnya - perbedaannya minimal, sintaksisnya sangat mirip. Blok kode juga dibentuk oleh sepasang kurung kurawal {}. Aturan untuk mengkompilasi pengidentifikasi sama dengan untuk C / C ++. Daftar kata kunci hampir sama dengan di C / C ++. Tipe data bawaan - mirip dengan yang ada di C / C ++. Array - semua juga dideklarasikan menggunakan tanda kurung siku.

Kontrol membangun if-else, sementara, do-while, for, switch juga hampir sepenuhnya identik. Perlu dicatat bahwa di Jawa ada label yang akrab bagi programmer-C (label yang digunakan dengan kata kunci goto dan yang penggunaannya sangat tidak disarankan). Namun, Java mengecualikan kemungkinan beralih ke label menggunakan goto. Label hanya boleh digunakan untuk keluar dari loop bersarang:

outer: for (int i = 0; i < 5; i++) { inner: for (int j = 0; j < 5; j++) { if (i == 2) break inner; if (i == 3) continue outer; } } 

Untuk meningkatkan keterbacaan program di Jawa, sebuah peluang menarik telah ditambahkan untuk memisahkan bit angka panjang dengan garis bawah:

 int value1 = 1_500_000; long value2 = 0xAA_BB_CC_DD; 

Secara eksternal, program Java tidak jauh berbeda dari program C. Famili. Perbedaan visual utama adalah bahwa Java tidak memungkinkan fungsi, variabel, definisi jenis baru (struktur), konstanta, dan sebagainya, yang "bebas" terletak di file sumber. Java adalah bahasa berorientasi objek, jadi semua entitas program harus milik kelas tertentu. Perbedaan signifikan lainnya adalah kurangnya preprosesor. Kedua perbedaan ini dijelaskan secara lebih rinci di bawah ini.

Pendekatan objek dalam bahasa C.


Ketika kita menulis program besar dalam C, kita pada dasarnya harus bekerja dengan objek. Peran objek di sini dilakukan oleh struktur yang menggambarkan esensi tertentu dari "dunia nyata":

 //   – «» struct Data { int field; char *str; /* ... */ }; 

Juga di C ada metode untuk memproses "objek" -struktur - fungsi. Namun, fungsi pada dasarnya tidak digabungkan dengan data. Ya, mereka biasanya ditempatkan dalam satu file, tetapi setiap kali perlu untuk melewatkan pointer ke objek untuk diproses menjadi fungsi "khas":

 int process(struct Data *ptr, int arg1, const char *arg2) { /* ... */ return result_code; } 

Anda dapat menggunakan "objek" hanya setelah mengalokasikan memori untuk menyimpannya:

 Data *data = malloc(sizeof(Data)); 

Dalam program C, fungsi biasanya didefinisikan yang bertanggung jawab untuk inisialisasi "objek" sebelum digunakan pertama kali:

 void init(struct Data *data) { data->field = 1541; data->str = NULL; } 

Maka siklus hidup "objek" dalam C biasanya seperti ini:

 /*    "" */ struct Data *data = malloc(sizeof(Data)); /*  "" */ init(data); /*   "" */ process(data, 0, "string"); /*  ,  ""     . */ free(data); 

Sekarang kita daftar kemungkinan runtime error yang dapat dibuat oleh programmer dalam siklus hidup "objek":

  1. Lupa mengalokasikan memori untuk "objek"
  2. Tentukan jumlah memori yang dialokasikan yang salah
  3. Lupa menginisialisasi "objek"
  4. Lupa membebaskan memori setelah menggunakan objek

Mungkin sangat sulit untuk mendeteksi kesalahan seperti itu, karena mereka tidak terdeteksi oleh kompiler dan muncul selama operasi program. Selain itu, efeknya bisa sangat beragam dan mempengaruhi variabel dan "objek" program lainnya.

Pendekatan Objek Java


Menghadapi OOP - pemrograman berorientasi objek, Anda mungkin pernah mendengar tentang salah satu paus OOP - enkapsulasi. Di Jawa, tidak seperti C, data dan metode untuk memprosesnya digabungkan bersama dan merupakan objek "benar". Dalam hal OOP, ini disebut enkapsulasi. Kelas adalah deskripsi objek, analog terdekat dari kelas di C adalah untuk mendefinisikan tipe baru menggunakan typedef struct. Dalam istilah Java, fungsi-fungsi yang dimiliki kelas disebut metode.

 //   class Entity { public int field; //   public String str; //   //  public int process(int arg1, String arg2) { /* ... */ return resultCode; } //  public Entity() { field = 1541; str = "value"; } } 

Ideologi bahasa Jawa didasarkan pada pernyataan "semuanya adalah objek." Oleh karena itu, tidak mengherankan bahwa Java melarang pembuatan kedua metode (fungsi) dan bidang data (variabel) secara terpisah dari kelas. Bahkan metode main () yang familier, dari mana program dimulai, harus menjadi milik salah satu kelas.

Definisi kelas di Jawa analog dengan deklarasi struktur dalam C. Dengan menjelaskan kelas, Anda tidak membuat apa pun di memori. Objek kelas ini muncul pada saat pembuatannya oleh operator baru. Membuat objek di Jawa adalah analog dari mengalokasikan memori dalam bahasa C, tetapi, tidak seperti yang terakhir, metode khusus secara otomatis dipanggil selama pembuatan objek - pembangun objek. Konstruktor mengambil peran inisialisasi awal objek - analog dari fungsi init () yang dibahas sebelumnya. Nama konstruktor harus sesuai dengan nama kelas. Konstruktor tidak dapat mengembalikan nilai.

Siklus hidup suatu objek dalam program Java adalah sebagai berikut:

 //   (   ,  ) Entity entity = new Entity(); //    entity.process(123, "argument"); 

Perhatikan bahwa jumlah kesalahan yang mungkin dalam program Java jauh lebih kecil daripada di program C. Ya, Anda masih bisa lupa untuk membuat objek sebelum penggunaan pertama (yang, bagaimanapun, akan menyebabkan NullPointerException mudah ditukar), tetapi untuk kesalahan lain yang melekat Program C, situasinya berubah secara mendasar:

  1. Tidak ada sizeof () operator di Jawa. Kompiler Java sendiri menghitung jumlah memori untuk menyimpan objek. Oleh karena itu, tidak mungkin untuk menentukan ukuran pemilihan yang salah.
  2. Inisialisasi objek terjadi pada saat penciptaan. Mustahil untuk melupakan inisialisasi.
  3. Memori yang ditempati oleh objek tidak perlu dibebaskan, pengumpul sampah melakukan pekerjaan ini. Tidak mungkin untuk menghapus objek setelah digunakan - ada kemungkinan efek "kehabisan memori" yang lebih kecil.

Jadi, segala sesuatu di Jawa adalah objek dari satu kelas atau yang lain. Pengecualian adalah primitif yang telah ditambahkan ke bahasa untuk meningkatkan kinerja dan konsumsi memori. Lebih banyak tentang primitif di bawah ini.

Pengumpul memori dan sampah


Java mempertahankan konsep heap dan stack untuk C / C ++, seorang programmer. Saat membuat objek dengan operator baru, memori untuk menyimpan objek dipinjam dari heap. Namun, tautan ke objek (tautan adalah analog dari penunjuk), jika objek yang dibuat bukan bagian dari objek lain, ditempatkan di tumpukan. Di heap disimpan "tubuh" objek, dan pada stack adalah variabel lokal: referensi ke objek dan tipe primitif. Jika tumpukan ada selama pelaksanaan program dan tersedia untuk semua utas program, maka tumpukan milik metode dan hanya ada selama pelaksanaannya, dan juga tidak dapat diakses oleh utas lain dari program.

Java tidak perlu dan bahkan lebih lagi - Anda tidak dapat secara manual membebaskan memori yang ditempati oleh suatu objek. Pekerjaan ini dilakukan oleh pengumpul sampah dalam mode otomatis. Runtime memonitor apakah mungkin untuk mencapai setiap objek di heap dari lokasi saat ini dari program dengan mengikuti tautan dari objek ke objek. Jika tidak, maka objek tersebut diakui sebagai "sampah" dan menjadi kandidat untuk dihapus.

Penting untuk dicatat bahwa penghapusan itu sendiri tidak terjadi pada saat objek "tidak lagi diperlukan" - pengumpul sampah memutuskan penghapusan, dan penghapusan dapat ditunda sebanyak yang diperlukan sampai program berakhir.

Tentu saja, pekerjaan pengumpul sampah membutuhkan overhead prosesor. Tetapi sebagai imbalannya, ia meringankan programmer dari sakit kepala besar yang terkait dengan kebutuhan untuk membebaskan memori setelah akhir penggunaan "objek". Bahkan, kita "mengambil" ingatan ketika kita membutuhkannya dan menggunakannya, tidak berpikir bahwa kita perlu membebaskannya setelah diri kita sendiri.

Berbicara tentang variabel lokal, kita harus memanggil kembali pendekatan Java ke inisialisasi mereka. Jika dalam C / C ++ variabel lokal yang tidak diinisialisasi berisi nilai acak, maka kompiler Java tidak akan membiarkannya tidak diinisialisasi:

 int i; //  . System.out.println("" + i); //  ! 

Tautan - Pointer Pengganti


Java tidak memiliki pointer, oleh karena itu, seorang programmer Java tidak memiliki kemampuan untuk membuat salah satu dari banyak kesalahan yang terjadi ketika bekerja dengan pointer. Saat Anda membuat objek, Anda mendapatkan tautan ke objek ini:

 //  entity –  . Entity entity = new Entity(); 

Di C, programmer punya pilihan: bagaimana mengoper, katakanlah, struktur ke suatu fungsi. Anda bisa melewati nilai:

 //    . int func(Data data);    –   : //    . void process(Data *data); 

Lewat nilai dijamin bahwa fungsi tidak akan mengubah data dalam struktur, tetapi tidak efektif dalam hal kinerja - pada saat fungsi dipanggil, salinan struktur dibuat. Melewati sebuah pointer jauh lebih efisien: pada kenyataannya, alamat di memori tempat struktur berada dilewatkan ke fungsi.

Di Jawa, hanya ada satu cara untuk melewatkan objek ke metode - dengan referensi. Melewati dengan referensi di Jawa analog dengan melewati pointer di C:
  • menyalin (kloning) memori tidak terjadi,
  • sebenarnya, alamat lokasi objek ini ditransmisikan.

Namun, tidak seperti penunjuk bahasa C, tautan Java tidak dapat ditambah / dikurangi. "Menjalankan" melalui elemen-elemen array menggunakan tautan ke dalamnya di Java tidak akan berfungsi. Semua yang bisa dilakukan dengan tautan adalah memberikan nilai yang berbeda.

Tentu saja, tidak adanya pointer seperti itu mengurangi jumlah kesalahan yang mungkin, namun, analog dari pointer nol tetap dalam bahasa - referensi nol dilambangkan dengan kata kunci nol.

Referensi nol adalah sakit kepala untuk programmer Java, seperti memaksa referensi objek untuk diperiksa nol sebelum menggunakannya atau untuk menangani pengecualian NullPointerException. Jika ini tidak dilakukan, maka program akan macet.

Jadi, semua objek di Jawa dilewatkan melalui tautan. Tipe data primitif (int, long, char ...) diteruskan oleh nilai (lebih lanjut tentang primitif diberikan di bawah).

Fitur Java Link


Akses ke objek apa pun dalam program ini adalah melalui tautan - ini jelas memiliki efek positif pada kinerja, tetapi mungkin mengejutkan seorang pemula:

 //  ,   entity1   . Entity entity1 = new Entity(); entity1.field = 123; //   entity2,     entity1. //    !   ! Entity entity2 = entity1; //   entity1  entity2         . entity2.field = 777; //  entity1.field  777. System.out.println(entity1.field); 

Argumen metode dan nilai pengembalian - semuanya dilewatkan melalui tautan. Selain keuntungan, ada kelemahan dibandingkan dengan bahasa C / C ++, di mana kita dapat secara eksplisit melarang fungsi dari mengubah nilai melewati pointer menggunakan kualifikasi const:

 void func(const struct Data* data) { //  ! //    ,    ! data->field = 0; } 

Yaitu, bahasa C memungkinkan Anda untuk melacak kesalahan ini pada tahap kompilasi. Java juga memiliki kata kunci const, tetapi dicadangkan untuk versi yang akan datang dan saat ini tidak digunakan sama sekali. Hingga taraf tertentu, kata kunci terakhir dipanggil untuk memenuhi perannya. Namun, itu tidak melindungi objek yang diteruskan ke metode dari perubahan:

 public class Main { void func(final Entity data) { //    . //    final,    . data.field = 0; } } 

Masalahnya adalah bahwa kata kunci terakhir dalam hal ini diterapkan ke tautan, dan bukan ke objek yang ditunjuk tautan. Jika final diterapkan pada primitif, maka kompiler berperilaku seperti yang diharapkan:

 void func(final int value) { //    . value = 0; } 

Tautan Java sangat mirip dengan tautan bahasa C ++.

Primitif Jawa


Setiap objek Java, selain bidang data, berisi informasi pendukung. Jika kita ingin mengoperasikan, misalnya, dalam byte terpisah dan setiap byte diwakili oleh suatu objek, maka dalam kasus array byte, overhead memori dapat berkali-kali melebihi ukuran yang dapat digunakan.
Agar Jawa tetap cukup efisien dalam kasus yang dijelaskan di atas, dukungan untuk tipe primitif - primitif - ditambahkan ke bahasa.
PrimitifLihatKedalaman bitKemungkinan analog dalam C
byteInteger8char
pendek16pendek
char16wchar_t
int32int (panjang)
panjang64panjang
mengapungAngka titik mengambang32mengapung
dobel64dobel
booleanLogis-int (C89) / bool (C99)

Semua primitif memiliki analog mereka dalam bahasa C. Namun, standar C tidak menentukan ukuran yang tepat dari tipe integer, melainkan rentang nilai yang dapat disimpan oleh tipe ini tetap. Seringkali, programmer ingin memastikan kedalaman bit yang sama untuk mesin yang berbeda, yang mengarah pada penampilan tipe seperti uint32_t dalam program, meskipun semua fungsi pustaka memerlukan argumen dari tipe int.
Fakta ini tidak dapat dikaitkan dengan keunggulan bahasa.

Primitif Java integer, tidak seperti C, memiliki kedalaman bit tetap. Dengan demikian, Anda tidak perlu khawatir tentang kedalaman bit sebenarnya dari mesin yang menjalankan program Java, serta tentang urutan byte ("jaringan" atau "Intel"). Fakta ini membantu mewujudkan prinsip “itu ditulis sekali - itu dipenuhi di mana-mana”.

Selain itu, di Jawa semua bilangan bulat primitif ditandatangani (bahasa tidak memiliki kata kunci yang tidak ditandatangani). Ini menghilangkan kesulitan menggunakan variabel yang ditandatangani dan tidak ditandatangani dalam satu ekspresi yang melekat dalam C.

Sebagai kesimpulan, urutan byte dalam primitif multi-byte di Jawa diperbaiki (byte rendah pada alamat rendah, Little-endian, urutan terbalik).

Kerugian dari implementasi operasi dengan primitif di Jawa termasuk fakta bahwa di sini, seperti dalam program C / C ++, overflow dari grid bit dapat terjadi, tanpa ada pengecualian yang dilemparkan:

 int i1 = 2_147_483_640; int i2 = 2_147_483_640; int r = (i1 + i2); // r = -16 

Jadi, data di Jawa diwakili oleh dua jenis entitas: objek dan primitif. Primitif melanggar konsep "semuanya adalah objek", tetapi dalam beberapa situasi mereka terlalu efektif untuk tidak menggunakannya.

Warisan


Warisan adalah paus OOP lain yang mungkin pernah Anda dengar. Jika Anda menjawab dengan singkat pertanyaan "mengapa warisan diperlukan", maka jawabannya adalah "penggunaan kembali kode".

Misalkan Anda memprogram dalam C, dan Anda memiliki "kelas" yang ditulis dengan baik dan di-debug - struktur dan fungsi untuk memprosesnya. Selanjutnya, muncul kebutuhan untuk membuat "kelas" yang serupa, tetapi dengan fungsionalitas yang ditingkatkan, dan "kelas" dasar masih diperlukan. Dalam hal bahasa C, Anda hanya memiliki satu cara untuk menyelesaikan masalah ini - komposisi. Ini adalah tentang membuat struktur baru yang diperluas - "class", yang harus berisi pointer ke struktur "class" dasar:

 struct Base { int field1; char *field2; }; void baseMethod(struct Base *obj, int arg); struct Extended { struct Base *base; int auxField; }; void extendedMethod(struct Extended *obj, int arg) { baseMethod(obj->base, 123); /* ... */ } 

Java sebagai bahasa berorientasi objek memungkinkan Anda untuk memperluas fungsionalitas kelas yang ada menggunakan mekanisme pewarisan:

 //   class Base { protected int baseField; private int hidden; public void baseMethod() { } } //   -   . class Extended extends Base { public void extendedMethod() { //    public  protected     . baseField = 123; baseMethod(); // !   private  ! hidden = 123; } } 

Perlu dicatat bahwa Java sama sekali tidak melarang penggunaan komposisi sebagai cara untuk memperluas fungsionalitas kelas yang sudah ditulis. Selain itu, dalam banyak situasi, komposisi lebih disukai daripada warisan.

Berkat warisan, kelas-kelas di Jawa diatur dalam struktur hierarkis, setiap kelas tentu memiliki satu dan hanya satu "orang tua" dan dapat memiliki sejumlah "anak". Tidak seperti C ++, kelas di Java tidak dapat mewarisi dari lebih dari satu orang tua (ini memecahkan masalah "warisan berlian").

Selama pewarisan, kelas turunan mendapatkan ke lokasi semua bidang publik dan terlindungi dan metode dari kelas dasarnya, serta kelas dasar dari kelas dasarnya, dan seterusnya naik hierarki warisan.

Di bagian atas hierarki warisan adalah nenek moyang yang sama dari semua kelas Java - kelas Object, satu-satunya yang tidak memiliki orangtua.

Identifikasi tipe dinamis


Salah satu poin utama dari bahasa Jawa adalah dukungan untuk identifikasi tipe dinamis (RTTI). Dengan kata sederhana, RTTI memungkinkan Anda untuk mengganti objek dari kelas turunan di mana referensi ke pangkalan diperlukan:

 //     Base link; //         link = new Extended(); 

Memiliki tautan saat runtime, Anda dapat menentukan tipe sebenarnya dari objek yang dirujuk oleh tautan - menggunakan instanceof dari operator:

 if (link instanceof Base) { // false } else if (link instanceof Extended) { // true } 

Metode Override


Mendefinisikan ulang suatu metode atau fungsi berarti mengganti tubuhnya pada tahap pelaksanaan program. Pemrogram C menyadari kemampuan suatu bahasa untuk mengubah perilaku suatu fungsi selama eksekusi program. Ini tentang menggunakan pointer fungsi. Misalnya, Anda bisa menyertakan pointer ke fungsi dalam struktur struktur dan menetapkan berbagai fungsi ke pointer untuk mengubah algoritma pemrosesan data dari struktur ini:

 struct Object { //   . void (*process)(struct Object *); int data; }; void divideByTwo(struct Object *obj) { obj->data = obj->data / 2; } void square(struct Object *obj) { obj->data = obj->data * obj->data; } struct Object obj; obj.data = 123; obj.process = divideByTwo; obj.process(&obj); // 123 / 2 = 61 obj.process = square; obj.process(&obj); // 61 * 61 = 3721 

Di Jawa, seperti dalam bahasa OOP lainnya, metode utama terkait erat dengan warisan. Kelas turunan mendapatkan akses ke metode kelas dasar yang dilindungi dan publik. Selain fakta bahwa ia dapat memanggil mereka, Anda dapat mengubah perilaku salah satu metode dari kelas dasar tanpa mengubah tanda tangannya. Untuk melakukan ini, cukup mendefinisikan metode dengan tanda tangan yang persis sama di kelas turunan:

 //   -   . class Extended extends Base { //  . public void method() { /* ... */ } //     ! // E      . //     . public void method(int i) { /* ... */ } } 

Sangat penting bahwa tanda tangan (nama metode, nilai balik, argumen) sama persis. Jika nama metode cocok, dan argumen berbeda, maka metode kelebihan beban, lebih lanjut tentang yang di bawah ini.

Polimorfisme


Seperti enkapsulasi dan pewarisan, paus OOP ketiga - polimorfisme - juga memiliki beberapa jenis analog dalam bahasa C yang berorientasi prosedural.

Misalkan kita memiliki beberapa "kelas" struktur yang dengannya Anda ingin melakukan jenis tindakan yang sama, dan fungsi yang melakukan tindakan ini harus bersifat universal - harus "dapat" bekerja dengan "kelas" apa pun sebagai argumen. Solusi yang mungkin adalah sebagai berikut:

  /*   */ enum Ids { ID_A, ID_B }; struct ClassA { int id; /* ... */ } void aInit(ClassA obj) { obj->id = ID_A; } struct ClassB { int id; /* ... */ } void bInit(ClassB obj) { obj->id = ID_B; } /* klass -   ClassA, ClassB, ... */ void commonFunc(void *klass) { /*   */ int id = (int *)klass; switch (id) { case ID_A: ClassA *obj = (ClassA *) klass; /* ... */ break; case ID_B: ClassB *obj = (ClassB *) klass; /* ... */ break; } /* ... */ } 

Solusinya terlihat rumit, tetapi tujuannya tercapai - fungsi universal commonFunc () menerima "objek" dari setiap "kelas" sebagai argumen. Prasyarat adalah struktur "kelas" di bidang pertama harus berisi pengidentifikasi yang menentukan "kelas" objek tersebut. Solusi semacam itu dimungkinkan karena penggunaan argumen dengan tipe "void *". Namun, pointer dari jenis apa pun dapat dilewatkan ke fungsi seperti itu, misalnya, "int *". Ini tidak akan menyebabkan kesalahan kompilasi, tetapi pada saat dijalankan program akan berperilaku tidak terduga.

Sekarang mari kita lihat bagaimana polimorfisme terlihat di Jawa (namun, seperti dalam bahasa OOP lainnya). Misalkan kita memiliki banyak kelas yang harus diproses dengan cara yang sama dengan beberapa metode. Berbeda dengan solusi untuk bahasa C yang disajikan di atas, metode polimorfik ini HARUS dimasukkan dalam semua kelas himpunan yang diberikan, dan semua versinya HARUS memiliki tanda tangan yang sama.

 class A { public void method() {/* ... */} } class B { public void method() {/* ... */} } class C { public void method() {/* ... */} } 

Selanjutnya, Anda perlu memaksa kompiler untuk memanggil versi metode yang dimiliki kelas yang sesuai.

 void executor(_set_of_class_ klass) { klass.method(); } 

Yaitu, metode executor (), yang dapat berada di mana saja dalam program, harus dapat bekerja dengan kelas apa pun dari set (A, B atau C). Kita harus entah bagaimana “memberi tahu” kompiler bahwa _set_of_class_ menunjukkan banyak kelas kita. Di sini pewarisan sangat berguna - perlu untuk membuat semua kelas dari turunan yang ditetapkan dari beberapa kelas dasar, yang akan berisi metode polimorfik:

 abstract class Base { abstract public void method(); } class A extends Base { public void method() {/* ... */} } class B extends Base { public void method() {/* ... */} } class C extends Base { public void method() {/* ... */} }   executor()   : void executor(Base klass) { klass.method(); } 

Dan sekarang setiap kelas yang merupakan pewaris Basis (berkat identifikasi tipe dinamis) dapat diteruskan sebagai argumen:

 executor(new A()); executor(new B()); executor(new C()); 

Bergantung pada objek kelas mana yang dilewatkan sebagai argumen, metode milik kelas ini akan dipanggil.

Kata kunci abstrak memungkinkan Anda untuk mengecualikan badan metode (buat abstrak, dalam hal OOP). Bahkan, kami memberi tahu kompiler bahwa metode ini harus diganti dalam kelas-kelas yang diturunkan darinya. Jika ini bukan masalahnya, kesalahan kompilasi terjadi. Kelas yang mengandung setidaknya satu metode abstrak juga disebut abstrak. Kompiler memerlukan penandaan kelas-kelas tersebut juga dengan kata kunci abstrak.

Struktur proyek Java


Di Jawa, semua file sumber memiliki ekstensi * .java. File header * .h dan prototipe fungsi atau kelas tidak ada. Setiap file sumber Java harus mengandung setidaknya satu kelas. Nama kelas adalah kebiasaan untuk menulis, dimulai dengan huruf kapital.

Beberapa file dengan kode sumber dapat digabungkan menjadi satu paket. Untuk melakukan ini, kondisi berikut harus dipenuhi:
  1. File dengan kode sumber harus berada di direktori yang sama di sistem file.
  2. Nama direktori ini harus sesuai dengan nama paket.
  3. Di awal setiap file sumber, paket yang dimiliki file ini harus ditunjukkan, misalnya:

 package com.company.pkg; 

Untuk memastikan keunikan nama-nama paket di dunia, diusulkan untuk menggunakan nama domain "terbalik" perusahaan. Namun, ini bukan persyaratan dan nama apa pun dapat digunakan dalam proyek lokal.

Anda juga disarankan untuk menentukan nama paket huruf kecil. Sehingga mereka dapat dengan mudah dibedakan dari nama kelas.

Penyembunyian implementasi


Aspek lain dari enkapsulasi adalah pemisahan antarmuka dan implementasi. Jika antarmuka dapat diakses ke bagian luar program (eksternal ke modul atau kelas), maka implementasinya tersembunyi. Dalam literatur, analogi kotak hitam sering digambarkan ketika implementasi internal "tidak terlihat" dari luar, tetapi apa yang dimasukkan ke dalam input kotak dan apa yang diberikannya adalah "terlihat".

Di C, implementasi tersembunyi dilakukan di dalam modul, menandai fungsi yang seharusnya tidak terlihat dari luar dengan kata kunci statis. Prototipe fungsi yang membentuk antarmuka modul ditempatkan di file header. Modul dalam C berarti pasangan: file sumber dengan ekstensi * .c dan header dengan ekstensi * .h.

Java juga memiliki kata kunci statis, tetapi tidak memengaruhi “visibilitas” metode atau bidang dari luar. Untuk mengontrol "visibilitas" ada 3 pengubah akses: pribadi, dilindungi, publik.

Bidang dan metode kelas yang ditandai sebagai pribadi hanya tersedia di dalamnya. Bidang dan metode yang dilindungi juga dapat diakses oleh keturunan kelas. Pengubah publik berarti bahwa elemen yang ditandai dapat diakses dari luar kelas, yaitu, itu adalah bagian dari antarmuka. Mungkin juga tidak ada pengubah, dalam hal ini akses ke elemen kelas dibatasi oleh paket di mana kelas berada.

Disarankan bahwa ketika menulis kelas, tandai semua bidang kelas sebagai pribadi dan memperpanjang hak akses yang diperlukan.

Metode kelebihan beban


Salah satu fitur menjengkelkan dari pustaka standar C adalah keberadaan seluruh kebun binatang fungsi yang pada dasarnya melakukan hal yang sama, tetapi berbeda dalam jenis argumen, misalnya: fabs (), fabsf (), fabsl (), berfungsi untuk mendapatkan nilai absolut untuk double, float dan long jenis ganda masing-masing.

Java (dan juga C ++) mendukung mekanisme kelebihan metode - mungkin ada beberapa metode di dalam kelas dengan nama yang sama persis, tetapi berbeda dalam jenis dan jumlah argumen. Dengan jumlah argumen dan jenisnya, kompiler akan memilih versi yang diperlukan dari metode itu sendiri - sangat nyaman dan meningkatkan keterbacaan program.

Di Jawa, tidak seperti C ++, operator tidak dapat kelebihan beban. Pengecualian adalah operator "+" dan "+ =", yang pada awalnya kelebihan beban untuk string String.

Karakter dan String di Jawa


Di C, Anda harus bekerja dengan string terminal-nol yang diwakili oleh pointer ke karakter pertama:

 char *str; //  ASCII  wchar_t *strw; //   ""  

Baris seperti itu harus diakhiri dengan karakter nol. Jika tidak sengaja "dihapus", maka sebuah string akan dianggap sebagai urutan byte dalam memori hingga karakter nol pertama. Yaitu, jika variabel program lain ditempatkan di memori setelah baris, maka setelah memodifikasi garis yang rusak, nilainya dapat (dan kemungkinan besar akan) terdistorsi.

Tentu saja, seorang programmer-C tidak berkewajiban untuk menggunakan string null-terminal klasik, tetapi menerapkan implementasi pihak ketiga, tetapi di sini harus diingat bahwa semua fungsi dari perpustakaan standar memerlukan string null-terminal sebagai argumen mereka. Selain itu, standar C tidak mendefinisikan pengkodean yang digunakan, titik ini juga harus dikontrol oleh programmer.

Di Jawa, tipe char primitif (serta pembungkus Karakter, tentang pembungkus di bawah) mewakili karakter tunggal sesuai dengan standar Unicode. Encoding UTF-16 digunakan, masing-masing, satu karakter menempati 2 byte dalam memori, yang memungkinkan Anda untuk menyandikan hampir semua karakter bahasa yang saat ini digunakan.

Karakter dapat ditentukan oleh Unicode mereka:

 char ch1 = '\u20BD'; 

Jika Unicode karakter melebihi 216 maksimum untuk char, maka karakter seperti itu harus diwakili oleh int. Dalam string, itu akan menempati 2 karakter 16 bit, tetapi sekali lagi, karakter dengan kode melebihi 216 jarang digunakan.

String Java diimplementasikan oleh kelas String bawaan dan menyimpan karakter char 16-bit. Kelas String berisi semua atau hampir semua yang mungkin diperlukan untuk bekerja dengan string. Tidak perlu memikirkan fakta bahwa garis harus diakhiri dengan nol, di sini tidak mungkin untuk secara tak terlihat "menghapus" karakter penghentian nol ini atau mengakses memori di luar garis. Secara umum, ketika bekerja dengan string di Jawa, programmer tidak memikirkan bagaimana string disimpan dalam memori.

Seperti yang disebutkan di atas, Java tidak mengizinkan overloading operator (seperti pada C ++), namun kelas String merupakan pengecualian - hanya untuk itu operator penggabung baris "+" dan "+ =" pada awalnya kelebihan beban.

 String str1 = "Hello, " + "World!"; String str2 = "Hello, "; str2 += "World!"; 

Patut dicatat bahwa string di Jawa tidak dapat diubah - setelah dibuat, mereka tidak mengizinkan perubahannya. Ketika kami mencoba mengubah garis, misalnya, seperti ini:

 String str = "Hello, World!"; str.toUpperCase(); System.out.println(str); //   "Hello, World!" 

Jadi string asli tidak benar-benar berubah. Alih-alih, salinan yang dimodifikasi dari string asli dibuat, yang pada gilirannya juga tidak berubah:

 String str = "Hello, World!"; String str2 = str.toUpperCase(); System.out.println(str2); //   "HELLO, WORLD!" 

Dengan demikian, setiap perubahan string pada kenyataannya menghasilkan penciptaan objek baru (pada kenyataannya, dalam kasus penggabungan string, kompiler dapat mengoptimalkan kode dan menggunakan kelas StringBuilder, yang akan dibahas nanti).

Kebetulan program itu sering perlu mengubah jalur yang sama. Dalam kasus seperti itu, untuk mengoptimalkan kecepatan program dan konsumsi memori, Anda dapat mencegah pembuatan objek baris baru. Untuk tujuan ini, kelas StringBuilder harus digunakan:

 String sourceString = "Hello, World!"; StringBuilder builder = new StringBuilder(sourceString); builder.setCharAt(4, '0'); builder.setCharAt(8, '0'); builder.append("!!"); String changedString = builder.toString(); System.out.println(changedString); //   "Hell0, W0rld!!!" 

Secara terpisah, perlu disebutkan perbandingan string. Kesalahan khas programmer Java pemula adalah membandingkan string menggunakan operator "==":

 //    "Yes" // ! if (usersInput == "Yes") { //    } 

Kode tersebut tidak secara resmi mengandung kesalahan pada tahap kompilasi atau kesalahan runtime, tetapi ia bekerja secara berbeda dari yang diharapkan. Karena semua objek dan string, termasuk di Jawa, diwakili oleh tautan, perbandingan dengan operator "==" memberikan perbandingan tautan, bukan nilai objek. Artinya, hasilnya hanya akan benar jika 2 tautan benar-benar merujuk ke baris yang sama. Jika string adalah objek yang berbeda dalam memori, dan Anda perlu membandingkan kontennya, maka Anda perlu menggunakan metode equals ():

 if (usersInput.equals("Yes")) { //    } 

Yang paling mengejutkan adalah bahwa dalam beberapa kasus, perbandingan menggunakan operator “==” berfungsi dengan benar:

 String someString = "abc", anotherString = "abc"; //   "true": System.out.println(someString == anotherString); 

Ini karena, pada kenyataannya, someString dan anotherString merujuk ke objek yang sama dalam memori. Compiler menempatkan string string yang sama dalam kumpulan string - yang disebut internment terjadi. Lalu setiap kali string string yang sama muncul dalam program, tautan ke string dari kumpulan digunakan. Pemintalan string mungkin dilakukan karena sifat kekekalan string.

Meskipun membandingkan konten string hanya diperbolehkan dengan metode equals (), di Jawa dimungkinkan untuk menggunakan string dengan benar dalam konstruksi case-switch (dimulai dengan Java 7):

 String str = new String(); // ... switch (str) { case "string_value_1": // ... break; case "string_value_2": // ... break; } 

Anehnya, objek Java apa pun dapat dikonversi menjadi string. Metode toString () yang sesuai didefinisikan dalam kelas dasar untuk semua kelas kelas Objek.

Pendekatan Penanganan Kesalahan


Saat memprogram dalam C, Anda mungkin menemukan pendekatan penanganan kesalahan berikut. Setiap fungsi perpustakaan mengembalikan tipe int. Jika fungsi berhasil, maka hasil ini adalah 0. Jika hasilnya bukan nol, ini menunjukkan kesalahan. Paling sering, kode kesalahan dilewatkan melalui nilai yang dikembalikan oleh fungsi. Karena fungsi hanya dapat mengembalikan satu nilai, dan sudah ditempati oleh kode kesalahan, hasil sebenarnya dari fungsi harus dikembalikan melalui argumen sebagai penunjuk, misalnya, seperti ini:

 int function(struct Data **result, const char *arg) { int errorCode; /* ... */ return errorCode; } 

Omong-omong, ini adalah salah satu kasus ketika dalam program C menjadi perlu untuk menggunakan pointer ke pointer.

Terkadang mereka menggunakan pendekatan yang berbeda. Fungsi tidak mengembalikan kode kesalahan, tetapi langsung hasil eksekusi, biasanya dalam bentuk pointer. Situasi kesalahan ditunjukkan dengan pointer nol. Kemudian pustaka biasanya berisi fungsi terpisah yang mengembalikan kode kesalahan terakhir:

 struct Data* function(const char *arg); int getLastError(); 

Salah satu cara atau yang lain, ketika pemrograman dalam C, kode yang berfungsi "berguna" dan kode yang bertanggung jawab untuk menangani kesalahan saling berhubungan, yang jelas tidak membuat program mudah dibaca.

Di Java, jika Anda mau, Anda bisa menggunakan pendekatan yang dijelaskan di atas, tetapi di sini Anda bisa menerapkan cara yang sama sekali berbeda untuk menangani kesalahan - penanganan pengecualian (namun, seperti dalam C ++). Keuntungan penanganan pengecualian adalah dalam hal ini kode "berguna" dan kode yang bertanggung jawab untuk menangani kesalahan dan kontinjensi dipisahkan secara logis dari satu sama lain.

Ini dicapai dengan menggunakan konstruksi try-catch: kode "berguna" ditempatkan di bagian percobaan, dan kode penanganan kesalahan ditempatkan di bagian tangkapan.

 //       try (FileReader reader = new FileReader("path\\to\\file.txt")) { //    -   . while (reader.read() != -1){ // ... } } catch (IOException ex) { //     } 

Ada situasi di mana tidak mungkin untuk memproses kesalahan dengan benar di tempat kejadiannya. Dalam kasus seperti itu, indikasi ditempatkan dalam tanda tangan metode bahwa metode ini dapat menyebabkan jenis pengecualian ini:

 public void func() throws Exception { // ... } 

Sekarang panggilan ke metode ini harus dibingkai dalam blok coba-tangkap, atau metode panggilan juga harus ditandai yang dapat membuang pengecualian ini.

Kurangnya preprosesor


Tidak peduli betapa nyamannya preprosesor yang akrab dengan programmer C / C ++, ia tidak ada dalam bahasa Java. Pengembang Java mungkin memutuskan bahwa itu hanya digunakan untuk memastikan portabilitas program, dan karena Java berjalan di mana-mana (hampir), preprocessor tidak diperlukan di dalamnya.

Anda dapat mengkompensasi kurangnya preprosesor menggunakan bidang bendera statis dan memeriksa nilainya dalam program, jika perlu.

Jika kita berbicara tentang organisasi pengujian, maka dimungkinkan untuk menggunakan anotasi dalam hubungannya dengan refleksi (refleksi).

Array juga merupakan objek.


Ketika bekerja dengan array di C, indeks keluar di luar batas array adalah kesalahan yang sangat berbahaya. Kompiler tidak akan melaporkannya dengan cara apa pun, dan selama eksekusi program tidak akan dihentikan dengan pesan yang sesuai:

 int array[5]; array[6] = 666; 

Kemungkinan besar, program akan melanjutkan eksekusi, tetapi nilai variabel yang ditemukan setelah array array dalam contoh di atas akan terdistorsi. Mendebug kesalahan seperti ini mungkin tidak mudah.

Di Jawa, programmer dilindungi dari kesalahan yang sulit didiagnosis. Saat Anda mencoba melampaui batasan array, sebuah ArrayIndexOutOfBoundsException dilempar. Jika tangkapan pengecualian tidak diprogram menggunakan konstruk coba-coba, program macet dan pesan yang sesuai dikirim ke aliran kesalahan standar yang menunjukkan file dengan kode sumber dan nomor baris tempat array terlampaui. Artinya, diagnosis kesalahan tersebut menjadi masalah sepele.

Perilaku program Java ini dimungkinkan karena array di Jawa diwakili oleh suatu objek. Array Java tidak dapat diubah ukurannya, ukurannya adalah kode-keras pada saat memori dialokasikan. Saat run time, mendapatkan ukuran array sesederhana itu:

 int[] array = new int[10]; int arraySize = array.length; // 10 

Jika kita berbicara tentang array multidimensi, maka, dibandingkan dengan bahasa C, Java menawarkan peluang menarik untuk mengatur array "ladder". Untuk kasus array dua dimensi, ukuran setiap baris individu mungkin berbeda dari yang lain:

 int[][] array = new int[10][]; for (int i = 0; i < array.length; i++) { array[i] = new int[i + 1]; } 

Seperti dalam C, elemen-elemen array terletak di memori satu per satu, sehingga akses ke array dianggap paling efisien. Jika Anda perlu melakukan operasi penyisipan / penghapusan elemen, atau membuat struktur data yang lebih kompleks, maka Anda perlu menggunakan koleksi, seperti kumpulan (Set), daftar (Daftar), peta (Peta).

Karena kurangnya pointer dan ketidakmampuan untuk menambah tautan, akses ke elemen array dimungkinkan menggunakan indeks.

Koleksi


Seringkali fungsi array tidak cukup - maka Anda perlu menggunakan struktur data dinamis. Karena pustaka C standar tidak mengandung implementasi struktur data dinamis yang siap pakai, Anda harus menggunakan implementasi dalam kode sumber atau dalam bentuk pustaka.

Tidak seperti C, pustaka Java standar berisi sekumpulan implementasi struktur atau koleksi data dinamis, yang dinyatakan dalam Java. Semua koleksi dibagi menjadi 3 kelas besar: daftar, set, dan peta.

Daftar - array dinamis - memungkinkan Anda untuk menambah / menghapus item. Banyak yang tidak memastikan urutan elemen yang ditambahkan, tetapi menjamin bahwa tidak ada elemen duplikat. Kartu atau array asosiatif beroperasi dengan pasangan nilai kunci, dan nilai kuncinya unik - tidak boleh ada 2 pasangan dengan kunci yang sama di dalam kartu.

Untuk daftar, set, dan peta, ada banyak implementasi, yang masing-masing dioptimalkan untuk operasi tertentu. Sebagai contoh, daftar diimplementasikan oleh kelas ArrayList dan LinkedList, dengan ArrayList memberikan kinerja yang lebih baik ketika mengakses elemen arbitrer, dan LinkedList lebih efisien ketika menyisipkan / menghapus elemen di tengah daftar.

Hanya objek Java lengkap yang dapat disimpan dalam koleksi (pada kenyataannya, referensi ke objek), oleh karena itu tidak mungkin untuk membuat kumpulan primitif secara langsung (int, char, byte, dll.). Dalam hal ini, kelas pembungkus yang tepat harus digunakan:
PrimitifKelas pembungkus
byteByte
pendekPendek
charKarakter
intInteger
panjangPanjang
mengapungMengapung
dobelDobel
booleanBoolean

Untungnya, ketika pemrograman di Jawa, tidak perlu mengikuti kebetulan yang tepat dari tipe primitif dan "pembungkusnya". Jika metode menerima argumen, misalnya, dari tipe Integer, maka itu dapat melewati tipe int. Dan sebaliknya, di mana tipe int diperlukan, Anda dapat menggunakan Integer dengan aman. Ini dimungkinkan berkat mekanisme bawaan Java untuk pengemasan / pembongkaran primitif.

Dari saat-saat yang tidak menyenangkan, harus disebutkan bahwa perpustakaan Java standar berisi kelas koleksi lama yang tidak berhasil diimplementasikan dalam versi pertama Java dan yang tidak boleh digunakan dalam program baru. Ini adalah kelas Enumeration, Vector, Stack, Dictionary, Hashtable, Properties.

Generalisasi


Koleksi umumnya digunakan sebagai tipe data generik. Inti dari generalisasi dalam kasus ini adalah bahwa kami menentukan jenis utama koleksi, misalnya, ArrayList, dan dalam kurung sudut menentukan jenis parameter, yang dalam hal ini menentukan jenis elemen yang disimpan dalam daftar:

 List<Integer> list = new ArrayList<Integer>(); 

Ini memungkinkan kompiler untuk melacak upaya untuk menambahkan objek tipe selain parameter tipe yang ditentukan:

 List<Integer> list = new ArrayList<Integer>(); //  ! list.add("First"); 

Sangat penting bahwa tipe-parameter dihapus selama eksekusi program, dan tidak ada perbedaan antara, misalnya, objek kelas
 ArrayList <Integer> 
dan objek kelas
 ArrayList <String>. 
Akibatnya, tidak ada cara untuk mengetahui jenis elemen koleksi selama eksekusi program:

 public boolean containsInteger(List list) { //  ! if (list instanceof List<Integer>) { return true; } return false; } 

Solusi parsial dapat berupa pendekatan berikut: ambil elemen pertama dari koleksi dan tentukan jenisnya:

 public boolean containsInteger(List list) { if (!list.isEmpty() && list.get(0) instanceof Integer) { return true; } return false; } 

Tetapi pendekatan ini tidak akan berfungsi jika daftar kosong.

Dalam hal ini, generalisasi Java secara signifikan lebih rendah daripada generalisasi C ++. Generalisasi Java sebenarnya berfungsi untuk "memotong" beberapa kesalahan potensial pada tahap kompilasi.

Ulangi semua elemen array atau koleksi


Saat memprogram dalam C, Anda sering harus mengulangi semua elemen array:

 for (int i = 0; i < SIZE; i++) { /* ... */ } 

Untuk membuat kesalahan di sini lebih sederhana, cukup tentukan ukuran yang salah dari array SIZE atau letakkan "<=" bukannya "<".

Di Jawa, selain bentuk "biasa" dari pernyataan for, ada bentuk untuk mengulangi semua elemen array atau koleksi (sering disebut foreach dalam bahasa lain):

 List<Integer> list = new ArrayList<>(); // ... for (Integer i : list) { // ... } 

Di sini kita dijamin untuk beralih ke semua elemen daftar, kesalahan yang melekat dalam bentuk "biasa" dari pernyataan for dihilangkan.

Koleksi Lain-lain


Karena semua objek diwarisi dari Objek root, Java memiliki peluang menarik untuk membuat daftar dengan berbagai jenis elemen aktual:

 List list = new ArrayList<>(); list.add(new String("First")); list.add(new Integer(2)); list.add(new Double(3.0));         instanceof: for (Object o : list) { if (o instanceof String) { // ... } else if (o instanceof Integer) { // ... } else if (o instanceof Double) { // ... } } 

Transfer


Membandingkan C / C ++ dan Java, tidak mungkin untuk tidak memperhatikan berapa banyak enumerasi fungsional diimplementasikan di Jawa. Di sini enumerasi adalah kelas penuh, dan elemen enumerasi adalah objek dari kelas ini. Ini memungkinkan satu elemen enumerasi untuk mengatur beberapa bidang jenis apa pun menjadi korespondensi:

 enum Colors { //     -   . RED ((byte)0xFF, (byte)0x00, (byte)0x00), GREEN ((byte)0x00, (byte)0xFF, (byte)0x00), BLUE ((byte)0x00, (byte)0x00, (byte)0xFF), WHITE ((byte)0xFF, (byte)0xFF, (byte)0xFF), BLACK ((byte)0x00, (byte)0x00, (byte)0x00); //  . private byte r, g, b; //  . private Colors(byte r, byte g, byte b) { this.r = r; this.g = g; this.b = b; } //  . public double getLuma() { return 0.2126 * r + 0.7152 * g + 0.0722 * b; } } 

Sebagai kelas lengkap, enumerasi dapat memiliki metode, dan menggunakan konstruktor pribadi, Anda dapat mengatur nilai bidang elemen enumerasi individu.

Ada peluang reguler untuk mendapatkan representasi string dari elemen enumerasi, nomor seri, dan juga array semua elemen:

 Colors color = Colors.BLACK; String str = color.toString(); // "BLACK" int i = color.ordinal(); // 4 Colors[] array = Colors.values(); // [RED, GREEN, BLUE, WHITE, BLACK] 

Dan sebaliknya - dengan representasi string, Anda bisa mendapatkan elemen enumerasi, dan juga memanggil metode-metodenya:

 Colors red = Colors.valueOf("RED"); // Colors.RED Double redLuma = red.getLuma(); // 0.2126 * 255 

Secara alami, enumerasi dapat digunakan dalam konstruksi kasus sakelar.

Kesimpulan


Tentu saja, bahasa C dan Java dirancang untuk menyelesaikan masalah yang sama sekali berbeda. Tetapi, jika kita tetap membandingkan proses pengembangan perangkat lunak dalam dua bahasa ini, maka, menurut kesan subjektif penulis, bahasa Jawa secara signifikan melampaui C dalam kenyamanan dan kecepatan penulisan program. Lingkungan pengembangan (IDE) memainkan peran penting dalam memberikan kenyamanan. Penulis bekerja dengan IDE IntelliJ IDEA. Saat memprogram di Jawa, Anda tidak harus selalu "takut" untuk melakukan kesalahan - seringkali lingkungan pengembangan akan memberi tahu Anda apa yang perlu diperbaiki, dan kadang-kadang itu akan melakukannya untuk Anda. Jika kesalahan runtime terjadi, maka jenis kesalahan dan tempat kemunculannya dalam kode sumber selalu ditunjukkan dalam log - perjuangan melawan kesalahan tersebut menjadi masalah sepele. Seorang programmer C tidak perlu melakukan upaya yang tidak manusiawi untuk beralih ke Java, dan semua karena sintaks bahasa telah sedikit berubah.

Jika pengalaman ini akan menarik bagi pembaca, pada artikel selanjutnya kita akan berbicara tentang pengalaman menggunakan mekanisme JNI (menjalankan kode C / C ++ asli dari aplikasi Java). Mekanisme JNI sangat diperlukan ketika Anda ingin mengontrol resolusi layar, modul Bluetooth, dan dalam kasus lain ketika kemampuan layanan dan manajer Android tidak cukup.

Source: https://habr.com/ru/post/id412775/


All Articles