Untuk apa Reflection API?

Mekanisme refleksi Java membolehkan pembangun membuat perubahan dan mendapatkan maklumat tentang kelas, antara muka, medan dan kaedah pada masa jalan tanpa mengetahui nama mereka.

API Refleksi juga membolehkan anda membuat objek baharu, kaedah panggilan dan mendapatkan atau menetapkan nilai medan.

Mari buat senarai semua yang anda boleh lakukan menggunakan refleksi:

  • Mengenal pasti/menentukan kelas sesuatu objek
  • Dapatkan maklumat tentang pengubahsuai kelas, medan, kaedah, pemalar, pembina dan superclass
  • Ketahui kaedah yang tergolong dalam antara muka yang dilaksanakan
  • Buat contoh kelas yang nama kelasnya tidak diketahui sehingga program dilaksanakan
  • Dapatkan dan tetapkan nilai medan contoh mengikut nama
  • Panggil kaedah contoh dengan nama

Hampir semua teknologi Java moden menggunakan refleksi. Ia mendasari kebanyakan rangka kerja dan perpustakaan Java / Java EE hari ini, sebagai contoh:

  • Rangka kerja musim bunga untuk membina aplikasi web
  • rangka kerja ujian JUnit

Jika anda tidak pernah menemui mekanisme ini sebelum ini, anda mungkin bertanya mengapa semua ini perlu. Jawapannya agak mudah tetapi juga sangat kabur: refleksi secara mendadak meningkatkan fleksibiliti dan keupayaan untuk menyesuaikan aplikasi dan kod kami.

Tetapi sentiasa ada kebaikan dan keburukan. Jadi mari kita sebutkan beberapa keburukan:

  • Pelanggaran keselamatan aplikasi. Refleksi membolehkan kami mengakses kod yang tidak sepatutnya (melanggar pengkapsulan).
  • Sekatan keselamatan. Refleksi memerlukan kebenaran masa jalan yang tidak tersedia untuk sistem yang menjalankan pengurus keselamatan.
  • Prestasi rendah. Refleksi dalam Java menentukan jenis secara dinamik dengan mengimbas laluan kelas untuk mencari kelas untuk dimuatkan. Ini mengurangkan prestasi program.
  • Sukar untuk dikekalkan. Kod yang menggunakan pantulan sukar dibaca dan nyahpepijat. Ia kurang fleksibel dan lebih sukar untuk diselenggara.

Bekerja dengan kelas menggunakan API Refleksi

Semua operasi pantulan bermula dengan objek java.lang.Class . Untuk setiap jenis objek, contoh java.lang.Class yang tidak boleh diubah dibuat. Ia menyediakan kaedah untuk mendapatkan sifat objek, mencipta objek baharu dan kaedah panggilan.

Mari lihat senarai kaedah asas untuk bekerja dengan java.lang.Class :

Kaedah Tindakan
String getName(); Mengembalikan nama kelas
int getModifiers(); Mengembalikan pengubah suai akses
Pakej getPackage(); Mengembalikan maklumat tentang pakej
Kelas getSuperclass(); Mengembalikan maklumat tentang kelas induk
Kelas[] getInterfaces(); Mengembalikan pelbagai antara muka
Pembina[] getConstructors(); Mengembalikan maklumat tentang pembina kelas
Medan[] getFields(); Mengembalikan medan kelas
Medan getField(String fieldName); Mengembalikan medan tertentu kelas mengikut nama
Kaedah[] getMethods(); Mengembalikan pelbagai kaedah

Ini adalah kaedah yang paling penting untuk mendapatkan data tentang kelas, antara muka, medan dan kaedah. Terdapat juga kaedah yang membolehkan anda mendapatkan atau menetapkan nilai medan dan mengakses medan peribadi . Kami akan melihat mereka sedikit kemudian.

Sekarang kita akan bercakap tentang mendapatkan java.lang.Class itu sendiri. Kami mempunyai tiga cara untuk melakukan ini.

1. Menggunakan Class.forName

Dalam aplikasi yang sedang berjalan, anda mesti menggunakan kaedah forName(String className) untuk mendapatkan kelas.

Kod ini menunjukkan cara kita boleh membuat kelas menggunakan refleksi. Mari buat kelas Orang yang boleh kita bekerjasama:

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;
    }
}

Dan bahagian kedua contoh kami ialah kod yang menggunakan refleksi:

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

Pendekatan ini boleh dilakukan jika nama penuh kelas diketahui. Kemudian anda boleh mendapatkan kelas yang sepadan menggunakan kaedah Class.forName() statik . Kaedah ini tidak boleh digunakan untuk jenis primitif.

2. Menggunakan .class

Jika jenis tersedia tetapi tidak ada contoh, maka anda boleh mendapatkan kelas dengan menambahkan .class pada nama jenis. Ini adalah cara paling mudah untuk mendapatkan kelas jenis primitif.

Class aClass = Person.class;

3. Menggunakan .getClass()

Jika objek tersedia, maka cara paling mudah untuk mendapatkan kelas adalah dengan memanggil object.getClass() .

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

Apakah perbezaan antara dua pendekatan terakhir?

Gunakan A.class jika anda tahu objek kelas yang anda minati pada masa pengekodan. Jika tiada contoh tersedia, maka anda harus menggunakan .class .

Mendapatkan kaedah kelas

Mari lihat kaedah yang mengembalikan kaedah kelas kami: getDeclaredMethods() dan getMethods() .

getDeclaredMethods() mengembalikan tatasusunan yang mengandungi objek Kaedah untuk semua kaedah yang diisytiharkan bagi kelas atau antara muka yang diwakili oleh objek Kelas, termasuk kaedah awam, persendirian, lalai dan dilindungi, tetapi bukan kaedah yang diwarisi.

getMethods() mengembalikan tatasusunan yang mengandungi objek Kaedah untuk semua kaedah awam kelas atau antara muka yang diwakili oleh objek Kelas — yang diisytiharkan oleh kelas atau antara muka, serta yang diwarisi daripada superclass dan superinterface.

Mari kita lihat bagaimana setiap daripada mereka berfungsi.

Mari kita mulakan dengan getDeclaredMethods() . Untuk membantu kami sekali lagi memahami perbezaan antara kedua-dua kaedah, di bawah kami akan bekerja dengan kelas Nombor abstrak . Mari tulis kaedah statik yang akan menukar tatasusunan Kaedah kami kepada 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());
    }
}

Inilah hasil daripada menjalankan kod ini:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue

Ini adalah kaedah yang diisytiharkan di dalam kelas Nombor . Apakah yang dikembalikan oleh getMethods() ? Mari kita tukar dua baris dalam contoh:

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

Melakukan ini, kita akan melihat set kaedah berikut:

byteValue
shortValue
intValue
longValue
float floatValue;
doubleValue
tunggu
tunggu
tunggu
sama
denganString
hashCode
getClass
notify
notifyAll

Kerana semua kelas mewarisi Object , kaedah kami juga mengembalikan kaedah awam kelas Object .

Mendapatkan bidang kelas

Kaedah getFields dan getDeclaredFields digunakan untuk mendapatkan medan kelas. Sebagai contoh, mari kita lihat kelas LocalDateTime . Kami akan menulis semula kod kami:

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());
    }
}

Hasil daripada melaksanakan kod ini, kami mendapat set medan yang terkandung dalam kelas LocalDateTime.

MIN
MAX
serialVersionUID masa
tarikh

Dengan analogi dengan pemeriksaan kaedah kami sebelum ini, mari lihat apa yang berlaku jika kita menukar sedikit kod:

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

Pengeluaran:

MIN
MAX

Sekarang mari kita fikirkan perbezaan antara kaedah ini.

Kaedah getDeclaredFields mengembalikan tatasusunan objek Medan untuk semua medan yang diisytiharkan oleh kelas atau antara muka yang diwakili oleh iniKelasobjek.

Kaedah getFields mengembalikan tatasusunan objek Medan untuk semua medan awam kelas atau antara muka yang diwakili olehKelasobjek.

Sekarang mari kita lihat dalam LocalDateTime .

Kelas ituMINdanMAXmedan adalah awam, yang bermaksud ia akan kelihatan melalui kaedah getFields . Sebaliknya, yangTarikh,masa,serialVersionUIDkaedah mempunyai pengubah suai peribadi , yang bermaksud ia tidak akan kelihatan melalui kaedah getFields , tetapi kita boleh mendapatkannya menggunakan getDeclaredFields . Beginilah cara kita boleh mengakses objek Medan untuk medan peribadi .

Penerangan kaedah lain

Kini tiba masanya untuk membincangkan beberapa kaedah kelas Kelas , iaitu:

Kaedah Tindakan
getModifiers Mendapatkan pengubah suai untuk kelas kami
getPackage Mendapatkan pakej yang mengandungi kelas kami
dapatkanSuperclass Mendapat kelas ibu bapa
getInterfaces Mendapatkan pelbagai antara muka yang dilaksanakan oleh kelas
getName Mendapatkan nama kelas yang layak sepenuhnya
getSimpleName Mendapat nama kelas

