Refleksja (Reflection)

Refleksja to mechanizm języka Java, który umożliwia na odczyt i modyfikację informacji o klasach. Dzięki refleksji możesz np. odczytać nazwy pól i metod zapisanych w klasie, a nawet odwołać się do prywatnych pól klasy.

W ramach tej lekcji zobaczymy jak:

  • wyświetlić wszystkie pola w klasie i zmienić ich wartość dla wybranego obiektu
  • wyświetlić wszystkie metody w klasie i je wywołać
  • wywołać konstruktor

Bazować będziemy na następującej klasie:

package pl.javastart.examples.reflection;  
  
public class User {  
  
    public String firstName;  
    private String lastName;  
    private int age;  
  
    public User() {  
    }  
  
    public User(String firstName, String lastName) {  
        this.firstName = firstName;  
        this.lastName = lastName;  
    }  
  
    public String getFirstName() {  
        return firstName;  
    }  
  
    public void setFirstName(String firstName) {  
        this.firstName = firstName;  
    }  
  
    public String getLastName() {  
        return lastName;  
    }  
  
    public void setLastName(String lastName) {  
        if (lastName == null || lastName.length() < 3) {  
            throw new RuntimeException("Nazwisko powinno mieć min. 3 znaki");  
        }  
        this.lastName = lastName;  
    }  
  
    public void setLastName(String lastName, String secondPart) {  
        this.lastName = lastName + secondPart;  
    }  
  
    @Override  
    public String toString() {  
        final StringBuffer sb = new StringBuffer("User{");  
        sb.append("firstName='").append(firstName).append('\'');  
        sb.append(", lastName='").append(lastName).append('\'');  
        sb.append(", age='").append(age).append('\'');  
        sb.append('}');  
        return sb.toString();  
    }  
}

Mamy tutaj zarówno pola publiczne, jak i prywatne. Dwa konstruktory oraz gettery oraz settery. Dodatkowo dodana jest metoda toString(), która pozwala na wyświetlenie zawartości tej klasy.

Wyświetlenie wszystkich pól w klasie

Zacznijmy od wyświetlenia wszystkich pól w klasie User. W tym celu najpierw potrzebny będzie obiekt klasy Class, a z którego możemy pobrać pola Field za pomocą getFields():

public class Main {  
  
    public static void main(String[] args) {  
  
        User user = new User();  
        user.firstName = "Jan";  
        user.setLastName("Kowalski");  
  
        Class<? extends User> userClass = user.getClass();  
  
        Field[] fields = userClass.getFields();  
        for (Field field : fields) {  
            System.out.println(field);  
        }  
  
    }  
  
}

Taki fragment wyświetli tylko jedno pole:

public java.lang.String pl.javastart.examples.reflection.User.firstName

Dzieje się tak dlatego, ponieważ firstName jest jedynym publicznym polem. Mechanizm refleksji pozwala dostać się też do prywatnych pól. W tym celu należy użyć getDeclaredFields().

public static void main(String[] args) {  
    User user = new User();  
    user.firstName = "Jan";  
    user.setLastName("Kowalski");  
  
    Class<? extends User> userClass = user.getClass();  
  
    Field[] fields = userClass.getDeclaredFields();  
    for (Field field : fields) {  
        System.out.println(field);  
    }  
}

W wyniku otrzymamy wszystkie 3 pola:

public java.lang.String pl.javastart.examples.reflection.User.firstName
private java.lang.String pl.javastart.examples.reflection.User.lastName
private int pl.javastart.examples.reflection.User.age

Wyświetlenie pól oraz ich wartości

No dobrze, teraz spróbujmy jeszcze wyświetlić wartości tych pól. W tym celu należy skorzystać z metody Field.get(Object) jako argument podając obiekt, którego wartość nas interesuje. Warto w tym momencie zauważyć, że samo Field dotyczy się tylko i wyłącznie klasy i nie ma pojęcia o obiektach.

User user = new User();  
user.firstName = "Jan";  
user.setLastName("Kowalski");  
  
Class<? extends User> userClass = user.getClass();  
  
Field[] fields = userClass.getDeclaredFields();  
for (Field field : fields) {  
    System.out.println(field.getName() + " -> " + field.get(user));  
}

Taka próba wywołania zakończy się jednak wyjątkiem:

firstName -> Jan
Exception in thread "main" java.lang.IllegalAccessException: class pl.javastart.examples.reflection.Main cannot access a member of class pl.javastart.examples.reflection.User with modifiers "private"
	at java.base/jdk.internal.reflect.Reflection.newIllegalAccessException(Reflection.java:394)
	at java.base/java.lang.reflect.AccessibleObject.checkAccess(AccessibleObject.java:709)
	at java.base/java.lang.reflect.Field.checkAccess(Field.java:1154)
	at java.base/java.lang.reflect.Field.get(Field.java:439)
	at pl.javastart.examples.reflection.Main.main(Main.java:17)

