Блок Примитивные типы
Скачать 6.67 Mb.
|
В чем преимущество ООП?более понятное представление структуры кода, основанное на сущностях предметной области, взятых из реального мира, что делает разработку более естественной для человека; уменьшение сложности программного обеспечения; повышение надежности программного обеспечения; обеспечение возможности модификации отдельных компонентов программного обеспечения без изменения остальных его компонентов; обеспечение возможности повторного использования отдельных компонентов программного обеспечения. Класс – это способ описания сущности, определяющий состояние и поведение, зависящее от этого состояния, а также правила для взаимодействия с данной сущностью (контракт). С точки зрения программирования класс можно рассматривать как набор данных (полей, атрибутов, членов класса) и функций для работы с ними (методов). С точки зрения структуры программы, класс является сложным типом данных. В нашем случае, класс будет отображать сущность – автомобиль. Атрибутами класса будут являться двигатель, подвеска, кузов, четыре колеса и т.д. Методами класса будет «открыть дверь», «нажать на педаль газа», а также «закачать порцию бензина из бензобака в двигатель». Первые два метода доступны для выполнения другим классам (в частности, классу «Водитель»). Последний описывает взаимодействия внутри класса и не доступен пользователю. Классы в Java могут иметь модификатор public, либо не иметь явно указанного модификатора. У полей и методов может быть один из четырех, считая модификатор по умолчанию. Однако объявление публичного метода в непубличном классе вряд ли имеет смысл. Анонимный класс - это вложенный локальный класс без имени, который разрешено декларировать в любом месте обрамляющего класса, разрешающем размещение выражений. Создание экземпляра анонимного класса происходит одновременно с его объявлением. В зависимости от местоположения анонимный класс ведет себя как статический либо как нестатический вложенный класс - в нестатическом контексте появляется окружающий его экземпляр. Анонимные классы имеют несколько ограничений: Их использование разрешено только в одном месте программы - месте его создания; Применение возможно только в том случае, если после порождения экземпляра нет необходимости на него ссылаться; Реализует лишь методы своего интерфейса или суперкласса, т.е. не может объявлять каких-либо новых методов, так как для доступа к ним нет поименованного типа. Класс называется вложенным (Nested class), если он определен внутри другого класса. Вложенный класс должен создаваться только для того, чтобы обслуживать обрамляющий его класс. Если вложенный класс оказывается полезен в каком-либо ином контексте, он должен стать классом верхнего уровня. Вложенные классы имеют доступ ко всем (в том числе приватным) полям и методам внешнего класса, но не наоборот. Из-за этого разрешения использование вложенных классов приводит к некоторому нарушению инкапсуляции. Существуют четыре категории вложенных классов: Static nested class (Статический вложенный класс); Member inner class (Простой внутренний класс); Local inner class (Локальный класс); Anonymous inner class (Анонимный класс). Такие категории классов, за исключением первого, также называют внутренними (Inner class). Внутренние классы ассоциируются не с внешним классом, а с экземпляром внешнего. Каждая из категорий имеет рекомендации по своему применению. Если вложенный класс должен быть виден за пределами одного метода или он слишком длинный для того, чтобы его можно было удобно разместить в границах одного метода и если каждому экземпляру такого класса необходима ссылка на включающий его экземпляр, то используется нестатический внутренний класс. В случае, если ссылка на обрамляющий класс не требуется - лучше сделать такой класс статическим. Если класс необходим только внутри какого-то метода и требуется создавать экземпляры этого класса только в этом методе, то используется локальный класс. А, если к тому же применение класса сводится к использованию лишь в одном месте и уже существует тип, характеризующий этот класс, то рекомендуется делать его анонимным классом. Класс, помеченный модификатором abstract, называется абстрактным классом. Такие классы могут выступать только предками для других классов. Создавать экземпляры самого абстрактного класса не разрешается. При этом наследниками абстрактного класса могут быть как другие абстрактные классы, так и классы, допускающие создание объектов. Абстрактные классы могут содержать абстрактные методы. Это методы, для которых есть сигнатура, но нет наполнения. Это способ заставить реализовать эти методы в классах-потомках. У абстрактного класса также может быть конструктор, который по принципу наследования смогут использовать его потомки. Это сделано для минимизации повторяющегося кода, если у классов-потомков, к примеру, есть повторяющиеся поля, их можно вынести в родительский абстрактный класс, написать конструктор, в который в качестве параметра передается это повторяющееся поле, а в самих классах-потомках конструктор будет начинаться с вызова конструктор родительского класса через ключевое слово super, а затем будет идти специализированная часть. Абстрактный класс может не иметь абстрактных методов. В таком случае, его единственная функция - это отсутствие возможности создания экземпляров этого класса. Но с другой стороны, если в классе есть хотя бы один абстрактный метод, класс тоже должен быть объявлен абстрактным. Интерфейс - это полностью абстрактный класс. Одним из главных отличий от абстрактного класса является то, что в интерфейсе все методы только определяются, но у них нет никакой реализации (с Java 8 есть возможность реализовать default-реализацию и наделять интерфейсы static-методами с написанной реализацией). Интерфейс также может содержать и поля. В этом случае они автоматически являются публичными public, статическими static и неизменяемыми final. В некоторых интерфейсах вообще не определяют методов. Это так называемые маркерные интерфейсы. Они просто указывают что класс относится к определенному типу. Примером может послужить интерфейс Clonable, который указывает на то, что класс поддерживает механизм клонирования. Интерфейсы - это способ достижения полиморфизма и максимальной абстракции. Это удобно, когда нам нужно выстроить иерархию не подобных объектов, реализующих похожие методы. Также класс в Java может реализовывать несколько интерфейсов, но наследоваться только от одного класса. В этом преимущество. Начиная с Java 8, любые методы в интерфейсе могут содержать default-реализацию. Также в интерфейсе можно написать static-метод с default-реализацией. Отличия абстрактного класса от интерфейса: В Java класс может одновременно реализовать несколько интерфейсов, но наследоваться только от одного класса. Абстрактные классы используются только тогда, когда присутствует тип отношений «is a» (является). Интерфейсы могут реализоваться классами, которые не связаны друг с другом. Абстрактный класс - средство, позволяющее избежать написания повторяющегося кода, инструмент для частичной реализации поведения. Интерфейс - это средство выражения семантики класса, контракт, описывающий возможности. Все методы интерфейса неявно объявляются как public abstract или (начиная с Java 8) default - методами с реализацией по-умолчанию, а поля - public static final. Интерфейсы позволяют создавать структуры типов без иерархии. Наследуясь от абстрактного, класс «растворяет» собственную индивидуальность. Реализуя интерфейс, он расширяет собственную функциональность. Абстрактные классы содержат частичную реализацию, которая дополняется или расширяется в подклассах. При этом все подклассы схожи между собой в части реализации, унаследованной от абстрактного класса, и отличаются лишь в части собственной реализации абстрактных методов родителя. Поэтому абстрактные классы применяются в случае построения иерархии однотипных, очень похожих друг на друга классов. В этом случае наследование от абстрактного класса, реализующего поведение объекта по умолчанию может быть полезно, так как позволяет избежать написания повторяющегося кода. Во всех остальных случаях лучше использовать интерфейсы. Объект (экземпляр) – это отдельный представитель класса, имеющий конкретное состояние и поведение, полностью определяемое классом. Говоря простым языком, объект имеет конкретные значения атрибутов и методы, работающие с этими значениями на основе правил, заданных в классе. В данном примере, если класс – это некоторый абстрактный автомобиль из «мира идей», то объект – это конкретный автомобиль, стоящий у вас под окнами. Пакеты позволяют организовать классы логически в наборы. Организация классов в виде пакетов позволяет избежать конфликта имен между классами. Ведь нередки ситуации, когда разработчики называют свои классы одинаковыми именами. Принадлежность к пакету позволяет гарантировать однозначность имен. Чтобы указать, что класс принадлежит определенному пакету, надо использовать директиву package, после которой указывается имя пакета. Классы необязательно определять в пакеты. Если для класса пакет не определен, то считается, что данный класс находится в пакете по умолчанию, который не имеет имени. Если нам надо использовать классы из других пакетов, то нам надо подключить эти пакеты и классы. Исключение составляют классы из пакета java.lang (например, String), которые подключаются в программу автоматически. То есть мы указываем полный путь к файлу в пакете при создании его объекта. Однако такое нагромождение имен пакетов не всегда удобно, и в качестве альтернативы мы можем импортировать пакеты и классы в проект с помощью директивы import, которая указывается после директивы package Конструктор - это специальный метод для инициализации объекта во время его создания при помощи оператора new. Существуют три базовых типа конструкторов в Java: • Конструктор по умолчанию (default constructor) • Конструктор без аргументов (no-args constructor) • Параметризованный конструктор (parameterized constructor) Конструктор по умолчанию (default constructor) - невидимый конструктор, который создается автоматически компилятором и инициализирует поля объекта значениями по умолчанию. Тут главное не путаться в терминологии: конструктор по умолчанию создает компилятор, если программист не создал свой; конструктор без аргументов (no-argument constructor) создает программист, а по умолчанию уже не создается. Оба конструктора не принимают аргументы, но при этом называются по-разному. Хотя у конструктора и не указывается возвращаемый тип, но он все же неявно возвращает тип создаваемого объекта. Непосредственно перед тем, как будет возвращен результат в виде ссылки на только что созданный объект, указанный конструктор будет использован для инициализации этого нового объекта.» Таким образом, вызов new BankAccount() вызывает созданный нами конструктор без аргументов и неявно возвращает результат в виде ссылки на только что созданный объект. Итак, чтобы сделать что-то действительно полезное с нашим банковским счетом, необходимо иметь возможность передавать нужные нам начальные значения в объект. Такой конструктор называется — параметризованный конструктор (parameterized constructor). Обратим внимание на особенности конструктора: Конструктор вызывается каждый раз при создании объекта Имя конструктора должно совпадать с именем класса Конструктор не может иметь возвращаемый тип (даже void) К конструктору можно применять модификаторы доступа для контроля его вызова Конструкторы в классе размещаются строго в определенном месте: Обратите внимание на способ и порядок размещения конструкторов: они располагаются сразу после полей в порядке, зависящем от принимаемых ими числа аргументов — от меньшего к большему. Конструктор может принимать в качестве аргумента как примитивные типы данных, так и объекты Класс может содержать любое количество конструкторов Компилятор не сгенерирует конструктор по умолчанию, если программист реализовал свой конструктор. Конструктором копирования (copy constructor) называется специальный конструктор, который принимает в качестве аргумента экземпляр того же класса для создания нового объекта на основе переданного. Такие конструкторы применяются тогда, когда необходимо создать копию сложного объекта, но при этом мы не хотим использовать метод clone(). Обратите внимание, ссылка на имя в конструкторе копирования копируется в значение name нового объекта. Т. е. значение нового объекта будет ссылаться на один и тот же адрес в памяти, что и оригинальный объект. Поскольку переменная String являеся immutable, она никогда не изменится. Чтобы изменить ее значение, ей нужно будет присвоить другую ссылку. При этом name других объектов будут по прежнему ссылаться на прежнее место в памяти (класс LocalDateTime также является immutable, и все сказанное выше справедливо и для него). Результат выполнения кода подтверждает, что заменив ссылку переменной name порожденного объекта, значение ссылки оригинального объекта осталось прежним. Что произойдет, если переменная объекта будет ссылаться на изменяемую (mutable) переменную? Рассмотрим механизм так называемого «поверхностного копирования» (shallow copy). Такой механизм характеризуется копированием ссылок оригинального объекта в создаваемый объект. Этот пример должен показать, что при поверхностном копировании копируются ссылки на значения из оригинального объекта. При этом, если копируемая ссылка принадлежит mutable-объекту, то при ее изменении в любом объекте (не важно, в новом или исходном) ее значение изменится во всех других объектах. Если результат работы поверхностного копирования вас не удовлетворяет, то можно воспользоваться глубоким копированием (deep copy). Этот вид копирования позволяет создавать объекты, которые будут содержать точные копии всех полей (в том числе ссылочных) оригинального объекта, но при этом будут абсолютно независимы от оригинального объекта. Значение примитивов (int, float, double и т. д.) копируются как есть Ссылки на объекты типа immutable (например String), также копируются как есть. Несмотря на то, что оригинальный и порожденный объекты ссылаются на тот же самый адрес в памяти, immutable-объекты и никогда не будут изменяться Ссылки на mutable объекты (например Date, List и т. д.) должны копироваться при помощи глубокого копирования. Иначе оригинальный и порожденный объекты будут ссылаться на один и тот же адрес в памяти и соответственно, любые изменения объекта (оригинального или порожденного) будут отображаться на всех объектах Нужно отметить, что мы не обязаны в конструктор передавать для инициализации всех полей класса какие-то значения. Часть обязанностей можно возложить на компилятор, который присвоит полям значения по умолчанию (если это не критично). Также, в процессе работы программы, можно использовать сеттеры. Для демонстрации этой возможности давайте создадим новый банковский счет, введя только имя. Для этого создадим конструктор с параметром имени, а другим параметрам дадим значения по умолчанию. Затем все эти аргументы передадим из одного конструктора в другой, используя this(): Как вы могли заметить, в теле конструктора используется ключевое слово this с круглыми скобками, которое позволяет вызывать другой конструктор, передавая в него аргументы (не любой, а только тот, который подходит по количеству и типам передаваемых аргументов). Это нормальная практика вызывать конструкторы по цепочке (в рамках текущего класса), когда часть значений используется по умолчанию, а какие-то являются новыми. Но бывает и другая ситуация, когда мы хотим перед отработкой конструктора потомка вызвать конструктор суперкласса. В таком случае нужно использовать ключевое слово super(). Создадим класс VipBankAccount, сделав его наследником BankAccount: Интересное использование конструкторов в Java — создание объектов-значений (Value Objects). Value Object — это объект, который не меняет своего внутреннего состояния после инициализации (становится immutable). Это значит, что если требуется изменить такой объект, то для этого придется создать его новый экземпляр, вместо того чтобы изменять существующий. Давайте создадим неизменяемый класс Transaction, представляющий собой транзакцию некоторого количества денежных средств с конкретного счета, с фиксацией даты и времени проведения транзакции: Обратите внимание, что теперь мы используем ключевое слово final при определении членов класса. Это означает, что каждый из них может быть инициализирован только с помощью конструктора. Их нельзя переназначить позже в каком-либо другом методе. Мы можем считать эти значения, но не можем их изменить. Если мы создадим несколько конструкторов для класса Transaction, то каждый из них должен будет инициализировать каждую финальную переменную. Невыполнение этого приведет к ошибке компиляции. Например, если раскомментировать конструктор в коде класса Transaction, то на этапе компиляции мы получим ошибку с сообщением «java: variable amount might not have been initialized». Конструктор не может быть объявлен как final, static, synchronized или abstract Конструктор может быть перегружен (overload). Перегрузка конструктора означает, что какой-либо класс может иметь множество конструкторов, но при этом их списки параметров должны отличаться между собой. Конструкторы не наследуются подобно методам суперкласса Конструкторы могут иметь модификатор доступа private. Мы можем сделать конструктор приватным (private), что не позволит создавать его экземпляры. Какой в этом смысл? Например, в паттерне проектирования Одиночка (Singleton) приватный конструктор используется для контроля над количеством создаваемых экземпляров. Всегда можно будет создать только один объект. Иногда класс может быть служебным и хранить какие-то статические поля и статические методы. Необходимости в создании экземпляров таких классов нет, поэтому и в конструкторе нет смысла, но как мы уже знаем, компилятор создаст конструктор по умолчанию. Чтобы этого не произошло, мы можем сами создать пустой конструктор и сделать его закрытым, используя модификатор доступа private. Такой конструктор называется закрытый. Конструктор по умолчанию имеет тот же самый модификатор доступа, что и класс Конструктор класса вызывает конструктор по умолчанию его суперкласса (по цепочке вплоть до Object). Компилятор Java автоматически вставляет неявно вызов super () в первую строку любого конструктора. Поэтому, обращайте внимание на это правило при наследовании родительского класса. • Первым выражением в конструкторе должен быть вызов метода this () или super() Конструктор и сеттеры можно (нужно) использовать совместно: Если используется конструктор по умолчанию, то далее предполагается, что при помощи сеттеров (setter) полям объекта присваиваются нужные нам значения, которые на момент его создания были неизвестны. В случае же с параметризованным конструктором, значения полей сразу инициализируются подходящими нам значениями. Один вызов такого конструктора заменяет собой вызов нескольких сеттеров. При этом создается объект с корректными значениями полей. Кроме того, параметризованный конструктор позволяет создать неизменяемый объект (immutable — это объект, состояние которого не может быть изменено после создания), что невозможно при использовании сеттеров. Также, возможны комбинации, когда объект создается с несколькими обязательными полями, и с полями, инициализированными значениями по умолчанию, которые в дальнейшем могут неоднократно изменяться сеттерами. Ключевые слова this и super. Ключевое слово super используется для доступа к методам базового класса, в то время как ключевое слово this используется для доступа к методам текущего класса. Также, если вы пишете super(), это относится к конструктору базового класса, и если вы пишите this(), это относится к конструктору того самого класса, где вы пишете этот код. Оператор new в Java в сочетании с конструктором выполняет несколько функций: во-первых, он выделяет память под новый объект; во-вторых, создает объект в сочетании с конструктором; в-третьих, и самое главное, он возвращает ссылку на созданный объект. Принципы ООП Принципы ООП - это основные механизмы, на которых строится реализация объектно-ориентированного подхода. Абстракция Инкапсуляция Наследование Полиморфизм Абстрагирование – это способ выделить набор значимых характеристик объекта в классе, исключая из рассмотрения незначимые. Соответственно, абстракция – это набор всех таких характеристик. Если бы для моделирования поведения автомобиля приходилось учитывать химический состав краски кузова и удельную теплоёмкость лампочки подсветки номеров, мы никогда бы не узнали, что такое NFS. Научное определение гласит, что «Инкапсуляция – это принцип, согласно которому любой класс и в более широком смысле – любая часть системы должны рассматриваться как «черный ящик»: пользователь класса или подсистемы должен видеть только интерфейс (т.е. список декларируемых свойств и методов) и не вникать во внутреннюю реализацию.» Таким образом, получается, что если класс A обращается к полям класса B напрямую, это приводит не к тому, что «нарушается информационная безопасность», а к тому, что класс A завязывается на внутренне устройство класса B, и попытка изменить внутреннее устройство класса B приведет к изменению класса А. Более того, класс A не просто так работает с полями класса B, он работает по некоторой бизнес-логике. То есть логика по работе с состоянием класса В лежит в классе А, и когда мы захотим переиспользовать класс В, это не удастся сделать, ведь без кусочка класса А класс В может быть бесполезным, что приведет к тому, что класс В придется отдавать вместе с классом А. Экстраполируя это на всю систему, получается, что переиспользовать можно будет только всю систему целиком. Инкапсуляция – это свойство системы, позволяющее объединить данные и методы, работающие с ними, в классе и скрыть детали реализации от пользователя. Инкапсуляция неразрывно связана с понятием интерфейса класса. По сути, всё то, что не входит в интерфейс, инкапсулируется в классе. Наследование – это свойство системы, позволяющее описать новый класс на основе уже существующего с частично или полностью заимствующейся функциональностью. Класс, от которого производится наследование, называется базовым или родительским. Новый класс – потомком, наследником или производным классом. Необходимо отметить, что производный класс полностью удовлетворяет спецификации родительского, однако может иметь дополнительную функциональность. С точки зрения интерфейсов, каждый производный класс полностью реализует интерфейс родительского класса. Обратное не верно. Полиморфизм – это свойство системы использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта. Например, если вы читаете данные из файла, то, очевидно, в классе, реализующем файловый поток, будет присутствовать метод похожий на следующий: byte[] readBytes( int n ); Предположим теперь, что вам необходимо считывать те же данные из сокета. В классе, реализующем сокет, также будет присутствовать метод readBytes. Достаточно заменить в вашей системе объект одного класса на объект другого класса, и результат будет достигнут. При этом логика системы может быть реализована независимо от того, будут ли данные прочитаны из файла или получены по сети. Таким образом, мы абстрагируемся от конкретной специализации получения данных и работаем на уровне интерфейса. Единственное требование при этом – чтобы каждый используемый объект имел метод readBytes. Полиморфизм - это свойство, позволяющее с помощью одного интерфейса обращаться к общему классу действий. Полиморфизм - это способность определять версию переопределенного метода в зависимости от типа объекта. Что такое композиция? Композиция - это когда между моделируемыми объектами явно прослеживается отношение "часть - целое" (has a). Например, моделируя автомобиль, естественно считать, что двигатель - это |