Когда класс C реализует I с помощью метода I f (I x), почему существует свобода в возвращаемом типе, но не в аргументе? - PullRequest
0 голосов
/ 10 октября 2019

Рассмотрим этот код:

public interface I { I f(I x); }

public class C1 implements I { public I  f (I  x) { return null; } }

public class C2 implements I { public C2 f (I  x) { return null; } }

public class C3 implements I { public I  f (C3 x) { return null; } }

public class C4 implements I { public C4 f (C4 x) { return null; } }

public class C5 implements I { public C1 f (I  x) { return null; } }

Если вы попытаетесь скомпилировать, вы увидите, что C3 и C4 не удаются:

C3 is not abstract and does not override abstract method f(I) in I

C4 is not abstract and does not override abstract method f(I) in I

Так что мне разрешено специализироватьсявозвращаемый тип, но аргумент должен оставаться абстрактным. Другими словами, мне разрешено указывать тип возврата метода (он может быть даже C2 в определении C1 - я могу смешивать) , но я не могу ограничить аргументы, поэтомуf всегда полиморфен на входе.

  • Какая логика стоит за этой ситуацией?
  • Почему нельзя специализировать аргумент?
  • ПочемуРазрешено ли специализировать возвращаемый тип для типа this или оставить его абстрактным?
  • Почему разрешено специализировать возвращаемый тип для другой реализации того же интерфейса? (Как и в C5.)


PS Этот другой вопрос о ковариантном возврате, очевидно, не охватывает всепункты выше, если таковые имеются.

  • Я не спрашиваю "что" , я здесь спрашиваю "почему" .
  • Опять же, мой вопрос здесь касается отношения класса к интерфейсу, а не к подклассам вообще (даже если мы действительно рассматриваем реализацию интерфейса как случай подкласса, который сам по себе является открытым, хотя и философским вопросом) .
  • И еще раз, этот вопрос является общим, в то время как мой вопрос относится к Java.
  • Наконец, формулировка совершенно иная. Я даже не знаю, откуда взялся этот "ковариантный" . Как в "вектор" ковариант? "Функтор" ковариант? Если есть глубокая аналогия, она должна быть объяснена.

Поэтому, пожалуйста, откройте снова.

Ответы [ 2 ]

1 голос
/ 10 октября 2019

Посмотрим.

public interface I { I f(I x); }
I[] all = { new C1(), new C2(), new C3(), new C4(), new C5() };
I argument = availableArguments[Random.nextInt(all.length)];
I c1 = new C1();
I c2 = new C2();
I c3 = new C3();
I c4 = new C4();
I c5 = new C5();

public class C1 implements I { public I  f (I  x) { return null; } }
I result = c1.f(argument); // ok; argument is an I, an I is returned

public class C2 implements I { public C2 f (I  x) { return null; } }
I result = c2.f(argument); // ok; argument is an I, returned C2 can be assigned to I

public class C3 implements I { public I  f (C3 x) { return null; } }
I result = c3.f(argument); // would fail: argument is not of type C3 as required

public class C4 implements I { public C4 f (C4 x) { return null; } }
I result = c4.f(argument); // would fail: argument is not of type C4 as required

public class C5 implements I { public C1 f (I  x) { return null; } }
I result = c5.f(argument); // ok; argument is an I, returned C1 can be assigned to I
0 голосов
/ 10 октября 2019

История идет некоторым путем.

Интерфейсы - это своего рода классы.

Это не сразу очевидно, но другое подходящее название для "интерфейса" Java будет "абстрактный класс" . То, что интерфейсы на самом деле являются подмножеством классов, конечно, сомнительно, поэтому мы должны ставить его отдельно. Если мы узнаем, что это неверно, мы должны также вернуться к этому ответу. Но сейчас давайте предположим, что это правда.

Мы также должны предположить, что Java "классы" являются разновидностями типов . Опять же, это может быть подвергнуто сомнению само по себе, в другом месте. Если мы предположим это, мы можем извлечь некоторую информацию из теории типов. Что нам конкретно нужно, так это понятие подтип . Короче говоря, то, что C является подтипом I, означает, что когда в вашем коде есть переменная типа I, вы можете заменить ее на переменную типа C, и некоторая часть вашего кода все еще будет работать.

Примечания:

  • Конечно, вы никогда не сможете создать значение типа I, поскольку I является абстрактным классом - у него нет определения;но вы можете рассмотреть значение, о котором известно только то, что он относится к некоторому классу, который реализует I.
  • Какая часть вашего кода будет работать впоследствии, зависит от точного определения подтипа, из которого естьнекоторые - читайте дальше, чтобы узнать, какое определение использует Java.

Типы аргумента и возврата принципиально различны в отношении подтипов.

Здесь ковариация и контравариантность введите. Эти термины пришли в информатику из физика и теория категорий . Это кажется неправдоподобным, но посмотрите, как подтип играет с функциями , и вы обязательно увидите, как все это выравнивается.

Вот пример. Это немного сложнее, поэтому возьмите ручку, немного бумаги и сами проверите.

  1. Возьмите функцию f: Integer -> Boolean. Он принимает число и возвращает значение истинности. Например, пусть он скажет нам, является ли число четным.
  2. Мы можем подтипировать эту функцию следующими двумя способами:
    1. Конечно, если она работает с любым числом, она будет работать с простыми числами. , поэтому мы можем использовать такую ​​же функцию, как эта: f -- specialize argument --> f1: Prime -> Boolean. (3 все еще дает False, 2 дает True. Все хорошо.) Там, где работает последний, первый тоже будет работать. Итак, f1 является подклассом f.
    2. Также верно, что мы можем использовать такую ​​же функцию, как эта: f -- generalize result --> f2 = Integer -> Byte. (Мы просто преобразовали бы True в 0x01 и False в 0x00.) Там, где требуется f2, подойдет f. Итак, f2 также является подклассом f!
  3. Обратите внимание, что Prime является подклассом Integer: вы все равно можете добавлять, умножать и инвертировать их, новы также можете использовать их для криптографии .
  4. Обратите внимание, что Bool является подклассом Byte: вы все равно можете сохранить его в памяти, но вы также можете сохранить массив Bool упаковано, в 8 раз меньше памяти, чем массив Byte.
  5. Теперь обратите внимание: когда мы перемещаем аргумент f в направлении подтипа (специализируем его) функция также перемещается в этом направлении, в то время как тип результата должен быть перемещен в противоположном направлении (обобщено) для функции, которая должна быть подтипирована. Читатель может на самом деле нарисовать это и увидеть, как она согласуется с определением векторной дисперсии в физике.

Это, конечно, не реальное доказательство, а лишь махание рукой. У меня нет реальных доказательств. Задайте вопрос!

Подклассы в Java выровнены с определенным «подтипом» частичного порядка типов.

Знаменитое требование подтипа Барбары Лисков является односторонним для определения отношения подтипа. Это конкретное определение называется «поведенческий» подтип, и оно открывает глубокую кроличью нору с неразрешимостью , которая может вызывать или не беспокоить нас. (Итак, снова у нас есть вопрос.) Именно здесь мы должны верить, что Java верна в своем поведенческом подтипировании. (Привычный вопрос. Один из возможных способов вбить клин - внимательно посмотреть на взаимосвязь между подтипом и наследованием .)

Итак, давайте предположим, что наследование совместимос подтипом в Java. Что это нам дает?

Функция с подтипированным аргументом не является подтипом оригинала.

То есть метод подтипированного элемента не является подтипом оригинала.

Итак, предположим, у вас есть эта функция, которая выполняет шифрование RSA, и ей просто нужно одно простое число, f: Prime -> X. Если вы поместите эту функцию в ситуацию, когда ей может быть дан непростой аргумент, то она, вероятно, создаст сообщение, которое зашифровано очень слабо. Вы ввели ошибку. Таким образом, хотя вы всегда можете поместить простое число в место, где ожидается число, вы не можете поместить вызов метода, который запрашивает простое число, в место, где ему в качестве аргумента будет дано простое число.

Но объект , из которого вызывается метод, сам по себе является неявным аргументом , можете спросить вы, поэтому подклассы должны быть вообще запрещены ! Действительно, предположим, что я определяю метод Integer, который вычисляет возможно слабый ключ RSA и подкласс Prime из этого Integer. Теперь тот же метод, с теми же очевидными типами, вычисляет сильный ключ RSA, потому что его неявный аргумент был подтипирован - и, если я хочу сильные ключи, я не могу слепо заменить Integer на Prime.

Это первый большой вопрос нашего фэнтезийного приключения.

(на данный момент я очень нечеткий.)

Java пытается убедиться в реализации методавсегда является подтипом своего определения.

Один из способов обеспечения этого - разрешить только подтипирование результата. (Это единственный способ? Правильный путь? Действительно ли это безопасно?)

По какой-то причине Java не пытается выдавать ошибки типа при использовании сайта, а просто запрещает определение.

В чем причина нашего большого вопроса номер два. Возможно, потому что он хочет выровнять подтипы и наследование.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...