Polimorfizm Java i jego typy

Polimorfizm odnosi się do zdolności niektórych bytów do występowania w różnych formach. Powszechnie reprezentowany jest przez motyla, który przekształca się z larwy w poczwarkę w imago. Polimorfizm istnieje również w językach programowania, jako technika modelowania, która umożliwia tworzenie pojedynczego interfejsu dla różnych operandów, argumentów i obiektów. Polimorfizm języka Java powoduje, że kod jest bardziej zwięzły i łatwiejszy w utrzymaniu.

Chociaż ten samouczek koncentruje się na polimorfizmie podtypów, istnieje kilka innych typów, o których powinieneś wiedzieć. Zaczniemy od przeglądu wszystkich czterech typów polimorfizmu.

pobierz Pobierz kod Pobierz kod źródłowy na przykład aplikacje w tym samouczku. Stworzone przez Jeffa Friesena dla JavaWorld.

Rodzaje polimorfizmu w Javie

Istnieją cztery typy polimorfizmu w Javie:

  1. Wymuszenie to operacja, która obsługuje wiele typów poprzez konwersję typu niejawnego. Na przykład można podzielić liczbę całkowitą przez inną liczbę całkowitą lub wartość zmiennoprzecinkową przez inną wartość zmiennoprzecinkową. Jeśli jeden argument jest liczbą całkowitą, a drugi argument jest wartością zmiennoprzecinkową, kompilator wymusza (niejawnie konwertuje) liczba całkowita na wartość zmiennoprzecinkową aby zapobiec typu błędu. (Nie ma operacji dzielenia obsługującej operand całkowity i operand zmiennoprzecinkowy). Innym przykładem jest przekazanie odwołania do obiektu podklasy do parametru nadklasy metody. Kompilator przekształca typ podklasy na typ nadklasy, aby ograniczyć operacje do tych z nadklasy.
  2. Przeciążanie odnosi się do używania tego samego symbolu operatora lub nazwy metody w różnych kontekstach. Na przykład można użyć +do wykonywania dodawania liczb całkowitych, dodawania zmiennoprzecinkowego lub konkatenacji ciągów, w zależności od typów jego operandów. Ponadto w klasie może pojawić się wiele metod o tej samej nazwie (poprzez deklarację i / lub dziedziczenie).
  3. Polimorfizm parametryczny zakłada, że ​​w deklaracji klasy nazwa pola może być skojarzona z różnymi typami, a nazwa metody może być skojarzona z różnymi parametrami i typami zwracanymi. Pole i metoda mogą wtedy przybierać różne typy w każdej instancji klasy (obiekcie). Na przykład pole może być typu Double(element składowy standardowej biblioteki klas Javy, która opakowuje doublewartość), a metoda może zwracać a Doublew jednym obiekcie, a to samo pole może być typu Stringi ta sama metoda może zwracać a Stringw innym obiekcie . Java obsługuje parametryczny polimorfizm za pośrednictwem typów generycznych, które omówię w przyszłym artykule.
  4. Podtyp oznacza, że ​​typ może służyć jako podtyp innego typu. Gdy wystąpienie podtypu pojawia się w kontekście nadtypu, wykonanie operacji nadtypu na wystąpieniu podtypu skutkuje wykonaniem wersji podtypu tej operacji. Weźmy na przykład pod uwagę fragment kodu, który rysuje dowolne kształty. Możesz bardziej zwięźle wyrazić ten kod rysowania, wprowadzając Shapeklasę za pomocą draw()metody; wprowadzając Circle, Rectanglei inne podklasy, które zastępują draw(); poprzez wprowadzenie tablicy typu, Shapektórej elementy przechowują odniesienia do Shapeinstancji podklas; i wywołując Shape„s draw()metody na każdej instancji. Kiedy dzwonisz draw(), jest to Circle„s, Rectangle” lub inna Shapeinstancjadraw()metoda, która jest wywoływana. Możemy powiedzieć, że istnieje wiele form Shape„s draw()metody.

W tym samouczku przedstawiono polimorfizm podtypów. Dowiesz się o upcastingu i późnym wiązaniu, klasach abstrakcyjnych (których nie można utworzyć instancji) i metodach abstrakcyjnych (których nie można wywołać). Dowiesz się również o obniżaniu i identyfikacji typu w czasie wykonywania, a także poznasz kowariantne typy zwracane. Zachowam polimorfizm parametryczny na przyszły tutorial.

Polimorfizm ad-hoc a polimorfizm uniwersalny

Podobnie jak wielu programistów, wymuszanie i przeciążanie klasyfikuję jako polimorfizm ad hoc, a parametryczny i podtyp jako uniwersalny polimorfizm. Chociaż cenne techniki, nie wierzę, że przymus i przeciążenie są prawdziwym polimorfizmem; bardziej przypominają konwersje typów i cukier syntaktyczny.

Polimorfizm podtypu: upcasting i późne wiązanie

Polimorfizm podtypu polega na upcastingu i późnym wiązaniu. Upcasting to forma rzutowania polegająca na przerzucaniu hierarchii dziedziczenia z podtypu na nadtyp. Żaden operator rzutowania nie jest zaangażowany, ponieważ podtyp jest specjalizacją nadtypu. Na przykład Shape s = new Circle();upcasts od Circledo Shape. Ma to sens, ponieważ koło jest rodzajem kształtu.

Po przesłaniu Circledo góry Shapenie można wywoływać Circlemetod specyficznych dla- , takich jak getRadius()metoda zwracająca promień okręgu, ponieważ Circlemetody -specyficzne nie są częścią Shapeinterfejsu. Utrata dostępu do cech podtypu po zawężeniu podklasy do jej nadklasy wydaje się bezcelowa, ale jest konieczna do osiągnięcia polimorfizmu podtypu.

Załóżmy, że Shapedeklaracja draw()metody, jej Circlepodklasa przesłania tę metodę, Shape s = new Circle();właśnie została wykonana, a następna linia określa s.draw();. Która draw()metoda nazywa się: Shape's draw()metoda lub Circlejest draw()metoda? Kompilator nie wie, którą draw()metodę wywołać. Wszystko, co może zrobić, to zweryfikować, czy metoda istnieje w nadklasie i sprawdzić, czy lista argumentów wywołania metody i typ zwracany są zgodne z deklaracją metody nadklasy. Jednak kompilator wstawia również instrukcję do skompilowanego kodu, która w czasie wykonywania pobiera i używa dowolnego odwołania, saby wywołać poprawną draw()metodę. To zadanie jest znane jako późne wiązanie .

Późne wiązanie vs wczesne wiązanie

Późne wiązanie jest używane do wywołań finalmetod niebędących instancjami. W przypadku wszystkich innych wywołań metod kompilator wie, którą metodę wywołać. Wstawia instrukcję do skompilowanego kodu, która wywołuje metodę skojarzoną z typem zmiennej, a nie jej wartością. Ta technika jest znana jako wczesne wiązanie .

Stworzyłem aplikację, która demonstruje polimorfizm podtypów w zakresie upcastingu i późnego wiązania. Aplikacja ta składa się z Shape, Circle, Rectanglei Shapesklas, gdzie każda klasa jest przechowywany w oddzielnym pliku źródłowym. Listing 1 przedstawia pierwsze trzy klasy.

Listing 1. Deklarowanie hierarchii kształtów

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 + ")"); } }

Listing 2 przedstawia Shapesklasę aplikacji, której main()metoda steruje aplikacją.

Listing 2. Upcasting i późne wiązanie w polimorfizmie podtypu

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(); } }

Deklaracja shapestablicy demonstruje upcasting. CircleI Rectanglereferencje są przechowywane w shapes[0]i shapes[1]a są uskok wpisywać Shape. Każdy z shapes[0]i shapes[1]jest traktowany jako Shapewystąpienie: shapes[0]nie jest traktowany jako Circle; shapes[1]nie jest traktowany jako Rectangle.

Późne wiązanie jest pokazane przez shapes[i].draw();wyrażenie. Kiedy irówna 0, kompilator generowane przyczyn Instruction Circle„s draw()metoda nazywać. Kiedy irówna 1jednak ta instrukcja przyczyny Rectangle„s draw()być zwana metoda. Na tym polega istota polimorfizmu podtypu.

Zakładając, że wszystkie cztery pliki źródłowe ( Shapes.java, Shape.java, Rectangle.java, i Circle.java) znajdują się w bieżącym katalogu, skompilować je za pomocą jednej z poniższych linii poleceń:

javac *.java javac Shapes.java

Uruchom wynikową aplikację:

java Shapes

Należy zwrócić uwagę na następujące dane wyjściowe:

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

Klasy i metody abstrakcyjne

Projektując hierarchie klas, zauważysz, że klasy znajdujące się bliżej początku tych hierarchii są bardziej ogólne niż klasy znajdujące się niżej. Na przykład Vehiclenadklasa jest bardziej ogólna niż Truckpodklasa. Podobnie, Shapenadklasa jest bardziej ogólna niż podklasa Circlelub Rectangle.

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.

W tym przypadku kompilator nie będzie narzekał, ponieważ obniżenie z nadklasy do podklasy w tej samej hierarchii typów jest dozwolone. To powiedziawszy, jeśli przypisanie było dozwolone, aplikacja uległaby awarii podczas próby wykonania subclass.method();. W tym przypadku maszyna JVM będzie próbowała wywołać nieistniejącą metodę, ponieważ Superclassnie deklaruje method(). Na szczęście maszyna JVM sprawdza, czy rzutowanie jest legalne przed wykonaniem operacji rzutowania. Wykrywanie tego Superclassnie deklaruje method(), wyrzuci ClassCastExceptionobiekt. (Omówię wyjątki w przyszłym artykule).

Skompiluj Listing 5 w następujący sposób:

javac BadDowncast.java

Uruchom wynikową aplikację:

java BadDowncast