Есть ли в интерфейсе нечто большее, чем правильные методы - PullRequest
157 голосов
/ 03 февраля 2009

Допустим, у меня есть этот интерфейс:

public interface IBox
{
   public void setSize(int size);
   public int getSize();
   public int getArea();
  //...and so on
}

И у меня есть класс, который реализует это:

public class Rectangle implements IBox
{
   private int size;
   //Methods here
}

Если я хочу использовать интерфейс IBox, я не могу создать его экземпляр, например:

public static void main(String args[])
{
    Ibox myBox=new Ibox();
}

право? Так что я бы на самом деле должен был сделать это:

public static void main(String args[])
{
    Rectangle myBox=new Rectangle();
}

Если это так, то единственная цель интерфейсов - убедиться, что класс, реализующий интерфейс, содержит в себе правильные методы, описанные интерфейсом? Или есть другие способы использования интерфейсов?

Ответы [ 17 ]

142 голосов
/ 03 февраля 2009

Интерфейсы - это способ сделать ваш код более гибким. Что вы делаете, это:

Ibox myBox=new Rectangle();

Затем, позже, если вы решите использовать другой тип ящика (может быть, есть другая библиотека с блоками лучшего типа), вы переключите свой код на:

Ibox myBox=new OtherKindOfBox();

Как только вы к этому привыкнете, вы обнаружите, что это отличный (действительно необходимый) способ работы.

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

myBox.close()

(при условии, что IBox имеет метод close ()), хотя фактический класс myBox изменяется в зависимости от того, в каком блоке вы находитесь в итерации.

120 голосов
/ 03 февраля 2009

Что делает интерфейсы полезными, так это , а не тот факт, что «вы можете передумать и позже использовать другую реализацию, и вам нужно только изменить одно место, где создается объект». Это не проблема.

Реальная точка уже в названии: они определяют интерфейс , который любой может реализовать для использования всего кода, который работает на этом интерфейсе. Лучшим примером является java.util.Collections, который предоставляет все виды полезных методов, которые работают исключительно на интерфейсах, таких как sort() или reverse() для List. Суть в том, что этот код теперь можно использовать для сортировки или реверсирования любого класса, который реализует интерфейсы List - не только ArrayList и LinkedList, но также и классов, которые вы пишете сами, что могут быть реализованы таким образом, что люди, написавшие java.util.Collections, никогда не представляли.

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

Другое распространенное использование интерфейсов - для обратных вызовов. Например, java.swing.table.TableCellRenderer , который позволяет влиять на то, как таблица Swing отображает данные в определенном столбце. Вы реализуете этот интерфейс, передаете экземпляр в JTable, и в какой-то момент во время рендеринга таблицы ваш код будет вызван для выполнения своих задач.

118 голосов
/ 20 июня 2013

Одно из многих применений, которые я читал, это где трудно без интерфейсов с множественным наследованием в Java:

class Animal
{
void walk() { } 
....
.... //other methods and finally
void chew() { } //concentrate on this
} 

Теперь представьте себе случай, когда:

class Reptile extends Animal 
{ 
//reptile specific code here
} //not a problem here

но

class Bird extends Animal
{
...... //other Bird specific code
} //now Birds cannot chew so this would a problem in the sense Bird classes can also call chew() method which is unwanted

Лучше дизайн будет:

class Animal
{
void walk() { } 
....
.... //other methods 
} 

Animal не имеет метода chew () и вместо этого помещается в интерфейс как:

interface Chewable {
void chew();
}

и класс Reptile реализует это, а не Birds (поскольку Birds не может жевать):

class Reptile extends Animal implements Chewable { } 

и Птицы просто:

class Bird extends Animal { }
46 голосов
/ 03 февраля 2009

Назначение интерфейсов: полиморфизм , a.k.a. замена типа . Например, учитывая следующий метод:

public void scale(IBox b, int i) {
   b.setSize(b.getSize() * i);
}

При вызове метода scale вы можете указать любое значение типа, реализующего интерфейс IBox. Другими словами, если Rectangle и Square оба реализуют IBox, вы можете предоставить либо Rectangle, либо Square везде, где ожидается IBox.

33 голосов
/ 03 февраля 2009

Интерфейсы позволяют статически типизированным языкам поддерживать полиморфизм. Объектно-ориентированный пурист будет настаивать на том, что язык должен обеспечивать наследование, инкапсуляцию, модульность и полиморфизм, чтобы быть полнофункциональным объектно-ориентированным языком. В динамически типизированных - или утиных - языках (таких как Smalltalk) полиморфизм тривиален; однако в статически типизированных языках (таких как Java или C #) полиморфизм далеко не тривиален (фактически, на первый взгляд он противоречит понятию строгой типизации.)

Позвольте мне продемонстрировать:

В динамически типизированном (или типизируемом) языке (например, Smalltalk) все переменные являются ссылками на объекты (не меньше и не более). Итак, в Smalltalk я могу сделать это:

|anAnimal|    
anAnimal := Pig new.
anAnimal makeNoise.

anAnimal := Cow new.
anAnimal makeNoise.

Этот код:

  1. Объявляет локальную переменную с именем anAnimal (обратите внимание, что мы НЕ УКАЗЫВАЕМ ТИП переменной - все переменные являются ссылками на объект, не больше и не меньше.)
  2. Создает новый экземпляр класса с именем "Свинья"
  3. Назначает этот новый экземпляр Pig переменной anAnimal.
  4. Отправляет сообщение makeNoise свинье.
  5. Повторяет все это, используя корову, но присваивая ее той же переменной, что и Свинья.

Тот же код Java будет выглядеть примерно так (при условии, что Duck и Cow являются подклассами Animal:

Animal anAnimal = new Pig();
duck.makeNoise();

anAnimal = new Cow();
cow.makeNoise();

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

В Smalltalk мы можем написать это:

|aFarmObject|
aFarmObject := Cow new.
aFarmObject grow.
aFarmObject makeNoise.

aFarmObject := Corn new.
aFarmObject grow.
aFarmObject harvest.

Это прекрасно работает в Smalltalk, потому что он имеет тип утки (если он ходит как утка и крякает как утка - это утка). В этом случае, когда сообщение отправляется объекту, поиск выполняется в списке методов получателя, и, если найден соответствующий метод, он вызывается. Если нет, генерируется какое-то исключение NoSuchMethodError - но все это делается во время выполнения.

Но в Java, статически типизированном языке, какой тип мы можем назначить нашей переменной? Кукуруза должна наследоваться от Овощей, чтобы поддерживать рост, но не может наследоваться от Животных, потому что она не производит шума. Корова должна унаследовать от Animal для поддержки makeNoise, но не может наследовать от Vegetable, потому что она не должна реализовывать урожай. Похоже, нам нужно множественное наследование - возможность наследовать от более чем одного класса. Но это оказывается довольно сложной функцией языка из-за всех всплывающих случаев (что происходит, когда несколько параллельных суперклассов реализуют один и тот же метод? И т. Д.)

Вдоль интерфейсов ...

Если мы создадим классы Animal и Vegetable, каждый из которых реализует Growable, мы можем объявить, что наша Cow - это Animal, а наша Corn - это Vegetable. Мы также можем заявить, что как Животное, так и Овощное растение. Это позволяет нам написать это, чтобы вырастить все:

List<Growable> list = new ArrayList<Growable>();
list.add(new Cow());
list.add(new Corn());
list.add(new Pig());

for(Growable g : list) {
   g.grow();
}

И это позволяет нам делать звуки животных:

List<Animal> list = new ArrayList<Animal>();
list.add(new Cow());
list.add(new Pig());
for(Animal a : list) {
  a.makeNoise();
}

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

|aFarmObject|
aFarmObject := Corn new.
aFarmObject makeNoise. // No compiler error - not checked until runtime.

Языки со статической типизацией обеспечивают гораздо лучшее "программирование по контракту", потому что во время компиляции они улавливают два типа ошибок:

// Compiler error: Corn cannot be cast to Animal.
Animal farmObject = new Corn();  
farmObject makeNoise();

-

// Compiler error: Animal doesn't have the harvest message.
Animal farmObject = new Cow();
farmObject.harvest(); 

Итак .... подведем итог:

  1. Реализация интерфейса позволяет вам указывать, что могут делать объекты (взаимодействие), а наследование классов позволяет вам определять, как все должно быть сделано (реализация).

  2. Интерфейсы дают нам много преимуществ «истинного» полиморфизма, не жертвуя проверкой типа компилятора.

9 голосов
/ 03 февраля 2009

Обычно интерфейсы определяют интерфейс, который вы должны использовать (как следует из названия ;-)). Пример


public void foo(List l) {
   ... do something
}

Теперь ваша функция foo принимает ArrayList с, LinkedList с, ... не только один тип.

Самое важное в Java - это то, что вы можете реализовать несколько интерфейсов, но вы можете расширить только ОДИН класс! Образец:


class Test extends Foo implements Comparable, Serializable, Formattable {
...
}
возможно но

class Test extends Foo, Bar, Buz {
...
}
не является!

Ваш код также может быть: IBox myBox = new Rectangle();. Теперь важно то, что myBox содержит ТОЛЬКО методы / поля из IBox, а не (возможно существующие) другие методы из Rectangle.

6 голосов
/ 03 февраля 2009

Я думаю, вы понимаете все, что делают интерфейсы, но вы еще не представляете, в каких ситуациях интерфейс полезен.

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

Где полезны интерфейсы, это когда объект нужно создать в одном месте и вернуть вызывающей стороне, которая может не заботиться о деталях реализации. Давайте изменим ваш пример IBox на Shape. Теперь у нас могут быть реализации Shape, такие как Rectangle, Circle, Triangle и т. Д. Реализации методов getArea () и getSize () будут совершенно разными для каждого конкретного класса.

Теперь вы можете использовать фабрику с различными методами createShape (params), которые будут возвращать соответствующую Shape в зависимости от переданных параметров. Очевидно, фабрика будет знать о том, какой тип Shape создается, но вызывающий выиграл не нужно заботиться о том, круг это, или квадрат, или так далее.

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

6 голосов
/ 15 мая 2017

ПОЧЕМУ ИНТЕРФЕЙС ??????

Все начинается с собаки. В частности, мопс .

Мопс имеет различные поведения:

public class Pug { 
private String name;
public Pug(String n) { name = n; } 
public String getName() { return name; }  
public String bark() { return  "Arf!"; } 
public boolean hasCurlyTail() { return true; } }

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

public class Lab { 
private String name; 
public Lab(String n) { name = n; } 
public String getName() { return name; } 
public String bark() { return "Woof!"; } 
public boolean hasCurlyTail() { return false; } }

Мы можем сделать несколько мопсов и лабораторий:

Pug pug = new Pug("Spot"); 
Lab lab = new Lab("Fido");

И мы можем ссылаться на их поведение:

pug.bark() -> "Arf!" 
lab.bark() -> "Woof!" 
pug.hasCurlyTail() -> true 
lab.hasCurlyTail() -> false 
pug.getName() -> "Spot"

Допустим, я управляю питомником собак, и мне нужно следить за всеми собаками, которых я держу. Мне нужно хранить своих мопсов и лабрадоров в отдельных массивах :

public class Kennel { 
Pug[] pugs = new Pug[10]; 
Lab[] labs = new Lab[10];  
public void addPug(Pug p) { ... } 
public void addLab(Lab l) { ... } 
public void printDogs() { // Display names of all the dogs } }

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

Понимание: и мопсы, и лабрадоры (и пудели) являются типами собак и имеют одинаковый набор поведений. То есть мы можем сказать (для целей этого примера), что все собаки могут лаять, иметь имя и могут иметь или не иметь курчавый хвост. Мы можем использовать интерфейс для определения того, что могут делать все собаки, но оставим это на усмотрение конкретных типов собак для реализации этих специфических поведений. Интерфейс говорит: «Вот то, что могут делать все собаки», но не говорит, как выполняется каждое поведение.

public interface Dog 
{
public String bark(); 
public String getName(); 
public boolean hasCurlyTail(); }

Затем я немного изменил классы Pug и Lab, чтобы реализовать поведение Dog. Можно сказать, что мопс - это собака, а лаборатория - это собака.

public class Pug implements Dog {
// the rest is the same as before } 

public class Lab implements Dog { 
// the rest is the same as before 
}

Я все еще могу создавать экземпляры Pugs и Labs, как раньше, но теперь у меня также есть новый способ сделать это:

Dog d1 = new Pug("Spot"); 
Dog d2 = new Lab("Fido");

Это говорит о том, что d1 - это не только собака, это мопс. И d2 тоже собака, а точнее лаборатория. Мы можем вызвать поведение, и оно работает как прежде:

d1.bark() -> "Arf!" 
d2.bark() -> "Woof!" 
d1.hasCurlyTail() -> true 
d2.hasCurlyTail() -> false 
d1.getName() -> "Spot"

Вот где вся дополнительная работа окупается. Класс питомника стал намного проще. Мне нужен только один массив и один метод addDog. Оба будут работать с любым объектом, который является собакой; то есть объекты, которые реализуют интерфейс Dog.

public class Kennel {
Dog[] dogs = new Dog[20]; 
public void addDog(Dog d) { ... } 
public void printDogs() {
// Display names of all the dogs } }

Вот как это использовать:

Kennel k = new Kennel(); 
Dog d1 = new Pug("Spot"); 
Dog d2 = new Lab("Fido"); 
k.addDog(d1); 
k.addDog(d2); 
k.printDogs();

Последнее выражение будет отображаться: Спот Фидо

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

6 голосов
/ 03 февраля 2009

вы могли бы сделать

Ibox myBox = new Rectangle();

таким образом, вы используете этот объект как Ibox, и вам все равно, что это действительно Rectangle.

3 голосов
/ 03 февраля 2009

Отличным примером того, как используются интерфейсы, является структура Collections. Если вы пишете функцию, которая принимает List, тогда не имеет значения, передаст ли пользователь Vector или ArrayList или HashList или что-то еще. И вы можете передать этот List любой функции, требующей интерфейса Collection или Iterable.

Это делает возможными такие функции, как Collections.sort(List list), независимо от того, как реализован List.

...