[Java] About Java method binding

7 minute read

Today I’m going to write an article about Java method bindings. I’ve been studying Java and I wasn’t really aware of it, so I decided to organize it properly.

Prior knowledge

Let’s review the cast as prior knowledge. Casts include explicit and implicit casts. For details, please see the following URL. https://techacademy.jp/magazine/28486 You should also be aware of compile-time and run-time timing. Compile time is the timing when the Java file I wrote is converted into bytecode. Runtime is when the JVM actually executes that bytecode. If you are developing in Eclipse, normally if you write code, it will be compiled without permission and the code will be executed at the timing of pressing the execute button. When developing in a terminal etc., compile with the javac command to create a class file and then execute it with the java command. Also, the terms “tie” and “bind” used in the article all have the same meaning. (I’m sorry to be confusing.)

What is method binding in the first place?

I didn’t know the meaning of this word myself, but it’s simply like Java’s method call, the signature of the called method, and the mechanism that binds (binds) the implementation parts. The signature is the method name + arguments, and the implementation part is the processing in {}. The method consists of these two, but for now, please think separately. If you don’t understand this, you may get stuck in unexpected places (though I’m not an engineer yet, I can’t say I’m great…). Let’s take a concrete look.

Animal.java


public class Animal {
      public void eat(double quantity) {
             System.out.println("Animal" + quantity + "g ate.");
      }
}

Human.java


public class Human extends Animal {
      public void eat(int quantity) {
            System.out.println("Human" + quantity + "g ate.");
      }
}

Test.java


public class Test {
     public static void main(String[] args) {
           Animal animal = new Human();
           int quantity = 500;
           animal.eat(quantity); //?
     }
}

All classes are in the same package. What will be output when Test.java is executed here? And why?

Method binding

The correct answer is output as “The animal ate 500.0 g.” Why. At first I thought that Human should have been output as “Human has eaten 500g” because Human is assigned as a reference. It is also an int type that is passed as an argument in the Test class. But the answer was different. Let’s look at the timing of method binding 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  

This time, eat is a non-static method, so I will explain based on this (static method also has the same idea).

Compile-time binding

At compile time, Java binds the called method with its signature. In other words, it’s safe to say that the compiler determines the signature of the method that is called each time. Consider this rule along with the code we are looking at right now. The last line of Test.java looked like this:

Test.java


animal.eat(quantity);

First, the compiler looks for the declaration type of the variable animal. The type of animal is Animal type. Then the compiler starts searching, “Yeah, yeah. This variable is of type Animal class, so let’s see if there is a method in this class that is being called.” At this time, the method that is compatible with the called method is also included in the search scope. Actually in the Animal class

Animal.java


eat(double quantity)

The signature is defined. At the caller, an int type is passed as an argument, but since the conversion of int → double is implicitly done (without explicit casting), the compiler says “This is the method currently called It is a method compatible with the above.” and connects the method call of animal.eat(quantity) and the eat method (signature) that takes a double type as an argument. At this point the compiler has determined that the argument to eat is of type double. And it can no longer be changed at runtime. Let’s actually check.

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
}

This is after compiling each file with the javac command from the terminal, and doing javap -c Test to see the contents of the compiled code. Here are the lines that you want to be aware of.

15: invokevirtual #4 // Method Animal.eat:(D)V

This corresponds to the calling part of the method in the Test class, animal.eat(quantity). (D) indicates that the argument is a double type, V is the return value is void. Invoke virtual means that the actual implementation is decided at runtime. The code is executed at Runtime according to the instructions of this bytecode. In other words, in the compiler, the contents of the process are determined by the JVM at run time by simply associating “the method being called is the eat method of Animal” as described above.

Runtime binding

All you have to do is execute the animal.eat(quantity) method according to the bytecode instructions. I said earlier that the compiler will look for the declaration type of the variable animal. The JVM will start searching from the assigned object. That is,

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 right side (Human) object. Then, since this is a Human class object, we try to find a method of eat:(D)V in the Human class. On the other hand, since it is eat:(I)V in the Human class (I is an int type), no matching method is found. So the JVM looks for the corresponding method in the inheritance relationship. In other words, if you look at the parent class that the class inherits and if you do not find it there, then you go up to the hierarchy and go search for eat:(D)V. This time there was a corresponding method in the Animal class that went up one level, so the implementation part in this is bound to the method (animal.eat(quantity)) of the caller and the process is executed. As a result, “the animal ate 500.0 g.” was output.

Method binding example

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 remove method of List type is overloaded with two methods that take different type arguments of List.remove(int index) and List.remove(Object o) (https://docs.oracle.com/javase /jp/8/docs/api/java/util/List.html ). Here, since the int type argument is put in remove, list.remove(3) and List.remove(int index) are bound at compile time. Then, at execution time, the processing (implementation part) in ArrayList.remove(int index) is bound to list.remove(3). As a result, the 4 at the third index in this list is deleted and displayed as [1, 2, 3]. So far nothing has changed. But what about the next 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 change is that the container of list is changed from List type to 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 list.remove(3) with the remove method that takes this Object type as an argument. As a result, the argument 3 of list.remove(3) is treated as an Object instead of an index. If you actually check it with the javap command as before, it will be displayed as Collection.remove:(Ljava/lang/Object;)Z (Z is a Boolean type). Then, according to the bytecode instructions, this time ArrayList.remove(int o) instead of ArrayList.remove(int index) is executed by the JVM. As a result, [1, 2, 4] is displayed on the screen. If you didn’t know about method binding, you might have programmed it with the assumption that the third index would normally be deleted. If that happens, it will cause a failure. I think that there is a big merit to know this mechanism to avoid it, so I wrote this article. Thank you for reading this far.

Tags:

Updated: