Статьи

Введение в обобщения в Java — часть 6

Это продолжение вводной дискуссии по 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 .