Когда мы указываем универсальный параметр у обобщений, то по умолчанию он может представлять любой тип. Однако иногда необходимо, чтобы параметр соответствовал только некоторому ограниченному набору типов. В этом случае применяются ограничения обобщений (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;
}
}