JavaRush /Java Blog /Random EN /Polymorphism and its friends
Viacheslav
Level 3

Polymorphism and its friends

Published in the Random EN group
Polymorphism is one of the basic principles of object-oriented programming. It allows you to harness the power of Java's strong typing and write usable and maintainable code. A lot has been said about him, but I hope everyone can take away something new from this review.
Polymorphism and its friends - 1

Introduction

I think we all know that the Java programming language belongs to Oracle. Therefore, our path begins with the site: www.oracle.com . There is a "Menu" on the main page. In it, in the “Documentation” section there is a “Java” subsection. Everything that relates to the basic functions of the language belongs to the "Java SE documentation", so we select this section. The documentation section will open for the latest version, but for now the "Looking for a different release?" Let's choose the option: JDK8. On the page we will see many different options. But we are interested in Learn the Language: " Java Tutorials Learning Paths ". On this page we will find another section: " Learning the Java Language ". This is the holiest of holies, a tutorial on Java basics from Oracle. Java is an object-oriented programming language (OOP), so learning the language even on the Oracle website begins with a discussion of the basic concepts of “ Object-Oriented Programming Concepts ”. From the name itself it is clear that Java is focused on working with objects. From the " What Is an Object? " subsection, it is clear that objects in Java consist of state and behavior. Imagine that we have a bank account. The amount of money in the account is a state, and methods of working with this state are behavior. Objects need to be described somehow (tell what state and behavior they may have) and this description is the class . When we create an object of some class, we specify this class and this is called the “ object type ”. Hence it is said that Java is a strongly typed language, as stated in the Java language specification in the section " Chapter 4. Types, Values, and Variables ". The Java language follows OOP concepts and supports inheritance using the extends keyword. Why expansion? Because with inheritance, a child class inherits the behavior and state of the parent class and can complement them, i.e. extend the functionality of the base class. An interface can also be specified in the class description using the implements keyword. When a class implements an interface, it means that the class conforms to some contract - a declaration by the programmer to the rest of the environment that the class has a certain behavior. For example, the player has various buttons. These buttons are an interface for controlling the behavior of the player, and the behavior will change the internal state of the player (for example, volume). In this case, the state and behavior as a description will give a class. If a class implements an interface, then an object created by this class can be described by a type not only by the class, but also by the interface. Let's look at an example:
public class MusicPlayer {

    public static interface Device {
        public void turnOn();
        public void turnOff();
    }

    public static class Mp3Player implements Device {
        public void turnOn() {
            System.out.println("On. Ready for mp3.");
        }
        public void turnOff() {
            System.out.println("Off");
        }
    }

    public static class Mp4Player extends Mp3Player {
        @Override
        public void turnOn() {
            System.out.println("On. Ready for mp3/mp4.");
        }
    }

    public static void main(String []args) throws Exception{
        // Какое-то устройство (Тип = Device)
        Device mp3Player = new Mp3Player();
        mp3Player.turnOn();
        // У нас есть mp4 проигрыватель, но нам от него нужно только mp3
        // Пользуемся им How mp3 проигрывателем (Тип = Mp3Player)
        Mp3Player mp4Player = new Mp4Player();
        mp4Player.turnOn();
    }
}
Type is a very important description. It tells how we are going to work with the object, i.e. what behavior we expect from the object. Behaviors are methods. Therefore, let's understand the methods. On the Oracle website, methods have their own section in the Oracle Tutorial: " Defining Methods ". The first thing to take away from the article: The signature of a method is the name of the method and types of parameters :
Polymorphism and its friends - 2
For example, when declaring a method public void method(Object o), the signature will be the name of the method and the type of the parameter Object. The return type is NOT included in the signature. It is important! Next, let's compile our source code. As we know, for this the code must be saved in a file with the name of the class and the extension java. Java code is compiled using the " javac " compiler into some intermediate format that can be executed by the Java Virtual Machine (JVM). This intermediate format is called bytecode and is contained in files with the .class extension. Let's run the command to compile: javac MusicPlayer.java After the java code is compiled, we can execute it. Using the " java " utility to start, the java virtual machine process will be launched to execute the bytecode passed in the class file. Let's run the command to launch the application: java MusicPlayer. We will see on the screen the text specified in the input parameter of the println method. Interestingly, having the bytecode in a file with the .class extension, we can view it using the " javap " utility. Let's run the command <ocde>javap -c MusicPlayer:
Polymorphism and its friends - 3
From the bytecode we can see that calling a method through an object whose type the class was specified is carried out using invokevirtual, and the compiler has calculated which method signature should be used. Why invokevirtual? Because there is a call (invoke is translated as calling) of a virtual method. What is a virtual method? This is a method whose body can be overridden during program execution. Simply imagine that you have a list of correspondence between a certain key (method signature) and the body (code) of the method. And this correspondence between the key and the body of the method may change during program execution. Therefore the method is virtual. By default, in Java, methods that are NOT static, NOT final, and NOT private are virtual. Thanks to this, Java supports the object-oriented programming principle of polymorphism. As you may have already understood, this is what our review is about today.

Polymorphism

On the Oracle website in their official Tutorial there is a separate section: " Polymorphism ". Let's use the Java Online Compiler to see how polymorphism works in Java. For example, we have some abstract class Number that represents a number in Java. What does it allow? He has some basic techniques that all heirs will have. Anyone who inherits from Number literally says - “I am a number, you can work with me as a number.” For example, for any successor you can use the intValue() method to get its Integer value. If you look at the java api for Number, you can see that the method is abstract, that is, each successor of Number must implement this method itself. But what does this give us? Let's look at an example:
public class HelloWorld {

    public static int summ(Number first, Number second) {
        return first.intValue() + second.intValue();
    }

    public static void main(String []args){
        System.out.println(summ(1, 2));
        System.out.println(summ(1L, 4L));
        System.out.println(summ(1L, 5));
        System.out.println(summ(1.0, 3));
    }
}
As can be seen from the example, thanks to polymorphism, we can write a method that will accept arguments of any type as input, which will be a descendant of Number (we cannot get Number, because it is an abstract class). As was the case with the player example, in this case we are saying that we want to work with something, like Number. We know that anyone who is a Number must be able to supply its integer value. And that's enough for us. We do not want to go into details of the implementation of a specific object and want to work with this object through methods common to all descendants of Number. The list of methods that will be available to us will be determined by type at compile time (as we saw earlier in bytecode). In this case, our type will be Number. As you can see from the example, we are passing different numbers of different types, that is, the summ method will receive Integer, Long, and Double as input. But what they all have in common is that they are descendants of the abstract Number, and therefore override their behavior in the intValue method, because each specific type knows how to cast that type to Integer. Such polymorphism is implemented through the so-called overriding, in English Overriding.
Polymorphism and its friends - 4
Overriding or dynamic polymorphism. So, let's start by saving the HelloWorld.java file with the following content:
public class HelloWorld {
    public static class Parent {
        public void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
Let's do javac HelloWorld.javaand javap -c HelloWorld:
Polymorphism and its friends - 5
As you can see, in the bytecode for the lines with a method call, the same reference to the method for calling is indicated invokevirtual (#6). Let's do it java HelloWorld. As we can see, the variables parent and child are declared with type Parent, but the implementation itself is called according to what object was assigned to the variable (i.e. what type of object). During program execution (they also say in runtime), the JVM, depending on the object, when calling methods using the same signature, executed different methods. That is, using the key of the corresponding signature, we first received one method body, and then received another. Depending on what object is in the variable. This determination at the time of program execution of which method will be called is also called late binding or Dynamic Binding. That is, the matching between the signature and the method body is performed dynamically, depending on the object on which the method is called. Naturally, you cannot override static members of a class (Class member), as well as class members with access type private or final. @Override annotations also come to the aid of developers. It helps the compiler understand that at this point we are going to override the behavior of an ancestor method. If we made a mistake in the method signature, the compiler will immediately tell us about it. For example:
public static class Parent {
        public void method() {
            System.out.println("parent");
        }
}
public static class Child extends Parent {
        @Override
        public void method(String text) {
            System.out.println("child");
        }
}
Doesn't compile with error: error: method does not override or implement a method from a supertype
Polymorphism and its friends - 6
Redefinition is also associated with the concept of “ covariance ”. Let's look at an example:
public class HelloWorld {
    public static class Parent {
        public Number method() {
            return 1;
        }
    }
    public static class Child extends Parent {
        @Override
        public Integer method() {
            return 2;
        }
    }

    public static void main(String[] args) {
        System.out.println(new Child().method());
    }
}
Despite the apparent abstruseness, the meaning comes down to the fact that when overriding, we can return not only the type that was specified in the ancestor, but also a more specific type. For example, the ancestor returned Number, and we can return Integer - the descendant of Number. The same applies to exceptions declared in the method's throws. Heirs can override the method and refine the exception thrown. But they can't expand. That is, if the parent throws an IOException, then we can throw the more precise EOFException, but we cannot throw an Exception. Likewise, you cannot narrow the scope and you cannot impose additional restrictions. For example, you cannot add static.
Polymorphism and its friends - 7

Hiding

There is also such a thing as “ concealment ”. Example:
public class HelloWorld {
    public static class Parent {
        public static void method() {
            System.out.println("Parent");
        }
    }
    public static class Child extends Parent {
        public static void method() {
            System.out.println("Child");
        }
    }

    public static void main(String[] args) {
        Parent parent = new Parent();
        Parent child = new Child();
        parent.method();
        child.method();
    }
}
This is a pretty obvious thing if you think about it. Static members of a class belong to the class, i.e. to the type of the variable. Therefore, it is logical that if child is of type Parent, then the method will be called on Parent, and not on child. If we look at the bytecode, as we did earlier, we will see that the static method is called using invokestatic. This explains to the JVM that it needs to look at the type, and not at the method table, as invokevirtual or invokeinterface did.
Polymorphism and its friends - 8

Overloading methods

What else do we see in the Java Oracle Tutorial? In the previously studied section " Defining Methods " there is something about Overloading. What it is? In Russian this is “method overloading”, and such methods are called “overloaded”. So, method overloading. At first glance, everything is simple. Let's open an online Java compiler, for example tutorialspoint online java compiler .
public class HelloWorld {

	public static void main(String []args){
		HelloWorld hw = new HelloWorld();
		hw.say(1);
		hw.say("1");
	}

	public static void say(Integer number) {
		System.out.println("Integer " + number);
	}
	public static void say(String number) {
		System.out.println("String " + number);
	}
}
So, everything seems simple here. As stated in the Oracle tutorial, overloaded methods (in this case the say method) differ in the number and type of arguments passed to the method. You cannot declare the same name and the same number of identical types of arguments, because the compiler will not be able to distinguish them from each other. It’s worth noting a very important thing right away:
Polymorphism and its friends - 9
That is, when overloading, the compiler checks for correctness. It is important. But how does the compiler actually determine that a certain method needs to be called? It uses the "the Most Specific Method" rule described in the Java language specification: " 15.12.2.5. Choosing the Most Specific Method ". To demonstrate how it works, let's take an example from Oracle Certified Professional Java Programmer:
public class Overload{
  public void method(Object o) {
    System.out.println("Object");
  }
  public void method(java.io.FileNotFoundException f) {
    System.out.println("FileNotFoundException");
  }
  public void method(java.io.IOException i) {
    System.out.println("IOException");
  }
  public static void main(String args[]) {
    Overload test = new Overload();
    test.method(null);
  }
}
Take an example from here: https://github.com/stokito/OCPJP/blob/master/src/ru/habrahabr/blogs/java/OCPJP1/question1/Overload.j... As you can see, we are passing null to the method. The compiler tries to determine the most specific type. Object is not suitable because everything is inherited from him. Go ahead. There are 2 classes of exceptions. Let's look at java.io.IOException and see that there is a FileNotFoundException in "Direct Known Subclasses". That is, it turns out that FileNotFoundException is the most specific type. Therefore, the result will be the output of the string "FileNotFoundException". But if we replace IOException with EOFException, it turns out that we have two methods at the same level of the hierarchy in the type tree, that is, for both of them, IOException is the parent. The compiler will not be able to choose which method to call and will throw a compilation error: reference to method is ambiguous. One more example:
public class Overload{
    public static void method(int... array) {
        System.out.println("1");
    }

    public static void main(String args[]) {
        method(1, 2);
    }
}
It will output 1. There are no questions here. The type int... is a vararg https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html and is really nothing more than "syntactic sugar" and is actually an int. .. array can be read as int[] array. If we now add a method:
public static void method(long a, long b) {
	System.out.println("2");
}
Then it will display not 1, but 2, because we are passing 2 numbers, and 2 arguments are a better match than one array. If we add a method:
public static void method(Integer a, Integer b) {
	System.out.println("3");
}
Then we will still see 2. Because in this case the primitives are a more exact match than boxing in Integer. However, if we execute, method(new Integer(1), new Integer(2));it will print 3. Constructors in Java are similar to methods, and since they can also be used to obtain a signature, the same “overloading resolution” rules apply to them as overloaded methods. The Java language specification tells us so in " 8.8.8. Constructor Overloading ". Method overload = Early binding (aka Static Binding) You can often hear about early and late binding, also known as Static Binding or Dynamic Binding. The difference between them is very simple. Early is compilation, late is the moment the program is executed. Therefore, early binding (static binding) is the determination of which method will be called on whom at compilation time. Well, late binding (dynamic binding) is the determination of which method to call directly at the time of program execution. As we saw earlier (when we changed IOException to EOFException), if we overload methods so that the compiler cannot understand where to make which call, then we will get a compile-time error: reference to method is ambiguous. The word ambiguous translated from English means ambiguous or uncertain, imprecise. It turns out that overload is early binding, because the check is performed at compile time. To confirm our conclusions, let’s open the Java Language Specification at the chapter “ 8.4.9. Overloading ”:
Polymorphism and its friends - 10
It turns out that during compilation, information about the types and number of arguments (which is available at compilation time) will be used to determine the signature of the method. If the method is one of the object's methods (i.e., instance method), the actual method call will be determined at runtime using dynamic method lookup (i.e., dynamic binding). To make it clearer, let’s take an example that is similar to the one discussed earlier:
public class HelloWorld {
    public void method(int intNumber) {
        System.out.println("intNumber");
    }
    public void method(Integer intNumber) {
        System.out.println("Integer");
    }
    public void method(String intNumber) {
        System.out.println("Number is: " + intNumber);
    }

    public static void main(String args[]) {
        HelloWorld test = new HelloWorld();
        test.method(2);
    }
}
Let's save this code to the HelloWorld.java file and compile it using javac HelloWorld.java Now let's see what our compiler wrote in the bytecode by running the command: javap -verbose HelloWorld.
Polymorphism and its friends - 11
As stated, the compiler has determined that some virtual method will be called in the future. That is, the method body will be defined at runtime. But at the time of compilation, of all three methods, the compiler chose the most suitable one, so it indicated the number:"invokevirtual #13"
Polymorphism and its friends - 12
What kind of methodref is this? This is a link to the method. Roughly speaking, this is some clue by which, at runtime, the Java Virtual Machine can actually determine which method to look for to execute. More details can be found in the super article: " How Does JVM Handle Method Overloading And Overriding Internally ".

Summarizing

So, we found out that Java, as an object-oriented language, supports polymorphism. Polymorphism can be static (Static Binding) or dynamic (Dynamic Binding). With static polymorphism, also known as early binding, the compiler determines which method should be called and where. This allows the use of a mechanism such as overload. With dynamic polymorphism, also known as late binding, based on the previously calculated signature of a method, a method will be calculated at runtime based on which object is used (i.e., which object’s method is called). How these mechanisms work can be seen using bytecode. The overload looks at the method signatures, and when resolving the overload, the most specific (most accurate) option is chosen. Overriding looks at the type to determine what methods are available, and the methods themselves are called based on the object. As well as materials on the topic: #Viacheslav
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION