-
Notifications
You must be signed in to change notification settings - Fork 67
/
chapter15-inheritance.jsh
360 lines (307 loc) · 11.3 KB
/
chapter15-inheritance.jsh
1
2
3
4
5
6
7
8
9
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
// To starts, run jshell --enable-preview which is a program able to interpret Java syntax
// then cut and paste the following lines to see how it works
// To exit jshell type /exit
// # Inheritance
// Historically, inheritance have been the way to sell Object Oriented Programming.
// It appears later that, inheritance is less important that it was envision first.
// The initial idea is to be able to reuse part of the definition of one class `A`
// to define another class `B` which is a kind of like `A`.
class A {
}
class B extends A { // I want to reuse and augment the definition of A
}
// Precisely, inheritance is 3 different things grouped together
// - __subtyping__
// everywhere something is typed `A`, you can send a `B` instead
// - __members inheritance__
// all instance members of `A` are copied in `B`
// - __polymorphism__
// you can replace the code of a method from `A` to adapt it to `B`
// ## Subtyping
// Subtyping is the most important part of the inheritance, it allows to reuse
// an existing code written for an `A` with an instance of `B`.
// let suppose, I have a method `sayHello()` for a `A`
class A {
}
void sayHello(A a) {
System.out.println("hello " + a);
}
var a = new A();
sayHello(a);
// if I create a `B` that inherits `A`, then i can use instance of `B`
// as argument of `hello()`.
class B extends A { // so B is a subtype of A
}
var b = new B();
sayHello(b);
// __subtyping__ is very important, because it means that we can reuse a
// method by calling it with several different types. And given that,
// The more a method is used, the less buggy it is, _subtyping_ helps
// to make applications more robust by sharing methods.
// ## Polymorphism
// Polymorphism works with __subtyping__, __subtyping__ allow to call a
// code with a subtype of the declared type. Polymorphism allows to
// adapt parts of the shared code to the subclass at runtime.
// By example, let suppose we have a class able to 'enhance' a text
// by making it more beautiful
class Enhancer {
String enhance(String text) {
return "_" + text + "_";
}
}
void sayHello(Enhancer enhancer, String text) {
System.out.println("hello " + enhancer.enhance(text));
}
var enhancer = new Enhancer();
sayHello(enhancer, "polymorphism");
//
class StarEnhancer extends Enhancer {
String enhance(String text) {
return "*" + text + "*";
}
}
var enhancer = new StarEnhancer();
sayHello(enhancer, "polymorphism");
// So not only we can call `sayHello()` with a `StarEnhancer` (__subtyping__),
// but inside `sayHello()`, the method call to `enhance()` will call
// the methode `StarEnhancer.enhance()` adapting the code of `hello()`
// to the fact that at runtime the enhancer is in fact a `StarEnhancer`.
// The mechanism that choose the `right` method in function of the object
// at runtime is called __polymorphism__.
// ### Overriding
// In the example above, `enhancer.enhance()` inside the method `sayHello()`
// can call `Enhancer.enhance()` or `StarEnhancer.enhance()`.
// We say that the method `enhance()` of `StarEnhancer` __overrides__
// the method `enhance()` of `Enhancer`.
// A method to __override__ another has to
// - have the same name
// - have the same number of parameter
// - can have a subtype as return type
// - can have subtypes of the declared exceptions (`throws`).
// ### `@Override`
// You can notice in the code below that we are using the annotation
// `@Override`. It is an annotation to document that the method
// override an existing method. The compiler also verifies that
// there is a method in the base class with the same parameter types.
class Enhancer {
String enhance(String text) {
return "_" + text + "_";
}
}
void sayHello(Enhancer enhancer, String text) {
System.out.println("hello " + enhancer.enhance(text));
}
class StarEnhancer extends Enhancer {
@Override // <-- aah
String enhance(String text) {
return "*" + text + "*";
}
}
var enhancer = new StarEnhancer();
sayHello(enhancer, "polymorphism");
// The annotation is not used by the runtime so it just make
// the code easier to understand for a human.
// ### Calling a method using `super.`
// The method that override another one can call the method it replace
// using the syntax `super.enhance(...)`.
class Enhancer {
String enhance(String text) {
return "_" + text + "_";
}
}
class StarEnhancer extends Enhancer {
@Override
String enhance(String text) {
return "*" + super.enhance(text) + "*";
}
}
var enhancer = new StarEnhancer();
sayHello(enhancer, "polymorphism");
// ## Members inheritance
// And last, when a class inherits from another one, then
// all the fields and methods defined in the super class
// are defined in the subclass.
// Here by example, the field `name` defined in `Animal`
// is also _implicily_ defined in `Lion`.
class Animal {
String name;
}
class Lion extends Animal {
boolean young;
void roar() {
System.out.println(name + " roar");
}
}
var lion = new Lion();
lion.name = "leo";
lion.young = true;
lion.roar();
// This mechanism is controversial because if the implementation of
// `Animal` change, the implementation of `Lion` has to be changed too.
// So a super class and a subclass are tightly bound to the point
// it's hard to maintain an application if the maintainer of the super class
// and the sub class are not the same person.
// > It's discouraged to inherits from a class you don't control.
// ### Constructor and inheritance
// In Java, the initialization of the super class has to be done first,
// before the initialization of the subclass.
// It's mandatory that the first statement of the constructor of the subclass
// to call the constructor of the super class.
// With a class `Point`
class Point {
private final int x;
private final int y;
Point(int x, int y) {
this.x = x;
this.y = y;
}
public String toString() {
return "x: " + x + " y: " + y;
}
}
var point = new Point(1, 5);
System.out.println(point);
// If the class `Point3D` inherits from `Point`, then the first statement of
// the constructor has to be a call to the constructor of the super class
// using the syntax `super(...)`.
class Point3D extends Point {
private final int z;
Point3D(int x, int y, int z) {
super(x, y); // call constructor of the super class
this.z = z;
}
public String toString() {
return super.toString() + " z: " + z;
}
}
var point3D = new Point3D(2, 4, 7);
System.out.println(point3D);
// > Note: that unlike the other members of a class, constructors are not inherited.
// ## Members inheritance and encapsulation
// We have seen that to have encapsulation, we have to declare the fields `private`.
// But with inheritance, a private field declared in the super class is present
// in the subclass but not accessible.
// Here the field `roomPrice` is inherited in `Palace` but can not be accessed
// in the method `price()` of the class `Palace` which doesn't compile
class Hotel {
private final int roomPrice;
public Hotel(int roomPrice) {
this.roomPrice = roomPrice;
}
public int price(int rooms) {
return rooms * roomPrice;
}
}
class Palace extends Hotel {
private final int extra;
public Palace(int roomPrice, int extra) {
super(roomPrice);
this.extra = extra;
}
public int price(int rooms) {
return rooms * (roomPrice + extra); // don't compile !
}
}
// The usual practice is to declare the super class and the subclass
// in the same package so declaring the field `roomPrice` with no keyword
// make it visible to the subclass
class Hotel {
/*package*/ final int roomPrice;
public Hotel(int roomPrice) {
this.roomPrice = roomPrice;
}
public int price(int rooms) {
return rooms * roomPrice;
}
}
class Palace extends Hotel {
private final int extra;
public Palace(int roomPrice, int extra) {
super(roomPrice);
this.extra = extra;
}
public int price(int rooms) {
return rooms * (roomPrice + extra);
}
}
var palace = new Palace(100, 50);
System.out.println(palace.price(2));
// ### Field protected
// In the code above, one can use the modifier `protected` too but
// because a `protected` field is visible by any subclass even the one
// the author of the subclass do not control. It means that the field
// can not be changed the same way a `public` field can not be changed.
// > Never use the keyword protected in Java.
// ## Relation with interfaces
// Nowadays, inheritance is used less and less in Java because interface
// provides __subtyping__ and __overriding__ without __members inheritance__.
// Given that the later mechanism is the one causing trouble,
// using an interface is often preferred to using inheritance.
// ### Records doesn't support inheritance
// Records doesn't support inheritance because it's so simple to declare
// a new record component that trying to share them will result into more
// code that necessary.
// Here is the class `Hotel` and `Palace` rewritten without inheritance
interface Bookable {
int price(int rooms);
}
record Hotel(int roomPrice) implements Bookable {
public int price(int rooms) {
return rooms * roomPrice;
}
}
record Palace(int roomPrice, int extra) implements Bookable {
public int price(int rooms) {
return rooms * (roomPrice + extra);
}
}
Bookable hotel = new Hotel(100);
System.out.println(hotel.price(2));
Bookable palace = new Palace(100, 50);
System.out.println(palace.price(2));
// ## Use delegation not inheritance
// Sometimes people are using inheritance where they should not !
// The worst occurrences is when people want __members inheritance__
// to avoid to write too many methods but forget that they get
// all the methods even the one they don't want.
// The problem is that if a class has a lot of methods, you are sure
// that at least one will not work correctly with the subclass.
// By example, this is a snippet of how the class `java.util.Properties`
// is defined in JDK. Because it inherits from `Hashtable<Object, Object>`,
// it means you can store a value which is not a String but get it as a String.
// Obviously, it will not work at runtime
class Properties extends Hashtable<Object, Object> {
public String getProperty(String key, String defaultValue) {
Objects.requireNonNull(key);
return (String)getOrDefault(key, defaultValue);
}
public void setProperty(String key, String value) {
Objects.requireNonNull(key);
Objects.requireNonNull(value);
put(key, value);
}
}
var properties = new Properties();
properties.put("java", 42);
System.out.println(properties.getProperty("java", "??"));
// The traditional advice is if you want to use part of an existing
// implementation, instead of inherits from that class, store it
// in a field and so your method can delegate a part of their implementations.
// So for the class `Properties`, it should be implemented like this
class Properties {
private final HashMap<String, String> map = new HashMap<>();
public String getProperty(String key, String defaultValue) {
Objects.requireNonNull(key);
return map.getOrDefault(key, defaultValue);
}
public void setProperty(String key, String value) {
Objects.requireNonNull(key);
Objects.requireNonNull(value);
map.put(key, value);
}
}
var properties = new Properties();
properties.setProperty("java", "best language ever, for life !");
System.out.println(properties.getProperty("java", "??"));
System.out.println(properties.getProperty("brainfuck", "??"));
// > Always prefer delegation to inheritance