Почему java требует приведения для создания экземпляра параметра ограниченного типа в его верхний класс ограничения? - PullRequest
5 голосов
/ 27 сентября 2019

Java требует, чтобы экземпляр параметра ограниченного типа в своем классе верхней границы имел приведение, например:

<T extends Integer> void passVal (T t) {
    Integer number = 5;
    t = (T) number; // Without cast a compile error is issued
}

T уже ограничен Integer или одним из его подклассов(Я знаю, что их нет) при объявлении, и мне кажется, что любой параметр ограниченного типа может быть создан только для своего класса верхней границы или одного из его подклассов, так почему же требуется приведение?

Кроме того, я знаю, что это не тот случай, если я создаю экземпляр параметра с помощью вызова вне этого метода, поэтому не предлагайте это как ответ;Этот вопрос относится к конкретным параметрам ограниченного типа, которые создаются в их объявленном классе / методах.

Ответы [ 2 ]

8 голосов
/ 27 сентября 2019

T не означает Integer, оно должно быть допустимым для Integer или любого класса, который происходит от него.Допустим, что StrangeInteger расширяется от Integer и заменяет T на StrangeInteger:

void passVal (StrangeInteger t) {
    Integer number = 5;
    t = (StrangeInteger) number;
}

Он пытается присвоить переменную Integer переменной StrangeInteger, вы не можете сделать это, если число не было StrangeInteger или производным классом впервое место.Фактически, ваш код должен (концептуально) генерировать исключение во время выполнения, если t не является целым числом, но на самом деле он не будет этого делать в этом случае из-за стирания типа (см. Редактирование 2).

Ситуация аналогичнаto:

Object obj = "Hello"
String t = (String)obj; // this will not fail, but requires a cast

Object obj2 = getDBConnection();
String t2 = (String)obj2; // this will fail at runtime

Edit: Integer действительно финальный, поэтому T может быть только Integer, но возможно, что компилятор не проверяет, является ли верхняя граница конечной, в конце концов, для верхнего нет никакого смысладолжен быть окончательным, поэтому допущение того, что этот особый случай добавляет сложности для очень небольшого реального выигрыша.

Редактировать 2: английский не мой родной язык, поэтому я могу быть не совсем понятен.

Я думаю, что выборются с разницей между использованием универсального типа с верхней границей и использованием только верхней границы в качестве типа.Идея обобщений (из C ++ и других языков) состоит в том, что код должен быть действительным, если вы замените T любым типом T, разрешенным границами, поэтому вы не можете вызвать метод, который не определен в верхней границе.

Будучи верхней границей T, A также означает, что вы всегда можете присвоить объект T переменной A.Вы не можете безопасно назначить объект A переменной T (если только A == T), вы можете сделать это, только если A была нижней границей T, а не верхней.Также см. Понимание верхней и нижней границ?в Java Generics.

Java использует стирание типа для реализации обобщений, есть несколько преимуществ , но это вызывает некоторые ограничения, которые не всегда очевидны.Из-за стирания типа в этом случае само приведение не будет неудачным, после проверки типа T заменяется верхней границей на шаге стирания типа, то есть число (T) заменяется числом (целое число).Исключение по-прежнему будет происходить, если вы сделаете все, что вызывает приведение к подклассу, например, если вы вернете измененный t и присвойте результат переменной подкласса, потому что компилятор добавляет неявное приведение.

Это также не удастся, если вы вызовете метод, который зависит от подкласса T, который является распространенным шаблоном, например:

List<Person> persons = ...
Comparator<Person> nameComparator = (p1,p2) -> p1.getName().compareTo(p2.getName())
java.util.Collections.sort(persons,nameComparator);

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

import java.util.function.Consumer;
import java.util.function.Function;

class A {
    @Override public String toString(){ return "A";}
    public String foo(){ return "foo";}
}

class B extends A {
    @Override public String toString(){ return "B";}
    public String bar(){ return "bar";}
}

class C extends B { }

public class Main {

    public static void main(String[] args) {
        Function<A,String> funA = a -> a.foo();
        Function<B,String> funB = b -> b.bar();
        Function<C,String> funC = c -> c.bar();
        Consumer<B> ignoreArgument = b -> {
            System.err.println("  Consumer called");
        };

        B b = new B();
        System.err.println("* voidTest *");
        voidTest(b);
        System.err.println("------------");
        System.err.println("* returnTest *"); 
        returnTest(b);
        System.err.println("returnTest without using result did not throw");
        System.err.println("------------");
        try {
            System.err.println("Returned " + returnTest(b).toString());
            System.err.println("returnTest: invoking method on result did not throw");
        }
        catch(Exception ex) {
            System.err.println("returnTest: invoking method on result threw");
            ex.printStackTrace();
        }
        System.err.println("------------");
        B b2 = null;
        try {
            b2 = returnTest(b);
            System.err.println("returnTest: assigning result to a B variable did not throw");
        }
        catch(Exception ex) {
            System.err.println("returnTest: assigning result to a B variable threw");
            ex.printStackTrace();
        }
        System.err.println("------------");
        System.err.println("* functionTest funA *");
        functionTest(b, funA);
        System.err.println("------------");
        System.err.println("* functionTest funB * ");
        functionTest(b, funB);
        System.err.println("------------");
        System.err.println("* consumerTest *");
        consumerTest(b, ignoreArgument);
        // The following won't work because C is not B or a superclass of B
        // Compiler error functionTest(T, Function<? super T,String>) is not applicable for the arguments (B, Function<C,String>)
        // functionTest(b, funC); 
    }

    private static <T extends A> void voidTest(T t){
        System.err.println("  Before: " + t.toString());
        t = (T)new A(); // warning Type safety: Unchecked cast from A to T
        System.err.println("  After: " + t.toString());
    }

    private static <T extends A> T returnTest(T t){
        System.err.println("  Before: " + t.toString());
        t = (T)new A();
        System.err.println("  After: " + t.toString());
        return t;
    }

    private static <T extends A> void functionTest(T t, Function<? super T,String> fun) {
        System.err.println("  fun Before: " + fun.apply(t));
        t = (T)new A();
        try {
            System.err.println("  fun After: " + fun.apply(t));
        }
        catch(Exception ex) {
            System.err.println("  fun After: threw");
            ex.printStackTrace();
        }
    }

    private static <T extends A> void consumerTest(T t, Consumer<? super T> c) {
        System.err.print("  Before: ");
        c.accept(t);
        t = (T)new A();
        try {
            System.err.println("  After: ");
            c.accept(t);
            System.err.println("    c.accept(t) After: worked");
        }
        catch(Exception ex) {
            System.err.println("    c.accept(t) After: threw");
            ex.printStackTrace();
        }
    }
}

Вывод в OpenJDK 11:

* voidTest *
  Before: B
  After: A
------------
* returnTest *
  Before: B
  After: A
returnTest without using result did not throw
------------
  Before: B
  After: A
returnTest: invoking method on result threw
java.lang.ClassCastException: class A cannot be cast to class B (A and B are in unnamed module of loader 'app')
    at Main.main(Main.java:35)
------------
  Before: B
  After: A
returnTest: assigning result to a B variable threw
java.lang.ClassCastException: class A cannot be cast to class B (A and B are in unnamed module of loader 'app')
    at Main.main(Main.java:45)
------------
* functionTest funA *
  fun Before: foo
  fun After: foo
------------
* functionTest funB * 
  fun Before: bar
  fun After: threw
java.lang.ClassCastException: class A cannot be cast to class B (A and B are in unnamed module of loader 'app')
    at Main.functionTest(Main.java:83)
    at Main.main(Main.java:57)
------------
* consumerTest *
  Before:   Consumer called
  After: 
    c.accept(t) After: threw
java.lang.ClassCastException: class A cannot be cast to class B (A and B are in unnamed module of loader 'app')
    at Main.consumerTest(Main.java:97)
    at Main.main(Main.java:60)

Я не совсем уверен в результате.Вызвано исключение, если результат полностью игнорируется, возможно, в этом случае язык не требуется для приведения или компилятор удалил его.Вызов метода, определенного в верхней границе результата, все еще вызывает исключение.Наконец, наблюдение от customerTest заключается в том, что для вызова ClassCastException не нужно было вызывать bar (), ему просто нужно было передать t потребителю, который ожидает аргумент B.

2 голосов
/ 27 сентября 2019

Спецификация языка в настоящее время не включает явных проверок для завершенных типов в любом смысле.

Я проверил:

И, хотя они не относятся к вашему делу, я позволил себе проверить:

Даже если Integer является окончательным, в противном случае ваш код может сломаться.


То, что у вас есть, при условии, что компилятор не проверяет завершенные типы, будет аналогичнымна:

class Super
class Sub extends Super

<T extends Super> void passVal (T t) {
    Super super = new Super();
    return (T) super;
}

Что бы сломалось, если бы мы позвонили:

passVal(new Sub());
...