Otkrijte čaroliju polimorfizma podtipa

Riječ polimorfizam potječe od grčkog "mnogi oblici". Većina programera Jave taj izraz povezuje sa sposobnošću objekta da magično izvrši ispravno ponašanje metode na odgovarajućim točkama u programu. Međutim, taj pogled usmjeren na implementaciju dovodi do slika čarobnjaštva, a ne do razumijevanja temeljnih pojmova.

Polimorfizam u Javi uvijek je podtip polimorfizma. Pažljivo ispitivanje mehanizama koji generiraju tu raznolikost polimorfnog ponašanja zahtijeva da odbacimo svoje uobičajene brige oko implementacije i razmišljamo u smislu tipa. Ovaj članak istražuje tipski orijentiranu perspektivu predmeta i kako ta perspektiva odvaja koje ponašanje objekt može izraziti od toga kako objekt zapravo izražava to ponašanje. Oslobađajući naš koncept polimorfizma iz hijerarhije implementacije, otkrivamo i kako Java sučelja olakšavaju polimorfno ponašanje u skupinama objekata koji uopće ne dijele implementacijski kod.

Quattro polimorfi

Polimorfizam je širok objektno orijentirani pojam. Iako obično izjednačavamo opći koncept s raznolikošću podtipa, zapravo postoje četiri različite vrste polimorfizma. Prije nego što detaljno ispitamo polimorfizam podtipa, sljedeći odjeljak predstavlja opći pregled polimorfizma u objektno orijentiranim jezicima.

Luca Cardelli i Peter Wegner, autori knjige "O razumijevanju tipova, apstrakcije podataka i polimorfizma" (pogledajte resurse za poveznicu do članka) dijele polimorfizam u dvije glavne kategorije - ad hoc i univerzalnu - i četiri vrste: prisila, preopterećenje, parametarski i uključivanje. Klasifikacijska struktura je:

| - prisila | - ad hoc - | | - preopterećujući polimorfizam - | | - parametarski | - univerzalni - | | - uključivanje

U toj općoj shemi polimorfizam predstavlja sposobnost entiteta da ima više oblika. Univerzalni polimorfizam odnosi se na ujednačenost tipske strukture, u kojoj polimorfizam djeluje na beskonačan broj tipova koji imaju zajedničko obilježje. Manje strukturirani ad hoc polimorfizam djeluje na konačan broj moguće nepovezanih tipova. Četiri sorte mogu se opisati kao:

  • Prisila: pojedina apstrakcija služi nekoliko tipova kroz implicitnu pretvorbu tipova
  • Preopterećenje: jedan identifikator označava nekoliko apstrakcija
  • Parametarski: apstrakcija djeluje jednoliko na različite vrste
  • Inkluzija: apstrakcija djeluje kroz inkluzijski odnos

Ukratko ću raspraviti svaku sortu prije nego što se obratim posebnom polimorfizmu podtipa.

Prisila

Prisila predstavlja implicitnu pretvorbu tipa parametra u tip koji očekuje metoda ili operator, čime se izbjegavaju pogreške tipa. Za sljedeće izraze, kompajler mora odrediti postoji li odgovarajući binarni +operator za vrste operanda:

 2,0 + 2,0 2,0 + 2 2,0 + "2" 

Prvi izraz dodaje dva doubleoperanda; jezik Java posebno definira takav operator.

Međutim, drugi izraz dodaje a doublei an int; Java ne definira operatora koji prihvaća te vrste operanda. Srećom, prevodilac implicitno pretvara drugi operand u doublei koristi operator definiran za dva doubleoperanda. To je izuzetno povoljno za programera; bez implicitne pretvorbe rezultirala bi pogreška vremena kompajliranja ili bi programer morao izričito prebaciti intna double.

Treći izraz dodaje a doublei a String. Još jednom, jezik Java ne definira takvog operatora. Dakle, kompajler prisiljava doubleoperand na a String, a operator plus izvodi spajanje nizova.

Prisila se također javlja pri pozivu metode. Pretpostavimo da klasa Derivedproširuje klasu Base, a klasa Cima metodu s potpisom m(Base). Za pozivanje metode u donjem kodu, sastavljač implicitno pretvara derivedreferentnu varijablu koja ima tip Derivedu Basetip propisan potpisom metode. Ta implicitna konverzija omogućuje da m(Base)implementacijski kod metode koristi samo tipske operacije definirane Base:

C c = novi C (); Izvedeno izvedeno = novo Izvedeno (); cm (izvedeno);

Opet, implicitna prisila tijekom pozivanja metode izbjegava glomazan ulog tipa ili nepotrebnu pogrešku tijekom kompajliranja. Naravno, prevoditelj još uvijek provjerava jesu li sve pretvorbe tipova u skladu s definiranom hijerarhijom tipova.

Preopterećenje

Preopterećenje dopušta upotrebu istog operatora ili naziva metode za označavanje višestrukih, različitih programskih značenja. +Operater koristi u prethodnom odjeljku pokazali su u dva oblika: jedan za dodavanje doubleoperanada, jedan za nadovezivanjem Stringobjekata. Postoje i drugi oblici za dodavanje dviju cijelih brojeva, dvije duljine i tako dalje. Operatora nazivamo preopterećenim i oslanjamo se na prevoditelja da odabere odgovarajuću funkcionalnost na temelju programskog konteksta. Kao što je prethodno napomenuto, ako je potrebno, prevodilac implicitno pretvara vrste operanda kako bi se podudarali s točnim potpisom operatora. Iako Java određuje određene preopterećene operatore, ne podržava korisnički definirano preopterećenje operatora.

Java dopušta korisnički definirano preopterećenje imena metoda. Klasa može posjedovati više metoda s istim imenom, pod uvjetom da su potpisi metode različiti. To znači da se broj parametara mora razlikovati ili da barem jedan položaj parametara mora imati drugačiji tip. Jedinstveni potpisi omogućavaju prevoditelju da razlikuje metode koje imaju isto ime. Prevoditelj upravlja nazivima metoda koristeći jedinstvene potpise, učinkovito stvarajući jedinstvena imena. U svjetlu toga, svako prividno polimorfno ponašanje isparava nakon detaljnijeg pregleda.

I prisila i preopterećenje klasificirani su kao ad hoc jer svaki pruža polimorfno ponašanje samo u ograničenom smislu. Iako potpadaju pod široku definiciju polimorfizma, ove sorte su prvenstveno pogodnosti za razvoj. Prisiljavanjem se izbjegavaju glomazni izričiti ulozi ili nepotrebne pogreške tipa kompajlera. Preopterećenje, s druge strane, daje sintaktički šećer, omogućavajući programeru da koristi isti naziv za različite metode.

Parametarski

Parametarski polimorfizam omogućuje upotrebu jedne apstrakcije u mnogim tipovima. Na primjer, Listapstrakcija, koja predstavlja popis homogenih objekata, može se pružiti kao generički modul. Abstrakciju biste ponovno upotrijebili tako što biste odredili vrste objekata sadržanih na popisu. Budući da parametrizirani tip može biti bilo koji korisnički definirani tip podataka, postoji potencijalno beskonačan broj upotreba za generičku apstrakciju, što čini ovaj nedvojbeno najmoćniji tip polimorfizma.

Na prvi pogled Listmože se činiti da je gornja apstrakcija korisnost klase java.util.List. Međutim, Java ne podržava pravi parametarsku polimorfizam u tipu siguran način, zbog čega je java.util.Listi java.util„s druge klase zbirka su napisane u smislu iskonske Java klase, java.lang.Object. (Pogledajte moj članak "Primordijalno sučelje?" Za više pojedinosti.) Javino jednokorijenjeno nasljeđivanje implementacije nudi djelomično rješenje, ali ne i stvarnu snagu parametarskog polimorfizma. Izvrsni članak Erica Allena, "Gledaj moć parametarskog polimorfizma", opisuje potrebu za generičkim tipovima u Javi i prijedloge za rješavanje Sunčevog zahtjeva za specifikaciju Java # 000014, "Dodavanje generičkih tipova u programski jezik Java." (Poveznicu pogledajte u Resursima.)

Uključenje, Ubrajanje

Inkluzijski polimorfizam postiže polimorfno ponašanje inkluzijskim odnosom između vrsta ili skupova vrijednosti. Za mnoge objektno orijentirane jezike, uključujući Javu, relacija uključivanja je relacija podtipa. Dakle, u Javi je inkluzijski polimorfizam podtip polimorfizma.

Kao što je ranije napomenuto, kada se programeri Java generički odnose na polimorfizam, oni uvijek znače polimorfizam podtipa. Da bismo stekli solidnu procjenu moći polimorfizma podtipa, potrebno je promatrati mehanizme koji daju polimorfno ponašanje iz perspektive orijentirane na tip. Ostatak ovog članka pomno istražuje tu perspektivu. Radi kratkoće i jasnoće, termin polimorfizam označavam polimorfizam podtipa.

Tipski orijentirani pogled

The UML class diagram in Figure 1 shows the simple type and class hierarchy used to illustrate the mechanics of polymorphism. The model depicts five types, four classes, and one interface. Although the model is called a class diagram, I think of it as a type diagram. As detailed in "Thanks Type and Gentle Class," every Java class and interface declares a user-defined data type. So from an implementation-independent view (i.e., a type-oriented view) each of the five rectangles in the figure represents a type. From an implementation point of view, four of those types are defined using class constructs, and one is defined using an interface.

The following code defines and implements each user-defined data type. I purposely keep the implementation as simple as possible:

/* Base.java */ public class Base { public String m1() { return "Base.m1()"; } public String m2( String s ) { return "Base.m2( " + s + " )"; } } /* IType.java */ interface IType { String m2( String s ); String m3(); } /* Derived.java */ public class Derived extends Base implements IType { public String m1() { return "Derived.m1()"; } public String m3() { return "Derived.m3()"; } } /* Derived2.java */ public class Derived2 extends Derived { public String m2( String s ) { return "Derived2.m2( " + s + " )"; } public String m4() { return "Derived2.m4()"; } } /* Separate.java */ public class Separate implements IType { public String m1() { return "Separate.m1()"; } public String m2( String s ) { return "Separate.m2( " + s + " )"; } public String m3() { return "Separate.m3()"; } } 

Using these type declarations and class definitions, Figure 2 depicts a conceptual view of the Java statement:

Derived2 derived2 = new Derived2(); 

The above statement declares an explicitly typed reference variable, derived2, and attaches that reference to a newly created Derived2 class object. The top panel in Figure 2 depicts the Derived2 reference as a set of portholes, through which the underlying Derived2 object can be viewed. There is one hole for each Derived2 type operation. The actual Derived2 object maps each Derived2 operation to appropriate implementation code, as prescribed by the implementation hierarchy defined in the above code. For example, the Derived2 object maps m1() to implementation code defined in class Derived. Furthermore, that implementation code overrides the m1() method in class Base. A Derived2 reference variable cannot access the overridden m1() implementation in class Base. That does not mean that the actual implementation code in class Derived can't use the Base class implementation via super.m1(). But as far as the reference variable derived2 is concerned, that code is inaccessible. The mappings of the other Derived2 operations similarly show the implementation code executed for each type operation.

Now that you have a Derived2 object, you can reference it with any variable that conforms to type Derived2. The type hierarchy in Figure 1's UML diagram reveals that Derived, Base, and IType are all super types of Derived2. So, for example, a Base reference can be attached to the object. Figure 3 depicts the conceptual view of the following Java statement:

Base base = derived2; 

There is absolutely no change to the underlying Derived2 object or any of the operation mappings, though methods m3() and m4() are no longer accessible through the Base reference. Calling m1() or m2(String) using either variable derived2 or base results in execution of the same implementation code:

String tmp; // Derived2 reference (Figure 2) tmp = derived2.m1(); // tmp is "Derived.m1()" tmp = derived2.m2( "Hello" ); // tmp is "Derived2.m2( Hello )" // Base reference (Figure 3) tmp = base.m1(); // tmp is "Derived.m1()" tmp = base.m2( "Hello" ); // tmp is "Derived2.m2( Hello )" 

Realizing identical behavior through both references makes sense because the Derived2 object does not know what calls each method. The object only knows that when called upon, it follows the marching orders defined by the implementation hierarchy. Those orders stipulate that for method m1(), the Derived2 object executes the code in class Derived, and for method m2(String), it executes the code in class Derived2. The action performed by the underlying object does not depend on the reference variable's type.

Međutim, nije sve jednako kada koristite referentne varijable derived2i base. Kao što je prikazano na slici 3, Basereferenca tipa može vidjeti samo Basetipske operacije osnovnog objekta. Dakle, iako Derived2ima mapiranja za metode m3()i m4(), varijabla basene može pristupiti tim metodama:

String tmp; // Izvedena referenca2 (slika 2) tmp = izvedena2.m3 (); // tmp je "Izvedeno.m3 ()" tmp = izvedeno2.m4 (); // tmp je "Izvedeno2.m4 ()" // Osnovna referenca (slika 3) tmp = base.m3 (); // Pogreška vremena prevođenja tmp = base.m4 (); // Pogreška vremena prevođenja

Vrijeme izvođenja

Derived2

objekt ostaje u potpunosti sposoban prihvatiti bilo

m3()

ili

m4()

pozivi metode. Ograničenja tipa koja zabranjuju one pokušaje poziva putem

Base