این مقاله برای افرادی در نظر گرفته شده است که هرگز با Annotations کار نکردهاند، اما میخواهند بدانند که چیست و در چه مواردی از آن استفاده میشود. اگر در این زمینه تجربه دارید، فکر نمی کنم این مقاله به نوعی دانش شما را گسترش دهد (و در واقع، من چنین هدفی را دنبال نمی کنم). همچنین این مقاله برای کسانی که تازه شروع به یادگیری زبان جاوا کرده اند مناسب نیست. اگر نمیدانید Map<> یا HashMap<> چیست یا نمیدانید ورودی ثابت{ } در تعریف کلاس به چه معناست، یا هرگز با بازتاب کار نکردهاید، برای خواندن این مقاله خیلی زود است و سعی کنید بفهمید که حاشیه نویسی چیست. این ابزار به خودی خود برای استفاده توسط مبتدیان ایجاد نشده است، زیرا به درک نه کاملاً پایه ای از تعامل کلاس ها و اشیاء (نظر من) نیاز دارد (با تشکر از نظرات برای نشان دادن نیاز به این پس اسکریپت). پس بیایید شروع کنیم. حاشیه نویسی در جاوا نوعی برچسب در کد است که ابرداده یک تابع/کلاس/بسته را توصیف می کند. به عنوان مثال، @Override Annotation معروف، که نشان می دهد که ما قصد داریم متدی از کلاس والد را لغو کنیم. بله، از یک طرف بدون آن امکان پذیر است، اما اگر والدین این روش را نداشته باشند، این امکان وجود دارد که ما کد را بیهوده نوشتیم، زیرا این روش خاص ممکن است هرگز فراخوانی نشود، اما با @Override Annotation کامپایلر به ما می گوید: "من چنین روشی را در والدین پیدا نکردم... اینجا چیزی کثیف است." با این حال، حاشیهنویسیها میتوانند بیش از معنای «قابلیت اطمینان» را داشته باشند: آنها میتوانند برخی از دادهها را ذخیره کنند که بعداً مورد استفاده قرار خواهند گرفت.
ابتدا به ساده ترین حاشیه نویسی های ارائه شده توسط کتابخانه استاندارد نگاه می کنیم.
(با تشکر مجدد از نظرات، در ابتدا فکر نمی کردم که این بلوک مورد نیاز باشد) ابتدا، اجازه دهید در مورد اینکه حاشیه نویسی چیست بحث کنیم. هر یک از آنها دارای 2 پارامتر اصلی مورد نیاز است:- نوع ذخیره سازی (Retention);
- نوع شیئی که روی آن نشان داده شده است (هدف).
نوع ذخیره سازی
منظور ما از "نوع ذخیره سازی" مرحله ای است که حاشیه نویسی ما در داخل کلاس "بقا" می کند. هر حاشیه نویسی تنها دارای یکی از «انواع حفظ» ممکن است که در کلاس RetentionPolicy مشخص شده است :- منبع - حاشیه نویسی فقط هنگام نوشتن کد استفاده می شود و توسط کامپایلر نادیده گرفته می شود (یعنی پس از کامپایل ذخیره نمی شود). معمولاً برای هر پیش پردازشگر (مشروط) یا دستورالعمل کامپایلر استفاده می شود
- CLASS - حاشیه نویسی پس از کامپایل حفظ می شود، اما توسط JVM نادیده گرفته می شود (یعنی در زمان اجرا نمی توان از آن استفاده کرد). معمولاً برای هر سرویس شخص ثالثی که کد شما را به عنوان یک برنامه افزونه بارگیری می کند، استفاده می شود
- RUNTIME یک حاشیه نویسی است که پس از کامپایل ذخیره شده و توسط JVM بارگذاری می شود (یعنی می تواند در حین اجرای خود برنامه استفاده شود). به عنوان علامت هایی در کد استفاده می شود که مستقیماً بر اجرای برنامه تأثیر می گذارد (مثالی در این مقاله مورد بحث قرار خواهد گرفت)
نوع جسمی که در بالای آن مشخص شده است
این توصیف را باید تقریباً تحت اللفظی در نظر گرفت، زیرا ... در جاوا، حاشیه نویسی را می توان روی هر چیزی (فیلدها، کلاس ها، توابع، و غیره) مشخص کرد و برای هر حاشیه نویسی مشخص می شود که دقیقاً روی چه چیزی می تواند مشخص شود. در اینجا دیگر یک قانون "یک چیز" وجود ندارد؛ یک حاشیه نویسی را می توان بالای همه موارد ذکر شده در زیر مشخص کرد، یا می توانید فقط عناصر ضروری کلاس ElementType را انتخاب کنید :- ANNOTATION_TYPE - حاشیه نویسی دیگر
- CONSTRUCTOR - سازنده کلاس
- FIELD - فیلد کلاس
- LOCAL_VARIABLE - متغیر محلی
- روش - روش کلاس
- بسته - توضیحات بسته بسته
- PARAMETER - پارامتر متد public void hello(@Annontation String param){}
- TYPE - در بالای کلاس نشان داده شده است
@Override
حفظ: SOURCE; هدف: روش. این حاشیه نشان می دهد که متدی که روی آن نوشته شده است از کلاس والد به ارث برده شده است. اولین حاشیه نویسی که هر برنامه نویس تازه کار جاوا هنگام استفاده از یک IDE که به طور مداوم این @Override را فشار می دهد، با آن مواجه می شود. اغلب، معلمان یوتیوب توصیه میکنند: «آن را پاک کنید تا مزاحم نشود» یا: «بدون اینکه تعجب کنید چرا آنجاست، آن را رها کنید». در واقع، حاشیه نویسی بسیار مفید است: نه تنها به شما این امکان را می دهد که بفهمید کدام متدها برای اولین بار در این کلاس تعریف شده اند، و والدین قبلا کدام روش ها را دارند (که بدون شک خوانایی کد شما را افزایش می دهد)، بلکه این حاشیه نویسی را نیز درک کنید. به عنوان یک "خود بررسی" عمل می کند که هنگام تعریف یک تابع بارگذاری شده اشتباه نکرده اید.@منسوخ
حفظ: زمان اجرا. هدف: CONSTRUCTOR، FIELD، LOCAL_VARIABLE، METHOD، PACKAGE، PARAMETER، TYPE. این حاشیهنویسی روشها، کلاسها یا متغیرهایی را مشخص میکند که «منسوخ» هستند و ممکن است در نسخههای بعدی محصول حذف شوند. این حاشیهنویسی معمولاً توسط کسانی که اسناد هر API یا همان کتابخانه استاندارد جاوا را میخوانند، مواجه میشوند. گاهی اوقات این حاشیه نویسی نادیده گرفته می شود زیرا ... هیچ خطایی ایجاد نمی کند و اصولاً به خودی خود تداخل زیادی در زندگی ایجاد نمی کند. با این حال، پیام اصلی این حاشیه نویسی این است که "ما روش راحت تری برای اجرای این عملکرد ارائه کرده ایم، از آن استفاده کنید، از قدیمی استفاده نکنید" - خوب، یا در غیر این صورت - "ما تابع را تغییر نام دادیم، اما این چنین است، ما آن را به میراث گذاشتیم...» (که عموماً هم بد نیست). به طور خلاصه، اگر @Deprecated را میبینید، بهتر است سعی کنید از چیزی که روی آن آویزان است استفاده نکنید، مگر اینکه کاملاً ضروری باشد، و ممکن است ارزش آن را داشته باشد که اسناد را دوباره بخوانید تا بفهمید که چگونه وظیفه انجام شده توسط عنصر منسوخ شده اکنون اجرا میشود. به عنوان مثال، به جای استفاده از Date().getYear() جدید ، توصیه می شود از Calendar.getInstance().get(Calendar.YEAR) استفاده کنید .@SuppressWarnings
حفظ: SOURCE; هدف: TYPE، FIELD، METHOD، PARAMETER، CONSTRUCTOR، LOCAL_VARIABLE این حاشیه نویسی خروجی هشدارهای کامپایلر را که مربوط به عنصری است که روی آن مشخص شده است، غیرفعال می کند. آیا حاشیه نویسی SOURCE در بالا فیلدها، روش ها، کلاس ها نشان داده شده است.@نگهداری
حفظ: زمان اجرا. هدف: ANNOTATION_TYPE؛ این حاشیهنویسی «نوع ذخیرهسازی» حاشیهنویسی که در بالای آن مشخص شده است را مشخص میکند. بله، این حاشیه نویسی حتی برای خودش استفاده می شود... جادو و بس.@هدف
حفظ: زمان اجرا. هدف: ANNOTATION_TYPE؛ این حاشیهنویسی نوع شیئی را که میتوان روی آن حاشیهنویسی که ایجاد میکنیم نشان داد، مشخص میکند. بله، و برای خودتان هم استفاده می شود، به آن عادت کنید... فکر می کنم اینجا جایی است که می توانیم مقدمه خود را با حاشیه نویسی استاندارد کتابخانه جاوا کامل کنیم، زیرا بقیه به ندرت استفاده می شوند و اگرچه مزایای خاص خود را دارند، اما همه مجبور نیستند با آنها مقابله کنند و کاملاً غیر ضروری هستند. اگر می خواهید در مورد یک حاشیه نویسی خاص از کتابخانه استاندارد صحبت کنم (یا، شاید، حاشیه نویسی هایی مانند @NotNull و @Nullable که در STL گنجانده نشده اند)، در نظرات بنویسید - یا کاربران مهربان در آنجا به شما پاسخ خواهند داد، یا وقتی ببینمش میکنم اگر افراد زیادی درخواست نوعی حاشیه نویسی کنند، آن را نیز به مقاله اضافه می کنم.کاربرد عملی حاشیه نویسی RUNTIME
در واقع، من فکر می کنم این به اندازه کافی گپ نظری است: بیایید به تمرین با استفاده از مثال یک ربات ادامه دهیم. فرض کنید می خواهید یک ربات برای یک شبکه اجتماعی بنویسید. همه شبکه های بزرگ مانند VK، Facebook، Discord، API های مخصوص به خود را دارند که به شما امکان می دهد یک ربات بنویسید. برای همین شبکهها، از قبل کتابخانههای مکتوب برای کار با APIها، از جمله در جاوا، وجود دارد. بنابراین، ما به کار هیچ API یا کتابخانه ای نخواهیم پرداخت. تنها چیزی که در این مثال باید بدانیم این است که ربات ما می تواند به پیام های ارسال شده به چتی که ربات ما در آن واقع شده است پاسخ دهد. یعنی فرض کنید یک کلاس MessageListener با یک تابع داریم:public class MessageListener
{
public void onMessageReceived(MessageReceivedEvent event)
{
}
}
وظیفه پردازش پیام دریافتی را بر عهده دارد. تنها چیزی که از کلاس MessageReceivedEvent نیاز داریم رشته پیام دریافتی است (مثلاً "Hello" یا "Bot, hello"). شایان ذکر است: در کتابخانه های مختلف این کلاس ها به طور متفاوتی نامیده می شوند. من از کتابخانه برای Discord استفاده کردم. و بنابراین میخواهیم ربات را وادار کنیم تا به برخی از دستورات که با «ربات» شروع میشوند واکنش نشان دهد (با یا بدون کاما - خودتان تصمیم بگیرید: به خاطر این درس، فرض میکنیم که نباید در آنجا کاما وجود داشته باشد). یعنی تابع ما با چیزی شبیه به این شروع خواهد شد:
public void onMessageReceived(MessageReceivedEvent event)
{
//Убираем чувствительность к регистру (БоТ, бОт и т.д.)
String message = event.getMessage().toLowerCase();
if (message.startsWith("бот"))
{
}
}
و اکنون گزینه های زیادی برای اجرای این یا آن دستور داریم. بدون شک، ابتدا باید دستور را از آرگومان های آن جدا کنید، یعنی آن را به یک آرایه تقسیم کنید.
public void onMessageReceived(MessageReceivedEvent event)
{
//Убираем чувствительность к регистру (БоТ, бОт и т.д.)
String message = event.getMessage().toLowerCase();
if (message.startsWith("бот"))
{
try
{
//получим массив {"Бот", "(команду)", "аргумент1", "аргумент2",... "аргументN"};
String[] args = message.split(" ");
//Для удобства уберем "бот" и отделим команду от аргументов
String command = args[1];
String[] nArgs = Arrays.copyOfRange(args, 2, args.length);
//Получor command = "(команда)"; nArgs = {"аргумент1", "аргумент2",..."аргументN"};
//Данный массив может быть пустым
}
catch (ArrayIndexOutOfBoundsException e)
{
//Вывод списка команд or Howого-либо messages
//В случае если просто написать "Бот"
}
}
}
هیچ راهی برای اجتناب از این قطعه کد وجود ندارد، زیرا جدا کردن دستور از آرگومان ها همیشه ضروری است. اما پس از آن ما یک انتخاب داریم:
- if(command.equalsIngnoreCase("...")) را انجام دهید
- انجام سوئیچ (فرمان)
- روش دیگری برای پردازش انجام دهید ...
- یا به کمک Annotations متوسل شوید.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//Указывает, что наша Аннотация может быть использована
//Во время выполнения через Reflection (нам How раз это нужно).
@Retention(RetentionPolicy.RUNTIME)
//Указывает, что целью нашей Аннотации является метод
//Не класс, не переменная, не поле, а именно метод.
@Target(ElementType.METHOD)
public @interface Command //Описание. Заметим, что перед interface стоит @;
{
//Команда за которую будет отвечать функция (например "привет");
String name();
//Аргументы команды, использоваться будут для вывода списка команд
String args();
//Минимальное количество аргументов, сразу присвоor 0 (логично)
int minArgs() default 0;
//Описание, тоже для списка
String desc();
//Максимальное число аргументов. В целом не обязательно, но тоже можно использовать
int maxArgs() default Integer.MAX_VALUE;
//Показывать ли команду в списке (вовсе необязательная строка, но мало ли, пригодится!)
boolean showInHelp() default true;
//Какие команды будут считаться эквивалентными нашей
//(Например для "привет", это может быть "Здаров", "Прив" и т.д.)
//Под каждый случай заводить функцию - не рационально
String[] aliases();
}
مهم! هر پارامتر به عنوان یک تابع (با پرانتز) توصیف می شود. فقط ابتدایی ها، String ، Enum را می توان به عنوان پارامتر استفاده کرد . شما نمی توانید List<String> args(); - خطا اکنون که Annotation را توضیح دادیم، بیایید یک کلاس ایجاد کنیم که آن را CommandListener نامگذاری کنیم .
public class CommandListener
{
@Command(name = "привет",
args = "",
desc = "Будь культурным, поздоровайся",
showInHelp = false,
aliases = {"здаров"})
public void hello(String[] args)
{
//Какой-то функционал, на Ваше усмотрение.
}
@Command(name = "пока",
args = "",
desc = "",
aliases = {"удачи"})
public void bye(String[] args)
{
// Функционал
}
@Command(name = "помощь",
args = "",
desc = "Выводит список команд",
aliases = {"help", "команды"})
public void help(String[] args)
{
StringBuilder sb = new StringBuilder("Список команд: \n");
for (Method m : this.getClass().getDeclaredMethods())
{
if (m.isAnnotationPresent(Command.class))
{
Command com = m.getAnnotation(Command.class);
if (com.showInHelp()) //Если нужно показывать команду в списке.
{
sb.append("Бот, ")
.append(com.name()).append(" ")
.append(com.args()).append(" - ")
.append(com.desc()).append("\n");
}
}
}
//Отправка sb.toString();
}
}
شایان ذکر است یک ناراحتی کوچک: t.c. ما اکنون در حال مبارزه برای جهانی بودن هستیم، همه توابع باید فهرست یکسانی از پارامترهای رسمی داشته باشند، بنابراین حتی اگر دستور هیچ آرگومان نداشته باشد، تابع باید یک پارامتر String[] args داشته باشد . اکنون 3 دستور را شرح داده ایم: سلام، خداحافظ، کمک. حالا بیایید MessageListener خود را برای انجام کاری با این تغییر دهیم. برای راحتی و سرعت کار، بلافاصله دستورات خود را در HashMap ذخیره می کنیم :
public class MessageListner
{
//Map который хранит How ключ команду
//А How meaning функцию которая будет обрабатывать команду
private static final Map<String, Method> COMMANDS = new HashMap<>();
//Объект класса с командами (по сути нужен нам для рефлексии)
private static final CommandListener LISTENER = new CommandListener();
static
{
//Берем список всех методов в классе CommandListener
for (Method m : LISTENER.getClass().getDeclaredMethods())
{
//Смотрим, есть ли у метода нужная нам Аннотация @Command
if (m.isAnnotationPresent(Command.class))
{
//Берем an object нашей Аннотации
Command cmd = m.getAnnotation(Command.class);
//Кладем в качестве ключа нашей карты параметр name()
//Определенный у нашей аннотации,
//m — переменная, хранящая наш метод
COMMANDS.put(cmd.name(), m);
//Также заносим каждый элемент aliases
//Как ключ указывающий на тот же самый метод.
for (String s : cmd.aliases())
{
COMMANDS.put(s, m);
}
}
}
}
public void onMessageReceived(MessageReceivedEvent event)
{
String message = event.getMessage().toLowerCase();
if (message.startsWith("бот"))
{
try
{
String[] args = message.split(" ");
String command = args[1];
String[] nArgs = Arrays.copyOfRange(args, 2, args.length);
Method m = COMMANDS.get(command);
if (m == null)
{
//(вывод помощи)
return;
}
Command com = m.getAnnotation(Command.class);
if (nArgs.length < com.minArgs())
{
//что-то если аргументов меньше чем нужно
}
else if (nArgs.length > com.maxArgs())
{
//что-то если аргументов больше чем нужно
}
//Через рефлексию вызываем нашу функцию-обработчик
//Именно потому что мы всегда передаем nArgs у функции должен быть параметр
//String[] args — иначе она просто не будет найдена;
m.invoke(LISTENER, nArgs);
}
catch (ArrayIndexOutOfBoundsException e)
{
//Вывод списка команд or Howого-либо messages
//В случае если просто написать "Бот"
}
}
}
}
این تمام چیزی است که برای کار تیم های ما لازم است. اکنون افزودن یک دستور جدید، اگر یک مورد جدید نیست، که در آن تعداد آرگومانها باید دوباره محاسبه شوند، و کمک نیز باید بازنویسی شود و خطوط جدیدی به آن اضافه شود. اکنون، برای افزودن یک دستور، فقط باید یک تابع جدید با حاشیهنویسی Command@ در کلاس CommandListener اضافه کنیم و تمام - دستور اضافه میشود، موارد در نظر گرفته میشوند، کمک به طور خودکار اضافه میشود. کاملاً غیر قابل انکار است که این مشکل را می توان از راه های دیگر حل کرد. بله، تمام کارهایی که با کمک حاشیه نویسی/بازتاب انجام می شود بدون آن ها انجام می شود، تنها سوال راحتی، بهینه بودن و اندازه کد است، البته چسباندن Annotation در هر جایی که کوچکترین اشاره ای وجود دارد که امکان استفاده وجود دارد. همچنین منطقی ترین گزینه نیست، در هر چیزی که باید بدانید چه زمانی باید متوقف شود =). اما هنگام نوشتن API ها، کتابخانه ها یا برنامه هایی که در آنها امکان تکرار کدهای مشابه (اما نه دقیقاً یکسان) وجود دارد، بدون شک حاشیه نویسی راه حل بهینه است.
GO TO FULL VERSION