به طور خلاصه، Generics به انواع (کلاسها و Interfaceها) اجازه میدهند هنگام تعریف کلاسها، Interfaceها و متدها به عنوان پارامتر عمل کنند. درست مثل پارامترهای رسمی آشناتری که در اعلان متدها استفاده میشوند، Type Parameterها راهی برای استفادهی مجدد از همان کد با ورودیهای متفاوت فراهم میکنند. تفاوت این است که ورودیهای پارامترهای رسمی مقادیر هستند، در حالی که ورودیهای Type Parameterها نوعها هستند.
کدی که از Generics استفاده میکند در مقایسه با کد بدون Generic مزایای زیادی دارد:
بررسی نوع قویتر در زمان کامپایل. کامپایلر جاوا بررسی نوع قوی روی کد Generic اعمال میکند و اگر کد نقض ایمنی نوع ایجاد کند، خطا صادر میکند. رفع خطاهای زمان کامپایل آسانتر از رفع خطاهای زمان اجراست که پیدا کردنشان میتواند دشوار باشد.
حذف Castها. قطعه کد زیر بدون Generics به Cast نیاز دارد:
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0);
وقتی با استفاده از Generics بازنویسی شود، کد به Cast نیاز ندارد:
List<String> list = new ArrayList<String>();
list.add("hello");
String s = list.get(0); // no cast
یک نوع Generic کلاس یا Interface Generic است که روی نوعها پارامتری شده است. کلاس Box زیر برای نشان دادن این مفهوم تغییر خواهد کرد.
public class Box {
private Object object;
public void set(Object object) { this.object = object; }
public Object get() { return object; }
}
چون متدهایش Object میپذیرند یا برمیگردانند، آزادید هر چه میخواهید رد کنید، به شرطی که یکی از انواع اولیه (Primitive) نباشد. هیچ راهی برای بررسی در زمان کامپایل وجود ندارد که کلاس چگونه استفاده میشود. بخشی از کد ممکن است یک Integer در جعبه قرار دهد و انتظار داشته باشد اشیایی از نوع Integer از آن خارج شود، در حالی که بخش دیگری ممکن است اشتباهاً یک String رد کند و منجر به خطای زمان اجرا شود.
کلاس Generic با فرمت زیر تعریف میشود:
class name<T1, T2, ..., Tn> { /* ... */ }
بخش Type Parameter، که با زاویهدارها (<>) محدود شده، بعد از نام کلاس میآید. Type Parameterهای T1، T2، ... و Tn را مشخص میکند.
برای بهروزرسانی کلاس Box به استفاده از Generics، یک اعلان نوع Generic با تغییر کد «public class Box» به «public class Box<T>» ایجاد میکنید. این کار متغیر نوع T را معرفی میکند که در هر جای داخل کلاس قابل استفاده است.
با این تغییر، کلاس Box به این صورت در میآید:
/**
* Generic version of the Box class.
* @param <T> the type of the value being boxed
*/
public class Box<T> {
// T stands for "Type"
private T t;
public void set(T t) { this.t = t; }
public T get() { return t; }
}
همانطور که میبینید، تمام جایگزینیهای Object با T جایگزین شدهاند. متغیر نوع میتواند هر نوع غیراولیهای باشد که مشخص کنید: هر نوع کلاس، هر نوع Interface، هر نوع آرایه، یا حتی یک متغیر نوع دیگر.
همین تکنیک را میتوان برای ایجاد Interfaceهای Generic هم به کار برد.
طبق قرارداد، نام Type Parameterها حروف تکی و بزرگ هستند. این در تضاد آشکار با قراردادهای نامگذاری متغیرهاست که از قبل میشناسید، و دلیل خوبی دارد: بدون این قرارداد، تشخیص تفاوت بین متغیر نوع و نام کلاس یا Interface معمولی دشوار خواهد بود.
رایجترین نامهای Type Parameter عبارتند از:
E - Element (بهطور گسترده در Java Collections Framework استفاده میشود)
K - Key
N - Number
T - Type
V - Value
S، U، V و غیره - نوع دوم، سوم، چهارم
این نامها را در سراسر Java SE API و بقیهی این بخش خواهید دید.
برای ارجاع به کلاس Generic Box از درون کد، باید فراخوانی نوع Generic انجام دهید که T را با مقدار مشخصی مثل Integer جایگزین میکند:
Box<Integer> integerBox;
میتوانید فراخوانی نوع Generic را شبیه فراخوانی متد معمولی در نظر بگیرید، اما به جای ارسال آرگومان به یک متد، نوع آرگومانی — در اینجا Integer — را به خود کلاس Box ارسال میکنید.
اصطلاحات Type Parameter و Type Argument: بسیاری از توسعهدهندگان اصطلاحات «Type Parameter» و «Type Argument» را به جای هم به کار میبرند، اما این اصطلاحات یکی نیستند. هنگام کدنویسی، Type Argumentها را برای ایجاد نوع پارامتریشده ارائه میدهید. بنابراین
TدرFoo<T>یک Type Parameter وStringدرFoo<String> fیک Type Argument است. این بخش از این تعریف هنگام استفاده از این اصطلاحات پیروی میکند.
مثل هر اعلان متغیر دیگری، این کد در واقع شی جدیدی از Box ایجاد نمیکند. فقط اعلام میکند integerBox ارجاعی به «Box of Integer» خواهد داشت، که نحوهی خواندن Box<Integer> است.
فراخوانی یک نوع Generic معمولاً به عنوان نوع پارامتریشده (Parameterized Type) شناخته میشود.
برای نمونهسازی این کلاس، مانند همیشه از کلمهکلیدی new استفاده کنید اما <Integer> را بین نام کلاس و پرانتز قرار دهید:
Box<Integer> integerBox = new Box<Integer>();
در جاوا SE 7 و بعد، میتوانید Type Argumentهای لازم برای فراخوانی Constructor یک کلاس Generic را با مجموعهی خالی Type Argument (<>) جایگزین کنید، به شرطی که کامپایلر بتواند Type Argumentها را از بافت استنباط کند. این جفت زاویهدار، <>، بهطور غیررسمی Diamond نامیده میشود. مثال:
Box<Integer> integerBox = new Box<>();
برای اطلاعات بیشتر درباره نماد Diamond و Type Inference، بخش Type Inference این آموزش را ببینید.
همانطور که قبلاً ذکر شد، کلاس Generic میتواند Type Parameterهای متعدد داشته باشد. مثال، کلاس Generic OrderedPair که Interface Generic Pair را پیادهسازی میکند:
public interface Pair<K, V> {
public K getKey();
public V getValue();
}
public class OrderedPair<K, V> implements Pair<K, V> {
private K key;
private V value;
public OrderedPair(K key, V value) {
this.key = key;
this.value = value;
}
public K getKey() { return key; }
public V getValue() { return value; }
}
عبارات زیر دو نمونه از کلاس OrderedPair ایجاد میکنند:
Pair<String, Integer> p1 = new OrderedPair<String, Integer>("Even", 8);
Pair<String, String> p2 = new OrderedPair<String, String>("hello", "world");
کد new OrderedPair<String, Integer>()، K را به عنوان String و V را به عنوان Integer نمونهسازی میکند. بنابراین، Type Parameterهای Constructor کلاس OrderedPair به ترتیب String و Integer هستند. به دلیل Autoboxing، رد کردن String و int به کلاس معتبر است.
همانطور که در بخش Diamond ذکر شد، چون کامپایلر جاوا میتواند نوعهای K و V را از اعلان OrderedPair<String, Integer> استنباط کند، این عبارات با استفاده از نماد Diamond کوتاهتر نوشته میشوند:
OrderedPair<String, Integer> p1 = new OrderedPair<>("Even", 8);
OrderedPair<String, String> p2 = new OrderedPair<>("hello", "world");
برای ایجاد Interface Generic، همان قراردادهای ایجاد کلاس Generic را دنبال کنید.
همچنین میتوانید Type Parameter (یعنی K یا V) را با نوع پارامتریشده، یعنی List<String> جایگزین کنید. مثال با استفاده از OrderedPair<K, V>:
OrderedPair<String, Box<Integer>> p = new OrderedPair<>("primes", new Box<Integer>(...));
---
نوع Raw نام یک کلاس یا Interface Generic بدون هیچ Type Argument است. مثال، کلاس Generic Box:
public class Box<T> {
public void set(T t) { /* ... */ }
// ...
}
برای ایجاد نوع پارامتریشدهی Box<T>، Type Argument واقعی برای Type Parameter رسمی T ارائه میدهید:
Box<Integer> intBox = new Box<>();
اگر Type Argument واقعی حذف شود، نوع Raw Box<T> ایجاد میشود:
Box rawBox = new Box();
بنابراین Box نوع Raw نوع Generic Box<T> است. اما نوع کلاس یا Interface غیرGeneric نوع Raw نیست.
انواع Raw در کدهای قدیمی ظاهر میشوند چون کلاسهای API زیادی (مثل کلاسهای Collections) قبل از JDK 5.0 Generic نبودند. هنگام استفاده از انواع Raw، اساساً رفتار قبل از Generics را دریافت میکنید — یک Box به شما Object برمیگرداند. برای سازگاری معکوس، اختصاص نوع پارامتریشده به نوع Raw آن مجاز است:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox; // OK
اما اگر نوع Raw را به نوع پارامتریشده اختصاص دهید، هشدار دریافت میکنید:
Box rawBox = new Box(); // rawBox is a raw type of Box<T>
Box<Integer> intBox = rawBox; // warning: unchecked conversion
همچنین اگر از نوع Raw برای فراخوانی متدهای Generic تعریفشده در نوع Generic متناظر استفاده کنید، هشدار دریافت میکنید:
Box<String> stringBox = new Box<>();
Box rawBox = stringBox;
rawBox.set(8); // warning: unchecked invocation to set(T)
هشدار نشان میدهد انواع Raw بررسیهای نوع Generic را دور میزنند و تشخیص کد ناامن را به زمان اجرا موکول میکنند. بنابراین باید از استفاده از انواع Raw اجتناب کنید.
بخش Type Erasure اطلاعات بیشتری درباره نحوهی استفادهی کامپایلر جاوا از انواع Raw دارد.
همانطور که قبلاً ذکر شد، هنگام ترکیب کد قدیمی با کد Generic، ممکن است با پیامهای هشداری شبیه به موارد زیر مواجه شوید:
Note: Example.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
این اتفاق هنگام استفاده از API قدیمیتری که با انواع Raw کار میافتد، مثل مثال زیر:
public class WarningDemo {
public static void main(String[] args){
Box<Integer> bi;
bi = createBox();
}
static Box createBox(){
return new Box();
}
}
اصطلاح «unchecked» به این معناست که کامپایلر اطلاعات نوع کافی برای اجرای تمام بررسیهای نوع لازم برای تضمین ایمنی نوع ندارد. هشدار «unchecked» بهطور پیشفرض غیرفعال است، اگرچه کامپایلر سرنخی ارائه میدهد. برای دیدن تمام هشدارهای «unchecked»، با -Xlint:unchecked دوباره کامپایل کنید.
کامپایل مجدد مثال قبلی با -Xlint:unchecked اطلاعات اضافی زیر را آشکار میکند:
WarningDemo.java:4: warning: [unchecked] unchecked conversion
found : Box
required: Box<java.lang.Integer>
bi = createBox();
^
1 warning
برای غیرفعال کردن کامل هشدارهای Unchecked، از پرچم -Xlint:-unchecked استفاده کنید. Annotation @SuppressWarnings("unchecked") هشدارهای Unchecked را سرکوب میکند. اگر با سینتکس @SuppressWarnings آشنا نیستید، بخش Annotations را ببینید.
متدهای Generic متدهایی هستند که Type Parameterهای خودشان را معرفی میکنند. این شبیه اعلان نوع Generic است، اما دامنهی Type Parameter محدود به متدی است که در آن اعلان شده. متدهای Generic ثابت و غیرثابت و همچنین Constructorهای کلاس Generic مجاز هستند.
سینتکس متد Generic شامل لیستی از Type Parameterها، داخل زاویهدارها، است که قبل از نوع بازگشتی متد ظاهر میشود. برای متدهای Generic ثابت، بخش Type Parameter باید قبل از نوع بازگشتی متد باشد.
کلاس Util شامل یک متد Generic به نام compare است که دو شی Pair را مقایسه میکند:
public class Util {
public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
public class Pair<K, V> {
private K key;
private V value;
public Pair(K key, V value) {
this.key = key;
this.value = value;
}
public void setKey(K key) { this.key = key; }
public void setValue(V value) { this.value = value; }
public K getKey() { return key; }
public V getValue() { return value; }
}
سینتکس کامل فراخوانی این متد به این صورت خواهد بود:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
نوع بهصورت صریح مشخص شده، همانطور که با برجسته نشان داده شده. معمولاً میتوان این را حذف کرد و کامپایلر نوع مورد نیاز را استنباط میکند:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);
این قابلیت که به آن Type Inference میگویند، به شما اجازه میدهد متد Generic را مثل یک متد معمولی فراخوانی کنید، بدون اینکه نوعی بین زاویهدارها مشخص کنید. این موضوع در بخش بعدی یعنی Type Inference بیشتر بحث میشود.
---ممکن است زمانهایی باشد که بخواهید نوعهایی که میتوانند به عنوان Type Argument در نوع پارامتریشده استفاده شوند را محدود کنید. مثلاً متدی که روی اعداد عمل میکند ممکن است فقط بخواهد نمونههایی از Number یا زیرکلاسهایش را بپذیرد. Type Parameterهای محدودشده دقیقاً برای همین هستند.
برای اعلان Type Parameter محدودشده، نام Type Parameter را لیست کنید، بعد از آن کلمهکلیدی extends و سپس بالاترین حد آن که در این مثال Number است. توجه کنید که در این بافت، extends به معنای عام به کار میرود و هم extends (برای کلاسها) و هم implements (برای Interfaceها) را شامل میشود.
public class Box<T> {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
public <U extends Number> void inspect(U u){
IO.println("T: " + t.getClass().getName());
IO.println("U: " + u.getClass().getName());
}
public static void main(String[] args) {
Box<Integer> integerBox = new Box<Integer>();
integerBox.set(new Integer(10));
integerBox.inspect("some text"); // error: this is still String!
}
}
با تغییر متد Generic برای شامل کردن این Type Parameter محدودشده، کامپایل اکنون ناموفق خواهد بود، چون فراخوانی inspect همچنان شامل String است:
Box.java:21: <U>inspect(U) in Box<java.lang.Integer> cannot
be applied to (java.lang.String)
integerBox.inspect("10");
^
1 error
علاوه بر محدود کردن نوعهایی که میتوانند برای نمونهسازی نوع Generic استفاده شوند، Type Parameterهای محدودشده به شما اجازه میدهند متدهای تعریفشده در حدود را فراخوانی کنید:
public class NaturalNumber<T extends Integer> {
private T n;
public NaturalNumber(T n) { this.n = n; }
public boolean isEven() {
return n.intValue() % 2 == 0;
}
// ...
}
متد isEven() متد intValue() تعریفشده در کلاس Integer را از طریق n فراخوانی میکند.
مثال قبلی استفاده از Type Parameter با یک حد را نشان میدهد، اما Type Parameter میتواند حدود متعددی داشته باشد:
<T extends B1 & B2 & B3>
متغیر نوع با حدود متعدد زیرنوعی از تمام نوعهای لیستشده در حد است. اگر یکی از حدود کلاس باشد، باید اول مشخص شود. مثال:
class A { /* ... */ }
interface B { /* ... */ }
interface C { /* ... */ }
class D <T extends A & B & C> { /* ... */ }
اگر حد A اول مشخص نشود، خطای زمان کامپایل دریافت میکنید:
class D <T extends B & A & C> { /* ... */ } // compile-time error
---
Type Parameterهای محدودشده کلید پیادهسازی الگوریتمهای Generic هستند. متد زیر را در نظر بگیرید که تعداد عناصری از آرایه T[] که بزرگتر از عنصر مشخص elem هستند را میشمارد.
public static <T> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e > elem) // compiler error
++count;
return count;
}
پیادهسازی متد ساده است اما کامپایل نمیشود چون عملگر بزرگتر از (>) فقط روی انواع اولیه مثل short، int، double، long، float، byte و char اعمال میشود. نمیتوانید از عملگر > برای مقایسهی اشیا استفاده کنید. برای رفع مشکل، از Type Parameter محدودشده توسط Interface Comparable<T> استفاده کنید:
public interface Comparable<T> {
public int compareTo(T o);
}
کد حاصل به این صورت خواهد بود:
public static <T extends Comparable<T>> int countGreaterThan(T[] anArray, T elem) {
int count = 0;
for (T e : anArray)
if (e.compareTo(elem) > 0)
++count;
return count;
}
---
همانطور که میدانید، میتوانید شیای از یک نوع را به شیای از نوع دیگر اختصاص دهید به شرطی که نوعها سازگار باشند. مثلاً میتوانید یک Integer را به یک Object اختصاص دهید، چون Object یکی از فوقنوعهای Integer است:
Object someObject = new Object();
Integer someInteger = new Integer(10);
someObject = someInteger; // OK
در اصطلاحات شیگرا، به این رابطهی «is a» میگویند. چون Integer نوعی از Object است، اختصاص مجاز است. اما Integer نوعی از Number هم هست، بنابراین کد زیر هم معتبر است:
public void someMethod(Number n) { /* ... */ }
someMethod(new Integer(10)); // OK
someMethod(new Double(10.1)); // OK
همین موضوع درباره Generics هم صادق است. میتوانید فراخوانی نوع Generic انجام دهید و Number را به عنوان Type Argument ارسال کنید، و هر فراخوانی بعدی از add اگر آرگومان با Number سازگار باشد مجاز خواهد بود:
Box<Number> box = new Box<Number>();
box.add(new Integer(10)); // OK
box.add(new Double(10.1)); // OK
اکنون متد زیر را در نظر بگیرید:
public void boxTest(Box<Number> n) { /* ... */ }
چه نوع آرگومانی میپذیرد؟ با نگاه به signature میبینید آرگومانی با نوع Box<Number> میپذیرد. اما این یعنی چه؟ آیا میتوانید Box<Integer> یا Box<Double> رد کنید، همانطور که انتظار دارید؟ پاسخ «خیر» است، چون Box<Integer> و Box<Double> زیرنوع Box<Number> نیستند.
این یک سوءتفاوم رایج در برنامهنویسی با Generics است اما مفهوم مهمی برای یادگیری است. Box<Integer> زیرنوع Box<Number> نیست حتی اگر Integer زیرنوع Number باشد.
نکته: با فرض دو نوع مشخص
AوB، مثلاًNumberوInteger،MyClass<A>رابطهای باMyClass<B>ندارد، صرفنظر از اینکهAوBمرتبط باشند یا نه. والد مشترکMyClass<A>وMyClass<B>Objectاست.
برای اطلاعات درباره نحوهی ایجاد رابطهی زیرنوعمانند بین دو کلاس Generic وقتی Type Parameterها مرتبط هستند، بخش Wildcards و زیرنوعها را ببینید.
میتوانید کلاس یا Interface Generic را با Extend یا Implement کردن زیرنوع کنید. رابطه بین Type Parameterهای یک کلاس یا Interface و Type Parameterهای دیگری توسط عبارات extends و implements تعیین میشود.
با استفاده از کلاسهای Collections به عنوان مثال، ArrayList<E> Interface List<E> را پیادهسازی میکند و List<E> از Collection<E> ارث میبرد. پس ArrayList<String> زیرنوع List<String> است که زیرنوع Collection<String> است. تا زمانی که Type Argument را تغییر ندهید، رابطهی زیرنوعسازی بین نوعها حفظ میشود.
اکنون فرض کنید میخواهیم Interface لیست خودمان را PayloadList تعریف کنیم که مقدار اختیاری از نوع Generic P را با هر عنصر مرتبط کند. اعلان آن ممکن است این شکلی باشد:
interface PayloadList<E,P> extends List<E> {
void setPayload(int index, P val);
...
}
پارامتریسازیهای زیر از PayloadList زیرنوع List<String> هستند:
PayloadList<String,String>PayloadList<String,Integer>PayloadList<String,Exception>
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://dev.java/learn/