getModifiers()

Pengubah suai boleh diakses menggunakan aKelasobjek.

Pengubah suai ialah kata kunci seperti public , static , interface , dsb. Kami mendapatkan pengubah menggunakan kaedah getModifiers() :

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

Kod ini menetapkan nilai anintpembolehubah iaitu medan sedikit. Setiap pengubah suai akses boleh dihidupkan atau dimatikan dengan menetapkan atau mengosongkan bit yang sepadan. Kita boleh menyemak pengubah menggunakan kaedah dalam kelas 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);
    }
}

Ingatlah rupa pengisytiharan Orang kita:

public class Person {}

Kami mendapat output berikut:

Pengubah suai kelas: 1
Is public: true
Is statik: false
Is final: false
Is abstract: false
Is interface: false

Jika kita membuat kelas kita abstrak, maka kita mempunyai:

public abstract class Person {}

dan output ini:

Pengubah suai kelas: 1025
Is public: true
Is statik: false
Is final: false
Is abstract: true
Is antara muka: false

Kami menukar pengubah suai akses, yang bermaksud kami juga menukar data yang dikembalikan melalui kaedah statik kelas Modifier .

getPackage()

Dengan hanya mengetahui kelas, kita boleh mendapatkan maklumat tentang pakejnya:

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

getSuperclass()

Jika kita mempunyai objek Kelas, maka kita boleh mengakses kelas induknya:

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

Kami mendapat kelas Objek yang terkenal :

class java.lang.Object

Tetapi jika kelas kami mempunyai kelas induk lain, maka kami akan melihatnya sebaliknya:

package com.company;

class Human {
    // Some info
}

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

    // Some info
}

Di sini kami mendapat kelas ibu bapa kami:

class com.company.Human

getInterfaces()

Begini cara kita boleh mendapatkan senarai antara muka yang dilaksanakan oleh kelas:

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

Dan jangan lupa untuk menukar kelas Person kami:

public class Person implements Serializable {}

Pengeluaran:

[antara muka java.io.Serializable]

Kelas boleh melaksanakan banyak antara muka. Itulah sebabnya kami mendapat pelbagaiKelasobjek. Dalam Java Reflection API, antara muka juga diwakili olehKelasobjek.

Sila ambil perhatian: Kaedah ini hanya mengembalikan antara muka yang dilaksanakan oleh kelas yang ditentukan, bukan kelas induknya. Untuk mendapatkan senarai lengkap antara muka yang dilaksanakan oleh kelas, anda perlu merujuk kepada kedua-dua kelas semasa dan semua nenek moyangnya di atas rantaian warisan.

getName() & getSimpleName() & getCanonicalName()

Mari tulis contoh yang melibatkan primitif, kelas bersarang, kelas tanpa nama dan kelas String :

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());
    }
}

Hasil program kami:

kelas int (primitif):
getName()): int
getCanonicalName()): int
getSimpleName()): int
getTypeName(): int

String.class (kelas biasa):
getName()): java.lang.String
getCanonicalName() ): java.lang.String
getSimpleName()): String
getTypeName(): java.lang.String

java.util.HashMap.SimpleEntry.class (nested class):
getName()): java.util.AbstractMap$SimpleEntry
getCanonicalName( )): java.util.AbstractMap.SimpleEntry
getSimpleName()): SimpleEntry
getTypeName(): java.util.AbstractMap$SimpleEntry

new java.io.Serializable(){}.getClass() (kelas dalaman tanpa nama):
getName() ): TestReflection$1
getCanonicalName()): null
getSimpleName()):
getTypeName(): TestReflection$1

Sekarang mari analisa output program kami:

  • getName() mengembalikan nama entiti.

  • getCanonicalName() mengembalikan nama kanonik kelas asas, seperti yang ditakrifkan oleh Spesifikasi Bahasa Java. Mengembalikan null jika kelas asas tidak mempunyai nama kanonik (iaitu, jika ia ialah kelas tempatan atau tanpa nama atau tatasusunan yang jenis elemennya tidak mempunyai nama kanonik).

  • getSimpleName() mengembalikan nama ringkas kelas asas seperti yang dinyatakan dalam kod sumber. Mengembalikan rentetan kosong jika kelas asas adalah tanpa nama.

  • getTypeName() mengembalikan rentetan bermaklumat untuk nama jenis ini.