JavaRush /وبلاگ جاوا /Random-FA /تئوری ژنریک در جاوا یا نحوه قرار دادن پرانتز در عمل
Viacheslav
مرحله

تئوری ژنریک در جاوا یا نحوه قرار دادن پرانتز در عمل

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

معرفی

با شروع JSE 5.0، ژنریک ها به زرادخانه زبان جاوا اضافه شدند.
تئوری ژنریک در جاوا یا نحوه قرار دادن پرانتز در عمل - 1

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

ژنریک ها (تعمیم ها) ابزارهای ویژه زبان جاوا برای اجرای برنامه نویسی تعمیم یافته هستند: یک رویکرد ویژه برای توصیف داده ها و الگوریتم ها که به شما امکان می دهد با انواع مختلف داده ها بدون تغییر توضیحات آنها کار کنید. در وب سایت اوراکل، یک آموزش جداگانه به ژنریک اختصاص داده شده است: " درس: ژنریک ".

ابتدا، برای درک ژنریک ها، باید بدانید که چرا اصلاً به آنها نیاز است و چه چیزی را ارائه می دهند. در آموزش در بخش " چرا از Generics استفاده کنیم ؟" گفته می شود که یکی از اهداف بررسی قوی تر نوع کامپایل و رفع نیاز به ریخته گری صریح است.
تئوری ژنریک در جاوا یا نحوه قرار دادن پرانتز در عمل - 2
بیایید آموزش های مورد علاقه خود را به کامپایلر آنلاین جاوا برای آزمایش ها آماده کنیم . بیایید این کد را تصور کنیم:
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 مسئله این است که در مورد ما List لیستی از اشیاء از نوع Object را ذخیره می کند. از آنجایی که 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);
		}
	}
}
با این حال، در این مورد، به دلیل لیست لیستی از اشیاء را می پذیرد، نه تنها رشته، بلکه عدد صحیح را نیز ذخیره می کند. اما بدترین چیز این است که در این حالت کامپایلر هیچ مشکلی نخواهد دید. و در اینجا ما یک خطا در حین اجرا دریافت خواهیم کرد (آنها همچنین می گویند که خطا "در زمان اجرا" دریافت شده است). خطا این خواهد بود: 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);
		}
	}
}
همانطور که می بینیم، دیگر نیازی به بازیگران برای String نداریم. علاوه بر این، اکنون براکت های زاویه ای داریم که کلیات را قاب می کنند. اکنون کامپایلر اجازه کامپایل کلاس را نمی دهد تا زمانی که اضافه شدن 123 را به لیست حذف نکنیم، زیرا این عدد صحیح است. او به ما چنین خواهد گفت. بسیاری از مردم ژنریک ها را "قند نحوی" می نامند. و آنها درست می‌گویند، زیرا ژنریک‌ها واقعاً به همان کاست‌ها تبدیل می‌شوند. بیایید به بایت کد کلاس های کامپایل شده نگاه کنیم: با ریخته گری دستی و استفاده از ژنریک:
تئوری ژنریک در جاوا یا نحوه قرار دادن پرانتز در عمل - 3
پس از جمع آوری، هر گونه اطلاعات در مورد ژنریک پاک می شود. این "پاک کردن نوع" یا " پاک کردن نوع " نامیده می شود. پاک کردن تایپ و ژنریک ها به گونه ای طراحی شده اند که با نسخه های قدیمی JDK سازگاری به عقب را ارائه دهند، در حالی که همچنان به کامپایلر اجازه می دهد تا به استنتاج نوع در نسخه های جدیدتر جاوا کمک کند.
تئوری ژنریک در جاوا یا نحوه قرار دادن پرانتز در عمل - 4

انواع خام یا انواع خام

وقتی در مورد ژنریک صحبت می کنیم، همیشه دو دسته داریم: انواع تایپ شده (انواع عمومی) و انواع خام (نوع های خام). انواع خام بدون مشخص کردن «صلاحیت» در براکت های زاویه ای انواعی هستند:
تئوری ژنریک در جاوا یا نحوه قرار دادن پرانتز در عمل - 5
انواع تایپ شده برعکس هستند، با علامت "روشن کردن":
تئوری ژنریک در جاوا یا نحوه قرار دادن پرانتز در عمل - 6
همانطور که می بینیم، ما از یک طراحی غیر معمول استفاده کردیم که با یک فلش در تصویر مشخص شده است. این یک نحو خاص است که در جاوا 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("unchecked") Suppress به عنوان suppress ترجمه می‌شود، یعنی به معنای واقعی کلمه، برای سرکوب هشدارها. اما به این فکر کنید که چرا تصمیم گرفتید آن را نشان دهید؟ قانون شماره یک را به خاطر بسپارید و شاید لازم باشد تایپ را اضافه کنید.
تئوری ژنریک در جاوا یا نحوه قرار دادن پرانتز در عمل - 7

روش های عمومی

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 نگاه کنید، دو متد تایپ شده در آن می بینیم. با استنتاج نوع می‌توانیم تعریف نوع را مستقیماً در اختیار کامپایلر قرار دهیم یا خودمان آن را مشخص کنیم. هر دو گزینه در مثال ارائه شده است. به هر حال، اگر در مورد آن فکر کنید، نحو کاملاً منطقی است. هنگام تایپ یک متد، ما قبل از متد، عمومی را مشخص می کنیم زیرا اگر بعد از متد از متد عمومی استفاده کنیم، جاوا نمی تواند تشخیص دهد که از کدام نوع استفاده کند. بنابراین ابتدا اعلام می کنیم که از ژنریک T استفاده می کنیم و سپس می گوییم که قرار است این ژنریک را برگردانیم. طبیعتاً Util.<Integer>getValue(element, String.class)با یک خطا از کار می افتد incompatible types: Class<String> cannot be converted to 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);
		}
    }
}
عالی کار خواهد کرد. اما فقط تا زمانی که کامپایلر بفهمد که متد فراخوانی شده دارای یک نوع Integer است. اجازه دهید خروجی کنسول را با خط زیر جایگزین کنیم: System.out.println(Util.getValue(element) + 1); و با خطا مواجه می‌شویم: نوع عملوند بد برای عملگر باینری '+'، نوع اول: Object، نوع دوم: int یعنی انواع پاک شده‌اند. کامپایلر می بیند که هیچ کس نوع را مشخص نکرده است، نوع به عنوان Object مشخص می شود و اجرای کد با خطا انجام نمی شود.
Теория дженериков в Java or How на практике ставить скобки - 8

انواع ژنریک

شما می توانید نه تنها متدها، بلکه خود کلاس ها را نیز تایپ کنید. اوراکل یک بخش « انواع عمومی » در راهنمای خود دارد. بیایید به یک مثال نگاه کنیم:
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);
		}
	}
}
اینجا همه چیز ساده است. اگر از یک کلاس استفاده کنیم، عمومی بعد از نام کلاس لیست می شود. حال بیایید یک نمونه از این کلاس را در متد main ایجاد کنیم:
public static void main(String []args) {
	SomeType<String> st = new SomeType<>();
	List<String> list = Arrays.asList("test");
	st.test(list);
}
به خوبی کار خواهد کرد. کامپایلر می بیند که لیستی از اعداد و مجموعه ای از نوع String وجود دارد. اما چه می شود اگر کلیات را پاک کنیم و این کار را انجام دهیم:
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 را به عنوان ورودی بپذیرد. این ویژگی در آموزش Oracle در بخش Bounded Type Parameters توضیح داده شده است . بیایید به یک مثال نگاه کنیم:
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");
    }
}
همانطور که می بینید، ما نوع عمومی را به کلاس/رابط Number و فرزندان آن محدود کرده ایم. جالب اینجاست که می توانید نه تنها یک کلاس، بلکه رابط ها را نیز مشخص کنید. به عنوان مثال: public static class NumberContainer<T extends Number & Comparable> { Generics همچنین دارای مفهوم Wildcard هستند https://docs.oracle.com/javase/tutorial/java/generics/wildcards.html آنها به نوبه خود به سه نوع تقسیم می شوند: به اصطلاح اصل Get Put برای Wildcards اعمال می شود . آنها را می توان به شکل زیر بیان کرد:
Теория дженериков в Java or How на практике ставить скобки - 9
این اصل را اصل PECS (Producer Extends Consumer Super) نیز می نامند. می‌توانید در مقاله « استفاده از حروف عام عمومی برای بهبود قابلیت استفاده از Java API » و همچنین در بحث عالی در مورد stackoverflow: « استفاده از حروف عام در Generics Java » اطلاعات بیشتری در مورد Habré بخوانید. در اینجا یک مثال کوچک از منبع جاوا - روش Collections.copy آورده شده است:
Теория дженериков в Java or How на практике ставить скобки - 10
خوب، یک مثال کوچک از اینکه چگونه کار نخواهد کرد:
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);
}
اما اگر اکستنشن را با super جایگزین کنید، همه چیز درست می شود. از آنجایی که ما لیست را با مقداری قبل از خروجی پر می کنیم، برای ما یک مصرف کننده است، یعنی یک مصرف کننده. بنابراین، ما از super استفاده می کنیم.

وراثت

یکی دیگر از ویژگی های غیر معمول ژنریک ها وجود دارد - وراثت آنها. وراثت ژنریک ها در آموزش Oracle در بخش " عمومی، وراثت و انواع فرعی " توضیح داده شده است. نکته اصلی این است که موارد زیر را به خاطر بسپارید و متوجه شوید. ما نمی توانیم این کار را انجام دهیم:
List<CharSequence> list1 = new ArrayList<String>();
زیرا وراثت با ژنریک ها متفاوت عمل می کند:
Теория дженериков в Java or How на практике ставить скобки - 11
و در اینجا یک مثال خوب دیگر است که با یک خطا شکست می خورد:
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