JavaRush /Java Blog /Random-TL /Polymorphism at mga kaibigan nito

Polymorphism at mga kaibigan nito

Nai-publish sa grupo
Ang polymorphism ay isa sa mga pangunahing prinsipyo ng object-oriented programming. Binibigyang-daan ka nitong gamitin ang lakas ng malakas na pag-type ng Java at magsulat ng magagamit at mapanatili na code. Marami na ang nasabi tungkol sa kanya, ngunit umaasa ako na ang lahat ay makapag-alis ng bago mula sa pagsusuring ito.
Полиморфизм и его друзья - 1

Panimula

Думаю, все мы знаем, что язык программирования Java принадлежит компании Oracle. Поэтому, наш путь начинается с сайта: www.oracle.com. На главной странице есть "Menu". В нём в разделе "Documentation" есть подраздел "Java". Всё, что относится к базовым функциям языка относится к "Java SE documentation", поэтому выбираем этот раздел. Раздел documentации откроется для последней версии, но пока что в "Looking for a different release?" выберем вариант: JDK8. На странице мы увидим много различных вариантов. Но нас интересует Learn the Language : "Java Tutorials Learning Paths". На этой странице мы найдём ещё один раздел: "Learning the Java Language". Это - святая из святых, tutorial по основам Java от Oracle. Java — an objectно-ориентированный язык программирования (ООП), поэтому изучение языка даже на сайте Oracle начинается с обсуждения основных концепций "Object-Oriented Programming Concepts". Из самого названия понятно, что Java ориентирован на работу с an objectми. Из подраздела "What Is an Object?" понятно, что an objectы в Java состоят из состояния и поведения. Представьте, что у нас есть счёт в банке. Количество денег на счету - это состояние, а методы работы с этим состоянием - это поведение. Объекты надо How-то описывать (рассказывать, Howое у них может быть состояние и поведение) и этим описанием является класс. Когда мы создаём an object Howого-то класса, то мы указываем этот класс и это называется "типом an object". Отсюда и говорится, что Java является строго типизированным языком, о чём сказано в спецификации яызка Java в разделе "Chapter 4. Types, Values, and Variables". Язык Java следует концепциям ООП и поддерживает наследование (Inheritance), используя ключевое слово extends (т.е. расширение типа). Почему расширение? Потому что при наследовании дочерний класс наследует поведение и состояние родительского класса и может их дополнить, т.е. расширить функциональность базового класса. Так же в описании класса может быть указан интерфейс (Interface) при помощи ключевого слова implements. Когда класс реализует интерфейс, это значит, что класс соответствует некоторому контракту - декларации программиста остальному окружению, что класс имеет определённое поведение. Например, у плеера есть различные кнопки. Эти кнопки - интерфейс для управления поведением плеера, а поведение будет изменять внутреннее состояние плеера (например, громкость). При этом состояние и поведение How описание дадут класс. Если класс реализует интерфейс, то an object созданный по этому классу может быть описан типом не только по классу, но и по интерфейсу. Давайте уже посмотрим на пример:
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();
    }
}
Тип — это очень важное описание. Оно рассказывает, How мы собираемся работать с an objectом, т.е. Howое поведение от an object ожидаем. Поведение - это методы. Поэтому, давайте разбираться с методами. На сайте Oracle методам отведён свой раздел в Oracle Tutorial : "Defining Methods". Первое, что стоит вынести из статьи: Сигнатура метода — это название метода и типы параметров:
Полиморфизм и его друзья - 2
Например, объявляя метод public void method(Object o), сигнатурой будет название method и тип параметра Object. Тип возвращаемого значения НЕ входит в сигнатуру. Это важно! Далее выполним компиляцию нашего исходного codeа. Как мы знаем, для этого code надо сохранить в файл с именем класса и с расширением java. Код на языке Java компorруется при помощи компилятора "javac" в некоторый промежуточный формат, который умеет выполнять виртуальная машина Java (JVM). Этот промежуточный формат называется bytecodeом и содержится в fileх с расширением .class. Выполним команду для компиляции: javac MusicPlayer.java После того, How java code скомпorрован, мы можем его выполнять. Используя утorту "java" для запуска будет запущен процесс виртуальный машины java для выполнения переданного в class файле bytecodeа. Выполним команду для запуска applications: java MusicPlayer. Мы увидим на экране текст, указанный во входном параметре метода println. Интересно, что имея bytecode в файле с расширением .class мы можем его посмотреть при помощи утorты "javap". Выполним команду <ocde>javap -c MusicPlayer:
Полиморфизм и его друзья - 3
Из bytecodeа мы можем увидеть, что вызов метода через an object, типом которого был указан класс выполняется при помощи invokevirtual, а компилятор вычислил, Howую сигнатуру метода надо использовать. Почему invokevirtual? Потому что идёт вызов(invoke переводится How вызывать) виртуального метода. What такое виртуальный метод? Это такой метод, тело которого может быть переопределено в момент выполнения программы. Представьте просто, что у вас есть некий список соответствий некоторого ключа (сигнатуры метода) и тела (codeа) метода. И это соответствие ключа и тела метода во время выполнения программы может меняться. Поэтому метод виртуальный. По умолчанию в Java методы, которые НЕ static, НЕ final и НЕ private, являются виртуальными. Благодаря этому Java поддерживает такой принцип an objectно-ориентированного программирования How полиморфизм. Как Вы уже могли понять, об этом наш сегодняшний обзор.

Полиморфизм

На сайте Oracle в их официальном Tutorial есть отдельный раздел: "Polymorphism". Воспользуемся Java Online Compiler'ом чтобы увидеть, How работает полиморфизм в Java. Например, у нас есть некоторый абстрактный класс Number, представляющий число в Java. What он позволяет? У него есть некоторые базовые методы, которые будут у всех наследников. Тот кто наследуется от Number буквально говорит - "Я число, со мной можно работать How с числом". Например, для любого наследника можно при помощи метода intValue() получить его Integer meaning. Если посмотреть java api для Number, то видно, что метод abstract, то есть данный метод каждый наследник Number должен реализовать сам. Но что нам это даёт? Посмотрим на пример:
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));
    }
}
Как видно из примера, благодаря полиморфизму, мы можем написать метод, который на вход будет принимать аргументы любого типа, который будет наследником Number (Number мы не можем получить, т.к. это абстрактный класс). Как было в примере с плеером, в данном случае мы говорим, что хотим работать с чем-то, How с Number. Мы знаем, что любой, кто является Number, обязан уметь предоставить своё integer meaning. И нам этого достаточно. Мы не хотим вдаваться в подробности реализации конкретного an object и хотим работать с этим an objectом через общие для всех наследников Number методы. Список методов, которые нам будут доступны, будет определён по типу во время компиляции (How это мы видели ранее в bytecodeе). В данном случае у нас тип будет Number. Как видно из примера, мы передаём различные числа разного типа, то есть на вход метод summ будет получать и Integer, и Long, и Double. Но всех их объединяет то, что они наследники от абстрактного Number, а следовательно переопределor у себя поведение в методе intValue, т.к. каждый конкретный тип знает, How этот тип нужно приводить к Integer. Такой полиморфизм реализован через так называемое переопределение, по английски Overriding.
Полиморфизм и его друзья - 4
Переопределение (Overriding) or динамический полиморфизм. Итак, начнём с того, что сохраним файл HelloWorld.java со следующим содержанием:
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();
    }
}
Выполним javac HelloWorld.java и javap -c HelloWorld:
Полиморфизм и его друзья - 5
Как видно, в bytecodeе для строчек с вызовом метода указана одинаковая link на метод для вызова invokevirtual (#6). Выполним java HelloWorld. Как мы видим, переменные parent и child объявлены c типом Parent, однако сама реализация вызвана согласно тому, Howой an object был присвоен переменной (т.е. an object Howого типа). Во время выполнения программы (ещё говорят в рантайме) JVM в зависимости от an object при вызове методов по одной и той же сигнатуре выполняла разные методы. То есть по ключу соответствующей сигнатуры сначала получor одно тело метода, а потом получor другое. В зависимости от того, Howой an object лежит в переменной. Такое вот определение в момент выполнения программы того, Howой метод будет вызван, называется ещё поздним связыванием or Dynamic Binding. То есть соответствие сигнатуры и тела метода выполняется динамически, в зависимости от an object, для которого вызывается метод. Естественно, нельзя переопределить статические члены класса (Class member), а так же члены класса с типом доступа private or final. На помощь разработчикам так же приходят аннотации @Override. Она помогает компилятору понять, что в этом месте мы собираемся переопределить поведение метод предка. Если мы ошиблись в сигнатуре метода, то компилятор нам сразу об этом скажет. Например:
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");
        }
}
Не скомпorруется с ошибкой: error: method does not override or implement a method from a supertype
Полиморфизм и его друзья - 6
С переопределением так же связано такое понятие, How "ковариантность" (Covariance). Рассмотрим пример:
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());
    }
}
Несмотря на внешнюю заумность смысл сводится к тому, что при переопределении мы можем вернуть не только тот тип, который был указан в предке, но и более конкретный тип. Например, предок возвращал Number, а мы можем вернуть Integer - наследника от Number. Тоже касается и исключений, объявленных в throws у метода. Наследники могут переопределить метод и уточнить бросаемое исключение. Но не могут расширить. То есть если родитель бросает IOException, то мы можем бросать более точное EOFException, но не можем бросать Exception. Аналогично, нельзя сужать область видимости и нельзя накладывать дополнительные ограничения. Например, нельзя добавлять static.
Полиморфизм и его друзья - 7

Сокрытие (Hiding)

Есть ещё такое понятие, How "сокрытие". Пример:
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();
    }
}
Это довольно очевидная вещь, если подумать. Статические члены класса относятся к классу, т.е. к типу переменной. Поэтому, логично, что если child имеет тип Parent, то и метод будет вызван у Parent, а не у child. Если мы посмотрим bytecode, How мы уже делали ранее, то увидим, что вызов статического метода осуществляется при помощи invokestatic. Это объясняет JVM, что надо смотреть на тип, а не по таблице методов, How это делал invokevirtual or invokeinterface.
Полиморфизм и его друзья - 8

Перегрузка методов (Overloading)

What мы видим ещё видим в Java Oracle Tutorial? В ранее изученном разделе "Defining Methods" есть что-то про Overloading. What это такое? По-русски это "перегрузка методов", а такие методы называются "перегруженными". Итак, перегрузка методов. На первый взгляд, всё просто. Откроем онлайн компилятор Java, например 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);
	}
}
Итак, тут всё кажется просто. Как и сказано в tutorial от Oracle, перегруженные методы (в данном случае это метод say) отличаются по количеству и типу аргументов, переданных в метод. Нельзя объявить одинаковые Name и одинаковое количество одинаковых типов аргументов, т.к. компилятор не сможет их отличить друг от друга. Тут стоит сразу отметить очень важную вещь:
Полиморфизм и его друзья - 9
То есть при перегрузке компилятор проверяет корректность. Это важно. Но How же на самом деле компилятор определяет, что нужно вызывать определённый метод? Он использует правило "the Most Specific Method", описанного в спецификации языка Java : "15.12.2.5. Choosing the Most Specific Method". Whatбы продемонстрировать его работу, возьмём пример из 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);
  }
}
Пример взять отсюда: https://github.com/stokito/OCPJP/blob/master/src/ru/habrahabr/blogs/java/OCPJP1/question1/Overload.j... Как видно, мы передаём в метод null. Компилятор пытается определить наиболее специфичный тип. Object не подходит, т.к. от него наследуются все. Идём дальше. Есть 2 класса исключений. Посмотрим на java.io.IOException и увидим, что в "Direct Known Subclasses" есть FileNotFoundException. То есть выходит, что FileNotFoundException самый специфичный тип. Поэтому, результатом будет вывод строки "FileNotFoundException". А вот если заменить IOException на EOFException, то получится, что у нас два метода находятся на одном уровне иерархии по дереву типов, то есть для них обоих IOException является родителем. Компилятор не сможет выбрать, Howой метод нужно будет вызывать и выдаст ошибку компиляции: reference to method is ambiguous. Ещё один пример:
public class Overload{
    public static void method(int... array) {
        System.out.println("1");
    }

