บทความนี้มีไว้สำหรับผู้ที่ไม่เคยใช้งาน Annotations มาก่อน แต่ต้องการทำความเข้าใจว่ามันคืออะไรและใช้กับอะไร หากคุณมีประสบการณ์ในด้านนี้ ฉันไม่คิดว่าบทความนี้จะเพิ่มพูนความรู้ของคุณ (และอันที่จริง ฉันไม่ได้บรรลุเป้าหมายดังกล่าว) นอกจากนี้บทความนี้ไม่เหมาะสำหรับผู้ที่เพิ่งเริ่มเรียนรู้ภาษา Java หากคุณไม่เข้าใจว่าMap<>หรือHashMap<> คืออะไร หรือไม่รู้ว่า รายการ static{ }ภายในคำจำกัดความของคลาสหมายถึงอะไร หรือไม่เคยคิดทบทวนเลย ยังเร็วเกินไปที่คุณจะอ่านบทความนี้และ พยายามทำความเข้าใจว่าคำอธิบายประกอบคืออะไร เครื่องมือนี้ไม่ได้ถูกสร้างขึ้นสำหรับผู้เริ่มต้นใช้งานเนื่องจากไม่จำเป็นต้องมีความเข้าใจพื้นฐานเกี่ยวกับการโต้ตอบของคลาสและวัตถุ (ความคิดเห็นของฉัน) (ขอบคุณความคิดเห็นที่แสดงความจำเป็นในการใช้คำลงท้ายนี้)
มาเริ่มกันเลย คำอธิบายประกอบใน Java เป็นป้ายกำกับชนิดหนึ่งในโค้ดที่อธิบายข้อมูลเมตาสำหรับฟังก์ชัน/คลาส/แพ็คเกจ ตัวอย่างเช่น @Override Annotation ที่รู้จักกันดี ซึ่งบ่งชี้ว่าเรากำลังจะแทนที่เมธอดของคลาสพาเรนต์ ใช่ในอีกด้านหนึ่งก็เป็นไปได้หากไม่มีวิธีนี้ แต่ถ้าผู้ปกครองไม่มีวิธีนี้ก็มีความเป็นไปได้ที่จะเขียนโค้ดอย่างไร้ประโยชน์เพราะ วิธีการเฉพาะนี้อาจไม่ถูกเรียกใช้ แต่ด้วยคำอธิบายประกอบ @Override คอมไพเลอร์จะบอกเราว่า: “ฉันไม่พบวิธีการดังกล่าวในผู้ปกครอง... มีบางอย่างสกปรกที่นี่” อย่างไรก็ตาม คำอธิบายประกอบสามารถสื่อได้มากกว่าความหมายของ "เพื่อความน่าเชื่อถือ" แต่ยังเก็บข้อมูลบางอย่างที่จะใช้ในภายหลังได้
ขั้นแรก มาดูคำอธิบายประกอบที่ง่ายที่สุดที่จัดทำโดยไลบรารีมาตรฐาน
(ขอบคุณอีกครั้งสำหรับความคิดเห็น ตอนแรกฉันไม่คิดว่าจำเป็นต้องมีบล็อกนี้) ก่อนอื่น เรามาพูดคุยกันก่อนว่าคำอธิบายประกอบคืออะไร แต่ละรายการมี 2 พารามิเตอร์ หลัก ที่จำเป็น :- ประเภทการจัดเก็บ (การเก็บรักษา);
- ประเภทของวัตถุที่ระบุ (เป้าหมาย)
ประเภทการจัดเก็บ
ตาม "ประเภทพื้นที่เก็บข้อมูล" เราหมายถึงขั้นตอนที่คำอธิบายประกอบของเรา "คงอยู่" ภายในชั้นเรียน หมายเหตุประกอบแต่ละรายการมี"ประเภทการเก็บรักษา" ที่เป็นไปได้เพียงหนึ่ง รายการที่ระบุไว้ในคลาส RetentionPolicy :- แหล่งที่มา - คำอธิบายประกอบจะใช้เฉพาะเมื่อเขียนโค้ดและคอมไพเลอร์จะละเว้น (เช่น จะไม่ถูกบันทึกหลังจากการคอมไพล์) โดยทั่วไปจะใช้สำหรับตัวประมวลผลล่วงหน้าใดๆ (ตามเงื่อนไข) หรือคำสั่งสำหรับคอมไพเลอร์
- CLASS - คำอธิบายประกอบจะถูกเก็บรักษาไว้หลังจากการคอมไพล์ แต่ JVM จะละเว้น (เช่น ไม่สามารถใช้ขณะรันไทม์) โดยทั่วไปใช้สำหรับบริการของบริษัทอื่นที่โหลดโค้ดของคุณเป็นแอปพลิเคชันปลั๊กอิน
- RUNTIMEเป็นคำอธิบายประกอบที่บันทึกไว้หลังจากการคอมไพล์และโหลดโดย JVM (เช่น สามารถใช้ระหว่างการทำงานของโปรแกรมได้) ใช้เป็นเครื่องหมายในโค้ดที่ส่งผลโดยตรงต่อการทำงานของโปรแกรม (ตัวอย่างจะกล่าวถึงในบทความนี้)
ประเภทของวัตถุที่ระบุไว้ข้างต้น
คำอธิบายนี้ควรใช้เกือบจะเป็นตัวอักษรเพราะ... ใน Java คำอธิบายประกอบสามารถระบุเหนืออะไรก็ได้ (ฟิลด์ คลาส ฟังก์ชัน ฯลฯ) และสำหรับคำอธิบายประกอบแต่ละรายการ จะระบุว่าสามารถระบุสิ่งใดได้บ้าง ไม่มีกฎ "สิ่งเดียว" อีกต่อไป คุณสามารถระบุคำอธิบายประกอบเหนือทุกสิ่งที่แสดงด้านล่าง หรือคุณสามารถเลือกเฉพาะองค์ประกอบที่จำเป็นของคลาสElementType :- ANNOTATION_TYPE - คำอธิบายประกอบอื่น
- CONSTRUCTOR - ตัวสร้างคลาส
- FIELD - ฟิลด์คลาส
- LOCAL_VARIABLE - ตัวแปรท้องถิ่น
- วิธีการ - วิธีการเรียน
- แพคเกจ - คำอธิบายของแพ็คเกจแพ็คเกจ
- PARAMETER - พารามิเตอร์เมธอด โมฆะสาธารณะ สวัสดี (@Annontation String param){}
- TYPE - ระบุไว้เหนือชั้นเรียน
@แทนที่
การเก็บรักษา: แหล่งที่มา; เป้าหมาย: วิธีการ คำอธิบายประกอบนี้แสดงให้เห็นว่าวิธีการเขียนนั้นสืบทอดมาจากคลาสพาเรนต์ คำอธิบายประกอบแรกที่โปรแกรมเมอร์ Java มือใหม่ทุกคนพบเจอเมื่อใช้ IDE ที่พุช @Override เหล่านี้อย่างต่อเนื่อง บ่อยครั้งที่ครูจาก YouTube แนะนำให้ "ลบมันเพื่อไม่ให้รบกวน" หรือ "ปล่อยไว้โดยไม่สงสัยว่าทำไมมันถึงอยู่ตรงนั้น" ในความเป็นจริง คำอธิบายประกอบมีประโยชน์มากกว่า: ไม่เพียงแต่ช่วยให้คุณเข้าใจว่าวิธีการใดที่ถูกกำหนดในคลาสนี้เป็นครั้งแรก และวิธีใดที่ผู้ปกครองมีอยู่แล้ว (ซึ่งเพิ่มความสามารถในการอ่านโค้ดของคุณอย่างไม่ต้องสงสัย) แต่ยังรวมถึงคำอธิบายประกอบนี้ด้วย ทำหน้าที่เป็น "การตรวจสอบตัวเอง" ที่คุณไม่ผิดเมื่อกำหนดฟังก์ชันที่โอเวอร์โหลด@เลิกใช้แล้ว
การเก็บรักษา: รันไทม์; เป้าหมาย: CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE คำอธิบายประกอบนี้ระบุวิธีการ คลาส หรือตัวแปรที่ "ล้าสมัย" และอาจถูกลบออกในเวอร์ชันอนาคตของผลิตภัณฑ์ โดยปกติแล้วผู้ที่อ่านเอกสารประกอบของ API ใดๆ หรือไลบรารี Java มาตรฐานเดียวกันจะพบคำอธิบายประกอบนี้ บางครั้งคำอธิบายประกอบนี้อาจถูกละเลยเนื่องจาก... มันไม่ก่อให้เกิดข้อผิดพลาดใด ๆ และโดยหลักการแล้ว ในตัวมันเองไม่ได้รบกวนชีวิตมากนัก อย่างไรก็ตาม ข้อความหลักที่คำอธิบายประกอบนี้นำเสนอคือ "เราได้คิดค้นวิธีที่สะดวกกว่าในการใช้งานฟังก์ชันนี้ ใช้มัน อย่าใช้อันเก่า" - หรืออย่างอื่น - "เราเปลี่ยนชื่อฟังก์ชัน แต่สิ่งนี้ เป็นเช่นนั้น เราทิ้งมันไว้เป็นมรดก…” (ซึ่งโดยทั่วไปแล้วก็ไม่เลวเช่นกัน) กล่าวโดยสรุป หากคุณเห็น @Deprecated จะเป็นการดีกว่าที่จะพยายามไม่ใช้สิ่งที่ค้างอยู่ เว้นแต่ว่าจำเป็นจริงๆ และอาจคุ้มค่าที่จะอ่านเอกสารประกอบอีกครั้งเพื่อทำความเข้าใจว่างานที่ทำโดยองค์ประกอบที่เลิกใช้แล้วได้รับการปฏิบัติอย่างไร ตัวอย่างเช่น แทนที่จะใช้new Date().getYear()ขอแนะนำให้ใช้Calendar.getInstance().get(Calendar.YEAR )@SuppressWarnings
การเก็บรักษา: แหล่งที่มา; เป้าหมาย: TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE คำอธิบายประกอบนี้ปิดใช้งานเอาต์พุตของคำเตือนคอมไพเลอร์ที่เกี่ยวข้องกับองค์ประกอบที่ระบุไว้ คำอธิบายประกอบ SOURCE ระบุไว้เหนือฟิลด์ วิธีการ คลาสหรือไม่@การเก็บรักษา
การเก็บรักษา: RUNTIME; เป้าหมาย: ANNOTATION_TYPE; คำอธิบายประกอบนี้ระบุ "ประเภทการจัดเก็บ" ของคำอธิบายประกอบข้างต้นซึ่งระบุไว้ ใช่ คำอธิบายประกอบนี้ใช้เพื่อตัวมันเองด้วยซ้ำ... เวทมนตร์เท่านั้นเอง@เป้า
การเก็บรักษา: RUNTIME; เป้าหมาย: ANNOTATION_TYPE; คำอธิบายประกอบนี้ระบุประเภทของออบเจ็กต์ที่สามารถระบุคำอธิบายประกอบที่เราสร้างได้ ใช่ และมันยังใช้สำหรับตัวคุณเองด้วย จงทำความคุ้นเคยกับมัน... ฉันคิดว่านี่คือจุดที่เราสามารถแนะนำคำอธิบายประกอบมาตรฐานของไลบรารี Java ให้สมบูรณ์ได้ เพราะ ส่วนที่เหลือไม่ค่อยได้ใช้และถึงแม้ว่าจะมีผลประโยชน์ในตัวเอง แต่ก็ไม่ใช่ทุกคนที่ต้องจัดการกับสิ่งเหล่านี้และไม่จำเป็นเลย หากคุณต้องการให้ฉันพูดเกี่ยวกับคำอธิบายประกอบเฉพาะจากไลบรารีมาตรฐาน (หรือบางทีอาจเป็นคำอธิบายประกอบเช่น @NotNull และ @Nullable ซึ่งไม่รวมอยู่ใน STL) ให้เขียนความคิดเห็น - ผู้ใช้ประเภทใดประเภทหนึ่งจะตอบคุณที่นั่น หรือ ฉันจะทำเมื่อฉันเห็นมัน หากมีคนขอคำอธิบายประกอบจำนวนมาก ฉันจะเพิ่มลงในบทความด้วยการประยุกต์ใช้คำอธิบายประกอบ RUNTIME ในทางปฏิบัติ
จริงๆ แล้ว ฉันคิดว่านั่นเป็นการพูดคุยเชิงทฤษฎีที่เพียงพอแล้ว เรามาฝึกใช้ตัวอย่างของบอทกันดีกว่า สมมติว่าคุณต้องการเขียนบอทสำหรับเครือข่ายโซเชียลบางแห่ง เครือข่ายหลักทั้งหมด เช่น VK, Facebook, Discord มี API ของตัวเองที่ให้คุณเขียนบอทได้ สำหรับเครือข่ายเดียวกันนี้ มีไลบรารี่ที่เขียนไว้สำหรับการทำงานกับ API รวมถึงใน Java ด้วย ดังนั้นเราจะไม่เจาะลึกการทำงานของ 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("..."))
- ทำสวิตช์ (คำสั่ง)
- ดำเนินการด้วยวิธีอื่น...
- หรือหันไปใช้คำอธิบายประกอบ
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 เพียงเท่านี้ - เพิ่มคำสั่งแล้ว คำนึงถึงกรณีและปัญหาต่างๆ เพิ่มความช่วยเหลือจะถูกเพิ่มโดยอัตโนมัติ ไม่ต้องสงสัยเลยว่าปัญหานี้สามารถแก้ไขได้ด้วยวิธีอื่นมากมาย ใช่ ทุกอย่างที่สามารถทำได้ด้วยความช่วยเหลือของคำอธิบายประกอบ/การสะท้อนกลับสามารถทำได้โดยไม่ต้องใช้สิ่งเหล่านั้น คำถามเดียวคือความสะดวก การเพิ่มประสิทธิภาพ และขนาดโค้ด แน่นอนว่าการติดคำอธิบายประกอบในทุกที่ที่มีคำใบ้เพียงเล็กน้อยว่าจะสามารถใช้งานได้ มันไม่ใช่ตัวเลือกที่สมเหตุสมผลที่สุด ในทุกสิ่งที่คุณจำเป็นต้องรู้ว่าเมื่อใดควรหยุด =) แต่เมื่อเขียน API, ไลบรารี หรือโปรแกรมที่สามารถทำซ้ำโค้ดประเภทเดียวกัน (แต่ไม่เหมือนกันทุกประการ) คำอธิบายประกอบถือเป็นโซลูชันที่ดีที่สุดอย่างไม่ต้องสงสัย
GO TO FULL VERSION