Статьи

Объектно-ориентированные концепции в Java — часть 2

Объектно-ориентированное программирование (ООП) является мощной силой в ландшафте разработки программного обеспечения сегодня. Некоторые говорят (совершенно правильно, на мой взгляд), что усилия по разработке крупных программных проектов, таких как Microsoft Office, просто невозможно было бы осуществить без модульности и повторного использования кода, которые стали возможными в современных объектно-ориентированных языках. Другие просто предпочитают ООП, потому что он платит лучше и веселее!

Какими бы ни были ваши причины для изучения принципов ООП, Java является идеальным языком для того, чтобы вымочить ноги. Он достаточно дружелюбен для начинающих, так что вам не придется перегружаться сложным синтаксисом, если вы знакомы с базовыми концепциями программирования, и все же это действительно полный, объектно-ориентированный язык, в отличие от многих других языков веб-разработки, таких как Perl, PHP и ASP, которые просто предоставляют объектно-ориентированные функции.

Эта статья является четвертой в серии статей SitePoint по разработке динамических веб-сайтов с использованием Java и второй из двух частей, которые посвящены обучению вас тому, что вам нужно знать об объектно-ориентированном программировании, чтобы в полной мере воспользоваться преимуществами языка Java. Если вы еще не читали предыдущие статьи этой серии, я бы определенно рекомендовал выполнить резервное копирование и начать с самого начала , поскольку представленные здесь концепции основаны на ваших знаниях всего, что было до этого.

В первой части мы рассмотрели основные понятия классов , объектов , свойств и методов . Мы разработали простой класс под названием Tree а также программу для создания экземпляров нескольких деревьев и проверки их свойства height и метода их роста. После краткого CoconutTree наследования , где мы разработали подкласс Tree под названием CoconutTree , мы рассмотрели проблемы, с которыми вы столкнетесь при копировании и сравнении объектов в Java. Во второй части мы продолжим с того места, где остановились, изучив некоторые более сложные типы методов и свойств. Мы узнаем, как их можно использовать для разработки лучших классов, демонстрирующих некоторые важные особенности хорошего объектно-ориентированного проектирования программного обеспечения. Кроме того, мы рассмотрим некоторые продвинутые концепции в дизайне классов и предоставим объяснение пакетов классов, которые позволят вам организовать ваши классы в группы.

Со всеми формальностями, давайте начнем!

Передача параметров и возвращаемых значений

Большинство методов, которые мы рассмотрели до сих пор, были специального типа. Вот объявление одного такого метода, метода pickNut для класса CoconutTree который мы разработали в первой части:

   public void pickNut() {    numNuts = numNuts – 1;  } 

Что делает это, и другие методы, которые мы рассмотрели до сих пор, особенными, так это то, что они не требуют никаких параметров и не возвращают значение . Как вы узнаете, посмотрев более практичные примеры классов Java позже в этой серии, большинство методов выполняют один или оба из них.

Параметры — это фрагменты информации, которые необходимо предоставить при вызове функции, чтобы полностью указать действие, которое необходимо выполнить. Например, если мы хотим сделать pickNut выше метод pickNut более мощным, мы можем дать ему параметр, указывающий количество орехов, которые нужно выбрать:

   public void pickNut(int numberToPick) {    numNuts = numNuts – numberToPick;  } 

В этой новой версии метода pickNut мы указали, что функция принимает целочисленный ( int ) параметр, значение которого хранится в переменной с именем numberToPick . Затем код метода использует его как число, которое необходимо вычесть из свойства numNuts . Таким образом, теперь мы можем выбрать столько орехов, сколько мы хотим, из CoconutTree с помощью одного вызова метода pickNut . Вот несколько примеров вызовов pickNut :

 CoconutTree ct = new CoconutTree(); // New tree   // Presumably we grow a few nuts first...   ct.pickNut(1); // Picks one nut  ct.pickNut(5); // picks five nuts  ct.pickNut(0); // Doesn't do anything   int nuts = 10;  ct.pickNut(nuts); // Picks ten nuts   ct.pickNut(-1); // Picks -1 nut (??) 

Как показывает последняя строка, в этом методе есть проблема. Поскольку оно принимает любое целое число в качестве числа выбираемых орехов, ничто не останавливает программу, которая использует это для выбора отрицательного числа орехов. Если посмотреть на код нашего метода, это на самом деле просто добавит больше орехов в дерево, но мы не должны допускать операций с нашим объектом, которые не имеют смысла в реальном мире. Другая операция, которая не имеет смысла, — это собрать больше орехов, чем доступно для сбора на дереве! С существующим кодом это привело бы к тому, что наше дерево сообщило бы отрицательное количество орехов — вряд ли это реалистичная ситуация.

Эти две проблемы показывают важную проблему при разработке методов, которые требуют параметров. Вы всегда должны убедиться, что значение, переданное методу, имеет смысл перед его использованием. Даже если вы планируете использовать класс только в своих собственных программах, удивительно легко забыть, какие значения будут и не будут вызывать проблем, если вы не проверяете значения автоматически.

Следующая модифицированная версия pickNut проверяет параметр, чтобы убедиться, что он не отрицательный, и что он не больше числа гаек в дереве:

   public void pickNut(int numberToPick) {    if (numberToPick < 0) return; // Cannot pick negative number    if (numberToPick > numNuts) return; // Not enough nuts    numNuts = numNuts – numberToPick;  } 

Команда return немедленно завершает метод. Таким образом, операция выбора орехов (вычитание из свойства numNuts ) будет происходить только в том случае, если оба условия в операторах if являются ложными. Это гарантирует, что наши два ограничения будут выполнены, прежде чем мы позволим продолжить операцию выбора.

Здесь остается одна проблема. Как может код, который вызывает метод pickNut узнать, была ли операция выбора успешной? В конце концов, если операция выбора не удалась из-за того, что одно из ограничений не было выполнено, мы не хотим, чтобы наша программа продолжала работать так, как если бы она была в состоянии отобрать гайки. Чтобы решить эту проблему, мы должны еще раз изменить pickNut ; на этот раз мы заставим его вернуть значение:

   public boolean pickNut(int numberToPick) {    if (numberToPick < 0) return false;    if (numberToPick > numNuts) return false;    numNuts = numNuts – numberToPick;    return true;  } 

Методы могут не только получать значения параметров при их вызове, но и отправлять их обратно, указав значение как часть команды возврата. В этой новой версии кода мы заменили слово void в объявлении метода словом boolean. Это указывает на то, что функция вернёт логическое (true / false) значение при завершении. В данном конкретном случае мы решили вернуть значение true, если операция подбора прошла успешно, и false, если она не удалась по какой-либо причине. Это позволяет нам структурировать код программы, которая вызывает метод, следующим образом:

 if (!ct.pickNut(10)) {  System.out.println("Error: Could not pick 10 nuts!");  System.exit();  }  nutsInHand = nutsInHand + 10; 

Условие оператора if вызывает pickNut со значением параметра 10, а затем проверяет его возвращаемое значение, чтобы увидеть, является ли оно ложным (обратите внимание на оператор ! ). Если это так, выводится сообщение об ошибке. Затем метод System.exit немедленно завершает программу, что является разумным ответом на неожиданную ошибку. В противном случае программа работает как обычно.

Другим распространенным использованием возвращаемых значений является создание методов, которые выполняют некоторые общие вычисления и возвращают результат для использования программой. В оставшейся части этой серии будет еще много примеров для изучения.

Пакеты классов

Большую часть первой части мы работали с классом Tree . Теперь, хотя Tree не является особенно оригинальным именем для класса, оно идеально подходит классу. Проблема в том, что одно и то же имя может подходить и другому классу, и у вас будет конфликт имен. Такие конфликты не слишком серьезны, когда вы пишете все свои классы; однако, когда вам нужно ввести набор классов, которые кто-то другой написал для использования в вашей программе, все может стать грязным.

Рассмотрим, например, что произойдет, если вы разработали все классы для обработки логики для программы, которая будет отслеживать продажи кнопок для одежды. В таком случае было бы естественно иметь класс с именем Button, но тогда ваш начальник скажет вам, что он хочет хороший графический интерфейс пользователя для программы. К вашему ужасу, вы обнаружите, что класс, встроенный в Java для создания кнопок на пользовательских интерфейсах, называется (как вы уже догадались) Button. Как можно разрешить этот конфликт, не возвращаясь назад к своему коду и не меняя каждую ссылку на ваш класс Button?

Пакеты классов на помощь! Java предоставляет пакеты классов (обычно называемые просто «пакетами») как способ группировки классов в соответствии с их назначением, компанией, которая их написала, или какими-либо другими критериями, которые вам нравятся. Пока вы гарантируете, что ваш класс Button не находится в том же пакете, что и встроенный в Java класс Button, вы можете использовать оба класса в своей программе без каких-либо конфликтов.

По умолчанию создаваемые вами классы находятся в пакете по умолчанию , в неназванном пакете, куда отправляются все классы, которым не назначены пакеты. Для большинства ваших программ безопасно оставлять свои классы в пакете по умолчанию. Все встроенные классы Java, а также большинство классов, которые вы найдете в Интернете и у других поставщиков программного обеспечения, сгруппированы в пакеты, так что вам обычно не нужно беспокоиться о столкновении имен ваших классов с именами других классов. классы в пакете по умолчанию.

Вы захотите сгруппировать ваши классы в пакеты, если вы собираетесь использовать их в будущих проектах (где новые имена классов могут конфликтовать с теми, которые вы хотите использовать повторно), или если вы хотите распространять их для использования другими разработчиками (где их имена классов может столкнуться с вашим собственным). Чтобы поместить ваш класс в пакет, вам просто нужно дать имя пакета в строке вверху вашего файла. Соглашение должно использовать «ком.» затем название вашей компании в качестве названия вашего пакета. Например, классы, которые мы разрабатываем на SitePoint.com, сгруппированы в пакет com.sitepoint , добавив следующую строку в начало наших файлов Java:

 package com.sitepoint; 

Имейте в виду, что при компиляции класса, который находится в пакете, файл класса будет помещен в каталог на основе имени пакета. Например, компиляция Button.java которая принадлежит пакету com.sitepoint создает файл Button.class в подкаталоге com/sitepoint/ текущего каталога. Чтобы запустить или иным образом использовать класс в таком пакете, вы должны ссылаться на него, как если бы он находился в каталоге, который содержит подкаталог com . Итак, чтобы запустить класс com.sitepoint.MyProgram , вы должны перейти в каталог, содержащий com (который содержит sitepoint , который, в свою очередь, содержит файл MyProgram.class ) и набрать:

 C:javadev> java com.sitepoint.MyProgram 

Java будет автоматически искать com/sitepoint/MyProgram.class .

Как оказалось, класс Button, встроенный в Java, фактически находится в пакете java.awt , который также содержит все остальные классы для создания базовых графических пользовательских интерфейсов в Java (AWT означает Abstract Windowing Toolkit, если вам интересно) , Таким образом, полное имя Java-класса Button — это java.awt.Button . Чтобы использовать этот класс, не думая, что ваша программа ссылается на свой собственный класс Button, вы можете использовать это полное имя. Например:

 // Create a Java Button  java.awt.Button b = new java.awt.Button(); 

Фактически, Java требует, чтобы вы использовали полное имя любого класса, который не находится в том же пакете, что и текущий класс!

Но что, если в вашей программе нет класса Button, который конфликтовал бы с классом, встроенным в Java? Изложение полного имени класса каждый раз означает много лишнего набора текста. Чтобы избавить себя от этого раздражения, вы можете импортировать класс в текущий пакет, поместив следующую строку в верхней части вашего файла .java (чуть ниже строки package , если есть):

 import java.awt.Button; 

После импорта вы можете использовать класс по его короткому имени (Button), как если бы он был частью того же пакета, что и ваш класс.

Другая удобная функция позволяет импортировать весь пакет классов в текущий пакет. Это снова пригодится при создании пользовательских интерфейсов для вашей программы, потому что для создания достойного интерфейса вам, возможно, придется легко использовать дюжину или более классов из пакета java.awt , и перечисление каждого по имени в отдельной строке import может стать утомительно, как ввод полного имени класса в вашем коде. Чтобы импортировать весь пакет java.awt для использования в классе без необходимости вводить их полные имена, вы можете добавить следующую строку в начало файла:

 import java.awt.*; 

Пакет java.lang , который содержит все самые основные классы языка Java (например, класс System , который мы использовали в форме метода System.out.println() для отображения текста на экране), автоматически импортируется в каждый файл Java автоматически.

Прежде чем вы сможете импортировать или использовать полное имя класса для доступа к нему из другого пакета, он должен быть объявлен public . По умолчанию классы доступны для использования только кодом из одного пакета. Очевидно, что это нежелательное ограничение, если вы планируете распространять свой код для использования другими или повторно использовать свои классы в нескольких проектах; поэтому любой класс, который вы считаете полезным для кодирования вне пакета класса, должен быть общедоступным. Сделать это так же просто, как добавить ключевое слово public в самом начале объявления класса. Например, имеет смысл объявить наш класс Tree общедоступным:

 package com.sitepoint;   public class Tree {  ...  } 

То же самое касается CoconutTree :

 package com.sitepoint;   public class CoconutTree extends Tree {  ...  } 

Модификаторы контроля доступа

Как мы только что видели, класс можно сделать общедоступным, чтобы позволить коду вне его пакета использовать его. Члены класса, которые включают свойства и методы, могут быть аналогичным образом изменены для контроля доступа к ним. Как и классы, члены имеют настройку доступа по умолчанию, которая ограничивает доступ к коду внутри одного пакета. Рассмотрим следующие примеры объявлений:

   int numNuts = 0;   boolean pickNut(int numberToPick) {    ...  } 

Даже если класс объявлен как открытый, значение указанного выше свойства numNuts (при условии, что оно объявлено как свойство класса Object) может быть доступно или изменено только кодом в том же пакете. Аналогично, метод pickNut показанный выше, может быть вызван только кодом в том же пакете, что и класс.

Как и с классами, свойства и методы могут быть объявлены public :

   public int numNuts = 0;   public boolean pickNut(int numberToPick) {    ...  } 

Любой код (из любого класса в любом пакете) может получить доступ и изменить значение открытого свойства или вызвать открытый метод.

Свойства и методы имеют две дополнительные настройки доступа, которых нет у классов. Первый является private :

   private int numNuts = 0;   private boolean pickNut(int numberToPick) {    ...  } 

Только код в том же классе может получить доступ к закрытым свойствам и методам. Если вы хотите хранить информацию в объекте, который полезен только для самого объекта или других объектов того же класса, или если вы хотите ограничить доступ к информации (как мы увидим в следующем разделе, это является частью хороший дизайн класса!), тогда вы должны использовать частную собственность для него. Аналогично, методы, которые выполняют внутренние вычисления или иным образом бесполезны для других классов, должны быть объявлены закрытыми.

Последний параметр доступа, который могут использовать методы и свойства, protected :

   protected int numNuts = 0;   protected boolean pickNut(int numberToPick) {    ...  } 

protected режим очень похож на режим по умолчанию в том, что он отказывает в доступе к коду вне пакета класса, но вводит одно исключение: подклассы текущего класса (то есть классы, расширяющие этот класс) также могут обращаться к защищенным членам.

Различение ситуаций, в которых каждая настройка контроля доступа является подходящей, требует небольшого опыта. Сначала заманчиво просто объявить все public чтобы избавить себя от беспокойства, когда что-то доступно, а когда нет. Хотя это, безусловно, будет работать, это определенно не в духе объектно-ориентированного программирования. Код, который вы планируете повторно использовать или распространять, особенно выиграет от того, что ему будут назначены наиболее строгие настройки управления доступом, которые уместны. Одна из причин этого иллюстрируется в следующем разделе.

Инкапсуляция с помощью аксессоров

Ранее мы модифицировали метод pickNut чтобы он не принимал слишком большое число, что заставило бы наше CoconutTree думать, что оно содержит отрицательное количество кокосовых орехов. Но есть гораздо более простой способ создать эту нереальную ситуацию:

 CoconutTree ct = new CoconutTree();  ct.numNuts = -10; 

Как мы можем защитить свойства объекта от присвоения таких значений, которые не имеют смысла? Решение состоит в том, чтобы сделать свойства частными и разрешить доступ к ним только с помощью методов. Вот обновленная версия нашего класса CoconutTree которая использует эту технику:

 1  package com.sitepoint;  2  3  public class CoconutTree extends Tree {  4    private int numNuts = 0;  5  6    public void growNut() {  7      numNuts = numNuts + 1;  8    }  9  10   public boolean pickNut(int numToPick) {  11     if (numToPick < 0) return false;  12     if (numToPick > numNuts) return false;  13     numNuts = numNuts – numToPick;  14     return true;  15   }  16  17   public int getNumNuts() {  18     return numNuts;  19   }  20  21   public boolean setNumNuts(int newNumNuts) {  22     if (newNumNuts < 0) return false;  23     numNuts = newNumNuts;  24     return true;  25   }  26 } 

Как видно из строки 4, свойство numNuts теперь является закрытым, что означает, что только коду в этом классе разрешен доступ к нему. growNut и pickNut остаются неизменными; они могут продолжать обновлять свойство numNuts напрямую (ограничения в pickNut гарантируют, что значение numNuts остается допустимым). Поскольку мы все еще хотим, чтобы код мог определять количество орехов в дереве, мы добавили открытый метод getNumNuts который просто возвращает значение свойства numNuts . Что касается установки количества орехов, мы добавили метод setNumNuts который принимает целочисленное значение в качестве параметра. Это значение проверяется, чтобы убедиться, что оно положительное или нулевое (поскольку у нас не может быть отрицательного числа гаек), а затем устанавливает для свойства numNuts это новое значение.

Эти два новых метода, getNumNuts и setNumNuts , известны как методы доступа ; то есть они являются методами, используемыми для доступа к свойству. Аксессоры очень типичны для хорошо спроектированного объекта. Даже в тех случаях, когда любое значение является приемлемым, вы должны сделать свойства своих объектов приватными и предоставить методы доступа для доступа к ним. Это позволяет вашим программам демонстрировать важную особенность объектно-ориентированного программирования, которая называется инкапсуляция.

Инкапсуляция означает, что внутреннее представление объекта отделено от интерфейса, который он представляет другим объектам в вашей программе. Другими словами, программист, который использует ваш класс, должен знать, что делают общедоступные методы, а не как они работают. Преимущество заключается в том, что вы можете изменить работу вашего класса, чтобы повысить производительность или добавить новые функции, не нарушая код, основанный на методах, предоставляемых вашим исходным классом.

Например, если вы решили, что хотите представить каждый кокос как отдельный объект класса Coconut вместо того, чтобы использовать единственную целочисленную переменную для подсчета, вы можете внести необходимые изменения и при этом иметь те же четыре метода, что и в приведенной выше реализации. Старый код, который был написан с учетом оригинального интерфейса, будет продолжать работать, как и раньше, в то время как новый код может использовать преимущества новых функций (которые, конечно, будут обеспечиваться новыми методами).

В качестве упражнения перепишите класс Tree чтобы он правильно инкапсулировал свое свойство height .

Конструкторы

Конструктор — это специальный тип метода, который вызывается автоматически при создании объекта. Конструкторы позволяют вам указывать значения для свойств и другие подробности инициализации.

Рассмотрим еще раз наш класс Tree ; в частности, объявление его свойства height (которое теперь должно быть приватным и сопровождаться методами доступа):

   private int height = 0; 

Это часть » = 0 «, которая касается нас здесь. Почему все новые деревья должны иметь нулевую высоту? Используя конструктор, мы можем позволить пользователям этого класса указать начальную высоту дерева. Вот как это выглядит:

   private int height;   public Tree(int height) {    if (height < 0) this.height = 0;    else this.height = height;  } 

На первый взгляд, это выглядит как обычный метод. Однако есть два отличия:

  • Конструкторы никогда не возвращают значение; таким образом, они не имеют возвращаемого типа ( void , int , boolean и т. д.) в своем объявлении.
  • Конструкторы имеют то же имя, что и класс, который они используют для инициализации. Поскольку мы пишем класс Tree , его конструктор также должен называться Tree . По соглашению, это единственный случай, когда имя метода должно начинаться с заглавной буквы.

Таким образом, разбирая эту строку построчно, мы объявляем открытый конструктор, который принимает один параметр и присваивает его значение целочисленной переменной height . Обратите внимание, что это не height свойства объекта, как мы вскоре увидим. Вторая строка проверяет, является ли переменная height меньше нуля. Если это так, мы устанавливаем свойство height дерева в ноль (так как мы не хотим разрешать отрицательные высоты дерева). Если нет, мы присваиваем значение параметра свойству.

Обратите внимание, что поскольку у нас есть локальная переменная с именем height , мы должны ссылаться на свойство height текущего объекта как this.height . this специальная переменная в Java, которая всегда ссылается на объект, в котором выполняется текущий код. Если это вас смущает, вы можете вместо этого назвать параметр конструктора чем-то вроде newHeight . После этого вы сможете ссылаться на свойство объекта просто как height .

Поскольку у класса Tree теперь есть конструктор с параметром, вы должны указать значение этого параметра при создании новых деревьев:

 Tree myTree = new Tree(10); // Initial height 10 

Перегруженные методы

Иногда имеет смысл иметь две разные версии одного и того же метода. Например, когда мы изменили метод CoconutTree классе CoconutTree чтобы требовать параметр, который определял количество орехов, которые нужно выбрать, мы потеряли удобство возможности выбирать один орех, просто вызывая pickNut() . Java фактически позволяет вам объявлять обе версии метода рядом и определяет, какую из них использовать, по количеству и типу параметров, передаваемых при вызове метода. Методы, которые объявлены с более чем одной версией, называются перегруженными методами .

Вот как объявить две версии метода pickNut :

   public boolean pickNut() {    if (numNuts == 0) return false;    numNuts = numNuts – 1;    return true;  }   public boolean pickNut(int numToPick) {    if (numToPick < 0) return false;    if (numToPick > numNuts) return false;    numNuts = numNuts – numToPick;    return true;  } 

Один из способов сэкономить время при наборе — заметить, что pickNut() на самом деле является особым случаем pickNut(int numToPick) ; то есть вызов pickNut() аналогичен вызову pickNut(1) , поэтому вы можете реализовать pickNut() , просто сделав эквивалентный вызов:

   public boolean pickNut() {    return pickNut(1);  }   public boolean pickNut(int numToPick) {    if (numToPick < 0) return false;    if (numToPick > numNuts) return false;    numNuts = numNuts – numToPick;    return true;  } 

Это не только сохраняет две строки кода, но если вы когда-либо измените способ pickNut метода pickNut , вам нужно будет настроить только один метод вместо двух.

Конструкторы могут быть перегружены так же, как и обычные методы. Если вы упустили удобство создания нового дерева нулевой высоты, вы можете объявить второй конструктор, который не принимает параметров:

   private int height;   public Tree() {    this(0);  }   public Tree(int height) {    if (height < 0) this.height = 0;    else this.height = height;  } 

Обратите внимание, что мы снова сохранили некоторую типизацию, реализовав более простой метод ( Tree() ), вызвав особый случай более сложного метода ( Tree(0) ). Однако в случае конструктора вы называете его специальным именем this .

Расширенное наследование: переопределение методов

Я рассмотрел наследование в первой части этой статьи, но для краткости оставил один продвинутый вопрос, который я хотел бы охватить сейчас: переопределение методов . Как вы знаете, объект класса CoconutTree наследует все функции класса Tree , на которых он основан. Таким образом, у CoconutTree есть методы grow как у Tree .

Но что, если вы хотите, чтобы CoconutTrees прорастали новые кокосы, когда они росли? Конечно, вы можете вызывать метод growNut каждый раз, когда вы вызываете CoconutTree , но было бы лучше, если бы вы могли обращаться с Tree и CoconutTree одинаково (то есть вызывать их метод CoconutTree и CoconutTree их обоих делать то, что они должны делать, когда объекты их типа растут.

Чтобы один и тот же метод делал что-то другое в подклассе, вы должны переопределить этот метод новым определением в подклассе. Проще говоря, вы можете повторно объявить метод CoconutTree классе CoconutTree чтобы он делал что-то другое! Вот новое определение для CoconutTree которое вы можете добавить в свой класс CoconutTree :

public void grow () {
высота = высота + 1;
growNut ();
}

Просто, правда? Но что, если вы добавили новую функциональность в метод grow в классе Tree ? Как вы могли убедиться, что это унаследовано классом CoconutTree ? Как и в нашем обсуждении перегруженных методов, где мы реализовали простой метод, вызвав особый случай более сложного метода, мы можем реализовать новое определение метода в подклассе, обратившись к его определению в суперклассе:

   public void grow() {    super.grow();    growNut();  } 

super.grow() вызывает версию grow определенную в суперклассе, что избавляет нас от необходимости заново изобретать колесо. Это особенно удобно, когда вы создаете класс, который расширяет класс, для которого у вас нет исходного кода (например, файл класса, предоставленный другим разработчиком). Просто вызывая версии суперкласса методов, которые вы переопределяете, вы можете убедиться, что ваши объекты не теряют никакой функциональности.

Конструкторы могут быть переопределены как обычные методы. Вот набор конструкторов для класса CoconutTree вместе с новым объявлением свойства numNuts без начального значения:

   private int numNuts;   public CoconutTree() {    super();    numNuts = 0;  }   public CoconutTree(int height) {    super(height);    numNuts = 0;  }   public CoconutTree(int height, int numNuts) {    super(height);    if (numNuts < 0) this.numNuts = 0;    else this.numNuts = numNuts;  } 

Первые два конструктора переопределяют свои эквиваленты в классе Tree , а третий — совершенно новый. Обратите внимание, что мы называем конструктор суперкласса как super() . Все три наших конструктора вызывают конструктор в суперклассе, чтобы гарантировать, что мы не теряем никакой функциональности.

Статические Члены

Если вы вернулись и изучили каждый пример кода, который мы видели до сих пор, то должно остаться только одно ключевое слово, которое озадачит вас. Удивительно, но он появляется в самой первой статье этой серии и в начале каждой Java-программы, которую мы написали до сих пор:

   public static void main(String[] args) { 

Если вы не заметили это, ключевое слово является static . Оба метода и свойства могут быть объявлены как статические. Статические члены принадлежат классу, а не объектам этого класса. Прежде чем объяснить, почему основной метод объявлен статическим, давайте рассмотрим более простой случай.

Возможно, было бы полезно узнать общее количество Trees , которые были созданы в нашей программе. Для этого мы могли бы создать статическое свойство totalTrees в классе Tree и изменить конструктор, увеличивая его значение на единицу при каждом создании Tree. Затем, используя статический метод getTotalTrees , мы можем проверить значение в любое время, вызвав Tree.getTotalTrees() .

Вот код для модифицированного класса Tree :

 public class Tree {   private static int totalTrees = 0;  private int height;   public Tree() {    this(0);  }   public Tree(int height) {    totalTrees = totalTrees + 1;    if (height < 0) this.height = 0;    else this.height = height;  }   public static int getTotalTrees() {    return totalTrees;  }   ...  } 

Статические члены полезны в двух основных ситуациях:

  • Когда вы хотите отслеживать некоторую информацию, которой поделились все члены класса (как указано выше).
  • Когда не имеет смысла иметь более одного экземпляра свойства или метода.

main функция является примером последнего случая. Поскольку не имеет смысла иметь более одного экземпляра main функции (т. Е. Программа может иметь только одну программу), она объявляется статической.

Другим примером статического члена, который мы видели, является свойство out класса System (также известное как System.out , мы неоднократно использовали его метод println ). Поскольку существует только одна система, в которой выполняется эта программа, она представлена ​​классом, содержащим статические свойства (например, out ) и методы (например, exit ), а не экземпляром класса.

Не беспокойтесь слишком сильно, если вы не можете полностью разобраться в причинах использования статических членов в программе и классах System . Пока вы можете понять, как приведенный выше пример подсчета деревьев отслеживает общее количество созданных объектов Tree , вы в хорошей форме.

Резюме

Я бы не стал винить вас за то, что вы немного ошеломлены. Если вы похожи на меня, ваше первое впечатление от объектно-ориентированного программирования (ООП) в Java — это то, что оно чрезвычайно мощное, чрезвычайно сложное и сильно отличается от всего, с чем вы работали раньше.

К счастью, вы привыкли к этому.

В продолжение этой серии статей иногда пугающие концепции, представленные в этой статье, сливаются в естественный способ работы с программными компонентами, которые моделируют реальные вещи и работают разумным образом для достижения желаемого результата. Чем больше Java вы видите и пишете сами, тем больше вы привыкнете к такому способу работы и тем меньше вам захочется вернуться к старому способу работы. В конечном итоге, объектно-ориентированное программирование становится еще интереснее

Теперь вы имеете полное представление о самом языке Java. Прежде чем вы сможете применить эти знания на практике, вам нужно изучить ряд классов. Java включает в себя пакеты классов для выполнения всевозможных задач, от создания графических пользовательских интерфейсов до создания динамических веб-сайтов. В следующей статье этой серии я познакомлю вас с классами, относящимися к созданию Java-сервлетов, самого основного компонента динамических веб-сайтов на основе Java.