ความแตกต่างของ Java และประเภทของมัน

ความหลากหลายหมายถึงความสามารถของเอนทิตีบางอย่างที่จะเกิดขึ้นในรูปแบบที่แตกต่างกัน เป็นที่นิยมในการแสดงโดยผีเสื้อซึ่งแปรเปลี่ยนจากตัวอ่อนเป็นดักแด้ไปจนถึงอิมาโก Polymorphism ยังมีอยู่ในภาษาโปรแกรมเช่นเดียวกับเทคนิคการสร้างแบบจำลองที่ช่วยให้คุณสร้างอินเทอร์เฟซเดียวสำหรับตัวถูกดำเนินการอาร์กิวเมนต์และอ็อบเจ็กต์ต่างๆ ความหลากหลายของ Java ส่งผลให้โค้ดมีความกระชับและดูแลรักษาง่ายขึ้น

แม้ว่าบทช่วยสอนนี้จะเน้นไปที่ความหลากหลายของประเภทย่อย แต่ก็มีประเภทอื่น ๆ อีกหลายประเภทที่คุณควรทราบ เราจะเริ่มต้นด้วยภาพรวมของความหลากหลายทั้งสี่ประเภท

ดาวน์โหลดรับโค้ดดาวน์โหลดซอร์สโค้ดสำหรับแอปพลิเคชันตัวอย่างในบทช่วยสอนนี้ สร้างโดย Jeff Friesen สำหรับ JavaWorld

ประเภทของความหลากหลายใน Java

ความหลากหลายใน Java มีสี่ประเภท:

  1. การบังคับขู่เข็ญคือการดำเนินการที่ให้บริการหลายประเภทผ่านการแปลงประเภทโดยนัย ตัวอย่างเช่นคุณหารจำนวนเต็มด้วยจำนวนเต็มอื่นหรือค่าทศนิยมด้วยค่าทศนิยมอื่น ถ้าตัวถูกดำเนินการตัวหนึ่งเป็นจำนวนเต็มและตัวถูกดำเนินการอีกตัวเป็นค่าทศนิยมคอมไพลเลอร์จะบังคับ (แปลงโดยปริยาย) จำนวนเต็มเป็นค่าทศนิยมเพื่อป้องกันข้อผิดพลาดประเภท (ไม่มีการดำเนินการหารที่สนับสนุนตัวถูกดำเนินการจำนวนเต็มและตัวถูกดำเนินการจุดลอยตัว) อีกตัวอย่างหนึ่งคือการส่งการอ้างอิงอ็อบเจ็กต์คลาสย่อยไปยังพารามิเตอร์ซูเปอร์คลาสของเมธอด คอมไพเลอร์บังคับประเภทซับคลาสให้เป็นประเภทซูเปอร์คลาสเพื่อ จำกัด การดำเนินการให้อยู่ในระดับซูเปอร์คลาส
  2. การโอเวอร์โหลดหมายถึงการใช้สัญลักษณ์ตัวดำเนินการเดียวกันหรือชื่อวิธีการในบริบทที่ต่างกัน ตัวอย่างเช่นคุณอาจใช้+เพื่อทำการเพิ่มจำนวนเต็มการเพิ่มทศนิยมหรือการต่อสตริงขึ้นอยู่กับประเภทของตัวถูกดำเนินการ นอกจากนี้หลายวิธีที่มีชื่อเดียวกันสามารถปรากฏในคลาส (ผ่านการประกาศและ / หรือการสืบทอด)
  3. ความหลากหลายเชิงพารามิเตอร์กำหนดว่าภายในการประกาศคลาสชื่อฟิลด์สามารถเชื่อมโยงกับประเภทต่างๆและชื่อเมธอดสามารถเชื่อมโยงกับพารามิเตอร์และชนิดการส่งคืนที่แตกต่างกัน จากนั้นฟิลด์และวิธีการสามารถใช้กับประเภทต่างๆในแต่ละอินสแตนซ์คลาส (อ็อบเจ็กต์) ตัวอย่างเช่นฟิลด์อาจเป็นประเภทDouble(สมาชิกของไลบรารีคลาสมาตรฐานของ Java ที่ตัดdoubleค่า) และเมธอดอาจส่งคืน a Doubleในอ็อบเจ็กต์เดียวและฟิลด์เดียวกันอาจเป็นประเภทStringและเมธอดเดียวกันอาจส่งคืนStringอ็อบเจกต์อื่น . Java รองรับพหุนามพาราเมตริกผ่าน generics ซึ่งฉันจะพูดถึงในบทความต่อไป
  4. ประเภทย่อยหมายถึงประเภทสามารถใช้เป็นประเภทย่อยของประเภทอื่นได้ เมื่ออินสแตนซ์ประเภทย่อยปรากฏในบริบท supertype การเรียกใช้การดำเนินการ supertype บนอินสแตนซ์ประเภทย่อยจะส่งผลให้การดำเนินการดังกล่าวเป็นเวอร์ชันย่อย ตัวอย่างเช่นพิจารณาส่วนของรหัสที่วาดรูปร่างโดยพลการ คุณสามารถแสดงโค้ดภาพวาดนี้ให้กระชับยิ่งขึ้นโดยการแนะนำShapeคลาสด้วยdraw()วิธีการ โดยการแนะนำCircle, Rectangleและอื่น ๆ ที่ subclasses แทนที่draw(); โดยการแนะนำอาร์เรย์ประเภทShapeที่องค์ประกอบเก็บการอ้างอิงไปยังShapeอินสแตนซ์คลาสย่อย และโดยการเรียกShape's draw()วิธีการในแต่ละกรณี เมื่อคุณเรียกdraw()มันเป็นCircle's, Rectangle' หรืออื่น ๆShapeของอินสแตนซ์draw()วิธีการที่เรียก เราบอกว่ามีหลายรูปแบบของShape's draw()วิธี

บทช่วยสอนนี้แนะนำความหลากหลายประเภทย่อย คุณจะได้เรียนรู้เกี่ยวกับ upcasting และ late binding คลาสนามธรรม (ซึ่งไม่สามารถสร้างอินสแตนซ์ได้) และวิธีการนามธรรม (ซึ่งไม่สามารถเรียกได้) นอกจากนี้คุณยังจะได้เรียนรู้เกี่ยวกับการดาวน์คาสติ้งและการระบุประเภทรันไทม์และคุณจะได้เห็นประเภทผลตอบแทนโควาเรียเป็นอันดับแรก ฉันจะบันทึกพหุนามพาราเมตริกสำหรับบทช่วยสอนในอนาคต

Ad-hoc กับความหลากหลายสากล

เช่นเดียวกับนักพัฒนาหลาย ๆ คนฉันจัดประเภทการบีบบังคับและการโอเวอร์โหลดเป็นความหลากหลายแบบเฉพาะกิจและพาราเมตริกและชนิดย่อยเป็นความหลากหลายสากล ในขณะที่เทคนิคที่มีคุณค่าฉันไม่เชื่อว่าการบีบบังคับและการใช้งานมากเกินไปเป็นความหลากหลายที่แท้จริง พวกมันเหมือนกับการแปลงประเภทและน้ำตาลวากยสัมพันธ์มากกว่า

ความหลากหลายประเภทย่อย: การอัปโหลดและการผูกมัดในภายหลัง

ความหลากหลายประเภทย่อยขึ้นอยู่กับ upcasting และ late binding Upcastingคือรูปแบบของการแคสต์ที่คุณสร้างลำดับชั้นการสืบทอดจากประเภทย่อยไปยังซุปเปอร์ไทป์ ไม่มีตัวดำเนินการแคสต์เกี่ยวข้องเนื่องจากชนิดย่อยเป็นความเชี่ยวชาญเฉพาะของซูเปอร์ไทป์ ยกตัวอย่างเช่นShape s = new Circle();upcasts จากไปCircle Shapeสิ่งนี้สมเหตุสมผลเพราะวงกลมเป็นรูปร่างชนิดหนึ่ง

หลังจาก upcasting CircleไปShapeคุณจะไม่สามารถเรียกCircleวิธีการ -specific เช่นgetRadius()วิธีการที่ผลตอบแทนรัศมีวงกลมเพราะCircleวิธีการ -specific ไม่ได้เป็นส่วนหนึ่งของShapeอินเตอร์เฟซ การสูญเสียการเข้าถึงคุณสมบัติประเภทย่อยหลังจากการ จำกัด คลาสย่อยให้แคบลงไปยังซูเปอร์คลาสนั้นดูเหมือนจะไม่มีจุดหมาย แต่จำเป็นสำหรับการบรรลุความหลากหลายประเภทย่อย

สมมติว่าShapeประกาศdraw()วิธีการของCirclesubclass แทนที่วิธีการนี้ได้ดำเนินการเพียงและระบุบรรทัดถัดไปShape s = new Circle(); s.draw();ซึ่งdraw()วิธีการที่เรียกว่า: Shape's draw()วิธีการหรือCircle' s draw()วิธี? คอมไพเลอร์ไม่ทราบว่าdraw()จะเรียกใช้วิธีใด สิ่งที่ทำได้คือตรวจสอบว่ามีเมธอดอยู่ในซูเปอร์คลาสและตรวจสอบว่ารายการอาร์กิวเมนต์ของการเรียกเมธอดและประเภทการส่งคืนตรงกับการประกาศเมธอดของ superclass อย่างไรก็ตามคอมไพเลอร์ยังแทรกคำสั่งลงในโค้ดที่คอมไพล์ซึ่งในขณะรันไทม์ดึงข้อมูลและใช้การอ้างอิงใด ๆ ในsการเรียกdraw()วิธีการที่ถูกต้อง งานนี้เป็นที่รู้จักกันผูกปลาย

การผูกล่าช้าเทียบกับการผูกในช่วงต้น

การผูกล่าช้าใช้สำหรับการโทรไปยังfinalวิธีการที่ไม่ใช่อินสแตนซ์ สำหรับการเรียกใช้เมธอดอื่น ๆ คอมไพเลอร์จะรู้ว่าจะเรียกใช้เมธอดใด แทรกคำสั่งลงในโค้ดที่คอมไพล์แล้วซึ่งเรียกใช้เมธอดที่เกี่ยวข้องกับชนิดของตัวแปรไม่ใช่ค่าของมัน เทคนิคนี้เป็นที่รู้จักกันผูกพันต้น

ฉันได้สร้างแอปพลิเคชั่นที่แสดงให้เห็นถึงความหลากหลายของประเภทย่อยในแง่ของการอัปคาสต์และการเชื่อมโยงล่าช้า โปรแกรมนี้ประกอบด้วยShape, Circle, RectangleและShapesชั้นเรียนซึ่งแต่ละระดับจะถูกเก็บไว้ในแฟ้มแหล่งที่มาของตัวเอง รายการ 1 แสดงสามคลาสแรก

รายการ 1. การประกาศลำดับชั้นของรูปร่าง

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

รายการ 2 แสดงShapesคลาสแอปพลิเคชันที่มีmain()วิธีการขับเคลื่อนแอปพลิเคชัน

Listing 2. Upcasting และ late binding in subtype polymorphism

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

การประกาศshapesอาร์เรย์แสดงให้เห็นถึงการอัปโหลด CircleและRectangleลำดับที่ถูกเก็บไว้ในshapes[0]และshapes[1]และ upCast Shapeประเภท แต่ละรายการshapes[0]และshapes[1]ถือเป็นShapeตัวอย่าง: shapes[0]ไม่ถือเป็นCircle; shapes[1]ไม่ถือว่าเป็นไฟล์Rectangle.

การผูกล่าช้าแสดงโดยshapes[i].draw();นิพจน์ เมื่อiเท่ากับ0คอมไพเลอร์ที่สร้างขึ้นโดยคำแนะนำและสาเหตุCircleของdraw()วิธีการที่จะเรียกว่า เมื่อiเท่ากับ1แต่คำสั่งนี้สาเหตุRectangleของdraw()วิธีการที่จะเรียกว่า นี่คือสาระสำคัญของความหลากหลายประเภทย่อย

สมมติว่าทุกไฟล์ที่มาสี่ ( Shapes.java, Shape.java, Rectangle.javaและCircle.java) ตั้งอยู่ในไดเรกทอรีปัจจุบันรวบรวมพวกเขาผ่านทางทั้งสองบรรทัดคำสั่งต่อไปนี้:

javac *.java javac Shapes.java

เรียกใช้แอปพลิเคชันที่เป็นผลลัพธ์:

java Shapes

คุณควรสังเกตผลลัพธ์ต่อไปนี้:

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

คลาสและวิธีการที่เป็นนามธรรม

เมื่อออกแบบลำดับชั้นชั้นเรียนคุณจะพบว่าชั้นเรียนที่อยู่ใกล้ด้านบนสุดของลำดับชั้นเหล่านี้มีลักษณะทั่วไปมากกว่าชั้นเรียนที่อยู่ต่ำลงมา ตัวอย่างเช่นVehiclesuperclass เป็นแบบทั่วไปมากกว่าTruckคลาสย่อย ในทำนองเดียวกันShapeซูเปอร์คลาสจะมีลักษณะทั่วไปมากกว่าคลาสย่อยCircleหรือ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.

ในกรณีนี้คอมไพลเลอร์จะไม่บ่นเพราะการดาวน์คาสติ้งจากซูเปอร์คลาสไปยังคลาสย่อยในลำดับชั้นประเภทเดียวกันนั้นถูกกฎหมาย subclass.method();ที่กล่าวว่าถ้าได้รับมอบหมายได้รับอนุญาตให้แอพลิเคชันจะผิดพลาดเมื่อมีความพยายามที่จะดำเนินการ ในกรณีนี้ JVM จะพยายามที่จะเรียกวิธีการดำรงอยู่เพราะไม่ได้ประกาศSuperclass method()โชคดีที่ JVM ตรวจสอบว่านักแสดงถูกกฎหมายก่อนที่จะทำการแคสต์ เมื่อตรวจพบว่าSuperclassไม่ประกาศmethod()ก็จะขว้างClassCastExceptionสิ่งของ (ฉันจะพูดถึงข้อยกเว้นในบทความในอนาคต)

รวบรวมรายชื่อ 5 ดังนี้

javac BadDowncast.java

เรียกใช้แอปพลิเคชันที่เป็นผลลัพธ์:

java BadDowncast