Metode Overloading dan Overriding di Java

1. Ikhtisar

Metode overloading dan overriding adalah konsep kunci dari bahasa pemrograman Java, dan karena itu, mereka layak untuk dilihat secara mendalam.

Dalam artikel ini, kita akan mempelajari dasar-dasar konsep ini dan melihat dalam situasi apa konsep tersebut dapat berguna.

2. Metode Overloading

Metode overloading adalah mekanisme yang kuat yang memungkinkan kita untuk mendefinisikan API kelas yang kohesif. Untuk lebih memahami mengapa metode overloading adalah fitur yang sangat berharga, mari kita lihat contoh sederhana.

Misalkan kita telah menulis kelas utilitas naif yang mengimplementasikan metode berbeda untuk mengalikan dua angka, tiga angka, dan seterusnya.

Jika kita telah memberikan metode nama yang menyesatkan atau ambigu, seperti multiply2 () , multiply3 () , multiply4 (), maka itu akan menjadi API kelas yang dirancang dengan buruk. Di sinilah metode overloading berperan.

Sederhananya, kita dapat mengimplementasikan overloading metode dalam dua cara berbeda:

  • mengimplementasikan dua atau lebih metode yang memiliki nama yang sama tetapi menggunakan jumlah argumen yang berbeda
  • menerapkan dua atau lebih metode yang memiliki nama yang sama tetapi mengambil argumen dari jenis yang berbeda

2.1. Jumlah Argumen yang Berbeda

The Multiplier menunjukkan kelas, singkatnya, bagaimana membebani multiply () metode dengan hanya mendefinisikan dua implementasi yang mengambil nomor yang berbeda argumen:

public class Multiplier { public int multiply(int a, int b) { return a * b; } public int multiply(int a, int b, int c) { return a * b * c; } }

2.2. Argumen dari Berbagai Jenis

Demikian pula, kita bisa membebani metode multiply () dengan membuatnya menerima argumen dari tipe yang berbeda:

public class Multiplier { public int multiply(int a, int b) { return a * b; } public double multiply(double a, double b) { return a * b; } } 

Selain itu, sah untuk menentukan kelas Pengali dengan kedua jenis metode overloading:

public class Multiplier { public int multiply(int a, int b) { return a * b; } public int multiply(int a, int b, int c) { return a * b * c; } public double multiply(double a, double b) { return a * b; } } 

Namun, perlu diperhatikan bahwa tidak mungkin memiliki dua implementasi metode yang hanya berbeda dalam jenis kembaliannya .

Untuk memahami alasannya - mari pertimbangkan contoh berikut:

public int multiply(int a, int b) { return a * b; } public double multiply(int a, int b) { return a * b; }

Dalam kasus ini, kode tidak dapat dikompilasi karena metode memanggil ambiguitas - kompilator tidak akan tahu implementasi multiply () mana yang akan dipanggil.

2.3. Jenis Promosi

Salah satu fitur rapi yang disediakan oleh metode overloading adalah apa yang disebut promosi tipe, alias konversi primitif pelebaran .

Secara sederhana, satu tipe tertentu secara implisit dipromosikan ke tipe lain ketika tidak ada kecocokan antara tipe argumen yang diteruskan ke metode kelebihan beban dan implementasi metode tertentu.

Untuk memahami lebih jelas cara kerja promosi tipe, pertimbangkan implementasi metode multiply () berikut :

public double multiply(int a, long b) { return a * b; } public int multiply(int a, int b, int c) { return a * b * c; } 

Sekarang, memanggil metode dengan dua argumen int akan mengakibatkan argumen kedua dipromosikan menjadi long , karena dalam kasus ini tidak ada implementasi metode yang cocok dengan dua argumen int .

Mari kita lihat unit test cepat untuk mendemonstrasikan promosi tipe:

@Test public void whenCalledMultiplyAndNoMatching_thenTypePromotion() { assertThat(multiplier.multiply(10, 10)).isEqualTo(100.0); }

Sebaliknya, jika kita memanggil metode dengan implementasi yang cocok, promosi jenis tidak akan terjadi:

@Test public void whenCalledMultiplyAndMatching_thenNoTypePromotion() { assertThat(multiplier.multiply(10, 10, 10)).isEqualTo(1000); }

Berikut ringkasan aturan promosi jenis yang berlaku untuk metode overloading:

  • byte bisa dipromosikan menjadi short, int, long, float, atau double
  • pendek bisa dipromosikan menjadi int, long, float, atau double
  • char dapat dipromosikan menjadi int, long, float, atau double
  • int dapat dipromosikan menjadi long, float, atau double
  • panjang bisa dipromosikan menjadi float atau double
  • float bisa dipromosikan menjadi double

2.4. Pengikatan Statis

Kemampuan untuk mengaitkan panggilan metode tertentu ke tubuh metode dikenal sebagai binding.

Dalam kasus overloading metode, pengikatan dilakukan secara statis pada waktu kompilasi, oleh karena itu disebut pengikatan statis.

Kompilator dapat secara efektif mengatur pengikatan pada waktu kompilasi hanya dengan memeriksa tanda tangan metode.

3. Mengganti Metode

Penimpaan metode memungkinkan kita menyediakan implementasi terperinci di subkelas untuk metode yang ditentukan dalam kelas dasar.

Meskipun penggantian metode adalah fitur yang kuat - mengingat itu adalah konsekuensi logis dari penggunaan warisan, salah satu pilar terbesar OOP - kapan dan di mana menggunakannya harus dianalisis dengan hati-hati, berdasarkan kasus penggunaan .

Mari kita lihat sekarang bagaimana menggunakan metode overriding dengan membuat hubungan sederhana berbasis warisan ("is-a").

Inilah kelas dasarnya:

public class Vehicle { public String accelerate(long mph) { return "The vehicle accelerates at : " + mph + " MPH."; } public String stop() { return "The vehicle has stopped."; } public String run() { return "The vehicle is running."; } }

Dan inilah subclass yang dibuat-buat:

public class Car extends Vehicle { @Override public String accelerate(long mph) { return "The car accelerates at : " + mph + " MPH."; } }

Dalam hierarki di atas, kita hanya mengganti metode accelerate () untuk menyediakan implementasi yang lebih halus untuk subtipe Car.

Di sini, jelas terlihat bahwa jika sebuah aplikasi menggunakan instance kelas Vehicle , maka itu bisa bekerja dengan instance Car juga , karena kedua implementasi metode accelerate () memiliki tanda tangan yang sama dan tipe kembalian yang sama.

Mari kita tulis beberapa unit test untuk memeriksa kelas Kendaraan dan Mobil :

@Test public void whenCalledAccelerate_thenOneAssertion() { assertThat(vehicle.accelerate(100)) .isEqualTo("The vehicle accelerates at : 100 MPH."); } @Test public void whenCalledRun_thenOneAssertion() { assertThat(vehicle.run()) .isEqualTo("The vehicle is running."); } @Test public void whenCalledStop_thenOneAssertion() { assertThat(vehicle.stop()) .isEqualTo("The vehicle has stopped."); } @Test public void whenCalledAccelerate_thenOneAssertion() { assertThat(car.accelerate(80)) .isEqualTo("The car accelerates at : 80 MPH."); } @Test public void whenCalledRun_thenOneAssertion() { assertThat(car.run()) .isEqualTo("The vehicle is running."); } @Test public void whenCalledStop_thenOneAssertion() { assertThat(car.stop()) .isEqualTo("The vehicle has stopped."); } 

Sekarang, mari kita lihat beberapa pengujian unit yang menunjukkan bagaimana metode run () dan stop () , yang tidak diganti, mengembalikan nilai yang sama untuk Car dan Vehicle :

@Test public void givenVehicleCarInstances_whenCalledRun_thenEqual() { assertThat(vehicle.run()).isEqualTo(car.run()); } @Test public void givenVehicleCarInstances_whenCalledStop_thenEqual() { assertThat(vehicle.stop()).isEqualTo(car.stop()); }

Dalam kasus kami, kami memiliki akses ke kode sumber untuk kedua kelas, sehingga kami dapat dengan jelas melihat bahwa memanggil metode accelerate () pada instance Vehicle dasar dan memanggil accelerate () pada instance Car akan mengembalikan nilai yang berbeda untuk argumen yang sama.

Oleh karena itu, pengujian berikut menunjukkan bahwa metode yang diganti dipanggil untuk instance Mobil :

@Test public void whenCalledAccelerateWithSameArgument_thenNotEqual() { assertThat(vehicle.accelerate(100)) .isNotEqualTo(car.accelerate(100)); }

3.1. Ketik Substitutabilitas

A core principle in OOP is that of type substitutability, which is closely associated with the Liskov Substitution Principle (LSP).

Simply put, the LSP states that if an application works with a given base type, then it should also work with any of its subtypes. That way, type substitutability is properly preserved.

The biggest problem with method overriding is that some specific method implementations in the derived classes might not fully adhere to the LSP and therefore fail to preserve type substitutability.

Of course, it's valid to make an overridden method to accept arguments of different types and return a different type as well, but with full adherence to these rules:

  • If a method in the base class takes argument(s) of a given type, the overridden method should take the same type or a supertype (a.k.a. contravariant method arguments)
  • If a method in the base class returns void, the overridden method should return void
  • If a method in the base class returns a primitive, the overridden method should return the same primitive
  • If a method in the base class returns a certain type, the overridden method should return the same type or a subtype (a.k.a. covariant return type)
  • If a method in the base class throws an exception, the overridden method must throw the same exception or a subtype of the base class exception

3.2. Dynamic Binding

Mempertimbangkan bahwa penggantian metode hanya dapat diterapkan dengan pewarisan, di mana terdapat hierarki jenis dasar dan subtipe, kompilator tidak dapat menentukan pada waktu kompilasi metode apa yang akan dipanggil, karena kelas dasar dan subkelas mendefinisikan metode yang sama.

Akibatnya, kompilator perlu memeriksa jenis objek untuk mengetahui metode apa yang harus dipanggil.

Karena pemeriksaan ini terjadi pada waktu proses, penggantian metode adalah contoh umum pengikatan dinamis.

4. Kesimpulan

Dalam tutorial ini, kami mempelajari cara mengimplementasikan overloading metode dan penggantian metode, dan kami menjelajahi beberapa situasi umum yang berguna.

Seperti biasa, semua contoh kode yang ditampilkan dalam artikel ini tersedia di GitHub.