Accediendo a campos/atributos privados de un objeto JAVA

reflec 18 de ago. de 2021

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étodo get() 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 clase Class expone mas características de una clase o interfaz, la mayoría derivan del archivo del archivo class 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 campo
  • getType() Class<?> - para conocer el Class del campo
  • get(_Object_) Object - para obtener el valor del campo, es decir el valor que contiene el campo en la clase User, recibe como argumento el objeto al que pertenece el objeto Field, en este caso user, se debe capturar las excepciones IllegalArgumentException e IllegalAccessException.
  • 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

Repositorio

click aquí

Referencias

Etiquetas

¿Te gustó el contenido o lo que hacemos? ¡Cualquier colaboración es agradecida para mantener los servidores o crear proyectos!

Comentarios:

¡Genial! Te has suscrito con éxito.
¡Genial! Ahora, completa el checkout para tener acceso completo.
¡Bienvenido de nuevo! Has iniciado sesión con éxito.
Éxito! Su cuenta está totalmente activada, ahora tienes acceso a todo el contenido.