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

Αποκαλύψτε τη μαγεία πίσω από τον υποτύπο του πολυμορφισμού

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

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

Πολύμορφοι Quattro

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

Ο Luca Cardelli και ο Peter Wegner, συγγραφείς του "On Understanding Types, Data Abstraction and Polymorphism" (βλ. Πόροι για σύνδεση με το άρθρο) χωρίζουν τον πολυμορφισμό σε δύο μεγάλες κατηγορίες - ad hoc και universal - και τέσσερις ποικιλίες: καταναγκασμός, υπερφόρτωση, παραμετρική και ένταξη. Η δομή ταξινόμησης είναι:

 | - εξαναγκασμός | - ad hoc - | | - υπερφόρτωση πολυμορφισμού - | | - παραμετρική | - καθολική - | | - συμπερίληψη 

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

  • Εξαναγκασμός: μια αφαίρεση εξυπηρετεί διάφορους τύπους μέσω της σιωπηρής μετατροπής τύπων
  • Υπερφόρτωση: ένα μοναδικό αναγνωριστικό σημαίνει πολλές αφαιρέσεις
  • Παραμετρική: μια άντληση λειτουργεί ομοιόμορφα σε διαφορετικούς τύπους
  • Συμπερίληψη: μια αφαίρεση λειτουργεί μέσω μιας σχέσης ένταξης

Θα συζητήσω εν συντομία κάθε ποικιλία πριν στραφώ συγκεκριμένα στον πολυμορφισμό υποτύπων.

Εξαναγκασμός

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

 2.0 + 2.0 2.0 + 2 2.0 + "2" 

Η πρώτη έκφραση προσθέτει δύο διπλό τελεστές; Η γλώσσα Java ορίζει συγκεκριμένα έναν τέτοιο χειριστή.

Ωστόσο, η δεύτερη έκφραση προσθέτει ένα διπλό και ένα int; Η Java δεν ορίζει έναν τελεστή που αποδέχεται αυτούς τους τύπους τελεστών. Ευτυχώς, ο μεταγλωττιστής μετατρέπει έμμεσα τον δεύτερο τελεστή σε διπλό και χρησιμοποιεί τον τελεστή που ορίζεται για δύο διπλό τελεστές. Αυτό είναι εξαιρετικά βολικό για τον προγραμματιστή. Χωρίς τη σιωπηρή μετατροπή, θα προέκυπτε σφάλμα χρόνου μεταγλώττισης ή ο προγραμματιστής θα έπρεπε να ρίξει ρητά το int προς την διπλό.

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

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

 C c = νέο C (); Παράγωγα παράγωγα = νέα παράγωγα (); c.m (παράγεται); 

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

Υπερφόρτωση

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

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

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

Παραμετρική

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

Με την πρώτη ματιά, τα παραπάνω Λίστα η αφαίρεση μπορεί να φαίνεται να είναι η χρησιμότητα της τάξης java.util. Λίστα. Ωστόσο, η Java δεν υποστηρίζει τον αληθινό παραμετρικό πολυμορφισμό με έναν ασφαλή τύπο, γι 'αυτό java.util. Λίστα και java.utilΟι άλλες κατηγορίες συλλογής γράφονται με βάση την αρχική τάξη Java, java.lang.Object. (Ανατρέξτε στο άρθρο μου "A Primordial Interface;" για περισσότερες λεπτομέρειες.) Η κληρονομιά υλοποίησης της Java μεμονωμένα προσφέρει μια μερική λύση, αλλά όχι την πραγματική δύναμη του παραμετρικού πολυμορφισμού. Το εξαιρετικό άρθρο του Eric Allen, "Ιδού η Δύναμη του Παραμετρικού Πολυμορφισμού", περιγράφει την ανάγκη για γενικούς τύπους στην Java και τις προτάσεις για την αντιμετώπιση του αιτήματος προδιαγραφής Java της Sun # 000014, "Προσθήκη γενικών τύπων στη γλώσσα προγραμματισμού Java." (Δείτε τους πόρους για έναν σύνδεσμο.)

Συμπερίληψη

Ο πολυμορφισμός εγκλεισμού επιτυγχάνει πολυμορφική συμπεριφορά μέσω μιας σχέσης ένταξης μεταξύ τύπων ή συνόλων τιμών. Για πολλές αντικειμενοστρεφείς γλώσσες, συμπεριλαμβανομένης της Java, η σχέση συμπερίληψης είναι μια σχέση υποτύπου. Έτσι στην Java, ο πολυμορφισμός εγκλεισμού είναι πολυμορφισμός υποτύπων.

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

Προσανατολισμένη προβολή

Το διάγραμμα τάξης UML στο Σχήμα 1 δείχνει την απλή ιεραρχία τύπου και τάξης που χρησιμοποιείται για την απεικόνιση των μηχανισμών του πολυμορφισμού. Το μοντέλο απεικονίζει πέντε τύπους, τέσσερις κατηγορίες και μία διεπαφή. Αν και το μοντέλο ονομάζεται διάγραμμα τάξης, το θεωρώ ως διάγραμμα τύπου. Όπως περιγράφεται λεπτομερώς στο "Thanks Type and Gentle Class", κάθε κλάση Java και διεπαφή δηλώνει έναν τύπο δεδομένων που καθορίζεται από το χρήστη. Έτσι, από μια προβολή ανεξάρτητη από την υλοποίηση (δηλαδή, μια προβολή προσανατολισμένη στον τύπο) καθένα από τα πέντε ορθογώνια στο σχήμα αντιπροσωπεύει έναν τύπο. Από άποψη εφαρμογής, τέσσερις από αυτούς τους τύπους ορίζονται χρησιμοποιώντας κατασκευές κλάσης και ένας ορίζεται χρησιμοποιώντας διεπαφή.

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

/ * Base.java * / public class Base {public String m1 () {return "Base.m1 ()"; } δημόσια συμβολοσειρά m2 (String s) {return "Base.m2 (" + s + ")"; }} / * IType.java * / διεπαφή IType {String m2 (String s); Συμβολοσειρά m3 (); } / * Derived.java * / public class Derived επεκτείνει Βασικές εφαρμογές IType {public String m1 () {return "Derived.m1 ()"; } δημόσια συμβολοσειρά m3 () {return "Derived.m3 ()"; }} / * Derived2.java * / δημόσια τάξη Το Derived2 επεκτείνει το Derived {public String m2 (String s) {return "Derived2.m2 (" + s + ")"; } δημόσια συμβολοσειρά m4 () {return "Derived2.m4 ()"; }} / * Separate.java * / public class Ξεχωριστή εφαρμογή IType {public String m1 () {return "Separate.m1 ()"; } δημόσια συμβολοσειρά m2 (String s) {return "Separate.m2 (" + s + ")"; } δημόσια συμβολοσειρά m3 () {return "Separate.m3 ()"; }} 

Χρησιμοποιώντας αυτές τις δηλώσεις τύπου και ορισμούς κλάσης, το Σχήμα 2 απεικονίζει μια εννοιολογική άποψη της δήλωσης Java:

Derived2 deriv2 = νέο Derived2 (); 

Η παραπάνω δήλωση δηλώνει μια ρητά δακτυλογραφημένη μεταβλητή αναφοράς, παράγεται2, και επισυνάπτει αυτήν την αναφορά σε μια νέα δημιουργία Προήλθε2 αντικείμενο κλάσης. Ο επάνω πίνακας στο σχήμα 2 απεικονίζει το Προήλθε2 αναφορά ως ένα σύνολο παραφωτίδων, μέσω των οποίων το υποκείμενο Προήλθε2 το αντικείμενο μπορεί να προβληθεί. Υπάρχει μια τρύπα για κάθε μία Προήλθε2 τύπος λειτουργίας. Το πραγματικό Προήλθε2 κάθε αντικείμενο χαρτών Προήλθε2 λειτουργία στον κατάλληλο κώδικα εφαρμογής, όπως ορίζεται από την ιεραρχία εφαρμογής που ορίζεται στον παραπάνω κώδικα. Για παράδειγμα, το Προήλθε2 χάρτες αντικειμένων m1 () στον κώδικα εφαρμογής που ορίζεται στην κλάση Συμπληρωματικός. Επιπλέον, αυτός ο κώδικας εφαρμογής παρακάμπτει το m1 () μέθοδος στην τάξη Βάση. ΕΝΑ Προήλθε2 η μεταβλητή αναφοράς δεν μπορεί να έχει πρόσβαση στην παράκαμψη m1 () εφαρμογή στην τάξη Βάση. Αυτό δεν σημαίνει ότι ο πραγματικός κώδικας εφαρμογής στην τάξη Συμπληρωματικός δεν μπορώ να χρησιμοποιήσω το Βάση εφαρμογή τάξης μέσω σούπερ.m1 (). Όμως, όσον αφορά τη μεταβλητή αναφοράς παράγεται2 ανησυχεί, αυτός ο κωδικός δεν είναι προσβάσιμος. Οι αντιστοιχίσεις του άλλου Προήλθε2 Οι λειτουργίες δείχνουν παρόμοια τον κώδικα εφαρμογής που εκτελείται για κάθε τύπο λειτουργίας.

