For me personally, the most obvious for understanding the essence of interfaces are collections and everything connected with them. Most likely this is the case, because they often had to work with them.
One of the main features of OOP is getting rid of duplication of logic, due to the correct architectural component. For this case (in a general sense), interfaces and abstract classes were invented.
Suppose I have a spherical method in vacuum that does some work with the collection:
/*Здесь может быть любой тип коллекции, который реализует соотв. интерфейс. Причем я использую интерфейс самого "низкого" уровня, т.к. он полностью покрывает данный функционал (т.е. проход по элементам в цикле) Также я могу написать любую свою реализацию для этого интерфейса */ public void showCollectionAtConsole(Iterable<?> col){ col.forEach(object -> System.out.println(object)); }
The convenience here is that only in Java itself there are quite a few implementations of this interface, ( more ) and I can use this logic for each of them.
Now I have another method that implements some kind of functional:
public boolean removeOrAddMyObject (Collection<MyObject> col, MyObject object){ //Если удалил объект - true, иначе - false boolean isRemove = true; if (col!=null){ if (col.isEmpty() || !col.remove(object)) { col.add(object); isRemove = false; } }else throw new IllegalStateException("Коллекция = null"); return isRemove; }
Here I have to use three collection methods: isEmpty (), add (), remove (). However, all these methods are described in the Collection interface, and there is no need to pass as a “higher” interface or class with an implementation as an argument. As I wrote above, this solution will allow me with a clear conscience to reuse this method repeatedly for other implementations.
Also, it seems to me that Callbacks are an excellent example of using interfaces:
public final class MyClass { static class MyObject { int id; String name; public MyObject(int id, String name) { this.id = id; this.name = name; } public void setId(int id) { this.id = id; } public void setName(String name){ this.name = name; } @Override public String toString(){ return "My Object: id = "+id+", name = "+name; } } //Интерфейс коллбэка interface MyCallback<T extends MyObject> { void doSome(T myObject); } static void doSomeAwesome(MyObject object, MyCallback<MyObject> callback) { System.out.println(object); callback.doSome(object); System.out.println(object); } public static void main(String[] args) { MyObject myObject = new MyObject(1,"someName"); MyCallback<MyObject> callback = new MyCallback<MyObject>() { @Override public void doSome(MyObject myObject) { myObject.setId(2); myObject.setName("someOtherName"); } }; doSomeAwesome(myObject,callback); } }
The bottom line is that you explicitly define a generic in the callback interface (if you want to pass parameters to the method and / or receive objects from the method) and describe the contract. In my case, this is one method that takes an argument as input. Now, I can implement the callback method as I see fit, and thus extend the use of the doSomeAwesome() method doSomeAwesome() additional logic.
If you execute the code at the output will be:
My Object: id = 1, name = someName
My Object: id = 2, name = someOtherName
UPD: You should also understand that there are other contexts for using interfaces:
- multiple inheritance context (in Java, you cannot inherit from more than 1 class, but you can implement more than 1 interface)
- interface as a layer between interacting entities. Those. There are two modules in the program that perform different functions while interacting with each other. In an ideal situation, both modules should represent a black box for each other with the necessary minimum of methods described by the interfaces. Thus, it is much easier to avoid difficulties with subsequent debugging of the code.
- application architecture design context. As a rule, at the initial stage of designing the architecture of a future application, it is very convenient to “figure out” the interaction between entities using interfaces. It is as if the draft, which does not require a “bother” with the implementation, and allows for relatively painless modification of the architecture at the early stages.
- testing context. As already mentioned in one of the answers, the use of interfaces allows you to: "use Mock objects instead of real ones". Believe me, in the future it makes life very easy.