CodeGym /وبلاگ جاوا /Random-FA /Java Generics: نحوه استفاده از براکت های زاویه دار در عمل...
John Squirrels
مرحله
San Francisco

Java Generics: نحوه استفاده از براکت های زاویه دار در عمل

در گروه منتشر شد

معرفی

با شروع JSE 5.0، ژنریک ها به زرادخانه زبان جاوا اضافه شدند.

ژنریک در جاوا چیست؟

ژنریک ها مکانیسم ویژه جاوا برای اجرای برنامه نویسی عمومی هستند - راهی برای توصیف داده ها و الگوریتم ها که به شما امکان می دهد بدون تغییر در توضیحات الگوریتم ها با انواع داده های مختلف کار کنید. وب سایت اوراکل دارای یک آموزش جداگانه است که به ژنریک اختصاص دارد: " درس ". برای درک ژنریک ها، ابتدا باید بفهمید که چرا آنها مورد نیاز هستند و چه چیزی ارائه می دهند. بخش " چرا از Generics استفاده می شود؟ " از آموزش می گوید که چند هدف بررسی نوع قوی تر در زمان کامپایل و حذف نیاز به بازیگران صریح است. بیایید برای چند تست در کامپایلر آنلاین جاوا Tutorialspoint ژنریک در جاوا: نحوه استفاده از براکت های زاویه دار در عمل - 1 عزیزمان آماده شویم . فرض کنید کد زیر را دارید:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello");
		String text = list.get(0) + ", world!";
		System.out.print(text);
	}
}
این کد به خوبی اجرا خواهد شد. اما اگر رئیس به سراغ ما بیاید و بگوید که "سلام، دنیا!" عبارتی است که بیش از حد استفاده شده است و باید فقط "سلام" را برگردانید؟ ما کدی را که ", world!" این به اندازه کافی بی ضرر به نظر می رسد، درست است؟ اما ما در واقع یک خطا در زمان کامپایل دریافت می کنیم:

error: incompatible types: Object cannot be converted to String
مشکل این است که در لیست ما Objects را ذخیره می کند. String از نسل Object است (از آنجایی که همه کلاس‌های جاوا به طور ضمنی Object را به ارث می‌برند )، به این معنی که ما به یک Cast واضح نیاز داریم، اما یکی را اضافه نکردیم. در طول عملیات الحاق، متد String.valueOf(obj) ثابت با استفاده از شی فراخوانی می شود. در نهایت متد toString کلاس Object را فراخوانی می کند . به عبارت دیگر، لیست ما حاوی یک شی است . این بدان معنی است که هر جا که به یک نوع خاص نیاز داشته باشیم (نه Object )، باید خودمان تبدیل نوع را انجام دهیم:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List list = new ArrayList();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + (String)str);
		}
	}
}
با این حال، در این مورد، از آنجا که List اشیاء را می گیرد، می تواند نه تنها String s، بلکه اعداد صحیح را نیز ذخیره کند. اما بدترین چیز این است که کامپایلر هیچ مشکلی در اینجا نمی بیند. و اکنون یک خطا در زمان اجرا (معروف به "خطای زمان اجرا") دریافت خواهیم کرد. خطا خواهد بود:

java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
باید قبول کنید که این خیلی خوب نیست. و همه اینها به این دلیل است که کامپایلر یک هوش مصنوعی نیست که بتواند همیشه هدف برنامه نویس را به درستی حدس بزند. جاوا SE 5 ژنریک ها را معرفی کرد تا به ما اجازه دهد به کامپایلر در مورد مقاصد خود بگوییم – در مورد انواعی که قرار است استفاده کنیم. ما کد خود را با گفتن آنچه می خواهیم به کامپایلر اصلاح می کنیم:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList<>();
		list.add("Hello!");
		list.add(123);
		for (Object str : list) {
		    System.out.println("-" + str);
		}
	}
}
همانطور که می بینید، ما دیگر نیازی به بازیگری برای یک رشته نداریم . علاوه بر این، ما براکت های زاویه ای داریم که آرگومان نوع را احاطه کرده اند. اکنون کامپایلر به ما اجازه نمی دهد کلاس را کامپایل کنیم تا زمانی که خطی را که 123 را به لیست اضافه می کند حذف نکنیم، زیرا این یک عدد صحیح است . و این را به ما خواهد گفت. بسیاری از مردم ژنریک ها را "قند نحوی" می نامند. و حق با آنهاست، زیرا پس از کامپایل شدن ژنریک ها، آنها واقعاً تبدیل به همان نوع می شوند. بیایید به بایت کد کلاس های کامپایل شده نگاهی بیندازیم: یکی که از یک Cast صریح استفاده می کند و دیگری که از ژنریک استفاده می کند: ژنریک در جاوا: نحوه استفاده از براکت های زاویه دار در عمل - 2پس از کامپایل، همه ژنریک ها پاک می شوند. به این " پاک کردن نوع " می گویند. پاک کردن تایپ و ژنریک به گونه ای طراحی شده اند که با نسخه های قدیمی JDK سازگار باشند و به طور همزمان به کامپایلر اجازه می دهند تا به تعاریف نوع در نسخه های جدید جاوا کمک کند.

انواع خام

صحبت از ژنریک ها، ما همیشه دو دسته داریم: انواع پارامتری و انواع خام. انواع خام، انواعی هستند که «روشن‌سازی نوع» را در براکت‌های زاویه حذف می‌کنند: ژنریک در جاوا: نحوه استفاده از براکت های زاویه دار در عمل - 3انواع پارامتری، روی دست، شامل «روشن‌سازی» می‌شوند: ژنریک در جاوا: نحوه استفاده از براکت های زاویه دار در عمل - 4همانطور که می‌بینید، ما از یک ساختار غیرمعمول استفاده کردیم که با یک فلش در تصویر مشخص شده است. این نحو خاصی است که به جاوا SE 7 اضافه شده است. به آن " الماس " می گویند. چرا؟ براکت های زاویه یک الماس را تشکیل می دهند: <> . همچنین باید بدانید که نحو الماس با مفهوم " استنتاج نوع " مرتبط است. از این گذشته، کامپایلر با دیدن <> در سمت راست، به سمت چپ عملگر انتساب نگاه می کند، جایی که نوع متغیری را که مقدار آن اختصاص داده می شود را پیدا می کند. بر اساس آنچه در این قسمت پیدا می کند، نوع مقدار سمت راست را درک می کند. در واقع، اگر یک نوع عمومی در سمت چپ داده شود، اما در سمت راست نباشد، کامپایلر می تواند نوع را استنباط کند:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = new ArrayList();
		list.add("Hello, World");
		String data = list.get(0);
		System.out.println(data);
	}
}
اما این سبک جدید را با ژنریک و سبک قدیمی بدون آنها مخلوط می کند. و این بسیار نامطلوب است. هنگام کامپایل کد بالا، پیام زیر را دریافت می کنیم:

Note: HelloWorld.java uses unchecked or unsafe operations
در واقع، دلیل اینکه شما حتی نیاز به اضافه کردن یک الماس در اینجا دارید غیرقابل درک به نظر می رسد. اما این یک مثال است:
import java.util.*;
public class HelloWorld {
	public static void main(String []args) {
		List<String> list = Arrays.asList("Hello", "World");
		List<Integer> data = new ArrayList(list);
		Integer intNumber = data.get(0);
		System.out.println(data);
	}
}
به یاد دارید که ArrayList سازنده دومی دارد که مجموعه ای را به عنوان آرگومان می گیرد. و اینجاست که چیزی شوم پنهان است. بدون نحو الماس، کامپایلر نمی فهمد که فریب خورده است. با نحو الماس، این کار را می کند. بنابراین، قانون شماره 1 این است: همیشه از نحو الماس با انواع پارامتری استفاده کنید. در غیر این صورت، خطر از دست دادن جایی که در حال استفاده از انواع خام هستیم وجود دارد. برای حذف اخطارهای "استفاده از عملیات چک نشده یا ناامن"، می‌توانیم از حاشیه‌نویسی @SuppressWarnings ("بررسی نشده") در یک متد یا کلاس استفاده کنیم. اما به این فکر کنید که چرا تصمیم به استفاده از آن گرفته اید. قانون شماره یک را به خاطر بسپارید. شاید لازم باشد یک آرگومان نوع اضافه کنید.

روش های عمومی جاوا

Generics به شما امکان می دهد متدهایی ایجاد کنید که نوع پارامتر و نوع بازگشتی آنها پارامتری باشد. بخش جداگانه ای به این قابلیت در آموزش اوراکل اختصاص داده شده است: " روش های عمومی ". مهم است که نحو آموزش داده شده در این آموزش را به خاطر بسپارید:
  • این شامل لیستی از پارامترهای نوع در داخل براکت های زاویه است.
  • لیست پارامترهای نوع قبل از نوع برگشتی متد قرار می گیرد.
بیایید به یک مثال نگاه کنیم:
import java.util.*;
public class HelloWorld {

    public static class Util {
        public static <T> T getValue(Object obj, Class<T> clazz) {
            return (T) obj;
        }
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList("Author", "Book");
		for (Object element : list) {
		    String data = Util.getValue(element, String.class);
		    System.out.println(data);
		    System.out.println(Util.<String>getValue(element));
		}
    }
}
اگر به کلاس Util نگاه کنید ، خواهید دید که دارای دو روش عمومی است. به لطف امکان استنتاج نوع، می‌توانیم نوع را مستقیماً به کامپایلر نشان دهیم یا خودمان آن را مشخص کنیم. هر دو گزینه در مثال ارائه شده است. به هر حال، اگر در مورد آن فکر کنید، نحو بسیار منطقی است. هنگام اعلان یک متد عمومی، پارامتر نوع را قبل از متد مشخص می کنیم، زیرا اگر پارامتر نوع را بعد از متد اعلام کنیم، JVM نمی تواند تشخیص دهد که از کدام نوع استفاده کند. بر این اساس ابتدا اعلام می کنیم که از پارامتر نوع T استفاده خواهیم کرد و سپس می گوییم که قرار است این نوع را برگردانیم. به طور طبیعی، Util.<Integer>getValue(element, String.class) با یک خطا شکست می خورد: انواع ناسازگار: Class<String> را نمی توان به Class<Integer> تبدیل کرد . هنگام استفاده از روش های عمومی، همیشه باید پاک کردن نوع را به خاطر بسپارید. بیایید به یک مثال نگاه کنیم:
import java.util.*;
public class HelloWorld {

    public static class Util {
        public static <T> T getValue(Object obj) {
            return (T) obj;
        }
    }

    public static void main(String []args) {
		List list = Arrays.asList(2, 3);
		for (Object element : list) {
		    System.out.println(Util.<Integer>getValue(element) + 1);
		}
    }
}
این به خوبی اجرا خواهد شد. اما فقط تا زمانی که کامپایلر بفهمد که نوع برگشتی متدی که فراخوانی می شود عدد صحیح است . عبارت خروجی کنسول را با خط زیر جایگزین کنید:
System.out.println(Util.getValue(element) + 1);
یک خطا دریافت می کنیم:

bad operand types for binary operator '+', first type: Object, second type: int.
به عبارت دیگر پاک کردن نوع رخ داده است. کامپایلر می بیند که هیچ کس نوع را مشخص نکرده است، بنابراین نوع به عنوان Object نشان داده می شود و متد با یک خطا از کار می افتد.

کلاس های عمومی

نه تنها روش ها را می توان پارامتر کرد. کلاس ها نیز می توانند. بخش «انواع عمومی» از آموزش اوراکل به این موضوع اختصاص دارد. بیایید یک مثال را در نظر بگیریم:
public static class SomeType<T> {
	public <E> void test(Collection<E> collection) {
		for (E element : collection) {
			System.out.println(element);
		}
	}
	public void test(List<Integer> collection) {
		for (Integer element : collection) {
			System.out.println(element);
		}
	}
}
اینجا همه چیز ساده است. اگر از کلاس عمومی استفاده کنیم، پارامتر type بعد از نام کلاس نشان داده می شود. حالا بیایید یک نمونه از این کلاس را در متد main ایجاد کنیم :
public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
این کد به خوبی اجرا خواهد شد. کامپایلر می بیند که لیستی از اعداد و مجموعه ای از رشته ها وجود دارد . اما چه می شود اگر پارامتر type را حذف کنیم و این کار را انجام دهیم:
SomeType st = new SomeType();
List<String> list = Arrays.asList("test");
st.test(list);
یک خطا دریافت می کنیم:

java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
باز هم، این پاک کردن نوع است. از آنجایی که کلاس دیگر از پارامتر نوع استفاده نمی کند، کامپایلر تصمیم می گیرد که از آنجایی که ما یک List را ارسال کردیم ، متد با List<Integer> مناسب ترین است. و با یک خطا شکست می خوریم. بنابراین، قانون شماره 2 را داریم: اگر یک کلاس عمومی دارید، همیشه پارامترهای نوع را مشخص کنید.

محدودیت های

ما می توانیم انواع مشخص شده در متدهای عمومی و کلاس ها را محدود کنیم. به عنوان مثال، فرض کنید می‌خواهیم یک ظرف فقط یک Number را به عنوان آرگومان نوع بپذیرد . این ویژگی در بخش Bounded Type Parameters در آموزش Oracle توضیح داده شده است. بیایید به یک مثال نگاه کنیم:
import java.util.*;
public class HelloWorld {

    public static class NumberContainer<T extends Number> {
        private T number;

        public NumberContainer(T number) { this.number = number; }

        public void print() {
            System.out.println(number);
        }
    }

    public static void main(String []args) {
		NumberContainer number1 = new NumberContainer(2L);
		NumberContainer number2 = new NumberContainer(1);
		NumberContainer number3 = new NumberContainer("f");
    }
}
همانطور که می بینید، ما پارامتر type را به کلاس/اینترفیس Number یا فرزندان آن محدود کرده ایم. توجه داشته باشید که می توانید نه تنها یک کلاس، بلکه رابط ها را نیز مشخص کنید. مثلا:
public static class NumberContainer<T extends Number & Comparable> {
ژنریک ها همچنین از حروف عام پشتیبانی می کنند. آنها به سه نوع تقسیم می شوند: استفاده شما از حروف عام باید از اصل Get-Put پیروی کند . می توان آن را به صورت زیر بیان کرد:
  • زمانی که فقط مقادیری را از یک ساختار دریافت می‌کنید، از علامت عام استفاده کنید .
  • هنگامی که فقط مقادیر را در یک ساختار قرار می دهید از یک علامت عام فوق العاده استفاده کنید.
  • و زمانی که هر دوی شما می‌خواهید از/به یک ساختار بگیرید و قرار دهید، از علامت عام استفاده نکنید.
این اصل را اصل تولیدکننده گسترش مصرف کننده (PECS) نیز می نامند. در اینجا یک مثال کوچک از کد منبع برای روش Collections.copy جاوا آورده شده است : ژنریک در جاوا: نحوه استفاده از براکت های زاویه دار در عمل - 5و در اینجا یک مثال کوچک از آنچه کار نمی کند وجود دارد:
public static class TestClass {
	public static void print(List<? extends String> list) {
		list.add("Hello, World!");
		System.out.println(list.get(0));
	}
}

public static void main(String []args) {
	List<String> list = new ArrayList<>();
	TestClass.print(list);
}
اما اگر Extends را با super جایگزین کنید ، همه چیز خوب است. از آنجایی که ما لیست را قبل از نمایش محتوای آن با یک مقدار پر می کنیم، این یک مصرف کننده است . بر این اساس از super استفاده می کنیم.

وراثت

ژنریک ها یک ویژگی جالب دیگر دارند: وراثت. نحوه کار وراثت برای ژنریک ها در بخش " عمومی، وراثت و انواع فرعی " در آموزش Oracle توضیح داده شده است. نکته مهم این است که موارد زیر را به خاطر بسپارید و تشخیص دهید. ما نمی توانیم این کار را انجام دهیم:
List<CharSequence> list1 = new ArrayList<String>();
زیرا وراثت با ژنریک ها متفاوت عمل می کند: ژنریک در جاوا: نحوه استفاده از براکت های زاویه دار در عمل - 6و در اینجا یک مثال خوب دیگر وجود دارد که با یک خطا شکست می خورد:
List<String> list1 = new ArrayList<>();
List<Object> list2 = list1;
باز هم، همه چیز در اینجا ساده است. List<String> از نسل List<Object> نیست ، حتی اگر String از نسل Object باشد . برای تقویت آموخته هایتان، پیشنهاد می کنیم یک درس ویدیویی از دوره جاوا ما تماشا کنید
بنابراین ما حافظه خود را در مورد ژنریک ها تازه کرده ایم. اگر به ندرت از قابلیت های آنها استفاده کامل می کنید، برخی از جزئیات مبهم می شوند. امیدوارم این بررسی کوتاه به تقویت حافظه شما کمک کرده باشد. برای نتایج بهتر، اکیداً توصیه می کنم که با مطالب زیر آشنا شوید:
نظرات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION