Generics برای ارائهی بررسی نوع سفتتر در زمان کامپایل و پشتیبانی از برنامهنویسی Generic به زبان جاوا معرفی شدند. برای پیادهسازی Generics، کامپایلر جاوا Type Erasure را اعمال میکند تا:
Type Erasure تضمین میکند نوع جدیدی برای انواع پارامتریشده ایجاد نشود؛ در نتیجه، Generics هزینهی اجرایی اضافه نمیکنند.
در فرآیند Type Erasure، کامپایلر جاوا تمام Type Parameterها را حذف و هر کدام را با اولین حد آن جایگزین میکند اگر Type Parameter محدود باشد، یا Object اگر Type Parameter بدون حد باشد.
کلاس Generic زیر که یک گره در لیست پیوندی تکی را نشان میدهد را در نظر بگیرید:
public class Node<T> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
چون Type Parameter T بدون حد است، کامپایلر جاوا آن را با Object جایگزین میکند:
public class Node {
private Object data;
private Node next;
public Node(Object data, Node next) {
this.data = data;
this.next = next;
}
public Object getData() { return data; }
// ...
}
در مثال زیر، کلاس Generic Node از Type Parameter محدودشده استفاده میکند:
public class Node<T extends Comparable<T>> {
private T data;
private Node<T> next;
public Node(T data, Node<T> next) {
this.data = data;
this.next = next;
}
public T getData() { return data; }
// ...
}
کامپایلر جاوا Type Parameter محدودشده T را با کلاس حد اول، Comparable، جایگزین میکند:
public class Node {
private Comparable data;
private Node next;
public Node(Comparable data, Node next) {
this.data = data;
this.next = next;
}
public Comparable getData() { return data; }
// ...
}
---
کامپایلر جاوا Type Parameterها در آرگومانهای متد Generic را هم حذف میکند. متد Generic زیر را در نظر بگیرید:
// Counts the number of occurrences of elem in anArray.
//
public static <T> int count(T[] anArray, T elem) {
int cnt = 0;
for (T e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
چون T بدون حد است، کامپایلر جاوا آن را با Object جایگزین میکند:
public static int count(Object[] anArray, Object elem) {
int cnt = 0;
for (Object e : anArray)
if (e.equals(elem))
++cnt;
return cnt;
}
فرض کنید کلاسهای زیر تعریف شدهاند:
class Shape { /* ... */ }
class Circle extends Shape { /* ... */ }
class Rectangle extends Shape { /* ... */ }
میتوانید متد Generic برای ترسیم شکلهای مختلف بنویسید:
public static <T extends Shape> void draw(T shape) { /* ... */ }
کامپایلر جاوا T را با Shape جایگزین میکند:
public static void draw(Shape shape) { /* ... */ }
---
گاهی Type Erasure وضعیتی ایجاد میکند که ممکن است انتظارش را نداشته باشید. مثال زیر نشان میدهد چگونه این اتفاق میافتد. همچنین نشان میدهد کامپایلر گاهی متد مصنوعی (Synthetic Method) به نام Bridge Method ایجاد میکند.
کلاسهای زیر را در نظر بگیرید:
public class Node<T> {
public T data;
public Node(T data) { this.data = data; }
public void setData(T data) {
IO.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node<Integer> {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
IO.println("MyNode.setData");
super.setData(data);
}
}
کد زیر را در نظر بگیرید:
MyNode mn = new MyNode(5);
Node n = mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello"); // Causes a ClassCastException to be thrown.
Integer x = mn.data;
بعد از Type Erasure، این کد به این صورت در میآید:
MyNode mn = new MyNode(5);
Node n = (MyNode)mn; // A raw type - compiler throws an unchecked warning
n.setData("Hello"); // Causes a ClassCastException to be thrown.
Integer x = (String)mn.data;
بخش بعدی توضیح میدهد چرا ClassCastException در عبارت n.setData("Hello"); پرتاب میشود.
هنگام کامپایل کلاس یا Interface که از کلاس پارامتریشده ارث میبرد یا Interface پارامتریشدهای را پیادهسازی میکند، کامپایلر ممکن است نیاز به ایجاد متد مصنوعی داشته باشد که به آن Bridge Method گفته میشود. معمولاً نیازی به نگرانی درباره Bridge Methodها نیست، اما ممکن است اگر یکی در Stack Trace ظاهر شود گیج شوید.
بعد از Type Erasure، کلاسهای Node و MyNode به این صورت در میآیند:
public class Node {
public Object data;
public Node(Object data) { this.data = data; }
public void setData(Object data) {
IO.println("Node.setData");
this.data = data;
}
}
public class MyNode extends Node {
public MyNode(Integer data) { super(data); }
public void setData(Integer data) {
IO.println("MyNode.setData");
super.setData(data);
}
}
بعد از Type Erasure، signature متدها تطبیق نمیدهند؛ متد Node.setData(T) به Node.setData(Object) تبدیل میشود. در نتیجه، متد MyNode.setData(Integer) متد Node.setData(Object) را بازنویسی نمیکند.
برای حل این مشکل و حفظ Polymorphism انواع Generic بعد از Type Erasure، کامپایلر Bridge Method تولید میکند تا مطمئن شود زیرنوعسازی طبق انتظار کار میکند.
برای کلاس MyNode، کامپایلر Bridge Method زیر را برای setData() تولید میکند:
class MyNode extends Node {
// Bridge method generated by the compiler
//
public void setData(Object data) {
setData((Integer) data);
}
public void setData(Integer data) {
IO.println("MyNode.setData");
super.setData(data);
}
// ...
}
Bridge Method MyNode.setData(object) به متد اصلی MyNode.setData(Integer) واگذار میکند. در نتیجه، عبارت n.setData("Hello"); متد MyNode.setData(Object) را فراخوانی میکند و ClassCastException پرتاب میشود چون "Hello" نمیتواند به Integer تبدیل شود.
فرآیندی را بحث کردیم که کامپایلر اطلاعات مرتبط با Type Parameterها و Type Argumentها را حذف میکند. Type Erasure پیامدهایی درباره متغیرهای متغیر (Varargs) دارد که پارامتر رسمی Varargs آن نوع Non-Reifiable دارد. اطلاعات بیشتر درباره متدهای Varargs در بخش تعداد دلخواه آرگومانها در ارسال اطلاعات به متد یا Constructor.
این صفحه موضوعات زیر را پوشش میدهد:
نوع Reifiable نوعی است که اطلاعات نوع آن در زمان اجرا کاملاً در دسترس است. این شامل انواع اولیه، انواع غیرGeneric، انواع Raw و فراخوانیهای Wildcard بدون حد میشود.
انواع Non-Reifiable نوعهایی هستند که اطلاعاتشان توسط Type Erasure در زمان کامپایل حذف شده — فراخوانیهای انواع Generic که به صورت Wildcard بدون حد تعریف نشدهاند. نوع Non-Reifiable تمام اطلاعاتش در زمان اجرا در دسترس نیست. مثالهایی از انواع Non-Reifiable عبارتند از List<String> و List<Number>; JVM نمیتواند در زمان اجرا تفاوت این نوعها را تشخیص دهد. همانطور که در بخش محدودیتهای Generics نشان داده شد، موقعیتهایی وجود دارد که نمیتوان از انواع Non-Reifiable استفاده کرد: مثلاً در عبارت instanceof یا به عنوان عنصر در آرایه.
آلودگی هیپ وقتی رخ میدهد که متغیری از نوع پارامتریشده به شیای اشاره کند که از آن نوع پارامتریشده نیست. این وضعیت وقتی پیش میآید که برنامه عملیاتی انجام داده باشد که هشدار Unchecked در زمان کامپایل ایجاد میکند. هشدار Unchecked تولید میشود اگر، در زمان کامپایل (در محدودهی قوانین بررسی نوع زمان کامپایل) یا در زمان اجرا، صحت عملیاتی شامل نوع پارامتریشده (مثلاً Cast یا فراخوانی متد) نتواند تأیید شود. مثلاً آلودگی هیپ هنگام ترکیب انواع Raw و انواع پارامتریشده یا انجام Castهای Unchecked رخ میدهد.
در شرایط عادی، وقتی تمام کد همزمان کامپایل شود، کامپایلر هشدار Unchecked صادر میکند تا توجه شما را به آلودگی هیپ احتمالی جلب کند. اگر بخشهایی از کد را جداگانه کامپایل کنید، تشخیص خطر احتمالی آلودگی هیپ دشوار است. اگر مطمئن شوید کد بدون هشدار کامپایل میشود، آلودگی هیپ رخ نمیدهد.
---متدهای Generic شامل پارامترهای ورودی Varargs میتوانند آلودگی هیپ ایجاد کنند.
کلاس ArrayBuilder زیر را در نظر بگیرید:
public class ArrayBuilder {
public static <T> void addToList (List<T> listArg, T... elements) {
for (T x : elements) {
listArg.add(x);
}
}
public static void faultyMethod(List<String>... l) {
Object[] objectArray = l; // Valid
objectArray[0] = Arrays.asList(42);
String s = l[0].get(0); // ClassCastException thrown here
}
}
مثال HeapPollutionExample زیر از کلاس ArrayBuiler استفاده میکند:
public class HeapPollutionExample {
public static void main(String[] args) {
List<String> stringListA = new ArrayList<String>();
List<String> stringListB = new ArrayList<String>();
ArrayBuilder.addToList(stringListA, "Seven", "Eight", "Nine");
ArrayBuilder.addToList(stringListB, "Ten", "Eleven", "Twelve");
List<List<String>> listOfStringLists =
new ArrayList<List<String>>();
ArrayBuilder.addToList(listOfStringLists,
stringListA, stringListB);
ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
}
}
هنگام کامپایل، هشدار زیر توسط تعریف متد ArrayBuilder.addToList() تولید میشود:
warning: [varargs] Possible heap pollution from parameterized vararg type T
وقتی کامپایلر با متد Varargs مواجه میشود، پارامتر رسمی Varargs را به آرایه ترجمه میکند. اما زبان برنامهنویسی جاوا ایجاد آرایههایی از انواع پارامتریشده را مجاز نمیداند. در متد ArrayBuilder.addToList()، کامپایلر پارامتر رسمی Varargs T... elements را به پارامتر رسمی T[] elements، یعنی آرایه، ترجمه میکند. اما به دلیل Type Erasure، کامپایلر پارامتر رسمی Varargs را به Object[] elements تبدیل میکند. در نتیجه، امکان آلودگی هیپ وجود دارد.
عبارت زیر پارامتر رسمی Varargs l را به آرایهی Object objectArgs اختصاص میدهد:
Object[] objectArray = l;
این عبارت میتواند بالقوه آلودگی هیپ ایجاد کند. مقداری که با نوع پارامتریشدهی پارامتر رسمی Varargs l مطابقت ندارد میتواند به متغیر objectArray و در نتیجه به l اختصاص داده شود. اما کامپایلر در این عبارت هشدار Unchecked تولید نمیکند. کامپایلر قبلاً هنگام ترجمهی پارامتر رسمی Varargs List<String>... l به پارامتر رسمی List[] l هشدار تولید کرده. این عبارت معتبر است؛ متغیر l نوع List[] دارد که زیرنوع Object[] است.
در نتیجه، کامپایلر هشدار یا خطایی صادر نمیکند اگر شی List از هر نوعی به هر جزء آرایهی objectArray اختصاص دهید:
objectArray[0] = Arrays.asList(42);
این عبارت اولین جزء آرایهی objectArray را با شی List حاوی یک شی از نوع Integer اختصاص میدهد.
فرض کنید ArrayBuilder.faultyMethod() را با عبارت زیر فراخوانی میکنید:
ArrayBuilder.faultyMethod(Arrays.asList("Hello!"), Arrays.asList("World!"));
در زمان اجرا، JVM ClassCastException در عبارت زیر پرتاب میکند:
// ClassCastException thrown here
String s = l[0].get(0);
شی ذخیرهشده در اولین جزء آرایهی متغیر l نوع List<Integer> دارد اما این عبارت شیای از نوع List<String> را انتظار دارد.
اگر متد Varargs اعلان کنید که پارامترهایی از نوع پارامتریشده دارد و مطمئن شوید بدنهی متد ClassCastException یا Exception مشابه دیگری به دلیل مدیریت نادرست پارامتر رسمی Varargs پرتاب نمیکند، میتوانید از هشداری که کامپایلر برای این نوع متدهای Varargs تولید میکند جلوگیری کنید با اضافه کردن Annotation زیر به اعلان متدهای static و non-constructor:
@SafeVarargs
Annotation @SafeVarargs بخش مستندی از قرارداد متد است؛ این Annotation تأیید میکند پیادهسازی متد پارامتر رسمی Varargs را نادرست مدیریت نخواهد کرد.
همچنین ممکن است، هرچند کمتر مطلوب، با اضافه کردن موارد زیر به اعلان متد این هشدارها را سرکوب کنید:
@SuppressWarnings({"unchecked", "varargs"})
اما این رویکرد هشدارهای تولیدشده از محل فراخوانی متد را سرکوب نمیکند. اگر با سینتکس @SuppressWarnings آشنا نیستید، بخش Annotations را ببینید.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://dev.java/learn/