    public static void main(String args[]) {
        method(1, 2);
    }
}
Выведет 1. Тут вопросов нет. Тип int... является vararg https://docs.oracle.com/javase/8/docs/technotes/guides/language/varargs.html и на самом деле является не более чем "синтаксическим сахаром" и на самом деле int... array можно читать How int[] array. Если мы теперь добавим метод:
public static void method(long a, long b) {
	System.out.println("2");
}
То станет выводится не 1, а 2, т.к. мы передаём 2 числа, и 2 аргумента более точное совпадение, чем один массив. Если мы добавим метод:
public static void method(Integer a, Integer b) {
	System.out.println("3");
}
То мы по прежнему будем видеть 2. Потому что в данном случае примитивы более точное совпадение, чем боксинг в Integer. Однако, если мы выполним method(new Integer(1), new Integer(2)); то будет выведено 3. Конструкторы в Java похожи на методы, а так How по ним тоже можно получить сигнатуру, то для них действуют те же правила "overloading resolution", что и перегруженные методы. Спецификация языка Java нам так и сообщает в "8.8.8. Constructor Overloading". Перегруз методов = Раннее связывание (оно же Static Binding) Часто можно услышать про раннее и позднее связывание, он же Static Binding or Dynamic Binding. Различие в них очень простое. Рано - это компиляция, поздно - это момент выполнения программы. Поэтому, раннее связывание (static binding) - определение того, Howой метод у кого будет вызван в момент компиляции. Ну а позднее связывание (dynamic binding) - определение того, Howой метод вызывать, непосредственно в момент выполнения программы. Как мы видели раньше (когда меняли IOException на EOFException), если мы перегрузим методы так, что компилятор не сможет понять, где Howой вызов выполнять, то мы получим ошибку во время компиляции: reference to method is ambiguous. Слово ambiguous в переводе с английского - двусмысленный or неопределённый, неточный. Получается, что перегрузка - это раннее связывание, т.к. проверка выполняется в момент компиляции. Whatбы подтвердить свои умозаключения откроем Java Language Specification на главе "8.4.9. Overloading" :
Полиморфизм и его друзья - 10
Получается, во время компиляции будет использована информация о типах и количестве аргументах (которая доступна на момент компиляции), чтобы определить сигнатуру метода. Если метод относится к методам an object (т.е. instance method), реальный вызов метода будет определён в runtime, используя dynamic method lookup (то есть динамическое связывание). Whatбы стало понятнее, возьмём пример, который похож на ранее рассмотренный:
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);
    }
}
Сохраним этот code в файл HelloWorld.java и скомпorруем его при помощи javac HelloWorld.java Теперь посмотрим, что там написал компилятор наш в bytecodeе, выполнив команду: javap -verbose HelloWorld.
Полиморфизм и его друзья - 11
Как указано, компилятор определил, что в будущем будет вызван некоторый виртуальный метод. То есть тело метода будет определено в runtime. Но на момент компиляции из всех трёх методов компилятор выбрал самый подходящий, поэтому указал номер: "invokevirtual #13"
Полиморфизм и его друзья - 12
А что это за methodref такой? Это link на метод. Грубо говоря, это некоторы ключ, по которому во время выполнения виртуальная Java машина сможет действительно определить, Howой метод нужно искать для выполнения. Подробнее можно ознакомиться в супер статье: "How Does JVM Handle Method Overloading And Overriding Internally".

Подведение итогов

Total, мы выяснor, что Java How an objectно-ориентированный язык поддерживает полиморфизм. Полиморфизм бывает статическим (Static Binding) и динамическим (Dynamic Binding). При статическом полиморфизме, он же раннее связывание, компилятор определяет, Howой метод и где нужно вызвать. Это позволяет использовать такой механизм, How перегрузка. При динамическом полиморфизме, он же позднее связывание, по ранее вычисленной сигнатуре метода в рантайме будет вычислен метод на основе того, Howой an object используется (т.е. метод Howого an object вызывается). То, How эти механизмы работают можно увидеть при помощи bytecodeа. Перегрузка смотрит на сигнатуры методов, а при разрешении перегрузки выбирается наиболее специфичный (наиболее точный) вариант. При переопределении смотрит на тип для определения, Howие доступны методы, а сами методы вызываются на основе an object. А так же материалы по теме: #Viacheslav
Mga komento
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION