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

Помимо использования стандартных аннотаций из пакета java.lang, о которых мы поговорим далее, можно также создавать свои аннотации и обрабатывать их.

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

Код примеров вы можете найти на GitHub.

Основы аннотаций

Аннотации начинаются с символа @. Например, в пакете java.lang определены аннотации @Override и @SuppressWarnings.

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

В качестве примера рассмотрим аннотацию @Override:

public class ParentClass {
  public String getName() {...}
}

public class ChildClass extends ParentClass {
  @Override
  public String getname() {...}
}

Аннотация @Override используется для обозначения переопределенного метода из базового класса. Приведенная выше программа при компиляции выдаст ошибку, потому что метод getname() в классе ChildClass аннотирован @Override, но в родительском классе ParentClass метода getname() нет.

Используя аннотацию @Override в ChildClass, компилятор проверяет, что имя переопределенного метода в дочернем классе совпадает с именем метода в родительском классе.

Стандартные аннотации

Рассмотрим некоторые из распространенных стандартных аннотаций из пакета java.lang. Чтобы увидеть их влияние на поведение компилятора, запускайте примеры из командной строки, поскольку большинство IDE могут подавлять предупреждения.

@SuppressWarnings

Аннотация @SuppressWarnings используется для подавления предупреждений компилятора. Например, @SuppressWarnings("unchecked") отключает  предупреждения, связанные с "сырыми" типами (Raw Types). 

Давайте рассмотрим пример использования @SuppressWarnings:

public class SuppressWarningsDemo {

  public static void main(String[] args) {
    SuppressWarningsDemo swDemo = new SuppressWarningsDemo();
    swDemo.testSuppressWarning();
  }

  public void testSuppressWarning() {
    Map testMap = new HashMap();
    testMap.put(1, "Item_1");
    testMap.put(2, "Item_2");
    testMap.put(3, "Item_3");
  }
}

Если мы запустим компиляцию из командной строки с параметром -Xlint:unchecked, то получим следующее сообщение:

javac -Xlint:unchecked ./com/reflectoring/SuppressWarningsDemo.java
Warning:
unchecked call to put(K,V) as a member of the raw type Map

Это пример легаси кода (до Java 5) — в коллекции мы можем случайно сохранить объекты разных типов. Для проверки подобных ошибок на этапе компиляции, были придуманы обобщенные типы (generics, дженерики). Чтобы этот код компилировался без предупреждений измените строку:

Map testMap = new HashMap();

на

Map<Integer, String> testMap = new HashMap<>();

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

@SuppressWarnings({"rawtypes", "unchecked"})
public class SuppressWarningsDemo {
  ...
}

Теперь при компиляции предупреждений не будет.

@Deprecated

Аннотация @Deprecated используется для пометки устаревших методов или типов.

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

В примере ниже метод testLegacyFunction() помечен как устаревший:

public class DeprecatedDemo {

  @Deprecated(since = "4.5", forRemoval = true)
  public void testLegacyFunction() {

    System.out.println("This is a legacy function");
  }
}

В атрибуте since этой аннотации содержится версия, с которой элемент объявлен устаревшим, а forRemoval указывает, будет ли элемент удален в следующей версии.

Теперь вызов устаревшего метода, вызовет предупреждение во время компиляции, указывая, что лучше этот метод не использовать:

./com/reflectoring/DeprecatedDemoTest.java:8: warning: [removal] testLegacyFunction() in DeprecatedDemo has been deprecated and marked for removal
    demo.testLegacyFunction();
      ^           
1 warning

@Override

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

public class Employee {
  public void getEmployeeStatus(){
    System.out.println("This is the Base Employee class");
  }
}

public class Manager extends Employee {
  public void getemployeeStatus(){
    System.out.println("This is the Manager class");
  }
}

Здесь мы хотели переопределить метод getEmployeeStatus(), но неправильно написали имя метода. Это может привести к серьезным ошибкам. Приведенная выше программа скомпилируется и запуститься без проблем, не обнаружив эту ошибку при компиляции.

Если добавить аннотацию @Override к методу getemployeeStatus(), то при компиляции получим следующую ошибку:

./com/reflectoring/Manager.java:5: error: method does not override or implement a method from a supertype
  @Override
  ^
1 error

@FunctionalInterface

Аннотация @FunctionalInterface используется для указания того, что в интерфейсе не может быть более одного абстрактного метода. Если абстрактных методов будет больше одного, то компилятор выдаст ошибку. Функциональные интерфейсы появились в Java 8 для реализации лямбда-выражений и гарантии того, что в них не более одного абстрактного метода.

