Java - объявление из типа интерфейса вместо класса

В моих поисках, чтобы правильно понять передовые методы интерфейса, я заметил такие объявления, как:

List<String> myList = new ArrayList<String>();

вместо

ArrayList<String> myList = new ArrayList<String>();

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

С этой логикой я установил пример:

public class InterfaceTest {

    public static void main(String[] args) {

        PetInterface p = new Cat();
        p.talk();

    }

}

interface PetInterface {                

    public void talk();

}

class Dog implements PetInterface {

    @Override
    public void talk() {
        System.out.println("Bark!");
    }

}

class Cat implements PetInterface {

    @Override
    public void talk() {
        System.out.println("Meow!");
    }

    public void batheSelf() {
        System.out.println("Cat bathing");
    }

}

Мой вопрос: я не могу получить доступ к методу batheSelf(), потому что он существует только для Cat. Это заставляет меня думать, что я должен только объявлять из интерфейса, если я буду использовать методы, объявленные в интерфейсе (а не дополнительные методы из подкласса), иначе я должен объявить непосредственно из класса (в данном случае Cat). Правильно ли я в этом предположении?

Ответ 1

Когда есть выбор между обращением к объекту по их interface или class, первый должен быть предпочтительным, но только в том случае, если существует соответствующий тип.

Рассмотрим String implements CharSequence в качестве примера. Вы не должны просто слепо использовать CharSequence в предпочтительности для String для всех случаев, потому что это будет отрицать ваши простые операции, такие как trim(), toUpperCase() и т.д.

Однако метод, который принимает String только для того, чтобы заботиться о его последовательности значений char, должен вместо этого использовать CharSequence, потому что в этом случае это подходящий тип. На самом деле это происходит с replace(CharSequence target, CharSequence replacement) в классе String.

Другим примером является java.util.regex.Pattern и его Matcher matcher(CharSequence). Это позволяет создать Matcher из Pattern для не только String, но и для всех остальных CharSequence.

Отличный пример в библиотеке, где должен был использоваться interface, но, к сожалению, нет, также можно найти в Matcher: его appendReplacement и appendTail методы принимают только StringBuffer. Этот класс в значительной степени был заменен его более быстрым кузеном StringBuilder с 1.5.

A StringBuilder не является StringBuffer, поэтому мы не можем использовать первое с методами append… в Matcher. Однако оба из них implements Appendable (также введены в 1.5). В идеале метод Matcher append… должен принимать любые Appendable, и тогда мы могли бы использовать StringBuilder, а также все остальные Appendable доступные!

Итак, мы можем видеть, как, когда соответствующий тип существует, ссылаясь на объекты по своим интерфейсам, может быть мощной абстракцией, но только если эти типы существуют. Если тип не существует, то вы можете рассмотреть возможность определения своего собственного, если это имеет смысл. В этом примере Cat вы можете определить interface SelfBathable, например. Затем вместо обращения к Cat вы можете принять любой объект SelfBathable (например, a Parakeet)

Если нет смысла создавать новый тип, то, безусловно, вы можете ссылаться на него по class.

См. также

  • Эффективное Java 2nd Edition, пункт 52: обратитесь к объектам по их интерфейсам

    Если существуют соответствующие типы интерфейсов, параметры, возвращаемые значения и поля должны быть объявлены с использованием типов интерфейсов. Если вы привыкнете к использованию типов интерфейсов, ваша программа будет намного более гибкой. Весьма целесообразно ссылаться на объект классом, если не существует соответствующего интерфейса.

Ссылки по теме

Ответ 2

Да, вы правы. Вы должны объявить как наиболее общий тип, предоставляющий методы, которые вы используете.

Это понятие полиморфизма.

Ответ 3

Вы правы, но можете при необходимости отбрасывать из интерфейса нужное домашнее животное. Например:

PetInterface p = new Cat();
((Cat)p).batheSelf();

Конечно, если вы попытаетесь бросить своего питомца собаке, вы не можете вызвать метод batheSelf(). Он даже не компилируется. Таким образом, чтобы избежать проблем, у вас может быть такой метод:

public void bathe(PetInterface p){
    if (p instanceof Cat) {
        Cat c = (Cat) p;
        c.batheSelf();
    }
}

При использовании instanceof вы убедитесь, что не пытаетесь заставить собаку купаться во время выполнения. Который выдавал бы ошибку.

Ответ 4

Да, вы правы. Благодаря тому, что Cat внедряет "PetInterface", вы можете использовать его в приведенном выше примере и легко добавлять больше видов домашних животных. Если вам действительно нужно быть Cat-specific, вам нужно получить доступ к классу Cat.

Ответ 5

Вы можете вызвать метод batheSelf из talk в Кат.

Ответ 6

Как правило, вы должны использовать интерфейсы для конкретных классов. Вдоль этих строк, если вы можете избежать использования нового оператора (который всегда требует конкретного типа, как в вашем новом примере ArrayList), еще лучше.

Все это связано с управлением зависимостями в коде. Лучше всего зависеть только от очень абстрактных вещей (например, интерфейсов), потому что они также очень стабильны (см. http://objectmentor.com/resources/articles/stability.pdf). Поскольку у них нет кода, они должны быть изменены только при изменении API... другими словами, если вы хотите, чтобы этот интерфейс отображал другое поведение в мире, т.е. Изменение дизайна.

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

Вам следует стремиться скрыть поведение ваших классов в соответствии с принципом открытого закрывания (см. http://objectmentor.com/resources/articles/ocp.pdf), таким образом, существующий интерфейсы не должны меняться, даже когда вы добавляете функциональность, вы можете просто указать новый субинтерфейс.

Старый способ избежать нового оператора заключался в использовании абстрактного шаблона Factory, но он имеет свой собственный набор проблем. Лучше использовать такой инструмент, как Guice, который выполняет инъекцию зависимостей, и предпочитает инъекцию конструктора. Убедитесь, что вы поняли Принцип инверсии зависимостей (см. http://objectmentor.com/resources/articles/dip.pdf), прежде чем вы начнете использовать инъекцию зависимостей. Я видел, как многие люди вводят ненадлежащие зависимости, а затем жалуются, что инструмент не помогает им... это не сделает вас отличным программистом, вам все равно придется использовать его соответствующим образом.

Пример: вы пишете программу, которая помогает учащимся изучать физику. В этой программе учащиеся могут поместить мяч в различные физические сценарии и посмотреть, как он себя ведет: стрелять из пушки со скалы, поднести ее под воду, в глубоком космосе и т.д. Вопрос: вы хотите включить что-то о тяжести мяч в Ball API... вы должны включить метод getMass() или метод getWeight()?

Вес зависит от среды, в которой находится мяч. Возможно, было бы удобно, чтобы звонящие могли вызвать один метод и получить вес шара, где бы он ни находился, но как вы пишете этот метод? Каждый экземпляр шара должен постоянно отслеживать, где он находится и какова текущая гравитационная константа. Поэтому вы должны предпочесть getMass(), потому что масса является внутренним свойством шара и не зависит от его окружения.

Подождите, что, если вы просто используете getWeight (Environment) вместо этого? Таким образом, экземпляр шара может просто получить свой текущий g из среды и продолжить... еще лучше, вы можете использовать Guice для вставки окружения в конструктор Ball! Это тип злоупотреблений, которые я часто вижу, и люди в конечном итоге обвиняют Guice в том, что они не могут обрабатывать инъекцию зависимостей так же легко, как они надеялись.

Проблема не в Guice, а в дизайне Ball API. Вес не является неотъемлемым свойством мяча, поэтому это не свойство, которое должно быть доступно с мяча. Вместо этого Ball должен реализовать интерфейс MassiveObject с помощью метода getMass(), а среда должна иметь метод, называемый getWeightOf (MassiveObject). Внутренняя среда - ее собственная гравитационная постоянная, так что это намного лучше. И среда теперь зависит только от простого интерфейса, MassiveObject... но это задание состоит в том, чтобы содержать объекты, поэтому так оно и должно быть.

Ответ 7

Почему бы просто не сделать это!

Cat c = new Cat();
PetInterface p = (PetInterface)c;
p.talk();
c.batheSelf();

Теперь у нас есть один объект, который можно манипулировать с помощью 2 ссылок.
Ссылка p может использоваться для вызова функций, определенных в интерфейсе, и c может использоваться для вызова функций, определенных только в классе (или суперклассе).