Τώρα που έχετε ένα Προήλθε2 αντικείμενο, μπορείτε να το αναφέρετε με οποιαδήποτε μεταβλητή που συμμορφώνεται με τον τύπο Προήλθε2. Η ιεραρχία τύπου στο διάγραμμα UML του Σχήματος 1 αποκαλύπτει ότι Συμπληρωματικός, Βάση, και Πληκτρολογώ είναι όλοι σούπερ τύποι Προήλθε2. Έτσι, για παράδειγμα, ένα Βάση αναφορά μπορεί να επισυναφθεί στο αντικείμενο. Το σχήμα 3 απεικονίζει την εννοιολογική άποψη της ακόλουθης δήλωσης Java:

Βάση βάσης = παράγεται2; 

Δεν υπάρχει απολύτως καμία αλλαγή στο υποκείμενο Προήλθε2 αντικείμενο ή οποιαδήποτε από τις αντιστοιχίσεις λειτουργίας, μέσω μεθόδων m3 () και m4 () δεν είναι πλέον προσβάσιμα μέσω του Βάση αναφορά. Κλήση m1 () ή m2 (συμβολοσειρά) χρησιμοποιώντας οποιαδήποτε από τις μεταβλητές παράγεται2 ή βάση έχει ως αποτέλεσμα την εκτέλεση του ίδιου κώδικα εφαρμογής:

Συμβολοσειρά tmp; // Αναφορά παραγώγου2 (Σχήμα 2) tmp = παράγεται2.m1 (); // tmp είναι "Derived.m1 ()" tmp = deriv2.m2 ("Γεια"); // tmp είναι "Derived2.m2 (Hello)" // Βάση αναφοράς (Σχήμα 3) tmp = base.m1 (); // tmp είναι "Derived.m1 ()" tmp = base.m2 ("Γεια"); Το // tmp είναι "Derived2.m2 (Hello)" 

Η πραγματοποίηση πανομοιότυπης συμπεριφοράς και από τις δύο αναφορές έχει νόημα επειδή το Προήλθε2 το αντικείμενο δεν γνωρίζει τι καλεί κάθε μέθοδο. Το αντικείμενο γνωρίζει μόνο ότι όταν καλείται, ακολουθεί τις εντολές πορείας που ορίζονται από την ιεραρχία εφαρμογής. Αυτές οι παραγγελίες το ορίζουν για τη μέθοδο m1 (), ο Προήλθε2 αντικείμενο εκτελεί τον κώδικα στην τάξη Συμπληρωματικός, και για τη μέθοδο m2 (συμβολοσειρά), εκτελεί τον κώδικα στην τάξη Προήλθε2. Η ενέργεια που εκτελείται από το υποκείμενο αντικείμενο δεν εξαρτάται από τον τύπο της μεταβλητής αναφοράς.

Ωστόσο, όλα δεν είναι ίδια όταν χρησιμοποιείτε τις μεταβλητές αναφοράς παράγεται2 και βάση. Όπως απεικονίζεται στο Σχήμα 3, a Βάση η αναφορά τύπου μπορεί να δει μόνο το Βάση λειτουργίες τύπου του υποκείμενου αντικειμένου. Έτσι, αν και Προήλθε2 έχει αντιστοιχίσεις για μεθόδους m3 () και m4 (), μεταβλητή βάση δεν μπορώ να αποκτήσω πρόσβαση σε αυτές τις μεθόδους:

Συμβολοσειρά tmp; // Αναφορά παραγώγου2 (Σχήμα 2) tmp = παράγεται2.m3 (); // tmp είναι "Derived.m3 ()" tmp = deriv2.m4 (); // tmp είναι "Derived2.m4 ()" // Βάση αναφοράς (Σχήμα 3) tmp = base.m3 (); // Σφάλμα χρόνου μεταγλώττισης tmp = base.m4 (); // Σφάλμα χρόνου μεταγλώττισης 

Ο χρόνος εκτέλεσης

Προήλθε2

αντικείμενο παραμένει πλήρως ικανό να αποδεχτεί είτε το

m3 ()

ή

m4 ()

κλήσεις μεθόδου. Οι περιορισμοί τύπου που δεν επιτρέπουν αυτές τις απόπειρες κλήσεις μέσω του

Βάση