Но и без аннотации @FunctionalInterface компилятор выдаст ошибку, если вы включите в интерфейс больше одного абстрактного метода. Так зачем же нужна необязательная аннотация @FunctionalInterface?

Давайте рассмотрим следующий пример:

@FunctionalInterface
interface Print {
  void printString(String testString);
}

Если в интерфейс Print мы добавим еще один метод printString2(), то компилятор или IDE выдаст ошибку.

А что, если интерфейс Print находится в отдельном модуле и без аннотации @FunctionalInterface? Разработчики этого модуля могут легко добавить в интерфейс еще один метод и сломать ваш код. Добавив аннотацию @FunctionalInterface, мы сразу получим предупреждение в IDE:

Multiple non-overriding abstract methods found in interface com.reflectoring.Print

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

@SafeVarargs

Функциональность varargs позволяет создавать методы с переменным количеством аргументов. До Java 5 единственной возможностью создания методов с необязательными параметрами было создание нескольких методов, каждый из которых с разным количеством параметров. Varargs позволяет создать один метод с переменным количеством параметров с помощью следующего синтаксиса:

// можно написать так:
void printStrings(String... stringList)

// вместо этого мы делаем:
void printStrings(String string1, String string2)

Однако при использовании в аргументах метода обобщенных типов выдаются предупреждения. Аннотация @SafeVarargs позволяет подавить их:

package com.reflectoring;

import java.util.Arrays;
import java.util.List;

public class SafeVarargsTest {

   private void printString(String test1, String test2) {
    System.out.println(test1);
    System.out.println(test2);
  }

  private void printStringVarargs(String... tests) {
    for (String test : tests) {
      System.out.println(test);
    }
  }

  private void printStringSafeVarargs(List<String>... testStringLists) {
    for (List<String> testStringList : testStringLists) {
      for (String testString : testStringList) {
        System.out.println(testString);
      }
    }
  }

  public static void main(String[] args) {
    SafeVarargsTest test = new SafeVarargsTest();

    test.printString("String1", "String2");
    test.printString("*******");

    test.printStringVarargs("String1", "String2");
    test.printString("*******");

    List<String> testStringList1 = Arrays.asList("One", "Two");
    List<String> testStringList2 = Arrays.asList("Three", "Four");

    test.printStringSafeVarargs(testStringList1, testStringList2);
  }
}

Методы printString() и printStringVarargs() приводят к одинаковому результату. Но при компиляции для метода printStringSafeVarargs() выдается предупреждение, поскольку в нем используются обобщенные типы:

javac -Xlint:unchecked ./com/reflectoring/SafeVarargsTest.java

./com/reflectoring/SafeVarargsTest.java:28: warning: [unchecked] Possible heap pollution from parameterized vararg type List<String>
  private void printStringSafeVarargs(List<String>... testStringLists) {
                            ^
./com/reflectoring/SafeVarargsTest.java:52: warning: [unchecked] unchecked generic array creation for varargs parameter of type List<String>[]
    test.printStringSafeVarargs(testStringList1, testStringList2);
                   ^
2 warnings

Добавив аннотацию @SafeVarargs, мы можем избавиться от этого предупреждения:

@SafeVarargs
private void printStringSafeVarargs(List<String>... testStringLists) {

Пользовательские аннотации

Мы можем создавать свои аннотации, например, для реализации следующей функциональности:

  1. Уменьшение дублирования кода.

  2. Автоматизация генерации бойлерплейт кода.

  3. Отлов ошибок во время компиляции, например, потенциальные Null Pointer Exception.

  4. Настройка поведения в рантайме на основе наличия аннотации.

Для примера рассмотрим аннотацию @Company:

@Company{  
  name="ABC"
  city="XYZ"
}
public class CustomAnnotatedEmployee { 
  ... 
}

При создании экземпляров класса CustomAnnotatedEmployee все экземпляры будут содержать одно и то же название компании (name) и города (city) — больше не нужно добавлять эту информацию в конструктор.

Создать пользовательскую аннотацию можно с помощью ключевого слова @interface:

public @interface Company{
}

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

Например, чтобы указать, что аннотация применяется только к классам, используется аннотация @Target(ElementType.TYPE). А мета-аннотация @Retention(RetentionPolicy.RUNTIME) указывает, что аннотация должна быть доступна в рантайме.

С мета-аннотациями наша аннотация @Company выглядит следующим образом:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Company{
}

Далее добавим атрибуты в нашу аннотацию: имя (name) и город (city). Добавляем их, как показано ниже:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Company{
	String name() default "ABC";
	String city() default "XYZ";
}

Создадим класс CustomAnnotatedEmployee и применим к нему аннотацию @Company:

@Company
public class CustomAnnotatedEmployee {

  private int id;
  private String name;

  public CustomAnnotatedEmployee(int id, String name) {
    this.id = id;
    this.name = name;
  }

  public void getEmployeeDetails(){
    System.out.println("Employee Id: " + id);
    System.out.println("Employee Name: " + name);
  }
}

Прочитать аннотацию @Company в рантайме можно следующим образом:

import java.lang.annotation.Annotation;

public class TestCustomAnnotatedEmployee {

  public static void main(String[] args) {

    CustomAnnotatedEmployee employee = new CustomAnnotatedEmployee(1, "John Doe");
    employee.getEmployeeDetails();

    Annotation companyAnnotation = employee
            .getClass()
            .getAnnotation(Company.class);
    Company company = (Company)companyAnnotation;

    System.out.println("Company Name: " + company.name());
    System.out.println("Company City: " + company.city());
  }
}

Результат будет следующий:

Employee Id: 1
Employee Name: John Doe
Company Name: ABC
Company City: XYZ

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

Мета-аннотации

Мета-аннотации — это аннотации, применяемые к другим аннотациям для предоставления информации об аннотации компилятору или среде выполнения.

Мета-аннотации могут ответить на следующие вопросы об аннотации:

  1. Может ли аннотация наследоваться дочерними классами?

  2. Должна ли аннотация отображаться в документации?

  3. Можно ли применить аннотацию несколько раз к одному и тому же элементу?

  4. К какому типу элементов можно применить аннотацию: к классу, методу, полю и т.д.?

  5. Обрабатывается ли аннотация во время компиляции или в рантайме?

@Inherited

По умолчанию аннотация не наследуется от родительского класса к дочернему. Мета-аннотация @Inherited позволяет ей наследоваться:

@Inherited
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Company{
  String name() default "ABC";
  String city() default "XYZ";
}

@Company
public class CustomAnnotatedEmployee {

  private int id;
  private String name;

  public CustomAnnotatedEmployee(int id, String name) {
    this.id = id;
    this.name = name;
  }

  public void getEmployeeDetails(){
    System.out.println("Employee Id: " + id);
    System.out.println("Employee Name: " + name);
  }
}

public class CustomAnnotatedManager extends CustomAnnotatedEmployee{
  public CustomAnnotatedManager(int id, String name) {
    super(id, name);
  }
}

Поскольку CustomAnnotatedEmployee аннотирован @Company, а CustomAnnotatedManager наследуется от него, то нет необходимости ставить аннотацию на класс CustomAnnotatedManager.

Давайте проверим это.

public class TestCustomAnnotatedManager {

  public static void main(String[] args) {
    CustomAnnotatedManager manager = new CustomAnnotatedManager(1, "John Doe");
    manager.getEmployeeDetails();

    Annotation companyAnnotation = manager
            .getClass()
            .getAnnotation(Company.class);
    Company company = (Company)companyAnnotation;

    System.out.println("Company Name: " + company.name());
    System.out.println("Company City: " + company.city());
  }
}

Аннотация @Company доступна, хотя мы не указывали ее явно для класса Manager.

@Documented

@Documented указывает, что аннотация должна присутствовать в JavaDoc.

По умолчанию информация об аннотациях не отображается в JavaDoc-документации, но если использовать @Documented, она появится:

@Inherited
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Company{
  String name() default "ABC";
  String city() default "XYZ";
}

@Repeatable

@Repeatable позволяет использовать аннотацию несколько раз на одном методе, классе или поле. Для использования @Repeatable — аннотации необходимо создать аннотацию-контейнер, которая хранит значение в виде массива исходных аннотаций:}

@Target(ElementType.TYPE)
@Repeatable(RepeatableCompanies.class)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatableCompany {
  String name() default "Name_1";
  String city() default "City_1";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatableCompanies {
  RepeatableCompany[] value() default{};
}

Использовать аннотацию можно следующим образом:

@RepeatableCompany
@RepeatableCompany(name =  "Name_2", city = "City_2")
public class RepeatedAnnotatedEmployee {
}

Протестируем:

public class TestRepeatedAnnotation {

  public static void main(String[] args) {

    RepeatableCompany[] repeatableCompanies = RepeatedAnnotatedEmployee.class
            .getAnnotationsByType(RepeatableCompany.class);
    for (RepeatableCompany repeatableCompany : repeatableCompanies) {
      System.out.println("Name: " + repeatableCompany.name());
      System.out.println("City: " + repeatableCompany.city());
    }
  }
}

Получим следующий результат, отображающий значение нескольких аннотаций @RepeatableCompany:

Name: Name_1
City: City_1
Name: Name_2
City: City_2

@Target

@Target определяет типы элементов, к которым может применяться аннотация. Например, в приведенном выше примере аннотация @Company была определена как TYPE, и поэтому может быть применена только к классам.

Давайте попробуем применить аннотацию @Company к методу:

@Company
public class Employee {

  @Company
  public void getEmployeeStatus(){
    System.out.println("This is the Base Employee class");
  }
}

В этом случае мы получим ошибку компилятора: @Company not applicable to method.

Существуют следующие типы целей, названия которых говорят сами за себя:

  • ElementType.ANNOTATION_TYPE

  • ElementType.CONSTRUCTOR

  • ElementType.FIELD

  • ElementType.LOCAL_VARIABLE

  • ElementType.METHOD

  • ElementType.PACKAGE

  • ElementType.PARAMETER

  • ElementType.TYPE

@Retention

@Retention указывает, когда аннотация будет доступна:

  • SOURCE — аннотация доступна в исходном коде и удаляется после компиляции.

  • CLASS — аннотация сохраняется в class-файле во время компиляции, но недоступна при выполнении программы.

  • RUNTIME — аннотация доступна в рантайме.

Если аннотация нужна только для проверки ошибок во время компиляции, как это делает @Override, мы используем SOURCE. Если аннотация нужна для обеспечения функциональности в рантайме, например, @Test в JUnit, то используем RUNTIME. Давайте поэкспериментируем с разными значениями RetentionPolicy:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface ClassRetention {
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface SourceRetention {
}

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface RuntimeRetention {
}

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

@SourceRetention
@RuntimeRetention
@ClassRetention
public class EmployeeRetentionAnnotation {
}

Для проверки доступности аннотаций запустите следующий код:

public class RetentionTest {

  public static void main(String[] args) {

    SourceRetention[] sourceRetention = new EmployeeRetentionAnnotation()
            .getClass()
            .getAnnotationsByType(SourceRetention.class);
    System.out.println("Source Retentions at runtime: " + sourceRetention.length);

    RuntimeRetention[] runtimeRetention = new EmployeeRetentionAnnotation()
            .getClass()
            .getAnnotationsByType(RuntimeRetention.class);
    System.out.println("Runtime Retentions at runtime: " + runtimeRetention.length);

    ClassRetention[] classRetention = new EmployeeRetentionAnnotation()
            .getClass()
            .getAnnotationsByType(ClassRetention.class);
    System.out.println("Class Retentions at runtime: " + classRetention.length);
  }
}

Результат будет следующим:

Source Retentions at runtime: 0
Runtime Retentions at runtime: 1
Class Retentions at runtime: 0

Итак, мы убедились, что в рантайме доступна только RUNTIME-аннотация.

Классификация аннотаций

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

Маркерные аннотации

Маркерные аннотации не содержат никаких членов или данных. Для определения наличия аннотации можно использовать метод isAnnotationPresent().

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

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface CSV {
}

Класс Client может использовать аннотацию следующим образом:

@CSV
public class XYZClient {
    ...
}

Обработать аннотацию можно следующим образом:

public class TestMarkerAnnotation {

  public static void main(String[] args) {

  XYZClient client = new XYZClient();
  Class clientClass = client.getClass();

    if (clientClass.isAnnotationPresent(CSV.class)){
        System.out.println("Write client data to CSV.");
    } else {
        System.out.println("Write client data to Excel file.");
    }
  }
}

На основании присутствия аннотации @CSV, мы можем решить, куда записать информацию — в CSV или в файл Excel. Приведенная выше программа выдаст следующий результат:

Write client data to CSV.

Аннотации с одним значением

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

Давайте создадим аннотацию SingleValueAnnotationCompany с одним атрибутом value:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface SingleValueAnnotationCompany {
  String value() default "ABC";
}

Создайте класс, использующий аннотацию:

@SingleValueAnnotationCompany("XYZ")
public class SingleValueAnnotatedEmployee {

  private int id;
  private String name;

  public SingleValueAnnotatedEmployee(int id, String name) {
    this.id = id;
    this.name = name;
  }

  public void getEmployeeDetails(){
    System.out.println("Employee Id: " + id);
    System.out.println("Employee Name: " + name);
  }
}

Запустите следующий пример:

public class TestSingleValueAnnotatedEmployee {

  public static void main(String[] args) {
    SingleValueAnnotatedEmployee employee = new SingleValueAnnotatedEmployee(1, "John Doe");
    employee.getEmployeeDetails();

    Annotation companyAnnotation = employee
            .getClass()
            .getAnnotation(SingleValueAnnotationCompany.class);
    SingleValueAnnotationCompany company = (SingleValueAnnotationCompany)companyAnnotation;

    System.out.println("Company Name: " + company.value());
  }
}

Переданное значение "XYZ" переопределяет значение атрибута аннотации по умолчанию. Результат выглядит следующим образом:

Employee Id: 1
Employee Name: John Doe
Company Name: XYZ

Полные аннотации

Они состоят из нескольких пар "имя-значение". Например, Company(name = "ABC", city = "XYZ"). Рассмотрим наш исходный пример Company:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Company{
  String name() default "ABC";
  String city() default "XYZ";
}

Давайте создадим класс MultiValueAnnotatedEmployee со значением параметров, как показано ниже. Значения по умолчанию будут перезаписаны.

@Company(name = "AAA", city = "ZZZ")
public class MultiValueAnnotatedEmployee {
  
}

Запустите следующий пример:

public class TestMultiValueAnnotatedEmployee {

  public static void main(String[] args) {

    MultiValueAnnotatedEmployee employee = new MultiValueAnnotatedEmployee();

    Annotation companyAnnotation = employee.getClass().getAnnotation(Company.class);
    Company company = (Company)companyAnnotation;

    System.out.println("Company Name: " + company.name());
    System.out.println("Company City: " + company.city());
  }
}

Результат:

Company Name: AAA
Company City: ZZZ

Практический пример

В качестве практического примера обработки аннотаций напишем простой аналог аннотации @Test из JUnit. Пометив методы аннотацией @Test, мы сможем определить в рантайме, какие методы тестового класса нужно запускать как тесты.

Сначала создадим маркерную аннотацию для методов-тестов:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD) 
public @interface Test {
}

Далее создадим класс AnnotatedMethods, в котором применим аннотацию @Test к методу test1(). Это позволит выполнить метод в рантайме. У метода test2() аннотации нет и он не должен выполняться.

public class AnnotatedMethods {

  @Test
  public void test1() {
    System.out.println("This is the first test");
  }

  public void test2() {
    System.out.println("This is the second test");
  }
}

Теперь напишем код для запуска тестов из класса AnnotatedMethods:

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

public class TestAnnotatedMethods {

  public static void main(String[] args) throws Exception {

    Class<AnnotatedMethods> annotatedMethodsClass = AnnotatedMethods.class;

    for (Method method : annotatedMethodsClass.getDeclaredMethods()) {

      Annotation annotation = method.getAnnotation(Test.class);
      Test test = (Test) annotation;

      // If the annotation is not null
      if (test != null) {

        try {
          method.invoke(annotatedMethodsClass
                  .getDeclaredConstructor()
                  .newInstance());
        } catch (Throwable ex) {
          System.out.println(ex.getCause());
        }

      }
    }
  }
}

Через метод getDeclaredMethods() мы получаем методы класса AnnotatedMethods. Затем перебираем методы и проверяем, аннотирован ли метод аннотацией @Test. Наконец, выполняем вызов методов, которые были аннотированы с помощью @Test.

В результате метод test1() выполнится, поскольку он аннотирован @Test, а test2() нет, так как он без аннотации @Test.

Результат:

This is the first test

Заключение

Мы сделали обзор основных стандартных аннотаций и рассмотрели, как создавать и обрабатывать свои аннотации.

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

Примеры кода вы можете найти на GitHub.


Всех желающих приглашаем на Demo-занятие «Объектно-ориентированное и функциональное программирование». На вебинаре поговорим о стилях программирования и необходимости каждого из них. Разберём основные принципы объектно-ориентированного стиля (Инкапсуляция, Наследование, Полиморфизм), а также возможности функционального стиля, которые предоставляет язык Java. Регистрация для всех желающих по ссылке.

Комментарии (0)