通过反射将一个类中的所有字段值复制到另一个类中

94

我有一个类,基本上是另一个类的复制。

public class A {
  int a;
  String b;
}

public class CopyA {
  int a;
  String b;
}

我正在做的是在通过Web服务调用之前,将类A中的值放入CopyA中。现在我想创建一个反射方法,基本上是将与类A相同(按名称和类型)的所有字段从类A复制到类CopyA。
我该如何做呢?
到目前为止,我所拥有的代码并不能完美地实现这一功能。我认为问题在于我试图在循环遍历的字段上设置一个字段。
private <T extends Object, Y extends Object> void copyFields(T from, Y too) {

    Class<? extends Object> fromClass = from.getClass();
    Field[] fromFields = fromClass.getDeclaredFields();

    Class<? extends Object> tooClass = too.getClass();
    Field[] tooFields = tooClass.getDeclaredFields();

    if (fromFields != null && tooFields != null) {
        for (Field tooF : tooFields) {
            logger.debug("toofield name #0 and type #1", tooF.getName(), tooF.getType().toString());
            try {
                // Check if that fields exists in the other method
                Field fromF = fromClass.getDeclaredField(tooF.getName());
                if (fromF.getType().equals(tooF.getType())) {
                    tooF.set(tooF, fromF);
                }
            } catch (SecurityException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (NoSuchFieldException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }

        }
    }

我相信已经有人以某种方式完成了这个。

2
请参见https://dev59.com/p3M_5IYBdhLWcg3wThR3。 - Ruben Bartelink
使用Apache Jakarta中的BeanUtils。 - Shaun
19个回答

118

如果你不介意使用第三方库,Apache Commons 的BeanUtils将很容易处理这个问题,使用copyProperties(Object, Object)方法。


15
显然,BeanUtils无法处理空的日期字段。如果这对您造成问题,请使用Apache PropertyUtils:http://www.mail-archive.com/user@commons.apache.org/msg02246.html - ripper234
11
似乎这对于没有getter和setter的私有字段无效。是否有任何直接操作字段而非属性的解决方案? - Andrea Ratto
它不能使用没有getter的公共字段工作:https://dev59.com/DJLea4cB1Zd3GeqPxAYE - Vadzim

20
为什么不使用gson库 https://github.com/google/gson

您只需将Class A转换为json字符串,然后使用以下代码将json字符串转换为您的子类(CopyA)。

Gson gson= new Gson();
String tmp = gson.toJson(a);
CopyA myObject = gson.fromJson(tmp,CopyA.class);

1
为什么要生成另一个可能也很大的字符串?这里有更好的替代方案。至少我们(行业)已经从XML进步到了json作为字符串表示,但我们仍然不希望在任何时候都将所有内容传递给该字符串表示形式... - arntg
1
请注意,使用反射时字符串是一个副产品。即使您没有保存它!!这是针对Java初学者的答案,旨在简洁明了。@arntg - Eric Ho
1
你仍然需要对源对象和目标对象进行反射,但现在你还要引入二进制/文本/二进制格式化和解析开销。 - Evvo
1
请注意,如果您使用Proguard混淆代码,则此代码将无法正常工作。 - SebaReal
这对我的情况很有用,谢谢。 - Mugeesh Husain

11

BeanUtils只会复制公共字段,而且速度有点慢。相反,使用getter和setter方法。

public Object loadData (RideHotelsService object_a) throws Exception{

        Method[] gettersAndSetters = object_a.getClass().getMethods();

        for (int i = 0; i < gettersAndSetters.length; i++) {
                String methodName = gettersAndSetters[i].getName();
                try{
                  if(methodName.startsWith("get")){
                     this.getClass().getMethod(methodName.replaceFirst("get", "set") , gettersAndSetters[i].getReturnType() ).invoke(this, gettersAndSetters[i].invoke(object_a, null));
                        }else if(methodName.startsWith("is") ){
                            this.getClass().getMethod(methodName.replaceFirst("is", "set") ,  gettersAndSetters[i].getReturnType()  ).invoke(this, gettersAndSetters[i].invoke(object_a, null));
                        }

                }catch (NoSuchMethodException e) {
                    // TODO: handle exception
                }catch (IllegalArgumentException e) {
                    // TODO: handle exception
                }

        }

        return null;
    }

只要getter/setter是公共的,BeanUtils在私有字段上运行得很好。关于性能,我没有进行任何基准测试,但我相信它会对已经内省的bean进行一些内部缓存。 - Greg Case
2
只有当这两个 Bean 具有相同的字段数据类型时,此方法才能正常工作。 - TimeToCodeTheRoad
@ Kra 如果您的字段没有getter/setter,那么它将不起作用。 - WoLfPwNeR

10

这里是一个可行且经过测试的解决方案。您可以控制类层次结构中映射的深度。

public class FieldMapper {

    public static void copy(Object from, Object to) throws Exception {
        FieldMapper.copy(from, to, Object.class);
    }

    public static void copy(Object from, Object to, Class depth) throws Exception {
        Class fromClass = from.getClass();
        Class toClass = to.getClass();
        List<Field> fromFields = collectFields(fromClass, depth);
        List<Field> toFields = collectFields(toClass, depth);
        Field target;
        for (Field source : fromFields) {
            if ((target = findAndRemove(source, toFields)) != null) {
                target.set(to, source.get(from));
            }
        }
    }

    private static List<Field> collectFields(Class c, Class depth) {
        List<Field> accessibleFields = new ArrayList<>();
        do {
            int modifiers;
            for (Field field : c.getDeclaredFields()) {
                modifiers = field.getModifiers();
                if (!Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers)) {
                    accessibleFields.add(field);
                }
            }
            c = c.getSuperclass();
        } while (c != null && c != depth);
        return accessibleFields;
    }

    private static Field findAndRemove(Field field, List<Field> fields) {
        Field actual;
        for (Iterator<Field> i = fields.iterator(); i.hasNext();) {
            actual = i.next();
            if (field.getName().equals(actual.getName())
                && field.getType().equals(actual.getType())) {
                i.remove();
                return actual;
            }
        }
        return null;
    }
}

1
我已经创建了类似的解决方案。我将一个类缓存到字段名到字段映射的映射中。 - Orden
解决方案很好,但是在这一行 i.remove() 你会遇到问题。即使你已经创建了迭代器,也不能在 Listiterator 上调用 remove。应该使用 ArrayList - Farid
Farid,删除不应该是一个问题,因为collectFields()创建了ArrayList对象。 - JHead

8

Spring内置有BeanUtils.copyProperties方法,但是它不能处理没有getter/setter的类。另一种复制字段的选择是JSON序列化/反序列化。可以使用Jackson来实现此目的。如果您正在使用Spring,则大多数情况下,Jackson已经在您的依赖列表中。

ObjectMapper mapper     = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
Clazz        copyObject = mapper.readValue(mapper.writeValueAsString(sourceObject), Clazz.class);

6

我的解决方案:

public static <T > void copyAllFields(T to, T from) {
        Class<T> clazz = (Class<T>) from.getClass();
        // OR:
        // Class<T> clazz = (Class<T>) to.getClass();
        List<Field> fields = getAllModelFields(clazz);

        if (fields != null) {
            for (Field field : fields) {
                try {
                    field.setAccessible(true);
                    field.set(to,field.get(from));
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }

public static List<Field> getAllModelFields(Class aClass) {
    List<Field> fields = new ArrayList<>();
    do {
        Collections.addAll(fields, aClass.getDeclaredFields());
        aClass = aClass.getSuperclass();
    } while (aClass != null);
    return fields;
}

我认为这对自定义对象无效。只有当你有一个没有父级和只有基本字段的类时,才有效。 - Shervin Asgari
1
为了覆盖超类字段,我正在使用“ getAllModelFields”自定义方法。 - Nik Kashi

6
这是一篇较晚的文章,但对于未来的人仍然有效。
Spring提供了一个实用程序BeanUtils.copyProperties(srcObj, tarObj),当两个类的成员变量名称相同时,它会从源对象复制值到目标对象。
如果存在日期转换(例如,从字符串到日期),则“null”将被复制到目标对象。我们可以根据需要显式设置日期值。
Apache Common中的BeanUtils在数据类型不匹配时(特别是在日期转换方面)会抛出错误。
希望这可以帮助你!

这并没有提供比 https://dev59.com/3HI-5IYBdhLWcg3wy72n#1667911 回答更多的信息。 - Sudip Bhandari

5
tooF.set()的第一个参数应该是目标对象(too),而不是字段,第二个参数应该是值,而不是值所在的字段。要获取值,需要调用fromF.get() -- 再次传入目标对象,在这种情况下为from
大多数反射API都是这样工作的。您可以从类中获取Field对象、Method对象等,而不是从实例中获取,因此要使用它们(除了静态成员),通常需要向它们传递一个实例。

4

3
  1. Without using BeanUtils or Apache Commons

  2. public static <T1 extends Object, T2 extends Object>  void copy(T1     
    origEntity, T2 destEntity) throws IllegalAccessException, NoSuchFieldException {
        Field[] fields = origEntity.getClass().getDeclaredFields();
        for (Field field : fields){
            origFields.set(destEntity, field.get(origEntity));
         }
    }
    

这不是一个可行的解决方案,但是一个很好的起点。需要过滤字段以处理仅存在于两个类中的非静态和公共字段。 - JHead
这样做会忽略父类中的字段吗? - Evvo

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接