Это продолжение вводной дискуссии по Generics, предыдущие части которой можно найти здесь .
В прошлой статье мы обсуждали рекурсивные оценки параметров типа. Мы увидели, как рекурсивная оценка помогла нам повторно использовать логику сравнения транспортных средств. В конце этой статьи я предположил, что возможное смешивание типов может произойти, если мы не будем достаточно осторожны. Сегодня мы увидим пример этого.
Микширование может произойти, если кто-то по ошибке создаст подкласс Vehicle следующим образом:
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
27
28
|
/** * Definition of Vehicle */ public abstract class Vehicle<E extends Vehicle<E>> implements Comparable<E> { // other methods and properties public int compareTo(E vehicle) { // method implementation } } /** * Definition of Bus */ public class Bus extends Vehicle<Bus> {} /** * BiCycle, new subtype of Vehicle */ public class BiCycle extends Vehicle<Bus> {} /** * Now this class’s compareTo method will take a Bus type * as its argument. As a result, you will not be able to compare * a BiCycle with another Bicycle, but with a Bus. */ cycle.compareTo(anotherCycle); // This will generate a compile time error cycle.compareTo(bus); // but you will be able to do this without any error |
Этот тип смешения не происходит с Enums, потому что JVM заботится о создании подклассов и создании экземпляров для типов enum, но если мы используем этот стиль в нашем коде, мы должны быть осторожны.
Давайте поговорим о другом интересном приложении рекурсивных границ. Рассмотрим следующий класс:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
public class MyClass { private String attrib1; private String attrib2; private String attrib3; private String attrib4; private String attrib5; public MyClass() {} public String getAttrib1() { return attrib1; } public void setAttrib1(String attrib1) { this .attrib1 = attrib1; } public String getAttrib2() { return attrib2; } public void setAttrib2(String attrib2) { this .attrib2 = attrib2; } public String getAttrib3() { return attrib3; } public void setAttrib3(String attrib3) { this .attrib3 = attrib3; } public String getAttrib4() { return attrib4; } public void setAttrib4(String attrib4) { this .attrib4 = attrib4; } public String getAttrib5() { return attrib5; } public void setAttrib5(String attrib5) { this .attrib5 = attrib5; } } |
Если мы хотим создать экземпляр этого класса, то мы можем сделать это:
1
2
3
|
MyClass mc = new MyClass(); mc.setAttrib1( "Attribute 1" ); mc.setAttrib2( "Attribute 2" ); |
Приведенный выше код создает экземпляр класса и инициализирует свойства. Если бы мы могли использовать здесь метод цепочки , мы могли бы написать:
1
2
|
MyClass mc = new MyClass().setAttrib1( "Attribute 1" ) .setAttrib2( "Attribute 2" ); |
который, очевидно, выглядит намного лучше, чем первая версия. Однако, чтобы включить этот тип цепочки методов, нам нужно изменить MyClass следующим образом:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
public class MyClass { private String attrib1; private String attrib2; private String attrib3; private String attrib4; private String attrib5; public MyClass() {} public String getAttrib1() { return attrib1; } public MyClass setAttrib1(String attrib1) { this .attrib1 = attrib1; return this ; } public String getAttrib2() { return attrib2; } public MyClass setAttrib2(String attrib2) { this .attrib2 = attrib2; return this ; } public String getAttrib3() { return attrib3; } public MyClass setAttrib3(String attrib3) { this .attrib3 = attrib3; return this ; } public String getAttrib4() { return attrib4; } public MyClass setAttrib4(String attrib4) { this .attrib4 = attrib4; return this ; } public String getAttrib5() { return attrib5; } public MyClass setAttrib5(String attrib5) { this .attrib5 = attrib5; return this ; } } |
и тогда мы сможем использовать цепочку методов для экземпляров этого класса. Однако, если мы хотим использовать цепочку методов, где задействовано наследование, все становится немного грязно:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
|
public abstract class Parent { private String attrib1; private String attrib2; private String attrib3; private String attrib4; private String attrib5; public Parent() {} public String getAttrib1() { return attrib1; } public Parent setAttrib1(String attrib1) { this .attrib1 = attrib1; return this ; } public String getAttrib2() { return attrib2; } public Parent setAttrib2(String attrib2) { this .attrib2 = attrib2; return this ; } public String getAttrib3() { return attrib3; } public Parent setAttrib3(String attrib3) { this .attrib3 = attrib3; return this ; } public String getAttrib4() { return attrib4; } public Parent setAttrib4(String attrib4) { this .attrib4 = attrib4; return this ; } public String getAttrib5() { return attrib5; } public Parent setAttrib5(String attrib5) { this .attrib5 = attrib5; return this ; } } public class Child extends Parent { private String attrib6; private String attrib7; public Child() {} public String getAttrib6() { return attrib6; } public Child setAttrib6(String attrib6) { this .attrib6 = attrib6; return this ; } public String getAttrib7() { return attrib7; } public Child setAttrib7(String attrib7) { this .attrib7 = attrib7; return this ; } } /** * Now try using method chaining for instances of Child * in the following way, you will get compile time errors. */ Child c = new Child().setAttrib1( "Attribute 1" ).setAttrib6( "Attribute 6" ); |
Причина этого в том, что, хотя Child наследует все методы установки от своего родителя, тип возвращаемого значения всех этих методов установки имеет тип Parent , а не Child . Таким образом, первый установщик вернет ссылку типа Parent , вызвав setAttrib6, что приведет к ошибке компиляции, поскольку у него нет такого метода.
Мы можем решить эту проблему, введя универсальный параметр типа в Parent и определив для него рекурсивную границу. Все его дочерние элементы передадут себя в качестве аргумента типа при расширении из него, гарантируя, что методы setter будут возвращать ссылки своего типа:
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
|
public abstract class Parent<T extends Parent<T>> { private String attrib1; private String attrib2; private String attrib3; private String attrib4; private String attrib5; public Parent() { } public String getAttrib1() { return attrib1; } @SuppressWarnings ( "unchecked" ) public T setAttrib1(String attrib1) { this .attrib1 = attrib1; return (T) this ; } public String getAttrib2() { return attrib2; } @SuppressWarnings ( "unchecked" ) public T setAttrib2(String attrib2) { this .attrib2 = attrib2; return (T) this ; } public String getAttrib3() { return attrib3; } @SuppressWarnings ( "unchecked" ) public T setAttrib3(String attrib3) { this .attrib3 = attrib3; return (T) this ; } public String getAttrib4() { return attrib4; } @SuppressWarnings ( "unchecked" ) public T setAttrib4(String attrib4) { this .attrib4 = attrib4; return (T) this ; } public String getAttrib5() { return attrib5; } @SuppressWarnings ( "unchecked" ) public T setAttrib5(String attrib5) { this .attrib5 = attrib5; return (T) this ; } } public class Child extends Parent<Child> { private String attrib6; private String attrib7; public String getAttrib6() { return attrib6; } public Child setAttrib6(String attrib6) { this .attrib6 = attrib6; return this ; } public String getAttrib7() { return attrib7; } public Child setAttrib7(String attrib7) { this .attrib7 = attrib7; return this ; } } |
Обратите внимание, что мы должны явно привести это к типу T, потому что компилятор не знает, возможно ли это преобразование, даже если это так, потому что T по определению ограничен Parent <T> . Также, поскольку мы приводим объектную ссылку на T , компилятор выдаст непроверенное предупреждение. Чтобы подавить это, мы использовали @SuppressWarnings («unchecked») над установщиками.
С вышеупомянутыми модификациями, совершенно правильно сделать это:
1
2
|
Child c = new Child().setAttrib1( "Attribute 1" ) .setAttrib6( "Attribute 6" ); |
При написании методов установки методов таким образом, мы должны быть осторожны, чтобы не использовать рекурсивные границы для любых других целей, например, для доступа к дочерним состояниям из parent, потому что это подвергнет parent внутренним деталям его подклассов и в конечном итоге нарушит инкапсуляцию.
Этим постом я заканчиваю базовое введение в Generics. Есть так много вещей, которые я не обсуждал в этой серии, потому что я считаю, что они выходят за рамки вводного уровня.
До скорого.
Ссылка: | Введение в Generics в Java — часть 6 от нашего партнера JCG Саима Ахмеда в блоге Codesod . |