Ограничения обобщений

Последнее обновление: 09.10.2025

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

Например, у нас есть следующий класс Message, который представляет некоторое сообщение, и его производные классы, которые представляют email-сообщение и sms-сообщение:

// Класс сообщения
class Message {
    private String text;

    String getText() { return text;  }

    Message(String text) { this.text = text; }
}
// класс сообщения по email
class EmailMessage extends Message {

    private String address;  // адрес электронной почты
    String getAddress(){ return address; }

    EmailMessage(String text, String address) {

        super(text);
        this.address = address;
    }
}
// класс сообщения по sms
class SmsMessage extends Message {

    private String number;  // номер телефона
    String getNumber(){ return number; }

    SmsMessage(String text, String number) {
        super(text);
        this.number = number;
    }
}

И, допустим, мы хотим определить функционал для отправки сообщений. И для этого создадим следующий класс Messenger:

class Messenger {

    private Message message;
    Message getMessage() { return this.message; }

    Messenger(Message message) { this.message = message;  }

    void send() {
        System.out.println("Отправляется сообщение: " + message.getText());
    }
}

Класс Messenger через конструктор принимает сообщение и через метод send() условно отправляет. С помощью метода getMessage() мы можем получить отправленное сообщение. И чтобы класс Messenger был предельно универсален, в качестве типа сообщений используем базовый тип Message.

Используем выше определенные классы:

class Program {

    public static void main(String[] args) {
        //  Создаем сообщения
        Message message1 = new EmailMessage("Привет, ты спишь?", "someaddress@hmail.com");
        Message message2 = new SmsMessage("Не, ты скажи, ты спишь?", "+71234567890");

        // Отправляем сообщения
        Messenger simpleMessenger = new Messenger(message1);
        simpleMessenger.send();

        simpleMessenger = new Messenger(message2);
        simpleMessenger.send();
    }
}

B все вроде прекрасно работает, и никаких проблем нет, поскольку класс Messenger принимает объект Message и соответственно также и объекты производных классов. Но посмотрим с какими проблемами мы можем столкнуться.

Преобразования типов

Однако при попытке получить из объекта Messenger отправленное сообщение мы можем столкнуться с необходимостью преобразования типов:

class Program {

    public static void main(String[] args) {

        Message message = new EmailMessage("Привет, ты спишь?", "someaddress@hmail.com");

        Messenger simpleMessenger = new Messenger(message);
        simpleMessenger.send(); // Отправляется сообщение: Привет, ты спишь?

        // получаем отправленное сообщение
        EmailMessage email = (EmailMessage) simpleMessenger.getMessage();
        System.out.println("Address: " + email.getAddress());  // Address: someaddress@hmail.com
    }
}

Здесь мы извлекаем с помощью вызова simpleMessenger.getMessage() отправленное сообщение и преобразуем в EmailMessage, чтобы иметь обращаться к методам, определенным непосредственно в классе EmailMessage.

Типобезопасность

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

class Program {

    public static void main(String[] args) {

        Message message = new EmailMessage("Привет, ты спишь?", "someaddress@hmail.com");
        Messenger simpleMessenger = new Messenger(message);

        // получаем сообщение
        SmsMessage sms = (SmsMessage) simpleMessenger.getMessage();  // Сюрпрайз - java.lang.ClassCastException!
        System.out.println("Phone number: " + sms.getNumber());
    }
}

Здесь при попытке преобразовать к типу SmsMessage мы столкнемся с ошибкой, поскольку преобразуемый объект представляет тип EmailMessage. И проблема усугубляется тем, что эту ошибку мы можем получить только в процессе выполнения. А компиляция пройдет успешно.

Использование обобщений

На первый взгляд, чтобы избежать преобразований и проблем с типобезопасностью, мы могли бы применить обобщения:

class Messenger<T> {

    private T message;
    T getMessage() { return this.message; }

    Messenger(T message) { this.message = message;  }

    void send() {
        System.out.println("Отправляется сообщение: " + message.getText());
    }
}

Теперь в класс Messenger мы можем передать сообщения любого типа, и объект Messenger будет строго использовать этот тип. Преобразования не потребуются, а ошибки можно будет отловить на этапе компиляции. Но теперь мы сталкиваемся с другой проблемой - универсальный параметр T подразумевает любой тип. Но не любой тип имеет метод getText(). Соответственно метод getText() для объекта типа T не определен и мы не можем этот метод использовать. Более того для объекта T по умолчанию достуны только методы типа Object.

Определение ограничений обобщений

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

Для установки ограничения после универсального параметра ставится слово extends, после которого указывается тип ограничения:

<T extends тип_ограничения>

Ограничения обобщений в типах

Ограничения обобщений можно применять на уровне типов:

class имя_класса<T extends тип_ограничения>

В качестве примера изменим класс мессенджера:

class Messenger<T extends Message> {

    private T message;
    T getMessage() { return this.message; }

    Messenger(T message) { this.message = message;  }

    void send() {
        System.out.println("Отправляется сообщение: " + message.getText());
    }
}

Выражение <T extends Message> в определении класса говорит, что через универсальный параметр T будут передаваться объекты класса Message и производных классов. Благодаря этому компилятор будет знать, что T будет иметь функционал класса Message, и соответственно мы сможем без проблем обратиться к методам класса Message внутри класса Messenger.

Применим класс для отправки сообщений:


class Program {

    public static void main(String[] args) {

        EmailMessage email = new EmailMessage("Привет, ты спишь?", "someaddress@hmail.com");
        Messenger<EmailMessage> mailClient = new Messenger<EmailMessage>(email);
        mailClient.send();  // Отправляется сообщение: Привет, ты спишь?

        SmsMessage sms = new SmsMessage("Hello World", "+71234567890");
        Messenger<SmsMessage> phone = new Messenger<SmsMessage>(sms);
        phone.send();   // Отправляется сообщение: Hello World
    }
}

Стоит отметить, что при определении переменной класса мы можем сократить ее определение:

var mailClient = new Messenger<EmailMessage>(email);

Либо так:

Messenger<EmailMessage> mailClient = new Messenger<>(email);

Если же мы попробуем получить объект несовместимого типа:

EmailMessage email = new EmailMessage("Привет", "someaddress@hmail.com");
var mailClient = new Messenger<EmailMessage>(email);

SmsMessage sms = mailClient.getMessage();  // Ошибка компиляции

то мы получим ошибку на этапе компиляции.

Использование нескольких универсальных параметров

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

class Messenger<T extends Message, P extends Person> {

    void sendMessage(P sender, P receiver, T message)
    {
        System.out.println("Отправитель: " + sender.getName());
        System.out.println("Получатель: " + receiver.getName());
        System.out.println("Сообщение: " + message.getText());
    }
}

class Person{

    private String name;
    String getName(){ return name;}

    Person(String name) {this.name = name; }
}

class Message{

    private String text; // текст сообщения
    String getText(){ return text; }

    Message(String text)
    {
        this.text = text;
    }
}

В данном случае для параметра P будут передаваться объекты типа Person, а для параметра T - объекты Message.

Применим классы:

public class Program{
      
    public static void main(String[] args) {
          
        Messenger<Message, Person> telegram = new Messenger<Message, Person>();
        Person tom = new Person("Tom");
        Person bob = new Person("Bob");
        Message hello = new Message("Hello, Bob!");
        telegram.sendMessage(tom, bob, hello);
    }
}

Консольный вывод:

Отправитель: Tom
Получатель: Bob
Сообщение: Hello, Bob!

Множественные ограничения

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

public class Program{
      
    public static void main(String[] args) {
          
        var icq = new Messenger<PrintedMessage>();
        var hello = new PrintedMessage("Hello METANIT.COM");
        icq.sendMessage(hello);     // Текст сообщения: Hello METANIT.COM
    }
}
class Messenger<T extends Message & Printable> {

    void sendMessage(T message){

        message.print();
    }
}
interface Printable{

    void print();
}

class Message{

    private String text; // текст сообщения
    String getText(){ return text; }

    Message(String text)
    {
        this.text = text;
    }
}

class PrintedMessage 
        extends Message 
        implements Printable{

    PrintedMessage(String text) { super(text); }

    public void print(){
        System.out.println("Текст сообщения: " + getText());
    }
}

В данном случае класс Messenger использует параметр типа T, который оновременно должен представлять кдасс Message и интерфейс Printable:

class Messenger<T extends Message & Printable> {

В качестве теста определяем подобный класс - PrintedMessage и затем передаем его объект в метод sendMessage:

var icq = new Messenger<PrintedMessage>();
var hello = new PrintedMessage("Hello METANIT.COM");
icq.sendMessage(hello);     // Текст сообщения: Hello METANIT.COM

Ограничения методов

Аналогично классам ограничения типизированных параметров можно можно применять и к методам. Например:

public class Program{
      
    public static void main(String[] args) {
          
        sendMessage(new Message("Hello World"));
        sendMessage(new EmailMessage("Bye World"));
    }
    static <T extends Message> void sendMessage(T message){

        System.out.println("Отправляется сообщение: " + message.getText());
    }
}
class EmailMessage extends Message{

    EmailMessage(String text) {  super(text); }
}
class SmsMessage extends Message{

    SmsMessage(String text)  { super(text); }
}
class Message{

    private String text; // текст сообщения
    String getText(){ return text; }

    Message(String text){

        this.text = text;
    }
}
Помощь сайту
Юмани:
410011174743222
Номер карты:
4048415020898850
Morty Proxy This is a proxified and sanitized view of the page, visit original site.