Абстрактные классы и интерфейсы в Java: погружение в продвинутую теорию
Java позволяет реализовывать полиморфизм двумя ключевыми механизмами: абстрактными классами и интерфейсами. Несмотря на очень похожие концепции, они имеют важные различия, которые важно понимать для разработки эффективных приложений и успешного прохождения технических собеседований. В этой статье мы рассмотрим основные сценарии использования и теоретические аспекты различий между абстрактными классами и интерфейсами, а также рассмотрим примеры реализации задач с их помощью.
Для кого предназначена эта статья:
- Вы используете язык Java в своей работе или только учитесь на нем программировать.
- Вы хотите детально разобраться в различиях абстрактных классов и интерфейсов в Java, включая такие новейшие изменения как Sealed Classes представленные в JEP 409 в 17-й версии Java.
Несмотря на то что мы будем уделять внимание детальному, иногда дотошному, разбору базовых вопросов, которые будут полезны начинающим свой путь в разработке, опытные инженеры-программисты также найдут для себя много полезного в этом материале.
Определения
Для начала нам нужно дать определения абстрактному классу и интерфейсу в Java. Давайте рассмотрим этот вопрос на аналогии из реальной жизни:
Представим, что у нас есть два человека: мужчина и женщина. После рождения они будут иметь базовые данные вроде имени, фамилии, роста и веса. Также они будут уметь дышать и употреблять пищу. Со временем, они будут учиться и получать новые навыки вроде проведения интегральных вычислений, съемки видео для TikTok или создания презентаций в PowerPoint.
В этом примере:
- Человек — это базовый абстрактный класс, определяющий состояние объекта вроде имени, фамилии, возраста и базовое поведение в виде навыков дыхания и употребления пищи.
- Мужчина и женщина — это реализации абстрактного класса, определяющие поведение абстрактных методов и добавляющие свое состояние.
- Навык создания презентаций в PowerPoint — это интерфейс, который могут реализовывать разные люди и который не является для них обязательным.
UML-диаграмма:
Стоит упомянуть, что это лишь аналогия, и она не может полностью отобразить сложность и разнообразие реального мира. В реальности люди не могут быть легко классифицированы или ограничены определенными атрибутами или способностями. Это также относится и к программированию: абстрактные классы, интерфейсы и их реализации часто могут быть гораздо сложнее и разнообразнее, чем может показаться на первый взгляд.
Формальное определение абстрактного класса будет звучать следующим образом:
Абстрактный класс — это класс, объявленый как abstract, который имеет возможность включать методы. Абстрактные классы не могут быть созданы (инстанциированы), но они могут быть расширены (наследованы) другими классами.
Формальное определение интерфейса:
Интерфейс — это ссылочный тип, который может содержать константы, сигнатуры методов, методы по-умолчанию, статические методы и вложенные типы, при этом тела методов могут иметь только методы по-умолчанию и статические методы. Интерфейсы не могут быть созданы (инстанциированы), но могут быть реализованы классами или расширены (наследованы) другими интерфейсами.
Абстрактный класс предоставляет структуру с базовым поведением и состоянием, присущим всем его наследникам. Интерфейс, в свою очередь, можно сравнить с набором навыков, которые могут иметь работники в конкретной области. Конкретные классы работников могут реализовывать этот интерфейс и добавлять свои собственные методы, связанные с особенностями их профессии.
Таким образом, абстрактный класс и интерфейс можно использовать вместе для создания гибких и расширяемых приложений, которые могут адаптироваться к изменяющимся условиям и потребностям в различных областях.
Отличия абстрактных классов и интерфейсов
Для начала давайте рассмотрим классическое определение различий между абстрактными классами и интерфейсами, которые можно чаще всего услышать в большинстве технический статей и дискуссий. В следующих разделах мы рассмотрим продвинутые нюансы, которые обычно упускаются при поверхностном изучении этого вопроса.
Итак, абстрактный класс — это класс, который может иметь абстрактные методы, не может быть создан (инстанциирован), но можем иметь конструктор и состояние. Абстрактные методы — это методы, имеющие сигнатуру (название метода, принимаемые параметры и возвращаемый тип), но не имеющие реализации (тела метода). Абстрактный класс также может иметь неабстрактные методы, которые имеют реализацию (тело метода). Все конкретные классы, наследующиеся от абстрактного класса, должны определять реализацию (тело метода) для всех абстрактных методов. Состояние класса можно определить как переменные экземпляра и неабстрактные методы, которые могут получать доступ к этим переменным и изменять их.
Интерфейс, в свою очередь, — это набор абстрактных методов, которые должны быть реализованы классом. Один класс может реализовать множество интерфейсов, но наследоваться только от одного класса. Это дает возможность использовать интерфейсы для, своего рода, реализации множественного наследования, которое не поддерживается Java-классами. Когда класс реализует интерфейс, он должен предоставить реализацию для всех методов, объявленных в интерфейсе — аналогично абстрактным методам в абстрактном классе.
Главное различие между абстрактным классом и интерфейсом заключается в том, что абстрактный класс может иметь состояние, тогда как интерфейс нет. Отсюда вытекает тот факт, что абстрактный класс может иметь конструктор, тогда как интерфейс нет. Когда создается подкласс, конструктор его супер-класса (включая любой абстрактный класс) вызывается автоматически. Интерфейс не может иметь конструктор, потому что он не может быть создан.
Здесь сразу возникает очевидный вопрос — почему мы не можем создать (инстанциировать) абстрактный класс, если у него есть конструктор? Несмотря на наличие конструктора, абстрактный класс не может быть создан (инстанциирован) напрямую потому что он не является полноценным классом из-за отсутствия деталей имплементации — абстрактных методов. Если бы мы давали возможность напрямую создавать абстрактные классы, то у нас бы возникали исключительные ситуации при попытке вызвать его абстрактные методы из-за отсутствия у них реализации (тела метода), поэтому такое решение просто не имеет смысла.
Когда мы создаем объект класса наследника, конструктор абстрактного класса вызывается неявно для инициализации полей абстрактного класса (его состояния). Следовательно, можно считать, что конструктор абстрактного класса вызывается косвенно, а не напрямую. Это же объясняет и отсутствия конструктора у интерфейса в Java — так как у интерфейса нет состояния в виде переменных экземпляра и методов имеющих к ним доступ, то нет необходимости и в конструкторе, который их инициализирует.
Рассмотрим несколько базовых примеров кода для иллюстрации различий между абстрактным классом и интерфейсом в Java.
UML-диаграмма:
Пример интерфейса:
В этом примере Drawable является интерфейсом, который определяет единственный абстрактный метод draw(). Классы Circle и Rectangle реализуют интерфейс Drawable и предоставляют реализации метода draw(). Интерфейс Drawable не имеет переменных экземпляра (состояния) или конструкторов.
Пример реализации абстрактного класса:
UML-диаграмма:
В этом примере Animal — это абстрактный класс, который имеет конструктор, абстрактный метод makeSound() и неабстрактный метод sleep(). Классы Dog и Cat расширяют (наследуют) класс Animal и предоставляют реализации для метода makeSound(), где они получают доступ к переменной экземпляра name, определенной в классе Animal.
Можем ли мы не реализовывать метод интерфейса
Это довольно частый случай в рабочей практике. У нас может возникнуть ситуация, когда используя какой-либо интерфейс, мы не можем или не хотим реализовывать некоторые его методы. Такая ситуация может возникнуть по самым разным причинам, но чаще всего это вызвана не соблюдением принципа разделения интерфейса или Interface segregation principle (ISP), который является одним из пяти принципов SOLID и звучит следующим образом:
Принцип разделения интерфейсов говорит о том, что слишком «толстые» интерфейсы необходимо разделять на более маленькие и специфические, чтобы программные сущности маленьких интерфейсов знали только о методах, которые необходимы им в работе. В итоге, при изменении метода интерфейса не должны меняться программные сущности, которые этот метод не используют.
Общая практика в таком случае гласит, что когда класс реализует интерфейс, он должен предоставить реализацию для всех методов интерфейса. Если класс не предоставляет реализацию для какого-либо метода из интерфейсе, то такой класс должен быть объявлен абстрактным. Это даст нам возможность реализовать те методы интерфейса, которые мы хотим реализовать и объявить абстрактными остальные методы, не определяя их реализации.
Давайте рассмотрим пример такого интерфейса:
Если мы хотим реализовать интерфейс InterfaceNotFollowingSegregationPrinciple, но не хотим предоставлять реализацию для unnecessaryMethod(), мы можем объявить свой класс абстрактным:
UML-диаграма:
В этом случае MyClass предоставляет реализацию для goodMethod(), но не предоставляет реализацию для unnecessaryMethod(). Однако, так как абстрактный класс не может быть создан (инстанциирован), то в какой-то момент нам все равно придется создать класс-наследник, реализующий все абстрактные методы.
Использование дефолтных и статических методов в интерфейсах
Что если нам не подходит решение в виде объявления класса абстрактным? В Java 8 и более поздних версиях можно использовать ключевое слово default для решения этой задачи.
Методы по-умолчанию были представлены в восьмой версии Java чтобы позволить интерфейсам определять дефолтную реализацию и предоставлять обратную совместимость при добавлении новых методов в существующий интерфейс. Метод по-умолчанию — это метод, объявленный в интерфейсе, который имеет ключевое слово default в свой сигнатуре и тело метода с реализацией по-умолчанию, которую могут использовать классы, реализующие интерфейс. Класс реализующий интерфейс, может использовать дефолтную реализацию или переопределить ее и предоставить свою собственную.
Пример интерфейса с методом по-умолчанию:
В этом примере у MyInterface есть дефолтная реализация для method2() . Любой класс, который реализует MyInterface, может выбрать между переопределением method2() или использованием реализации по-умолчанию.
Вот пример класса, который реализует MyInterface и переопределяет method1(), но не method2() :
UML-диаграмма:
В этом примере MyClass реализует метод method1(), но не предоставляет реализацию для метода method2(). Так как метод method2() имеет реализацию по-умолчанию в интерфейсе MyInterface, MyClass может использовать эту дефолтную реализацию.
Также стоит упомянуть, что кроме дефолтных методов в Java 8 были введено использование статических (static) методов в интерфейсе. Это методы которые также имеют тело метода с реализацией, но которые не могут быть переопределены классами реализующими интерфейс, что обусловленно самой спецификой статических методов.
UML-диаграмма:
Пример кода с использованием статического методы в интерфейсе:
Возвращаясь к нашему вопросу о том, что делать в случае если мы не хотим определять реализацию метода интерфейса в каком-либо классе. Здесь можно использовать еще один довольно экзотический вариант, если по какой-то причине мы не можем использовать абстрактный класс или дефолтный метод — реализовать класс-обертку, который реализует интерфейс и предоставляет реализацию по-умолчанию для всех методов в интерфейсе. Затем мы сможем расширить этот класс-обертку и переопределить только те методы, которые мы хотим реализовать в своем классе.
UML-диаграмма:
Пример класса-обертки:
В этом примере MyWrapperClass реализует интерфейс MyInterface и предоставляет реализацию по-умолчанию для обеих методов method1() и method2() . MyClass расширяет MyWrapperClass и переопределяет только method1() . Поскольку method2() имеет реализацию по умолчанию в MyWrapperClass, MyClass может использовать реализацию по-умолчанию для method2() . Если MyClass предоставит свою реализацию для method2() , это переопределит дефолтную реализацию в MyWrapperClass.
Этот подход может быть полезен, если мы хотим предоставить дефолтные реализации для нескольких методов в интерфейсе, не используя дефолтные методы или абстрактные классы. Однако это является сложной и не интуитивной реализацией, которую стоит избегать в современных версиях языка.
Лучшей практикой является использование абстрактных классов для случаев когда реализация метода может быть определена в классах-наследниках. В случаях когда нам нужно предоставить обратную совместимость, лучше всего подойдет использование методов по-умолчанию в интерфейсах. Если же по какой-то причине нам необходимо реализовать какой-то интерфейс без определения тела одного из его методов и явно указать, что это метод не поддерживается нашим интерфейсом, то мы можем использовать следующий подход:
UML-диагрмма:
В этом примере MyClass реализует MyInterface и оба его метода. В теле unsupportedMethod() мы выбрасываем исключение UnsupportedOperationException с сообщением «Not supported method». При вызове метода unsupportedMethod() в классе Main, пользователь получит исключение UnsupportedOperationException, что явно укажет ему на то, что данный метод не поддерживается.
Изменения абстрактных классов и интерфейсов в версиях Java 8-17
Мы уже рассмотрели часть довольно фундаментальных изменений представленных в восьмой версии Java — добавление статических методов и методов по-умолчанию в интерфейсы. Кроме этого, в Java 8 было представлено еще одно мощное изменение — функциональные интерфейсы, которое мы рассмотрим далее в данной статье.
В девятой версии Java возможности интерфейсов расширились еще больше — в этой версии была представлена возможность создавать приватные переменные и классы внутри интерфейсов, что позволило им иметь свое внутреннее состояние. Это значительное изменение, которое частично влияет на главное различие между абстрактными классами и интерфейсами — наличие своего состояния и методов способных его изменять.
Пятнадцатая версия Java добавила еще одно важное изменение — возможность создавать новые типы запечатанных (sealed) классов и интерфейсов, что было окончательно закреплено в семнадцатой версии языка (JEP 409). Это позволило ограничивать типы, которые могут реализовывать или наследовать класс или интерфейс.
Давайте по порядку рассмотрим все эти изменения чтобы полноценно понимать различия между абстрактными классами и интерфейсами, которые значительно изменились в новейших версиях Java.
Функциональные интерфейсы
Функциональные интерфейсы в Java – это интерфейсы, которые имеют только один абстрактный метод. С появлением Java 8 и введением лямбда-выражений, функциональные интерфейсы стали основой для этих новых функций. Они позволяют использовать лаконичные и читаемые лямбда-выражения вместо анонимных классов.
Вот простой пример функционального интерфейса:
В этом примере интерфейс SimpleFunction имеет только один абстрактный метод apply. Аннотация @FunctionalInterface необязательна, но она помогает явно указать, что интерфейс должен быть функциональным, и компилятор Java будет проверять, что интерфейс удовлетворяет требованиям функционального интерфейса.
Пример использования этого интерфейса с лямбда-выражением:
В этом примере triple – это экземпляр функционального интерфейса SimpleFunction, созданный с использованием лямбда-выражения. Это позволяет создавать гибкие и мощные абстракции с минимальным синтаксисом.
Java 8 также предоставляет набор встроенных функциональных интерфейсов в пакете java.util.function, таких как Function<T,R>, Predicate<T>, Consumer<T>, Supplier<T> и др., что позволяет использовать лямбда-выражения и ссылки на методы еще более широко в нашем коде.
Внутреннее состояние интерфейсов
С версии Java 9, интерфейсы получили возможность определять приватные методы. Это позволяет разработчикам писать код, который может быть общим для двух и более методов по-умолчанию или статических методов в интерфейсе, и скрывать этот общий код в приватном методе.
Помимо этого, в Java 9 была добавлена возможность определять приватные статические методы, что позволяет разрабатывать более структурированный и модульный код.
Важно отметить, что оба типа методов не могут быть переопределены в классе, который реализует интерфейс. Такие методы могут быть вызваны только из других методов в том же интерфейсе. Они предназначены для внутреннего использования интерфейсом и не являются частью его общедоступного API.
Пример использования приватного и статического методов в интерфейсе:
В этом примере commonPrivateMethod() — это приватный метод, который содержит общий код для defaultMethod1() и defaultMethod2() — это методы по-умолчанию, которые имеют реализацию прямо в интерфейсе. Эти методы могут быть переопределены в классе, который реализует интерфейс, но если они не переопределены, то будет использоваться реализация по-умолчанию.
Приватный метод commonPrivateMethod() используется для определения поведения, которое является общим для defaultMethod1() и defaultMethod2(), что позволяет избежать дублирования кода в методах по-умолчанию. Метод nonDefaultMethod() — это абстрактный метод, который не имеет реализации в интерфейсе, и, соответственно, не имеет доступа к приватным и статическим методам интерфейса.
Интерфейсы в Java могут содержать статические переменные. Эти переменные являются публичными, статическими и финальными по-умолчанию, и их можно использовать для определения констант, которые связаны с интерфейсом. В этом примере добавлены две статические переменные: COMMON_MESSAGE и STATIC_MESSAGE. Они используются в методах commonPrivateMethod() и commonPrivateStaticMethod(), соответственно, для вывода сообщений.
Вот пример класса, который реализует MyInterface:
UML-диаграмма:
В общем, добавление приватных статических методов в интерфейсы позволяет улучшить структуру и модульность кода, предотвращая дублирование кода и упрощая его чтение и поддержку.
Sealed классы и интерфейсы
Sealed классы и интерфейсы являются одной из новых функций в Java, впервые появившихся в Java 15 в виде предварительного предложения (preview feature) и ставших стандартом с Java 17 (JEP 409).
Sealed классы и интерфейсы позволяют разработчикам ограничивать наследование. В обычной Java любой класс или интерфейс может быть расширен или реализован, если он не является финальным. Однако с помощью sealed классов и интерфейсов разработчики могут указать, какие другие классы или интерфейсы могут наследоваться от них.
Это делается с помощью ключевого слова sealed, а также с помощью permits в объявлении класса или интерфейса. Ключевое слово permits используется для указания списка классов, которые могут наследовать или реализовать sealed класс или интерфейс.
Пример абстрактного sealed класса:
В этом примере Shape является абстрактным sealed классом, который содержит одно поле name и один метод area(). Метод area() абстрактный, поэтому каждый подкласс должен предоставить свою реализацию этого метода.
Теперь давайте определим классы Circle и Rectangle:
UML-диаграмма:
В этом примере классы Circle и Rectangle расширяют класс Shape и предоставляют свою реализацию метода area(). Класс Circle содержит дополнительное поле radius, а класс Rectangle — поля width и height. Оба этих класса объявлены как final, так что они не могут быть дальше расширены.
Классы, которые расширяют или реализуют sealed классы или интерфейсы, должны быть объявлены как final, sealed или non-sealed. Ключевое слово final означает, что класс не может быть дальше расширен. Sealed означает, что класс сам по себе является sealed и должен указать, какие классы могут его расширять. Non-sealed — что класс может быть расширен любыми другими классами.
Sealed классы и интерфейсы улучшают модель объектно-ориентированного программирования в Java, поскольку они предоставляют больше контроля над наследованием — это удобно, если вы хотите ограничить наследование только некоторыми подклассами, чтобы обеспечить определенные гарантии поведения или инварианты. Также это приводит к созданию ограниченных иерархий, где вы знаете все возможные подклассы во время компиляции, что удобно для моделирования алгебраических типов данных или ограниченных доменных моделей.
Sealed классы особенно полезны в сочетании с pattern matching (шаблонное сопоставление). Поскольку компилятор знает все подклассы sealed класса, он может проверить, что ваш код обрабатывает все возможные случаи, и предупредить вас, если вы пропустили какой-то. Если же вы хотите разрешить наследование в некоторых случаях, но сохранить большую часть иерархии классов закрытой, вы можете использовать non-sealed классы. Это удобно, когда вы хотите предоставить разработчикам возможность расширять часть вашей иерархии классов, но ограничить их в других местах.
Стоит помнить, что, как всегда при использовании наследования, стоит быть осторожными и использовать его только тогда, когда оно действительно имеет смысл с точки зрения дизайна вашего приложения. Во многих случаях, композиция может быть более гибкой и мощной альтернативой.
Сценарии использования
Учитывая изменения в современных версиях Java, мы можем логично обратить внимания на то факт, что абстрактные классы и интерфейсы стали крайне близкими по функционалу и поведению. В пользу какой концепции стоит же делать выбор при проектированию иерархии объектов в ходе программирования? Давайте рассмотрим ключевые сценарии использования интерфейсов и абстрактных классов.
Интерфейсы в Java служат для определения "контрактов" поведения, которые классы обязуются выполнять. В контексте программирования, контракт представляет собой набор методов (сигнатур методов), которые класс должен реализовать, если он реализует интерфейс.
Давайте рассмотрим пример из реального мира. Предположим, у вас есть приложение для электронной коммерции, и вам необходимо обрабатывать различные типы платежей, такие как кредитные карты, PayPal, банковские переводы и т.д. В этом случае, вы могли бы определить интерфейс PaymentProcessor следующим образом:
Здесь PaymentData — это класс, который содержит всю информацию о платеже. Теперь каждый класс, который обрабатывает конкретный тип платежа (например, CreditCardProcessor, PaypalProcessor, BankTransferProcessor и т.д.), должен реализовывать этот интерфейс и обеспечивать свою собственную реализацию метода processPayment().
С введением методов по-умолчанию (default methods) в Java 8, мы получили возможность определять стандартное поведение в интерфейсе, которое может быть переопределено в классах, реализующих интерфейс. Это может быть полезно, например, когда вы хотите расширить интерфейс без нарушения существующих классов, которые его реализуют. Это одна из лучших практики использования методов по-умолчанию, которая помогает не нарушать существующие контракты в коде.
Например, вы можете добавить в интерфейс PaymentProcessor метод по умолчанию supportsRecurringPayments(), который возвращает false:
Теперь каждый процессор платежей по умолчанию не поддерживает повторяющиеся платежи, но процессоры, которые поддерживают такие платежи, могут переопределить этот метод и возвращать true.
Таким образом, с помощью интерфейсов, Java предоставляет мощные инструменты для организации и структурирования кода, упрощения тестирования (поскольку интерфейсы облегчают создание мок-объектов) и обеспечения гибкости и расширяемости приложений.
Абстрактные классы в Java используются для создания классов, которые содержат общую реализацию для нескольких подклассов, но которые не могут быть использованы для создания объектов напрямую. С помощью абстрактных классов можно определять общую структуру и иерархию объектов связанных типов. Подклассы, расширяющие супер-класс, будут иметь возможность предоставить свою реализацию абстрактных методов, дополнить состояние и расширить поведение таких классов.
Представим, что вы разрабатываете коммуникационный сервис и вам нужно обрабатывать "возвраты" сообщений отправленных по электронной почте, к примеру, из-за некорректно указанного email-адреса. В случае если мы используем разных почтовых провайдеров, у каждого из них будет свой формат ответа на такие некорректные отправки (bounce). В этом случае мы можем создать абстрактный класс AbstractBounceProperties определив общие свойства (состояние) таких объектов следующим образом:
Здесь AbstractBounceProperties — это абстрактная модель свойств для управления обработкой возвращаемых электронных писем (bounce messages). Он содержит свойства, являющиеся базовыми для всех почтовых провадеров:
- serverAddress — адрес сервера, с которого отправляются письма;
- BounceType — тип возврата, который может быть неопределенным, постоянным, или временным;
- bouncedRecipient — вложенный класс, который содержит информацию о получателе, которому не удалось доставить письмо;
- prefixText и postfixText — эти два поля представляют собой текст, который будет добавлен в начало и конец возвращаемого сообщения соответственно.
Теперь вы сможете создать конкретные классы-наследники для каждого почтового провайдера (например, GmailBounceProperties и AmazomSesBounceProperties). В этих классах вы сможете определить свойства и методы, которые специфичны для каждого провайдера.
Этот подход помогает упростить код и обеспечивает большую гибкость. Однако стоит помнить, что, как всегда при использовании наследования, стоит быть осторожными и использовать его только тогда, когда оно действительно имеет смысл с точки зрения дизайна вашего приложения, поскольку оно может привести к жесткой связанности кода. Во многих случаях, композиция может быть более гибкой и мощной альтернативой.
Композиция позволяет создавать более модульные и управляемые структуры, объединяя объекты в более сложные структуры, что способствует повторному использованию кода и изолированию обязанностей. Это также облегчает тестирование, поскольку каждый компонент может быть протестирован независимо.
Возвращаясь к нашему примеру с обработкой возвратов email-сообщений, можно создать отдельные компоненты, такие как BounceHandler, RecipientHandler, MessageTypeHandler и т.д., каждый из которых имеет свои обязанности и манипулирует своими конкретными аспектами процесса обработки возвращаемых писем. Это бы обеспечило хорошую модульность, упростило бы тестирование и сделало код более понятным и управляемым.
Однако, как уже было упомянуто, наследование имеет свое место и может быть очень полезным при правильном использовании. Важно понимать требования вашего конкретного проекта и применять наиболее подходящие паттерны и подходы, чтобы обеспечить чистоту и эффективность вашего кода.
Заключение
В этой статье мы рассмотрели ключевые концепции, лежащие в основе использования интерфейсов и абстрактных классов в Java, и то, как они эволюционировали с учётом последних обновлений языка.
Мы увидели, что использование абстрактных классов и интерфейсов позволяет увеличить модульность и повторное использование кода, делая его более гибким и легко адаптируемым к изменяющимся требованиям. Однако, мы также обсудили потенциальные проблемы, которые могут возникнуть при неправильном использовании этих концепций, включая жесткую связанность кода и вызовы связанные с тестированием.
Важно отметить, что в то время как интерфейсы и абстрактные классы являются важными инструментами в арсенале Java-программистов, они не являются единственными. Язык продолжает активно развиваться, и в последних версиях было внесено множество улучшений, которые предоставляют разработчикам больше возможностей для написания чистого, эффективного и легко-читаемого кода.
Например, долгожданное введение записей (records) в Java 16 упрощает создание классов, которые просто "держат" данные. Введение сопоставления типов (pattern matching), которое в настоящее время находится в процессе стандартизации, позволяет создавать более чистый и понятный код при работе с объектами разных типов. Новые функции, такие как Sealed Classes (запечатанные классы) и Pattern Matching, предоставленные в Java 17 и Java 18, позволяют разработчикам писать более безопасный и выразительный код.
Правильное использование рассмотренных нами функций и концепций, включая интерфейсы и абстрактные классы, поможет вам писать качественный код и проектировать расширяемые системы, которые могут эффективно адаптироваться и развиваться по мере изменения требований и условий.
Всегда стоит помнить, что выбор подхода зависит от конкретной задачи, и нет единого "правильного" решения для всех сценариев. Экспериментируйте с разными подходами, используйте лучшие практики, но помните, что конечная цель — это создание решений, которые работают эффективно и отвечают требованиям вашего конкретного проекта.