Ошибка "с закрытым доступом" с генериками

У меня была проблема, которую я мог бы решить самостоятельно, но я до сих пор не понимаю, почему мой исходный код не работает, или если есть более элегантное решение, чем тот, который я нашел. Я представляю упрощенную версию моего кода здесь.

Рассмотрим следующий абстрактный суперкласс X:

public abstract class X{

  private int i;

  public void m1(X x){
    x.i = 1; 
    m2(x);
  }

  public abstract void m2(X x);

}

Когда вызывается m1, мы управляем частным полем X переданного экземпляра, а затем мы вызываем m2 с этим экземпляром.

У меня есть несколько подклассов X, они все одинаковы в том смысле, что они также объявляют частных членов, которыми они манипулируют. Чтобы достичь этого, им всегда нужно сделать бросок в начале м2. Вот один из них:

public class Y extends X{

  private int j;

  public void m2(X x){
     Y y = (Y) x;
     y.j = 0;
  }

}

Но - я могу гарантировать, что каждый вызов m1 экземпляра подкласса X всегда будет иметь параметр того же типа, например. когда у меня есть экземпляр Y, параметр метода m1 всегда будет другим экземпляром Y.

Из-за этой гарантии я хотел сделать ненужным бросок, введя дженерики. Вот как я хочу, чтобы мои подклассы выглядели следующим образом:

public class Y extends X<Y>{

  private int j;

  public void m2(Y y){
     y.j = 0;
  }

}

Как выглядит суперкласс X теперь? Моя первая попытка заключалась в том, что:

public abstract class X<T extends X<T>>{

  private int i;

  public void m1(T x){
    x.i = 1; 
    m2(x);
  }

  public abstract void m2(T x);

}

Но - это не работает, когда я скомпилирую это, я получаю следующую ошибку:

X.java:6: error: i has private access in X

Обычно вы пытаетесь получить доступ к закрытым членам другого класса. Очевидно, что Java не признает, что T всегда является экземпляром X, хотя я использовал "T extends X" в объявлении.

Я зафиксировал X следующим образом:

public abstract class X<T extends X<T>>{

  private int i;

  public void m1(T x){
    X<?> y = x;
    y.i = 1; 
    m2(x);
  }

  public abstract void m2(T x);

}

По крайней мере, я больше не использую роли, но почему это дополнительное назначение необходимо? И почему исходный код не работал? Кроме того, мне показалось странным, что я должен был использовать X<?> и не мог использовать X<T>.

Ответ 1

Я считаю, что мы можем уменьшить ваш вопрос до: Почему следующий пример не удается скомпилировать?

public class Foo {  
   private final String bar = "bar";

   public <T extends Foo> void printFoo(T baz) {
      System.out.println(baz.bar); //bar is not visible
   }
}

Это отличный вопрос, и это наверняка застало меня врасплох. Но мы можем фактически удалить Generics из уравнения, заметив, что это тоже не работает:

public class Foo {

    private final String bar = "bar";

    public void printFoo(SubFoo baz) {
        System.out.println(baz.bar); //bar is not visible
    }
}

class SubFoo extends Foo {      
}

Другими словами, проблема в том, что вы имеете дело с подклассом Foo, а не Foo. В случае T мы не знаем, какой подкласс, но мы знаем, что это подкласс или Foo.

Как вы уже поняли, решение (неожиданно, по крайней мере, для меня) - это upcast:

System.out.println(((Foo)baz).bar);

Или для общего случая:

public <T extends Foo> void printFoo(T baz) {
    System.out.println(((Foo)baz).bar);
}

Является ли бросок настолько плохим? На самом деле, нет. Это конечно же хорошо или лучше, чем избегать приведения с промежуточной переменной. Как и при любом повышении, я предполагаю, что он будет удален компилятором. Он существует только как подсказка для компилятора. Мы, конечно, не должны беспокоиться о безопасности приведения, потому что стирание T уже Foo.

Я могу только предположить, что это ограничение требуется для того, чтобы быть понятным о доступе... поскольку SubFoo может обновлять bar сам, он может стать двусмысленным, к которому относится bar, и поэтому листинг необходимо. Это продемонстрировано в этом сложном примере:

public class Foo {

    private final String bar = "hello";


    static class SubFoo extends Foo {
        private final String bar = "world";
    }

    public <T extends SubFoo> void printFoo(T baz) {
//      System.out.println(baz.bar); // doesn't compile
        System.out.println(((Foo)baz).bar); //hello
        System.out.println(((SubFoo)baz).bar); //world
    }

    public static void main(String[] args) {
        new Foo().printFoo(new SubFoo()); //prints "hello\nworld"
    }
}

В этом отношении он служит скорее как квалификатор, чем как литой.