2 votes

Comment affecter les valeurs de la carte aux champs de la POJO (champs dont les noms sont liés aux clés) ?

J'ai une classe pour POJO avec plusieurs champs comme ceci :

class DataObj {
    private String v1 = null;
    private String v2 = null;
    ...

Je veux prendre les valeurs de ces champs dans une carte où les noms des clés sont liés aux noms des champs. Le site data vient (d'un dispositif externe) dans la carte comme ceci :

V1=11
V2=22
...

Donc, actuellement, je définis un ensemble de constantes et j'utilise un commutateur pour le faire, comme ceci :

private static final String V1 = "V1";
private static final String V2 = "V2";
...

DataObj(Map<String, String> data) {
    for (String key : data.keySet()) {
        String value = data.get(key);
        switch (key) {
            case V1:
                v1 = value;
                break;
            case V2:
                v2 = value;
                break;
            ...
        }
    }
}

Cela me semble être une solution très brutale... De plus, j'ai beaucoup de ces champs et ils ne diffèrent que par des caractères uniques, donc écrire un tel bloc de commutation peut être très source d'erreurs. Peut-être que quelqu'un pourrait partager un mécanisme plus astucieux (à côté de la réflexion) pour résoudre de telles tâches ?

EDIT - SOLUTION SÉLECTIONNÉE

En utilisant la réponse sélectionnée, j'ai créé une classe :

abstract class PropertyMapper<T> {
    private Map<String, Setter<T>> setters = new HashMap<>();

    abstract void mapProperties();

    public PropertyMapper() {
        mapProperties();
    }

    protected void updateBean(Map<String, T> map) {
        for (String key : map.keySet()) {
            setField(key, map.get(key));
        }
    }

    protected void mapProperty(String property, Setter<T> fieldAssignment) {
        setters.put(property, fieldAssignment);
    }

    protected interface Setter<T> { void set(T o); }

    private void setField(String s, T o) { setters.get(s).set(o); }
}

Et ensuite, je remplace simplement mapProperties méthode.

class DataObj extends PropertyMapper<String> {
    private String v1 = null;
    private String v2 = null;
    ...

    DataObj(Map<String, String> data) {
        updateBean(data);
    }

    @Override
    void mapProperties() {
        mapProperty("V1", o -> v1 = o);
        mapProperty("V2", o -> v2 = o);
        ...
    }
}

Et c'est ce que je recherchais : un mécanisme astucieux permettant d'obtenir un code concis de mise en correspondance des propriétés et des champs.

2voto

Danilo M. Oliveira Points 525

Vous pouvez toujours éviter une déclaration de cas avec les cartes et les interfaces/classes abstraites + classes concrètes anonymes.

C'est toujours aussi moche, mais c'est très flexible, puisque vous pouvez ajouter d'autres setters au moment de l'exécution. Cela peut être moins laid avec des fonctions lambda. Vous pouvez imaginer des moyens astucieux de remplir cette carte.

C'est la seule alternative que je connaisse pour éviter la réflexion. Je fais cela dans un simulateur d'événements discrets que je suis en train d'écrire car la réflexion est lente par rapport à cette approche. De plus, avec la réflexion, vous ne pouvez pas obscurcir votre code.

public class PojoTest {

public int a;
public int b;
public int c;

private Map<String, Setter> setters = new HashMap<String, Setter>();

public PojoTest() {
initSetters();
}

public void set(Map<String, Integer> map) {
for (String s : map.keySet()) {
    setField(s, map.get(s));
}
}

public String toString() {
return a + ", " + b + ", " + c;
}

public static void main(String[] args) {
PojoTest t = new PojoTest();
Map<String, Integer> m = new HashMap<>();
m.put("a", 1);
m.put("b", 2);
m.put("c", 3);

t.set(m);

System.out.println(t);
}

private void setField(String s, Object o) {
setters.get(s).set(o);
}

private void initSetters() {
setters.put("a", new Setter() {
    @Override
    public void set(Object o) {
    a = (Integer) o;
    }
});

setters.put("b", new Setter() {
    @Override
    public void set(Object o) {
    b = (Integer) o;
    }
});

setters.put("c", new Setter() {
    @Override
    public void set(Object o) {
    c = (Integer) o;
    }
});
}

private static interface Setter {

public void set(Object o);
}

}

2voto

slim Points 12620

Je crains que Reflection ne soit le seul moyen d'obtenir une traduction programmatique d'une chaîne vers un champ ou une méthode au moment de l'exécution.

Apache Commons BeanUtils fournit des méthodes telles que setProperty(Object bean, String name, Object value) . Cela signifie que setProperty(myObj, "foo", "bar") appellera myObj.setFoo("bar") . Bien sûr, il utilise Reflection en coulisses.

Ou vous pouvez prendre du recul et vous demander pourquoi vous utilisez ce modèle POJO en premier lieu. Si votre programme pouvait utiliser un Map<String,String> ou un Properties à la place, ce problème disparaît.

Bien sûr, il serait assez trivial d'automatiser l'écriture de votre switch déclaration. Dans bash :

while read fieldname; do
    cat << EOF
case("${fieldname}"):
   this.$fieldname = data.get(key);
   break;
EOF
done

(Ajoutez vos propres casses ou autres)

1voto

slim Points 12620

Je sais que vous avez demandé des méthodes sans Reflection, mais le faire avec est très simple :

public class PojoWithValues {

    private String v1;
    private String v2;
    private String v3;

    public void configureWithReflection(Map<String, String> values)
            throws IllegalAccessException, IllegalArgumentException {

        Field[] fields = PojoWithValues.class.getDeclaredFields();

        for (Field field : fields) {
            String name = field.getName();
            field.set(this, values.get(name));
        }
    }

}

Vous pouvez bien sûr passer en boucle par values en fonction du nombre de champs et du nombre de champs que l'on s'attend à trouver dans une carte d'entrée.

Dans le monde réel, vous devrez travailler sur ce point, afin de filtrer les champs de mauvais type ou les champs qui ne doivent pas être paramétrables.

Comme j'étais curieux de connaître les performances, j'ai ajouté deux autres méthodes d'initialisation, l'une qui n'utilise pas du tout la réflexion et l'autre qui met en cache la méthode d'initialisation. Field[] le tableau :

public class PojoWithValues {

    private String v1;
    private String v2;
    private String v3;

    private Field[] FIELDS;

    public PojoWithValues() {
        this.FIELDS = PojoWithValues.class.getDeclaredFields();
    }

    public void configureWithoutReflection(Map<String, String> values) {
        v1 = values.get("v1");
        v2 = values.get("v2");
        v3 = values.get("v3");
    }

    public void configureWithReflection(Map<String, String> values)
            throws IllegalAccessException, IllegalArgumentException {
        Field[] fields = PojoWithValues.class.getDeclaredFields();

        for (Field field : fields) {
            String name = field.getName();
            field.set(this, values.get(name));
        }
    }

    public void configureWithCache(Map<String, String> values) throws IllegalAccessException, IllegalArgumentException {
        for (Field field : FIELDS) {
            String name = field.getName();
            field.set(this, values.get(name));
        }
    }
}

Le profilage de ce plus de 100 000 invocations avec le profileur Netbeans :

                                Total time      Total time (CPU) Invocations
testInitWithReflection ()       507 ms (36.4%)  472 ms (36.9%)   100,000
testInitWithCache ()            480 ms (34.5%)  441 ms (34.5%)   100,000
testInitWithoutReflection ()    458 ms (32.9%)  419 ms (32.7%)   100,000

... donc les différences de performance sont mesurables mais pas importantes.


Le coût le plus important de l'utilisation de Reflection est la perte de la vérification au moment de la compilation et la fuite des données internes des classes.

0voto

Kai Points 962

Je pense que l'utilisation de la réflexion sera le moyen le plus propre et le plus direct de réaliser ce que vous essayez de faire ici si la carte de valeurs provient d'une autre classe Java. L'idée de base est de récupérer les champs de la classe, puis d'itérer sur ceux-ci pour obtenir les valeurs à affecter au nouvel objet.

Class<?> objClass = obj.getClass();
Field[] fields = objClass.getDeclaredFields();
//loop over array of fields and create your new object

La réponse à cette question peut vous être utile.

Ou bien si vous ne voulez vraiment pas utiliser la réflexion et qu'il vous est possible de lire les valeurs à partir d'un fichier de propriétés, vous pouvez alors appeler le constructeur du nouvel objet avec ces valeurs (voir l'exemple ci-dessous).

public class App {

    public static void main( String[] args ){

        App app = new App();
        app.createNewObjectFromProperties();

    }

    private void createNewObjectFromProperties() {
        Properties prop = new Properties();
        InputStream input = null;

        try {

            String filename = "your.properties";
            input = App.class.getResourceAsStream(filename);
            if(input==null){
                System.out.println("File not found: " + filename);
                return;
            }

            prop.load(input);

            String v1 = prop.getProperty("V1");
            String v2 = prop.getProperty("V2");
            NewObject newObject = new NewObject(v1, v2);

        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    private class NewObject {
        private String v1;
        private String v2;

        public NewObject(String v1, String v2) {
            this.v1 = v1;
            this.v2 = v2;
            System.out.println(v1);
            System.out.println(v2);
        }
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X