Today I'm going to write an article about Java method bindings. I usually study Java and wasn't very conscious of it, so I'll organize it properly at this time.
Let's review the cast as prior knowledge. Casts have explicit and implicit conversions. Please see the following URL for details. https://techacademy.jp/magazine/28486 You should also know about compile-time and run-time timing. Compile time is the timing when the Java file you wrote is converted to bytecode. Execution is the time when the JVM actually executes the bytecode. If you are developing in Eclipse, usually when you write the code, it will be compiled without permission, and the code will be executed when you press the execute button. When developing in a terminal etc., compile with the javac command to create a class file, and then actually execute it with the java command. The terms "tie" and "bind" used in the article all have the same meaning. (I'm sorry it's confusing ...)
I didn't even know the meaning of this word, but in simple terms, it's like a mechanism in Java that connects (binds) a method call, the signature of the called method, and the implementation part. The signature is the method name + argument, and the implementation part is the processing inside {}. The method is a lump of these two, but for now, think about it separately. If you don't understand this, you may stumble in unexpected places (although I'm not an engineer yet, so I can't say it's great ...). Let's look at it concretely.
Animal.java
public class Animal {
public void eat(double quantity) {
System.out.println("Animals" + quantity + "g ate.");
}
}
Human.java
public class Human extends Animal {
public void eat(int quantity) {
System.out.println("People are" + quantity + "g ate.");
}
}
Test.java
public class Test {
public static void main(String[] args) {
Animal animal = new Human();
int quantity = 500;
animal.eat(quantity); // ?
}
}
Suppose all the classes are in the same package. What will be output if I run Test.java here? And why?
The correct answer is "The animal ate 500.0g." Why. At first, I thought that "Human ate 500g" should be output because Human is assigned as the reference destination. The int type is also passed as an argument in the Test class. But in reality the answer was different. Let's take a look at when method bindings occur in Java to understand what happened. See the table below.
Method type | Binding at compile time | Binding at run time |
---|---|---|
non-static method | Method signature | Method implementation |
static method | Method signature, method implementation |
Since eat is a non-static method this time, I will explain based on this (the idea is the same for static methods).
At compile time, Java associates the called method with its signature. In other words, it may be said that the compiler determines the signature of the method called each time. Let's think about this rule along with the code we are looking at right now. The last line in Test.java looks like this:
Test.java
animal.eat(quantity);
First, the compiler looks at the declaration type (type) of the variable animal. The type of animal is Animal type. The compiler then begins the search, "Yeah, yeah. This variable is the type of the Animal class. Let's find out if there is a method in this class that is currently being called." At this time, the search range also includes methods that are compatible with the method being called. Actually in the Animal class
Animal.java
eat(double quantity)
The signature is defined. In the caller, the int type is passed as an argument, but the conversion from int to double is done arbitrarily (without explicitly casting), so the compiler says, "This is the method that is being called now. It is a method compatible with ", and it connects the method call of animal.eat (quantity) with the eat method (signature) that takes a double type as an argument. At this point the compiler has determined that the eat argument is of type double. And it can no longer be changed at runtime. Let's actually check it.
Compiled from "Test.java"
public class Test {
public Test();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public static void main(java.lang.String[]);
Code:
0: new #2 // class Human
3: dup
4: invokespecial #3 // Method Human."<init>":()V
7: astore_1
8: sipush 500
11: istore_2
12: aload_1
13: iload_2
14: i2d
15: invokevirtual #4 // Method Animal.eat:(D)V
18: return
}
It compiles each file with the javac command from the terminal and then looks at the contents of the compiled code with javap -c Test. Here are the lines that you should be aware of.
15: invokevirtual #4 // Method Animal.eat:(D)V
This corresponds to the method call part of the Test class, animal.eat (quantity). (D) indicates that the argument is of double type and V indicates that the return value is void. invokevirtual means that the actual implementation is determined at runtime. The code is executed at Runtime according to the instructions of this bytecode. In other words, the compiler only associates "the method being called is the Animal eat method" as described above, and the content of the process is determined by the JVM at runtime.
All you have to do is execute the animal.eat (quantity) method according to the bytecode instructions. Earlier I said that the compiler would go to see the declaration type of the variable animal. The JVM starts the search on the assigned object. In other words
Test.java
Animal animal = new Human();
The compiler looks at the left side (Animal) of this expression for a series of bindings, while the JVM first looks at the object on the right side (Human). Then, since this is an object of the Human class, it tries to find the method of eat: (D) V in the Human class. On the other hand, since the Human class contains eat: (I) V (I is an int type), I can't find a matching method. Therefore, the JVM searches for the corresponding method from the inheritance relationship. In other words, if you look at the parent class that the class inherits and if it is not there, you go to see the parent class, and so on, going up the hierarchy and looking for eat: (D) V. In this case, there was a corresponding method in the Animal class that went up one level, so the implementation part in this is bound to the calling method (animal.eat (quantity)) and processing is executed. As a result, the output was "The animal ate 500.0g."
Finally, let's look at an example of method binding.
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
System.out.println(list); // [1, 2, 3, 4]
list.remove(3);
System.out.println(list); // [1, 2, 3]
The List type remove method is overloaded with two methods that take different types of arguments, List.remove (int index) and List.remove (Object o) (https://docs.oracle.com/javase). /jp/8/docs/api/java/util/List.html). Here, remove has an int type argument, so list.remove (3) and List.remove (int index) are bound at compile time. And at runtime, the process (implementation part) in ArrayList.remove (int index) is bound to list.remove (3). As a result, the 4 in the third index of this list is removed and displayed as [1, 2, 3]. Nothing has changed so far. But what about the following example?
Collection<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
System.out.println(list); // [1, 2, 3, 4]
list.remove(3);
System.out.println(list); // [1, 2, 4] ← ??
The only thing that has changed is that the list container has changed from a List type to a Collection type. The result is displayed as [1, 2, 4]. The number 3 itself was removed, not the third in the index. Because Collection has only one remove method, Collection.remove (Object o). The compiler binds a remove method that takes this Object type as an argument to list.remove (3). As a result, the argument 3 in list.remove (3) is treated as an Object, not an index. Actually, when I check it with the javap command as before, it is displayed as Collection.remove: (Ljava / lang / Object;) Z (Z is a Boolean type). Then, following the bytecode instructions, the JVM will now execute ArrayList.remove (Object o) instead of ArrayList.remove (int index). As a result, [1, 2, 4] was displayed on the screen. If you didn't know about method bindings, you might have programmed with the assumption that the third index would normally be removed. If this happens, it will cause a failure. I thought that it would be a great advantage to know this mechanism to avoid it, so I wrote this article. Thank you for reading this far.
Recommended Posts