Warisan versus komposisi: Bagaimana memilih

Pewarisan dan komposisi adalah dua teknik pemrograman yang digunakan pengembang untuk membangun hubungan antara kelas dan objek. Sedangkan pewarisan menurunkan satu kelas dari yang lain, komposisi mendefinisikan kelas sebagai jumlah bagiannya.

Kelas dan objek yang dibuat melalui pewarisan sangat erat karena mengubah induk atau superclass dalam hubungan pewarisan berisiko merusak kode Anda. Kelas dan objek yang dibuat melalui komposisi digabungkan secara longgar , artinya Anda dapat lebih mudah mengubah bagian komponen tanpa merusak kode Anda.

Karena kode yang digabungkan secara longgar menawarkan lebih banyak fleksibilitas, banyak pengembang telah belajar bahwa komposisi adalah teknik yang lebih baik daripada pewarisan, tetapi kenyataannya lebih kompleks. Memilih alat pemrograman serupa dengan memilih alat dapur yang benar: Anda tidak akan menggunakan pisau mentega untuk memotong sayuran, dan dengan cara yang sama Anda tidak boleh memilih komposisi untuk setiap skenario pemrograman. 

Dalam Java Challenger ini Anda akan mempelajari perbedaan antara pewarisan dan komposisi, serta cara memutuskan mana yang benar untuk program Anda. Selanjutnya, saya akan memperkenalkan Anda pada beberapa aspek penting namun menantang dari warisan Java: penimpaan metode, superkata kunci, dan pemilihan tipe. Terakhir, Anda akan menguji apa yang telah Anda pelajari dengan mengerjakan contoh warisan baris demi baris untuk menentukan keluaran yang seharusnya.

Kapan menggunakan warisan di Java

Dalam pemrograman berorientasi objek, kita dapat menggunakan pewarisan ketika kita tahu ada hubungan "ada" antara anak dan kelas induknya. Beberapa contohnya adalah:

  • Seseorang adalah manusia.
  • Seekor kucing adalah binatang.
  • Mobil adalah   kendaraan.

Dalam setiap kasus, anak atau subkelas adalah versi khusus dari induk atau kelas super. Mewarisi dari superclass adalah contoh penggunaan kembali kode. Untuk lebih memahami hubungan ini, luangkan waktu sejenak untuk mempelajari Carkelas, yang diwarisi dari Vehicle:

 class Vehicle { String brand; String color; double weight; double speed; void move() { System.out.println("The vehicle is moving"); } } public class Car extends Vehicle { String licensePlateNumber; String owner; String bodyStyle; public static void main(String... inheritanceExample) { System.out.println(new Vehicle().brand); System.out.println(new Car().brand); new Car().move(); } } 

Saat Anda mempertimbangkan untuk menggunakan warisan, tanyakan pada diri Anda apakah subclass benar-benar merupakan versi superclass yang lebih terspesialisasi. Dalam hal ini, mobil adalah sejenis kendaraan, jadi hubungan warisan masuk akal. 

Kapan menggunakan komposisi di Java

Dalam pemrograman berorientasi objek, kita dapat menggunakan komposisi dalam kasus di mana satu objek "memiliki" (atau merupakan bagian dari) objek lain. Beberapa contohnya adalah:

  • Mobil memiliki baterai (baterai adalah bagian dari mobil).
  • Seseorang memiliki hati (hati adalah bagian dari seseorang).
  • Rumah memiliki ruang tamu (ruang tamu adalah bagian dari rumah).

