- How to Return a Subclass Instance in an Abstract Class with Java Generics
- 2 thoughts on “How to Return a Subclass Instance in an Abstract Class with Java Generics”
- Теория дженериков в Java или как на практике ставить скобки
- Raw Types или сырые типы
- Типизированные методы (Generic Methods)
- Типизированные классы (Generic Types)
- Ограничения
- Наследование
- Final
How to Return a Subclass Instance in an Abstract Class with Java Generics
How to return a subclass instance in an abstract class with Java generics was a question for me when I was dealing with fluent test automation projects. In this article, I will share my solution with you. When we are writing fluent test automation scripts, we need to use this keyword to return that page, screen, step etc. class’s instance in their fluent methods. I want to show this below code snippet. For example, XPage and YPage are using commonMethodForAllPages common method.
//A common step for several page/step/screen classes public XPage commonMethodForAllPages () < //A common operation for all or some pages. System.out.println("Common Text for all pages."); return this; >
//A common step for several page/step/screen classes public YPage commonMethodForAllPages () < //A common operation for all or some pages. System.out.println("Common Text for all pages."); return this; >
As you see, we are using the same method for both two classes, it is contradictory to DRY (Don’t Repeat Yourself) principle. Ok, then we need to put this common method into BasePage class and write this common method only once for all subclasses. Right? Yes, it sounds great but there is a problem here. You see, these methods are returning specific subclass instances (XPage and YPage) by using this keyword. So, how can we solve this problem? Maybe there are several techniques that exist to solve this problem but I used JAVA Generics to solve it. In the parent class (in this scenario our parent class is BasePage class), I created the below method by using JAVA Generics to return specific page class (subclass) instances by using this keyword.
public abstract class BasePage > < //Here you have your PageBase class operations, declerations, etc. //. //A common step for several page/step/screen classes public T commonMethodForAllPages () < //A common operation for all or some pages. System.out.println("Common Text for all pages."); return (T) this; >>
Now, we can use this method in our XPage and YPage (subclasses) by extending BasePage class. No need to write this common method into our page classes. We can write only page-specific methods inside these page classes.
public class XPage extends BasePage
public class YPage extends BasePage
In this way, you will not repeat yourself and your project will be leaner. Assume that, if we have fifty-page classes which use the same fluent method which returns a specific page class instance by this keyword, we might write the same method fifty times. But in this way, we can write that common method only once and use it in all our page classes by extending BasePage class.
I hope this trick will help you in your fluent test automation projects. If you have a better solution please do not hesitate to write a comment and share it with us.
Onur Baskirt is a Software Engineering Leader with international experience in world-class companies. Now, he is a Software Engineering Lead at Emirates Airlines in Dubai.
2 thoughts on “How to Return a Subclass Instance in an Abstract Class with Java Generics”
Hi Onur, thank you for publishing your article. I do have one question. In my case I have 2 methods in YPage, 1 that clicks a return button, and 1 that clicks a next button. The return method must return XPage, and the next button must return ZPage (think of a wizard with multiple steps and pages). Both methods use a generic clickButton method in BasePage. This clickButton method is created like you are creating the commonMethodForAllPages. Following your example, the commonMethodForAllPages only returns the type YPage as this is provided with “BasePage”. Do you know if it is possible to deal with this case, more less, following your approach? Reply
Теория дженериков в Java или как на практике ставить скобки
Дженерики (обобщения) — это особые средства языка Java для реализации обобщённого программирования: особого подхода к описанию данных и алгоритмов, позволяющего работать с различными типами данных без изменения их описания. На сайте Oracle дженерикам посвящён отдельный tutorial: «Lesson: Generics».
Во-первых, чтобы понять дженерики, нужно разобраться, зачем они вообще нужны и что они дают. В tutorial в разделе «Why Use Generics?» сказано, что одно из назначений — более сильная проверка типов во время компиляции и устранение необходимости явного приведения.
import java.util.*; public class HelloWorld < public static void main(String []args)< List list = new ArrayList(); list.add("Hello"); String text = list.get(0) + ", world!"; System.out.print(text); >>
Этот код выполнится хорошо. Но что если к нам пришли и сказали, что фраза «Hello, world!» избита и можно вернуть только Hello? Удалим из кода конкатенацию со строкой «, world!» . Казалось бы, что может быть безобиднее? Но на деле мы получим ошибку ПРИ КОМПИЛЯЦИИ: error: incompatible types: Object cannot be converted to String Всё дело в том, что в нашем случае List хранит список объектов типа Object. Так как String — наследник для Object (ибо все классы неявно наследуются в Java от Object), то требует явного приведения, чего мы не сделали. А при конкатенации для объекта будет вызван статический метод String.valueOf(obj), который в итоге вызовет метод toString для Object. То есть List у нас содержит Object. Выходит, там где нам нужен конкретный тип, а не Object, нам придётся самим делать приведение типов:
import java.util.*; public class HelloWorld < public static void main(String []args)< List list = new ArrayList(); list.add("Hello!"); list.add(123); for (Object str : list) < System.out.println((String)str); >> >
Однако, в данном случае, т.к. List принимает список объектов, он хранит не только String, но и Integer. Но самое плохое, в этом случае компилятор не увидит ничего плохого. И тут мы получим ошибку уже ВО ВРЕМЯ ВЫПОЛНЕНИЯ (ещё говорят, что ошибка получена «в Runtime»). Ошибка будет: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String Согласитесь, не самое приятное. И всё это потому, что компилятор — не искусcтвенный интеллект и он не может угадать всё, что подразумевает программист. Чтобы рассказать компилятору подробнее о своих намерениях, какие типы мы собираемся использовать, в Java SE 5 ввели дженерики. Исправим наш вариант, подсказав компилятору, что же мы хотим:
import java.util.*; public class HelloWorld < public static void main(String []args)< Listlist = new ArrayList<>(); list.add("Hello!"); list.add(123); for (Object str : list) < System.out.println(str); >> >
Как мы видим, нам больше не нужно приведение к String. Кроме того, у нас появились угловые скобки (angle brackets), которые обрамляют дженерики. Теперь компилятор не даст скомпилировать класс, пока мы не удалим добавление 123 в список, т.к. это Integer. Он нам так и скажет. Многие называют дженерики «синтаксическим сахаром». И они правы, так как дженерики действительно при компиляции станут теми самыми кастами. Посмотрим на байткод скомпилированных классов: с кастом вручную и с использованием дженериков:
После компиляции какая-либо информация о дженериках стирается. Это называется «Стирание типов» или «Type Erasure». Стирание типов и дженерики сделаны так, чтобы обеспечить обратную совместимость со старыми версиями JDK, но при этом дать возможность помогать компилятору с определением типа в новых версиях Java.
Raw Types или сырые типы
Говоря о дженериках мы всегда имеем две категории: типизированные типы (Generic Types) и «сырые» типы (Raw Types). Сырые типы — это типы без указания «уточненения» в фигурных скобках (angle brackets):
Как мы видим, мы использовали необычную конструкцию, отмеченную стрелкой на скриншоте. Это особый синтаксис, который добавили в Java SE 7, и называется он «the diamond», что в переводе означает алмаз. Почему? Можно провести аналогию формы алмаза и формы фигурных скобок: <> Также Diamond синтаксис связан с понятием «Type Inference», или же выведение типов. Ведь компилятор, видя справа <> смотрит на левую часть, где расположено объявление типа переменной, в которую присваивается значение. И по этой части понимает, каким типом типизируется значение справа. На самом деле, если в левой части указан дженерик, а справа не указан, компилятор сможет вывести тип:
import java.util.*; public class HelloWorld < public static void main(String []args) < Listlist = new ArrayList(); list.add("Hello World"); String data = list.get(0); System.out.println(data); > >
Однако это будет смешиванием нового стиля с дженериками и старого стиля без них. И это крайне нежелательно. При компиляции кода выше мы получим сообщение: Note: HelloWorld.java uses unchecked or unsafe operations . На самом деле кажется непонятным, зачем вообще нужен тут diamond добавлять. Но вот пример:
import java.util.*; public class HelloWorld < public static void main(String []args) < Listlist = Arrays.asList("Hello", "World"); List data = new ArrayList(list); Integer intNumber = data.get(0); System.out.println(data); > >
Как мы помним, у ArrayList есть и второй конструктор, который принимает на вход коллекцию. И вот тут-то и кроется коварство. Без diamond синтаксиса компилятор не понимает, что его обманывают, а вот с diamond — понимает. Поэтому, правило #1: всегда использовать diamond синтаксис, если мы используем типизированные типы. В противном случае мы рискуем пропустить, где у нас используется raw type. Чтобы избежать предупреждений в логе о том, что «uses unchecked or unsafe operations» можно над используемым методом или классом указать особую аннотацию: @SuppressWarnings(«unchecked») Suppress переводится как подавлять, то есть дословно — подавить предупреждения. Но подумайте, почему вы решили её указать? Вспомните о правиле номер один и, возможно, вам нужно добавить типизацию.
Типизированные методы (Generic Methods)
- включает список типизированных параметров внутри угловых скобок;
- список типизированных параметров идёт до возвращаемого метода.
import java.util.*; public class HelloWorld < public static class Util < public static T getValue(Object obj, Class clazz) < return (T) obj; >public static T getValue(Object obj) < return (T) obj; >> public static void main(String []args) < List list = Arrays.asList("Author", "Book"); for (Object element : list) < String data = Util.getValue(element, String.class); System.out.println(data); System.out.println(Util.getValue(element)); > > >
Если посмотреть на класс Util, видим в нём два типизированных метода. Благодаря возможности выведения типов мы можем предоставить определение типа непосредственно компилятору, а можем сами это указать. Оба варианта представлены в примере. Кстати, синтаксис весьма логичен, если подумать. При типизировании метода мы указываем дженерик ДО метода, потому что если мы будем использовать дженерик после метода, Java не сможет понять, какой тип использовать. Поэтому сначала объявляем, что будем использовать дженерик T, а потом уже говорим, что этот дженерик мы собираемся возвращать. Естественно, Util.
import java.util.*; public class HelloWorld < public static class Util < public static T getValue(Object obj) < return (T) obj; >> public static void main(String []args) < List list = Arrays.asList(2, 3); for (Object element : list) < System.out.println(Util.getValue(element) + 1); > > >
Он будет прекрасно работать. Но только до тех пор, пока компилятор будет понимать, что у вызываемого метода тип Integer. Заменим вывод на консоль на следующую строку: System.out.println(Util.getValue(element) + 1); И мы получим ошибку: bad operand types for binary operator ‘+’, first type: Object , second type: int То есть произошло стирание типов. Компилятор видит, что тип никто не указал, тип указывается как Object и выполнение кода падает с ошибкой.
Типизированные классы (Generic Types)
Типизировать можно не только методы, но и сами классы. У Oracle в их гайде этому посвящён раздел «Generic Types». Рассмотрим пример:
public static class SomeType < public void test(Collection collection) < for (E element : collection) < System.out.println(element); >> public void test(List collection) < for (Integer element : collection) < System.out.println(element); >> >
Тут всё просто. Если мы используем класс, дженерик указывается после имени класса. Давайте теперь в методе main создадим экземпляр этого класса:
public static void main(String []args) < SomeTypest = new SomeType<>(); List list = Arrays.asList("test"); st.test(list); >
Он отработает хорошо. Компилятор видит, что есть List из чисел и Collection типа String. Но что если мы сотрём дженерики и сделаем так:
SomeType st = new SomeType(); List list = Arrays.asList("test"); st.test(list);
Мы получим ошибку: java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer Опять стирание типов. Поскольку у класса больше нет дженерика, компилятор решает: раз мы передали List, метод с List
Ограничения
К типам, указываемым в дженериках мы можем применить ограничение. Например, мы хотим, чтобы контейнер принимал на вход только Number. Данная возможность описана в Oracle Tutorial в разделе Bounded Type Parameters. Посмотрим на пример:
import java.util.*; public class HelloWorld < public static class NumberContainer < private T number; public NumberContainer(T number) < this.number = number; >public void print() < System.out.println(number); >> public static void main(String []args) < NumberContainer number1 = new NumberContainer(2L); NumberContainer number2 = new NumberContainer(1); NumberContainer number3 = new NumberContainer("f"); >>
Данный принцип ещё называют принципом PECS (Producer Extends Consumer Super). Подробнее можно прочитать на хабре в статье «Использование generic wildcards для повышения удобства Java API», а также в отличном обсуждении на stackoverflow: «Использование wildcard в Generics Java». Вот небольшой пример из исходников Java — метод Collections.copy:
public static class TestClass < public static void print(List extends String>list) < list.add("Hello World!"); System.out.println(list.get(0)); >> public static void main(String []args) < Listlist = new ArrayList<>(); TestClass.print(list); >
Но если заменить extends на super, всё станет хорошо. Так как мы наполняем список list значением перед выводом, он для нас является потребителем, то есть consumer’ом. Следовательно, используем super.
Наследование
Есть ещё одна необычная особенность дженериков — это их наследование. Наследование дженериков описано в tutorial от Oracle в разделе «Generics, Inheritance, and Subtypes». Главное это запомнить и осознать следующее. Мы не можем сделать так:
List list1 = new ArrayList();
List list1 = new ArrayList<>(); List list2 = list1;