Reflection API มีไว้เพื่ออะไร?

กลไกการสะท้อนกลับของ Java ช่วยให้นักพัฒนาทำการเปลี่ยนแปลงและรับข้อมูลเกี่ยวกับคลาส อินเทอร์เฟซ ฟิลด์ และเมธอดขณะรันไทม์โดยไม่ทราบชื่อ

Reflection API ยังให้คุณสร้างอ็อบเจกต์ใหม่ วิธีการเรียก และรับหรือตั้งค่าฟิลด์

มาทำรายการทุกสิ่งที่คุณสามารถทำได้โดยใช้การไตร่ตรอง:

  • ระบุ/กำหนดคลาสของวัตถุ
  • รับข้อมูลเกี่ยวกับตัวดัดแปลงคลาส ฟิลด์ เมธอด ค่าคงที่ ตัวสร้าง และคลาสระดับสูง
  • ค้นหาวิธีการที่เป็นของอินเทอร์เฟซที่ใช้งาน
  • สร้างอินสแตนซ์ของคลาสที่ไม่รู้จักชื่อคลาสจนกว่าโปรแกรมจะถูกเรียกใช้งาน
  • รับและตั้งค่าของฟิลด์อินสแตนซ์ตามชื่อ
  • เรียกวิธีการอินสแตนซ์ด้วยชื่อ

เทคโนโลยี Java สมัยใหม่เกือบทั้งหมดใช้การสะท้อนกลับ มันรองรับเฟรมเวิร์กและไลบรารี Java / Java EE ส่วนใหญ่ในปัจจุบัน เช่น:

  • Spring frameworks สำหรับสร้างเว็บแอพพลิเคชั่น
  • กรอบการทดสอบJUnit

หากคุณไม่เคยพบกลไกเหล่านี้มาก่อน คุณอาจสงสัยว่าเหตุใดจึงจำเป็นทั้งหมดนี้ คำตอบค่อนข้างง่าย แต่ก็คลุมเครือมาก การสะท้อนจะเพิ่มความยืดหยุ่นและความสามารถในการปรับแต่งแอปพลิเคชันและโค้ดของเราอย่างมาก

แต่มีข้อดีและข้อเสียเสมอ ลองพูดถึงข้อเสียบางประการ:

  • การละเมิดความปลอดภัยของแอปพลิเคชัน การสะท้อนกลับช่วยให้เราเข้าถึงรหัสที่เราไม่ควร (การละเมิดการห่อหุ้ม)
  • ข้อจำกัดด้านความปลอดภัย การสะท้อนต้องการสิทธิ์รันไทม์ที่ไม่มีให้สำหรับระบบที่เรียกใช้ตัวจัดการความปลอดภัย
  • ประสิทธิภาพต่ำ การสะท้อนกลับใน Java กำหนดประเภทแบบไดนามิกโดยการสแกนclasspathเพื่อค้นหาคลาสที่จะโหลด สิ่งนี้จะลดประสิทธิภาพของโปรแกรม
  • ดูแลรักษายาก รหัสที่ใช้การสะท้อนนั้นอ่านและดีบักได้ยาก มีความยืดหยุ่นน้อยกว่าและดูแลรักษายากกว่า

การทำงานกับคลาสโดยใช้ Reflection API

การดำเนินการสะท้อนทั้งหมดเริ่มต้นด้วยวัตถุjava.lang.Class สำหรับออบเจกต์แต่ละประเภทจะมีการสร้าง อินสแตนซ์ที่ไม่เปลี่ยนรูปของ java.lang.Class มีเมธอดสำหรับการรับคุณสมบัติของอ็อบเจกต์ การสร้างอ็อบเจกต์ใหม่ และวิธีการเรียก

มาดูรายการเมธอดพื้นฐานสำหรับการทำงานกับjava.lang.Class :

วิธี การกระทำ
สตริง getName(); ส่งกลับชื่อของชั้นเรียน
int getModifiers (); ส่งคืนตัวแก้ไขการเข้าถึง
แพ็คเกจ getPackage (); ส่งคืนข้อมูลเกี่ยวกับแพ็คเกจ
คลาส getSuperclass(); ส่งคืนข้อมูลเกี่ยวกับคลาสพาเรนต์
คลาส [] getInterfaces (); ส่งกลับอาร์เรย์ของอินเทอร์เฟซ
ตัวสร้าง [] getConstructors (); ส่งกลับข้อมูลเกี่ยวกับตัวสร้างคลาส
ฟิลด์[] getFields(); ส่งกลับเขตข้อมูลของชั้นเรียน
ฟิลด์ getField (ชื่อฟิลด์สตริง); ส่งกลับเขตข้อมูลเฉพาะของชั้นเรียนตามชื่อ
เมธอด[] getMethods(); ส่งคืนอาร์เรย์ของเมธอด

วิธีการเหล่านี้เป็นวิธีการที่สำคัญที่สุดในการรับข้อมูลเกี่ยวกับคลาส ส่วนต่อประสาน ฟิลด์ และเมธอด นอกจากนี้ยังมีวิธีการที่ให้คุณรับหรือตั้งค่าฟิลด์ และเข้าถึงฟิลด์ส่วนตัว เราจะดูพวกเขาในภายหลัง

ตอนนี้เราจะพูดถึงการสร้างjava.lang.Classเอง เรามีสามวิธีในการทำเช่นนี้

1. การใช้ Class.forName

ในแอปพลิเคชันที่กำลังทำงานอยู่ คุณต้องใช้ เมธอด forName(String className)เพื่อรับคลาส

รหัสนี้แสดงให้เห็นว่าเราสามารถสร้างชั้นเรียนโดยใช้การสะท้อนกลับได้อย่างไร มาสร้าง คลาส บุคคลที่เราสามารถทำงานด้วย:

package com.company;

public class Person {
    private int age;
    private String name;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

และส่วนที่สองของตัวอย่างของเราคือโค้ดที่ใช้การสะท้อน:

public class TestReflection {
    public static void main(String[] args) {
        try {
            Class<?> aClass = Class.forName("com.company.Person");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

วิธีการนี้เป็นไปได้หากทราบชื่อเต็มของคลาส จากนั้นคุณจะได้รับคลาสที่เกี่ยวข้องโดยใช้เมธอดClass.forName()แบบ คงที่ วิธีนี้ไม่สามารถใช้กับประเภทดั้งเดิมได้

2. การใช้ .class

หากมีประเภทอยู่ แต่ไม่มีอินสแตนซ์ของประเภทนั้น คุณสามารถรับคลาสได้โดยเพิ่ม.classให้กับชื่อประเภท นี่เป็นวิธีที่ง่ายที่สุดในการรับคลาสประเภทดั้งเดิม

Class aClass = Person.class;

3. การใช้ .getClass()

หากมีออบเจกต์ วิธีที่ง่ายที่สุดในการรับคลาสคือการเรียกobject.getClass( )

Person person = new Person();
Class aClass = person.getClass();

อะไรคือความแตกต่างระหว่างสองแนวทางสุดท้าย?

ใช้A.classถ้าคุณรู้ว่าคุณสนใจวัตถุคลาสใดในขณะเขียนโค้ด หากไม่มีอินสแตนซ์ คุณควรใช้. class

รับวิธีการของชั้นเรียน

มาดูเมธอดที่ส่งคืนเมธอดของคลาสของเรา: getDeclaredMethods ()และgetMethods()

getDeclaredMethods()ส่งคืนอาร์เรย์ที่มี วัตถุ เมธอดสำหรับเมธอดที่ประกาศทั้งหมดของคลาสหรืออินเทอร์เฟซที่แสดงโดยออบเจ็กต์คลาส รวมถึงเมธอดสาธารณะ ส่วนตัว ดีฟอลต์ และป้องกัน แต่ไม่ใช่เมธอดที่สืบทอดมา

getMethods()ส่งคืนอาร์เรย์ที่มี วัตถุ เมธอดสำหรับเมธอดสาธารณะทั้งหมดของคลาสหรืออินเตอร์เฟสที่แสดงโดยคลาสอ็อบเจกต์ — คลาสหรืออินเตอร์เฟสที่ประกาศโดยคลาสหรืออินเตอร์เฟส รวมถึงที่สืบทอดมาจากซูเปอร์คลาสและซูเปอร์อินเตอร์เฟส

ลองมาดูกันว่าแต่ละวิธีทำงานอย่างไร

เริ่มจากgetDeclaredMethods()กัน ก่อน เพื่อช่วยให้เราเข้าใจความแตกต่างระหว่างสองวิธีอีกครั้ง ด้านล่างเราจะทำงานกับคลาสตัวเลข ที่เป็นนามธรรม ลองเขียนเมธอดคงที่ที่จะแปลง อาร์เรย์ เมธอด ของเรา เป็นList<String> :

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Method[] declaredMethods = Number.class.getDeclaredMethods();
        List<String> actualMethodNames = getMethodNames(declaredMethods);
        actualMethodNames.forEach(System.out::println);
    }

    private static List<String> getMethodNames(Method[] methods) {
        return Arrays.stream(methods)
                .map(Method::getName)
                .collect(Collectors.toList());
    }
}

นี่คือผลลัพธ์ของการเรียกใช้รหัสนี้:

byteValue
shortValue
intValue
longValue
float floatValue;
ค่าสองเท่า

นี่คือวิธีการที่ประกาศในคลาสNumber getMethods()ส่งคืนอะไร ลองเปลี่ยนสองบรรทัดในตัวอย่าง:

final Method[] methods = Number.class.getMethods();
List<String> actualMethodNames = getMethodNames(methods);

การทำเช่นนี้เราจะเห็นชุดของวิธีการต่อไปนี้:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue
wait
wait
wait
เท่ากับ
String
hashCode
getClass
แจ้งเตือน แจ้ง
เตือนทั้งหมด

เนื่องจากทุกคลาสสืบทอดObjectเมธอดของเราจึงส่งคืนเมธอดสาธารณะของคลาสอ็อบเจกต์

รับเขตข้อมูลของชั้นเรียน

เมธอดgetFieldsและgetDeclaredFieldsใช้เพื่อรับฟิลด์ของคลาส ตัวอย่างเช่น ลองดูที่คลาสLocalDateTime เราจะเขียนรหัสของเราใหม่:

import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class TestReflection {
    public static void main(String[] args) {
        final Field[] declaredFields = LocalDateTime.class.getDeclaredFields();
        List<String> actualFieldNames = getFieldNames(declaredFields);
        actualFieldNames.forEach(System.out::println);
    }

    private static List<String> getFieldNames(Field[] fields) {
        return Arrays.stream(fields)
                .map(Field::getName)
                .collect(Collectors.toList());
    }
}

จากการดำเนินการโค้ดนี้ เราได้รับชุดของฟิลด์ที่อยู่ในคลาส LocalDateTime

วันที่ เวลาMIN
MAX
serialVersionUID

โดยเปรียบเทียบกับวิธีการตรวจสอบก่อนหน้านี้ มาดูกันว่าจะเกิดอะไรขึ้นหากเราเปลี่ยนรหัสเล็กน้อย:

final Field[] fields = LocalDateTime.class.getFields();
List<String> actualFieldNames = getFieldNames(fields);

เอาท์พุต:

ต่ำ
สุด

ตอนนี้เรามาหาความแตกต่างระหว่างวิธีการเหล่านี้

เมธอดgetDeclaredFieldsส่งคืนอาร์เรย์ของ วัตถุ ฟิลด์สำหรับฟิลด์ทั้งหมดที่ประกาศโดยคลาสหรืออินเทอร์เฟซที่แสดงโดยสิ่งนี้ระดับวัตถุ.

เมธอดgetFieldsส่งคืนอาร์เรย์ของ วัตถุ ฟิลด์สำหรับ ฟิลด์ สาธารณะ ทั้งหมด ของคลาสหรืออินเทอร์เฟซที่แสดงโดยระดับวัตถุ.

ทีนี้มาดูภายในLocalDateTime

ชั้นเรียนนาทีและสูงสุดฟิลด์เป็นแบบสาธารณะ ซึ่งหมายความว่าจะมองเห็นได้ผ่านเมธอดgetFields ในทางตรงกันข้ามวันที่,เวลา,serialVersionUIDเมธอดมี ตัวแก้ไข ส่วนตัวซึ่งหมายความว่าจะไม่สามารถมองเห็นได้ผ่านเมธอดgetFieldsแต่เราสามารถรับได้โดยใช้getDeclaredFields นี่คือวิธีที่เราสามารถเข้าถึงวัตถุฟิลด์สำหรับฟิลด์ส่วนตัว

คำอธิบายของวิธีการอื่นๆ

ตอนนี้ถึงเวลาพูดคุยเกี่ยวกับวิธีการบางอย่างของ คลาส คลาสได้แก่ :

วิธี การกระทำ
รับการปรับเปลี่ยน รับการปรับเปลี่ยนสำหรับชั้นเรียนของเรา
รับแพคเกจ รับแพ็คเกจที่มีคลาสของเรา
getSuperclass รับคลาสผู้ปกครอง
รับอินเทอร์เฟซ รับอาร์เรย์ของอินเทอร์เฟซที่นำไปใช้โดยคลาส
รับชื่อ รับชื่อชั้นที่มีคุณสมบัติครบถ้วน
getSimpleName รับชื่อชั้นเรียน

getModifiers()

ตัวดัดแปลงสามารถเข้าถึงได้โดยใช้ aระดับวัตถุ.

ตัวดัดแปลงเป็นคำหลักเช่นpublic , static , interfaceและอื่น ๆ เรารับตัวปรับแต่งโดยใช้ เมธอด getModifiers() :

Class<Person> personClass = Person.class;
int classModifiers = personClass.getModifiers();

รหัสนี้ตั้งค่าของนานาชาติตัวแปรที่เป็นฟิลด์บิต ตัวแก้ไขการเข้าถึงแต่ละตัวสามารถเปิดหรือปิดได้โดยการตั้งค่าหรือล้างบิตที่เกี่ยวข้อง เราสามารถตรวจสอบตัวดัดแปลงโดยใช้เมธอดใน คลาส java.lang.reflect.Modifier :

import com.company.Person;
import java.lang.reflect.Modifier;

public class TestReflection {
    public static void main(String[] args) {
        Class<Person> personClass = Person.class;
        int classModifiers = personClass.getModifiers();

        boolean isPublic = Modifier.isPublic(classModifiers);
        boolean isStatic = Modifier.isStatic(classModifiers);
        boolean isFinal = Modifier.isFinal(classModifiers);
        boolean isAbstract = Modifier.isAbstract(classModifiers);
        boolean isInterface = Modifier.isInterface(classModifiers);

        System.out.printf("Class modifiers: %d%n", classModifiers);
        System.out.printf("Is public: %b%n", isPublic);
        System.out.printf("Is static: %b%n", isStatic);
        System.out.printf("Is final: %b%n", isFinal);
        System.out.printf("Is abstract: %b%n", isAbstract);
        System.out.printf("Is interface: %b%n", isInterface);
    }
}

จำได้ว่าคำประกาศของบุคคล ของเรา มีลักษณะอย่างไร:

public class Person {}

เราได้ผลลัพธ์ต่อไปนี้:

ตัวดัดแปลงคลาส: 1
เป็นสาธารณะ: จริง
เป็นคงที่: เท็จ
เป็นขั้นสุดท้าย: เท็จ
เป็นนามธรรม: เท็จ
เป็นอินเทอร์เฟซ: เท็จ

ถ้าเราทำให้คลาสของเราเป็นนามธรรม เราก็มี:

public abstract class Person {}

และผลลัพธ์นี้:

ตัวดัดแปลงคลาส: 1025
เป็นสาธารณะ: จริง
เป็นคงที่: เท็จ
เป็นขั้นสุดท้าย: เท็จ
เป็นนามธรรม: จริง
เป็นอินเทอร์เฟซ: เท็จ

เราเปลี่ยนตัวแก้ไขการเข้าถึง ซึ่งหมายความว่าเรายังเปลี่ยนข้อมูลที่ส่งคืนผ่านเมธอดแบบสแตติกของคลาสตัวแก้ไข

รับแพ็คเกจ ()

เรารู้เพียงคลาสเดียว เราสามารถรับข้อมูลเกี่ยวกับแพ็คเกจของมันได้:

Class<Person> personClass = Person.class;
final Package aPackage = personClass.getPackage();
System.out.println(aPackage.getName());

getSuperclass()

ถ้าเรามีอ็อบเจกต์คลาส เราสามารถเข้าถึงคลาสพาเรนต์ของมันได้:

public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<? super Person> superclass = personClass.getSuperclass();
    System.out.println(superclass);
}

เราได้รับคลาส Objectที่รู้จักกันดี:

class java.lang.Object

แต่ถ้าคลาสของเรามีคลาสพาเรนต์อื่น เราจะเห็นคลาสนั้นแทน:

package com.company;

class Human {
    // Some info
}

public class Person extends Human {
    private int age;
    private String name;

    // Some info
}

ที่นี่เราได้รับคลาสผู้ปกครองของเรา:

class com.company.Human

getInterfaces()

นี่คือวิธีที่เราสามารถรับรายการอินเทอร์เฟซที่คลาสนำไปใช้ได้:

public static void main(String[] args) {
    Class<Person> personClass = Person.class;
    final Class<?>[] interfaces = personClass.getInterfaces();
    System.out.println(Arrays.toString(interfaces));
}

และอย่าลืมเปลี่ยน คลาส บุคคล ของเรา :

public class Person implements Serializable {}

เอาท์พุต:

[อินเทอร์เฟซ java.io.Serializable]

คลาสสามารถใช้หลายอินเตอร์เฟส นั่นเป็นเหตุผลที่เราได้รับอาร์เรย์ของระดับวัตถุ ใน Java Reflection API อินเทอร์เฟซจะแสดงด้วยระดับวัตถุ

โปรดทราบ:เมธอดจะส่งคืนเฉพาะอินเทอร์เฟซที่ใช้โดยคลาสที่ระบุ ไม่ใช่คลาสพาเรนต์ หากต้องการดูรายการอินเทอร์เฟซทั้งหมดที่ใช้โดยคลาส คุณต้องอ้างถึงทั้งคลาสปัจจุบันและบรรพบุรุษทั้งหมดของคลาสในสายการสืบทอด

getName() & getSimpleName() & getCanonicalName()

ลองเขียนตัวอย่างเกี่ยวกับคลาสดั้งเดิม คลาสซ้อน คลาสนิรนาม และ คลาส สตริง :

public class TestReflection {
    public static void main(String[] args) {
        printNamesForClass(int.class, "int class (primitive)");
        printNamesForClass(String.class, "String.class (ordinary class)");
        printNamesForClass(java.util.HashMap.SimpleEntry.class,
                "java.util.HashMap.SimpleEntry.class (nested class)");
        printNamesForClass(new java.io.Serializable() {
                }.getClass(),
                "new java.io.Serializable(){}.getClass() (anonymous inner class)");
    }

    private static void printNamesForClass(final Class<?> clazz, final String label) {
        System.out.printf("%s:%n", label);
        System.out.printf("\tgetName()):\t%s%n", clazz.getName());
        System.out.printf("\tgetCanonicalName()):\t%s%n", clazz.getCanonicalName());
        System.out.printf("\tgetSimpleName()):\t%s%n", clazz.getSimpleName());
        System.out.printf("\tgetTypeName():\t%s%n%n", clazz.getTypeName());
    }
}

ผลลัพธ์ของโปรแกรมของเรา:

int class (ดั้งเดิม):
getName()): int
getCanonicalName()): int
getSimpleName()): int
getTypeName(): int

String.class (คลาสสามัญ):
getName()): java.lang.String
getCanonicalName() ): java.lang.String
getSimpleName()): สตริง
getTypeName(): java.lang.String

java.util.HashMap.SimpleEntry.class (คลาสที่ซ้อนกัน):
getName()): java.util.AbstractMap$SimpleEntry
getCanonicalName( )): java.util.AbstractMap.SimpleEntry
getSimpleName()): SimpleEntry
getTypeName(): java.util.AbstractMap$SimpleEntry

ใหม่ java.io.Serializable(){}.getClass() (คลาสภายในที่ไม่ระบุชื่อ):
getName() ): TestReflection$1
getCanonicalName()): null
getSimpleName()):
getTypeName(): TestReflection$1

ตอนนี้มาวิเคราะห์ผลลัพธ์ของโปรแกรมของเรากัน:

  • getName()ส่งคืนชื่อของเอนทิตี

  • getCanonicalName()ส่งคืนชื่อบัญญัติของคลาสฐานตามที่กำหนดโดยข้อกำหนดภาษาจาวา คืนค่า null หากคลาสฐานไม่มีชื่อบัญญัติ (นั่นคือ หากเป็นคลาสในเครื่องหรือไม่ระบุชื่อ หรืออาร์เรย์ที่มีองค์ประกอบประเภทไม่มีชื่อบัญญัติ)

  • getSimpleName()ส่งคืนชื่ออย่างง่ายของคลาสพื้นฐานตามที่ระบุในซอร์สโค้ด ส่งคืนสตริงว่างหากคลาสพื้นฐานไม่ระบุตัวตน

  • getTypeName()ส่งคืนสตริงข้อมูลสำหรับชื่อประเภทนี้