Java polimorfizam i njegove vrste

Polimorfizam se odnosi na sposobnost nekih entiteta da se javljaju u različitim oblicima. Popularno ga predstavlja leptir, koji prelazi od ličinke do kukuljice do imaga. Polimorfizam postoji i u programskim jezicima, kao tehnika modeliranja koja vam omogućuje stvaranje jedinstvenog sučelja za razne operande, argumente i objekte. Rezultat Java polimorfizma je sažetiji i lakši za održavanje kod.

Iako se ovaj vodič usredotočuje na polimorfizam podtipova, postoji nekoliko drugih tipova koje biste trebali znati. Započet ćemo s pregledom sve četiri vrste polimorfizma.

preuzimanje Preuzmite kod Preuzmite izvorni kod za primjere aplikacija u ovom vodiču. Stvorio Jeff Friesen za JavaWorld.

Vrste polimorfizma u Javi

U Javi postoje četiri vrste polimorfizma:

  1. Prisila je operacija koja služi više vrsta putem implicitne pretvorbe tipa. Na primjer, cijeli broj dijelite s drugim cijelim brojem ili vrijednost s pomičnom zarezom s drugom vrijednošću s pomičnom zarezom. Ako je jedan operand cijeli broj, a drugi operand vrijednost s pomičnom zarezom, sastavljač prisiljava (implicitno pretvara) cijeli broj u vrijednost s pomičnom zarezom kako bi spriječio pogrešku u tipu. (Ne postoji operacija podjele koja podržava cjelobrojni operand i operand s pomičnom zarezom.) Drugi primjer je prosljeđivanje reference objekta podklase na parametar superklase metode. Prevoditelj prisiljava tip podklase na tip superklase kako bi ograničio operacije na one superklase.
  2. Preopterećenje se odnosi na upotrebu istog simbola operatora ili naziva metode u različitim kontekstima. Na primjer, mogli biste koristiti +za izvođenje cjelobrojnog zbrajanja, zbrajanja s pomičnom zarezom ili spajanja nizova, ovisno o vrstama njegovih operanda. Također, u klasi se može pojaviti više metoda s istim imenom (putem deklaracije i / ili nasljeđivanja).
  3. Parametarski polimorfizam propisuje da se unutar deklaracije klase naziv polja može povezati s različitim tipovima, a naziv metode s različitim tipovima parametara i povratka. Polje i metoda tada mogu poprimiti različite tipove u svakoj instanci klase (objekta). Na primjer, polje može biti tipa Double(član Java-ove standardne knjižnice klasa koja omotava doublevrijednost), a metoda može vratiti jedan Doubleu jedan objekt, a isto polje može biti tipa Stringi ista metoda može vratiti a Stringu drugom objektu . Java podržava parametarski polimorfizam putem generičkih podataka, o čemu ću raspravljati u budućem članku.
  4. Podtip znači da tip može poslužiti kao podtip drugog tipa. Kada se instanca podtipa pojavi u kontekstu supertipa, izvršavanje operacije supertipa na instanci podtipa rezultira izvršavanjem verzije podtipa te operacije. Na primjer, razmotrite fragment koda koji crta proizvoljne oblike. Ovaj crtežni kôd možete izraziti sažetije uvođenjem Shapeklase s draw()metodom; uvođenjem Circle, Rectanglei drugih potklasa koje imaju prednost draw(); uvođenjem niza tipa Shapečiji elementi pohranjuju reference na Shapeinstance podklase; i pozivom Shape„s draw()metodu na svakom stupnju. Kada nazovete draw(), to su one Circle, one Rectangleili druge Shapeinstancedraw()metoda koja se poziva. Kažemo da postoje mnogi oblici Shape„s draw()metodom.

Ovaj vodič uvodi polimorfizam podtipa. Naučit ćete o ažuriranju i kasnom vezivanju, apstraktnim klasama (koje se ne mogu instancirati) i apstraktnim metodama (koje se ne mogu nazvati). Također ćete naučiti o downcastingu i identifikaciji tipa izvođenja i dobit ćete prvi pogled na kovarijantne tipove povrata. Spremit ću parametarski polimorfizam za budući vodič.

Ad-hoc vs univerzalni polimorfizam

Kao i mnogi programeri, prisilu i preopterećenje klasificiram kao ad-hoc polimorfizam, a parametrijske i podtipove kao univerzalni polimorfizam. Iako su vrijedne tehnike, ne vjerujem da su prisila i preopterećenje istinski polimorfizam; više su poput pretvorbe tipa i sintaktičkog šećera.

