Переопределение универсальных методов Java

Я хотел создать интерфейс для копирования объекта в целевой объект того же класса. Простой способ - использовать кастинг:

import org.junit.Test;
import org.junit.internal.runners.JUnit4ClassRunner;
import org.junit.runner.RunWith;

@RunWith(JUnit4ClassRunner.class)
public class TestGenerics {
public static interface Copyable {
    public void copy(Copyable c);
}

public static class A implements Copyable {
    private String aField = "--A--";
    protected void innerCopy(Copyable c) {
        A a = (A)c;
        System.out.println(a.aField);
    }
    public void copy(Copyable c) {
        innerCopy(c);
    }
}

public static class B extends A {
    private String bField = "--B--";
    protected void innerCopy(Copyable c) {
        B b = (B)c;
        super.innerCopy(b);
        System.out.println(b.bField);
    }
}

@Test
public void testCopy() {
    Copyable b1 = new B();
    Copyable b2 = new B();
    b1.copy(b2);
}
}

Но также я нашел способ сделать это с помощью дженериков:

import org.junit.Test;
import org.junit.internal.runners.JUnit4ClassRunner;
import org.junit.runner.RunWith;

@RunWith(JUnit4ClassRunner.class)
public class TestGenerics {
    public static interface Copyable<T> {
        public void copy(T t);
    }

    public static class A<T extends A<?>> implements Copyable<T> {
        private String a = "--A--";
        public void copy(T t) {
            System.out.println(t.a);
        }
    }

    public static class B<T extends B<?>> extends A<T> {
        private String b = "--B--";
        public void copy(T t) {
            super.copy(t);
            System.out.println(t.b);
        }
    }

    @Test
    @SuppressWarnings("unchecked")
    public void testCopy() {
        Copyable b1 = new B();
        Copyable b2 = new B();
        b1.copy(b2);
    }
}

Хотя единственный способ избавиться от предупреждений - это аннотация. И такое ощущение, что что-то не так. Так что же не так? Я могу признать, что что-то не так в корне проблемы. Так что любые разъяснения приветствуются.


person Oleg Galako    schedule 10.03.2009    source источник
comment
На какой строке вы получаете предупреждение и что оно говорит?   -  person Michael Myers    schedule 10.03.2009
comment
все 3 строки внутри testCopy() выдают предупреждения о том, что ссылки на общий тип должны быть параметризованы   -  person Oleg Galako    schedule 11.03.2009


Ответы (6)


Ваше определение интерфейса:

public interface Copyable<T extends Copyable<T>> {
    void copy(T copyFrom);
}

Ваша реализация:

public class Example implements Copyable<Example> {
    private Object data;
    void copy(Example copyFrom) {
        data = copyFrom.data;
    }
    //nontrivial stuff
}

Это должно позаботиться о ваших предупреждениях.

person wowest    schedule 11.03.2009
comment
Да, это то, с чего я начал, но как теперь расширить класс Example, чтобы copy(...) работало так же? - person Oleg Galako; 11.03.2009

Предполагая, что вы не хотите создавать подклассы, вам просто нужно:

public static /*final*/ class AClass implements Copyable<AClass> {

Для абстрактного класса вы делаете вещь "enum":

public static abstract class AClass<T extends AClass<T>> implements Copyable<T> {
person Tom Hawtin - tackline    schedule 10.03.2009
comment
Ваш код похож на то, что я опубликовал. Но я до сих пор не понимаю 2 вещи. Во-первых: что и как мне звонить, чтобы не было предупреждений? Во-вторых: подходит ли мой способ использования дженериков для этого случая или он выглядит просто как плохой код, который работает так, как задумано? - person Oleg Galako; 11.03.2009
comment
А также, что означает [перечисление вещи]? - person Oleg Galako; 11.03.2009
comment
Вы бы назвали конструктор первым примером с new AClass. Во втором примере вы должны создать подкласс, используя форму, аналогичную первой. Что касается перечисления — посмотрите на определение java.lang.Enum (Enum‹E расширяет Enum‹E››). - person Tom Hawtin - tackline; 11.03.2009
comment
Спасибо! Ваш указатель на java.lang.Enum (Enum‹E расширяет Enum‹E›› помог мне найти больше полезной информации по этому вопросу. - person Oleg Galako; 11.03.2009

В testCopy одно из предупреждений связано с тем, что вы создаете экземпляр «сырого типа» Copyable, а не какой-то конкретный Copyable‹T›. После того, как вы создадите экземпляр Copyable, его можно будет применить только к T (включая подтипы T). Чтобы создать экземпляр с формальным типом, необходимо немного изменить определения классов:

public static class A<T extends A> implements Copyable<T>
public static class B<T extends B> extends A<T>

Следующая проблема заключается в том, что Copyable‹B› может быть передан только тип времени компиляции B (на основе определения Copyable). И testCopy() выше передает ему тип Copyable времени компиляции. Ниже приведены несколько примеров того, что будет работать, с краткими описаниями:

public void testExamples()
{
    // implementation of A that applies to A and subtypes
    Copyable<A> aCopier = new A<A>();

    // implementation of B that applies to B and subtypes
    Copyable<B> bCopier = new B<B>();

    // implementation of A that applies to B and subtypes
    Copyable<B> bCopier2 = new A<B>();
}
person Community    schedule 11.03.2009
comment
A‹A›, A‹B› и т. д. дают одно и то же предупреждение, потому что типы в скобках по-прежнему являются дженериками. - person Oleg Galako; 11.03.2009

Я продолжаю пытаться найти способ избавиться от предупреждений в вашем первом подходе, и я не могу придумать ничего, что сработает. Тем не менее, я думаю, что первый подход — меньшее из двух зол. Небезопасное приведение лучше, чем необходимость дать вашим классам такой сложный API.

Совершенно отдельный подход состоял бы в том, чтобы переопределить Object.clone() и реализовать Cloneable.

person Alex Rockwell    schedule 11.03.2009
comment
Да, я думаю то же самое о меньшем из двух зол. Но все же интересно узнать, близок ли этот способ использования дженериков к тому, для чего они предназначены, или это просто плохой код, который работает. Cloneable не решит мою проблему, потому что мне нужно скопировать существующий объект. - person Oleg Galako; 11.03.2009

Это наилучший возможный код второго подхода. Он компилируется без каких-либо предупреждений.

import static org.junit.Assert.fail;

import org.junit.Test;
import org.junit.internal.runners.JUnit4ClassRunner;
import org.junit.runner.RunWith;

@RunWith(JUnit4ClassRunner.class)
public class TestGenerics {
    public static interface Copyable<T> {
        public void copy(T t);
    }

    public static class A<T extends A<T>> implements Copyable<T> {
        private String a = "--A--";
        public void copy(T t) {
            System.out.println(t.a);
        }
        @SuppressWarnings("unchecked")
        public static Copyable<Object> getInstance() {
            return new A();
        }

    }

    public static class B<T extends B<T>> extends A<T> {
        private String b = "--B--";
        public void copy(T t) {
            super.copy(t);
            System.out.println(t.b);
        }
        @SuppressWarnings("unchecked")
        public static Copyable<Object> getInstance() {
            return new B();
        }
    }


    @Test
    public void testCopy() {
        Copyable<Object> b1 = B.getInstance();
        Copyable<Object> b2 = B.getInstance();
        Copyable<Object> a = A.getInstance();
        b1.copy(b2); // this works as intended
        try {
            b1.copy(a); // this throws ClassCastException
            fail();
        } catch (ClassCastException cce) {
        }
    }
}

А также я разобрался со всем, что происходит в этой программе, с помощью рефлексии:

       for (Method method : A.class.getMethods()) {
               if (method.getName().equals("copy")) {
                       System.out.println(method.toString());
               }

       }
       for (Method method : B.class.getMethods()) {
               if (method.getName().equals("copy")) {
                       System.out.println(method.toString());
               }

       }

Вот результат:

public void com.sbp.core.TestGenerics$A.copy(com.sbp.core.TestGenerics$A)
public void com.sbp.core.TestGenerics$A.copy(java.lang.Object)

public void com.sbp.core.TestGenerics$B.copy(com.sbp.core.TestGenerics$B)
public void com.sbp.core.TestGenerics$B.copy(com.sbp.core.TestGenerics$A)
public void com.sbp.core.TestGenerics$A.copy(java.lang.Object)

Это означает, что:

  1. Методы copy(...) в A и B заставляют компилятор генерировать "мосты" - 2 разных метода для каждого, один с reifed типом аргумента от предка (reified T из Copyable становится Object, reified "T extends A" из A становится A ) и именно поэтому он переопределяет, а не перегружает, а другой с овеществленным типом аргумента для определения класса. Первый метод (с автоматически сгенерированным телом) преобразует свой аргумент для вызова второго (они называют его мостом). Из-за этого понижения мы получаем ClassCastException во время выполнения, если мы вызываем b1.copy(a).

  2. Похоже, что прямое приведение типов является более чистым и лучшим инструментом для моей проблемы, а дженерики лучше использовать по прямому назначению — обеспечить проверку типов во время компиляции.

person Oleg Galako    schedule 11.03.2009

Я изучил Scala и теперь знаю, что то, что я хотел 2 года назад, можно было бы достичь с помощью параметра контравариантного типа и системы типов Scala:

trait CopyableTo[-T] {
  def copyTo(t: T)
}

class A(private var a: Int) extends CopyableTo[A] {
  override def copyTo(t: A) {
    println("A:copy")
    t.a = this.a
  }
}

class B(private var a: Int, private var b: Int) extends A(a) with CopyableTo[B] {
  def copyTo(t: B) {
    println("B:copy")
    super.copyTo(t)
    t.b = this.b
  }
}

@Test
def zzz {
  val b1 = new B(1, 2)
  val a1 = new A(3)
  val b2 = new B(4, 5)
  b1.copyTo(a1)
  a1.copyTo(b1)
  b1.copyTo(b2)
}

Система типов Java слишком слаба для этого.

person Oleg Galako    schedule 22.02.2011