Java:从父对象创建子类对象

46
新手Java问题。假设我有以下代码:
public class Car{
  ...
}

public class Truck extends Car{
  ...
}

假设我已经有了一个Car对象,如何从这个Car对象创建一个新的Truck对象,以便将Car对象的所有值复制到我的新Truck对象中? 理想情况下,我可以像这样做:
Car c = new Car();
/* ... c gets populated */

Truck t = new Truck(c);
/* would like t to have all of c's values */

我需要编写自己的拷贝构造函数吗?每次Car添加新字段都必须更新拷贝构造函数...

11个回答

41

是的,只需在Truck中添加构造函数即可。您可能还想为Car添加构造函数,但不一定需要公开:

public class Car {
    protected Car(Car orig) {
    ...
}

public class Truck extends Car {
    public Truck(Car orig) {
        super(orig);
    }
    ...
}

一般来说,最好将类定义为叶子类(你可能需要将这些标记为final)或抽象类。

看起来你想要一个Car对象,然后让同一个实例变成一个Truck。更好的方法是在Car中委托行为给另一个对象(Vehicle)。所以:

public final class Vehicle {
    private VehicleBehaviour behaviour = VehicleBehaviour.CAR;

    public void becomeTruck() {
        this.behaviour =  VehicleBehaviour.TRUCK;
    } 
    ...
}

如果你实现了Cloneable接口,那么你可以将一个对象 "自动" 复制到同一类的实例中。然而这样做存在许多问题,其中包括必须复制可变对象的每个字段,这容易出错并禁止使用 final。


实现 Clonable 可能是一个不好的主意。 - jpaugh
@jpaugh,你的链接已经六年了... 你确定 Clonable 在此期间没有改进吗? - Mindwin Remember Monica
@Mindwin 我怀疑现在有很好的Clonable替代品。使Clonable变得糟糕的是其API的一部分。如果更改它,就会破坏向后兼容性。我找到了各种引用,它们都与上述内容相同,日期不同。 - jpaugh
@jpaugh 所以它就像牛排上那块绵软的肉,孩子们把它推到盘子的角落里,因为他们不想处理它。 - Mindwin Remember Monica
@Mindwin Clonable可能更像我妈妈经常做的牛排:烤得焦脆——没有任何细长的东西!顺便说一下,我对这个主题的记忆自去年十月以来就没有被刷新过。 - jpaugh

9
如果你的项目中使用了Spring,你可以使用ReflectionUtils。

从初始测试来看,这个工作非常出色。既然Spring已经为您完成了这些事情,为什么还要做其他的呢? - Bryan Larson
4
例如,ReflectionUtils.shallowCopyFieldState(baseObj, extendedObj) 可以使用。 - Utku A.

4
我需要写自己的复制构造函数吗?每次Car添加一个新的字段都要更新它吗?
完全不需要!
可以试试这种方式:
public class Car{
    ...
}

public class Truck extends Car{
    ...

    public Truck(Car car){
        copyFields(car, this);
    }
}


public static void copyFields(Object source, Object target) {
        Field[] fieldsSource = source.getClass().getFields();
        Field[] fieldsTarget = target.getClass().getFields();

        for (Field fieldTarget : fieldsTarget)
        {
            for (Field fieldSource : fieldsSource)
            {
                if (fieldTarget.getName().equals(fieldSource.getName()))
                {
                    try
                    {
                        fieldTarget.set(target, fieldSource.get(source));
                    }
                    catch (SecurityException e)
                    {
                    }
                    catch (IllegalArgumentException e)
                    {
                    }
                    catch (IllegalAccessException e)
                    {
                    }
                    break;
                }
            }
        }
    }

1
由于 OP 想要复制所有字段,因此您的答案应进行两个更改:使用 getDeclaredFields 而不是 getFields,并访问 Parent 类的字段,而不是类本身:Field[] fieldsTarget = target.getClass().getSuperclass().getDeclaredFields(); - cabad

4
是的,你必须手动完成这个过程。你还需要决定如何“深度”复制事物。例如,假设汽车有一个轮胎集合 - 你可以对集合进行浅复制(这样,如果原始对象更改其集合的内容,新对象也会看到更改),或者你可以进行深度复制,从而创建一个新集合。
(这就是不可变类型(如String)经常发挥作用的地方 - 没有必要克隆它们;你只需复制引用并知道对象的内容不会更改。)

3
你可以使用反射来实现,我已经使用过并且运行良好:
public Child(Parent parent){
    for (Method getMethod : parent.getClass().getMethods()) {
        if (getMethod.getName().startsWith("get")) {
            try {
                Method setMethod = this.getClass().getMethod(getMethod.getName().replace("get", "set"), getMethod.getReturnType());
                setMethod.invoke(this, getMethod.invoke(parent, (Object[]) null));

            } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
                //not found set
            }
        }
    }
 }

2
“我是否需要编写自己的复制构造函数?这将不得不在Car获得新字段时更新...”基本上是的-你不能只是在Java中转换一个对象。

幸运的是,你不必自己编写所有代码-看看commons-beanutils,特别是像cloneBean这样的方法。这有一个额外的优点,即您不必每次它获得新字段时更新它!


1
你可以始终使用像Dozer这样的映射框架。默认情况下(不需要进一步配置),它使用getter和setter方法将一个对象中相同名称的所有字段映射到另一个对象中。
依赖项:
<dependency>
    <groupId>net.sf.dozer</groupId>
    <artifactId>dozer</artifactId>
    <version>5.5.1</version>
</dependency>

代码:

import org.dozer.DozerBeanMapper;
import org.dozer.Mapper;

// ...

Car c = new Car();
/* ... c gets populated */

Truck t = new Truck();
Mapper mapper = new DozerBeanMapper();
mapper.map(c, t);
/* would like t to have all of c's values */

0
上述解决方案存在一些限制,您应该注意。以下是从一个类复制字段的算法的简短摘要。
  • Tom Hawtin:如果您的超类具有复制构造函数,请使用此选项。如果没有,则需要其他解决方案。
  • Christian:如果超类没有扩展任何其他类,请使用此选项。此方法不会递归向上复制字段。
  • Sean Patrick Floyd:这是递归向上复制所有字段的通用解决方案。请确保阅读@jett的评论,其中必须添加一行代码以防止无限循环。

我使用缺失语句复制了Sean Patrick Floyd的analyze函数:

private static Map<String, Field> analyze(Object object) {
    if (object == null) throw new NullPointerException();

    Map<String, Field> map = new TreeMap<String, Field>();

    Class<?> current = object.getClass();
    while (current != Object.class) {
        Field[] declaredFields = current.getDeclaredFields();
        for (Field field : declaredFields) {
            if (!Modifier.isStatic(field.getModifiers())) {
                if (!map.containsKey(field.getName())) {
                    map.put(field.getName(), field);
                }
            }
        }

        current = current.getSuperclass();   /* The missing statement */
    }
    return map;
}

0
你需要一个拷贝构造函数,但是你的拷贝构造函数可以使用反射来查找两个对象之间的公共字段,在“原型”对象中获取它们的值,并将它们设置在子对象上。

0
您可以使用反射API循环遍历每个车辆字段并将值分配给对应的卡车字段。这可以在卡车内完成。此外,这是访问汽车的私有字段的唯一方法-至少在自动情况下,前提是没有安全管理器限制对私有字段的访问。

1
是的,您可以使用反射或可能使用一些现有框架来完成此类操作。但要小心反射。在某些情况下它很好用,但仍存在一些性能问题,因此请勿在高负载情况下使用它。 - Don Branson

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