Como substituir corretamente o método de clone?

114

Preciso implementar um clone profundo em um dos meus objetos que não tem superclasse.

Qual é a melhor maneira de lidar com o check CloneNotSupportedExceptionlançado pela superclasse (que é Object)?

Um colega de trabalho me aconselhou a lidar com isso da seguinte maneira:

@Override
public MyObject clone()
{
    MyObject foo;
    try
    {
        foo = (MyObject) super.clone();
    }
    catch (CloneNotSupportedException e)
    {
        throw new Error();
    }

    // Deep clone member fields here

    return foo;
}

Esta parece ser uma boa solução para mim, mas eu gostaria de jogá-la na comunidade StackOverflow para ver se há outras informações que posso incluir. Obrigado!

Cuga
fonte
6
Se você sabe que a classe pai implementa Cloneable, lançar um em AssertionErrorvez de apenas um plano Erroré um pouco mais expressivo.
Andrew Duffy
Edit: Prefiro não usar clone (), mas o projeto já é baseado nele e não valeria a pena refatorar todas as referências neste ponto.
Cuga
Melhor morder a bala agora do que mais tarde (bem, a menos que você esteja prestes a atingir a produção).
Tom Hawtin - tackline
Temos um mês e meio restante de programação até que isso seja concluído. Não podemos justificar o tempo.
Cuga
Acho que essa solução é perfeita. E não se sinta mal por usar clone. Se você tem uma classe que é usada como um objeto de transporte e contém apenas primitves e imutáveis ​​como String e Enums, tudo o mais além de clone seria uma completa perda de tempo por razões dogmáticas. Basta ter em mente o que o clone faz e o que não faz! (sem clonagem profunda)
TomWolk

Respostas:

125

Você absolutamente tem que usar clone? A maioria das pessoas concorda que o Java cloneestá quebrado.

Josh Bloch no projeto - Construtor de cópia versus clonagem

Se você leu o item sobre clonagem em meu livro, especialmente se você leu nas entrelinhas, você saberá que acho que cloneestá profundamente quebrado. [...] é uma pena que Cloneableestá quebrado, mas acontece.

Você pode ler mais discussões sobre o tópico em seu livro Effective Java 2nd Edition, Item 11: Override clonejudiciosamente . Ele recomenda, em vez disso, usar um construtor de cópias ou uma fábrica de cópias.

Ele escreveu páginas de páginas sobre como, se você sentir que deve, você deve implementar clone. Mas ele fechou com isso:

Todas essas complexidades são realmente necessárias? Raramente. Se você estender uma classe que implementa Cloneable, terá pouca escolha a não ser implementar um clonemétodo bem comportado . Caso contrário, é melhor fornecer meios alternativos de cópia de objetos ou simplesmente não fornecer esse recurso .

A ênfase era dele, não minha.


Como você deixou claro que não tem escolha a não ser implementar clone, aqui está o que você pode fazer neste caso: certifique-se disso MyObject extends java.lang.Object implements java.lang.Cloneable. Se for esse o caso, você pode garantir que NUNCA pegará a CloneNotSupportedException. Lançar AssertionErrorcomo alguns sugeriram parece razoável, mas você também pode adicionar um comentário que explica por que o bloco catch nunca será inserido neste caso específico .


Como alternativa, como outros também sugeriram, talvez você possa implementar clonesem chamar super.clone.

poligenelubrificantes
fonte
1
Infelizmente, o projeto já foi escrito usando o método clone, caso contrário, eu absolutamente não o usaria. Eu concordo inteiramente com você que a implementação de clone do Java é fakakta.
Cuga
5
Se uma classe e todas as suas superclasses chamam super.clone()dentro de seus métodos de clone, uma subclasse geralmente só terá que substituir clone()se adicionar novos campos cujo conteúdo precisaria ser clonado. Se alguma superclasse usar em newvez de super.clone(), então todas as subclasses devem sobrescrever clone()se adicionam ou não novos campos.
supercat
56

Às vezes, é mais simples implementar um construtor de cópia:

public MyObject (MyObject toClone) {
}

Economiza o trabalho de manuseio CloneNotSupportedException, trabalha com finalcampos e você não precisa se preocupar com o tipo a retornar.

Aaron Digulla
fonte
11

A maneira como seu código funciona é muito parecida com a maneira "canônica" de escrevê-lo. Eu jogaria um AssertionErrordentro da captura, no entanto. Sinaliza que essa linha nunca deve ser alcançada.

catch (CloneNotSupportedException e) {
    throw new AssertionError(e);
}
Chris Jester-Young
fonte
Veja a resposta de @polygenelubricants para saber por que isso pode ser uma má ideia.
Karl Richter
@KarlRichter eu li a resposta deles. Não quer dizer que seja uma má ideia, exceto que, Cloneableem geral, é uma ideia quebrada, conforme explicado em Effective Java. O OP já expressou que eles têm que usar Cloneable. Portanto, não tenho ideia de como minha resposta poderia ser melhorada, exceto talvez excluí-la de uma vez.
Chris Jester-Young
9

Existem dois casos em que o CloneNotSupportedExceptionserá lançado:

  1. A classe que está sendo clonada não é implementada Cloneable(assumindo que a clonagem real eventualmente adie para Objecto método de clone de). Se a classe que você está escrevendo este método em implementa Cloneable, isso nunca acontecerá (já que qualquer subclasse irá herdá-lo apropriadamente).
  2. A exceção é lançada explicitamente por uma implementação - esta é a maneira recomendada de prevenir a clonagem em uma subclasse quando a superclasse é Cloneable.

O último caso não pode ocorrer em sua classe (já que você está chamando diretamente o método da superclasse no trybloco, mesmo se invocado a partir de uma chamada de subclasse super.clone()) e o primeiro não deve, já que sua classe claramente deve implementar Cloneable.

Basicamente, você deve registrar o erro com certeza, mas neste caso em particular isso só acontecerá se você bagunçar a definição da sua classe. Portanto, trate-o como uma versão verificada de NullPointerException(ou semelhante) - ele nunca será lançado se seu código for funcional.


Em outras situações, você precisa estar preparado para esta eventualidade - não há garantia de que um determinado objeto seja clonável, portanto, ao capturar a exceção, você deve tomar as medidas adequadas dependendo desta condição (continue com o objeto existente, execute uma estratégia de clonagem alternativa por exemplo, serializar-desserializar, lançar um IllegalParameterExceptionse seu método requer o parâmetro por clonável, etc. etc.).

Edit : Embora no geral eu deva apontar que sim, clone()realmente é difícil implementar corretamente e para os chamadores saber se o valor de retorno será o que eles desejam, duplamente quando você considera clones profundos versus clones rasos. Muitas vezes, é melhor apenas evitar a coisa toda e usar outro mecanismo.

Andrzej Doyle
fonte
Se um objeto expõe um método de clonagem público, qualquer objeto derivado que não o suporte violaria o Princípio de Substituição de Liskov. Se um método de clonagem estiver protegido, eu pensaria que seria melhor sombreado com algo diferente de um método que retorne o tipo apropriado, para evitar que uma subclasse sequer tente chamar super.clone().
supercat
5

Use a serialização para fazer cópias profundas. Esta não é a solução mais rápida, mas não depende do tipo.

Michał Ziober
fonte
Esta foi uma solução ótima e óbvia, considerando que eu já estava usando GSON.
Jacob Tabak
3

Você pode implementar construtores de cópia protegida como:

/* This is a protected copy constructor for exclusive use by .clone() */
protected MyObject(MyObject that) {
    this.myFirstMember = that.getMyFirstMember(); //To clone primitive data
    this.mySecondMember = that.getMySecondMember().clone(); //To clone complex objects
    // etc
}

public MyObject clone() {
    return new MyObject(this);
}
Dolph
fonte
3
Isso não funciona na maioria das vezes. Você não pode chamar clone()o objeto retornado por, a getMySecondMember()menos que tenha um public clonemétodo.
poligenelubrificantes
Obviamente, você precisa implementar esse padrão em cada objeto que deseja clonar ou encontrar outra maneira de clonar profundamente cada membro.
Dolph
Sim, esse é um requisito necessário.
poligenelubrificantes
1

Por mais que a maioria das respostas aqui sejam válidas, preciso dizer que sua solução também é como os desenvolvedores de API Java reais fazem. (Josh Bloch ou Neal Gafter)

Aqui está um extrato de openJDK, classe ArrayList:

public Object clone() {
    try {
        ArrayList<?> v = (ArrayList<?>) super.clone();
        v.elementData = Arrays.copyOf(elementData, size);
        v.modCount = 0;
        return v;
    } catch (CloneNotSupportedException e) {
        // this shouldn't happen, since we are Cloneable
        throw new InternalError(e);
    }
}

Como você notou e outros mencionaram, CloneNotSupportedExceptionquase não tem chance de cair se você declarar que implementou a Cloneableinterface.

Além disso, não há necessidade de substituir o método se você não fizer nada de novo no método substituído. Você só precisa substituí-lo quando precisar fazer operações extras no objeto ou torná-lo público.

Em última análise, ainda é melhor evitá-lo e fazê-lo de outra forma.

Haggra
fonte
0
public class MyObject implements Cloneable, Serializable{   

    @Override
    @SuppressWarnings(value = "unchecked")
    protected MyObject clone(){
        ObjectOutputStream oos = null;
        ObjectInputStream ois = null;
        try {
            ByteArrayOutputStream bOs = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(bOs);
            oos.writeObject(this);
            ois = new ObjectInputStream(new ByteArrayInputStream(bOs.toByteArray()));
            return  (MyObject)ois.readObject();

        } catch (Exception e) {
            //Some seriouse error :< //
            return null;
        }finally {
            if (oos != null)
                try {
                    oos.close();
                } catch (IOException e) {

                }
            if (ois != null)
                try {
                    ois.close();
                } catch (IOException e) {

                }
        }
    }
}
Kvant_hv
fonte
Você acabou de escrever no sistema de arquivos e leu o objeto de volta. Ok, este é o melhor método para lidar com clones? Alguém da comunidade SO pode comentar sobre esta abordagem? Eu acho que isso amarra desnecessariamente Clonagem e Serialização - dois conceitos totalmente diferentes. Vou esperar para ver o que os outros têm a dizer sobre isso.
Saurabh Patil
2
Não está no sistema de arquivos, mas na memória principal (ByteArrayOutputStream). para objetos profundamente aninhados, é a solução que funciona bem. Especialmente se você não precisa disso com frequência, por exemplo, em testes de unidade. onde o desempenho não é o objetivo principal.
AlexWien
0

Só porque a implementação do Java de Cloneable está quebrada, não significa que você não pode criar um por conta própria.

Se o propósito real do OP era criar um clone profundo, acho que é possível criar uma interface como esta:

public interface Cloneable<T> {
    public T getClone();
}

em seguida, use o construtor de protótipo mencionado antes para implementá-lo:

public class AClass implements Cloneable<AClass> {
    private int value;
    public AClass(int value) {
        this.vaue = value;
    }

    protected AClass(AClass p) {
        this(p.getValue());
    }

    public int getValue() {
        return value;
    }

    public AClass getClone() {
         return new AClass(this);
    }
}

e outra classe com um campo de objeto AClass:

public class BClass implements Cloneable<BClass> {
    private int value;
    private AClass a;

    public BClass(int value, AClass a) {
         this.value = value;
         this.a = a;
    }

    protected BClass(BClass p) {
        this(p.getValue(), p.getA().getClone());
    }

    public int getValue() {
        return value;
    }

    public AClass getA() {
        return a;
    }

    public BClass getClone() {
         return new BClass(this);
    }
}

Desta forma, você pode facilmente clonar profundamente um objeto da classe BClass sem a necessidade de @SuppressWarnings ou outro código enganoso.

Francesco Rogo
fonte