Jak widać udało się wyświetlić firstName, ale przy kolejnym polu wystąpił wyjątek. Dzieje się tak, ponieważ domyślnie nie ma dostępu do pól prywatnych. Możemy to jednak zmienić za pomocą Field.setAccessible(true)

public static void main(String[] args) throws IllegalAccessException {  
    User user = new User();  
    user.firstName = "Jan";  
    user.setLastName("Kowalski");  
  
    Class<? extends User> userClass = user.getClass();  
  
    Field[] fields = userClass.getDeclaredFields();  
    for (Field field : fields) {  
        field.setAccessible(true); // dostęp do metod prywatnych  
        System.out.println(field.getName() + " -> " + field.get(user));  
    }  
}

Tym razem wyświetlają się wszystkie pola wraz z wartościami:

firstName -> Jan
lastName -> Kowalski
age -> 0

Wywoływanie metod

Oprócz dostępu do pól mamy również bardzo analogiczny dostęp do metod. Możemy wyświetlić wszystkie dostępne metody oraz je wywołać. Spróbujmy w takim razie wywołać metodę setLastName korzystając z refleksji.

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {  
    User user = new User();  
    user.firstName = "Jan";  
    user.setLastName("Kowalski");  
  
    Class<? extends User> userClass = user.getClass();  
  
    Method setLastNameMethod = userClass.getMethod("setLastName", String.class);  
    setLastNameMethod.invoke(user, "Nowak");  
  
    System.out.println(user);  
}

Kluczowe są tutaj dwie metody:

  • Class.getMethod - jako parametr przyjmuje nazwę metody oraz argumenty tej metody (w naszym przypadku setLastName przyjmuje tylko jeden String)
  • Method.invoke- wywołuje metodę. Jako pierwszy argument przyjmuje informację o obiekcie, na którym wywołać metodę, natomiast drugim argumentem jest wartość, która ma zostać przekazana jako parametr do metody

Ostatecznie wynik jest następujący:

User{firstName='Jan', lastName='Nowak', age='0'}

Warto w tym momencie zaznaczyć, że gdyby metoda była prywatna to należy użyć metody getDeclaredMethod oraz ustawić setAccessible(true) w metodzie.

Wywoływanie konstruktorów

Wywoływanie konstruktorów jest bardzo podobne do uruchamiania metod. Zaczynamy od pobrania interesującego nas konstruktora za pomocą Class.getContructor(<argumenty>), a następnie wywołujemy go Constructor.newInstance podając tyle argumentów ile przy pobieraniu konstruktora.

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, InstantiationException {  
    Class<? extends User> userClass = User.class;  
  
    Constructor<? extends User> constructor = userClass.getConstructor();// kontruktor bezargumentowy  
    Constructor<? extends User> constructor1 = userClass.getConstructor(String.class, String.class);// konstruktor przyjmujący imię i nazwisko  
  
    User user1 = constructor.newInstance();  
    System.out.println(user1);  
  
    User user2 = constructor1.newInstance("Anna", "Nowak");  
    System.out.println(user2);  
}

W powyższym przykładzie constructor1 jest bezargumentowy, constructor2 oczekuje dwóch parametrów. Wynik wywołania jest następujący:

User{firstName='null', lastName='null', age='0'}
User{firstName='Anna', lastName='Nowak', age='0'}

Własny toString()

Korzystając w wiedzy opisanej powyżej możemy pokusić się stworzenie własnej metody toString(), która wypisze wszystkie pola w zadanym obiekcie. Wystarczy zacząć od pobrania klasy obiektu, a potem jest już z górki:

private static void printObject(Object object) throws IllegalAccessException {  
    Class<?> clazz = object.getClass();  
  
    StringBuilder stringBuilder = new StringBuilder();  
    stringBuilder.append(clazz.getSimpleName());  
    stringBuilder.append("{");  
    Field[] declaredFields = clazz.getDeclaredFields();  
    for (Field declaredField : declaredFields) {  
        declaredField.setAccessible(true);  
        stringBuilder.append(declaredField.getName());  
        stringBuilder.append("='");  
        stringBuilder.append(declaredField.get(object));  
        stringBuilder.append("', ");  
    }  
    String substring = stringBuilder.substring(0, stringBuilder.length() - 2);  
    System.out.println(substring + "}");  
}

Kod źródłowy

Kod źródłowy można znaleźć tutaj:

Dyskusja i komentarze

Masz pytania do tego wpisu? Może chcesz się podzielić spostrzeżeniami? Zapraszamy dyskusji na naszej grupie na Facebooku.