Προγραμματισμός

Κληρονομικότητα έναντι σύνθεσης: Πώς να επιλέξετε

Η κληρονομικότητα και η σύνθεση είναι δύο τεχνικές προγραμματισμού που χρησιμοποιούν οι προγραμματιστές για τη δημιουργία σχέσεων μεταξύ τάξεων και αντικειμένων. Ενώ η κληρονομιά προέρχεται από μια κατηγορία από την άλλη, η σύνθεση ορίζει μια τάξη ως το άθροισμα των μερών της.

Τα μαθήματα και τα αντικείμενα που δημιουργούνται μέσω της κληρονομιάς είναι σφιχτά συνδεδεμένο επειδή η αλλαγή του γονέα ή του superclass σε σχέση κληρονομιάς κινδυνεύει να σπάσει τον κωδικό σας. Τα μαθήματα και τα αντικείμενα που δημιουργούνται μέσω της σύνθεσης είναι χαλαρά συνδεδεμένο, που σημαίνει ότι μπορείτε να αλλάξετε ευκολότερα τα εξαρτήματα χωρίς να σπάσετε τον κωδικό σας.

Επειδή ο χαλαρά συνδεδεμένος κώδικας προσφέρει περισσότερη ευελιξία, πολλοί προγραμματιστές έχουν μάθει ότι η σύνθεση είναι καλύτερη τεχνική από την κληρονομιά, αλλά η αλήθεια είναι πιο περίπλοκη. Η επιλογή ενός εργαλείου προγραμματισμού είναι παρόμοια με την επιλογή του σωστού εργαλείου κουζίνας: Δεν θα χρησιμοποιούσατε ένα μαχαίρι βουτύρου για να κόψετε λαχανικά και με τον ίδιο τρόπο δεν πρέπει να επιλέξετε σύνθεση για κάθε σενάριο προγραμματισμού.

Σε αυτό το Java Challenger θα μάθετε τη διαφορά μεταξύ κληρονομιάς και σύνθεσης και πώς να αποφασίσετε ποια είναι σωστή για το πρόγραμμά σας. Στη συνέχεια, θα σας παρουσιάσω αρκετές σημαντικές αλλά προκλητικές πτυχές της κληρονομιάς Java: παράκαμψη μεθόδου, το σούπερ λέξη-κλειδί και τύπος μετάδοσης. Τέλος, θα δοκιμάσετε τι έχετε μάθει δουλεύοντας μέσω ενός παραδείγματος κληρονομιάς γραμμής προς γραμμή για να προσδιορίσετε ποιο θα πρέπει να είναι το αποτέλεσμα.

Πότε να χρησιμοποιήσετε κληρονομιά στην Java

Στον αντικειμενοστρεφή προγραμματισμό, μπορούμε να χρησιμοποιήσουμε την κληρονομιά όταν γνωρίζουμε ότι υπάρχει μια σχέση "είναι" μεταξύ ενός παιδιού και της τάξης των γονέων του. Μερικά παραδείγματα θα ήταν:

  • Ενα άτομο είναι ένα ο άνθρωπος.
  • Μια γάτα είναι ένα ζώο.
  • Ενα αυτοκίνητο είναι ένα όχημα.

Σε κάθε περίπτωση, το παιδί ή η υποκατηγορία είναι α ειδικευμένος έκδοση του γονέα ή του superclass. Η κληρονομιά από το superclass είναι ένα παράδειγμα επαναχρησιμοποίησης κώδικα. Για να κατανοήσετε καλύτερα αυτήν τη σχέση, αφιερώστε λίγο χρόνο για να μελετήσετε το Αυτοκίνητο τάξη, η οποία κληρονομεί από Οχημα:

 Όχημα κατηγορίας {μάρκα String; Χρώμα χορδής; διπλό βάρος διπλή ταχύτητα? άκυρη κίνηση () {System.out.println ("Το όχημα κινείται"); }} δημόσια τάξη Το αυτοκίνητο επεκτείνει το όχημα {String licensePlateNumber; Ιδιοκτήτης συμβολοσειράς; String bodyStyle; public static void main (String ... HeritageExample) {System.out.println (νέο Vehicle (). brand); System.out.println (νέο αυτοκίνητο (). Μάρκα); νέο αυτοκίνητο (). μετακίνηση (); }} 

Όταν σκέφτεστε να χρησιμοποιήσετε την κληρονομιά, αναρωτηθείτε αν η υποκατηγορία είναι πραγματικά μια πιο εξειδικευμένη έκδοση του superclass. Σε αυτήν την περίπτωση, ένα αυτοκίνητο είναι ένας τύπος οχήματος, επομένως η σχέση κληρονομιάς έχει νόημα.

Πότε να χρησιμοποιήσετε τη σύνθεση σε Java

Στον αντικειμενοστραφή προγραμματισμό, μπορούμε να χρησιμοποιήσουμε σύνθεση σε περιπτώσεις όπου ένα αντικείμενο "έχει" (ή είναι μέρος) ενός άλλου αντικειμένου. Μερικά παραδείγματα θα ήταν:

  • Ενα αυτοκίνητο έχει ένα μπαταρία (μια μπαταρία είναι μέρος του ένα αυτοκίνητο).
  • Ενα άτομο έχει ένα καρδιά (καρδιά) είναι μέρος του ένα άτομο).
  • Ενα σπίτι έχει ένα σαλόνι (σαλόνι είναι μέρος του ένα σπίτι).

Για να κατανοήσετε καλύτερα αυτόν τον τύπο σχέσης, σκεφτείτε τη σύνθεση του α σπίτι:

 δημόσια τάξη CompositionExample {public static void main (String ... houseComposition) {new House (new Bedroom (), new LivingRoom ()); // Το σπίτι αποτελείται τώρα από ένα υπνοδωμάτιο και ένα LivingRoom} στατική τάξη Σπίτι {Υπνοδωμάτιο υπνοδωμάτιο. LivingRoom LivingRoom; Σπίτι (Υπνοδωμάτιο υπνοδωμάτιο, LivingRoom livingRoom) {this.bedroom = υπνοδωμάτιο; this.livingRoom = livingRoom; }} στατική τάξη Υπνοδωμάτιο {} στατική τάξη LivingRoom {}} 

Σε αυτήν την περίπτωση, γνωρίζουμε ότι ένα σπίτι διαθέτει σαλόνι και υπνοδωμάτιο, ώστε να μπορούμε να το χρησιμοποιήσουμε Υπνοδωμάτιο και Σαλόνι αντικείμενα στη σύνθεση ενός σπίτι

Λάβετε τον κωδικό

Λάβετε τον πηγαίο κώδικα για παραδείγματα σε αυτό το Java Challenger. Μπορείτε να εκτελέσετε τις δικές σας δοκιμές ακολουθώντας τα παραδείγματα.

Κληρονομικότητα έναντι σύνθεσης: Δύο παραδείγματα

Εξετάστε τον ακόλουθο κωδικό. Είναι καλό παράδειγμα κληρονομιάς;

 εισαγωγή java.util.HashSet; δημόσια κλάση CharacterBadExampleInheritance επεκτείνει το HashSet {public static void main (String ... badExampleOfInheritance) {BadExampleInheritance badExampleInheritance = new BadExampleInheritance (); badExampleInheritance.add ("Όμηρος"); badExampleInheritance.forEach (System.out :: println); } 

Σε αυτήν την περίπτωση, η απάντηση είναι όχι. Η παιδική τάξη κληρονομεί πολλές μεθόδους που δεν θα χρησιμοποιήσει ποτέ, με αποτέλεσμα έναν σφιχτά συζευγμένο κώδικα που είναι συγχέοντας και δύσκολο να διατηρηθεί. Εάν κοιτάξετε προσεκτικά, είναι επίσης σαφές ότι αυτός ο κωδικός δεν περνάει το τεστ "είναι ένα".

Τώρα ας δοκιμάσουμε το ίδιο παράδειγμα χρησιμοποιώντας τη σύνθεση:

 εισαγωγή java.util.HashSet; εισαγωγή java.util.Set; δημόσια κλάση CharacterCompositionExample {static Set set = new HashSet (); public static void main (String ... goodExampleOfComposition) {set.add ("Όμηρος"); set.forEach (System.out :: println); } 

Η χρήση σύνθεσης για αυτό το σενάριο επιτρέπει το CharacterCompositionExample τάξη για να χρησιμοποιήσετε μόνο δύο από HashSetτις μεθόδους, χωρίς να τις κληρονομήσουμε όλες. Αυτό έχει ως αποτέλεσμα έναν απλούστερο, λιγότερο συζευγμένο κώδικα που θα είναι ευκολότερο να κατανοηθεί και να διατηρηθεί.

Παραδείγματα κληρονομικότητας στο JDK

Το Java Development Kit είναι γεμάτο καλά παραδείγματα κληρονομιάς:

 Η κλάση IndexOutOfBoundsException επεκτείνει το RuntimeException {...} Η κλάση ArrayIndexOutOfBoundsException επεκτείνει το IndexOutOfBoundsException {...} class FileWriter επεκτείνει το OutputStreamWriter {...} Η κλάση OutputStreamWriter επεκτείνει το Writer {...} Η ροή επεκτείνει το BaseStream {...} 

Σημειώστε ότι σε κάθε ένα από αυτά τα παραδείγματα, η παιδική τάξη είναι μια εξειδικευμένη έκδοση του γονέα της. για παράδειγμα, IndexOutOfBoundsException είναι ένας τύπος RuntimeException.

Μέθοδος παράκαμψης με κληρονομιά Java

Η κληρονομικότητα μας επιτρέπει να επαναχρησιμοποιήσουμε τις μεθόδους και άλλα χαρακτηριστικά μιας τάξης σε μια νέα τάξη, κάτι που είναι πολύ βολικό. Αλλά για να λειτουργήσει πραγματικά η κληρονομιά, πρέπει επίσης να είμαστε σε θέση να αλλάξουμε μέρος της κληρονομικής συμπεριφοράς στη νέα υποκατηγορία μας. Για παράδειγμα, ίσως θέλουμε να ειδικεύσουμε τον ήχο a Γάτα κάνει:

 τάξη Animal {void emitSound () {System.out.println ("Το ζώο εκπέμπει έναν ήχο"); }} Η κατηγορία Cat επεκτείνει το Animal {@Override void emitSound () {System.out.println ("Meow"); }} Η κατηγορία Dog επεκτείνει το Animal {} public class Main {public static void main (String ... doYourBest) {Animal cat = new Cat (); // Meow Animal dog = νέο σκυλί (); // Το ζώο εκπέμπει ένα υγιές ζώο ζώο = νέο ζώο (); // Το ζώο εκπέμπει έναν ήχο cat.emitSound (); dog.emitSound (); animal.emitSound (); }} 

Αυτό είναι ένα παράδειγμα κληρονομιάς Java με παράκαμψη μεθόδου. Αρχικά, εμείς επεκτείνω ο Ζώο τάξη για να δημιουργήσετε ένα νέο Γάτα τάξη. Στη συνέχεια, εμείς καταπατώ ο Ζώο της τάξης emitSound () μέθοδος για να λάβετε τον συγκεκριμένο ήχο Γάτα κάνει. Παρόλο που έχουμε δηλώσει τον τύπο τάξης ως Ζώο, όταν το δημιουργούμε ως Γάτα θα πάρουμε το νιαούρισμα της γάτας.

Η μέθοδος υπέρβασης είναι ο πολυμορφισμός

Ίσως να θυμάστε από την τελευταία μου ανάρτηση ότι η παράκαμψη της μεθόδου είναι ένα παράδειγμα πολυμορφισμού ή επίκλησης εικονικής μεθόδου.

Η Java έχει πολλαπλή κληρονομιά;

Σε αντίθεση με ορισμένες γλώσσες, όπως το C ++, η Java δεν επιτρέπει πολλαπλή κληρονομιά με τάξεις. Ωστόσο, μπορείτε να χρησιμοποιήσετε πολλαπλή κληρονομιά με διεπαφές. Η διαφορά μεταξύ μιας κλάσης και μιας διεπαφής, σε αυτήν την περίπτωση, είναι ότι οι διεπαφές δεν διατηρούν την κατάσταση.

Εάν επιχειρήσετε πολλαπλή κληρονομιά όπως έχω παρακάτω, ο κωδικός δεν θα μεταγλωττιστεί:

 class Animal {} class Mammal {} η κατηγορία Dog επεκτείνει το Animal, Mammal {} 

Μια λύση που χρησιμοποιεί τάξεις θα ήταν να κληρονομήσουμε ένα προς ένα:

 τάξη ζώων {} τάξη θηλαστικό επεκτείνει ζώο {} τάξη σκυλί επεκτείνει θηλαστικό {} 

Μια άλλη λύση είναι να αντικαταστήσετε τις κλάσεις με διεπαφές:

 interface Animal {} interface Mammal {} class Dog υλοποιεί Animal, Mammal {} 

Χρήση του «super» για πρόσβαση σε μεθόδους γονικών τάξεων

Όταν δύο τάξεις σχετίζονται μέσω κληρονομιάς, η παιδική τάξη πρέπει να έχει πρόσβαση σε κάθε προσβάσιμο πεδίο, μέθοδο ή κατασκευαστή της μητρικής του τάξης. Στην Java, χρησιμοποιούμε την δεσμευμένη λέξη σούπερ για να διασφαλιστεί ότι η παιδική τάξη μπορεί να έχει ακόμη πρόσβαση στην παράκαμψη της μεθόδου του γονέα της:

 δημόσια τάξη SuperWordExample {class Character {Character () {System.out.println ("A Character has been δημιουργήθηκε"); } άκυρη κίνηση () {System.out.println ("Character walking ..."); }} Η κλάση Moe επεκτείνει το χαρακτήρα {Moe () {super (); } άκυρο giveBeer () {super.move (); System.out.println ("Δώστε μπύρα"); }}} 

Σε αυτό το παράδειγμα, Χαρακτήρας είναι η τάξη γονέα για Moe. Χρησιμοποιώντας σούπερ, μπορούμε να έχουμε πρόσβαση Χαρακτήρας'μικρό κίνηση() μέθοδο για να δώσει στη Moe μια μπύρα.

Χρήση κατασκευαστών με κληρονομιά

Όταν μια τάξη κληρονομεί από μια άλλη, ο κατασκευαστής του superclass θα φορτώνεται πάντα πρώτα, πριν φορτώσει την υποκατηγορία της. Στις περισσότερες περιπτώσεις, η δεσμευμένη λέξη σούπερ θα προστεθεί αυτόματα στον κατασκευαστή. Ωστόσο, εάν το superclass έχει μια παράμετρο στον κατασκευαστή του, θα πρέπει να επικαλεστεί σκόπιμα το σούπερ κατασκευαστής, όπως φαίνεται παρακάτω:

 δημόσια κλάση ConstructorSuper {class Character {Character () {System.out.println ("Ο σούπερ κατασκευαστής κλήθηκε"); }} Η τάξη Barney επεκτείνει τον χαρακτήρα {// Δεν χρειάζεται να δηλώσει τον κατασκευαστή ή να καλέσει τον σούπερ κατασκευαστή // Η θέληση του JVM σε αυτό}} 

Εάν η μητρική κλάση έχει έναν κατασκευαστή με τουλάχιστον μία παράμετρο, τότε πρέπει να δηλώσουμε τον κατασκευαστή στην υποκατηγορία και να χρησιμοποιήσουμε σούπερ για να καλέσετε ρητά τον γονικό κατασκευαστή. ο σούπερ Η δεσμευμένη λέξη δεν θα προστεθεί αυτόματα και ο κωδικός δεν θα μεταγλωττιστεί χωρίς αυτήν. Για παράδειγμα:

 δημόσια κλάση CustomizedConstructorSuper {class Character {Character (String name) {System.out.println (name + "invused"); }} Η κλάση Barney επεκτείνει το χαρακτήρα {// Θα έχουμε σφάλμα συλλογής εάν δεν επικαλούμεθα τον κατασκευαστή ρητά // Πρέπει να το προσθέσουμε Barney () {super ("Barney Gumble"); }}} 

Τύπος μετάδοσης και το ClassCastException

Η μετάδοση είναι ένας τρόπος ρητής επικοινωνίας στον μεταγλωττιστή που σκοπεύετε πραγματικά να μετατρέψετε έναν συγκεκριμένο τύπο. Είναι σαν να λέτε, "Γεια, JVM, ξέρω τι κάνω, παρακαλώ ρίξτε αυτήν την τάξη με αυτόν τον τύπο." Εάν μια τάξη που έχετε κάνει μετάδοση δεν είναι συμβατή με τον τύπο τάξης που δηλώσατε, θα λάβετε ένα ClassCastException.

Στην κληρονομιά, μπορούμε να αντιστοιχίσουμε την παιδική τάξη στην τάξη γονέων χωρίς μετάδοση, αλλά δεν μπορούμε να εκχωρήσουμε μια τάξη γονέα στην παιδική τάξη χωρίς χρήση μετάδοσης.

Εξετάστε το ακόλουθο παράδειγμα:

 δημόσια τάξη CastingExample {public static void main (String ... castingExample) {Animal animal = new Animal (); Σκύλος σκύλος Ζώο = (Σκύλος) ζώο; // Θα λάβουμε το ClassCastException Dog dog = νέο Dog (); Animal dogWithAnimalType = νέο σκυλί (); Σκύλος συγκεκριμένος Dog = (Dog) dogWithAnimalType; συγκεκριμέναDog.bark (); Animal anotherDog = σκύλος; // Είναι εντάξει εδώ, δεν υπάρχει ανάγκη για casting System.out.println (((Dog) anotherDog)); // Αυτός είναι ένας άλλος τρόπος για να πετάξετε το αντικείμενο}} κλάση ζώου {} τάξη Σκύλος επεκτείνει το ζώο {void bark () {System.out.println ("Au au"); }} 

Όταν προσπαθούμε να ρίξουμε ένα Ζώο παράδειγμα σε ένα Σκύλος έχουμε μια εξαίρεση. Αυτό συμβαίνει επειδή το Ζώο δεν ξέρει τίποτα για το παιδί του. Θα μπορούσε να είναι μια γάτα, ένα πουλί, μια σαύρα κ.λπ. Δεν υπάρχουν πληροφορίες για το συγκεκριμένο ζώο.

Το πρόβλημα σε αυτήν την περίπτωση είναι ότι έχουμε δημιουργήσει Ζώο σαν αυτό:

 Ζώο ζώου = νέο ζώο (); 

Στη συνέχεια προσπάθησε να το ρίξει έτσι:

 Σκύλος σκύλος Ζώο = (Σκύλος) ζώο; 

Επειδή δεν έχουμε Σκύλος Για παράδειγμα, είναι αδύνατο να εκχωρηθεί ένα Ζώο στο Σκύλος. Εάν προσπαθήσουμε, θα πάρουμε ένα ClassCastException

Προκειμένου να αποφευχθεί η εξαίρεση, πρέπει να δημιουργήσουμε το Σκύλος σαν αυτό:

 Dog dog = νέο Dog (); 

και μετά να το αναθέσετε Ζώο:

 Animal anotherDog = σκύλος; 

Σε αυτήν την περίπτωση, επειδή έχουμε επεκτείνει το Ζώο τάξη, το Σκύλος Το παράδειγμα δεν χρειάζεται καν να παίξει. ο Ζώο ο τύπος γονικής κατηγορίας δέχεται απλώς την εργασία.

Μετάδοση με υπερτύπους

Είναι δυνατό να δηλώσετε ένα Σκύλος με το supertype Ζώο, αλλά αν θέλουμε να επικαλεστούμε μια συγκεκριμένη μέθοδο από Σκύλος, θα πρέπει να το ρίξουμε. Για παράδειγμα, τι γίνεται αν θέλουμε να επικαλεστούμε το φλοιός() μέθοδος? ο Ζώο Το supertype δεν έχει κανέναν τρόπο να ξέρει ακριβώς τι ζώα εμφανίζουμε, γι 'αυτό πρέπει να ρίξουμε Σκύλος χειροκίνητα προτού μπορέσουμε να επικαλεστούμε το φλοιός() μέθοδος:

 Animal dogWithAnimalType = νέο σκυλί (); Ειδικός σκύλος Dog = (Dog) dogWithAnimalType; συγκεκριμέναDog.bark (); 

Μπορείτε επίσης να χρησιμοποιήσετε τη μετάδοση χωρίς να αντιστοιχίσετε το αντικείμενο σε έναν τύπο κλάσης. Αυτή η προσέγγιση είναι πρακτική όταν δεν θέλετε να δηλώσετε άλλη μεταβλητή:

 System.out.println (((Dog) anotherDog)); // Αυτός είναι ένας άλλος τρόπος για να ρίξετε το αντικείμενο 

Πάρτε την πρόκληση κληρονομιάς Java!

Έχετε μάθει μερικές σημαντικές έννοιες της κληρονομιάς, οπότε τώρα ήρθε η ώρα να δοκιμάσετε μια πρόκληση κληρονομιάς. Για να ξεκινήσετε, μελετήστε τον ακόλουθο κώδικα: