Лучшая практика Java: кастинг объектов против интерфейсов

Предположим, у нас есть следующие игрушечные интерфейсы:

interface Speakable
{
    public abstract void Speak();
}

interface Flyer
{
    public abstract void Fly();
}

и у нас есть класс, который реализует оба интерфейса:

class Duck implements Speakable, Flyer
{
    public void Speak()
    {
        System.out.println("quack quack don't eat me I taste bad.");
    }

    public void Fly()
    {
        System.out.println("I am flying");
    }
}

на данный момент я вижу разные способы вызова методов на Duck и я не могу решить, какой из них является лучшей практикой. Рассмотрим такой сценарий:

public class Lab 
{
        private static void DangerousSpeakAndFly(Object x)
        {
            Speakable temp  = (Speakable) x;
            temp.Speak();
            Flyer temp2= (Flyer) x;
            temp2.Fly();
        }

        public static void main(String[] args) 
        {
            Duck daffy= new Duck();
            DangerousSpeakAndFly(daffy);
        }
}

эта программа будет вести себя так, как ожидалось, потому что объект, переданный функции, оказывается castable в Flyer и Speakable, но я съеживаюсь, когда вижу код как это, потому что он не позволяет проверять тип времени компиляции и из-за плотной связи он может вызывать неожиданные исключения, например, когда в качестве параметра передается объект с другим типом (не кастабельный ни одному из интерфейсов), или если реализация Duck изменяется вниз по строке, поэтому он больше не реализует Flyer.

Я вижу Java-код, написанный так все время, иногда в учебниках (например, pg. 300 из "Head First Design Patterns" О'Рейли) так в этом должна быть заслуга, которой мне не хватает.

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

interface SpeakingFlyer extends Flyer, Speakable
{

}

class BuzzLightyear implements SpeakingFlyer
{
    public void Speak()
    {
        System.out.println("My name is Buzz");
    }
    public void Fly()
    {
        System.out.println("To infinity and beyond!");
    }
}

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

private static void SafeSpeakAndFly(SpeakingFlyer x)
{
    x.Speak();
    x.Fly();
}

public static void main(String[] args) 
{
    BuzzLightyear bly= new BuzzLightyear();
    SafeSpeakAndFly(bly);
}

это ненужный перебор? какие подводные камни для этого?

Я чувствую, что этот дизайн развязывает SafeSpeakAndFly() функция от своих параметров и держит насекомых в страхе из-за компиляции проверка типа.

почему первый метод используется так широко на практике, а последний нет?

2 ответов


Я вижу Java-код, написанный так все время, иногда в учебниках (например, pg. 300 из "Head First Design Patterns" О'Рейли), поэтому в этом должна быть заслуга, которую мне не хватает.

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

private static void dangerousSpeakAndFly(Object x) {
    if (x instanceof Speakable) {
        Speakable temp  = (Speakable) x;
        temp.Speak();
    }
    if (x instanceof Flyer) {
        Flyer temp2= (Flyer) x;
        temp2.Fly();
    }
}

имея дженерики, однако, позволяет нам сделать это:

private static <T extends Speakable & Flyer> void reallySafeSpeakAndFly(T x) {
    x.Speak();
    x.Fly();
}

здесь компилятор может убедиться, что мы не пропустим что-то, что не реализует Speakable и Flyer и может обнаружить такие нахальные попытки во время компиляции.

почему первый метод используется так широко на практике, а последний нет?

Это может полагаю, вы видели много устаревшего кода. :)


вы можете применить аргумент, чтобы быть в то же время Speakable и Flyer создание универсального метода с пересечением типов:

private <T extends Speakable & Flyer> static void DangerousSpeakAndFly(T x) { 
    // use any of `Speakable` or `Flyer` methods of `x`
}

таким образом, вам не нужно кастинг или создание дополнительного интерфейса.