Accediendo a campos/atributos privados de un objeto JAVA
Hace unos días tuve que acceder al valor que tenía un objeto el cual era
private
por lo cual "no se puede" acceder directamente, también no podía añadir un métodoget()
que sería lo mas rápido, pero de no ser por esa restricción no habría este post. Enlace del repositorio
Java JDK
-> java -version
java version "15.0.2" 2021-01-19
Java(TM) SE Runtime Environment (build 15.0.2+7-27)
Java HotSpot(TM) 64-Bit Server VM (build 15.0.2+7-27, mixed mode, sharing)
La estructura de archivos es:
.
├── code
│ ├── Address.java
│ ├── Explorer.java
│ ├── Main.java
│ └── User.java
└── README.md
Las clases User
y Address
servirán de ejemplo y pruebas. Explorer
contiene la funcionalidad a exponer y que explicaremos en este post, la clase Main
creara instancias de User, Address y Explorer repositorio.
code/Address.java
public class Address {
private String city;
private String street;
private int number;
public Address(String city, String street, int num){
this.city = city;
this.street = street;
number = num;
}
@Override
public String toString(){
...
}
}
Address.java
está formado por un tipo de dato primitivo int
y dos cadenas de caracteres String
, estos 'atributos', que a partir de ahora los llamaremos 'campos' (field), tienen el modificador de acceso private
y definen al objeto Address que no posee getters o setters.
code/User.java
import java.util.Date;
public class User {
private final String name;
private final String dni;
private final Date date;
private int age;
private boolean married;
private Address address;
public User(String name, String dni, Address address) {
this.name = name;
this.dni = dni;
date = new Date();
age = getRandomInt(17, 35);
married = isMarried();
this.address = address;
}
private int getRandomInt(int min, int max) {
return (int) (Math.random() * (max-min)) + min;
}
private boolean isMarried(){
return age>=25;
}
@Override
public String toString(){
...
}
}
User.java
consta de campos primitivos, cadena de caracteres, java.util.Date
y Address
que son tipos que encierran mas complejidad.
Ok como vemos todos los 'campos' de User y Address tienen el modificador de acceso: private
y en las clases no tenemos métodos de acceso y menos modificar los valores de cada campo.
Objetivo
El objetivo es mostrar comó podemos acceder, conocer y modificar campos que tengan, en este caso, el modificador de acceso private
usando métodos de la clase Object
y clases del paquete java.lang.reflect
.
Obtén mas información en la sección de referencias.
Algunos conceptos necesarios
-
Class<?> - Representa Clases e Interfaces de una aplicación Java en ejecución. Los tipos primitivos de Java (boolean, byte, char, short, int, long, float y double) y la palabra clave void también se representan como objetos
Class
. La claseClass
expone mas características de una clase o interfaz, la mayoría derivan del archivo del archivoclass
que el class loader paso a la Java Virtual Machine pero unas pocas características estas determinadas por el entorno class loading en tiempo de ejecución. -
Object.getClass() - Devuelve la clase en tiempo de ejecución del objeto
-
java.lang.reflect - Permite el acceso a la información sobre los campos (fields), métodos y constructores de las clases cargadas.
Main
En la clase main definimos un objeto User, Address y Explorer; iremos llamando a los métodos de Explorer según sea necesario.
code/Main.java
class Main {
public static void main(String[] args) {
Address address = new Address("Duck town", "Donal Ave", 204);
User user = new User("Donald Juan", "5896224CBN", address);
Explorer exp = new Explorer();
}
}
Fields - Campos
Lo primero es conocer los campos que tiene el objeto user, y ademas el tipo de dato al que pertenecen (int, String, etc.) asi que en la clase Explorer
definiremos el método lookFields(Object o)
para imprimir información de los campos del objeto.
Interactuando con la clase User en tiempo de ejecución llamamos al método user.getClass()
, del objeto class
podemos hacer la llamada al método getDeclaredFields()
y lo almacenamos un objeto Field[]
del paquete java.lang.reflect.Field
.
Class<?> uClass = user.getClass();
Field[] uFields = uClass.getDeclaredFields(); //contiene un arreglo con los campos del objeto 'user'
Si conocemos el nombre de un campo, podemos acceder mediante el método uClass.getDeclaredField("field_name")
, pero como asumimos que no conocemos cuales son, iteraremos cada uno de los objetos Field
en el arreglo, y obtendremos información de cada campo del objeto User
.
Primero debemos hacer accesible cada campo llamando al método setAccessible(true)
, ok ahora sobre el Objeto Field
usaremos los métodos:
getName()
String - para obtener el nombre del campogetType()
Class<?> - para conocer elClass
del campoget(_Object_)
Object - para obtener el valor del campo, es decir el valor que contiene el campo en la claseUser
, recibe como argumento el objeto al que pertenece el objetoField
, en este casouser
, se debe capturar las excepcionesIllegalArgumentException
eIllegalAccessException
.getModifiers()
int - para obtener los modificadores de acceso del campo, consulta refect.Modifier en la sección Referencias.
Main.java
lookFields(user)
Explorer.java
import java.lang.reflect.Field;
public class Explorer {
public void lookFields(Object o){
Class<?> oClass = o.getClass();
Field[] oFields = oClass.getDeclaredFields();
for (Field f : oFields) {
f.setAccessible(true); // hacemos accesible en campo
System.out.print("Field name:\t" + f.getName() + "\t"); // nombre del campo/atributo
try {
Class<?> fclassType = f.getType(); // la clase del campo
System.out.print("class type:\t" + fclassType + "\t");
Object obj = f.get(o); // el valor del campo
System.out.println("\tvalue:\t" + obj);
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
}
}
}
Salida:
Field name: name class type: class java.lang.String value: Donald Juan
Field name: dni class type: class java.lang.String value: 5896224CBN
Field name: date class type: class java.util.Date value: Fri Jul 30 18:51:10 BOT 2021
Field name: age class type: int value: 20
Field name: married class type: boolean value: false
Field name: address class type: class Address value: City: Duck town Street: Donal Ave Number: 204
Ahora sabemos comó obtener información acerca de los campos de User
, resaltan las clases de los campos date
y los de tipo 'String' java.lang.String
, java.util.Date
respectivamente, la clase Address
, el valor devuelto por cada uno de estos campos es interesante en el caso del campo date
y address
que para retornar el valor llaman al método toString()
que la clase Date y Address tienen implementado, de lo contrario la salida sería una dirección de memoria, algo asi:
Field name: address class type: class Address value: Address@23fc625e
Analizando el campo 'address'
Como vimos en la salida hace uso del método toString()
para conocer el valor, pero si necesitamos conocer mas acerca de este objeto como crear nuevas instancias de esta para poder cambiar el valor, debemos hacer algunas cosas mas, esta es una sección complementaria y eres libre de pasar a la siguiente sección
Ya conocemos el nombre del campo o Field name (address
) con eso podemos comenzar y tratarlo en un método separado si el nombre del campo coincide con "address".
public void lookFields(Object o){
// ...
for (Field f : oFields) {
// ...
String fieldName = f.getName();
if(fieldName.equals("address")){
instanceObject(f);
}
// ...
}
}
private void instanceObject(Field f){ ... }
Con el método f.getType()
sobre el objeto Field
obtenemos el objeto Class
que identifica el tipo declarado para el campo representado por este objeto Field
.
Podemos repetir en mismo proceso anterior para obtener los campos y valores del objeto address
usando getDeclaredFields()
, es exactamente igual, lo interesante es crear un objeto de esa clase con valores que queramos cambiar y es lo que haremos.
Usando la clase Constructor
del paquete java.lang.reflect
y el método newInstance(Object... initargs) : T
podemos crear una nueva instancia de la clase declarada por el constructor, con los parámetros de inicialización especificados.
Address
tiene los campos: String : city
, String : street
y int : number
, el orden y tipos de datos son muy importante para el objeto Constructor
con la clase con el método getConstructor(Class<?>... parameterTypes)
que recibe como parámetro objetos Class que identifican los tipos de parámetros formales del constructor, en el orden declarado.
private Object instanceObject(Field f){
Class<?> fclassType = f.getType();
Object o = null;
try{
Constructor<?> cons = fclassType.getConstructor(String.class, String.class, int.class);
String city = "New City";
String street = "New Street";
int number = 888;
o = cons.newInstance(city, street, number);
System.out.println("Object:\t"+ o);
System.out.println("Class Object:\t"+ o.getClass());
}catch (NoSuchMethodException |
InvocationTargetException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
return o;
}
Salida:
Object: City: New City Street: New Street Number: 888
Class Object: class Address
Como observamos ya tenemos una instancia del objeto Address
con los campos diferentes, esto servirá cuando queramos modificar un campo.
Modificando campos
La clase Field
posee métodos para modificar campos de tipo primitivo, y set(Object obj, Object value)
donde obj
es el objeto al que pertenece el campo a modificar y value
el nuevo valor para el campo de obj que se está modificando, si el argumento del objeto especificado no es una instancia de la clase o interfaz (User
) que declara el campo del objeto, el método arroja un IllegalArgumentException
e IllegalAccessException
Añadimos el método changeFields(Object o)
y changeFieldValue
para cambiar el valor de los campos, hasta este punto ya conocemos en nombre de los campos y podemos efectuar cambios.
import java.lang.reflect.Field;
import java.util.Date;
// ....
public void changeFields(Object o){
Class<?> oClass = o.getClass();
Field[] oFields = oClass.getDeclaredFields();
System.out.println(o);
for(Field f : oFields){
f.setAccessible(true);
try {
Object valueChanged = changeFieldValue(f.getName());
f.set(o, valueChanged); // Set the field value
} catch (IllegalArgumentException | IllegalAccessException e) {
e.printStackTrace();
}
}
System.out.println(o);
}
private Object changeFieldValue(String fieldName){
Object obj = null;
switch (fieldName) {
case "name" -> obj = "Este es un nombre cambiado";
case "dni" -> obj = "Un nuevo DNI";
case "date" -> obj = new Date();
case "age" -> obj = 55;
case "address" -> obj = new Address("Toronto", "Queen ave", 404);
case "married" -> obj = false;
}
return obj;
}
Salida
Name: Pedro Perez Pereira DNI: 5896224CBN Age: 25 Married: true Address: City: CBBA Street: Av. Pando Number: 204 Date: Sat Jun 12 23:16:34 BOT 2021
Name: Este es un nombre cambiado DNI: Un nuevo DNI Age: 55 Married: false Address: City: Toronto Street: Queen ave Number: 404 Date: Sat Jun 12 23:16:35 BOT 2021
Puedes crear una instancia para el campo address
como se vio en la sección Analizando el campo address
Comentarios: