สวัสดี! ในบทเรียนวันนี้ เราจะมาศึกษาเรื่องทั่วไปกันต่อไป เมื่อมันเกิดขึ้น นี่เป็นหัวข้อใหญ่ แต่ก็ไม่มีทางหลีกเลี่ยงได้ — มันเป็นส่วนสำคัญอย่างยิ่งของภาษา :) เมื่อคุณศึกษาเอกสาร Oracle เกี่ยวกับข้อมูลทั่วไปหรืออ่านบทช่วยสอนออนไลน์ คุณจะพบกับคำศัพท์ประเภทที่ไม่สามารถแก้ไขซ้ำได้และประเภทที่สามารถแก้ไขได้ ประเภทที่สามารถแก้ไขได้คือประเภทที่ข้อมูลพร้อมใช้งานอย่างสมบูรณ์ ณ รันไทม์ ใน Java ประเภทดังกล่าวประกอบด้วยประเภทดั้งเดิม ประเภทดิบ และประเภทที่ไม่ใช่ประเภททั่วไป ในทางตรงกันข้ามประเภทที่ไม่สามารถแก้ไขได้คือประเภทที่ข้อมูลถูกลบและไม่สามารถเข้าถึงได้ในขณะรันไทม์ เมื่อเกิดขึ้น สิ่งเหล่านี้คือยาสามัญ —
มลพิษจากกองขยะสามารถเกิดขึ้นได้ในสถานการณ์ต่อไปนี้:
List<String>
, List<Integer>
, ฯลฯ
คุณจำได้ไหมว่า varargs คืออะไร?
ในกรณีที่คุณลืม นี่คืออาร์กิวเมนต์ที่มีความยาวผันแปรได้ พวกมันมีประโยชน์ในสถานการณ์ที่เราไม่รู้ว่าอาจมีการส่งผ่านอาร์กิวเมนต์จำนวนเท่าใดไปยังเมธอดของเรา ตัวอย่างเช่น ถ้าเรามีคลาสเครื่องคิดเลขที่มีsum
เมธอด วิธีsum()
รับเลข 2 ตัว 3 ตัว 5 ตัว หรือกี่ตัวก็ได้ เป็นเรื่องแปลกมากที่จะใช้sum()
วิธีโอเวอร์โหลดสำหรับอาร์กิวเมนต์ทุกจำนวนที่เป็นไปได้ เราสามารถทำได้แทน:
public class SimpleCalculator {
public static int sum(int...numbers) {
int result = 0;
for(int i : numbers) {
result += i;
}
return result;
}
public static void main(String[] args) {
System.out.println(sum(1,2,3,4,5));
System.out.println(sum(2,9));
}
}
เอาต์พุตคอนโซล:
15
11
สิ่งนี้แสดงให้เราเห็นว่ามีคุณสมบัติที่สำคัญบางประการเมื่อใช้ varargs ร่วมกับยาชื่อสามัญ ลองดูรหัสต่อไปนี้:
import javafx.util.Pair;
import java.util.ArrayList;
import java.util.List;
public class Main {
public static <E> void addAll(List<E> list, E... array) {
for (E element : array) {
list.add(element);
}
}
public static void main(String[] args) {
addAll(new ArrayList<String>(), // This is okay
"Leonardo da Vinci",
"Vasco de Gama"
);
// but here we get a warning
addAll(new ArrayList<Pair<String, String>>(),
new Pair<String, String>("Leonardo", "da Vinci"),
new Pair<String, String>("Vasco", "de Gama")
);
}
}
เมธอดaddAll()
ใช้เป็นอินพุต a List<E>
และวัตถุจำนวนเท่าใดก็ได้E
จากนั้นจึงเพิ่มวัตถุเหล่านี้ทั้งหมดลงในรายการ ในmain()
เมธอด เราเรียกaddAll()
เมธอดของเราสองครั้ง ในกรณีแรก เราเพิ่มสตริงธรรมดาสองสตริงในList
. ทุกอย่างเป็นไปตามลำดับที่นี่ ในกรณีที่สอง เราเพิ่มสองPair<String, String>
อ็อบเจกต์ในList
. แต่ที่นี่เราได้รับคำเตือนโดยไม่คาดคิด:
Unchecked generics array creation for varargs parameter
นั่นหมายความว่าอย่างไร? เหตุใดเราจึงได้รับคำเตือน และเหตุใดจึงมีการกล่าวถึงarray
? เพราะรหัสของเราไม่มีarray
! เรามาเริ่มกันที่กรณีที่สอง คำเตือนกล่าวถึงอาร์เรย์เนื่องจากคอมไพเลอร์แปลงอาร์กิวเมนต์ความยาวผันแปร (varargs) เป็นอาร์เรย์ กล่าวอีกนัยหนึ่งลายเซ็นของaddAll()
วิธีการของเราคือ:
public static <E> void addAll(List<E> list, E... array)
ดูเหมือนว่าจริง ๆ แล้ว:
public static <E> void addAll(List<E> list, E[] array)
นั่นคือ ในmain()
เมธอด คอมไพเลอร์จะแปลงโค้ดของเราเป็น:
public static void main(String[] args) {
addAll(new ArrayList<String>(),
new String[] {
"Leonardo da Vinci",
"Vasco de Gama"
}
);
addAll(new ArrayList<Pair<String,String>>(),
new Pair<String,String>[] {
new Pair<String,String>("Leonardo","da Vinci"),
new Pair<String,String>("Vasco","de Gama")
}
);
}
อาร์เรย์String
ก็ใช้ได้ แต่Pair<String, String>
อาร์เรย์ไม่ใช่ ปัญหาคือPair<String, String>
ประเภทที่ไม่สามารถแก้ไขได้ ระหว่างการคอมไพล์ ข้อมูลทั้งหมดเกี่ยวกับอาร์กิวเมนต์ประเภท (<String, String>) จะถูกลบ ไม่อนุญาตให้สร้างอาร์เรย์ประเภทที่ไม่สามารถแก้ไขซ้ำได้ใน Java คุณสามารถดูสิ่งนี้ได้หากคุณพยายามสร้างอาร์เรย์ Pair<String, String> ด้วยตนเอง
public static void main(String[] args) {
// Compilation error Generic array creation
Pair<String, String>[] array = new Pair<String, String>[10];
}
เหตุผลชัดเจน: ประเภทความปลอดภัย อย่างที่คุณคงจำได้ เมื่อสร้างอาร์เรย์ คุณต้องระบุวัตถุ (หรือวัตถุดั้งเดิม) ที่อาร์เรย์จะจัดเก็บอย่างแน่นอน
int array[] = new int[10];
ในบทเรียนก่อนหน้านี้ เราได้ตรวจสอบการลบประเภทโดยละเอียด ในกรณีนี้ การลบประเภททำให้เราสูญเสียข้อมูลที่Pair
ออบเจกต์จัดเก็บไว้<String, String>
คู่กัน การสร้างอาร์เรย์จะไม่ปลอดภัย เมื่อใช้วิธีการที่เกี่ยวข้องกับvarargsและ generics อย่าลืมจำเกี่ยวกับการลบประเภทและวิธีการทำงาน หากคุณมั่นใจอย่างแน่นอนเกี่ยวกับโค้ดที่คุณเขียน และรู้ว่าโค้ดจะไม่ก่อให้เกิดปัญหาใดๆ คุณสามารถปิดคำเตือนที่เกี่ยวข้องกับ varargs ได้โดยใช้คำอธิบาย@SafeVarargs
ประกอบ
@SafeVarargs
public static <E> void addAll(List<E> list, E... array) {
for (E element : array) {
list.add(element);
}
}
หากคุณเพิ่มคำอธิบายประกอบนี้ในวิธีการของคุณ คำเตือนที่เราพบก่อนหน้านี้จะไม่ปรากฏขึ้น อีกปัญหาหนึ่งที่สามารถเกิดขึ้นได้เมื่อใช้ varargs ร่วมกับยาชื่อสามัญคือมลพิษแบบกอง 
import java.util.ArrayList;
import java.util.List;
public class Main {
static List<String> polluteHeap() {
List numbers = new ArrayList<Number>();
numbers.add(1);
List<String> strings = numbers;
strings.add("");
return strings;
}
public static void main(String[] args) {
List<String> stringsWithHeapPollution = polluteHeap();
System.out.println(stringsWithHeapPollution.get(0));
}
}
เอาต์พุตคอนโซล:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
พูดง่ายๆ ก็คือ มลพิษแบบฮีปคือเมื่อวัตถุประเภทA
ควรอยู่ในฮีป แต่วัตถุประเภทนั้นB
จบลงที่นั่นเนื่องจากข้อผิดพลาดที่เกี่ยวข้องกับความปลอดภัยของประเภท ในตัวอย่างของเรา นี่คือสิ่งที่เกิดขึ้นอย่างแน่นอน ขั้นแรก เราสร้างnumbers
ตัวแปรดิบและกำหนดคอลเล็กชันทั่วไป ( ArrayList<Number>
) ให้กับตัวแปรนั้น จากนั้นเราเพิ่มหมายเลขลง1
ในคอลเล็กชัน
List<String> strings = numbers;
ในบรรทัดนี้ คอมไพเลอร์พยายามเตือนเราถึงข้อผิดพลาดที่อาจเกิดขึ้นโดยออกคำเตือน " Uncheckedassignment... " แต่เราเพิกเฉย เราลงเอยด้วยตัวแปรประเภททั่วไปList<String>
ที่ชี้ไปที่คอลเล็กชันประเภทArrayList<Number>
ทั่วไป เห็นได้ชัดว่าสถานการณ์นี้อาจทำให้เกิดปัญหาได้! และมันก็เป็นเช่นนั้น ด้วยการใช้ตัวแปรใหม่ของเรา เราเพิ่มสตริงลงในคอลเลกชัน ขณะนี้เรามีมลภาวะเป็นกอง — เราเพิ่มตัวเลขและสตริงลงในคอลเล็กชันแบบพาราเมตริก คอมไพเลอร์เตือนเรา แต่เราเพิกเฉยต่อคำเตือนของมัน เป็นผลให้เราได้รับClassCastException
เฉพาะในขณะที่โปรแกรมกำลังทำงาน แล้วมันเกี่ยวอะไรกับ varargs? การใช้ varargs กับยาชื่อสามัญสามารถนำไปสู่มลพิษแบบกองได้ง่าย นี่คือตัวอย่างง่ายๆ:
import java.util.Arrays;
import java.util.List;
public class Main {
static void polluteHeap(List<String>... stringsLists) {
Object[] array = stringsLists;
List<Integer> numbersList = Arrays.asList(66,22,44,12);
array[0] = numbersList;
String str = stringsLists[0].get(0);
}
public static void main(String[] args) {
List<String> cars1 = Arrays.asList("Ford", "Fiat", "Kia");
List<String> cars2 = Arrays.asList("Ferrari", "Bugatti", "Zaporozhets");
polluteHeap(cars1, cars2);
}
}
เกิดอะไรขึ้นที่นี่? เนื่องจากการลบประเภท อาร์กิวเมนต์ความยาวผันแปรของเรา
List<String>...stringsLists
กลายเป็นอาร์เรย์ของรายการ เช่นList[]
ของวัตถุประเภทที่ไม่รู้จัก (อย่าลืมว่า varargs จะกลายเป็นอาร์เรย์ปกติระหว่างการคอมไพล์) ด้วยเหตุนี้ เราจึงสามารถกำหนดให้ตัวแปรObject[] array
ในบรรทัดแรกของเมธอดได้อย่างง่ายดาย — ประเภทของวัตถุในรายการของเราถูกลบไปแล้ว! และตอนนี้เรามีObject[]
ตัวแปรแล้ว ซึ่งเราสามารถเพิ่มอะไรก็ได้ เพราะอ็อบเจกต์ทั้งหมดใน Java สืบทอดObject
! ในตอนแรก เรามีอาร์เรย์ของรายการสตริงเท่านั้น แต่ต้องขอบคุณการลบประเภทและการใช้ varargs ของเรา เราจึงสามารถเพิ่มรายการตัวเลขซึ่งเราทำได้อย่างง่ายดาย เป็นผลให้เราก่อมลพิษกองโดยการผสมวัตถุประเภทต่างๆ ผลลัพธ์จะเป็นอีกแบบหนึ่งClassCastException
เมื่อเราพยายามอ่านสตริงจากอาร์เรย์ เอาต์พุตคอนโซล:
Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
ผลลัพธ์ที่ไม่คาดคิดอาจเกิดขึ้นได้จากการใช้ varargs ซึ่งเป็นกลไกที่ดูเหมือนง่าย :) และด้วยเหตุนี้ บทเรียนของวันนี้ก็จบลง อย่าลืมแก้โจทย์สองสามข้อ และถ้ามีเวลาและแรง ลองอ่านเพิ่มเติม " Effective Java " จะไม่อ่านตัวเอง! :) จนกว่าจะถึงครั้งต่อไป!
GO TO FULL VERSION