CodeGym /وبلاگ جاوا /Random-FA /ژنریک در جاوا
John Squirrels
مرحله
San Francisco

ژنریک در جاوا

در گروه منتشر شد
سلام! ما در مورد Java Generics صحبت خواهیم کرد. باید بگویم که چیزهای زیادی یاد خواهید گرفت! نه تنها این درس، بلکه چند درس بعدی نیز به ژنریک اختصاص داده خواهد شد. بنابراین، اگر به ژنریک ها علاقه دارید، امروز روز خوش شانسی هستید: در مورد ویژگی های ژنریک ها چیزهای زیادی یاد خواهید گرفت. و اگر نه، استعفا دهید و استراحت کنید! :) این موضوع بسیار مهمی است و شما باید آن را بدانید. بیایید با ساده شروع کنیم: "چی" و "چرا".

Java Generics چیست؟

ژنریک ها انواعی هستند که دارای یک پارامتر هستند. هنگام ایجاد یک نوع عمومی، شما نه تنها یک نوع، بلکه نوع داده ای که با آن کار خواهد کرد را نیز مشخص می کنید. من حدس می زنم واضح ترین مثال قبلاً به ذهن شما رسیده است: ArrayList! به این صورت است که ما معمولاً در یک برنامه یکی را ایجاد می کنیم:

import java.util.ArrayList;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<String> myList1 = new ArrayList<>();
       myList1.add("Test String 1");
       myList1.add("Test String 2");
   }
}
همانطور که ممکن است حدس بزنید، یکی از ویژگی های این لیست این است که ما نمی توانیم همه چیز را در آن قرار دهیم: منحصراً با اشیاء String کار می کند . حال بیایید کمی به تاریخچه جاوا انحراف داشته باشیم و سعی کنیم به این سوال پاسخ دهیم "چرا؟" برای انجام این کار، ما نسخه ساده شده خود از کلاس ArrayList را می نویسیم. لیست ما فقط می داند چگونه داده ها را به یک آرایه داخلی اضافه کرده و داده ها را از آن بازیابی کند:

public class MyListClass {

   private Object[] data;
   private int count;

   public MyListClass() {
       this.data = new Object[10];
       this.count = 0;
   }

   public void add(Object o) {
       this.data[count] = o;
       count++;
   }

   public Object[] getData() {
       return data;
   }
}
فرض کنید می خواهیم لیست ما فقط عدد صحیح را ذخیره کند . ما از یک نوع عمومی استفاده نمی کنیم. ما نمی خواهیم یک چک صریح "instanceof Integer " را در متد add() قرار دهیم . اگر این کار را می‌کردیم، کل کلاس ما فقط برای Integer مناسب بود و باید برای هر نوع داده دیگری در جهان یک کلاس مشابه بنویسیم! ما به برنامه نویسان خود تکیه می کنیم و فقط یک نظر در کد بگذارید تا اطمینان حاصل کنیم که چیزی را که ما نمی خواهیم اضافه نمی کنند:

// Use this class ONLY with the Integer data type
public void add(Object o) {
   this.data[count] = o;
   count++;
}
یکی از برنامه نویسان این نظر را از دست داد و به طور ناخواسته چندین رشته را در لیستی از اعداد قرار داد و سپس مجموع آنها را محاسبه کرد:

public class Main {

   public static void main(String[] args) {

       MyListClass list = new MyListClass();
       list.add(100);
       list.add(200);
       list.add("Lolkek");
       list.add("Shalala");

       Integer sum1 = (Integer) list.getData()[0] + (Integer) list.getData()[1];
       System.out.println(sum1);

       Integer sum2 = (Integer) list.getData()[2] + (Integer) list.getData()[3];
       System.out.println(sum2);
   }
}
خروجی کنسول:

300 
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer 
      at Main.main (Main.java:14)
بدترین قسمت این وضعیت چیست؟ مطمئناً بی دقتی برنامه نویس نیست. بدترین بخش این است که کد نادرست در یک مکان مهم در برنامه ما قرار گرفت و با موفقیت کامپایل شد. اکنون نه در هنگام نوشتن کد، بلکه فقط در حین آزمایش با این باگ مواجه خواهیم شد (و این بهترین حالت است!). رفع اشکالات در مراحل بعدی توسعه هزینه بسیار بیشتری دارد - هم از نظر پول و هم از نظر زمان. این دقیقاً جایی است که ژنریک ها به نفع ما هستند: یک کلاس عمومی به برنامه نویس بدشانس اجازه می دهد فورا خطا را تشخیص دهد. برنامه به سادگی کامپایل نمی شود!

import java.util.ArrayList;
import java.util.List;

public class Main {

   public static void main(String[] args) {

       List<Integer> myList1 = new ArrayList<>();
      
       myList1.add(100);
       myList1.add(100);
       myList1.add ("Lolkek"); // Error!
       myList1.add("Shalala"); // Error!
   }
}
برنامه نویس بلافاصله متوجه اشتباه خود می شود و فوراً بهتر می شود. به هر حال، ما مجبور نبودیم کلاس List خودمان را ایجاد کنیم تا این نوع خطا را ببینیم. به سادگی براکت های زاویه را بردارید و ( <Integer> ) را از یک ArrayList معمولی تایپ کنید!

import java.util.ArrayList;
import java.util.List;

public class Main {

   public static void main(String[] args) {

      List list = new ArrayList();

      list.add(100);
      list.add(200);
      list.add("Lolkek");
      list.add("Shalala");

       System.out.println((Integer) list.get(0) + (Integer) list.get(1));
       System.out.println((Integer) list.get(2) + (Integer) list.get(3));
   }
}
خروجی کنسول:

300 
Exception in thread "main" java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer 
     at Main.main(Main.java:16)
به عبارت دیگر، حتی با استفاده از مکانیسم‌های «بومی» جاوا، می‌توانیم این نوع اشتباهات را مرتکب شویم و مجموعه‌ای ناامن ایجاد کنیم. با این حال، اگر این کد را در یک IDE جایگذاری کنیم، یک اخطار دریافت می کنیم: "تماس بدون علامت برای افزودن(E) به عنوان عضوی از نوع خام java.util.List" به ما گفته می شود که ممکن است هنگام افزودن یک مورد مشکلی پیش بیاید. به مجموعه ای که فاقد نوع عمومی است. اما عبارت «نوع خام» به چه معناست؟ نوع خام یک کلاس عمومی است که نوع آن حذف شده است. به عبارت دیگر، List myList1 یک نوع خام است . متضاد نوع خام یک نوع عمومی است - یک کلاس عمومی با نشانی از نوع(های) پارامتر شده . به عنوان مثال، List<String> myList1 . ممکن است بپرسید چرا زبان اجازه استفاده از انواع خام را می دهد ؟ دلیلش هم ساده است. سازندگان جاوا برای جلوگیری از ایجاد مشکلات سازگاری، پشتیبانی از انواع خام در زبان را ترک کردند. تا زمانی که جاوا 5.0 منتشر شد (نسخه های عمومی برای اولین بار در این نسخه ظاهر شد)، بسیاری از کدها قبلاً با استفاده از انواع خام نوشته شده بودند . در نتیجه، این مکانیسم هنوز هم پشتیبانی می شود. بارها در درس ها به کتاب کلاسیک جاشوا بلوخ «جاوای مؤثر» اشاره کرده ایم. او به عنوان یکی از پدیدآورندگان زبان، در کتاب خود از انواع خام و انواع عمومی چشم پوشی نکرد. ژنریک در جاوا چیست؟  - 2فصل 23 کتاب یک عنوان بسیار شیوا دارد: "از انواع خام در کدهای جدید استفاده نکنید" این چیزی است که باید به خاطر بسپارید. هنگام استفاده از کلاس های عمومی، هرگز یک نوع عمومی را به نوع خام تبدیل نکنید .

روش های عمومی

جاوا به شما امکان می دهد تا با ایجاد روش های به اصطلاح عمومی، روش های فردی را پارامتر کنید. چنین روش هایی چگونه مفید هستند؟ مهمتر از همه، آنها از این جهت مفید هستند که به شما اجازه می دهند با انواع مختلف پارامترهای متد کار کنید. اگر منطق یکسانی را بتوان با خیال راحت برای انواع مختلف اعمال کرد، یک روش عمومی می تواند راه حل عالی باشد. این را یک مثال بسیار ساده در نظر بگیرید: فرض کنید فهرستی به نام myList1 داریم . می خواهیم همه مقادیر را از لیست حذف کنیم و تمام فضاهای خالی را با مقادیر جدید پر کنیم. در اینجا کلاس ما با یک روش عمومی به نظر می رسد:

public class TestClass {

   public static <T> void fill(List<T> list, T val) {
       for (int i = 0; i < list.size(); i++)
           list.set(i, val);
   }

   public static void main(String[] args) {

       List<String> strings = new ArrayList<>();
       strings.add("Old String 1");
       strings.add("Old String 2");
       strings.add("Old String 3");

       fill(strings, "New String");

       System.out.println(strings);

       List<Integer> numbers = new ArrayList<>();
       numbers.add(1);
       numbers.add(2);
       numbers.add(3);

       fill(numbers, 888);
       System.out.println(numbers);
   }
}
به نحو دقت کنید. کمی غیر معمول به نظر می رسد:

public static <T> void fill(List<T> list, T val)
قبل از نوع بازگشتی <T> را می نویسیم. این نشان می دهد که ما با یک روش عمومی روبرو هستیم. در این حالت، متد 2 پارامتر را به عنوان ورودی می پذیرد: یک لیست از اشیاء T و یک شی T جداگانه دیگر. با استفاده از <T>، انواع پارامترهای متد را پارامتر می کنیم: نمی توانیم در لیستی از رشته ها و یک عدد صحیح عبور دهیم. یک لیست از رشته ها و یک رشته، یک لیست از اعداد صحیح و یک عدد صحیح، یک لیست از اشیاء Cat خودمان و یک شی Cat دیگر - این کاری است که ما باید انجام دهیم. متد ()main نشان می دهد که چگونه می توان از متد fill() به راحتی برای کار با انواع مختلف داده استفاده کرد. ابتدا از روش با لیستی از رشته ها و یک رشته به عنوان ورودی و سپس با لیستی از اعداد صحیح و یک عدد صحیح استفاده می کنیم. خروجی کنسول:

[New String, New String, New String] [888, 888, 888]
تصور کنید که متدهای عمومی نداشتیم و به منطق متد fill() برای 30 کلاس مختلف نیاز داشتیم. ما باید یک روش را 30 بار برای انواع داده های مختلف بنویسیم! اما به لطف روش‌های عمومی، می‌توانیم از کد خود دوباره استفاده کنیم! :)

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

شما محدود به کلاس های عمومی ارائه شده در کتابخانه های استاندارد جاوا نیستید - می توانید کلاس های خود را ایجاد کنید! در اینجا یک مثال ساده آورده شده است:

public class Box<T> {

   private T t;

   public void set(T t) {
       this.t = t;
   }

   public T get() {
       return t;
   }

   public static void main(String[] args) {

       Box<String> stringBox = new Box<>();

       stringBox.set("Old String");
       System.out.println(stringBox.get());
       stringBox.set("New String");

       System.out.println(stringBox.get());
      
       stringBox.set(12345); // Compilation error!
   }
}
کلاس Box<T> ما یک کلاس عمومی است. هنگامی که یک نوع داده ( <T> ) را در حین ایجاد اختصاص می دهیم ، دیگر نمی توانیم اشیایی از انواع دیگر را در آن قرار دهیم. این را می توان در مثال مشاهده کرد. هنگام ایجاد شی ما، نشان دادیم که با رشته ها کار می کند:

Box<String> stringBox = new Box<>();
و در خط آخر کد وقتی می خواهیم عدد 12345 را داخل کادر قرار دهیم با خطای کامپایل مواجه می شویم! به همین راحتی! ما کلاس عمومی خود را ایجاد کرده ایم! :) با آن، درس امروز به پایان می رسد. اما ما با ژنریک ها خداحافظی نمی کنیم! در درس‌های بعدی، در مورد ویژگی‌های پیشرفته‌تر صحبت خواهیم کرد، پس دور نشوید! ) برای تقویت آموخته هایتان، پیشنهاد می کنیم یک درس ویدیویی از دوره جاوا ما تماشا کنید
بهترین موفقیت در تحصیل :)
نظرات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION