Polymorfisme in Java

1. Overzicht

Alle Object-Oriented Programming (OOP) -talen moeten vier basiskenmerken vertonen: abstractie, inkapseling, overerving en polymorfisme.

In dit artikel behandelen we twee kerntypen polymorfisme: statisch of compileertijd polymorfisme en dynamisch of runtimepolymorfisme. Statisch polymorfisme wordt afgedwongen tijdens het compileren, terwijl dynamisch polymorfisme tijdens runtime wordt gerealiseerd.

2. Statisch polymorfisme

Volgens Wikipedia is statisch polymorfisme een imitatie van polymorfisme dat wordt opgelost tijdens het compileren en dus een einde maakt aan run-time virtuele-tabelzoekopdrachten.

Bijvoorbeeld onze Tekstbestand class in een bestandsbeheer-app kan drie methoden hebben met dezelfde handtekening als de lezen() methode:

public class TextFile breidt GenericFile uit {// ... public String read () {return this.getContent () .toString (); } public String read (int limit) {return this.getContent () .toString () .substring (0, limit); } public String lezen (int start, int stop) {return this.getContent () .toString () .substring (start, stop); }}

Tijdens het compileren van de code controleert de compiler of alle aanroepen van het lezen methode komen overeen met ten minste een van de drie hierboven gedefinieerde methoden.

3. Dynamisch polymorfisme

Met dynamisch polymorfisme, de Java Virtual Machine (JVM) zorgt voor de detectie van de juiste methode die moet worden uitgevoerd wanneer een subklasse wordt toegewezen aan de bovenliggende vorm. Dit is nodig omdat de subklasse sommige of alle methoden die in de bovenliggende klasse zijn gedefinieerd, kan overschrijven.

Laten we in een hypothetische bestandsbeheer-app de bovenliggende klasse definiëren voor alle aangeroepen bestanden GenericFile:

openbare klasse GenericFile {private String-naam; // ... public String getFileInfo () {return "Generic File Impl"; }}

We kunnen ook een Beeldbestand klasse die de extensie GenericFile maar overschrijft de getFileInfo () methode en voegt meer informatie toe:

public class ImageFile breidt GenericFile {private int hoogte uit; private int breedte; // ... getters en setters public String getFileInfo () {return "Image File Impl"; }}

Wanneer we een instantie van Beeldbestand en wijs het toe aan een GenericFile class, is een impliciete cast gedaan. De JVM houdt echter een verwijzing bij naar de feitelijke vorm van Beeldbestand.

Het bovenstaande construct is analoog aan het overschrijven van een methode. We kunnen dit bevestigen door een beroep te doen op de getFileInfo () methode door:

public static void main (String [] args) {GenericFile genericFile = new ImageFile ("SampleImageFile", 200, 100, nieuwe BufferedImage (100, 200, BufferedImage.TYPE_INT_RGB) .toString () .getBytes (), "v1.0.0" ); logger.info ("Bestandsinfo: \ n" + genericFile.getFileInfo ()); }

Zoals verwacht, genericFile.getFileInfo () triggert het getFileInfo () methode van de Beeldbestand klasse zoals te zien in de onderstaande uitvoer:

Bestandsinfo: afbeeldingbestand impl

4. Andere polymorfe kenmerken in Java

Naast deze twee hoofdtypen polymorfisme in Java, zijn er andere kenmerken in de programmeertaal Java die polymorfisme vertonen. Laten we enkele van deze kenmerken bespreken.

4.1. Dwang

Polymorfe dwang houdt zich bezig met de impliciete typeconversie die door de compiler wordt uitgevoerd om typefouten te voorkomen. Een typisch voorbeeld is te zien in een aaneenschakeling van gehele getallen en tekenreeksen:

String str = "string" + 2;

4.2. Overbelasting door operator

Overbelasting van operator of methode verwijst naar een polymorf kenmerk van hetzelfde symbool of dezelfde operator met verschillende betekenissen (vormen), afhankelijk van de context.

Het plusteken (+) kan bijvoorbeeld zowel voor wiskundige optelling als voor Draad aaneenschakeling. In beide gevallen bepaalt alleen de context (d.w.z. argumenttypen) de interpretatie van het symbool:

String str = "2" + 2; int som = 2 + 2; System.out.printf ("str =% s \ n som =% d \ n", str, som);

Uitgang:

str = 22 som = 4

4.3. Polymorfe parameters

Met parametrisch polymorfisme kan een naam van een parameter of methode in een klasse worden geassocieerd met verschillende typen. We hebben hieronder een typisch voorbeeld waar we het definiëren inhoud als een Draad en later als een Geheel getal:

openbare klasse TextFile breidt GenericFile {privé String-inhoud uit; openbare String setContentDelimiter () {int content = 100; this.content = this.content + inhoud; }}

Het is ook belangrijk om dat op te merken declaratie van polymorfe parameters kan leiden tot een probleem dat bekend staat alsvariabele verbergen waarbij een lokale declaratie van een parameter altijd de globale declaratie van een andere parameter met dezelfde naam overschrijft.

Om dit probleem op te lossen, is het vaak raadzaam om globale verwijzingen te gebruiken, zoals dit trefwoord om te verwijzen naar globale variabelen binnen een lokale context.

4.4. Polymorfe subtypes

Polymorf subtype maakt het voor ons gemakkelijk mogelijk om meerdere subtypen aan een type toe te wijzen en te verwachten dat alle aanroepen op het type de beschikbare definities in het subtype activeren.

Als we bijvoorbeeld een verzameling hebben van GenericFiles en we roepen de informatie verkrijgen() methode op elk van hen, kunnen we verwachten dat de uitvoer anders is, afhankelijk van het subtype waarvan elk item in de verzameling is afgeleid:

GenericFile [] bestanden = {nieuw ImageFile ("SampleImageFile", 200, 100, nieuwe BufferedImage (100, 200, BufferedImage.TYPE_INT_RGB) .toString () .getBytes (), "v1.0.0"), nieuw TextFile ("SampleTextFile" , "Dit is een voorbeeldtekstinhoud", "v1.0.0")}; voor (int i = 0; i <files.length; i ++) {bestanden [i] .getInfo (); }

Subtype polymorfisme wordt mogelijk gemaakt door een combinatie vanupcasting en late binding. Upcasting omvat het casten van de overervingshiërarchie van een supertype naar een subtype:

ImageFile imageFile = nieuw ImageFile (); GenericFile file = imageFile;

Het resulterende effect van het bovenstaande is dat Beeldbestand-specifieke methoden kunnen niet worden aangeroepen op de nieuwe upcast GenericFile. Methoden in het subtype overschrijven echter vergelijkbare methoden die in het supertype zijn gedefinieerd.

Om het probleem op te lossen van het niet kunnen aanroepen van subtype-specifieke methoden bij upcasting naar een supertype, kunnen we de overerving van een supertype naar een subtype downcasting. Dit wordt gedaan door:

ImageFile imageFile = (ImageFile) bestand;

Late bindingstrategie helpt de compiler om te bepalen wiens methode moet worden geactiveerd na upcasting. In het geval van imageFile # getInfo vs bestand # getInfo in het bovenstaande voorbeeld bewaart de compiler een verwijzing naar Beeldbestand‘S informatie verkrijgen methode.

5. Problemen met polymorfisme

Laten we eens kijken naar enkele onduidelijkheden in polymorfisme die mogelijk kunnen leiden tot runtime-fouten als ze niet goed worden gecontroleerd.

5.1. Typ identificatie tijdens downcasting

Bedenk dat we eerder de toegang tot sommige subtype-specifieke methoden hebben verloren na het uitvoeren van een upcast. Hoewel we dit met een downcast hebben kunnen oplossen, is dit geen garantie voor daadwerkelijke typecontrole.

Als we bijvoorbeeld een upcast en daaropvolgende downcast uitvoeren:

GenericFile-bestand = nieuwe GenericFile (); ImageFile imageFile = (ImageFile) bestand; System.out.println (imageFile.getHeight ());

We merken dat de compiler een downcast van een GenericFile in een Beeldbestand, ook al is de klas eigenlijk een GenericFile en niet een Beeldbestand.

Als we daarom proberen het getHeight () methode op de beeldbestand klasse, we krijgen een ClassCastException net zo GenericFile definieert niet getHeight () methode:

Uitzondering in thread "main" java.lang.ClassCastException: GenericFile kan niet naar ImageFile worden gecast

Om dit probleem op te lossen, voert de JVM een Run-Time Type Information (RTTI) -controle uit. We kunnen ook proberen een expliciete type-identificatie te gebruiken door de instantie van trefwoord net als dit:

ImageFile imageFile; if (bestand exemplaar van ImageFile) {imageFile = bestand; }

Het bovenstaande helpt om een ClassCastException uitzondering tijdens runtime. Een andere optie die kan worden gebruikt, is het gipsverband in een proberen en vangst blok en het vangen van de ClassCastException.

het zou genoteerd moeten worden dat RTTI-controle is duur vanwege de tijd en middelen die nodig zijn om effectief te verifiëren dat een type correct is. Bovendien veelvuldig gebruik van de instantie van trefwoord impliceert bijna altijd een slecht ontwerp.

5.2. Kwetsbare basisklasse probleem

Volgens Wikipedia worden basis- of superklassen als kwetsbaar beschouwd als schijnbaar veilige modificaties aan een basisklasse ervoor kunnen zorgen dat afgeleide klassen niet goed werken.

Laten we eens kijken naar een verklaring van een superklasse met de naam GenericFile en zijn subklasse Tekstbestand:

openbare klasse GenericFile {privé String-inhoud; void writeContent (String content) {this.content = content; } void toString (String str) {str.toString (); }}
public class TextFile breidt GenericFile uit {@Override void writeContent (String content) {toString (content); }}

Wanneer we het GenericFile klasse:

openbare klasse GenericFile {// ... void toString (String str) {writeContent (str); }}

We merken dat de bovenstaande wijziging weggaat Tekstbestand in een oneindige recursie in de writeContent () methode, wat uiteindelijk resulteert in een stack-overflow.

Om een ​​kwetsbaar basisklasseprobleem aan te pakken, kunnen we de laatste trefwoord om te voorkomen dat subklassen de writeContent () methode. Een goede documentatie kan ook helpen. En last but not least, de compositie verdient over het algemeen de voorkeur boven overerving.

6. Conclusie

In dit artikel hebben we het fundamentele concept van polymorfisme besproken, waarbij we ons zowel op voor- als nadelen concentreren.

Zoals altijd is de broncode voor dit artikel beschikbaar op GitHub.