Статьи

Как проектировать классы и интерфейсы

Эта статья является частью нашего Академического курса под названием Advanced Java .

Этот курс призван помочь вам наиболее эффективно использовать Java. В нем обсуждаются сложные темы, включая создание объектов, параллелизм, сериализацию, рефлексию и многое другое. Он проведет вас через ваше путешествие в мастерство Java! Проверьте это здесь !

1. Введение

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

Точнее, мы собираемся обсудить интерфейсы и интерфейсы с методами по умолчанию (новая особенность Java 8), абстрактными и конечными классами, неизменяемыми классами, наследованием, композицией и немного пересмотреть правила видимости (или доступности), которые мы кратко затронули частично 1 из учебника, Как создавать и уничтожать объекты .

2. Интерфейсы

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

Многие языки программирования имеют интерфейсы в той или иной форме, но Java, в частности, обеспечивает языковую поддержку для этого. Давайте посмотрим на простое определение интерфейса в Java.

1
2
3
4
5
package com.javacodegeeks.advanced.design;
 
public interface SimpleInterface {
    void performAction();
}

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

Однако интерфейсы в Java могут быть более сложными, чем это: они могут включать в себя вложенные интерфейсы, классы, перечисления, аннотации (перечисления и аннотации будут подробно рассмотрены в части 5 руководства « Как и когда использовать перечисления и аннотации» ) и константы , Например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
package com.javacodegeeks.advanced.design;
 
public interface InterfaceWithDefinitions {
    String CONSTANT = "CONSTANT";
 
    enum InnerEnum {
        E1, E2;
    }
 
    class InnerClass {
    }
 
    interface InnerInterface {
        void performInnerAction();
    }
 
    void performAction();
}

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

1
2
public void performAction();
void performAction();

Стоит отметить, что каждый отдельный метод в интерфейсе неявно объявляется как абстрактный, и даже эти объявления методов эквивалентны:

1
2
3
public abstract void performAction();
public void performAction();
void performAction();

Что касается объявлений константных полей, то, кроме того, что они являются public , они неявно являются static и final поэтому следующие объявления также эквивалентны:

1
2
String CONSTANT = "CONSTANT";
public static final String CONSTANT = "CONSTANT";

И, наконец, вложенные классы, интерфейсы или перечисления, в дополнение к public , неявно объявляются как static . Например, эти объявления классов также эквивалентны:

1
2
3
4
5
class InnerClass {
}
 
static class InnerClass {
}

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

3. Маркерные интерфейсы

Маркерные интерфейсы — это особый вид интерфейсов, для которых не определены методы или другие вложенные конструкции. Мы уже видели один пример интерфейса маркера во второй части руководства. Использование методов, общих для всех объектов , интерфейса Cloneable . Вот как это определено в библиотеке Java:

1
2
public interface Cloneable {
}

Интерфейсы маркеров — это не контракты как таковые, а несколько полезный метод для «прикрепления» или «привязывания» некоторой особой черты к классу. Например, что касается Cloneable , класс помечается как доступный для клонирования, однако способ, которым он должен или мог быть сделан, не является частью интерфейса. Другой очень известный и широко используемый пример интерфейса маркера — Serializable :

1
2
public interface Serializable {
}

Этот интерфейс помечает класс как доступный для сериализации и десериализации, и, опять же, он не указывает, каким образом это может или должно быть сделано.

Интерфейсы маркеров имеют свое место в объектно-ориентированном дизайне, хотя они не удовлетворяют главной цели интерфейса — быть контрактом.

4. Функциональные интерфейсы, стандартные и статические методы

С выпуском Java 8 интерфейсы получили новые очень интересные возможности: статические методы, методы по умолчанию и автоматическое преобразование из лямбд (функциональных интерфейсов).

В разделе Интерфейсы мы подчеркнули тот факт, что интерфейсы в Java могут только объявлять методы, но им не разрешается предоставлять их реализации. С методами по умолчанию это уже не так: интерфейс может пометить метод ключевым словом по default и предоставить реализацию для него. Например:

1
2
3
4
5
6
7
8
9
package com.javacodegeeks.advanced.design;
 
public interface InterfaceWithDefaultMethods {
    void performAction();
 
    default void performDefaulAction() {
        // Implementation here
    }
}

Будучи уровнем экземпляра, методы по умолчанию могут быть переопределены каждым разработчиком интерфейса, но теперь интерфейсы могут также включать static методы, например:

1
2
3
4
5
6
7
package com.javacodegeeks.advanced.design;
 
public interface InterfaceWithDefaultMethods {
    static void createAction() {
        // Implementation here
    }
}

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

Функциональные интерфейсы — это отдельная история, и они оказались очень полезным дополнением к языку. По сути, функциональный интерфейс — это интерфейс, в котором объявлен только один абстрактный метод. Интерфейс Runnable из стандартной библиотеки Java является хорошим примером этой концепции:

1
2
3
4
@FunctionalInterface
public interface Runnable {
    void run();
}

Компилятор Java обрабатывает функциональные интерфейсы по-разному и может преобразовать лямбда-функцию в реализацию функционального интерфейса, где это имеет смысл. Давайте посмотрим на следующее определение функции:

1
2
3
public void runMe( final Runnable r ) {
    r.run();
}

Чтобы вызвать эту функцию в Java 7 и ниже, должна быть обеспечена реализация интерфейса Runnable (например, с использованием классов Anonymous ), но в Java 8 достаточно передать реализацию метода run() с использованием синтаксиса лямбда-выражений:

1
runMe( () -> System.out.println( "Run!" ) );

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

5. Абстрактные занятия

Еще одна интересная концепция, поддерживаемая языком Java, — это понятие абстрактных классов. Абстрактные классы чем-то похожи на интерфейсы в Java 7 и очень близки к интерфейсам с методами по умолчанию в Java 8. В отличие от обычных классов, абстрактные классы не могут быть созданы, но могут быть разделены на подклассы (более подробную информацию см. В разделе « Наследование» ). Что еще более важно, абстрактные классы могут содержать абстрактные методы: особый вид методов без реализаций, как это делают интерфейсы. Например:

1
2
3
4
5
6
7
8
9
package com.javacodegeeks.advanced.design;
 
public abstract class SimpleAbstractClass {
    public void performAction() {
        // Implementation here
    }
 
    public abstract void performAnotherAction();
}

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

Следует отметить одну вещь: в отличие от интерфейсов, которые могут содержать только public объявления, абстрактные классы могут использовать все возможности правил доступности для управления видимостью абстрактных методов (для получения более подробной информации обратитесь к разделам « Видимость и наследование» ).

6. Неизменные занятия

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

К сожалению, язык Java не обеспечивает сильной поддержки неизменности классов. Однако, используя комбинацию методов, можно создавать классы, которые являются неизменяемыми. Прежде всего, все поля класса должны быть final . Это хорошее начало, но оно не гарантирует неизменности.

1
2
3
4
5
6
7
8
9
package com.javacodegeeks.advanced.design;
 
import java.util.Collection;
 
public class ImmutableClass {
    private final long id;
    private final String[] arrayOfStrings;
    private final Collection< String > collectionOfString;
}

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

1
2
3
4
5
6
public ImmutableClass( final long id, final String[] arrayOfStrings,
        final Collection< String > collectionOfString) {
    this.id = id;
    this.arrayOfStrings = Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
    this.collectionOfString = new ArrayList<>( collectionOfString );
}

И наконец, предоставьте правильные методы доступа (получатели). Для коллекции неизменяемое представление должно быть предоставлено с использованием оболочек Collections.unmodifiableXxx .

1
2
3
public Collection<String> getCollectionOfString() {
    return Collections.unmodifiableCollection( collectionOfString );
}

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

1
2
3
public String[] getArrayOfStrings() {
    return Arrays.copyOf( arrayOfStrings, arrayOfStrings.length );
}

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

Есть пара отличных анализаторов исходного кода Java, таких как FindBugs ) и PMD ), которые могут очень помочь, проверяя ваш код и указывая на общие недостатки Java-программирования. Эти инструменты являются хорошими друзьями любого разработчика Java.

7. Анонимные занятия

В эпоху до Java 8 анонимные классы были единственным способом предоставить определения классов на месте и немедленные реализации. Цель анонимных классов состояла в том, чтобы уменьшить шаблон и обеспечить краткий и простой способ представления классов в качестве выражений. Давайте посмотрим на типичный старомодный способ создания нового потока в Java:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
package com.javacodegeeks.advanced.design;
 
public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread(
            // Example of creating anonymous class which implements
            // Runnable interface
            new Runnable() {
                @Override
                public void run() {
                    // Implementation here
                }
            }
        ).start();
    }
}

В этом примере реализация интерфейса Runnable предоставляется как анонимный класс. Хотя есть некоторые ограничения, связанные с анонимными классами, фундаментальными недостатками их использования являются довольно подробные синтаксические конструкции, которые Java навязывает как язык. Даже самый простой анонимный класс, который ничего не делает, требует написания как минимум 5 строк кода каждый раз.

1
2
3
4
5
new Runnable() {
   @Override
   public void run() {
   }
}

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

1
2
3
4
5
6
7
package com.javacodegeeks.advanced.design;
 
public class AnonymousClass {
    public static void main( String[] args ) {
        new Thread( () -> { /* Implementation here */ } ).start();
    }
}

8. Видимость

Мы уже немного поговорили о правилах видимости и доступности Java в первой части руководства « Как создавать классы и интерфейсы» . В этой части мы снова вернемся к этой теме, но в контексте подклассов.

Модификатор пакет Подкласс Все остальные
общественности доступной доступной доступной
защищенный доступной доступной Недоступно
<без модификатора> доступной Недоступно Недоступно
частный Недоступно Недоступно Недоступно

Таблица 1

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

В следующем разделе, Наследование , мы увидим это в действии.

9. Наследование

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

Концептуально наследование в Java реализовано с использованием подклассов и ключевого слова extends , за которым следует родительский класс. Подкласс наследует все открытые и защищенные члены своего родительского класса. Кроме того, подкласс наследует закрытые для пакета члены родительского класса, если оба находятся в одном пакете. Сказав это, очень важно, что бы вы ни пытались спроектировать, сохранить минимальный набор методов, которые класс предоставляет публично или для своих подклассов. Например, давайте взглянем на класс Parent и его подкласс Child чтобы продемонстрировать различные уровни видимости и их влияние:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.javacodegeeks.advanced.design;
 
public class Parent {
    // Everyone can see it
    public static final String CONSTANT = "Constant";
 
    // No one can access it
    private String privateField;
    // Only subclasses can access it
    protected String protectedField;
 
    // No one can see it
    private class PrivateClass {
    }
 
    // Only visible to subclasses
    protected interface ProtectedInterface {
    }
 
    // Everyone can call it
    public void publicAction() {
    }
 
    // Only subclass can call it
    protected void protectedAction() {
    }
 
    // No one can call it
    private void privateAction() {
    }
 
    // Only subclasses in the same package can call it
    void packageAction() {
    }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
package com.javacodegeeks.advanced.design;
 
// Resides in the same package as parent class
public class Child extends Parent implements Parent.ProtectedInterface {
    @Override
    protected void protectedAction() {
        // Calls parent's method implementation
        super.protectedAction();
    }
 
    @Override
    void packageAction() {
        // Do nothing, no call to parent's method implementation
    }
 
    public void childAction() {
        this.protectedField = "value";
    }
}

Наследование — очень большая тема, с множеством тонких деталей, специфичных для Java. Тем не менее, есть несколько простых в использовании правил, которые могут помочь вам сохранить краткость иерархии ваших классов. В Java каждый подкласс может переопределять любой унаследованный метод своего родителя, если только он не был объявлен как final (см. Раздел « Классы и методы Final» ).

Однако не существует специального синтаксиса или ключевого слова, чтобы пометить метод как переопределенный, что может привести к путанице. Вот почему была @Override аннотация @Override : если вы хотите переопределить унаследованный метод, всегда используйте аннотацию @Override чтобы указать это.

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

10. Множественное наследование

В отличие от C ++ и некоторых других языков, Java не поддерживает множественное наследование: в Java каждый класс имеет ровно одного прямого родителя (с классом Object находящимся на вершине иерархии, как мы уже знали во второй части учебника, Использование общих методов на все объекты ). Однако класс может реализовывать несколько интерфейсов, и, как таковые, интерфейсы стекирования являются единственным способом достижения (или имитации) множественного наследования в Java.

01
02
03
04
05
06
07
08
09
10
11
12
13
package com.javacodegeeks.advanced.design;
 
public class MultipleInterfaces implements Runnable, AutoCloseable {
    @Override
    public void run() {
        // Some implementation here
    }
 
    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}

Реализация нескольких интерфейсов на самом деле довольно мощная, но часто необходимость повторного использования реализации приводит к глубоким иерархиям классов как к способу преодоления отсутствия поддержки множественного наследования в Java.

1
2
3
4
5
6
public class A implements Runnable {
    @Override
    public void run() {
        // Some implementation here
    }
}
1
2
3
4
5
6
7
// Class B wants to inherit the implementation of run() method from class A.
public class B extends A implements AutoCloseable {
    @Override
    public void close() throws Exception {
       // Some implementation here
    }
}
1
2
3
4
5
6
7
8
// Class C wants to inherit the implementation of run() method from class A
// and the implementation of close() method from class B.
public class C extends B implements Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}

И так далее … Недавний выпуск Java 8 несколько решил проблему с введением методов по умолчанию. Из-за методов по умолчанию интерфейсы фактически начали предоставлять не только контракт, но и реализацию. Следовательно, классы, которые реализуют эти интерфейсы, автоматически наследуют и эти реализованные методы. Например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.javacodegeeks.advanced.design;
 
public interface DefaultMethods extends Runnable, AutoCloseable {
    @Override
    default void run() {
        // Some implementation here
    }
 
    @Override
    default void close() throws Exception {
       // Some implementation here
    }
}
 
// Class C inherits the implementation of run() and close() methods from the
// DefaultMethods interface.
public class C implements DefaultMethods, Readable {
    @Override
    public int read(java.nio.CharBuffer cb) throws IOException {
       // Some implementation here
    }
}

Помните, что множественное наследование является мощным, но в то же время опасным инструментом для использования. Хорошо известную проблему «Алмаз смерти» часто называют фундаментальным недостатком реализации множественного наследования, поэтому разработчикам настоятельно рекомендуется очень тщательно проектировать иерархии классов. К сожалению, интерфейсы Java 8 с методами по умолчанию также становятся жертвами этих недостатков.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
interface A {
    default void performAction() {
    }
}
 
interface B extends A {
    @Override
    default void performAction() {
    }
}
 
interface C extends A {
    @Override
    default void performAction() {
    }
}

Например, следующий фрагмент кода не компилируется:

1
2
3
// E is not compilable unless it overrides performAction() as well
interface E extends B, C {
}

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

11. Наследование и состав

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

Давайте посмотрим на этот пример:

1
2
3
4
5
public class Vehicle {
    private Engine engine;
    private Wheels[] wheels;
    // ...
}

Класс Vehicle состоит из engine и wheels (а также многих других деталей, которые для простоты оставлены в стороне). Тем не менее, можно сказать, что класс Vehicle также является двигателем и может быть разработан с использованием наследования.

1
2
3
4
public class Vehicle extends Engine {
    private Wheels[] wheels;
    // ...
}

Какое дизайнерское решение является правильным? Общие рекомендации известны как принципы IS-A и HAS-A . IS-A является отношением наследования: подкласс также удовлетворяет спецификации родительского класса и такой вариации IS-A родительского класса. Следовательно, HAS-A является композиционным отношением: класс владеет (или HAS-A ) объектами, которые ему принадлежат. В большинстве случаев принцип HAS-A работает лучше, чем IS-A, по нескольким причинам:

  • Конструкция более гибкая, чтобы ее можно было изменить
  • Модель более стабильна, поскольку изменения не распространяются по иерархии классов
  • Класс и его композиты слабо связаны по сравнению с наследованием, которое тесно связывает родителя и его подклассы
  • Рассуждения о классе проще, так как все его зависимости включены в него, в одном месте

Тем не менее, наследование имеет свое место, решает реальные проблемы проектирования по-другому и не следует пренебрегать. Пожалуйста, помните об этих двух альтернативах при проектировании ваших объектно-ориентированных моделей.

12. Инкапсуляция

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

Инкапсуляция в Java достигается с помощью правил видимости и доступности. Лучше всего в Java никогда не открывать поля напрямую, только с помощью методов получения и установки (если поле не объявлено как final ). Например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.javacodegeeks.advanced.design;
 
public class Encapsulation {
    private final String email;
    private String address;
 
    public Encapsulation( final String email ) {
        this.email = email;
    }
 
    public String getAddress() {
        return address;
    }
 
    public void setAddress(String address) {
        this.address = address;
    }
 
    public String getEmail() {
        return email;
    }
}

Этот пример напоминает то, что на языке Java называется JavaBeans : обычные классы Java, написанные в соответствии с набором соглашений, один из которых разрешает доступ к полям только с помощью методов getter и setter.

Как мы уже подчеркивали в разделе « Наследование », всегда старайтесь, чтобы публичный контракт класса был минимальным, следуя принципу инкапсуляции. Все, что не должно быть public , должно быть private (или protected / package private зависимости от решаемой вами проблемы). В долгосрочной перспективе это окупится, предоставив вам свободу в разработке вашего дизайна без внесения существенных изменений (или, по крайней мере, минимизации их).

13. Финальные занятия и методы

В Java есть способ предотвратить подкласс класса к любому другому классу: он должен быть объявлен как final .

1
2
3
4
package com.javacodegeeks.advanced.design;
 
public final class FinalClass {
}

То же ключевое слово final в объявлении метода предотвращает переопределение метода в подклассах.

1
2
3
4
5
6
package com.javacodegeeks.advanced.design;
 
public class FinalMethod {
    public final void performAction() {
    }
}

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

Стандартная библиотека Java имеет несколько примеров final классов, наиболее известным из которых является класс String . На ранней стадии было принято решение превентивно предотвращать любые попытки разработчиков придумать собственные, «лучшие» реализации строк.

14. Что дальше

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

15. Скачать исходный код

Это был урок о том, как проектировать классы и интерфейсы.