Ограниченные подстановочные знаки в ссылочных типах

Мне трудно понять следующий код о двух объектах Predicate. Первый использует подстановочный знак с нижней границей, второй — с верхней границей.

Predicate<? super String> p1 = s -> s.startsWith("a"); // why can I call startsWith()?
Predicate<? extends String> p2 = s -> s.startsWith("a");

p1.test("a"); // works
p2.test("a"); // doesn't work (why?)

Чего я не понимаю в p1, так это почему можно вызывать методы из класса String, например. startsWith()? Почему я могу передавать только объекты String в p1.test(), я ожидал, что смогу вызвать его и для объектов Number и Object.

Как ведет себя p1, я думал, что p2 будет, но это не так. Я даже не могу передать объект String в p2.test(). Для меня это не имеет смысла, потому что мы ожидаем объект, который наследуется от String (включая String).

Я думаю, что это как-то связано с тем, что мы указываем тип ссылки, а не тип самого объекта. Но какой тип тогда используется для объекта?


person Huntro    schedule 08.10.2019    source источник


Ответы (2)


Вы можете вызывать startsWith вместо p1, даже если p1 набирается с нижней границей ? super String, потому что аргумент типа выводится как String. Лямбда-выражение s -> s.startsWith("a"); подразумевается как Predicate<String>, которое допустимо присваивать переменной типа Predicate<? super String>.

Это компилирует:

Predicate<String> ps = s -> s.startsWith("a");
Predicate<? super String> p1 = ps;

Это не:

// no "startsWith" on Object
Predicate<? super String> p1 = (Object s) -> s.startsWith("a");

Справочник по JLS находится в разделе . 15.27.3, «Тип лямбда-выражения».

Если T является типом функционального интерфейса с параметризованными подстановочными знаками и лямбда-выражение неявно типизировано, то основной целевой тип является параметризацией T без подстановочных знаков (§9.9).

Здесь наземный целевой тип — это тип лямбда-выражения, а T — это целевой тип, который здесь является вашим типом данных переменной нижней границы. Это позволяет компилятору назначить Predicate<String> в качестве базового целевого типа, который становится типом лямбда-выражения.

Также обратите внимание, что вы не можете передать объект суперкласса в p1.test, потому что вы могли бы (и уже сделали) присвоить Predicate<String> p1, который принимает String.

p1.test(new Object()); // Error: can't pass something higher than String

Что касается того, почему вы не можете передать String в p2.test, когда у вас есть верхний ограниченный подстановочный знак, такой как ? extends String, это означает, что параметр типа может быть любым классом, который является либо String, либо подтипом. (Компилятор игнорирует, что String здесь равно final, и не может быть никаких подклассов String.) Предикату p2 можно присвоить Predicate<SillyString>, предполагая, что SillyString является подклассом String. Но вы не можете передать String методу, который может ожидать SillyString.

person rgettman    schedule 08.10.2019
comment
Спасибо за ваш ответ, теперь понятно, почему p1 работает. Но я все еще борюсь с p2. Каков фактический тип объекта, почему он не может быть Predicate<String>, а только, например. Predicate<SillyString>? - person Huntro; 09.10.2019
comment
С помощью p2 вы объявили тип Predicate<? extends String>, поэтому было бы законно присвоить ему Predicate<SillyString>, который, в свою очередь, принял бы только SillyString в качестве параметра. Дело не в том, что только SillyString будет законным, там может быть SillyString. Это похоже на почему вы не можете позвонить add на List с параметром типа, ограниченным сверху. - person rgettman; 09.10.2019
comment
Хантро. Возможно, вы упускаете из виду, что компилятор решает, что вы можете сделать с переменной, основываясь на типе переменной, а не на классе объекта, на который она указывает. Другими словами, когда вы объявляете Predicate<? extends String> p2, вы говорите, что p2 может быть Predicate<SillyString> (а может и не быть). Но компилятор предполагает худшее и сообщает, что вы не можете передать "a" (не SillyString) в p2. Он игнорирует тот факт, что вы сделали ссылку p2 на Predicate<String>. - person Dawood ibn Kareem; 09.10.2019
comment
Спасибо, ребята, теперь я понял. Объяснения, а также пример List помогли. - person Huntro; 09.10.2019

Когда у вас есть Predicate<? super String>, это означает, что фактическая реализация Predicate гарантированно сможет обрабатывать экземпляр String. Видит ли реализация String как String или CharSequence или даже Object, не имеет значения, пока она может обрабатывать этот тип. Это обеспечивает гибкость API, который использует интерфейс. Например:

void addFilter(Predicate<? super String> filter) { ... }

Вы можете вызвать addFilter с экземпляром Predicate<CharSequence> или Predicate<Object>, для API это не имеет значения. Например:

addFilter((CharSequence cs) -> cs.length() % 2 == 0);

Однако, когда API фактически вызывает filter, он должен передать экземпляр String в test. Если бы API было разрешено вызывать filter.test(new Object()), то Predicate<CharSequence>, переданное выше addFilter, завершилось бы ошибкой с ClassCastException.

Когда у вас есть Predicate<? extends CharSequence> (используя CharSequence, поскольку String является окончательным классом), вы не можете вызывать test, потому что реализация, вероятно, не сможет обрабатывать любой тип CharSequence. Например:

Predicate<? extends CharSequence> predicate = (String s) -> s.startsWith("...");
predicate.test(new StringBuilder());

Будет выброшено ClassCastException, потому что «настоящий тип» predicate на самом деле Predicate<String>, а не Predicate<StringBuilder>. Таким образом, компилятор отклоняет вызов test, потому что это небезопасно для типов.

Я также рекомендую прочитать Что такое PECS (Producer Extends Consumer Super)? .

person Slaw    schedule 08.10.2019
comment
Спасибо, ваше объяснение, почему Predicate<? extends CharSequence> не разрешено, имеет смысл. - person Huntro; 09.10.2019