Väljad
Sissejuhatus
Kui sul on olemas Class<?> objekt, saad sellelt küsida, millised väljad on vastaval tüübil deklareeritud.
Igat välja esindab java.lang.reflect.Field objekt - see on samuti väärtus, mida saab salvestada muutujasse, edasi anda ja mille meetodeid välja kutsuda.
Field objekt ei salvesta välja väärtust ennast.
See kirjeldab välja - selle nime, tüüpi, modifikaatoreid ja annotatsioone -
ning pakub meetodeid selle välja väärtuse lugemiseks või kirjutamiseks konkreetse objekti peal.
Väljade loetlemine
Toome näiteks sellise klassi:
public class Book {
public String title;
private String author;
private int year;
}
Selleks, et klassi välju loetleda, tuleks kasutada kas getDeclaredFields() või getFields() meetodit:
for (Field field : Book.class.getDeclaredFields()) {
System.out.println(field.getType().getSimpleName() + " " + field.getName());
}
// String title
// String author
// int year
getDeclaredFields() tagastab kõik klassis deklareeritud väljad sõltumata nende nähtavusest.
getFields() samas tagastab ainult klassi public-väljad, kuid sisaldab ka ülemklassidest päritud välju.
Enamiku reflektsiooni kasutusjuhtude puhul epaks piisama getDeclaredFields() meetodist.
Iga välja kohta saab teha mitmeid kasulikke päringuid:
field.getName(); // "author"
field.getType(); // Class<?> - String.class
field.getModifiers(); // int - bit flags, check with Modifier
field.getAnnotations(); // Annotation[]
Pane tähele, et getModifiers() tagastab täisarvu.
Läbi Modifier abiklassi on võimalik seda tõlgendada inimloetavale kujule:
int mods = field.getModifiers();
Modifier.isPrivate(mods); // true
Modifier.isStatic(mods); // false
Modifier.isFinal(mods); // false
Välja väärtuse lugemine
Konkreetse objekti välja hetkeväärtuse lugemiseks saab kasutada field.get(obj) meetodit:
Book book = new Book("Clean Code", "Robert C. Martin", 2008);
Field titleField = Book.class.getDeclaredField("title");
Object value = titleField.get(book);
System.out.println(value); // Clean Code
Selle tagastustüüp on Object, sest välja tüüp ei pruugi kompileerimise ajal teada olla.
Kui tüüp on teada, siis on võimalik läbi viia tüübiteisendust, kuid sellega kaob osa paindlikusest, mida reflektsioon algselt pakkus.
getDeclaredField otsib üles ühe konkreetse välja ette antud nime järgi.
Kui välja ei eksisteeri, siis visatakse NoSuchFieldException erind.
Välja väärtuse muutmine
Väljale uue väärtuse kirjutamiseks saab kasutada field.set(obj, value) meetodit:
Field yearField = Book.class.getDeclaredField("year");
yearField.set(book, 2009);
System.out.println(book.year); // 2009
Väärtus peab olema väljaga ühilduvat tüüpi, vastasel juhul viskab meetod IllegalArgumentException erindi.
Ligipääs privaatsetele väljadele
Kui proovida ülaltoodud lugemis- või kirjutamisnäiteid private välja peal kasutada,
siis tulemuseks tekib IllegalAccessException erind:
java.lang.IllegalAccessException: class X cannot access a member of class Book
with modifiers "private"
See on Java tavapärane nähtavuskontroll, mida rakendatakse ka refleksiooni kaudu juurdepääsul.
Sellest möödahiilimiseks tuleb Field objekt kõigepealt ligipääsetavaks märkida:
Field authorField = Book.class.getDeclaredField("author");
authorField.setAccessible(true);
System.out.println(authorField.get(book)); // Robert C. Martin
authorField.set(book, "Uncle Bob");
authorField.setAccessible(false);
setAccessible(true) keelab nähtavuskontrolli ainult selle konkreetse Field objekti jaoks.
See ei muuda välja ennast - muu kood näeb seda endiselt private väljana ega saa sellele otse ligi.
setAccessible(true) kasutamine raamistike või JDK klasside puhul on mõnikord vajalik, kuid see rikub kapseldamist, mille klassi autor on teadlikult loonud.
Kui klassis on private väli, siis on otsustatud seda mitte avalikult eksponeerida.
Väljastpoolt selle muutmine võib jätta objekti ebaselgesse olekusse.
Kasuta seda võimalust ainult siis, kui see on tõesti vajalik - näiteks serialiseerimise, testimise või päris raamistikutaseme töö jaoks.
Staatilised väljad
Staatiline väli kuulub klassile, mitte konkreetsele objekti eksemplarile.
Sellise välja lugemisel või muutmisel tuleb sihtobjektina anda null:
public class Constants {
public static int MAX_BOOKS = 1000;
}
Field max = Constants.class.getDeclaredField("MAX_BOOKS");
int value = (int) max.get(null); // 1000
max.set(null, 5000);
Praktiline näide
Toome näiteks koodi, mille eesmärk on kopeerida ühised väljad kahe erinevat tüüpi objekti vahel. Selline lähenemine on kasulik näiteks siis, kui andmeid teisendatakse andmebaasimudelist vaatemudeliks (view model) või ühest andmestruktuurist teise.
public static void copyCommonFields(Object source, Object target)
throws IllegalAccessException {
Class<?> targetType = target.getClass();
for (Field sourceField : source.getClass().getDeclaredFields()) {
try {
Field targetField = targetType.getDeclaredField(sourceField.getName());
if (targetField.getType().equals(sourceField.getType())) {
sourceField.setAccessible(true);
targetField.setAccessible(true);
targetField.set(target, sourceField.get(source));
}
} catch (NoSuchFieldException ignored) {
// target has no matching field — skip
}
}
}
Funktsioon ei maini ühtegi konkreetset tüüpi. See käib läbi lähteobjekti väljad, otsib sihtobjektilt üles sama nime ja tüübiga välja ning kopeerib väärtuse, kui selline väli leitakse. Sarnast lähenemist kasutavad paljud objektide kaardistamise (object mapping) teegid.
Field-iga seotud erandid
Field objektidega töötamisel tuleb arvestada kolme olulise erindiga, mida tuleb kas käsitleda või edasi visata:
| Erand | Millal tekib |
|---|---|
NoSuchFieldException | getDeclaredField("name") ei leia selle nimega välja |
IllegalAccessException | Nähtavuskontroll blokeerib lugemise või kirjutamise |
IllegalArgumentException | set meetodile antud väärtus ei sobi välja tüübiga (kontrollimata erand / unchecked exception) |
Väikestes näidetes kasutatakse sageli lihtsalt throws Exception või mähitakse erind RuntimeException-i sisse.
Toodangus tasub aga teadlikult otsustada, mida iga selline tõrge tähendab: kas tegemist on valesti seadistatud klassiga, programmiveaga või taastatava sisendveaga.