Untuk lebih memahami jenis hubungan ini, pertimbangkan komposisi a House:

 public class CompositionExample { public static void main(String... houseComposition) { new House(new Bedroom(), new LivingRoom()); // The house now is composed with a Bedroom and a LivingRoom } static class House { Bedroom bedroom; LivingRoom livingRoom; House(Bedroom bedroom, LivingRoom livingRoom) { this.bedroom = bedroom; this.livingRoom = livingRoom; } } static class Bedroom { } static class LivingRoom { } } 

Dalam hal ini, kita mengetahui bahwa sebuah rumah memiliki ruang tamu dan kamar tidur, sehingga kita dapat menggunakan objek Bedroomdan  LivingRoomdalam komposisi a House

Dapatkan kodenya

Dapatkan kode sumber untuk contoh di Java Challenger ini. Anda dapat menjalankan pengujian Anda sendiri sambil mengikuti contoh.

Warisan vs komposisi: Dua contoh

Perhatikan kode berikut. Apakah ini contoh warisan yang bagus?

 import java.util.HashSet; public class CharacterBadExampleInheritance extends HashSet { public static void main(String... badExampleOfInheritance) { BadExampleInheritance badExampleInheritance = new BadExampleInheritance(); badExampleInheritance.add("Homer"); badExampleInheritance.forEach(System.out::println); } 

Dalam hal ini, jawabannya tidak. Kelas anak mewarisi banyak metode yang tidak akan pernah digunakannya, menghasilkan kode yang digabungkan erat yang membingungkan dan sulit untuk dipertahankan. Jika Anda melihat lebih dekat, jelas juga bahwa kode ini tidak lulus tes "is a".

Sekarang mari kita coba contoh yang sama menggunakan komposisi:

 import java.util.HashSet; import java.util.Set; public class CharacterCompositionExample { static Set set = new HashSet(); public static void main(String... goodExampleOfComposition) { set.add("Homer"); set.forEach(System.out::println); } 

Menggunakan komposisi untuk skenario ini memungkinkan  CharacterCompositionExamplekelas untuk menggunakan hanya dua HashSetmetode, tanpa mewarisi semuanya. Ini menghasilkan kode yang lebih sederhana dan kurang digabungkan yang akan lebih mudah dipahami dan dikelola.

Contoh pewarisan di JDK

Java Development Kit penuh dengan contoh warisan yang bagus:

 class IndexOutOfBoundsException extends RuntimeException {...} class ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException {...} class FileWriter extends OutputStreamWriter {...} class OutputStreamWriter extends Writer {...} interface Stream extends BaseStream
    
      {...} 
    

Perhatikan bahwa dalam setiap contoh ini, kelas anak adalah versi khusus dari induknya; misalnya, IndexOutOfBoundsExceptionadalah tipe RuntimeException.

Metode menimpa dengan warisan Java

Inheritance memungkinkan kita untuk menggunakan kembali metode dan atribut lain dari satu kelas di kelas baru, yang sangat memudahkan. Tetapi agar warisan benar-benar berfungsi, kita juga harus dapat mengubah beberapa perilaku yang diwariskan dalam subkelas baru kita. Misalnya, kita mungkin ingin mengkhususkan suara a Catmake:

 class Animal { void emitSound() { System.out.println("The animal emitted a sound"); } } class Cat extends Animal { @Override void emitSound() { System.out.println("Meow"); } } class Dog extends Animal { } public class Main { public static void main(String... doYourBest) { Animal cat = new Cat(); // Meow Animal dog = new Dog(); // The animal emitted a sound Animal animal = new Animal(); // The animal emitted a sound cat.emitSound(); dog.emitSound(); animal.emitSound(); } } 

Ini adalah contoh pewarisan Java dengan penggantian metode. Pertama, kita memperluas dengan Animalkelas untuk membuat yang baru Catkelas. Selanjutnya, kami mengganti metode Animalkelas emitSound()untuk mendapatkan suara spesifik yang Catdibuat. Meskipun kita telah mendeklarasikan tipe kelas sebagai Animal, ketika kita membuatnya seperti Catkita akan mendapatkan kucing meong. 

Penimpaan metode adalah polimorfisme

Anda mungkin ingat dari posting terakhir saya bahwa metode overriding adalah contoh polimorfisme, atau pemanggilan metode virtual.

Apakah Java memiliki banyak warisan?

Tidak seperti beberapa bahasa, seperti C ++, Java tidak mengizinkan banyak pewarisan dengan kelas. Namun, Anda dapat menggunakan beberapa warisan dengan antarmuka. Perbedaan antara kelas dan antarmuka, dalam hal ini, adalah antarmuka tidak menyimpan status.

Jika Anda mencoba beberapa warisan seperti yang saya miliki di bawah ini, kode tidak akan dapat dikompilasi:

 class Animal {} class Mammal {} class Dog extends Animal, Mammal {} 

Solusi menggunakan kelas akan mewarisi satu-per-satu:

 class Animal {} class Mammal extends Animal {} class Dog extends Mammal {} 

Solusi lain adalah mengganti kelas dengan antarmuka:

 interface Animal {} interface Mammal {} class Dog implements Animal, Mammal {} 

Menggunakan 'super' untuk mengakses metode kelas induk

When two classes are related through inheritance, the child class must be able to access every accessible field, method, or constructor of its parent class. In Java, we use the reserved word super to ensure the child class can still access its parent's overridden method:

 public class SuperWordExample { class Character { Character() { System.out.println("A Character has been created"); } void move() { System.out.println("Character walking..."); } } class Moe extends Character { Moe() { super(); } void giveBeer() { super.move(); System.out.println("Give beer"); } } } 

In this example, Character is the parent class for Moe.  Using super, we are able to access Character's  move() method in order to give Moe a beer.

Using constructors with inheritance

When one class inherits from another, the superclass's constructor always will be loaded first, before loading its subclass. In most cases, the reserved word super will be added automatically to the constructor.  However, if the superclass has a parameter in its constructor, we will have to deliberately invoke the super constructor, as shown below:

 public class ConstructorSuper { class Character { Character() { System.out.println("The super constructor was invoked"); } } class Barney extends Character { // No need to declare the constructor or to invoke the super constructor // The JVM will to that } } 

If the parent class has a constructor with at least one parameter, then we must declare the constructor in the subclass and use super to explicitly invoke the parent constructor. The super reserved word won't be added automatically and the code won't compile without it.  For example:

 public class CustomizedConstructorSuper { class Character { Character(String name) { System.out.println(name + "was invoked"); } } class Barney extends Character { // We will have compilation error if we don't invoke the constructor explicitly // We need to add it Barney() { super("Barney Gumble"); } } } 

Type casting and the ClassCastException

Casting is a way of explicitly communicating to the compiler that you really do intend to convert a given type.  It's like saying, "Hey, JVM, I know what I'm doing so please cast this class with this type." If a class you've cast isn't compatible with the class type you declared, you will get a ClassCastException.

In inheritance, we can assign the child class to the parent class without casting but we can't assign a parent class to the child class without using casting.

Consider the following example:

 public class CastingExample { public static void main(String... castingExample) { Animal animal = new Animal(); Dog dogAnimal = (Dog) animal; // We will get ClassCastException Dog dog = new Dog(); Animal dogWithAnimalType = new Dog(); Dog specificDog = (Dog) dogWithAnimalType; specificDog.bark(); Animal anotherDog = dog; // It's fine here, no need for casting System.out.println(((Dog)anotherDog)); // This is another way to cast the object } } class Animal { } class Dog extends Animal { void bark() { System.out.println("Au au"); } } 

When we try to cast an Animal instance to a Dog we get an exception. This is because the Animal doesn't know anything about its child. It could be a cat, a bird, a lizard, etc. There is no information about the specific animal. 

The problem in this case is that we've instantiated Animal like this:

 Animal animal = new Animal(); 

Then tried to cast it like this:

 Dog dogAnimal = (Dog) animal; 

Because we don't have a Dog instance, it's impossible to assign an Animal to the Dog.  If we try, we will get a ClassCastException

In order to avoid the exception, we should instantiate the Dog like this:

 Dog dog = new Dog(); 

then assign it to Animal:

 Animal anotherDog = dog; 

In this case, because  we've extended the Animal class, the Dog instance doesn't even need to be cast; the Animal parent class type simply accepts the assignment.

Casting with supertypes

Ini mungkin untuk mendeklarasikan a Dogdengan supertipe Animal, tetapi jika kita ingin memanggil metode tertentu dari Dog, kita perlu mentransmisikannya. Sebagai contoh, bagaimana jika kita ingin memanggil bark()metode tersebut? The Animalsupertype tidak memiliki cara untuk mengetahui apa yang hewan misalnya kita memohon sedang, jadi kita harus cor Dogmanual sebelum kita dapat memanggil bark()metode:

 Animal dogWithAnimalType = new Dog(); Dog specificDog = (Dog) dogWithAnimalType; specificDog.bark(); 

Anda juga dapat menggunakan transmisi tanpa menetapkan objek ke jenis kelas. Pendekatan ini berguna saat Anda tidak ingin mendeklarasikan variabel lain:

 System.out.println(((Dog)anotherDog)); // This is another way to cast the object 

Ambil tantangan warisan Java!

Anda telah mempelajari beberapa konsep penting tentang warisan, jadi sekarang saatnya mencoba tantangan warisan. Untuk memulai, pelajari kode berikut: