Статьи

Как создавать и уничтожать объекты

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

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

1. Введение

Согласно индексу сообщества программистов TIOBE, язык программирования Java, созданный в Sun Microsystems и выпущенный еще в 1995 году, является одним из наиболее широко используемых языков программирования в мире. Java — это язык программирования общего назначения. Он привлекателен для разработчиков программного обеспечения, прежде всего, из-за его мощной библиотеки и среды выполнения, простого синтаксиса, богатого набора поддерживаемых платформ (Write Once, Run Anywhere — WORA) и потрясающего сообщества.

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

Вдоль курса будет много фрагментов кода, чтобы посмотреть. Там, где это имеет смысл, тот же пример будет представлен с использованием синтаксиса Java 7 и Java 8.

2. Экземпляр Конструкция

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

2.1. Неявный (сгенерированный) конструктор

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

1
2
3
4
package com.javacodegeeks.advanced.construction;
 
public class NoConstructor {
}

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

1
final NoConstructor noConstructorInstance = new NoConstructor();

2.2. Конструкторы без аргументов

Конструктор без аргументов (или конструктор без аргументов) — это самый простой способ явного выполнения работы компилятора Java.

1
2
3
4
5
6
7
package com.javacodegeeks.advanced.construction;
 
public class NoArgConstructor {
    public NoArgConstructor() {
        // Constructor body here
    }
}

Этот конструктор будет вызываться после создания нового экземпляра класса с использованием ключевого слова new .

1
final NoArgConstructor noArgConstructor = new NoArgConstructor();

2,3. Конструкторы с аргументами

Конструкторы с аргументами являются наиболее интересным и полезным способом параметризации создания новых экземпляров классов. В следующем примере определяется конструктор с двумя аргументами.

1
2
3
4
5
6
7
package com.javacodegeeks.advanced.construction;
 
public class ConstructorWithArguments {
    public ConstructorWithArguments(final String arg1,final String arg2) {
        // Constructor body here
    }
}

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

1
2
final ConstructorWithArguments constructorWithArguments =
    new ConstructorWithArguments( "arg1", "arg2" );

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

1
2
3
public ConstructorWithArguments(final String arg1) {
    this(arg1, null);
}

2,4. Блоки инициализации

У Java есть еще один способ предоставить логику инициализации, используя блоки инициализации. Эта функция используется редко, но лучше знать, что она существует.

1
2
3
4
5
6
7
package com.javacodegeeks.advanced.construction;
 
public class InitializationBlock {
    {
        // initialization code here
    }
}

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

01
02
03
04
05
06
07
08
09
10
11
12
package com.javacodegeeks.advanced.construction;
 
public class InitializationBlocks {
    {
        // initialization code here
    }
 
    {
        // initialization code here
    }
 
}

Блоки инициализации не заменяют конструкторы и могут использоваться вместе с ними. Но очень важно отметить, что блоки инициализации всегда вызываются перед любым конструктором.

01
02
03
04
05
06
07
08
09
10
package com.javacodegeeks.advanced.construction;
 
public class InitializationBlockAndConstructor {
    {
        // initialization code here
    }
     
    public InitializationBlockAndConstructor() {
    }
}

2.5. Строительная гарантия

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

Тип Значение по умолчанию
логический Ложь
байт 0
короткая 0
ИНТ 0
долго 0L
голец \ u0000
поплавок 0.0f
двойной 0.0d
ссылка на объект ноль

Таблица 1

Давайте подтвердим это, используя следующий класс в качестве простого примера:

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
package com.javacodegeeks.advanced.construction;
 
public class InitializationWithDefaults {
    private boolean booleanMember;
    private byte byteMember;
    private short shortMember;
    private int intMember;
    private long longMember;
    private char charMember;
    private float floatMember;
    private double doubleMember;
    private Object referenceMember;
 
    public InitializationWithDefaults() {    
        System.out.println( "booleanMember = " + booleanMember );
        System.out.println( "byteMember = " + byteMember );
        System.out.println( "shortMember = " + shortMember );
        System.out.println( "intMember = " + intMember );
        System.out.println( "longMember = " + longMember );
        System.out.println( "charMember = " +
            Character.codePointAt( new char[] { charMember }, 0  ) );
        System.out.println( "floatMember = " + floatMember );
        System.out.println( "doubleMember = " + doubleMember );
        System.out.println( "referenceMember = " + referenceMember );
    }
}

После создания экземпляра с использованием new ключевого слова:

1
final InitializationWithDefaults initializationWithDefaults = new InitializationWithDefaults(),

Следующий вывод будет показан в консоли:

1
2
3
4
5
6
7
8
9
booleanMember = false
byteMember = 0
shortMember = 0
intMember = 0
longMember = 0
charMember = 0
floatMember = 0.0
doubleMember = 0.0
referenceMember = null

2.6. видимость

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

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

Таблица 2

2,7. Вывоз мусора

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

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

2,8. Финализаторы

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

Однако в Java есть одна особенность, называемая финализаторами, которая немного напоминает деструкторы, но служит для другой цели очистки ресурсов. Финализаторы считаются опасной функцией (которая приводит к многочисленным побочным эффектам и проблемам с производительностью). Как правило, они не являются необходимыми и их следует избегать (за исключением очень редких случаев, в основном связанных с нативными объектами). Гораздо лучшая альтернатива финализаторам — представленная языковой конструкцией Java 7 под названием try-with-resources и интерфейс AutoCloseable который позволяет писать чистый код, например так:

1
2
3
try ( final InputStream in = Files.newInputStream( path ) ) {
    // code here
}

3. Статическая инициализация

До сих пор мы рассматривали создание и инициализацию экземпляра класса. Но Java также поддерживает конструкции инициализации на уровне класса, называемые статическими инициализаторами . Они очень похожи на блоки инициализации, за исключением дополнительного static ключевого слова. Обратите внимание, что статическая инициализация выполняется один раз для загрузчика классов. Например:

1
2
3
4
5
6
7
package com.javacodegeeks.advanced.construction;
 
public class StaticInitializationBlock {
    static {
        // static initialization code here
    }
}

Подобно блокам инициализации, вы можете включить в определение класса любое количество статических блоков инициализатора, и они будут выполняться в том порядке, в котором они появляются в коде. Например:

01
02
03
04
05
06
07
08
09
10
11
package com.javacodegeeks.advanced.construction;
 
public class StaticInitializationBlocks {
    static {
        // static initialization code here
    }
 
    static {
        // static initialization code here
    }
}

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

4. Строительные конструкции

За прошедшие годы в сообществе Java появилось несколько понятных и широко применимых шаблонов построения (или создания). Мы рассмотрим самые известные из них: синглтон, помощники, фабрика и внедрение зависимостей (также известное как инверсия управления).

4.1. одиночка

Singleton — один из самых старых и противоречивых шаблонов в сообществе разработчиков программного обеспечения. По сути, основная идея заключается в том, чтобы в любой момент времени мог быть создан только один экземпляр класса. Однако, будучи настолько простым, singleton поднял много дискуссий о том, как сделать это правильно и, в частности, поточно-ориентированным. Вот как может выглядеть наивная версия синглтон-класса:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
package com.javacodegeeks.advanced.construction.patterns;
 
public class NaiveSingleton {
    private static NaiveSingleton instance;
     
    private NaiveSingleton() {       
    }
     
    public static NaiveSingleton getInstance() {
        if( instance == null ) {
            instance = new NaiveSingleton();
        }
         
        return instance;
    }
}

По крайней мере, одна проблема с этим кодом состоит в том, что он может создавать много экземпляров класса, если он вызывается одновременно несколькими потоками. Один из способов правильно спроектировать синглтон (но не ленивым способом) — использовать static свойство final класса.

01
02
03
04
05
06
07
08
09
10
11
12
13
final property of the class.
package com.javacodegeeks.advanced.construction.patterns;
 
public class EagerSingleton {
    private static final EagerSingleton instance = new EagerSingleton();
     
    private EagerSingleton() {       
    }
     
    public static EagerSingleton getInstance() {
        return instance;
    }
}

Если вы не хотите тратить свои ресурсы и хотели бы, чтобы ваши синглеты создавались лениво, когда они действительно необходимы, необходима явная синхронизация, которая может привести к снижению параллелизма в многопоточных средах (более подробные сведения о параллелизме в Java будут обсуждаться в часть 9 учебника « Лучшие практики параллелизма» .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
package com.javacodegeeks.advanced.construction.patterns;
 
public class LazySingleton {
    private static LazySingleton instance;
     
    private LazySingleton() {       
    }
     
    public static synchronized LazySingleton getInstance() {
        if( instance == null ) {
            instance = new LazySingleton();
        }
         
        return instance;
    }
}

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

4.2. Утилита / Хелпер Класс

Утилиты или вспомогательные классы — довольно популярная модель, используемая многими разработчиками Java. По сути, он представляет неинстанцируемый класс (с конструктором, объявленным как private ), опционально объявленным как final (более подробная информация об объявлении классов как final будет предоставлена ​​в части 3 руководства « Как разрабатывать классы и интерфейсы» ) и содержит static методы только. Например:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
package com.javacodegeeks.advanced.construction.patterns;
 
public final class HelperClass {
    private HelperClass() {       
    }
     
    public static void helperMethod1() {
        // Method body here
    }
     
    public static void helperMethod2() {
        // Method body here
    }
}

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

4,3. завод

Заводская модель оказалась чрезвычайно полезной техникой в ​​руках разработчиков программного обеспечения. Как таковой, он имеет несколько разновидностей в Java, от фабричного метода до абстрактной фабрики . Простейшим примером фабричного шаблона является static метод, который возвращает новый экземпляр определенного класса ( фабричный метод ). Например:

01
02
03
04
05
06
07
08
09
10
package com.javacodegeeks.advanced.construction.patterns;
 
public class Book {
    private Book( final String title) {
    }    
 
    public static Book newBook( final String title ) {
        return new Book( title );
    }
}

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

1
2
3
public interface BookFactory {
    Book newBook();
}

С несколькими различными реализациями, в зависимости от типа библиотеки:

01
02
03
04
05
06
07
08
09
10
11
12
13
public class Library implements BookFactory {
    @Override
    public Book newBook() {
        return new PaperBook();
    }
}
 
public class KindleLibrary implements BookFactory {
    @Override
    public Book newBook() {
        return new KindleBook();
    }
}

Теперь конкретный класс Book скрыт за BookFactory интерфейса BookFactory , все еще обеспечивая общий способ создания книг.

4.4. Внедрение зависимости

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

01
02
03
04
05
06
07
08
09
10
11
12
package com.javacodegeeks.advanced.construction.patterns;
 
import java.text.DateFormat;
import java.util.Date;
 
public class Dependant {
    private final DateFormat format = DateFormat.getDateInstance();
     
    public String format( final Date date ) {
        return format.format( date );
    }
}

Класс Dependant нуждается в экземпляре DateFormat, и он просто создает его, вызывая DateFormat.getDateInstance() во время создания. Лучше было бы использовать аргумент конструктора, чтобы сделать то же самое:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
package com.javacodegeeks.advanced.construction.patterns;
 
import java.text.DateFormat;
import java.util.Date;
 
public class Dependant {
    private final DateFormat format;
     
    public Dependant( final DateFormat format ) {
        this.format = format;
    }
     
    public String format( final Date date ) {
        return format.format( date );
    }
}

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

5. Загрузите исходный код

6. Что дальше

В этой части руководства мы рассмотрели классы и методы создания и инициализации экземпляров классов, а также рассмотрели несколько широко используемых шаблонов. В следующей части мы рассмотрим класс Object и использование его хорошо известных методов: equals , hashCode , toString и clone .