Лучшая практика 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`
}
таким образом, вам не нужно кастинг или создание дополнительного интерфейса.