Podtip polimorfizma: nadogradnja i kasno vezanje

Podtip polimorfizma oslanja se na nadogradnju i kasno vezanje. Ažuriranje je oblik emitiranja gdje hijerarhiju nasljeđivanja prebacujete iz podtipa u nadtip. Nijedan operater emitiranja nije uključen, jer je podtip specijalizacija za supertip. Na primjer, Shape s = new Circle();ažuriranja od Circledo Shape. To ima smisla jer je krug svojevrsni oblik.

Nakon nadogradnje Circlena Shape, ne možete pozivati Circle-specifične metode, poput getRadius()metode koja vraća radijus kruga, jer Circle-specifične metode nisu dio Shapesučelja programa. Izgubiti pristup značajkama podtipa nakon sužavanja podrazreda na njegov superrazred čini se besmislenim, ali je neophodno za postizanje polimorfizma podtipa.

Pretpostavimo da Shapedeklarira draw()metodu, njen Circlepodrazred nadjačava ovu metodu, Shape s = new Circle();upravo je izvršen i sljedeći redak navodi s.draw();. Koja draw()metoda se zove: Shape's draw()metodom ili Circleje draw()metoda? Prevoditelj ne zna koju draw()metodu pozvati. Sve što može učiniti je provjeriti postoji li metoda u superklasi i provjeriti podudaraju li se argumenti poziva i tip povratka metode metodom deklaracije metode superklase. Međutim, prevodilac također uvodi naredbu u prevedeni kôd koja tijekom izvođenja dohvaća i koristi bilo koju referencu sda pozove ispravnu draw()metodu. Ovaj je zadatak poznat kao kasno vezivanje .

Kasno vezanje vs rano vezanje

Kasno vezanje koristi se za pozive neinstacijskim finalmetodama. Za sve ostale pozive metode, sastavljač zna koju metodu pozvati. U kompajlirani kôd ubavlja uputu koja poziva metodu povezanu s vrstom varijable, a ne s njezinom vrijednošću. Ova je tehnika poznata kao rano vezivanje .

Napravio sam aplikaciju koja pokazuje polimorfizam podtipa u smislu nadogradnje i kasnog vezanja. Ovaj program se sastoji od Shape, Circle, Rectanglei Shapesnastave, gdje je svaki razred pohranjeni u vlastitom izvorne datoteke. Popis 1 predstavlja prva tri razreda.

Popis 1. Deklariranje hijerarhije oblika

class Shape { void draw() { } } class Circle extends Shape { private int x, y, r; Circle(int x, int y, int r) { this.x = x; this.y = y; this.r = r; } // For brevity, I've omitted getX(), getY(), and getRadius() methods. @Override void draw() { System.out.println("Drawing circle (" + x + ", "+ y + ", " + r + ")"); } } class Rectangle extends Shape { private int x, y, w, h; Rectangle(int x, int y, int w, int h) { this.x = x; this.y = y; this.w = w; this.h = h; } // For brevity, I've omitted getX(), getY(), getWidth(), and getHeight() // methods. @Override void draw() { System.out.println("Drawing rectangle (" + x + ", "+ y + ", " + w + "," + h + ")"); } }

Popis 2 predstavlja Shapesklasu aplikacije čija main()metoda pokreće aplikaciju.

Popis 2. Ažuriranje i kasno vezanje u polimorfizmu podtipa

class Shapes { public static void main(String[] args) { Shape[] shapes = { new Circle(10, 20, 30), new Rectangle(20, 30, 40, 50) }; for (int i = 0; i < shapes.length; i++) shapes[i].draw(); } }

Deklaracija shapesniza pokazuje nadogradnju. CircleI Rectanglereference su pohranjeni u shapes[0]te shapes[1]i izlazni upisati Shape. Svaki od shapes[0]i shapes[1]smatra se Shapeprimjerom: shapes[0]ne smatra se Circle; shapes[1]ne smatra se a Rectangle.

Kasno vezanje dokazuje se shapes[i].draw();izrazom. Kada je ijednako 0, naredba koju generira kompajler uzrokuje Circlepozivanje draw()metode ' . Međutim, kada je ijednako 1, ova uputa uzrokuje Rectanglepozivanje draw()metode ' . To je bit polimorfizma podtipa.

Pod pretpostavkom da su sva četiri izvorne datoteke ( Shapes.java, Shape.java, Rectangle.javai Circle.java) nalaze se u tekućem direktoriju, sastaviti ih putem bilo kojeg od sljedećih linija zapovijedanja:

javac *.java javac Shapes.java

Pokrenite rezultirajuću aplikaciju:

java Shapes

Trebali biste promatrati sljedeći rezultat:

Drawing circle (10, 20, 30) Drawing rectangle (20, 30, 40, 50)

Apstraktne klase i metode

Kada dizajnirate hijerarhiju klasa, otkrit ćete da su klase bliže vrhu ovih hijerarhija generičnije od klasa niže dolje. Na primjer, Vehiclesuperrazred je generičkiji od Truckpodrazreda. Slično tome, Shapesuperrazred je generičkiji od a Circleili Rectanglepodrazreda.

It doesn't make sense to instantiate a generic class. After all, what would a Vehicle object describe? Similarly, what kind of shape is represented by a Shape object? Rather than code an empty draw() method in Shape, we can prevent this method from being called and this class from being instantiated by declaring both entities to be abstract.

Java provides the abstract reserved word to declare a class that cannot be instantiated. The compiler reports an error when you try to instantiate this class. abstract is also used to declare a method without a body. The draw() method doesn't need a body because it is unable to draw an abstract shape. Listing 3 demonstrates.

Listing 3. Abstracting the Shape class and its draw() method

abstract class Shape { abstract void draw(); // semicolon is required }

Abstract cautions

The compiler reports an error when you attempt to declare a class abstract and final. For example, the compiler complains about abstract final class Shape because an abstract class cannot be instantiated and a final class cannot be extended. The compiler also reports an error when you declare a method abstract but don't declare its class abstract. Removing abstract from the Shape class's header in Listing 3 would result in an error, for instance. This would be an error because a non-abstract (concrete) class cannot be instantiated when it contains an abstract method. Finally, when you extend an abstract class, the extending class must override all of the abstract methods, or else the extending class must itself be declared to be abstract; otherwise, the compiler will report an error.

An abstract class can declare fields, constructors, and non-abstract methods in addition to or instead of abstract methods. For example, an abstract Vehicle class might declare fields describing its make, model, and year. Also, it might declare a constructor to initialize these fields and concrete methods to return their values. Check out Listing 4.

Listing 4. Abstracting a vehicle

abstract class Vehicle { private String make, model; private int year; Vehicle(String make, String model, int year) { this.make = make; this.model = model; this.year = year; } String getMake() { return make; } String getModel() { return model; } int getYear() { return year; } abstract void move(); }

You'll note that Vehicle declares an abstract move() method to describe the movement of a vehicle. For example, a car rolls down the road, a boat sails across the water, and a plane flies through the air. Vehicle's subclasses would override move() and provide an appropriate description. They would also inherit the methods and their constructors would call Vehicle's constructor.

Downcasting and RTTI

Moving up the class hierarchy, via upcasting, entails losing access to subtype features. For example, assigning a Circle object to Shape variable s means that you cannot use s to call Circle's getRadius() method. However, it's possible to once again access Circle's getRadius() method by performing an explicit cast operation like this one: Circle c = (Circle) s;.

This assignment is known as downcasting because you are casting down the inheritance hierarchy from a supertype to a subtype (from the Shape superclass to the Circle subclass). Although an upcast is always safe (the superclass's interface is a subset of the subclass's interface), a downcast isn't always safe. Listing 5 shows what kind of trouble could ensue if you use downcasting incorrectly.

Listing 5. The problem with downcasting

class Superclass { } class Subclass extends Superclass { void method() { } } public class BadDowncast { public static void main(String[] args) { Superclass superclass = new Superclass(); Subclass subclass = (Subclass) superclass; subclass.method(); } }

Listing 5 presents a class hierarchy consisting of Superclass and Subclass, which extends Superclass. Furthermore, Subclass declares method(). A third class named BadDowncast provides a main() method that instantiates Superclass. BadDowncast then tries to downcast this object to Subclass and assign the result to variable subclass.

U ovom slučaju kompajler se neće žaliti jer je spuštanje superklase na podklasu u hijerarhiji istog tipa legalno. To je reklo, ako se dodijeli zadatak, aplikacija bi se srušila kad bi pokušala izvršiti subclass.method();. U ovom slučaju JVM bi pokušao nazvati nepostojeću metodu, jer Superclassse ne deklarira method(). Srećom, JVM provjerava je li lijevanje legalno prije izvođenja operacije lijevanja. Otkrivajući ono što Superclassse ne deklarira method(), bacilo bi ClassCastExceptionobjekt. (O iznimkama ću raspravljati u budućem članku.)

Sastavite popis 5 kako slijedi:

javac BadDowncast.java

Pokrenite rezultirajuću aplikaciju:

java BadDowncast