forked from 724888/h2pl.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
/
search.xml
605 lines (341 loc) · 517 KB
/
search.xml
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
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>Java集合详解3:Iterator,fail-fast机制与比较器</title>
<link href="/2018/05/09/collection3/"/>
<url>/2018/05/09/collection3/</url>
<content type="html"><![CDATA[<p>今天我们来探索一下LIterator,fail-fast机制与比较器的源码。</p><p>具体代码在我的GitHub中可以找到</p><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p><p>喜欢的话麻烦star一下哈</p><p>文章首发于我的个人博客:</p><p><a href="https://h2pl.github.io/2018/05/9/collection3">https://h2pl.github.io/2018/05/9/collection3</a></p><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:<a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p><p>我的个人博客主要发原创文章,也欢迎浏览<br><a href="https://h2pl.github.io/">https://h2pl.github.io/</a></p><h1 id="Iterator"><a href="#Iterator" class="headerlink" title="Iterator"></a>Iterator</h1><p>本文参考 <a href="http://cmsblogs.com/?p=1185" target="_blank" rel="noopener">http://cmsblogs.com/?p=1185</a></p><p>迭代对于我们搞Java的来说绝对不陌生。我们常常使用JDK提供的迭代接口进行Java集合的迭代。</p><pre><code>Iterator iterator = list.iterator(); while(iterator.hasNext()){ String string = iterator.next(); //do something }</code></pre><p>迭代其实我们可以简单地理解为遍历,是一个标准化遍历各类容器里面的所有对象的方法类,它是一个很典型的设计模式。Iterator模式是用于遍历集合类的标准访问方法。</p><p>它可以把访问逻辑从不同类型的集合类中抽象出来,从而避免向客户端暴露集合的内部结构。 在没有迭代器时我们都是这么进行处理的。如下:</p><p>对于数组我们是使用下标来进行处理的:</p><pre><code>int[] arrays = new int[10]; for(int i = 0 ; i < arrays.length ; i++){ int a = arrays[i]; //do something }</code></pre><p>对于ArrayList是这么处理的:</p><pre><code>List<String> list = new ArrayList<String>(); for(int i = 0 ; i < list.size() ; i++){ String string = list.get(i); //do something }</code></pre><p>对于这两种方式,我们总是都事先知道集合的内部结构,访问代码和集合本身是紧密耦合的,无法将访问逻辑从集合类和客户端代码中分离出来。同时每一种集合对应一种遍历方法,客户端代码无法复用。</p><p>在实际应用中如何需要将上面将两个集合进行整合是相当麻烦的。所以为了解决以上问题,Iterator模式腾空出世,它总是用同一种逻辑来遍历集合。</p><p>使得客户端自身不需要来维护集合的内部结构,所有的内部状态都由Iterator来维护。客户端从不直接和集合类打交道,它总是控制Iterator,向它发送”向前”,”向后”,”取当前元素”的命令,就可以间接遍历整个集合。</p><p>上面只是对Iterator模式进行简单的说明,下面我们看看Java中Iterator接口,看他是如何来进行实现的。</p><h2 id="java-util-Iterator"><a href="#java-util-Iterator" class="headerlink" title="java.util.Iterator"></a>java.util.Iterator</h2><p>在Java中Iterator为一个接口,它只提供了迭代了基本规则,在JDK中他是这样定义的:对 collection 进行迭代的迭代器。迭代器取代了 Java Collections Framework 中的 Enumeration。迭代器与枚举有两点不同:</p><pre><code>1、迭代器允许调用者利用定义良好的语义在迭代期间从迭代器所指向的 collection 移除元素。2、方法名称得到了改进。</code></pre><p>其接口定义如下:</p><pre><code>public interface Iterator { boolean hasNext(); Object next(); void remove();}</code></pre><p>其中:</p><pre><code>Object next():返回迭代器刚越过的元素的引用,返回值是Object,需要强制转换成自己需要的类型boolean hasNext():判断容器内是否还有可供访问的元素void remove():删除迭代器刚越过的元素</code></pre><p>对于我们而言,我们只一般只需使用next()、hasNext()两个方法即可完成迭代。如下:</p><pre><code>for(Iterator it = c.iterator(); it.hasNext(); ) { Object o = it.next(); //do something}</code></pre><p>==前面阐述了Iterator有一个很大的优点,就是我们不必知道集合的内部结果,集合的内部结构、状态由Iterator来维持,通过统一的方法hasNext()、next()来判断、获取下一个元素,至于具体的内部实现我们就不用关心了。==</p><p>但是作为一个合格的程序员我们非常有必要来弄清楚Iterator的实现。下面就ArrayList的源码进行分析分析。</p><h2 id="各个集合的Iterator的实现"><a href="#各个集合的Iterator的实现" class="headerlink" title="各个集合的Iterator的实现"></a>各个集合的Iterator的实现</h2><p>下面就ArrayList的Iterator实现来分析,其实如果我们理解了ArrayList、Hashset、TreeSet的数据结构,内部实现,对于他们是如何实现Iterator也会胸有成竹的。因为ArrayList的内部实现采用数组,所以我们只需要记录相应位置的索引即可,其方法的实现比较简单。</p><p>ArrayList的Iterator实现</p><p>在ArrayList内部首先是定义一个内部类Itr,该内部类实现Iterator接口,如下:</p><pre><code>private class Itr implements Iterator<E> { //do something}而ArrayList的iterator()方法实现:public Iterator<E> iterator() { return new Itr(); }</code></pre><p>所以通过使用ArrayList.iterator()方法返回的是Itr()内部类,所以现在我们需要关心的就是Itr()内部类的实现:</p><p>在Itr内部定义了三个int型的变量:cursor、lastRet、expectedModCount。其中cursor表示下一个元素的索引位置,lastRet表示上一个元素的索引位置</p><pre><code>int cursor; int lastRet = -1; int expectedModCount = modCount;</code></pre><p>从cursor、lastRet定义可以看出,lastRet一直比cursor少一所以hasNext()实现方法异常简单,只需要判断cursor和lastRet是否相等即可。</p><pre><code>public boolean hasNext() { return cursor != size;}</code></pre><p>对于next()实现其实也是比较简单的,只要返回cursor索引位置处的元素即可,然后修改cursor、lastRet即可。</p><pre><code>public E next() { checkForComodification(); int i = cursor; //记录索引位置 if (i >= size) //如果获取元素大于集合元素个数,则抛出异常 throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; //cursor + 1 return (E) elementData[lastRet = i]; //lastRet + 1 且返回cursor处元素}</code></pre><blockquote><p>checkForComodification()主要用来判断集合的修改次数是否合法,即用来判断遍历过程中集合是否被修改过。</p><p>。modCount用于记录ArrayList集合的修改次数,初始化为0,,每当集合被修改一次(结构上面的修改,内部update不算),如add、remove等方法,modCount + 1,所以如果modCount不变,则表示集合内容没有被修改。</p><p>该机制主要是用于实现ArrayList集合的快速失败机制,在Java的集合中,较大一部分集合是存在快速失败机制的,这里就不多说,后面会讲到。</p><p>所以要保证在遍历过程中不出错误,我们就应该保证在遍历过程中不会对集合产生结构上的修改(当然remove方法除外),出现了异常错误,我们就应该认真检查程序是否出错而不是catch后不做处理。</p></blockquote><pre><code>final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }对于remove()方法的是实现,它是调用ArrayList本身的remove()方法删除lastRet位置元素,然后修改modCount即可。public void remove() { if (lastRet < 0) throw new IllegalStateException(); checkForComodification(); try { ArrayList.this.remove(lastRet); cursor = lastRet; lastRet = -1; expectedModCount = modCount; } catch (IndexOutOfBoundsException ex) { throw new ConcurrentModificationException(); }}</code></pre><p>这里就对ArrayList的Iterator实现讲解到这里,对于Hashset、TreeSet等集合的Iterator实现,各位如果感兴趣可以继续研究,个人认为在研究这些集合的源码之前,有必要对该集合的数据结构有清晰的认识,这样会达到事半功倍的效果!!!!</p><h1 id="fail-fast机制"><a href="#fail-fast机制" class="headerlink" title="fail-fast机制"></a>fail-fast机制</h1><p>这部分参考<a href="http://cmsblogs.com/?p=1220" target="_blank" rel="noopener">http://cmsblogs.com/?p=1220</a></p><p>在JDK的Collection中我们时常会看到类似于这样的话:</p><p>例如,ArrayList:</p><blockquote><p>注意,迭代器的快速失败行为无法得到保证,因为一般来说,不可能对是否出现不同步并发修改做出任何硬性保证。快速失败迭代器会尽最大努力抛出ConcurrentModificationException。<br>因此,为提高这类迭代器的正确性而编写一个依赖于此异常的程序是错误的做法:迭代器的快速失败行为应该仅用于检测 bug。</p></blockquote><p>HashMap中:</p><blockquote><p>注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。</p></blockquote><p>在这两段话中反复地提到”快速失败”。那么何为”快速失败”机制呢?</p><blockquote><p>“快速失败”也就是fail-fast,它是Java集合的一种错误检测机制。当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast机制。</p><p>记住是有可能,而不是一定。例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException异常,从而产生fail-fast机制。</p></blockquote><h2 id="fail-fast示例"><a href="#fail-fast示例" class="headerlink" title="fail-fast示例"></a>fail-fast示例</h2><pre><code>public class FailFastTest { private static List<Integer> list = new ArrayList<>(); /** * @desc:线程one迭代list * @Project:test * @file:FailFastTest.java * @Authro:chenssy * @data:2014年7月26日 */ private static class threadOne extends Thread{ public void run() { Iterator<Integer> iterator = list.iterator(); while(iterator.hasNext()){ int i = iterator.next(); System.out.println("ThreadOne 遍历:" + i); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } } /** * @desc:当i == 3时,修改list * @Project:test * @file:FailFastTest.java * @Authro:chenssy * @data:2014年7月26日 */ private static class threadTwo extends Thread{ public void run(){ int i = 0 ; while(i < 6){ System.out.println("ThreadTwo run:" + i); if(i == 3){ list.remove(i); } i++; } } } public static void main(String[] args) { for(int i = 0 ; i < 10;i++){ list.add(i); } new threadOne().start(); new threadTwo().start(); }}</code></pre><p>运行结果:</p><pre><code>ThreadOne 遍历:0ThreadTwo run:0ThreadTwo run:1ThreadTwo run:2ThreadTwo run:3ThreadTwo run:4ThreadTwo run:5Exception in thread "Thread-0" java.util.ConcurrentModificationException at java.util.ArrayList$Itr.checkForComodification(Unknown Source) at java.util.ArrayList$Itr.next(Unknown Source) at test.ArrayListTest$threadOne.run(ArrayListTest.java:23)</code></pre><h2 id="fail-fast产生原因"><a href="#fail-fast产生原因" class="headerlink" title="fail-fast产生原因"></a>fail-fast产生原因</h2><p>通过上面的示例和讲解,我初步知道fail-fast产生的原因就在于程序在对 collection 进行迭代时,某个线程对该 collection 在结构上对其做了修改,这时迭代器就会抛出 ConcurrentModificationException 异常信息,从而产生 fail-fast。</p><blockquote><p>要了解fail-fast机制,我们首先要对ConcurrentModificationException 异常有所了解。当方法检测到对象的并发修改,但不允许这种修改时就抛出该异常。同时需要注意的是,该异常不会始终指出对象已经由不同线程并发修改,如果单线程违反了规则,同样也有可能会抛出改异常。</p></blockquote><p>诚然,迭代器的快速失败行为无法得到保证,它不能保证一定会出现该错误,但是快速失败操作会尽最大努力抛出ConcurrentModificationException异常,所以因此,为提高此类操作的正确性而编写一个依赖于此异常的程序是错误的做法,正确做法是:ConcurrentModificationException 应该仅用于检测 bug。下面我将以ArrayList为例进一步分析fail-fast产生的原因。</p><blockquote><p>从前面我们知道fail-fast是在操作迭代器时产生的。现在我们来看看ArrayList中迭代器的源代码:</p></blockquote><pre><code>private class Itr implements Iterator<E> { int cursor; int lastRet = -1; int expectedModCount = ArrayList.this.modCount; public boolean hasNext() { return (this.cursor != ArrayList.this.size); } public E next() { checkForComodification(); /** 省略此处代码 */ } public void remove() { if (this.lastRet < 0) throw new IllegalStateException(); checkForComodification(); /** 省略此处代码 */ } final void checkForComodification() { if (ArrayList.this.modCount == this.expectedModCount) return; throw new ConcurrentModificationException(); } }</code></pre><p>从上面的源代码我们可以看出,迭代器在调用next()、remove()方法时都是调用checkForComodification()方法,该方法主要就是检测modCount == expectedModCount ? 若不等则抛出ConcurrentModificationException 异常,从而产生fail-fast机制。所以要弄清楚为什么会产生fail-fast机制我们就必须要用弄明白为什么modCount != expectedModCount ,他们的值在什么时候发生改变的。</p><p>expectedModCount 是在Itr中定义的:int expectedModCount = ArrayList.this.modCount;所以他的值是不可能会修改的,所以会变的就是modCount。modCount是在 AbstractList 中定义的,为全局变量:</p><p>protected transient int modCount = 0;<br>那么他什么时候因为什么原因而发生改变呢?请看ArrayList的源码:</p><pre><code>public boolean add(E paramE) { ensureCapacityInternal(this.size + 1); /** 省略此处代码 */}private void ensureCapacityInternal(int paramInt) { if (this.elementData == EMPTY_ELEMENTDATA) paramInt = Math.max(10, paramInt); ensureExplicitCapacity(paramInt);}private void ensureExplicitCapacity(int paramInt) { this.modCount += 1; //修改modCount /** 省略此处代码 */}</code></pre><p> public boolean remove(Object paramObject) {<br> int i;<br> if (paramObject == null)<br> for (i = 0; i < this.size; ++i) {<br> if (this.elementData[i] != null)<br> continue;<br> fastRemove(i);<br> return true;<br> }<br> else<br> for (i = 0; i < this.size; ++i) {<br> if (!(paramObject.equals(this.elementData[i])))<br> continue;<br> fastRemove(i);<br> return true;<br> }<br> return false;<br> }</p><pre><code>private void fastRemove(int paramInt) { this.modCount += 1; //修改modCount /** 省略此处代码 */}public void clear() { this.modCount += 1; //修改modCount /** 省略此处代码 */}</code></pre><blockquote><p>从上面的源代码我们可以看出,ArrayList中无论add、remove、clear方法只要是涉及了改变ArrayList元素的个数的方法都会导致modCount的改变。</p></blockquote><p>所以我们这里可以初步判断由于expectedModCount 得值与modCount的改变不同步,导致两者之间不等从而产生fail-fast机制。知道产生fail-fast产生的根本原因了,我们可以有如下场景:</p><p>有两个线程(线程A,线程B),其中线程A负责遍历list、线程B修改list。线程A在遍历list过程的某个时候(此时expectedModCount = modCount=N),线程启动,同时线程B增加一个元素,这是modCount的值发生改变(modCount + 1 = N + 1)。</p><p>线程A继续遍历执行next方法时,通告checkForComodification方法发现expectedModCount = N ,而modCount = N + 1,两者不等,这时就抛出ConcurrentModificationException 异常,从而产生fail-fast机制。</p><p>所以,直到这里我们已经完全了解了fail-fast产生的根本原因了。知道了原因就好找解决办法了。</p><p>三、fail-fast解决办法</p><p>通过前面的实例、源码分析,我想各位已经基本了解了fail-fast的机制,下面我就产生的原因提出解决方案。这里有两种解决方案:</p><blockquote><p>方案一:在遍历过程中所有涉及到改变modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,这样就可以解决。但是不推荐,因为增删造成的同步锁可能会阻塞遍历操作。</p><p>方案二:使用CopyOnWriteArrayList来替换ArrayList。推荐使用该方案。</p></blockquote><p>CopyOnWriteArrayList为何物?ArrayList 的一个线程安全的变体,其中所有可变操作(add、set 等等)都是通过对底层数组进行一次新的复制来实现的。 该类产生的开销比较大,但是在两种情况下,它非常适合使用。</p><blockquote><p>1:在不能或不想进行同步遍历,但又需要从并发线程中排除冲突时。</p><p>2:当遍历操作的数量大大超过可变操作的数量时。遇到这两种情况使用CopyOnWriteArrayList来替代ArrayList再适合不过了。那么为什么CopyOnWriterArrayList可以替代ArrayList呢?</p></blockquote><p>第一、CopyOnWriterArrayList的无论是从数据结构、定义都和ArrayList一样。它和ArrayList一样,同样是实现List接口,底层使用数组实现。在方法上也包含add、remove、clear、iterator等方法。</p><p>第二、CopyOnWriterArrayList根本就不会产生ConcurrentModificationException异常,也就是它使用迭代器完全不会产生fail-fast机制。请看:</p><p>private static class COWIterator<e> implements ListIterator<e> {<br> /*<em> 省略此处代码 </em>/<br> public E next() {<br> if (!(hasNext()))<br> throw new NoSuchElementException();<br> return this.snapshot[(this.cursor++)];<br> }</e></e></p><pre><code> /** 省略此处代码 */}</code></pre><p>CopyOnWriterArrayList的方法根本就没有像ArrayList中使用checkForComodification方法来判断expectedModCount 与 modCount 是否相等。它为什么会这么做,凭什么可以这么做呢?我们以add方法为例:</p><pre><code>public boolean add(E paramE) { ReentrantLock localReentrantLock = this.lock; localReentrantLock.lock(); try { Object[] arrayOfObject1 = getArray(); int i = arrayOfObject1.length; Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1); arrayOfObject2[i] = paramE; setArray(arrayOfObject2); int j = 1; return j; } finally { localReentrantLock.unlock(); } } final void setArray(Object[] paramArrayOfObject) { this.array = paramArrayOfObject; }</code></pre><p>CopyOnWriterArrayList的add方法与ArrayList的add方法有一个最大的不同点就在于,下面三句代码:</p><pre><code>Object[] arrayOfObject2 = Arrays.copyOf(arrayOfObject1, i + 1);arrayOfObject2[i] = paramE;setArray(arrayOfObject2);</code></pre><p>就是这三句代码使得CopyOnWriterArrayList不会抛ConcurrentModificationException异常。他们所展现的魅力就在于copy原来的array,再在copy数组上进行add操作,这样做就完全不会影响COWIterator中的array了。</p><blockquote><p>所以CopyOnWriterArrayList所代表的核心概念就是:任何对array在结构上有所改变的操作(add、remove、clear等),CopyOnWriterArrayList都会copy现有的数据,再在copy的数据上修改,这样就不会影响COWIterator中的数据了,修改完成之后改变原有数据的引用即可。同时这样造成的代价就是产生大量的对象,同时数组的copy也是相当有损耗的。</p></blockquote><h1 id="Comparable-和-Comparator"><a href="#Comparable-和-Comparator" class="headerlink" title="Comparable 和 Comparator"></a>Comparable 和 Comparator</h1><p>Java 中为我们提供了两种比较机制:Comparable 和 Comparator,他们之间有什么区别呢?今天来了解一下。</p><h2 id="Comparable"><a href="#Comparable" class="headerlink" title="Comparable"></a>Comparable</h2><p>Comparable 在 java.lang包下,是一个接口,内部只有一个方法 compareTo():</p><pre><code>public interface Comparable<T> { public int compareTo(T o);}</code></pre><p>Comparable 可以让实现它的类的对象进行比较,具体的比较规则是按照 compareTo 方法中的规则进行。这种顺序称为 自然顺序。</p><p>compareTo 方法的返回值有三种情况:</p><pre><code>e1.compareTo(e2) > 0 即 e1 > e2e1.compareTo(e2) = 0 即 e1 = e2e1.compareTo(e2) < 0 即 e1 < e2</code></pre><p>注意:</p><blockquote><p>1.由于 null 不是一个类,也不是一个对象,因此在重写 compareTo 方法时应该注意 e.compareTo(null) 的情况,即使 e.equals(null) 返回 false,compareTo 方法也应该主动抛出一个空指针异常 NullPointerException。</p><p>2.Comparable 实现类重写 compareTo 方法时一般要求 e1.compareTo(e2) == 0 的结果要和 e1.equals(e2) 一致。这样将来使用 SortedSet 等根据类的自然排序进行排序的集合容器时可以保证保存的数据的顺序和想象中一致。<br>有人可能好奇上面的第二点如果违反了会怎样呢?</p></blockquote><p>举个例子,如果你往一个 SortedSet 中先后添加两个对象 a 和 b,a b 满足 (!a.equals(b) && a.compareTo(b) == 0),同时也没有另外指定个 Comparator,那当你添加完 a 再添加 b 时会添加失败返回 false, SortedSet 的 size 也不会增加,因为在 SortedSet 看来它们是相同的,而 SortedSet 中是不允许重复的。</p><blockquote><p>实际上所有实现了 Comparable 接口的 Java 核心类的结果都和 equlas 方法保持一致。<br>实现了 Comparable 接口的 List 或则数组可以使用 Collections.sort() 或者 Arrays.sort() 方法进行排序。</p></blockquote><blockquote><p>实现了 Comparable 接口的对象才能够直接被用作 SortedMap (SortedSet) 的 key,要不然得在外边指定 Comparator 排序规则。</p></blockquote><p>因此自己定义的类如果想要使用有序的集合类,需要实现 Comparable 接口,比如:</p><p>**</p><ul><li>description: 测试用的实体类 书, 实现了 Comparable 接口,自然排序</li><li><br></li><li>author: shixinzhang</li><li><br></li><li>data: 10/5/2016<br>*/<br>public class BookBean implements Serializable, Comparable {<br> private String name;<br> private int count;</li></ul><pre><code>public BookBean(String name, int count) { this.name = name; this.count = count;}public String getName() { return name;}public void setName(String name) { this.name = name;}public int getCount() { return count;}public void setCount(int count) { this.count = count;}/** * 重写 equals * @param o * @return */@Overridepublic boolean equals(Object o) { if (this == o) return true; if (!(o instanceof BookBean)) return false; BookBean bean = (BookBean) o; if (getCount() != bean.getCount()) return false; return getName().equals(bean.getName());}/** * 重写 hashCode 的计算方法 * 根据所有属性进行 迭代计算,避免重复 * 计算 hashCode 时 计算因子 31 见得很多,是一个质数,不能再被除 * @return */@Overridepublic int hashCode() { //调用 String 的 hashCode(), 唯一表示一个字符串内容 int result = getName().hashCode(); //乘以 31, 再加上 count result = 31 * result + getCount(); return result;}@Overridepublic String toString() { return "BookBean{" + "name='" + name + '\'' + ", count=" + count + '}';}/** * 当向 TreeSet 中添加 BookBean 时,会调用这个方法进行排序 * @param another * @return */@Overridepublic int compareTo(Object another) { if (another instanceof BookBean){ BookBean anotherBook = (BookBean) another; int result; //比如这里按照书价排序 result = getCount() - anotherBook.getCount(); //或者按照 String 的比较顺序 //result = getName().compareTo(anotherBook.getName()); if (result == 0){ //当书价一致时,再对比书名。 保证所有属性比较一遍 result = getName().compareTo(anotherBook.getName()); } return result; } // 一样就返回 0 return 0;}</code></pre><p>上述代码还重写了 equlas(), hashCode() 方法,自定义的类将来可能会进行比较时,建议重写这些方法。</p><blockquote><p>这里我想表达的是在有些场景下 equals 和 compareTo 结果要保持一致,这时候不重写 equals,使用 Object.equals 方法得到的结果会有问题,比如说 HashMap.put() 方法,会先调用 key 的 equals 方法进行比较,然后才调用 compareTo。</p><p>后面重写 compareTo 时,要判断某个相同时对比下一个属性,把所有属性都比较一次。</p></blockquote><h2 id="Comparable-1"><a href="#Comparable-1" class="headerlink" title="Comparable"></a>Comparable</h2><p>Comparable 接口属于 Java 集合框架的一部分。</p><p>Comparator 定制排序</p><p>Comparator 在 java.util 包下,也是一个接口,JDK 1.8 以前只有两个方法:</p><pre><code>public interface Comparator<T> { public int compare(T lhs, T rhs); public boolean equals(Object object);}</code></pre><p>JDK 1.8 以后又新增了很多方法:</p><p>基本上都是跟 Function 相关的,这里暂不介绍 1.8 新增的。</p><blockquote><p>从上面内容可知使用自然排序需要类实现 Comparable,并且在内部重写 comparaTo 方法。</p><p>而 Comparator 则是在外部制定排序规则,然后作为排序策略参数传递给某些类,比如 Collections.sort(), Arrays.sort(), 或者一些内部有序的集合(比如 SortedSet,SortedMap 等)。</p></blockquote><p>Comparator的使用方法<br>使用方式主要分三步:</p><p>创建一个 Comparator 接口的实现类,并赋值给一个对象<br>在 compare 方法中针对自定义类写排序规则<br>将 Comparator 对象作为参数传递给 排序类的某个方法<br>向排序类中添加 compare 方法中使用的自定义类<br>举个例子:</p><pre><code>// 1.创建一个实现 Comparator 接口的对象Comparator comparator = new Comparator() { @Override public int compare(Object object1, Object object2) { if (object1 instanceof NewBookBean && object2 instanceof NewBookBean){ NewBookBean newBookBean = (NewBookBean) object1; NewBookBean newBookBean1 = (NewBookBean) object2; //具体比较方法参照 自然排序的 compareTo 方法,这里只举个栗子 return newBookBean.getCount() - newBookBean1.getCount(); } return 0; }};//2.将此对象作为形参传递给 TreeSet 的构造器中TreeSet treeSet = new TreeSet(comparator);//3.向 TreeSet 中添加 步骤 1 中 compare 方法中设计的类的对象treeSet.add(new NewBookBean("A",34));treeSet.add(new NewBookBean("S",1));treeSet.add( new NewBookBean("V",46));treeSet.add( new NewBookBean("Q",26));</code></pre><p>其实可以看到,Comparator 的使用是一种策略模式。<br>排序类中持有一个 Comparator 接口的引用:</p><pre><code>Comparator<? super K> comparator;</code></pre><p>而我们可以传入各种自定义排序规则的 Comparator 实现类,对同样的类制定不同的排序策略。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>Java 中的两种排序方式:</p><pre><code>Comparable 自然排序。(实体类实现)Comparator 是定制排序。(无法修改实体类时,直接在调用方创建)同时存在时采用 Comparator(定制排序)的规则进行比较。</code></pre><p>对于一些普通的数据类型(比如 String, Integer, Double…),它们默认实现了Comparable 接口,实现了 compareTo 方法,我们可以直接使用。</p><p>而对于一些自定义类,它们可能在不同情况下需要实现不同的比较策略,我们可以新创建 Comparator 接口,然后使用特定的 Comparator 实现进行比较。</p><p>这就是 Comparable 和 Comparator 的区别。</p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java集合框架 </tag>
</tags>
</entry>
<entry>
<title>Java集合详解2:LinkedList和Queue</title>
<link href="/2018/05/09/collection2/"/>
<url>/2018/05/09/collection2/</url>
<content type="html"><![CDATA[<p>今天我们来探索一下LinkedList和Queue,以及Stack的源码。</p><p>具体代码在我的GitHub中可以找到</p><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p><p>喜欢的话麻烦star一下哈</p><p>文章首发于我的个人博客:</p><p><a href="https://h2pl.github.io/2018/05/09/collection2">https://h2pl.github.io/2018/05/09/collection2</a></p><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:<a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p><p>我的个人博客主要发原创文章,也欢迎浏览<br><a href="https://h2pl.github.io/">https://h2pl.github.io/</a></p><p>本文参考 <a href="http://cmsblogs.com/?p=155" target="_blank" rel="noopener">http://cmsblogs.com/?p=155</a><br>和<br><a href="https://www.jianshu.com/p/0e84b8d3606c" target="_blank" rel="noopener">https://www.jianshu.com/p/0e84b8d3606c</a></p><a id="more"></a><h1 id="LinkedList概述"><a href="#LinkedList概述" class="headerlink" title="LinkedList概述"></a>LinkedList概述</h1><blockquote><p> LinkedList与ArrayList一样实现List接口,只是ArrayList是List接口的大小可变数组的实现,LinkedList是List接口链表的实现。基于链表实现的方式使得LinkedList在插入和删除时更优于ArrayList,而随机访问则比ArrayList逊色些。</p><p> LinkedList实现所有可选的列表操作,并允许所有的元素包括null。</p><p> 除了实现 List 接口外,LinkedList 类还为在列表的开头及结尾 get、remove 和 insert 元素提供了统一的命名方法。这些操作允许将链接列表用作堆栈、队列或双端队列。</p><p> 此类实现 Deque 接口,为 add、poll 提供先进先出队列操作,以及其他堆栈和双端队列操作。</p><p> 所有操作都是按照双重链接列表的需要执行的。在列表中编索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。</p><p> 同时,与ArrayList一样此实现不是同步的。</p><p> (以上摘自JDK 6.0 API)。</p></blockquote><p>源码分析</p><h2 id="定义"><a href="#定义" class="headerlink" title="定义"></a>定义</h2><p> 首先我们先看LinkedList的定义:</p><pre><code>public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable 从这段代码中我们可以清晰地看出LinkedList继承AbstractSequentialList,实现List、Deque、Cloneable、Serializable。其中AbstractSequentialList提供了 List 接口的骨干实现,从而最大限度地减少了实现受“连续访问”数据存储(如链接列表)支持的此接口所需的工作,从而以减少实现List接口的复杂度。Deque一个线性 collection,支持在两端插入和移除元素,定义了双端队列的操作。</code></pre><h2 id="属性"><a href="#属性" class="headerlink" title="属性"></a>属性</h2><p>在LinkedList中提供了两个基本属性size、header。</p><p>private transient Entry<e> header = new Entry<e>(null, null, null);<br>private transient int size = 0;<br>其中size表示的LinkedList的大小,header表示链表的表头,Entry为节点对象。</e></e></p><pre><code>private static class Entry<E> { E element; //元素节点 Entry<E> next; //下一个元素 Entry<E> previous; //上一个元素 Entry(E element, Entry<E> next, Entry<E> previous) { this.element = element; this.next = next; this.previous = previous; }} 上面为Entry对象的源代码,Entry为LinkedList的内部类,它定义了存储的元素。该元素的前一个元素、后一个元素,这是典型的双向链表定义方式。</code></pre><h2 id="构造方法"><a href="#构造方法" class="headerlink" title="构造方法"></a>构造方法</h2><p>LinkedList提供了两个构造方法:LinkedList()和LinkedList(Collection<? extends E> c)。</p><pre><code>/** * 构造一个空列表。 */ public LinkedList() { header.next = header.previous = header; } /** * 构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列。 */ public LinkedList(Collection<? extends E> c) { this(); addAll(c); }</code></pre><p> LinkedList()构造一个空列表。里面没有任何元素,仅仅只是将header节点的前一个元素、后一个元素都指向自身。</p><p> LinkedList(Collection<? extends E> c): 构造一个包含指定 collection 中的元素的列表,这些元素按其 collection 的迭代器返回的顺序排列。该构造函数首先会调用LinkedList(),构造一个空列表,然后调用了addAll()方法将Collection中的所有元素添加到列表中。以下是addAll()的源代码:</p><pre><code>/** * 添加指定 collection 中的所有元素到此列表的结尾,顺序是指定 collection 的迭代器返回这些元素的顺序。 */ public boolean addAll(Collection<? extends E> c) { return addAll(size, c); }/** * 将指定 collection 中的所有元素从指定位置开始插入此列表。其中index表示在其中插入指定collection中第一个元素的索引 */public boolean addAll(int index, Collection<? extends E> c) { //若插入的位置小于0或者大于链表长度,则抛出IndexOutOfBoundsException异常 if (index < 0 || index > size) throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); Object[] a = c.toArray(); int numNew = a.length; //插入元素的个数 //若插入的元素为空,则返回false if (numNew == 0) return false; //modCount:在AbstractList中定义的,表示从结构上修改列表的次数 modCount++; //获取插入位置的节点,若插入的位置在size处,则是头节点,否则获取index位置处的节点 Entry<E> successor = (index == size ? header : entry(index)); //插入位置的前一个节点,在插入过程中需要修改该节点的next引用:指向插入的节点元素 Entry<E> predecessor = successor.previous; //执行插入动作 for (int i = 0; i < numNew; i++) { //构造一个节点e,这里已经执行了插入节点动作同时修改了相邻节点的指向引用 // Entry<E> e = new Entry<E>((E) a[i], successor, predecessor); //将插入位置前一个节点的下一个元素引用指向当前元素 predecessor.next = e; //修改插入位置的前一个节点,这样做的目的是将插入位置右移一位,保证后续的元素是插在该元素的后面,确保这些元素的顺序 predecessor = e; } successor.previous = predecessor; //修改容量大小 size += numNew; return true;} 在addAll()方法中,涉及到了两个方法,一个是entry(int index),该方法为LinkedList的私有方法,主要是用来查找index位置的节点元素。/** * 返回指定位置(若存在)的节点元素 */ private Entry<E> entry(int index) { if (index < 0 || index >= size) throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); //头部节点 Entry<E> e = header; //判断遍历的方向 if (index < (size >> 1)) { for (int i = 0; i <= index; i++) e = e.next; } else { for (int i = size; i > index; i--) e = e.previous; } return e; }</code></pre><p> 从该方法有两个遍历方向中我们也可以看出LinkedList是双向链表,这也是在构造方法中为什么需要将header的前、后节点均指向自己。</p><p> 如果对数据结构有点了解,对上面所涉及的内容应该问题,我们只需要清楚一点:LinkedList是双向链表,其余都迎刃而解。</p><p> 由于篇幅有限,下面将就LinkedList中几个常用的方法进行源码分析。</p><h2 id="增加方法"><a href="#增加方法" class="headerlink" title="增加方法"></a>增加方法</h2><pre><code> add(E e): 将指定元素添加到此列表的结尾。public boolean add(E e) { addBefore(e, header); return true; } 该方法调用addBefore方法,然后直接返回true,对于addBefore()而已,它为LinkedList的私有方法。private Entry<E> addBefore(E e, Entry<E> entry) { //利用Entry构造函数构建一个新节点 newEntry, Entry<E> newEntry = new Entry<E>(e, entry, entry.previous); //修改newEntry的前后节点的引用,确保其链表的引用关系是正确的 newEntry.previous.next = newEntry; newEntry.next.previous = newEntry; //容量+1 size++; //修改次数+1 modCount++; return newEntry; }</code></pre><p> 在addBefore方法中无非就是做了这件事:构建一个新节点newEntry,然后修改其前后的引用。</p><p> LinkedList还提供了其他的增加方法:</p><pre><code>add(int index, E element):在此列表中指定的位置插入指定的元素。addAll(Collection<? extends E> c):添加指定 collection 中的所有元素到此列表的结尾,顺序是指定 collection 的迭代器返回这些元素的顺序。addAll(int index, Collection<? extends E> c):将指定 collection 中的所有元素从指定位置开始插入此列表。AddFirst(E e): 将指定元素插入此列表的开头。addLast(E e): 将指定元素添加到此列表的结尾。</code></pre><h2 id="移除方法"><a href="#移除方法" class="headerlink" title="移除方法"></a>移除方法</h2><pre><code> remove(Object o):从此列表中移除首次出现的指定元素(如果存在)。该方法的源代码如下:public boolean remove(Object o) { if (o==null) { for (Entry<E> e = header.next; e != header; e = e.next) { if (e.element==null) { remove(e); return true; } } } else { for (Entry<E> e = header.next; e != header; e = e.next) { if (o.equals(e.element)) { remove(e); return true; } } } return false; }</code></pre><p> 该方法首先会判断移除的元素是否为null,然后迭代这个链表找到该元素节点,最后调用remove(Entry<e> e),remove(Entry<e> e)为私有方法,是LinkedList中所有移除方法的基础方法,如下:</e></e></p><pre><code>private E remove(Entry<E> e) { if (e == header) throw new NoSuchElementException(); //保留被移除的元素:要返回 E result = e.element; //将该节点的前一节点的next指向该节点后节点 e.previous.next = e.next; //将该节点的后一节点的previous指向该节点的前节点 //这两步就可以将该节点从链表从除去:在该链表中是无法遍历到该节点的 e.next.previous = e.previous; //将该节点归空 e.next = e.previous = null; e.element = null; size--; modCount++; return result; }</code></pre><p>其他的移除方法:</p><pre><code>clear(): 从此列表中移除所有元素。remove():获取并移除此列表的头(第一个元素)。remove(int index):移除此列表中指定位置处的元素。remove(Objec o):从此列表中移除首次出现的指定元素(如果存在)。removeFirst():移除并返回此列表的第一个元素。removeFirstOccurrence(Object o):从此列表中移除第一次出现的指定元素(从头部到尾部遍历列表时)。removeLast():移除并返回此列表的最后一个元素。removeLastOccurrence(Object o):从此列表中移除最后一次出现的指定元素(从头部到尾部遍历列表时)。</code></pre><h2 id="查找方法"><a href="#查找方法" class="headerlink" title="查找方法"></a>查找方法</h2><pre><code>对于查找方法的源码就没有什么好介绍了,无非就是迭代,比对,然后就是返回当前值。get(int index):返回此列表中指定位置处的元素。getFirst():返回此列表的第一个元素。getLast():返回此列表的最后一个元素。indexOf(Object o):返回此列表中首次出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。lastIndexOf(Object o):返回此列表中最后出现的指定元素的索引,如果此列表中不包含该元素,则返回 -1。</code></pre><h1 id="Queue"><a href="#Queue" class="headerlink" title="Queue"></a>Queue</h1><p>Queue接口定义了队列数据结构,元素是有序的(按插入顺序),先进先出。Queue接口相关的部分UML类图如下:</p><p><img src="https://upload-images.jianshu.io/upload_images/195193-bcff191213cf126a.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/578" alt="image"></p><h2 id="DeQueue"><a href="#DeQueue" class="headerlink" title="DeQueue"></a>DeQueue</h2><blockquote><p>DeQueue(Double-ended queue)为接口,继承了Queue接口,创建双向队列,灵活性更强,可以前向或后向迭代,在队头队尾均可心插入或删除元素。它的两个主要实现类是ArrayDeque和LinkedList。</p></blockquote><h2 id="ArrayDeque-(底层使用循环数组实现双向队列)"><a href="#ArrayDeque-(底层使用循环数组实现双向队列)" class="headerlink" title="ArrayDeque (底层使用循环数组实现双向队列)"></a>ArrayDeque (底层使用循环数组实现双向队列)</h2><p>创建</p><pre><code>public ArrayDeque() { // 默认容量为16 elements = new Object[16];}public ArrayDeque(int numElements) { // 指定容量的构造函数 allocateElements(numElements);}private void allocateElements(int numElements) { int initialCapacity = MIN_INITIAL_CAPACITY;// 最小容量为8 // Find the best power of two to hold elements. // Tests "<=" because arrays aren't kept full. // 如果要分配的容量大于等于8,扩大成2的幂(是为了维护头、尾下标值);否则使用最小容量8 if (numElements >= initialCapacity) { initialCapacity = numElements; initialCapacity |= (initialCapacity >>> 1); initialCapacity |= (initialCapacity >>> 2); initialCapacity |= (initialCapacity >>> 4); initialCapacity |= (initialCapacity >>> 8); initialCapacity |= (initialCapacity >>> 16); initialCapacity++; if (initialCapacity < 0) // Too many elements, must back off initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements } elements = new Object[initialCapacity]; }</code></pre><p>add操作</p><pre><code>add(E e) 调用 addLast(E e) 方法:public void addLast(E e) { if (e == null) throw new NullPointerException("e == null"); elements[tail] = e; // 根据尾索引,添加到尾端 // 尾索引+1,并与数组(length - 1)进行取‘&’运算,因为length是2的幂,所以(length-1)转换为2进制全是1, // 所以如果尾索引值 tail 小于等于(length - 1),那么‘&’运算后仍为 tail 本身;如果刚好比(length - 1)大1时, // ‘&’运算后 tail 便为0(即回到了数组初始位置)。正是通过与(length - 1)进行取‘&’运算来实现数组的双向循环。 // 如果尾索引和头索引重合了,说明数组满了,进行扩容。 if ((tail = (tail + 1) & (elements.length - 1)) == head) doubleCapacity();// 扩容为原来的2倍}addFirst(E e) 的实现:public void addFirst(E e) { if (e == null) throw new NullPointerException("e == null"); // 此处如果head为0,则-1(1111 1111 1111 1111 1111 1111 1111 1111)与(length - 1)进行取‘&’运算,结果必然是(length - 1),即回到了数组的尾部。 elements[head = (head - 1) & (elements.length - 1)] = e; // 如果尾索引和头索引重合了,说明数组满了,进行扩容 if (head == tail) doubleCapacity();}</code></pre><p>remove操作</p><pre><code>remove()方法最终都会调对应的poll()方法: public E poll() { return pollFirst(); } public E pollFirst() { int h = head; @SuppressWarnings("unchecked") E result = (E) elements[h]; // Element is null if deque empty if (result == null) return null; elements[h] = null; // Must null out slot // 头索引 + 1 head = (h + 1) & (elements.length - 1); return result; } public E pollLast() { // 尾索引 - 1 int t = (tail - 1) & (elements.length - 1); @SuppressWarnings("unchecked") E result = (E) elements[t]; if (result == null) return null; elements[t] = null; tail = t; return result; }</code></pre><p><img src="https://upload-images.jianshu.io/upload_images/195193-e36436dd0c750c3c.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/700" alt="image"></p><h2 id="PriorityQueue(底层用数组实现堆的结构)"><a href="#PriorityQueue(底层用数组实现堆的结构)" class="headerlink" title="PriorityQueue(底层用数组实现堆的结构)"></a>PriorityQueue(底层用数组实现堆的结构)</h2><blockquote><p>优先队列跟普通的队列不一样,普通队列是一种遵循FIFO规则的队列,拿数据的时候按照加入队列的顺序拿取。 而优先队列每次拿数据的时候都会拿出优先级最高的数据。</p><p>优先队列内部维护着一个堆,每次取数据的时候都从堆顶拿数据(堆顶的优先级最高),这就是优先队列的原理。</p></blockquote><p>add,添加方法</p><pre><code>public boolean add(E e) { return offer(e); // add方法内部调用offer方法}public boolean offer(E e) { if (e == null) // 元素为空的话,抛出NullPointerException异常 throw new NullPointerException(); modCount++; int i = size; if (i >= queue.length) // 如果当前用堆表示的数组已经满了,调用grow方法扩容 grow(i + 1); // 扩容 size = i + 1; // 元素个数+1 if (i == 0) // 堆还没有元素的情况 queue[0] = e; // 直接给堆顶赋值元素 else // 堆中已有元素的情况 siftUp(i, e); // 重新调整堆,从下往上调整,因为新增元素是加到最后一个叶子节点 return true;}private void siftUp(int k, E x) { if (comparator != null) // 比较器存在的情况下 siftUpUsingComparator(k, x); // 使用比较器调整 else // 比较器不存在的情况下 siftUpComparable(k, x); // 使用元素自身的比较器调整}private void siftUpUsingComparator(int k, E x) { while (k > 0) { // 一直循环直到父节点还存在 int parent = (k - 1) >>> 1; // 找到父节点索引,等同于(k - 1)/ 2 Object e = queue[parent]; // 获得父节点元素 // 新元素与父元素进行比较,如果满足比较器结果,直接跳出,否则进行调整 if (comparator.compare(x, (E) e) >= 0) break; queue[k] = e; // 进行调整,新位置的元素变成了父元素 k = parent; // 新位置索引变成父元素索引,进行递归操作 } queue[k] = x; // 新添加的元素添加到堆中}</code></pre><p><img src="https://upload-images.jianshu.io/upload_images/195193-be988ac1a1a415d1.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/670" alt="image"><br>poll,出队方法</p><pre><code>public E poll() { if (size == 0) return null; int s = --size; // 元素个数-1 modCount++; E result = (E) queue[0]; // 得到堆顶元素 E x = (E) queue[s]; // 最后一个叶子节点 queue[s] = null; // 最后1个叶子节点置空 if (s != 0) siftDown(0, x); // 从上往下调整,因为删除元素是删除堆顶的元素 return result;}private void siftDown(int k, E x) { if (comparator != null) // 比较器存在的情况下 siftDownUsingComparator(k, x); // 使用比较器调整 else // 比较器不存在的情况下 siftDownComparable(k, x); // 使用元素自身的比较器调整}private void siftDownUsingComparator(int k, E x) { int half = size >>> 1; // 只需循环节点个数的一般即可 while (k < half) { int child = (k << 1) + 1; // 得到父节点的左子节点索引,即(k * 2)+ 1 Object c = queue[child]; // 得到左子元素 int right = child + 1; // 得到父节点的右子节点索引 if (right < size && comparator.compare((E) c, (E) queue[right]) > 0) // 左子节点跟右子节点比较,取更大的值 c = queue[child = right]; if (comparator.compare(x, (E) c) <= 0) // 然后这个更大的值跟最后一个叶子节点比较 break; queue[k] = c; // 新位置使用更大的值 k = child; // 新位置索引变成子元素索引,进行递归操作 } queue[k] = x; // 最后一个叶子节点添加到合适的位置}</code></pre><p><img src="https://upload-images.jianshu.io/upload_images/195193-c88e7314648144da.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/680" alt="image"><br>remove,删除队列元素</p><pre><code>public boolean remove(Object o) { int i = indexOf(o); // 找到数据对应的索引 if (i == -1) // 不存在的话返回false return false; else { // 存在的话调用removeAt方法,返回true removeAt(i); return true; }}private E removeAt(int i) { modCount++; int s = --size; // 元素个数-1 if (s == i) // 如果是删除最后一个叶子节点 queue[i] = null; // 直接置空,删除即可,堆还是保持特质,不需要调整 else { // 如果是删除的不是最后一个叶子节点 E moved = (E) queue[s]; // 获得最后1个叶子节点元素 queue[s] = null; // 最后1个叶子节点置空 siftDown(i, moved); // 从上往下调整 if (queue[i] == moved) { // 如果从上往下调整完毕之后发现元素位置没变,从下往上调整 siftUp(i, moved); // 从下往上调整 if (queue[i] != moved) return moved; } } return null;}</code></pre><p>先执行 siftDown() 下滤过程:</p><p><img src="https://upload-images.jianshu.io/upload_images/195193-a64dbb5508a9c668.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/642" alt="image"></p><p>再执行 siftUp() 上滤过程:</p><p><img src="https://upload-images.jianshu.io/upload_images/195193-e9ad437213e69b07.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/633" alt="image"></p><h2 id="总结和同步的问题"><a href="#总结和同步的问题" class="headerlink" title="总结和同步的问题"></a>总结和同步的问题</h2><p>1、jdk内置的优先队列PriorityQueue内部使用一个堆维护数据,每当有数据add进来或者poll出去的时候会对堆做从下往上的调整和从上往下的调整。</p><p>2、PriorityQueue不是一个线程安全的类,如果要在多线程环境下使用,可以使用 PriorityBlockingQueue 这个优先阻塞队列。其中add、poll、remove方法都使用 ReentrantLock 锁来保持同步,take() 方法中如果元素为空,则会一直保持阻塞。</p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java集合框架 </tag>
</tags>
</entry>
<entry>
<title>Java集合详解1:ArrayList,Vector与Stack</title>
<link href="/2018/05/08/collection1/"/>
<url>/2018/05/08/collection1/</url>
<content type="html"><![CDATA[<p>本文非常详尽地介绍了Java中的三个集合类<br>ArrayList,Vector与Stack</p><p>”Java集合详解系列“是我在完成Java基础篇的系列博客后准备开始写的新系列。</p><p>之前的Java基础系列博客首发于我的个人博客:<a href="https://h2pl.github.io/">https://h2pl.github.io/</a></p><p>在这个分类中,将会写写Java中的集合。集合是Java中非常重要而且基础的内容,因为任何数据必不可少的就是该数据是如何存储的,集合的作用就是以一定的方式组织、存储数据。</p><p>之所以把这三个集合类放在一起讲解,是因为这三个集合类的底层都是数组实现(Stack继承自vector)并且比较常用。<br>后面还会另外讲底层是链表实现的linkedlist和queue;</p><p>今天我们来探索一下ArrayList和Vector,以及Stack的源码</p><p>具体代码在我的GitHub中可以找到</p><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p><p>喜欢的话麻烦star一下哈</p><p>文章首发于我的个人博客:</p><p><a href="https://h2pl.github.io/2018/05/08/collection1">https://h2pl.github.io/2018/05/08/collection1</a></p><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:<a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p><p>我的个人博客主要发原创文章,也欢迎浏览<br><a href="https://h2pl.github.io/">https://h2pl.github.io/</a></p><a id="more"></a><pre><code>//一般讨论集合类无非就是。这里的两种数组类型更是如此// 1底层数据结构// 2增删改查方式// 3初始容量,扩容方式,扩容时机。// 4线程安全与否// 5是否允许空,是否允许重复,是否有序 </code></pre><h1 id="ArrayList"><a href="#ArrayList" class="headerlink" title="ArrayList"></a>ArrayList</h1><p>ArrayList概述</p><p> ArrayList是实现List接口的动态数组,所谓动态就是它的大小是可变的。实现了所有可选列表操作,并允许包括 null 在内的所有元素。除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。</p><p> 每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。默认初始容量为10。随着ArrayList中元素的增加,它的容量也会不断的自动增长。</p><p> 在每次添加新的元素时,ArrayList都会检查是否需要进行扩容操作,扩容操作带来数据向新数组的重新拷贝,所以如果我们知道具体业务数据量,在构造ArrayList时可以给ArrayList指定一个初始容量,这样就会减少扩容时数据的拷贝问题。当然在添加大量元素前,应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。</p><p> 注意,ArrayList实现不是同步的。如果多个线程同时访问一个ArrayList实例,而其中至少一个线程从结构上修改了列表,那么它必须保持外部同步。所以为了保证同步,最好的办法是在创建时完成,以防止意外对列表进行不同步的访问:</p><pre><code>List list = Collections.synchronizedList(new ArrayList(...)); </code></pre><h2 id="底层数据结构"><a href="#底层数据结构" class="headerlink" title="底层数据结构"></a>底层数据结构</h2><p> ArrayList的底层是一个object数组,并且由trasient修饰。</p><pre><code>//transient Object[] elementData; //</code></pre><p>non-private to simplify nested class access<br>//ArrayList底层数组不会参与序列化,而是使用另外的序列化方式。</p><p>//使用writeobject方法进行序列化,具体为什么这么做欢迎查看我之前的关于序列化的文章</p><p>//总结一下就是只复制数组中有值的位置,其他未赋值的位置不进行序列化,可以节省空间。</p><pre><code>// private void writeObject(java.io.ObjectOutputStream s)// throws java.io.IOException{// // Write out element count, and any hidden stuff// int expectedModCount = modCount;// s.defaultWriteObject();//// // Write out size as capacity for behavioural compatibility with clone()// s.writeInt(size);//// // Write out all elements in the proper order.// for (int i=0; i<size; i++) {// s.writeObject(elementData[i]);// }//// if (modCount != expectedModCount) {// throw new ConcurrentModificationException();// }// }</code></pre><h2 id="增删改查"><a href="#增删改查" class="headerlink" title="增删改查"></a>增删改查</h2><pre><code>//增删改查</code></pre><p>添加元素时,首先判断索引是否合法,然后检测是否需要扩容,最后使用System.arraycopy方法来完成数组的复制。</p><p>这个方法无非就是使用System.arraycopy()方法将C集合(先准换为数组)里面的数据复制到elementData数组中。这里就稍微介绍下System.arraycopy(),因为下面还将大量用到该方法</p><p>。该方法的原型为:</p><pre><code>public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)。</code></pre><p>它的根本目的就是进行数组元素的复制。即从指定源数组中复制一个数组,复制从指定的位置开始,到目标数组的指定位置结束。</p><p>将源数组src从srcPos位置开始复制到dest数组中,复制长度为length,数据从dest的destPos位置开始粘贴。</p><pre><code>// public void add(int index, E element) {// rangeCheckForAdd(index);//// ensureCapacityInternal(size + 1); // Increments modCount!!// System.arraycopy(elementData, index, elementData, index + 1,// size - index);// elementData[index] = element;// size++;// }//</code></pre><p>删除元素时,同样判断索引是否和法,删除的方式是把被删除元素右边的元素左移,方法同样是使用System.arraycopy进行拷贝。</p><pre><code>// public E remove(int index) {// rangeCheck(index);//// modCount++;// E oldValue = elementData(index);//// int numMoved = size - index - 1;// if (numMoved > 0)// System.arraycopy(elementData, index+1, elementData, index,// numMoved);// elementData[--size] = null; // clear to let GC do its work//// return oldValue;// }</code></pre><p>ArrayList提供一个清空数组的办法,方法是将所有元素置为null,这样就可以让GC自动回收掉没有被引用的元素了。</p><pre><code>//// /**// * Removes all of the elements from this list. The list will// * be empty after this call returns.// */// public void clear() {// modCount++;//// // clear to let GC do its work// for (int i = 0; i < size; i++)// elementData[i] = null;//// size = 0;// }</code></pre><p>修改元素时,只需要检查下标即可进行修改操作。</p><pre><code>// public E set(int index, E element) {// rangeCheck(index);//// E oldValue = elementData(index);// elementData[index] = element;// return oldValue;// }//// public E get(int index) {// rangeCheck(index);//// return elementData(index);// }//</code></pre><p>上述方法都使用了rangeCheck方法,其实就是简单地检查下标而已。</p><pre><code>// private void rangeCheck(int index) {// if (index >= size)// throw new IndexOutOfBoundsException(outOfBoundsMsg(index));// }</code></pre><h2 id="modCount"><a href="#modCount" class="headerlink" title="modCount"></a>modCount</h2><pre><code>// protected transient int modCount = 0;</code></pre><p>由以上代码可以看出,在一个迭代器初始的时候会赋予它调用这个迭代器的对象的mCount,如何在迭代器遍历的过程中,一旦发现这个对象的mcount和迭代器中存储的mcount不一样那就抛异常 </p><blockquote><p>好的,下面是这个的完整解释<br>Fail-Fast 机制<br>我们知道 java.util.ArrayList 不是线程安全的,ArrayList,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。</p><p>这一策略在源码中的实现是通过 modCount 域,modCount 顾名思义就是修改次数,对ArrayList 内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的 expectedModCount。</p><p>在迭代过程中,判断 modCount 跟 expectedModCount 是否相等,如果不相等就表示已经有其他线程修改了 ArrayList。</p><p>所以在这里和大家建议,当大家遍历那些非线程安全的数据结构时,尽量使用迭代器</p></blockquote><h2 id="初始容量和扩容方式"><a href="#初始容量和扩容方式" class="headerlink" title="初始容量和扩容方式"></a>初始容量和扩容方式</h2><p>初始容量是10,下面是扩容方法。<br>首先先取</p><pre><code>// private static final int DEFAULT_CAPACITY = 10;扩容发生在add元素时,传入当前元素容量加一 public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true;}这里给出初始化时的数组private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};这说明:如果数组还是初始数组,那么最小的扩容大小就是size+1和初始容量中较大的一个,初始容量为10。因为addall方法也会调用该函数,所以此时需要做判断。private void ensureCapacityInternal(int minCapacity) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); } ensureExplicitCapacity(minCapacity);}//开始精确地扩容private void ensureExplicitCapacity(int minCapacity) { modCount++; // overflow-conscious code 如果此时扩容容量大于数组长度吗,执行grow,否则不执行。 if (minCapacity - elementData.length > 0) grow(minCapacity);}</code></pre><p>真正执行扩容的方法grow</p><p>扩容方式是让新容量等于旧容量的1.5被。</p><p>当新容量大于最大数组容量时,执行大数扩容</p><pre><code>// private void grow(int minCapacity) {// // overflow-conscious code// int oldCapacity = elementData.length;// int newCapacity = oldCapacity + (oldCapacity >> 1);// if (newCapacity - minCapacity < 0)// newCapacity = minCapacity;// if (newCapacity - MAX_ARRAY_SIZE > 0)// newCapacity = hugeCapacity(minCapacity);// // minCapacity is usually close to size, so this is a win:// elementData = Arrays.copyOf(elementData, newCapacity);// }</code></pre><p>当新容量大于最大数组长度,有两种情况,一种是溢出,抛异常,一种是没溢出,返回整数的最大值。</p><pre><code>private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;}</code></pre><p>在这里有一个疑问,为什么每次扩容处理会是1.5倍,而不是2.5、3、4倍呢?通过google查找,发现1.5倍的扩容是最好的倍数。因为一次性扩容太大(例如2.5倍)可能会浪费更多的内存(1.5倍最多浪费33%,而2.5被最多会浪费60%,3.5倍则会浪费71%……)。但是一次性扩容太小,需要多次对数组重新分配内存,对性能消耗比较严重。所以1.5倍刚刚好,既能满足性能需求,也不会造成很大的内存消耗。</p><p> 处理这个ensureCapacity()这个扩容数组外,ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。它可以通过trimToSize()方法来实现。该方法可以最小化ArrayList实例的存储量。</p><pre><code>public void trimToSize() { modCount++; int oldCapacity = elementData.length; if (size < oldCapacity) { elementData = Arrays.copyOf(elementData, size); }}</code></pre><h2 id="线程安全"><a href="#线程安全" class="headerlink" title="线程安全"></a>线程安全</h2><p>ArrayList是线程不安全的。在其迭代器iteator中,如果有多线程操作导致modcount改变,会执行fastfail。抛出异常。</p><pre><code>final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException();}</code></pre><h1 id="Vector"><a href="#Vector" class="headerlink" title="Vector"></a>Vector</h1><p>Vector简介</p><p>Vector可以实现可增长的对象数组。与数组一样,它包含可以使用整数索引进行访问的组件。不过,Vector的大小是可以增加或者减小的,以便适应创建Vector后进行添加或者删除操作。</p><p>Vector实现List接口,继承AbstractList类,所以我们可以将其看做队列,支持相关的添加、删除、修改、遍历等功能。</p><p>Vector实现RandmoAccess接口,即提供了随机访问功能,提供提供快速访问功能。在Vector我们可以直接访问元素。</p><p>Vector 实现了Cloneable接口,支持clone()方法,可以被克隆。</p><p>vector底层数组不加transient,序列化时会全部复制</p><pre><code> protected Object[] elementData;// private void writeObject(java.io.ObjectOutputStream s)// throws java.io.IOException {// final java.io.ObjectOutputStream.PutField fields = s.putFields();// final Object[] data;// synchronized (this) {// fields.put("capacityIncrement", capacityIncrement);// fields.put("elementCount", elementCount);// data = elementData.clone();// }// fields.put("elementData", data);// s.writeFields();// }</code></pre><p>Vector除了iterator外还提供Enumeration枚举方法,不过现在比较过时。</p><pre><code>// public Enumeration<E> elements() {// return new Enumeration<E>() {// int count = 0;//// public boolean hasMoreElements() {// return count < elementCount;// }//// public E nextElement() {// synchronized (Vector.this) {// if (count < elementCount) {// return elementData(count++);// }// }// throw new NoSuchElementException("Vector Enumeration");// }// };// }//</code></pre><h2 id="增删改查-1"><a href="#增删改查-1" class="headerlink" title="增删改查"></a>增删改查</h2><p>vector的增删改查既提供了自己的实现,也继承了abstractList抽象类的部分方法。<br>下面的方法是vector自己实现的。</p><pre><code>//// public synchronized E elementAt(int index) {// if (index >= elementCount) {// throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);// }//// return elementData(index);// }////// public synchronized void setElementAt(E obj, int index) {// if (index >= elementCount) {// throw new ArrayIndexOutOfBoundsException(index + " >= " +// elementCount);// }// elementData[index] = obj;// }//// public synchronized void removeElementAt(int index) {// modCount++;// if (index >= elementCount) {// throw new ArrayIndexOutOfBoundsException(index + " >= " +// elementCount);// }// else if (index < 0) {// throw new ArrayIndexOutOfBoundsException(index);// }// int j = elementCount - index - 1;// if (j > 0) {// System.arraycopy(elementData, index + 1, elementData, index, j);// }// elementCount--;// elementData[elementCount] = null; /* to let gc do its work */// }// public synchronized void insertElementAt(E obj, int index) {// modCount++;// if (index > elementCount) {// throw new ArrayIndexOutOfBoundsException(index// + " > " + elementCount);// }// ensureCapacityHelper(elementCount + 1);// System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);// elementData[index] = obj;// elementCount++;// }//// public synchronized void addElement(E obj) {// modCount++;// ensureCapacityHelper(elementCount + 1);// elementData[elementCount++] = obj;// }</code></pre><h2 id="初始容量和扩容"><a href="#初始容量和扩容" class="headerlink" title="初始容量和扩容"></a>初始容量和扩容</h2><p>扩容方式与ArrayList基本一样,但是扩容时不是1.5倍扩容,而是有一个扩容增量。</p><pre><code>// protected int elementCount;// protected int capacityIncrement;////// }// public Vector() {// this(10);// }</code></pre><p>capacityIncrement:向量的大小大于其容量时,容量自动增加的量。如果在创建Vector时,指定了capacityIncrement的大小;则,每次当Vector中动态数组容量增加时>,增加的大小都是capacityIncrement。如果容量的增量小于等于零,则每次需要增大容量时,向量的容量将增大一倍。</p><pre><code>// public synchronized void ensureCapacity(int minCapacity) {// if (minCapacity > 0) {// modCount++;// ensureCapacityHelper(minCapacity);// }// }// private void ensureCapacityHelper(int minCapacity) {// // overflow-conscious code// if (minCapacity - elementData.length > 0)// grow(minCapacity);// }//// private void grow(int minCapacity) {// // overflow-conscious code// int oldCapacity = elementData.length;// int newCapacity = oldCapacity + ((capacityIncrement > 0) ?// capacityIncrement : oldCapacity);// if (newCapacity - minCapacity < 0)// newCapacity = minCapacity;// if (newCapacity - MAX_ARRAY_SIZE > 0)// newCapacity = hugeCapacity(minCapacity);// elementData = Arrays.copyOf(elementData, newCapacity);// }</code></pre><h2 id="线程安全-1"><a href="#线程安全-1" class="headerlink" title="线程安全"></a>线程安全</h2><p>vector大部分方法都使用了synchronized修饰符,所以他是线层安全的集合类。</p><h1 id="Stack"><a href="#Stack" class="headerlink" title="Stack"></a>Stack</h1><p>在Java中Stack类表示后进先出(LIFO)的对象堆栈。栈是一种非常常见的数据结构,它采用典型的先进后出的操作方式完成的。每一个栈都包含一个栈顶,每次出栈是将栈顶的数据取出,如下:</p><p><img src="https://images0.cnblogs.com/blog/381060/201407/091242265826653.jpg" alt="image"></p><p>Stack通过五个操作对Vector进行扩展,允许将向量视为堆栈。这个五个操作如下:</p><blockquote><p>empty()</p><p>测试堆栈是否为空。</p><p>peek()</p><p>查看堆栈顶部的对象,但不从堆栈中移除它。</p><p>pop()</p><p>移除堆栈顶部的对象,并作为此函数的值返回该对象。</p><p>push(E item)</p><p>把项压入堆栈顶部。</p><p>search(Object o)</p><p>返回对象在堆栈中的位置,以 1 为基数。</p></blockquote><p>Stack继承Vector,他对Vector进行了简单的扩展:</p><p>public class Stack<e> extends Vector<e><br> Stack的实现非常简单,仅有一个构造方法,五个实现方法(从Vector继承而来的方法不算与其中),同时其实现的源码非常简单</e></e></p><pre><code>/** * 构造函数 */public Stack() {}/** * push函数:将元素存入栈顶 */public E push(E item) { // 将元素存入栈顶。 // addElement()的实现在Vector.java中 addElement(item); return item;}/** * pop函数:返回栈顶元素,并将其从栈中删除 */public synchronized E pop() { E obj; int len = size(); obj = peek(); // 删除栈顶元素,removeElementAt()的实现在Vector.java中 removeElementAt(len - 1); return obj;}/** * peek函数:返回栈顶元素,不执行删除操作 */public synchronized E peek() { int len = size(); if (len == 0) throw new EmptyStackException(); // 返回栈顶元素,elementAt()具体实现在Vector.java中 return elementAt(len - 1);}/** * 栈是否为空 */public boolean empty() { return size() == 0;}/** * 查找“元素o”在栈中的位置:由栈底向栈顶方向数 */public synchronized int search(Object o) { // 获取元素索引,elementAt()具体实现在Vector.java中 int i = lastIndexOf(o); if (i >= 0) { return size() - i; } return -1;}</code></pre><p>Stack的源码很多都是基于Vector,所以这里不再累述</p><h1 id="区别"><a href="#区别" class="headerlink" title="区别"></a>区别</h1><p>ArrayList的优缺点</p><p>从上面的几个过程总结一下ArrayList的优缺点。ArrayList的优点如下:</p><blockquote><p>1、ArrayList底层以数组实现,是一种随机访问模式,再加上它实现了RandomAccess接口,因此查找也就是get的时候非常快</p><p>2、ArrayList在顺序添加一个元素的时候非常方便,只是往数组里面添加了一个元素而已</p></blockquote><p>不过ArrayList的缺点也十分明显:</p><blockquote><p>1、删除元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能</p><p>2、插入元素的时候,涉及到一次元素复制,如果要复制的元素很多,那么就会比较耗费性能</p><p>因此,ArrayList比较适合顺序添加、随机访问的场景。</p></blockquote><p>ArrayList和Vector的区别</p><blockquote><p>ArrayList是线程非安全的,这很明显,因为ArrayList中所有的方法都不是同步的,在并发下一定会出现线程安全问题。那么我们想要使用ArrayList并且让它线程安全怎么办?一个方法是用Collections.synchronizedList方法把你的ArrayList变成一个线程安全的List,比如:</p></blockquote><pre><code>List<String> synchronizedList = Collections.synchronizedList(list);synchronizedList.add("aaa");synchronizedList.add("bbb");for (int i = 0; i < synchronizedList.size(); i++){ System.out.println(synchronizedList.get(i));}</code></pre><p>另一个方法就是Vector,它是ArrayList的线程安全版本,其实现90%和ArrayList都完全一样,区别在于:</p><blockquote><p>1、Vector是线程安全的,ArrayList是线程非安全的</p><p>2、Vector可以指定增长因子,如果该增长因子指定了,那么扩容的时候会每次新的数组大小会在原数组的大小基础上加上增长因子;如果不指定增长因子,那么就给原数组大小*2,源代码是这样的:</p></blockquote><pre><code>int newCapacity = oldCapacity + ((capacityIncrement > 0) ? capacityIncrement : oldCapacity);</code></pre>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java集合框架 </tag>
</tags>
</entry>
<entry>
<title>Java基础20:Java8新特性终极指南</title>
<link href="/2018/05/07/javase20/"/>
<url>/2018/05/07/javase20/</url>
<content type="html"><![CDATA[<p>毫无疑问,Java 8发行版是自Java 5(发行于2004,已经过了相当一段时间了)以来最具革命性的版本。Java 8 为Java语言、编译器、类库、开发工具与JVM(Java虚拟机)带来了大量新特性。在这篇教程中,我们将一一探索这些变化,并用真实的例子说明它们适用的场景。</p><p>本文由以下几部分组成,它们分别涉及到Java平台某一特定方面的内容:</p><p>Java语言<br>编译器<br>类库<br>工具<br>Java运行时(JVM)</p><p>本文参考<a href="http://www.importnew.com/11908.html" target="_blank" rel="noopener">http://www.importnew.com/11908.html</a></p><p>具体代码在我的GitHub中可以找到</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>喜欢的话麻烦点一下星哈谢谢。</p><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/05/06/javase20">https://h2pl.github.io/2018/05/06/javase20</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><blockquote><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p></blockquote><a id="more"></a><p>这是一个Java8新增特性的总结图。接下来让我们一次实践一下这些新特性吧</p><p><img src="http://7xsskq.com1.z0.glb.clouddn.com/blog/Java8-features.png" alt="image"></p><h1 id="Java语言新特性"><a href="#Java语言新特性" class="headerlink" title="Java语言新特性"></a>Java语言新特性</h1><h2 id="Lambda表达式"><a href="#Lambda表达式" class="headerlink" title="Lambda表达式"></a>Lambda表达式</h2><p>Lambda表达式(也称为闭包)是整个Java 8发行版中最受期待的在Java语言层面上的改变,Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中),或者把代码看成数据:函数式程序员对这一概念非常熟悉。在JVM平台上的很多语言(Groovy,Scala,……)从一开始就有Lambda,但是Java程序员不得不使用毫无新意的匿名类来代替lambda。</p><p>关于Lambda设计的讨论占用了大量的时间与社区的努力。可喜的是,最终找到了一个平衡点,使得可以使用一种即简洁又紧凑的新方式来构造Lambdas。在最简单的形式中,一个lambda可以由用逗号分隔的参数列表、–>符号与函数体三部分表示。例如:</p><pre><code>Arrays.asList( "a", "b", "d" ).forEach( e -> System.out.println( e ) );</code></pre><p>请注意参数e的类型是由编译器推测出来的。同时,你也可以通过把参数类型与参数包括在括号中的形式直接给出参数的类型:</p><pre><code>Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.println( e ) );</code></pre><p>在某些情况下lambda的函数体会更加复杂,这时可以把函数体放到在一对花括号中,就像在Java中定义普通函数一样。例如:</p><pre><code>Arrays.asList( "a", "b", "d" ).forEach( e -> { System.out.print( e ); System.out.print( e );} );</code></pre><p>Lambda可以引用类的成员变量与局部变量(如果这些变量不是final的话,它们会被隐含的转为final,这样效率更高)。例如,下面两个代码片段是等价的:</p><pre><code>String separator = ",";Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.print( e + separator ) );</code></pre><p>和:</p><pre><code>final String separator = ",";Arrays.asList( "a", "b", "d" ).forEach( ( String e ) -> System.out.print( e + separator ) );</code></pre><p>Lambda可能会返回一个值。返回值的类型也是由编译器推测出来的。如果lambda的函数体只有一行的话,那么没有必要显式使用return语句。下面两个代码片段是等价的:</p><pre><code>Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> e1.compareTo( e2 ) );</code></pre><p>和:</p><pre><code>Arrays.asList( "a", "b", "d" ).sort( ( e1, e2 ) -> { int result = e1.compareTo( e2 ); return result;} );</code></pre><p>语言设计者投入了大量精力来思考如何使现有的函数友好地支持lambda。</p><p>最终采取的方法是:增加函数式接口的概念。函数式接口就是一个具有一个方法的普通接口。像这样的接口,可以被隐式转换为lambda表达式。</p><p>java.lang.Runnable与java.util.concurrent.Callable是函数式接口最典型的两个例子。</p><p>在实际使用过程中,函数式接口是容易出错的:如有某个人在接口定义中增加了另一个方法,这时,这个接口就不再是函数式的了,并且编译过程也会失败。</p><p>为了克服函数式接口的这种脆弱性并且能够明确声明接口作为函数式接口的意图,Java8增加了一种特殊的注解@FunctionalInterface(Java8中所有类库的已有接口都添加了@FunctionalInterface注解)。让我们看一下这种函数式接口的定义:</p><p>@FunctionalInterface<br>public interface Functional {<br> void method();<br>}<br>需要记住的一件事是:默认方法与静态方法并不影响函数式接口的契约,可以任意使用:</p><p>@FunctionalInterface<br>public interface FunctionalDefaultMethods {<br> void method();</p><pre><code>default void defaultMethod() { } </code></pre><p>}<br>Lambda是Java 8最大的卖点。它具有吸引越来越多程序员到Java平台上的潜力,并且能够在纯Java语言环境中提供一种优雅的方式来支持函数式编程。更多详情可以参考官方文档。</p><p>下面看一个例子:</p><pre><code>public class lambda和函数式编程 { @Test public void test1() { List names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, new Comparator<String>() { @Override public int compare(String a, String b) { return b.compareTo(a); } }); System.out.println(Arrays.toString(names.toArray())); } @Test public void test2() { List<String> names = Arrays.asList("peter", "anna", "mike", "xenia"); Collections.sort(names, (String a, String b) -> { return b.compareTo(a); }); Collections.sort(names, (String a, String b) -> b.compareTo(a)); Collections.sort(names, (a, b) -> b.compareTo(a)); System.out.println(Arrays.toString(names.toArray())); }} static void add(double a,String b) { System.out.println(a + b); } @Test public void test5() { D d = (a,b) -> add(a,b);// interface D {// void get(int i,String j);// } //这里要求,add的两个参数和get的两个参数吻合并且返回类型也要相等,否则报错// static void add(double a,String b) {// System.out.println(a + b);// } } @FunctionalInterface interface D { void get(int i,String j); }</code></pre><h2 id="函数式接口"><a href="#函数式接口" class="headerlink" title="函数式接口"></a>函数式接口</h2><p>所谓的函数式接口就是只有一个抽象方法的接口,注意这里说的是抽象方法,因为Java8中加入了默认方法的特性,但是函数式接口是不关心接口中有没有默认方法的。 一般函数式接口可以使用@FunctionalInterface注解的形式来标注表示这是一个函数式接口,该注解标注与否对函数式接口没有实际的影响, 不过一般还是推荐使用该注解,就像使用@Override注解一样。</p><p>lambda表达式是如何符合 Java 类型系统的?每个lambda对应于一个给定的类型,用一个接口来说明。而这个被称为函数式接口(functional interface)的接口必须仅仅包含一个抽象方法声明。每个那个类型的lambda表达式都将会被匹配到这个抽象方法上。因此默认的方法并不是抽象的,你可以给你的函数式接口自由地增加默认的方法。</p><p>我们可以使用任意的接口作为lambda表达式,只要这个接口只包含一个抽象方法。为了保证你的接口满足需求,你需要增加@FunctionalInterface注解。编译器知道这个注解,一旦你试图给这个接口增加第二个抽象方法声明时,它将抛出一个编译器错误。</p><p>下面举几个例子</p><pre><code>public class 函数式接口使用 { @FunctionalInterface interface A { void say(); default void talk() { } } @Test public void test1() { A a = () -> System.out.println("hello"); a.say(); } @FunctionalInterface interface B { void say(String i); } public void test2() { //下面两个是等价的,都是通过B接口来引用一个方法,而方法可以直接使用::来作为方法引用 B b = System.out::println; B b1 = a -> Integer.parseInt("s");//这里的a其实换成别的也行,只是将方法传给接口作为其方法实现 B b2 = Integer::valueOf;//i与方法传入参数的变量类型一直时,可以直接替换 B b3 = String::valueOf; //B b4 = Integer::parseInt;类型不符,无法使用 } @FunctionalInterface interface C { int say(String i); } public void test3() { C c = Integer::parseInt;//方法参数和接口方法的参数一样,可以替换。 int i = c.say("1"); //当我把C接口的int替换为void时就会报错,因为返回类型不一致。 System.out.println(i); //综上所述,lambda表达式提供了一种简便的表达方式,可以将一个方法传到接口中。 //函数式接口是只提供一个抽象方法的接口,其方法由lambda表达式注入,不需要写实现类, //也不需要写匿名内部类,可以省去很多代码,比如实现runnable接口。 //函数式编程就是指把方法当做一个参数或引用来进行操作。除了普通方法以外,静态方法,构造方法也是可以这样操作的。 }}</code></pre><p>请记住如果@FunctionalInterface 这个注解被遗漏,此代码依然有效。</p><h2 id="方法引用"><a href="#方法引用" class="headerlink" title="方法引用"></a>方法引用</h2><p>Lambda表达式和方法引用</p><p>有了函数式接口之后,就可以使用Lambda表达式和方法引用了。其实函数式接口的表中的函数描述符就是Lambda表达式,在函数式接口中Lambda表达式相当于匿名内部类的效果。 举个简单的例子:</p><p>public class TestLambda {</p><pre><code>public static void execute(Runnable runnable) { runnable.run();}public static void main(String[] args) { //Java8之前 execute(new Runnable() { @Override public void run() { System.out.println("run"); } }); //使用Lambda表达式 execute(() -> System.out.println("run"));}</code></pre><p>}</p><p>可以看到,相比于使用匿名内部类的方式,Lambda表达式可以使用更少的代码但是有更清晰的表述。注意,Lambda表达式也不是完全等价于匿名内部类的, 两者的不同点在于this的指向和本地变量的屏蔽上。</p><p>方法引用可以看作Lambda表达式的更简洁的一种表达形式,使用::操作符,方法引用主要有三类:</p><pre><code>指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt);指向任意类型实例方法的方法引用(例如String的length方法,写作String::length);指向现有对象的实例方法的方法引用(例如假设你有一个本地变量localVariable用于存放Variable类型的对象,它支持实例方法getValue,那么可以写成localVariable::getValue)。</code></pre><p>举个方法引用的简单的例子:</p><pre><code>Function<String, Integer> stringToInteger = (String s) -> Integer.parseInt(s);</code></pre><p>//使用方法引用</p><pre><code>Function<String, Integer> stringToInteger = Integer::parseInt;</code></pre><p>方法引用中还有一种特殊的形式,构造函数引用,假设一个类有一个默认的构造函数,那么使用方法引用的形式为:</p><pre><code>Supplier<SomeClass> c1 = SomeClass::new;SomeClass s1 = c1.get();</code></pre><p>//等价于</p><pre><code>Supplier<SomeClass> c1 = () -> new SomeClass();SomeClass s1 = c1.get();</code></pre><p>如果是构造函数有一个参数的情况:</p><pre><code>Function<Integer, SomeClass> c1 = SomeClass::new;SomeClass s1 = c1.apply(100);</code></pre><p>//等价于</p><pre><code>Function<Integer, SomeClass> c1 = i -> new SomeClass(i);SomeClass s1 = c1.apply(100);</code></pre><h2 id="接口的默认方法"><a href="#接口的默认方法" class="headerlink" title="接口的默认方法"></a>接口的默认方法</h2><p>Java 8 使我们能够使用default 关键字给接口增加非抽象的方法实现。这个特性也被叫做 扩展方法(Extension Methods)。如下例所示:</p><pre><code>public class 接口的默认方法 { class B implements A {// void a(){}实现类方法不能重名 } interface A { //可以有多个默认方法 public default void a(){ System.out.println("a"); } public default void b(){ System.out.println("b"); } //报错static和default不能同时使用// public static default void c(){// System.out.println("c");// } } public void test() { B b = new B(); b.a(); }}</code></pre><p>默认方法出现的原因是为了对原有接口的扩展,有了默认方法之后就不怕因改动原有的接口而对已经使用这些接口的程序造成的代码不兼容的影响。 在Java8中也对一些接口增加了一些默认方法,比如Map接口等等。一般来说,使用默认方法的场景有两个:可选方法和行为的多继承。</p><p>默认方法的使用相对来说比较简单,唯一要注意的点是如何处理默认方法的冲突。关于如何处理默认方法的冲突可以参考以下三条规则:</p><p>类中的方法优先级最高。类或父类中声明的方法的优先级高于任何声明为默认方法的优先级。</p><p>如果无法依据第一条规则进行判断,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口。即如果B继承了A,那么B就比A更具体。</p><p>最后,如果还是无法判断,继承了多个接口的类必须通过显式覆盖和调用期望的方法,显式地选择使用哪一个默认方法的实现。那么如何显式地指定呢:</p><pre><code>public class C implements B, A { public void hello() { B.super().hello(); }}</code></pre><p>使用X.super.m(..)显式地调用希望调用的方法。</p><p>Java 8用默认方法与静态方法这两个新概念来扩展接口的声明。默认方法使接口有点像Traits(Scala中特征(trait)类似于Java中的Interface,但它可以包含实现代码,也就是目前Java8新增的功能),但与传统的接口又有些不一样,它允许在已有的接口中添加新方法,而同时又保持了与旧版本代码的兼容性。</p><p>默认方法与抽象方法不同之处在于抽象方法必须要求实现,但是默认方法则没有这个要求。相反,每个接口都必须提供一个所谓的默认实现,这样所有的接口实现者将会默认继承它(如果有必要的话,可以覆盖这个默认实现)。让我们看看下面的例子:</p><pre><code>private interface Defaulable { // Interfaces now allow default methods, the implementer may or // may not implement (override) them. default String notRequired() { return "Default implementation"; } }private static class DefaultableImpl implements Defaulable {}private static class OverridableImpl implements Defaulable { @Override public String notRequired() { return "Overridden implementation"; }}</code></pre><p>Defaulable接口用关键字default声明了一个默认方法notRequired(),Defaulable接口的实现者之一DefaultableImpl实现了这个接口,并且让默认方法保持原样。Defaulable接口的另一个实现者OverridableImpl用自己的方法覆盖了默认方法。</p><p>Java 8带来的另一个有趣的特性是接口可以声明(并且可以提供实现)静态方法。例如:</p><pre><code>private interface DefaulableFactory { // Interfaces now allow static methods static Defaulable create( Supplier< Defaulable > supplier ) { return supplier.get(); }}</code></pre><p>下面的一小段代码片段把上面的默认方法与静态方法黏合到一起。</p><pre><code>public static void main( String[] args ) { Defaulable defaulable = DefaulableFactory.create( DefaultableImpl::new ); System.out.println( defaulable.notRequired() ); defaulable = DefaulableFactory.create( OverridableImpl::new ); System.out.println( defaulable.notRequired() );}</code></pre><p>这个程序的控制台输出如下:</p><p>Default implementation<br>Overridden implementation<br>在JVM中,默认方法的实现是非常高效的,并且通过字节码指令为方法调用提供了支持。默认方法允许继续使用现有的Java接口,而同时能够保障正常的编译过程。这方面好的例子是大量的方法被添加到java.util.Collection接口中去:stream(),parallelStream(),forEach(),removeIf(),……</p><p>尽管默认方法非常强大,但是在使用默认方法时我们需要小心注意一个地方:在声明一个默认方法前,请仔细思考是不是真的有必要使用默认方法,因为默认方法会带给程序歧义,并且在复杂的继承体系中容易产生编译错误。更多详情请参考官方文档</p><h2 id="重复注解"><a href="#重复注解" class="headerlink" title="重复注解"></a>重复注解</h2><p>自从Java 5引入了注解机制,这一特性就变得非常流行并且广为使用。然而,使用注解的一个限制是相同的注解在同一位置只能声明一次,不能声明多次。Java 8打破了这条规则,引入了重复注解机制,这样相同的注解可以在同一地方声明多次。</p><p>重复注解机制本身必须用@Repeatable注解。事实上,这并不是语言层面上的改变,更多的是编译器的技巧,底层的原理保持不变。让我们看一个快速入门的例子:</p><pre><code>package com.javacodegeeks.java8.repeatable.annotations;import java.lang.annotation.ElementType;import java.lang.annotation.Repeatable;import java.lang.annotation.Retention;import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;public class RepeatingAnnotations { @Target( ElementType.TYPE ) @Retention( RetentionPolicy.RUNTIME ) public @interface Filters { Filter[] value(); } @Target( ElementType.TYPE ) @Retention( RetentionPolicy.RUNTIME ) @Repeatable( Filters.class ) public @interface Filter { String value(); }; @Filter( "filter1" ) @Filter( "filter2" ) public interface Filterable { } public static void main(String[] args) { for( Filter filter: Filterable.class.getAnnotationsByType( Filter.class ) ) { System.out.println( filter.value() ); } }}</code></pre><p>正如我们看到的,这里有个使用@Repeatable( Filters.class )注解的注解类Filter,Filters仅仅是Filter注解的数组,但Java编译器并不想让程序员意识到Filters的存在。这样,接口Filterable就拥有了两次Filter(并没有提到Filter)注解。</p><p>同时,反射相关的API提供了新的函数getAnnotationsByType()来返回重复注解的类型(请注意Filterable.class.getAnnotation( Filters.class )经编译器处理后将会返回Filters的实例)。</p><p>程序输出结果如下:</p><p>filter1<br>filter2<br>更多详情请参考官方文档</p><h1 id="Java编译器的新特性"><a href="#Java编译器的新特性" class="headerlink" title="Java编译器的新特性"></a>Java编译器的新特性</h1><h2 id="方法参数名字可以反射获取"><a href="#方法参数名字可以反射获取" class="headerlink" title="方法参数名字可以反射获取"></a>方法参数名字可以反射获取</h2><p>很长一段时间里,Java程序员一直在发明不同的方式使得方法参数的名字能保留在Java字节码中,并且能够在运行时获取它们(比如,Paranamer类库)。最终,在Java 8中把这个强烈要求的功能添加到语言层面(通过反射API与Parameter.getName()方法)与字节码文件(通过新版的javac的–parameters选项)中。</p><p>package com.javacodegeeks.java8.parameter.names;</p><p>import java.lang.reflect.Method;<br>import java.lang.reflect.Parameter;</p><p>public class ParameterNames {<br> public static void main(String[] args) throws Exception {<br> Method method = ParameterNames.class.getMethod( “main”, String[].class );<br> for( final Parameter parameter: method.getParameters() ) {<br> System.out.println( “Parameter: “ + parameter.getName() );<br> }<br> }<br>}<br>如果不使用–parameters参数来编译这个类,然后运行这个类,会得到下面的输出:</p><p>Parameter: arg0<br>如果使用–parameters参数来编译这个类,程序的结构会有所不同(参数的真实名字将会显示出来):</p><p>Parameter: args</p><h1 id="Java-类库的新特性"><a href="#Java-类库的新特性" class="headerlink" title="Java 类库的新特性"></a>Java 类库的新特性</h1><p>Java 8 通过增加大量新类,扩展已有类的功能的方式来改善对并发编程、函数式编程、日期/时间相关操作以及其他更多方面的支持。</p><h2 id="Optional"><a href="#Optional" class="headerlink" title="Optional"></a>Optional</h2><p>到目前为止,臭名昭著的空指针异常是导致Java应用程序失败的最常见原因。以前,为了解决空指针异常,Google公司著名的Guava项目引入了Optional类,Guava通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。受到Google Guava的启发,Optional类已经成为Java 8类库的一部分。</p><p>Optional实际上是个容器:它可以保存类型T的值,或者仅仅保存null。Optional提供很多有用的方法,这样我们就不用显式进行空值检测。更多详情请参考官方文档。</p><p>我们下面用两个小例子来演示如何使用Optional类:一个允许为空值,一个不允许为空值。</p><pre><code>public class 空指针Optional { public static void main(String[] args) { //使用of方法,仍然会报空指针异常// Optional optional = Optional.of(null);// System.out.println(optional.get()); //抛出没有该元素的异常 //Exception in thread "main" java.util.NoSuchElementException: No value present// at java.util.Optional.get(Optional.java:135)// at com.javase.Java8.空指针Optional.main(空指针Optional.java:14)// Optional optional1 = Optional.ofNullable(null);// System.out.println(optional1.get()); Optional optional = Optional.ofNullable(null); System.out.println(optional.isPresent()); System.out.println(optional.orElse(0));//当值为空时给与初始值 System.out.println(optional.orElseGet(() -> new String[]{"a"}));//使用回调函数设置默认值 //即使传入Optional容器的元素为空,使用optional.isPresent()方法也不会报空指针异常 //所以通过optional.orElse这种方式就可以写出避免空指针异常的代码了 //输出Optional.empty。 }}</code></pre><p>如果Optional类的实例为非空值的话,isPresent()返回true,否从返回false。为了防止Optional为空值,orElseGet()方法通过回调函数来产生一个默认值。map()函数对当前Optional的值进行转化,然后返回一个新的Optional实例。orElse()方法和orElseGet()方法类似,但是orElse接受一个默认值而不是一个回调函数。下面是这个程序的输出:</p><p>Full Name is set? false<br>Full Name: [none]<br>Hey Stranger!<br>让我们来看看另一个例子:</p><pre><code>Optional< String > firstName = Optional.of( "Tom" );System.out.println( "First Name is set? " + firstName.isPresent() ); System.out.println( "First Name: " + firstName.orElseGet( () -> "[none]" ) ); System.out.println( firstName.map( s -> "Hey " + s + "!" ).orElse( "Hey Stranger!" ) );System.out.println();</code></pre><p>下面是程序的输出:</p><p>First Name is set? true<br>First Name: Tom<br>Hey Tom!</p><h2 id="Stream"><a href="#Stream" class="headerlink" title="Stream"></a>Stream</h2><p>最新添加的Stream API(java.util.stream) 把真正的函数式编程风格引入到Java中。这是目前为止对Java类库最好的补充,因为Stream API可以极大提供Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。</p><p>Stream API极大简化了集合框架的处理(但它的处理的范围不仅仅限于集合框架的处理,这点后面我们会看到)。让我们以一个简单的Task类为例进行介绍:</p><p>Task类有一个分数的概念(或者说是伪复杂度),其次是还有一个值可以为OPEN或CLOSED的状态.让我们引入一个Task的小集合作为演示例子:</p><pre><code>final Collection< Task > tasks = Arrays.asList( new Task( Status.OPEN, 5 ), new Task( Status.OPEN, 13 ), new Task( Status.CLOSED, 8 ) );</code></pre><p>我们下面要讨论的第一个问题是所有状态为OPEN的任务一共有多少分数?在Java 8以前,一般的解决方式用foreach循环,但是在Java 8里面我们可以使用stream:一串支持连续、并行聚集操作的元素。</p><pre><code>// Calculate total points of all active tasks using sum()final long totalPointsOfOpenTasks = tasks .stream() .filter( task -> task.getStatus() == Status.OPEN ) .mapToInt( Task::getPoints ) .sum();System.out.println( "Total points: " + totalPointsOfOpenTasks );</code></pre><p>程序在控制台上的输出如下:</p><p>Total points: 18</p><p>这里有几个注意事项。</p><p>第一,task集合被转换化为其相应的stream表示。然后,filter操作过滤掉状态为CLOSED的task。</p><p>下一步,mapToInt操作通过Task::getPoints这种方式调用每个task实例的getPoints方法把Task的stream转化为Integer的stream。最后,用sum函数把所有的分数加起来,得到最终的结果。</p><p>在继续讲解下面的例子之前,关于stream有一些需要注意的地方(详情在这里).stream操作被分成了中间操作与最终操作这两种。</p><p>中间操作返回一个新的stream对象。中间操作总是采用惰性求值方式,运行一个像filter这样的中间操作实际上没有进行任何过滤,相反它在遍历元素时会产生了一个新的stream对象,这个新的stream对象包含原始stream<br>中符合给定谓词的所有元素。</p><p>像forEach、sum这样的最终操作可能直接遍历stream,产生一个结果或副作用。当最终操作执行结束之后,stream管道被认为已经被消耗了,没有可能再被使用了。在大多数情况下,最终操作都是采用及早求值方式,及早完成底层数据源的遍历。</p><p>stream另一个有价值的地方是能够原生支持并行处理。让我们来看看这个算task分数和的例子。</p><p>stream另一个有价值的地方是能够原生支持并行处理。让我们来看看这个算task分数和的例子。</p><pre><code>// Calculate total points of all tasksfinal double totalPoints = tasks .stream() .parallel() .map( task -> task.getPoints() ) // or map( Task::getPoints ) .reduce( 0, Integer::sum );System.out.println( "Total points (all tasks): " + totalPoints );</code></pre><p>这个例子和第一个例子很相似,但这个例子的不同之处在于这个程序是并行运行的,其次使用reduce方法来算最终的结果。<br>下面是这个例子在控制台的输出:</p><p>Total points (all tasks): 26.0<br>经常会有这个一个需求:我们需要按照某种准则来对集合中的元素进行分组。Stream也可以处理这样的需求,下面是一个例子:</p><pre><code>// Group tasks by their statusfinal Map< Status, List< Task > > map = tasks .stream() .collect( Collectors.groupingBy( Task::getStatus ) );System.out.println( map );</code></pre><p>这个例子的控制台输出如下:</p><p>{CLOSED=[[CLOSED, 8]], OPEN=[[OPEN, 5], [OPEN, 13]]}<br>让我们来计算整个集合中每个task分数(或权重)的平均值来结束task的例子。</p><pre><code>// Calculate the weight of each tasks (as percent of total points) final Collection< String > result = tasks .stream() // Stream< String > .mapToInt( Task::getPoints ) // IntStream .asLongStream() // LongStream .mapToDouble( points -> points / totalPoints ) // DoubleStream .boxed() // Stream< Double > .mapToLong( weigth -> ( long )( weigth * 100 ) ) // LongStream .mapToObj( percentage -> percentage + "%" ) // Stream< String> .collect( Collectors.toList() ); // List< String > System.out.println( result );</code></pre><p>下面是这个例子的控制台输出:</p><p>[19%, 50%, 30%]<br>最后,就像前面提到的,Stream API不仅仅处理Java集合框架。像从文本文件中逐行读取数据这样典型的I/O操作也很适合用Stream API来处理。下面用一个例子来应证这一点。</p><pre><code>final Path path = new File( filename ).toPath();try( Stream< String > lines = Files.lines( path, StandardCharsets.UTF_8 ) ) { lines.onClose( () -> System.out.println("Done!") ).forEach( System.out::println );}</code></pre><p>对一个stream对象调用onClose方法会返回一个在原有功能基础上新增了关闭功能的stream对象,当对stream对象调用close()方法时,与关闭相关的处理器就会执行。</p><p>Stream API、Lambda表达式与方法引用在接口默认方法与静态方法的配合下是Java 8对现代软件开发范式的回应。更多详情请参考官方文档。</p><h2 id="Date-Time-API-JSR-310"><a href="#Date-Time-API-JSR-310" class="headerlink" title="Date/Time API (JSR 310)"></a>Date/Time API (JSR 310)</h2><p>Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。对日期与时间的操作一直是Java程序员最痛苦的地方之一。标准的 java.util.Date以及后来的java.util.Calendar一点没有改善这种情况(可以这么说,它们一定程度上更加复杂)。</p><p>这种情况直接导致了Joda-Time——一个可替换标准日期/时间处理且功能非常强大的Java API的诞生。Java 8新的Date-Time API (JSR 310)在很大程度上受到Joda-Time的影响,并且吸取了其精髓。新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作。在设计新版API时,十分注重与旧版API的兼容性:不允许有任何的改变(从java.util.Calendar中得到的深刻教训)。如果需要修改,会返回这个类的一个新实例。</p><p>让我们用例子来看一下新版API主要类的使用方法。第一个是Clock类,它通过指定一个时区,然后就可以获取到当前的时刻,日期与时间。Clock可以替换System.currentTimeMillis()与TimeZone.getDefault()。</p><pre><code>// Get the system clock as UTC offset final Clock clock = Clock.systemUTC();System.out.println( clock.instant() );System.out.println( clock.millis() );</code></pre><p>下面是程序在控制台上的输出:</p><p>2014-04-12T15:19:29.282Z<br>1397315969360</p><p>我们需要关注的其他类是LocaleDate与LocalTime。LocaleDate只持有ISO-8601格式且无时区信息的日期部分。相应的,LocaleTime只持有ISO-8601格式且无时区信息的时间部分。LocaleDate与LocalTime都可以从Clock中得到。</p><pre><code>// Get the local date and local timefinal LocalDate date = LocalDate.now();final LocalDate dateFromClock = LocalDate.now( clock );System.out.println( date );System.out.println( dateFromClock );// Get the local date and local timefinal LocalTime time = LocalTime.now();final LocalTime timeFromClock = LocalTime.now( clock );System.out.println( time );System.out.println( timeFromClock );</code></pre><p>下面是程序在控制台上的输出:</p><p>2014-04-12<br>2014-04-12<br>11:25:54.568<br>15:25:54.568</p><p>下面是程序在控制台上的输出:</p><p>2014-04-12T11:47:01.017-04:00[America/New_York]<br>2014-04-12T15:47:01.017Z<br>2014-04-12T08:47:01.017-07:00[America/Los_Angeles]<br>最后,让我们看一下Duration类:在秒与纳秒级别上的一段时间。Duration使计算两个日期间的不同变的十分简单。下面让我们看一个这方面的例子。</p><pre><code>// Get duration between two datesfinal LocalDateTime from = LocalDateTime.of( 2014, Month.APRIL, 16, 0, 0, 0 );final LocalDateTime to = LocalDateTime.of( 2015, Month.APRIL, 16, 23, 59, 59 );final Duration duration = Duration.between( from, to );System.out.println( "Duration in days: " + duration.toDays() );System.out.println( "Duration in hours: " + duration.toHours() );</code></pre><p>上面的例子计算了两个日期2014年4月16号与2014年4月16号之间的过程。下面是程序在控制台上的输出:</p><p>Duration in days: 365<br>Duration in hours: 8783<br>对Java 8在日期/时间API的改进整体印象是非常非常好的。一部分原因是因为它建立在“久战杀场”的Joda-Time基础上,另一方面是因为用来大量的时间来设计它,并且这次程序员的声音得到了认可。更多详情请参考官方文档。</p><h2 id="并行(parallel)数组"><a href="#并行(parallel)数组" class="headerlink" title="并行(parallel)数组"></a>并行(parallel)数组</h2><p>Java 8增加了大量的新方法来对数组进行并行处理。可以说,最重要的是parallelSort()方法,因为它可以在多核机器上极大提高数组排序的速度。下面的例子展示了新方法(parallelXxx)的使用。</p><pre><code>package com.javacodegeeks.java8.parallel.arrays;import java.util.Arrays;import java.util.concurrent.ThreadLocalRandom;public class ParallelArrays { public static void main( String[] args ) { long[] arrayOfLong = new long [ 20000 ]; Arrays.parallelSetAll( arrayOfLong, index -> ThreadLocalRandom.current().nextInt( 1000000 ) ); Arrays.stream( arrayOfLong ).limit( 10 ).forEach( i -> System.out.print( i + " " ) ); System.out.println(); Arrays.parallelSort( arrayOfLong ); Arrays.stream( arrayOfLong ).limit( 10 ).forEach( i -> System.out.print( i + " " ) ); System.out.println(); }}</code></pre><p>上面的代码片段使用了parallelSetAll()方法来对一个有20000个元素的数组进行随机赋值。然后,调用parallelSort方法。这个程序首先打印出前10个元素的值,之后对整个数组排序。这个程序在控制台上的输出如下(请注意数组元素是随机生产的):</p><p>Unsorted: 591217 891976 443951 424479 766825 351964 242997 642839 119108 552378<br>Sorted: 39 220 263 268 325 607 655 678 723 793</p><h2 id="CompletableFuture"><a href="#CompletableFuture" class="headerlink" title="CompletableFuture"></a>CompletableFuture</h2><p>在Java8之前,我们会使用JDK提供的Future接口来进行一些异步的操作,其实CompletableFuture也是实现了Future接口, 并且基于ForkJoinPool来执行任务,因此本质上来讲,CompletableFuture只是对原有API的封装, 而使用CompletableFuture与原来的Future的不同之处在于可以将两个Future组合起来,或者如果两个Future是有依赖关系的,可以等第一个执行完毕后再实行第二个等特性。</p><p><strong>先来看看基本的使用方式:</strong></p><pre><code>public Future<Double> getPriceAsync(final String product) { final CompletableFuture<Double> futurePrice = new CompletableFuture<>(); new Thread(() -> { double price = calculatePrice(product); futurePrice.complete(price); //完成后使用complete方法,设置future的返回值 }).start(); return futurePrice;}</code></pre><p>得到Future之后就可以使用get方法来获取结果,CompletableFuture提供了一些工厂方法来简化这些API,并且使用函数式编程的方式来使用这些API,例如:</p><p>Fufure<double> price = CompletableFuture.supplyAsync(() -> calculatePrice(product));<br>代码是不是一下子简洁了许多呢。之前说了,CompletableFuture可以组合多个Future,不管是Future之间有依赖的,还是没有依赖的。 </double></p><p><strong>如果第二个请求依赖于第一个请求的结果,那么可以使用thenCompose方法来组合两个Future</strong></p><pre><code>public List<String> findPriceAsync(String product) { List<CompletableFutute<String>> priceFutures = tasks.stream() .map(task -> CompletableFuture.supplyAsync(() -> task.getPrice(product),executor)) .map(future -> future.thenApply(Work::parse)) .map(future -> future.thenCompose(work -> CompletableFuture.supplyAsync(() -> Count.applyCount(work), executor))) .collect(Collectors.toList()); return priceFutures.stream().map(CompletableFuture::join).collect(Collectors.toList());}</code></pre><p>上面这段代码使用了thenCompose来组合两个CompletableFuture。supplyAsync方法第二个参数接受一个自定义的Executor。 首先使用CompletableFuture执行一个任务,调用getPrice方法,得到一个Future,之后使用thenApply方法,将Future的结果应用parse方法, 之后再使用执行完parse之后的结果作为参数再执行一个applyCount方法,然后收集成一个CompletableFuture<string>的List, 最后再使用一个流,调用CompletableFuture的join方法,这是为了等待所有的异步任务执行完毕,获得最后的结果。</string></p><p>注意,这里必须使用两个流,如果在一个流里调用join方法,那么由于Stream的延迟特性,所有的操作还是会串行的执行,并不是异步的。</p><p><strong>再来看一个两个Future之间没有依赖关系的例子:</strong></p><pre><code>Future<String> futurePriceInUsd = CompletableFuture.supplyAsync(() -> shop.getPrice(“price1”)) .thenCombine(CompletableFuture.supplyAsync(() -> shop.getPrice(“price2”)), (s1, s2) -> s1 + s2);</code></pre><p>这里有两个异步的任务,使用thenCombine方法来组合两个Future,thenCombine方法的第二个参数就是用来合并两个Future方法返回值的操作函数。</p><p>有时候,我们并不需要等待所有的异步任务结束,只需要其中的一个完成就可以了,CompletableFuture也提供了这样的方法:</p><pre><code>//假设getStream方法返回一个Stream<CompletableFuture<String>>CompletableFuture[] futures = getStream(“listen”).map(f -> f.thenAccept(System.out::println)).toArray(CompletableFuture[]::new);//等待其中的一个执行完毕CompletableFuture.anyOf(futures).join();使用anyOf方法来响应CompletableFuture的completion事件。</code></pre><h1 id="Java虚拟机(JVM)的新特性"><a href="#Java虚拟机(JVM)的新特性" class="headerlink" title="Java虚拟机(JVM)的新特性"></a>Java虚拟机(JVM)的新特性</h1><p>PermGen空间被移除了,取而代之的是Metaspace(JEP 122)。JVM选项-XX:PermSize与-XX:MaxPermSize分别被-XX:MetaSpaceSize与-XX:MaxMetaspaceSize所代替。</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>更多展望:Java 8通过发布一些可以增加程序员生产力的特性来推进这个伟大的平台的进步。现在把生产环境迁移到Java 8还为时尚早,但是在接下来的几个月里,它会被大众慢慢的接受。毫无疑问,现在是时候让你的代码与Java 8兼容,并且在Java 8足够安全稳定的时候迁移到Java 8。</p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java基础19:Java集合框架梳理</title>
<link href="/2018/05/06/javase19/"/>
<url>/2018/05/06/javase19/</url>
<content type="html"><![CDATA[<p>本文介绍了Java集合类的基本框架,接口结构以及部分源码分析,并且通过自己实现一些集合类来更好地剖析Java集合类的整体结构。</p><p>本文只是对集合类框架进行一个大概的梳理,毕竟集合框架中包含的类太多了,一篇文章不可能讲完,这里先开一个头,对整体框架有一个清晰认识之后,再去探索各个接口实现类的奥秘。</p><p>后面会专门地写几篇关于集合类的文章,分别介绍一下List,Map,Set以及Stack等等这些接口的实现类,敬请期待。</p><p>具体代码在我的GitHub中可以找到</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>喜欢的话麻烦点一下星哈谢谢。</p><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/05/06/javase19">https://h2pl.github.io/2018/05/06/javase19</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><blockquote><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p></blockquote><a id="more"></a><p>在编写java程序中,我们最常用的除了八种基本数据类型,String对象外还有一个集合类,在我们的的程序中到处充斥着集合类的身影!</p><p>java中集合大家族的成员实在是太丰富了,有常用的ArrayList、HashMap、HashSet,也有不常用的Stack、Queue,有线程安全的Vector、HashTable,也有线程不安全的LinkedList、TreeMap等等!</p><p><img src="https://images0.cnblogs.com/blog/381060/201312/28124707-3a873160808e457686d67c118af6fa70.png" alt="image"></p><p>上面的图展示了整个集合大家族的成员以及他们之间的关系。下面就上面的各个接口、基类做一些简单的介绍(主要介绍各个集合的特点。区别)。</p><p>下面几张图更清晰地介绍了结合类接口间的关系:</p><blockquote><p>Collections和Collection。<br>Arrays和Collections。</p></blockquote><p><img src="https://www.programcreek.com/wp-content/uploads/2009/02/CollectionVsCollections.jpeg" alt="image"></p><blockquote><p>Collection的子接口</p></blockquote><p><img src="https://www.programcreek.com/wp-content/uploads/2009/02/java-collection-hierarchy.jpeg" alt="image"></p><blockquote><p>map的实现类</p></blockquote><p><img src="https://www.programcreek.com/wp-content/uploads/2009/02/MapClassHierarchy-600x354.jpg" alt="image"></p><h2 id="Collection接口"><a href="#Collection接口" class="headerlink" title="Collection接口"></a>Collection接口</h2><p> Collection接口是最基本的集合接口,它不提供直接的实现,Java SDK提供的类都是继承自Collection的“子接口”如List和Set。Collection所代表的是一种规则,它所包含的元素都必须遵循一条或者多条规则。如有些允许重复而有些则不能重复、有些必须要按照顺序插入而有些则是散列,有些支持排序但是有些则不支持。</p><p> 在Java中所有实现了Collection接口的类都必须提供两套标准的构造函数,一个是无参,用于创建一个空的Collection,一个是带有Collection参数的有参构造函数,用于创建一个新的Collection,这个新的Collection与传入进来的Collection具备相同的元素。<br>//要求实现基本的增删改查方法,并且需要能够转换为数组类型</p><pre><code>public class Collection接口 { class collect implements Collection { @Override public int size() { return 0; } @Override public boolean isEmpty() { return false; } @Override public boolean contains(Object o) { return false; } @Override public Iterator iterator() { return null; } @Override public Object[] toArray() { return new Object[0]; } @Override public boolean add(Object o) { return false; } @Override public boolean remove(Object o) { return false; } @Override public boolean addAll(Collection c) { return false; } @Override public void clear() { }//省略部分代码 @Override public Object[] toArray(Object[] a) { return new Object[0]; } }}</code></pre><h2 id="List接口"><a href="#List接口" class="headerlink" title="List接口"></a>List接口</h2><blockquote><p> List接口为Collection直接接口。List所代表的是有序的Collection,即它用某种特定的插入顺序来维护元素顺序。用户可以对列表中每个元素的插入位置进行精确地控制,同时可以根据元素的整数索引(在列表中的位置)访问元素,并搜索列表中的元素。实现List接口的集合主要有:ArrayList、LinkedList、Vector、Stack。</p></blockquote><p>2.1、ArrayList</p><blockquote><p> ArrayList是一个动态数组,也是我们最常用的集合。它允许任何符合规则的元素插入甚至包括null。每一个ArrayList都有一个初始容量(10),该容量代表了数组的大小。随着容器中的元素不断增加,容器的大小也会随着增加。在每次向容器中增加元素的同时都会进行容量检查,当快溢出时,就会进行扩容操作。所以如果我们明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间、效率。</p><p> size、isEmpty、get、set、iterator 和 listIterator 操作都以固定时间运行。add 操作以分摊的固定时间运行,也就是说,添加 n 个元素需要 O(n) 时间(由于要考虑到扩容,所以这不只是添加元素会带来分摊固定时间开销那样简单)。</p><p> ArrayList擅长于随机访问。同时ArrayList是非同步的。</p></blockquote><p>2.2、LinkedList</p><blockquote><p> 同样实现List接口的LinkedList与ArrayList不同,ArrayList是一个动态数组,而LinkedList是一个双向链表。所以它除了有ArrayList的基本操作方法外还额外提供了get,remove,insert方法在LinkedList的首部或尾部。</p><p> 由于实现的方式不同,LinkedList不能随机访问,它所有的操作都是要按照双重链表的需要执行。在列表中索引的操作将从开头或结尾遍历列表(从靠近指定索引的一端)。这样做的好处就是可以通过较低的代价在List中进行插入和删除操作。</p><p> 与ArrayList一样,LinkedList也是非同步的。如果多个线程同时访问一个List,则必须自己实现访问同步。一种解决方法是在创建List时构造一个同步的List:<br>List list = Collections.synchronizedList(new LinkedList(…));</p></blockquote><blockquote><p>2.3、Vector<br> 与ArrayList相似,但是Vector是同步的。所以说Vector是线程安全的动态数组。它的操作与ArrayList几乎一样。</p><p>2.4、Stack<br> Stack继承自Vector,实现一个后进先出的堆栈。Stack提供5个额外的方法使得Vector得以被当作堆栈使用。基本的push和pop 方法,还有peek方法得到栈顶的元素,empty方法测试堆栈是否为空,search方法检测一个元素在堆栈中的位置。Stack刚创建后是空栈。。</p></blockquote><pre><code>public class List接口 { //下面是List的继承关系,由于List接口规定了包括诸如索引查询,迭代器的实现,所以实现List接口的类都会有这些方法。 //所以不管是ArrayList和LinkedList底层都可以使用数组操作,但一般不提供这样外部调用方法。 // public interface Iterable<T>// public interface Collection<E> extends Iterable<E>// public interface List<E> extends Collection<E> class MyList implements List { @Override public int size() { return 0; } @Override public boolean isEmpty() { return false; } @Override public boolean contains(Object o) { return false; } @Override public Iterator iterator() { return null; } @Override public Object[] toArray() { return new Object[0]; } @Override public boolean add(Object o) { return false; } @Override public boolean remove(Object o) { return false; } @Override public void clear() { } //省略部分代码 @Override public Object get(int index) { return null; } @Override public ListIterator listIterator() { return null; } @Override public ListIterator listIterator(int index) { return null; } @Override public List subList(int fromIndex, int toIndex) { return null; } @Override public Object[] toArray(Object[] a) { return new Object[0]; } }}</code></pre><h2 id="Set接口"><a href="#Set接口" class="headerlink" title="Set接口"></a>Set接口</h2><blockquote><p>Set是一种不包括重复元素的Collection。它维持它自己的内部排序,所以随机访问没有任何意义。与List一样,它同样运行null的存在但是仅有一个。由于Set接口的特殊性,所有传入Set集合中的元素都必须不同,同时要注意任何可变对象,如果在对集合中元素进行操作时,导致e1.equals(e2)==true,则必定会产生某些问题。实现了Set接口的集合有:EnumSet、HashSet、TreeSet。</p><p>3.1、EnumSet<br> 是枚举的专用Set。所有的元素都是枚举类型。</p><p>3.2、HashSet<br> HashSet堪称查询速度最快的集合,因为其内部是以HashCode来实现的。它内部元素的顺序是由哈希码来决定的,所以它不保证set 的迭代顺序;特别是它不保证该顺序恒久不变。</p></blockquote><pre><code>public class Set接口 { // Set接口规定将set看成一个集合,并且使用和数组类似的增删改查方式,同时提供iterator迭代器 // public interface Set<E> extends Collection<E> // public interface Collection<E> extends Iterable<E> // public interface Iterable<T> class MySet implements Set { @Override public int size() { return 0; } @Override public boolean isEmpty() { return false; } @Override public boolean contains(Object o) { return false; } @Override public Iterator iterator() { return null; } @Override public Object[] toArray() { return new Object[0]; } @Override public boolean add(Object o) { return false; } @Override public boolean remove(Object o) { return false; } @Override public boolean addAll(Collection c) { return false; } @Override public void clear() { } @Override public boolean removeAll(Collection c) { return false; } @Override public boolean retainAll(Collection c) { return false; } @Override public boolean containsAll(Collection c) { return false; } @Override public Object[] toArray(Object[] a) { return new Object[0]; } }}</code></pre><h2 id="Map接口"><a href="#Map接口" class="headerlink" title="Map接口"></a>Map接口</h2><blockquote><p> Map与List、Set接口不同,它是由一系列键值对组成的集合,提供了key到Value的映射。同时它也没有继承Collection。在Map中它保证了key与value之间的一一对应关系。也就是说一个key对应一个value,所以它不能存在相同的key值,当然value值可以相同。实现map的有:HashMap、TreeMap、HashTable、Properties、EnumMap。</p></blockquote><blockquote><p>4.1、HashMap<br> 以哈希表数据结构实现,查找对象时通过哈希函数计算其位置,它是为快速查询而设计的,其内部定义了一个hash表数组(Entry[] table),元素会通过哈希转换函数将元素的哈希地址转换成数组中存放的索引,如果有冲突,则使用散列链表的形式将所有相同哈希地址的元素串起来,可能通过查看HashMap.Entry的源码它是一个单链表结构。</p><p>4.2、TreeMap<br> 键以某种排序规则排序,内部以red-black(红-黑)树数据结构实现,实现了SortedMap接口</p><p>4.3、HashTable<br> 也是以哈希表数据结构实现的,解决冲突时与HashMap也一样也是采用了散列链表的形式,不过性能比HashMap要低</p></blockquote><pre><code>public class Map接口 { //Map接口是最上层接口,Map接口实现类必须实现put和get等哈希操作。 //并且要提供keyset和values,以及entryset等查询结构。 //public interface Map<K,V> class MyMap implements Map { @Override public int size() { return 0; } @Override public boolean isEmpty() { return false; } @Override public boolean containsKey(Object key) { return false; } @Override public boolean containsValue(Object value) { return false; } @Override public Object get(Object key) { return null; } @Override public Object put(Object key, Object value) { return null; } @Override public Object remove(Object key) { return null; } @Override public void putAll(Map m) { } @Override public void clear() { } @Override public Set keySet() { return null; } @Override public Collection values() { return null; } @Override public Set<Entry> entrySet() { return null; } }}</code></pre><h2 id="Queue"><a href="#Queue" class="headerlink" title="Queue"></a>Queue</h2><blockquote><p> 队列,它主要分为两大类,一类是阻塞式队列,队列满了以后再插入元素则会抛出异常,主要包括ArrayBlockQueue、PriorityBlockingQueue、LinkedBlockingQueue。另一种队列则是双端队列,支持在头、尾两端插入和移除元素,主要包括:ArrayDeque、LinkedBlockingDeque、LinkedList。</p></blockquote><pre><code>public class Queue接口 { //queue接口是对队列的一个实现,需要提供队列的进队出队等方法。一般使用linkedlist作为实现类 class MyQueue implements Queue { @Override public int size() { return 0; } @Override public boolean isEmpty() { return false; } @Override public boolean contains(Object o) { return false; } @Override public Iterator iterator() { return null; } @Override public Object[] toArray() { return new Object[0]; } @Override public Object[] toArray(Object[] a) { return new Object[0]; } @Override public boolean add(Object o) { return false; } @Override public boolean remove(Object o) { return false; } //省略部分代码 @Override public boolean offer(Object o) { return false; } @Override public Object remove() { return null; } @Override public Object poll() { return null; } @Override public Object element() { return null; } @Override public Object peek() { return null; } }}</code></pre><h2 id="关于Java集合的小抄"><a href="#关于Java集合的小抄" class="headerlink" title="关于Java集合的小抄"></a>关于Java集合的小抄</h2><p>这部分内容转自我偶像 江南白衣 的博客:<a href="http://calvin1978.blogcn.com/articles/collection.html" target="_blank" rel="noopener">http://calvin1978.blogcn.com/articles/collection.html</a><br>在尽可能短的篇幅里,将所有集合与并发集合的特征、实现方式、性能捋一遍。适合所有”精通Java”,其实还不那么自信的人阅读。</p><p>期望能不止用于面试时,平时选择数据结构,也能考虑一下其成本与效率,不要看着API合适就用了。</p><h3 id="List"><a href="#List" class="headerlink" title="List"></a>List</h3><p>1.1 ArrayList<br>以数组实现。节约空间,但数组有容量限制。超出限制时会增加50%容量,用System.arraycopy()复制到新的数组。因此最好能给出数组大小的预估值。默认第一次插入元素时创建大小为10的数组。</p><p>按数组下标访问元素-get(i)、set(i,e) 的性能很高,这是数组的基本优势。</p><p>如果按下标插入元素、删除元素-add(i,e)、 remove(i)、remove(e),则要用System.arraycopy()来复制移动部分受影响的元素,性能就变差了。</p><p>越是前面的元素,修改时要移动的元素越多。直接在数组末尾加入元素-常用的add(e),删除最后一个元素则无影响。</p><p>1.2 LinkedList<br>以双向链表实现。链表无容量限制,但双向链表本身使用了更多空间,每插入一个元素都要构造一个额外的Node对象,也需要额外的链表指针操作。</p><p>按下标访问元素-get(i)、set(i,e) 要悲剧的部分遍历链表将指针移动到位 (如果i>数组大小的一半,会从末尾移起)。</p><p>插入、删除元素时修改前后节点的指针即可,不再需要复制移动。但还是要部分遍历链表的指针才能移动到下标所指的位置。</p><p>只有在链表两头的操作-add()、addFirst()、removeLast()或用iterator()上的remove()倒能省掉指针的移动。</p><p>Apache Commons 有个TreeNodeList,里面是棵二叉树,可以快速移动指针到位。</p><p>1.3 CopyOnWriteArrayList<br>并发优化的ArrayList。基于不可变对象策略,在修改时先复制出一个数组快照来修改,改好了,再让内部指针指向新数组。</p><p>因为对快照的修改对读操作来说不可见,所以读读之间不互斥,读写之间也不互斥,只有写写之间要加锁互斥。但复制快照的成本昂贵,典型的适合读多写少的场景。</p><p>虽然增加了addIfAbsent(e)方法,会遍历数组来检查元素是否已存在,性能可想像的不会太好。</p><p>1.4 遗憾<br>无论哪种实现,按值返回下标contains(e), indexOf(e), remove(e) 都需遍历所有元素进行比较,性能可想像的不会太好。</p><p>没有按元素值排序的SortedList。</p><p>除了CopyOnWriteArrayList,再没有其他线程安全又并发优化的实现如ConcurrentLinkedList。凑合着用Set与Queue中的等价类时,会缺少一些List特有的方法如get(i)。如果更新频率较高,或数组较大时,还是得用Collections.synchronizedList(list),对所有操作用同一把锁来保证线程安全。</p><h3 id="Map"><a href="#Map" class="headerlink" title="Map"></a>Map</h3><p>2.1 HashMap</p><p>以Entry[]数组实现的哈希桶数组,用Key的哈希值取模桶数组的大小可得到数组下标。</p><p>插入元素时,如果两条Key落在同一个桶(比如哈希值1和17取模16后都属于第一个哈希桶),我们称之为哈希冲突。</p><p>JDK的做法是链表法,Entry用一个next属性实现多个Entry以单向链表存放。查找哈希值为17的key时,先定位到哈希桶,然后链表遍历桶里所有元素,逐个比较其Hash值然后key值。</p><p>在JDK8里,新增默认为8的阈值,当一个桶里的Entry超过閥值,就不以单向链表而以红黑树来存放以加快Key的查找速度。</p><p>当然,最好还是桶里只有一个元素,不用去比较。所以默认当Entry数量达到桶数量的75%时,哈希冲突已比较严重,就会成倍扩容桶数组,并重新分配所有原来的Entry。扩容成本不低,所以也最好有个预估值。</p><p>取模用与操作(hash & (arrayLength-1))会比较快,所以数组的大小永远是2的N次方, 你随便给一个初始值比如17会转为32。默认第一次放入元素时的初始值是16。</p><p>iterator()时顺着哈希桶数组来遍历,看起来是个乱序。</p><p>2.2 LinkedHashMap<br>扩展HashMap,每个Entry增加双向链表,号称是最占内存的数据结构。</p><p>支持iterator()时按Entry的插入顺序来排序(如果设置accessOrder属性为true,则所有读写访问都排序)。</p><p>插入时,Entry把自己加到Header Entry的前面去。如果所有读写访问都要排序,还要把前后Entry的before/after拼接起来以在链表中删除掉自己,所以此时读操作也是线程不安全的了。</p><p>2.3 TreeMap<br>以红黑树实现,红黑树又叫自平衡二叉树:</p><p>对于任一节点而言,其到叶节点的每一条路径都包含相同数目的黑结点。<br>上面的规定,使得树的层数不会差的太远,使得所有操作的复杂度不超过 O(lgn),但也使得插入,修改时要复杂的左旋右旋来保持树的平衡。</p><p>支持iterator()时按Key值排序,可按实现了Comparable接口的Key的升序排序,或由传入的Comparator控制。可想象的,在树上插入/删除元素的代价一定比HashMap的大。</p><p>支持SortedMap接口,如firstKey(),lastKey()取得最大最小的key,或sub(fromKey, toKey), tailMap(fromKey)剪取Map的某一段。</p><p>2.4 EnumMap<br>EnumMap的原理是,在构造函数里要传入枚举类,那它就构建一个与枚举的所有值等大的数组,按Enum. ordinal()下标来访问数组。性能与内存占用俱佳。</p><p>美中不足的是,因为要实现Map接口,而 V get(Object key)中key是Object而不是泛型K,所以安全起见,EnumMap每次访问都要先对Key进行类型判断,在JMC里录得不低的采样命中频率。</p><p>2.5 ConcurrentHashMap<br>并发优化的HashMap。</p><p>在JDK5里的经典设计,默认16把写锁(可以设置更多),有效分散了阻塞的概率。数据结构为Segment[],每个Segment一把锁。Segment里面才是哈希桶数组。Key先算出它在哪个Segment里,再去算它在哪个哈希桶里。</p><p>也没有读锁,因为put/remove动作是个原子动作(比如put的整个过程是一个对数组元素/Entry 指针的赋值操作),读操作不会看到一个更新动作的中间状态。</p><p>但在JDK8里,Segment[]的设计被抛弃了,改为精心设计的,只在需要锁的时候加锁。</p><p>支持ConcurrentMap接口,如putIfAbsent(key,value)与相反的replace(key,value)与以及实现CAS的replace(key, oldValue, newValue)。</p><p>2.6 ConcurrentSkipListMap<br>JDK6新增的并发优化的SortedMap,以SkipList结构实现。Concurrent包选用它是因为它支持基于CAS的无锁算法,而红黑树则没有好的无锁算法。</p><p>原理上,可以想象为多个链表组成的N层楼,其中的元素从稀疏到密集,每个元素有往右与往下的指针。从第一层楼开始遍历,如果右端的值比期望的大,那就往下走一层,继续往前走。</p><p>典型的空间换时间。每次插入,都要决定在哪几层插入,同时,要决定要不要多盖一层楼。</p><p>它的size()同样不能随便调,会遍历来统计。</p><h3 id="Set"><a href="#Set" class="headerlink" title="Set"></a>Set</h3><p>所有Set几乎都是内部用一个Map来实现, 因为Map里的KeySet就是一个Set,而value是假值,全部使用同一个Object即可。</p><p>Set的特征也继承了那些内部的Map实现的特征。</p><p>HashSet:内部是HashMap。</p><p>LinkedHashSet:内部是LinkedHashMap。</p><p>TreeSet:内部是TreeMap的SortedSet。</p><p>ConcurrentSkipListSet:内部是ConcurrentSkipListMap的并发优化的SortedSet。</p><p>CopyOnWriteArraySet:内部是CopyOnWriteArrayList的并发优化的Set,利用其addIfAbsent()方法实现元素去重,如前所述该方法的性能很一般。</p><p>好像少了个ConcurrentHashSet,本来也该有一个内部用ConcurrentHashMap的简单实现,但JDK偏偏没提供。Jetty就自己简单封了一个,Guava则直接用java.util.Collections.newSetFromMap(new ConcurrentHashMap()) 实现。</p><h3 id="Queue-1"><a href="#Queue-1" class="headerlink" title="Queue"></a>Queue</h3><p>Queue是在两端出入的List,所以也可以用数组或链表来实现。</p><p>4.1 普通队列<br>4.1.1 LinkedList<br>是的,以双向链表实现的LinkedList既是List,也是Queue。</p><p>4.1.2 ArrayDeque<br>以循环数组实现的双向Queue。大小是2的倍数,默认是16。</p><p>为了支持FIFO,即从数组尾压入元素(快),从数组头取出元素(超慢),就不能再使用普通ArrayList的实现了,改为使用循环数组。</p><p>有队头队尾两个下标:弹出元素时,队头下标递增;加入元素时,队尾下标递增。如果加入元素时已到数组空间的末尾,则将元素赋值到数组[0],同时队尾下标指向0,再插入下一个元素则赋值到数组[1],队尾下标指向1。如果队尾的下标追上队头,说明数组所有空间已用完,进行双倍的数组扩容。</p><p>4.1.3 PriorityQueue<br>用平衡二叉最小堆实现的优先级队列,不再是FIFO,而是按元素实现的Comparable接口或传入Comparator的比较结果来出队,数值越小,优先级越高,越先出队。但是注意其iterator()的返回不会排序。</p><p>平衡最小二叉堆,用一个简单的数组即可表达,可以快速寻址,没有指针什么的。最小的在queue[0] ,比如queue[4]的两个孩子,会在queue[2<em>4+1] 和 queue[2</em>(4+1)],即queue[9]和queue[10]。</p><p>入队时,插入queue[size],然后二叉地往上比较调整堆。</p><p>出队时,弹出queue[0],然后把queque[size]拿出来二叉地往下比较调整堆。</p><p>初始大小为11,空间不够时自动50%扩容。</p><p>4.2 线程安全的队列<br>4.2.1 ConcurrentLinkedQueue/Deque<br>无界的并发优化的Queue,基于链表,实现了依赖于CAS的无锁算法。</p><p>ConcurrentLinkedQueue的结构是单向链表和head/tail两个指针,因为入队时需要修改队尾元素的next指针,以及修改tail指向新入队的元素两个CAS动作无法原子,所以需要的特殊的算法。</p><p>4.3 线程安全的阻塞队列<br>BlockingQueue,一来如果队列已空不用重复的查看是否有新数据而会阻塞在那里,二来队列的长度受限,用以保证生产者与消费者的速度不会相差太远。当入队时队列已满,或出队时队列已空,不同函数的效果见下表:</p><p>立刻报异常 立刻返回布尔 阻塞等待 可设定等待时间<br>入队 add(e) offer(e) put(e) offer(e, timeout, unit)<br>出队 remove() poll() take() poll(timeout, unit)<br>查看 element() peek() 无 无</p><p>4.3.1 ArrayBlockingQueue<br>定长的并发优化的BlockingQueue,也是基于循环数组实现。有一把公共的锁与notFull、notEmpty两个Condition管理队列满或空时的阻塞状态。</p><p>4.3.2 LinkedBlockingQueue/Deque<br>可选定长的并发优化的BlockingQueue,基于链表实现,所以可以把长度设为Integer.MAX_VALUE成为无界无等待的。</p><p>利用链表的特征,分离了takeLock与putLock两把锁,继续用notEmpty、notFull管理队列满或空时的阻塞状态。</p><p>4.3.3 PriorityBlockingQueue<br>无界的PriorityQueue,也是基于数组存储的二叉堆(见前)。一把公共的锁实现线程安全。因为无界,空间不够时会自动扩容,所以入列时不会锁,出列为空时才会锁。</p><p>4.3.4 DelayQueue<br>内部包含一个PriorityQueue,同样是无界的,同样是出列时才会锁。一把公共的锁实现线程安全。元素需实现Delayed接口,每次调用时需返回当前离触发时间还有多久,小于0表示该触发了。</p><p>pull()时会用peek()查看队头的元素,检查是否到达触发时间。ScheduledThreadPoolExecutor用了类似的结构。</p><p></p><p>4.4 同步队列<br>SynchronousQueue同步队列本身无容量,放入元素时,比如等待元素被另一条线程的消费者取走再返回。JDK线程池里用它。</p><p>JDK7还有个LinkedTransferQueue,在普通线程安全的BlockingQueue的基础上,增加一个transfer(e) 函数,效果与SynchronousQueue一样。</p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java基础18:Java序列化与反序列化</title>
<link href="/2018/05/05/javase18/"/>
<url>/2018/05/05/javase18/</url>
<content type="html"><![CDATA[<p>本文介绍了Java序列化的基本概念,序列化和反序列化的使用方法,以及实现原理等,比较全面地总结序列化相关知识点,并且使用具体例子来加以佐证。</p><p>具体代码在我的GitHub中可以找到</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>喜欢的话麻烦点一下星哈谢谢。</p><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/05/05/javase18">https://h2pl.github.io/2018/05/05/javase18</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><blockquote><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p></blockquote><p>本文参考 <a href="http://www.importnew.com/17964.html和" target="_blank" rel="noopener">http://www.importnew.com/17964.html和</a><br><a href="https://www.ibm.com/developerworks/cn/java/j-lo-serial/" target="_blank" rel="noopener">https://www.ibm.com/developerworks/cn/java/j-lo-serial/</a></p><a id="more"></a><h2 id="序列化与反序列化概念"><a href="#序列化与反序列化概念" class="headerlink" title="序列化与反序列化概念"></a>序列化与反序列化概念</h2><p>序列化 (Serialization)是将对象的状态信息转换为可以存储或传输的形式的过程。一般将一个对象存储至一个储存媒介,例如档案或是记亿体缓冲等。在网络传输过程中,可以是字节或是XML等格式。而字节的或XML编码格式可以还原完全相等的对象。这个相反的过程又称为反序列化。</p><p><strong>Java对象的序列化与反序列化</strong></p><p>在Java中,我们可以通过多种方式来创建对象,并且只要对象没有被回收我们都可以复用该对象。但是,我们创建出来的这些Java对象都是存在于JVM的堆内存中的。</p><p>只有JVM处于运行状态的时候,这些对象才可能存在。一旦JVM停止运行,这些对象的状态也就随之而丢失了。</p><p>但是在真实的应用场景中,我们需要将这些对象持久化下来,并且能够在需要的时候把对象重新读取出来。Java的对象序列化可以帮助我们实现该功能。</p><blockquote><p>对象序列化机制(object serialization)是Java语言内建的一种对象持久化方式,通过对象序列化,可以把对象的状态保存为字节数组,并且可以在有需要的时候将这个字节数组通过反序列化的方式再转换成对象。</p></blockquote><p>对象序列化可以很容易的在JVM中的活动对象和字节数组(流)之间进行转换。</p><p>在Java中,对象的序列化与反序列化被广泛应用到RMI(远程方法调用)及网络传输中。</p><h2 id="相关接口及类"><a href="#相关接口及类" class="headerlink" title="相关接口及类"></a>相关接口及类</h2><p>Java为了方便开发人员将Java对象进行序列化及反序列化提供了一套方便的API来支持。其中包括以下接口和类:</p><pre><code>java.io.Serializablejava.io.ExternalizableObjectOutputObjectInputObjectOutputStreamObjectInputStreamSerializable 接口</code></pre><p><strong>类通过实现 java.io.Serializable 接口以启用其序列化功能。</strong></p><p>未实现此接口的类将无法使其任何状态序列化或反序列化。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。 (该接口并没有方法和字段,为什么只有实现了该接口的类的对象才能被序列化呢?)</p><p>当试图对一个对象进行序列化的时候,如果遇到不支持 Serializable 接口的对象。在此情况下,将抛出NotSerializableException。</p><p>如果要序列化的类有父类,要想同时将在父类中定义过的变量持久化下来,那么父类也应该集成java.io.Serializable接口。</p><p>下面是一个实现了java.io.Serializable接口的类</p><pre><code>public class 序列化和反序列化 { public static void main(String[] args) { } //注意,内部类不能进行序列化,因为它依赖于外部类 @Test public void test() throws IOException { A a = new A(); a.i = 1; a.s = "a"; FileOutputStream fileOutputStream = null; FileInputStream fileInputStream = null; try { //将obj写入文件 fileOutputStream = new FileOutputStream("temp"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(a); fileOutputStream.close(); //通过文件读取obj fileInputStream = new FileInputStream("temp"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); A a2 = (A) objectInputStream.readObject(); fileInputStream.close(); System.out.println(a2.i); System.out.println(a2.s); //打印结果和序列化之前相同 } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }}class A implements Serializable { int i; String s;}</code></pre><p><strong>Externalizable接口</strong></p><p>除了Serializable 之外,java中还提供了另一个序列化接口Externalizable</p><p>为了了解Externalizable接口和Serializable接口的区别,先来看代码,我们把上面的代码改成使用Externalizable的形式。</p><pre><code>class B implements Externalizable { //必须要有公开无参构造函数。否则报错 public B() { } int i; String s; @Override public void writeExternal(ObjectOutput out) throws IOException { } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { }}@Test public void test2() throws IOException, ClassNotFoundException { B b = new B(); b.i = 1; b.s = "a"; //将obj写入文件 FileOutputStream fileOutputStream = new FileOutputStream("temp"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(b); fileOutputStream.close(); //通过文件读取obj FileInputStream fileInputStream = new FileInputStream("temp"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); B b2 = (B) objectInputStream.readObject(); fileInputStream.close(); System.out.println(b2.i); System.out.println(b2.s); //打印结果为0和null,即初始值,没有被赋值 //0 //null }</code></pre><p>通过上面的实例可以发现,对B类进行序列化及反序列化之后得到的对象的所有属性的值都变成了默认值。也就是说,之前的那个对象的状态并没有被持久化下来。这就是Externalizable接口和Serializable接口的区别:</p><p>Externalizable继承了Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。</p><p>当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。由于上面的代码中,并没有在这两个方法中定义序列化实现细节,所以输出的内容为空。</p><blockquote><p>还有一点值得注意:在使用Externalizable进行序列化的时候,在读取对象时,会调用被序列化类的无参构造器去创建一个新的对象,然后再将被保存对象的字段的值分别填充到新对象中。所以,实现Externalizable接口的类必须要提供一个public的无参的构造器。</p></blockquote><pre><code>class C implements Externalizable { int i; int j; String s; public C() { } //实现下面两个方法可以选择序列化中需要被复制的成员。 //并且,写入顺序和读取顺序要一致,否则报错。 //可以写入多个同类型变量,顺序保持一致即可。 @Override public void writeExternal(ObjectOutput out) throws IOException { out.writeInt(i); out.writeInt(j); out.writeObject(s); } @Override public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { i = in.readInt(); j = in.readInt(); s = (String) in.readObject(); }}@Test public void test3() throws IOException, ClassNotFoundException { C c = new C(); c.i = 1; c.j = 2; c.s = "a"; //将obj写入文件 FileOutputStream fileOutputStream = new FileOutputStream("temp"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(fileOutputStream); objectOutputStream.writeObject(c); fileOutputStream.close(); //通过文件读取obj FileInputStream fileInputStream = new FileInputStream("temp"); ObjectInputStream objectInputStream = new ObjectInputStream(fileInputStream); C c2 = (C) objectInputStream.readObject(); fileInputStream.close(); System.out.println(c2.i); System.out.println(c2.j); System.out.println(c2.s); //打印结果为0和null,即初始值,没有被赋值 //0 //null }</code></pre><h2 id="序列化ID"><a href="#序列化ID" class="headerlink" title="序列化ID"></a>序列化ID</h2><p>序列化 ID 问题<br>情境:两个客户端 A 和 B 试图通过网络传递对象数据,A 端将对象 C 序列化为二进制数据再传给 B,B 反序列化得到 C。</p><p>问题:C 对象的全类路径假设为 com.inout.Test,在 A 和 B 端都有这么一个类文件,功能代码完全一致。也都实现了 Serializable 接口,但是反序列化时总是提示不成功。</p><p>解决:虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID = 1L)。清单 1 中,虽然两个类的功能代码完全一致,但是序列化 ID 不同,他们无法相互序列化和反序列化。</p><pre><code>package com.inout; import java.io.Serializable; public class A implements Serializable { private static final long serialVersionUID = 1L; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } } package com.inout; import java.io.Serializable; public class A implements Serializable { private static final long serialVersionUID = 2L; private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }</code></pre><h2 id="静态变量不参与序列化"><a href="#静态变量不参与序列化" class="headerlink" title="静态变量不参与序列化"></a>静态变量不参与序列化</h2><p>清单 2 中的 main 方法,将对象序列化后,修改静态变量的数值,再将序列化对象读取出来,然后通过读取出来的对象获得静态变量的数值并打印出来。依照清单 2,这个 System.out.println(t.staticVar) 语句输出的是 10 还是 5 呢?</p><pre><code>public class Test implements Serializable { private static final long serialVersionUID = 1L; public static int staticVar = 5; public static void main(String[] args) { try { //初始时staticVar为5 ObjectOutputStream out = new ObjectOutputStream( new FileOutputStream("result.obj")); out.writeObject(new Test()); out.close(); //序列化后修改为10 Test.staticVar = 10; ObjectInputStream oin = new ObjectInputStream(new FileInputStream( "result.obj")); Test t = (Test) oin.readObject(); oin.close(); //再读取,通过t.staticVar打印新的值 System.out.println(t.staticVar); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (ClassNotFoundException e) { e.printStackTrace(); } }}</code></pre><p>最后的输出是 10,对于无法理解的读者认为,打印的 staticVar 是从读取的对象里获得的,应该是保存时的状态才对。之所以打印 10 的原因在于序列化时,并不保存静态变量,这其实比较容易理解,序列化保存的是对象的状态,静态变量属于类的状态,因此 序列化并不保存静态变量。</p><h2 id="探究ArrayList的序列化"><a href="#探究ArrayList的序列化" class="headerlink" title="探究ArrayList的序列化"></a>探究ArrayList的序列化</h2><p>ArrayList的序列化<br>在介绍ArrayList序列化之前,先来考虑一个问题:</p><p>如何自定义的序列化和反序列化策略</p><p>带着这个问题,我们来看java.util.ArrayList的源码</p><pre><code>public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{ private static final long serialVersionUID = 8683452581122892189L; transient Object[] elementData; // non-private to simplify nested class access private int size;}</code></pre><p>笔者省略了其他成员变量,从上面的代码中可以知道ArrayList实现了java.io.Serializable接口,那么我们就可以对它进行序列化及反序列化。</p><p>因为elementData是transient的(1.8好像改掉了这一点),所以我们认为这个成员变量不会被序列化而保留下来。我们写一个Demo,验证一下我们的想法:</p><pre><code>public class ArrayList的序列化 { public static void main(String[] args) throws IOException, ClassNotFoundException { ArrayList list = new ArrayList(); list.add("a"); list.add("b"); ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("arr")); objectOutputStream.writeObject(list); objectOutputStream.close(); ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("arr")); ArrayList list1 = (ArrayList) objectInputStream.readObject(); objectInputStream.close(); System.out.println(Arrays.toString(list.toArray())); //序列化成功,里面的元素保持不变。 }</code></pre><p>了解ArrayList的人都知道,ArrayList底层是通过数组实现的。那么数组elementData其实就是用来保存列表中的元素的。通过该属性的声明方式我们知道,他是无法通过序列化持久化下来的。那么为什么code 4的结果却通过序列化和反序列化把List中的元素保留下来了呢?</p><p><strong>writeObject和readObject方法</strong></p><p>在ArrayList中定义了来个方法: writeObject和readObject。</p><p>这里先给出结论:</p><blockquote><p>在序列化过程中,如果被序列化的类中定义了writeObject 和 readObject 方法,虚拟机会试图调用对象类里的 writeObject 和 readObject 方法,进行用户自定义的序列化和反序列化。</p><p>如果没有这样的方法,则默认调用是 ObjectOutputStream 的 defaultWriteObject 方法以及 ObjectInputStream 的 defaultReadObject 方法。</p><p>用户自定义的 writeObject 和 readObject 方法可以允许用户控制序列化的过程,比如可以在序列化的过程中动态改变序列化的数值。</p></blockquote><p>来看一下这两个方法的具体实现:</p><pre><code>private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { elementData = EMPTY_ELEMENTDATA; // Read in size, and any hidden stuff s.defaultReadObject(); // Read in capacity s.readInt(); // ignored if (size > 0) { // be like clone(), allocate array based upon size not capacity ensureCapacityInternal(size); Object[] a = elementData; // Read in all elements in the proper order. for (int i=0; i<size; i++) { a[i] = s.readObject(); } } }private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount = modCount; s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone() s.writeInt(size); // Write out all elements in the proper order. for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } }</code></pre><p>那么为什么ArrayList要用这种方式来实现序列化呢?</p><pre><code>why transientArrayList实际上是动态数组,每次在放满以后自动增长设定的长度值,如果数组自动增长长度设为100,而实际只放了一个元素,那就会序列化99个null元素。为了保证在序列化的时候不会将这么多null同时进行序列化,ArrayList把元素数组设置为transient。why writeObject and readObject前面说过,为了防止一个包含大量空对象的数组被序列化,为了优化存储,所以,ArrayList使用transient来声明elementData。 但是,作为一个集合,在序列化过程中还必须保证其中的元素可以被持久化下来,所以,通过重写writeObject 和 readObject方法的方式把其中的元素保留下来。writeObject方法把elementData数组中的元素遍历的保存到输出流(ObjectOutputStream)中。readObject方法从输入流(ObjectInputStream)中读出对象并保存赋值到elementData数组中。</code></pre><h2 id="如何自定义的序列化和反序列化策略"><a href="#如何自定义的序列化和反序列化策略" class="headerlink" title="如何自定义的序列化和反序列化策略"></a>如何自定义的序列化和反序列化策略</h2><p>延续上一部分,刚刚我们明白了ArrayList序列化数组元素的原理。</p><p>至此,我们先试着来回答刚刚提出的问题:</p><p>如何自定义的序列化和反序列化策略</p><p>答:可以通过在被序列化的类中增加writeObject 和 readObject方法。那么问题又来了:</p><blockquote><p>虽然ArrayList中写了writeObject 和 readObject 方法,但是这两个方法并没有显示的被调用啊。</p><p>那么如果一个类中包含writeObject 和 readObject 方法,那么这两个方法是怎么被调用的呢?</p></blockquote><p>ObjectOutputStream<br>从code 4中,我们可以看出,对象的序列化过程通过ObjectOutputStream和ObjectInputputStream来实现的,那么带着刚刚的问题,我们来分析一下ArrayList中的writeObject 和 readObject 方法到底是如何被调用的呢?</p><p>为了节省篇幅,这里给出ObjectOutputStream的writeObject的调用栈:</p><p>writeObject —> writeObject0 —>writeOrdinaryObject—>writeSerialData—>invokeWriteObject</p><p>这里看一下invokeWriteObject:</p><pre><code>void invokeWriteObject(Object obj, ObjectOutputStream out) throws IOException, UnsupportedOperationException { if (writeObjectMethod != null) { try { writeObjectMethod.invoke(obj, new Object[]{ out }); } catch (InvocationTargetException ex) { Throwable th = ex.getTargetException(); if (th instanceof IOException) { throw (IOException) th; } else { throwMiscException(th); } } catch (IllegalAccessException ex) { // should not occur, as access checks have been suppressed throw new InternalError(ex); } } else { throw new UnsupportedOperationException(); } }</code></pre><p>其中writeObjectMethod.invoke(obj, new Object[]{ out });是关键,通过反射的方式调用writeObjectMethod方法。官方是这么解释这个writeObjectMethod的:</p><p>class-defined writeObject method, or null if none</p><p>在我们的例子中,这个方法就是我们在ArrayList中定义的writeObject方法。通过反射的方式被调用了。</p><p>至此,我们先试着来回答刚刚提出的问题:</p><pre><code>如果一个类中包含writeObject 和 readObject 方法,那么这两个方法是怎么被调用的?答:在使用ObjectOutputStream的writeObject方法和ObjectInputStream的readObject方法时,会通过反射的方式调用。</code></pre><h2 id="为什么要实现Serializable"><a href="#为什么要实现Serializable" class="headerlink" title="为什么要实现Serializable"></a>为什么要实现Serializable</h2><p>至此,我们已经介绍完了ArrayList的序列化方式。那么,不知道有没有人提出这样的疑问:</p><p>Serializable明明就是一个空的接口,它是怎么保证只有实现了该接口的方法才能进行序列化与反序列化的呢?</p><pre><code>Serializable接口的定义:public interface Serializable {}读者可以尝试把code 1中的继承Serializable的代码去掉,再执行code 2,会抛出java.io.NotSerializableException。</code></pre><p>其实这个问题也很好回答,我们再回到刚刚ObjectOutputStream的writeObject的调用栈:</p><pre><code>writeObject ---> writeObject0 --->writeOrdinaryObject--->writeSerialData--->invokeWriteObject</code></pre><p>writeObject0方法中有这么一段代码:</p><pre><code>if (obj instanceof String) { writeString((String) obj, unshared); } else if (cl.isArray()) { writeArray(obj, desc, unshared); } else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); } else if (obj instanceof Serializable) { writeOrdinaryObject(obj, desc, unshared); } else { if (extendedDebugInfo) { throw new NotSerializableException( cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException(cl.getName()); } }</code></pre><p>在进行序列化操作时,会判断要被序列化的类是否是Enum、Array和Serializable类型,如果不是则直接抛出NotSerializableException。</p><h2 id="序列化知识点总结"><a href="#序列化知识点总结" class="headerlink" title="序列化知识点总结"></a>序列化知识点总结</h2><blockquote><p>1、如果一个类想被序列化,需要实现Serializable接口。否则将抛出NotSerializableException异常,这是因为,在序列化操作过程中会对类型进行检查,要求被序列化的类必须属于Enum、Array和Serializable类型其中的任何一种。</p><p>2、通过ObjectOutputStream和ObjectInputStream对对象进行序列化及反序列化</p><p>3、虚拟机是否允许反序列化,不仅取决于类路径和功能代码是否一致,一个非常重要的一点是两个类的序列化 ID 是否一致(就是 private static final long serialVersionUID)</p><p>序列化 ID 在 Eclipse 下提供了两种生成策略,一个是固定的 1L,一个是随机生成一个不重复的 long 类型数据(实际上是使用 JDK 工具生成),在这里有一个建议,如果没有特殊需求,就是用默认的 1L 就可以,这样可以确保代码一致时反序列化成功。那么随机生成的序列化 ID 有什么作用呢,有些时候,通过改变序列化 ID 可以用来限制某些用户的使用。</p><p>4、序列化并不保存静态变量。</p><p>5、要想将父类对象也序列化,就需要让父类也实现Serializable 接口。</p><p>6、Transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。</p><p>7、服务器端给客户端发送序列化对象数据,对象中有一些数据是敏感的,比如密码字符串等,希望对该密码字段在序列化时,进行加密,而客户端如果拥有解密的密钥,只有在客户端进行反序列化时,才可以对密码进行读取,这样可以一定程度保证序列化对象的数据安全。</p><p>8、在类中增加writeObject 和 readObject 方法可以实现自定义序列化策略</p></blockquote>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java基础17:Java IO流总结</title>
<link href="/2018/05/04/javase17/"/>
<url>/2018/05/04/javase17/</url>
<content type="html"><![CDATA[<p>本文介绍了Java IO流的基本概念,使用方法,以及使用的注意事项等。帮助你更好地理解和使用Java的IO流。</p><p>具体代码在我的GitHub中可以找到</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>喜欢的话麻烦点一下星哈谢谢。</p><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/05/04/javase17">https://h2pl.github.io/2018/05/04/javase17</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><blockquote><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p></blockquote><p>本文参考</p><p>并发编程网 – ifeve.com</p><a id="more"></a><h2 id="IO流概述"><a href="#IO流概述" class="headerlink" title="IO流概述"></a>IO流概述</h2><blockquote><p>在这一小节,我会试着给出Java IO(java.io)包下所有类的概述。更具体地说,我会根据类的用途对类进行分组。这个分组将会使你在未来的工作中,进行类的用途判定时,或者是为某个特定用途选择类时变得更加容易。</p></blockquote><p><strong>输入和输出</strong></p><pre><code>术语“输入”和“输出”有时候会有一点让人疑惑。一个应用程序的输入往往是另外一个应用程序的输出那么OutputStream流到底是一个输出到目的地的流呢,还是一个产生输出的流?InputStream流到底会不会输出它的数据给读取数据的程序呢?就我个人而言,在第一天学习Java IO的时候我就感觉到了一丝疑惑。为了消除这个疑惑,我试着给输入和输出起一些不一样的别名,让它们从概念上与数据的来源和数据的流向相联系。</code></pre><p>Java的IO包主要关注的是从原始数据源的读取以及输出原始数据到目标媒介。以下是最典型的数据源和目标媒介:</p><pre><code>文件管道网络连接内存缓存System.in, System.out, System.error(注:Java标准输入、输出、错误输出)</code></pre><p>下面这张图描绘了一个程序从数据源读取数据,然后将数据输出到其他媒介的原理:</p><p><img src="http://ifeve.com/wp-content/uploads/2014/10/%E6%97%A0%E6%A0%87%E9%A2%981.png" alt="image"></p><p><strong>流</strong></p><pre><code>在Java IO中,流是一个核心的概念。流从概念上来说是一个连续的数据流。你既可以从流中读取数据,也可以往流中写数据。流与数据源或者数据流向的媒介相关联。在Java IO中流既可以是字节流(以字节为单位进行读写),也可以是字符流(以字符为单位进行读写)。</code></pre><p>类InputStream, OutputStream, Reader 和Writer<br>一个程序需要InputStream或者Reader从数据源读取数据,需要OutputStream或者Writer将数据写入到目标媒介中。以下的图说明了这一点:</p><p><img src="http://ifeve.com/wp-content/uploads/2014/10/%E6%97%A0%E6%A0%87%E9%A2%982.png" alt="image"></p><p>InputStream和Reader与数据源相关联,OutputStream和writer与目标媒介相关联。</p><p><strong>Java IO的用途和特征</strong></p><p>Java IO中包含了许多InputStream、OutputStream、Reader、Writer的子类。这样设计的原因是让每一个类都负责不同的功能。这也就是为什么IO包中有这么多不同的类的缘故。各类用途汇总如下:</p><pre><code>文件访问网络访问内存缓存访问线程内部通信(管道)缓冲过滤解析读写文本 (Readers / Writers)读写基本类型数据 (long, int etc.)读写对象</code></pre><p>当通读过Java IO类的源代码之后,我们很容易就能了解这些用途。这些用途或多或少让我们更加容易地理解,不同的类用于针对不同业务场景。</p><p>Java IO类概述表<br>已经讨论了数据源、目标媒介、输入、输出和各类不同用途的Java IO类,接下来是一张通过输入、输出、基于字节或者字符、以及其他比如缓冲、解析之类的特定用途划分的大部分Java IO类的表格。</p><p><img src="http://ifeve.com/wp-content/uploads/2014/10/QQ%E6%88%AA%E5%9B%BE20141020174145.png" alt="image"></p><p>Java IO类图</p><p><img src="https://images.cnblogs.com/cnblogs_com/davidgu/java_io_hierarchy.jpg" alt="image"></p><h2 id="什么是Java-IO流"><a href="#什么是Java-IO流" class="headerlink" title="什么是Java IO流"></a>什么是Java IO流</h2><p>Java IO流是既可以从中读取,也可以写入到其中的数据流。正如这个系列教程之前提到过的,流通常会与数据源、数据流向目的地相关联,比如文件、网络等等。</p><p>流和数组不一样,不能通过索引读写数据。在流中,你也不能像数组那样前后移动读取数据,除非使用RandomAccessFile 处理文件。流仅仅只是一个连续的数据流。</p><p>某些类似PushbackInputStream 流的实现允许你将数据重新推回到流中,以便重新读取。然而你只能把有限的数据推回流中,并且你不能像操作数组那样随意读取数据。流中的数据只能够顺序访问。</p><blockquote><p>Java IO流通常是基于字节或者基于字符的。字节流通常以“stream”命名,比如InputStream和OutputStream。除了DataInputStream 和DataOutputStream 还能够读写int, long, float和double类型的值以外,其他流在一个操作时间内只能读取或者写入一个原始字节。</p><p>字符流通常以“Reader”或者“Writer”命名。字符流能够读写字符(比如Latin1或者Unicode字符)。可以浏览Java Readers and Writers获取更多关于字符流输入输出的信息。</p></blockquote><p><strong>InputStream</strong></p><p>java.io.InputStream类是所有Java IO输入流的基类。如果你正在开发一个从流中读取数据的组件,请尝试用InputStream替代任何它的子类(比如FileInputStream)进行开发。这么做能够让你的代码兼容任何类型而非某种确定类型的输入流。</p><p><strong>组合流</strong></p><p>你可以将流整合起来以便实现更高级的输入和输出操作。比如,一次读取一个字节是很慢的,所以可以从磁盘中一次读取一大块数据,然后从读到的数据块中获取字节。为了实现缓冲,可以把InputStream包装到BufferedInputStream中。</p><p>代码示例<br> InputStream input = new BufferedInputStream(new FileInputStream(“c:\data\input-file.txt”));</p><blockquote><p>缓冲同样可以应用到OutputStream中。你可以实现将大块数据批量地写入到磁盘(或者相应的流)中,这个功能由BufferedOutputStream实现。</p><p>缓冲只是通过流整合实现的其中一个效果。你可以把InputStream包装到PushbackInputStream中,之后可以将读取过的数据推回到流中重新读取,在解析过程中有时候这样做很方便。或者,你可以将两个InputStream整合成一个SequenceInputStream。</p><p>将不同的流整合到一个链中,可以实现更多种高级操作。通过编写包装了标准流的类,可以实现你想要的效果和过滤器。</p></blockquote><h2 id="IO文件"><a href="#IO文件" class="headerlink" title="IO文件"></a>IO文件</h2><p>在Java应用程序中,文件是一种常用的数据源或者存储数据的媒介。所以这一小节将会对Java中文件的使用做一个简短的概述。这篇文章不会对每一个技术细节都做出解释,而是会针对文件存取的方法提供给你一些必要的知识点。在之后的文章中,将会更加详细地描述这些方法或者类,包括方法示例等等。</p><p><strong>通过Java IO读文件</strong></p><pre><code>如果你需要在不同端之间读取文件,你可以根据该文件是二进制文件还是文本文件来选择使用FileInputStream或者FileReader。这两个类允许你从文件开始到文件末尾一次读取一个字节或者字符,或者将读取到的字节写入到字节数组或者字符数组。你不必一次性读取整个文件,相反你可以按顺序地读取文件中的字节和字符。</code></pre><p>如果你需要跳跃式地读取文件其中的某些部分,可以使用RandomAccessFile。</p><p><strong>通过Java IO写文件</strong></p><pre><code>如果你需要在不同端之间进行文件的写入,你可以根据你要写入的数据是二进制型数据还是字符型数据选用FileOutputStream或者FileWriter。你可以一次写入一个字节或者字符到文件中,也可以直接写入一个字节数组或者字符数据。数据按照写入的顺序存储在文件当中。</code></pre><p><strong>通过Java IO随机存取文件</strong></p><p>正如我所提到的,你可以通过RandomAccessFile对文件进行随机存取。</p><pre><code>随机存取并不意味着你可以在真正随机的位置进行读写操作,它只是意味着你可以跳过文件中某些部分进行操作,并且支持同时读写,不要求特定的存取顺序。这使得RandomAccessFile可以覆盖一个文件的某些部分、或者追加内容到它的末尾、或者删除它的某些内容,当然它也可以从文件的任何位置开始读取文件。</code></pre><p>下面是具体例子:</p><pre><code>@Test //文件流范例,打开一个文件的输入流,读取到字节数组,再写入另一个文件的输出流 public void test1() { try { FileInputStream fileInputStream = new FileInputStream(new File("a.txt")); FileOutputStream fileOutputStream = new FileOutputStream(new File("b.txt")); byte []buffer = new byte[128]; while (fileInputStream.read(buffer) != -1) { fileOutputStream.write(buffer); } //随机读写,通过mode参数来决定读或者写 RandomAccessFile randomAccessFile = new RandomAccessFile(new File("c.txt"), "rw"); } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } }</code></pre><h2 id="字符流和字节流"><a href="#字符流和字节流" class="headerlink" title="字符流和字节流"></a>字符流和字节流</h2><p>Java IO的Reader和Writer除了基于字符之外,其他方面都与InputStream和OutputStream非常类似。他们被用于读写文本。InputStream和OutputStream是基于字节的,还记得吗?</p><p>Reader<br>Reader类是Java IO中所有Reader的基类。子类包括BufferedReader,PushbackReader,InputStreamReader,StringReader和其他Reader。</p><p>Writer<br>Writer类是Java IO中所有Writer的基类。子类包括BufferedWriter和PrintWriter等等。</p><p>这是一个简单的Java IO Reader的例子:</p><pre><code>Reader reader = new FileReader("c:\\data\\myfile.txt");int data = reader.read();while(data != -1){ char dataChar = (char) data; data = reader.read();}</code></pre><p>你通常会使用Reader的子类,而不会直接使用Reader。Reader的子类包括InputStreamReader,CharArrayReader,FileReader等等。可以查看Java IO概述浏览完整的Reader表格。</p><p><strong>整合Reader与InputStream</strong></p><p>一个Reader可以和一个InputStream相结合。如果你有一个InputStream输入流,并且想从其中读取字符,可以把这个InputStream包装到InputStreamReader中。把InputStream传递到InputStreamReader的构造函数中:</p><pre><code>Reader reader = new InputStreamReader(inputStream);</code></pre><p>在构造函数中可以指定解码方式。</p><p><strong>Writer</strong></p><p>Writer类是Java IO中所有Writer的基类。子类包括BufferedWriter和PrintWriter等等。这是一个Java IO Writer的例子:</p><pre><code>Writer writer = new FileWriter("c:\\data\\file-output.txt"); writer.write("Hello World Writer"); writer.close();</code></pre><p>同样,你最好使用Writer的子类,不需要直接使用Writer,因为子类的实现更加明确,更能表现你的意图。常用子类包括OutputStreamWriter,CharArrayWriter,FileWriter等。Writer的write(int c)方法,会将传入参数的低16位写入到Writer中,忽略高16位的数据。</p><p><strong>整合Writer和OutputStream</strong></p><p>与Reader和InputStream类似,一个Writer可以和一个OutputStream相结合。把OutputStream包装到OutputStreamWriter中,所有写入到OutputStreamWriter的字符都将会传递给OutputStream。这是一个OutputStreamWriter的例子:</p><pre><code>Writer writer = new OutputStreamWriter(outputStream);</code></pre><h2 id="IO管道"><a href="#IO管道" class="headerlink" title="IO管道"></a>IO管道</h2><p>Java IO中的管道为运行在同一个JVM中的两个线程提供了通信的能力。所以管道也可以作为数据源以及目标媒介。</p><p>你不能利用管道与不同的JVM中的线程通信(不同的进程)。在概念上,Java的管道不同于Unix/Linux系统中的管道。在Unix/Linux中,运行在不同地址空间的两个进程可以通过管道通信。在Java中,通信的双方应该是运行在同一进程中的不同线程。</p><p>通过Java IO创建管道</p><pre><code>可以通过Java IO中的PipedOutputStream和PipedInputStream创建管道。一个PipedInputStream流应该和一个PipedOutputStream流相关联。一个线程通过PipedOutputStream写入的数据可以被另一个线程通过相关联的PipedInputStream读取出来。</code></pre><p>Java IO管道示例<br>这是一个如何将PipedInputStream和PipedOutputStream关联起来的简单例子:</p><pre><code>//使用管道来完成两个线程间的数据点对点传递 @Test public void test2() throws IOException { PipedInputStream pipedInputStream = new PipedInputStream(); PipedOutputStream pipedOutputStream = new PipedOutputStream(pipedInputStream); new Thread(new Runnable() { @Override public void run() { try { pipedOutputStream.write("hello input".getBytes()); pipedOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } }).start(); new Thread(new Runnable() { @Override public void run() { try { byte []arr = new byte[128]; while (pipedInputStream.read(arr) != -1) { System.out.println(Arrays.toString(arr)); } pipedInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } }).start();</code></pre><p>管道和线程<br>请记得,当使用两个相关联的管道流时,务必将它们分配给不同的线程。read()方法和write()方法调用时会导致流阻塞,这意味着如果你尝试在一个线程中同时进行读和写,可能会导致线程死锁。</p><p>管道的替代<br>除了管道之外,一个JVM中不同线程之间还有许多通信的方式。实际上,线程在大多数情况下会传递完整的对象信息而非原始的字节数据。但是,如果你需要在线程之间传递字节数据,Java IO的管道是一个不错的选择。</p><h2 id="Java-IO:网络"><a href="#Java-IO:网络" class="headerlink" title="Java IO:网络"></a>Java IO:网络</h2><p>Java中网络的内容或多或少的超出了Java IO的范畴。关于Java网络更多的是在我的Java网络教程中探讨。但是既然网络是一个常见的数据来源以及数据流目的地,并且因为你使用Java IO的API通过网络连接进行通信,所以本文将简要的涉及网络应用。</p><p>当两个进程之间建立了网络连接之后,他们通信的方式如同操作文件一样:利用InputStream读取数据,利用OutputStream写入数据。换句话来说,Java网络API用来在不同进程之间建立网络连接,而Java IO则用来在建立了连接之后的进程之间交换数据。</p><p>基本上意味着如果你有一份能够对文件进行写入某些数据的代码,那么这些数据也可以很容易地写入到网络连接中去。你所需要做的仅仅只是在代码中利用OutputStream替代FileOutputStream进行数据的写入。因为FileOutputStream是OuputStream的子类,所以这么做并没有什么问题。</p><pre><code>//从网络中读取字节流也可以直接使用OutputStreampublic void test3() { //读取网络进程的输出流 OutputStream outputStream = new OutputStream() { @Override public void write(int b) throws IOException { } };}public void process(OutputStream ouput) throws IOException { //处理网络信息 //do something with the OutputStream}</code></pre><h2 id="字节和字符数组"><a href="#字节和字符数组" class="headerlink" title="字节和字符数组"></a>字节和字符数组</h2><p>从InputStream或者Reader中读入数组</p><p>从OutputStream或者Writer中写数组</p><p>在java中常用字节和字符数组在应用中临时存储数据。而这些数组又是通常的数据读取来源或者写入目的地。如果你需要在程序运行时需要大量读取文件里的内容,那么你也可以把一个文件加载到数组中。</p><p>前面的例子中,字符数组或字节数组是用来缓存数据的临时存储空间,不过它们同时也可以作为数据来源或者写入目的地。<br>举个例子:</p><pre><code>//字符数组和字节数组在io过程中的作用 public void test4() { //arr和brr分别作为数据源 char []arr = {'a','c','d'}; CharArrayReader charArrayReader = new CharArrayReader(arr); byte []brr = {1,2,3,4,5}; ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(brr); }</code></pre><h2 id="System-in-System-out-System-err"><a href="#System-in-System-out-System-err" class="headerlink" title="System.in, System.out, System.err"></a>System.in, System.out, System.err</h2><p>System.in, System.out, System.err这3个流同样是常见的数据来源和数据流目的地。使用最多的可能是在控制台程序里利用System.out将输出打印到控制台上。</p><p>JVM启动的时候通过Java运行时初始化这3个流,所以你不需要初始化它们(尽管你可以在运行时替换掉它们)。</p><pre><code>System.inSystem.in是一个典型的连接控制台程序和键盘输入的InputStream流。通常当数据通过命令行参数或者配置文件传递给命令行Java程序的时候,System.in并不是很常用。图形界面程序通过界面传递参数给程序,这是一块单独的Java IO输入机制。System.outSystem.out是一个PrintStream流。System.out一般会把你写到其中的数据输出到控制台上。System.out通常仅用在类似命令行工具的控制台程序上。System.out也经常用于打印程序的调试信息(尽管它可能并不是获取程序调试信息的最佳方式)。System.errSystem.err是一个PrintStream流。System.err与System.out的运行方式类似,但它更多的是用于打印错误文本。一些类似Eclipse的程序,为了让错误信息更加显眼,会将错误信息以红色文本的形式通过System.err输出到控制台上。</code></pre><p>System.out和System.err的简单例子:<br>这是一个System.out和System.err结合使用的简单示例:</p><pre><code> //测试System.in, System.out, System.err public static void main(String[] args) { int in = new Scanner(System.in).nextInt(); System.out.println(in); System.out.println("out"); System.err.println("err"); //输入10,结果是// err(红色)// 10// out }</code></pre><h2 id="字符流的Buffered和Filter"><a href="#字符流的Buffered和Filter" class="headerlink" title="字符流的Buffered和Filter"></a>字符流的Buffered和Filter</h2><p>BufferedReader能为字符输入流提供缓冲区,可以提高许多IO处理的速度。你可以一次读取一大块的数据,而不需要每次从网络或者磁盘中一次读取一个字节。特别是在访问大量磁盘数据时,缓冲通常会让IO快上许多。</p><p>BufferedReader和BufferedInputStream的主要区别在于,BufferedReader操作字符,而BufferedInputStream操作原始字节。只需要把Reader包装到BufferedReader中,就可以为Reader添加缓冲区(译者注:默认缓冲区大小为8192字节,即8KB)。代码如下:</p><pre><code>Reader input = new BufferedReader(new FileReader("c:\\data\\input-file.txt"));</code></pre><p>你也可以通过传递构造函数的第二个参数,指定缓冲区大小,代码如下:</p><pre><code>Reader input = new BufferedReader(new FileReader("c:\\data\\input-file.txt"), 8 * 1024);</code></pre><p>这个例子设置了8KB的缓冲区。最好把缓冲区大小设置成1024字节的整数倍,这样能更高效地利用内置缓冲区的磁盘。</p><p>除了能够为输入流提供缓冲区以外,其余方面BufferedReader基本与Reader类似。BufferedReader还有一个额外readLine()方法,可以方便地一次性读取一整行字符。</p><p><strong>BufferedWriter</strong></p><p>与BufferedReader类似,BufferedWriter可以为输出流提供缓冲区。可以构造一个使用默认大小缓冲区的BufferedWriter(译者注:默认缓冲区大小8 * 1024B),代码如下:</p><pre><code>Writer writer = new BufferedWriter(new FileWriter("c:\\data\\output-file.txt"));</code></pre><p>也可以手动设置缓冲区大小,代码如下:</p><pre><code>Writer writer = new BufferedWriter(new FileWriter("c:\\data\\output-file.txt"), 8 * 1024);</code></pre><p>为了更好地使用内置缓冲区的磁盘,同样建议把缓冲区大小设置成1024的整数倍。除了能够为输出流提供缓冲区以外,其余方面BufferedWriter基本与Writer类似。类似地,BufferedWriter也提供了writeLine()方法,能够把一行字符写入到底层的字符输出流中。</p><p><strong>值得注意是,你需要手动flush()方法确保写入到此输出流的数据真正写入到磁盘或者网络中。</strong></p><p><strong>FilterReader</strong></p><p>与FilterInputStream类似,FilterReader是实现自定义过滤输入字符流的基类,基本上它仅仅只是简单覆盖了Reader中的所有方法。</p><p>就我自己而言,我没发现这个类明显的用途。除了构造函数取一个Reader变量作为参数之外,我没看到FilterReader任何对Reader新增或者修改的地方。如果你选择继承FilterReader实现自定义的类,同样也可以直接继承自Reader从而避免额外的类层级结构。</p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java基础16:Java多线程基础最全总结</title>
<link href="/2018/05/04/javase16/"/>
<url>/2018/05/04/javase16/</url>
<content type="html"><![CDATA[<p>本文介绍了Java多线程的基本概念,使用方法,以及底层实现原理。帮助你更好地使用Java的多线程。</p><p>具体代码在我的GitHub中可以找到</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>喜欢的话麻烦点一下星哈谢谢。</p><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/05/04/javase16">https://h2pl.github.io/2018/05/04/javase16</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><blockquote><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p></blockquote><a id="more"></a><h2 id="Java中的线程"><a href="#Java中的线程" class="headerlink" title="Java中的线程"></a>Java中的线程</h2><p>Java之父对线程的定义是:</p><blockquote><p>线程是一个独立执行的调用序列,同一个进程的线程在同一时刻共享一些系统资源(比如文件句柄等)也能访问同一个进程所创建的对象资源(内存资源)。java.lang.Thread对象负责统计和控制这种行为。</p></blockquote><blockquote><p>每个程序都至少拥有一个线程-即作为Java虚拟机(JVM)启动参数运行在主类main方法的线程。在Java虚拟机初始化过程中也可能启动其他的后台线程。这种线程的数目和种类因JVM的实现而异。然而所有用户级线程都是显式被构造并在主线程或者是其他用户线程中被启动。</p></blockquote><pre><code> 本文主要讲了java中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。在这之前,首先让我们来了解下在操作系统中进程和线程的区别: 进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程。(进程是资源分配的最小单位) 线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。(线程是cpu调度的最小单位) 线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。 多进程是指操作系统能同时运行多个任务(程序)。 多线程是指在同一程序中有多个顺序流在执行。在java中要想实现多线程,有两种手段,一种是继续Thread类,另外一种是实现Runable接口.(其实准确来讲,应该有三种,还有一种是实现Callable接口,并与Future、线程池结合使用</code></pre><h2 id="Java线程内存模型"><a href="#Java线程内存模型" class="headerlink" title="Java线程内存模型"></a>Java线程内存模型</h2><p>下面的图大致介绍了Java线程的调用过程,每个线程使用一个独立的调用栈进行线程执行,栈中的数据不共享,堆区和方法区的数据是共享的。<br><img src="http://incdn1.b0.upaiyun.com/2017/10/0daf3c6197b0a14eef74a013a154024a.png" alt="image"></p><p><img src="http://incdn1.b0.upaiyun.com/2017/10/0daf3c6197b0a14eef74a013a154024a.png" alt="image"></p><p><img src="http://incdn1.b0.upaiyun.com/2017/10/3d9d0af74829fa666dc137ef89a7b332.png" alt="image"></p><h2 id="构造方法和守护线程"><a href="#构造方法和守护线程" class="headerlink" title="构造方法和守护线程"></a>构造方法和守护线程</h2><pre><code>构造方法Thread类中不同的构造方法接受如下参数的不同组合:一个Runnable对象,这种情况下,Thread.start方法将会调用对应Runnable对象的run方法。如果没有提供Runnable对象,那么就会立即得到一个Thread.run的默认实现。一个作为线程标识名的String字符串,该标识在跟踪和调试过程中会非常有用,除此别无它用。线程组(ThreadGroup),用来放置新创建的线程,如果提供的ThreadGroup不允许被访问,那么就会抛出一个SecurityException 。Thread对象拥有一个守护(daemon)标识属性,这个属性无法在构造方法中被赋值,但是可以在线程启动之前设置该属性(通过setDaemon方法)。当程序中所有的非守护线程都已经终止,调用setDaemon方法可能会导致虚拟机粗暴的终止线程并退出。isDaemon方法能够返回该属性的值。守护状态的作用非常有限,即使是后台线程在程序退出的时候也经常需要做一些清理工作。(daemon的发音为”day-mon”,这是系统编程传统的遗留,系统守护进程是一个持续运行的进程,比如打印机队列管理,它总是在系统中运行。)</code></pre><h2 id="启动线程的方式和isAlive方法"><a href="#启动线程的方式和isAlive方法" class="headerlink" title="启动线程的方式和isAlive方法"></a>启动线程的方式和isAlive方法</h2><p>启动线程<br>调用start方法会触发Thread实例以一个新的线程启动其run方法。新线程不会持有调用线程的任何同步锁。</p><p>当一个线程正常地运行结束或者抛出某种未检测的异常(比如,运行时异常(RuntimeException),错误(ERROR) 或者其子类)线程就会终止。</p><p><strong>当线程终止之后,是不能被重新启动的。在同一个Thread上调用多次start方法会抛出InvalidThreadStateException异常。</strong></p><p>如果线程已经启动但是还没有终止,那么调用isAlive方法就会返回true.即使线程由于某些原因处于阻塞(Blocked)状态该方法依然返回true。</p><p>如果线程已经被取消(cancelled),那么调用其isAlive在什么时候返回false就因各Java虚拟机的实现而异了。没有方法可以得知一个处于非活动状态的线程是否已经被启动过了。</p><h2 id="优先级"><a href="#优先级" class="headerlink" title="优先级"></a>优先级</h2><p><strong>Java的线程实现基本上都是内核级线程的实现,所以Java线程的具体执行还取决于操作系统的特性。</strong></p><p>Java虚拟机为了实现跨平台(不同的硬件平台和各种操作系统)的特性,Java语言在线程调度与调度公平性上未作出任何的承诺,甚至都不会严格保证线程会被执行。但是Java线程却支持优先级的方法,这些方法会影响线程的调度:</p><p>每个线程都有一个优先级,分布在Thread.MIN_PRIORITY和Thread.MAX_PRIORITY之间(分别为1和10)<br>默认情况下,新创建的线程都拥有和创建它的线程相同的优先级。main方法所关联的初始化线程拥有一个默认的优先级,这个优先级是Thread.NORM_PRIORITY (5).</p><p>线程的当前优先级可以通过getPriority方法获得。<br>线程的优先级可以通过setPriority方法来动态的修改,一个线程的最高优先级由其所在的线程组限定。</p><h2 id="线程的控制方法"><a href="#线程的控制方法" class="headerlink" title="线程的控制方法"></a>线程的控制方法</h2><p>只有很少几个方法可以用于跨线程交流:</p><pre><code>每个线程都有一个相关的Boolean类型的中断标识。在线程t上调用t.interrupt会将该线程的中断标识设为true,除非线程t正处于Object.wait,Thread.sleep,或者Thread.join,这些情况下interrupt调用会导致t上的这些操作抛出InterruptedException异常,但是t的中断标识会被设为false。任何一个线程的中断状态都可以通过调用isInterrupted方法来得到。如果线程已经通过interrupt方法被中断,这个方法将会返回true。但是如果调用了Thread.interrupted方法且中断标识还没有被重置,或者是线程处于wait,sleep,join过程中,调用isInterrupted方法将会抛出InterruptedException异常。调用t.join()方法将会暂停执行调用线程,直到线程t执行完毕:当t.isAlive()方法返回false的时候调用t.join()将会直接返回(return)。另一个带参数毫秒(millisecond)的join方法在被调用时,如果线程没能够在指定的时间内完成,调用线程将重新得到控制权。因为isAlive方法的实现原理,所以在一个还没有启动的线程上调用join方法是没有任何意义的。同样的,试图在一个还没有创建的线程上调用join方法也是不明智的。起初,Thread类还支持一些另外一些控制方法:suspend,resume,stop以及destroy。这几个方法已经被声明过期。其中destroy方法从来没有被实现,估计以后也不会。而通过使用等待/唤醒机制增加suspend和resume方法在安全性和可靠性的效果有所欠缺</code></pre><h2 id="Thread的静态方法"><a href="#Thread的静态方法" class="headerlink" title="Thread的静态方法"></a>Thread的静态方法</h2><pre><code>静态方法Thread类中的部分方法被设计为只适用于当前正在运行的线程(即调用Thread方法的线程)。为强调这点,这些方法都被声明为静态的。Thread.currentThread方法会返回当前线程的引用,得到这个引用可以用来调用其他的非静态方法,比如Thread.currentThread().getPriority()会返回调用线程的优先级。Thread.interrupted方法会清除当前线程的中断状态并返回前一个状态。(一个线程的中断状态是不允许被其他线程清除的)Thread.sleep(long msecs)方法会使得当前线程暂停执行至少msecs毫秒。Thread.yield方法纯粹只是建议Java虚拟机对其他已经处于就绪状态的线程(如果有的话)调度执行,而不是当前线程。最终Java虚拟机如何去实现这种行为就完全看其喜好了。</code></pre><h2 id="线程组"><a href="#线程组" class="headerlink" title="线程组"></a>线程组</h2><pre><code>每一个线程都是一个线程组中的成员。默认情况下,新建线程和创建它的线程属于同一个线程组。线程组是以树状分布的。当创建一个新的线程组,这个线程组成为当前线程组的子组。getThreadGroup方法会返回当前线程所属的线程组,对应地,ThreadGroup类也有方法可以得到哪些线程目前属于这个线程组,比如enumerate方法。ThreadGroup类存在的一个目的是支持安全策略来动态的限制对该组的线程操作。比如对不属于同一组的线程调用interrupt是不合法的。这是为避免某些问题(比如,一个applet线程尝试杀掉主屏幕的刷新线程)所采取的措施。ThreadGroup也可以为该组所有线程设置一个最大的线程优先级。线程组往往不会直接在程序中被使用。在大多数的应用中,如果仅仅是为在程序中跟踪线程对象的分组,那么普通的集合类(比如java.util.Vector)应是更好的选择。</code></pre><h2 id="多线程的实现"><a href="#多线程的实现" class="headerlink" title="多线程的实现"></a>多线程的实现</h2><pre><code>public class 多线程实例 { //继承thread @Test public void test1() { class A extends Thread { @Override public void run() { System.out.println("A run"); } } A a = new A(); a.start(); } //实现Runnable @Test public void test2() { class B implements Runnable { @Override public void run() { System.out.println("B run"); } } B b = new B(); //Runable实现类需要由Thread类包装后才能执行 new Thread(b).start(); } //有返回值的线程 @Test public void test3() { Callable callable = new Callable() { int sum = 0; @Override public Object call() throws Exception { for (int i = 0;i < 5;i ++) { sum += i; } return sum; } }; //这里要用FutureTask,否则不能加入Thread构造方法 FutureTask futureTask = new FutureTask(callable); new Thread(futureTask).start(); try { System.out.println(futureTask.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } //线程池实现 @Test public void test4() { ExecutorService executorService = Executors.newFixedThreadPool(5); //execute直接执行线程 executorService.execute(new Thread()); executorService.execute(new Runnable() { @Override public void run() { System.out.println("runnable"); } }); //submit提交有返回结果的任务,运行完后返回结果。 Future future = executorService.submit(new Callable<String>() { @Override public String call() throws Exception { return "a"; } }); try { System.out.println(future.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } ArrayList<String> list = new ArrayList<>(); //有返回值的线程组将返回值存进集合 for (int i = 0;i < 5;i ++ ) { int finalI = i; Future future1 = executorService.submit(new Callable<String>() { @Override public String call() throws Exception { return "res" + finalI; } }); try { list.add((String) future1.get()); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } } for (String s : list) { System.out.println(s); } }}</code></pre><h2 id="线程状态转换"><a href="#线程状态转换" class="headerlink" title="线程状态转换"></a>线程状态转换</h2><pre><code>public class 线程的状态转换 {//一开始线程是init状态,结束时是terminated状态class t implements Runnable { private String name; public t(String name) { this.name = name; } @Override public void run() { System.out.println(name + "run"); }}//测试join,父线程在子线程运行时进入waiting状态@Testpublic void test1() throws InterruptedException { Thread dad = new Thread(new Runnable() { Thread son = new Thread(new t("son")); @Override public void run() { System.out.println("dad init"); son.start(); try { //保证子线程运行完再运行父线程 son.join(); System.out.println("dad run"); } catch (InterruptedException e) { e.printStackTrace(); } } }); //调用start,线程进入runnable状态,等待系统调度 dad.start(); //在父线程中对子线程实例使用join,保证子线程在父线程之前执行完}//测试sleep@Testpublic void test2(){ Thread t1 = new Thread(new Runnable() { @Override public void run() { System.out.println("t1 run"); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } } }); //主线程休眠。进入time waiting状态 try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } t1.start();}//线程2进入blocked状态。public static void main(String[] args) { test4(); Thread.yield();//进入runnable状态}//测试blocked状态public static void test4() { class A { //线程1获得实例锁以后线程2无法获得实例锁,所以进入blocked状态 synchronized void run() { while (true) { System.out.println("run"); } } } A a = new A(); new Thread(new Runnable() { @Override public void run() { System.out.println("t1 get lock"); a.run(); } }).start(); new Thread(new Runnable() { @Override public void run() { System.out.println("t2 get lock"); a.run(); } }).start();}//volatile保证线程可见性volatile static int flag = 1;//object作为锁对象,用于线程使用wait和notify方法volatile static Object o = new Object();//测试wait和notify//wait后进入waiting状态,被notify进入blocked(阻塞等待锁释放)或者runnable状态(获取到锁)public void test5() { new Thread(new Runnable() { @Override public void run() { //wait和notify只能在同步代码块内使用 synchronized (o) { while (true) { if (flag == 0) { try { Thread.sleep(2000); System.out.println("thread1 wait"); //释放锁,线程挂起进入object的等待队列,后续代码运行 o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("thread1 run"); System.out.println("notify t2"); flag = 0; //通知等待队列的一个线程获取锁 o.notify(); } } } }).start(); //解释同上 new Thread(new Runnable() { @Override public void run() { while (true) { synchronized (o) { if (flag == 1) { try { Thread.sleep(2000); System.out.println("thread2 wait"); o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("thread2 run"); System.out.println("notify t1"); flag = 1; o.notify(); } } } }).start();}//输出结果是// thread1 run// notify t2// thread1 wait// thread2 run// notify t1// thread2 wait// thread1 run// notify t2//不断循环}</code></pre>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java原理学习总结</title>
<link href="/2018/05/02/javase/"/>
<url>/2018/05/02/javase/</url>
<content type="html"><![CDATA[<p>本文主要是我最近复习Java基础原理过程中写的Java基础学习总结。Java的知识点其实非常多,并且有些知识点比较难以理解,有时候我们自以为理解了某些内容,其实可能只是停留在表面上,没有理解其底层实现原理。</p><p>纸上得来终觉浅,绝知此事要躬行。笔者之前对每部分的内容<br>对做了比较深入的学习以及代码实现,基本上比较全面地讲述了每一个Java基础知识点,当然可能有些遗漏和错误,还请读者指正。</p><p><strong>这里先把整体的学习大纲列出来,让大家对知识框架有个基本轮廓,具体每个部分的内容,笔者都对应写了一篇博文来加以讲解和剖析,并且发表在我的个人博客和csdn技术专栏里,下面给出地址</strong></p><p>专栏:深入理解Java原理</p><p><a href="https://blog.csdn.net/column/details/21930.html" target="_blank" rel="noopener">https://blog.csdn.net/column/details/21930.html</a></p><p>相关代码实现在我的GitHub里:</p><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p><p><strong>喜欢的话麻烦star一下哈</strong></p><p>本系列技术文章首发于我的个人博客:</p><p><a href="https://h2pl.github.io">https://h2pl.github.io</a></p><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p><a id="more"></a><h2 id="Java基础学习总结"><a href="#Java基础学习总结" class="headerlink" title="Java基础学习总结"></a>Java基础学习总结</h2><p>每部分内容会重点写一些常见知识点,方便复习和记忆,但是并不是全部内容,详细的内容请参见具体的文章地址。</p><h3 id="面向对象三大特性"><a href="#面向对象三大特性" class="headerlink" title="面向对象三大特性"></a>面向对象三大特性</h3><pre><code>继承:一般类只能单继承,内部类实现多继承,接口可以多继承封装:访问权限控制public > protected > 包 > private 内部类也是一种封装多态:编译时多态,体现在向上转型和向下转型,通过引用类型判断调用哪个方法(静态分派)。运行时多态,体现在同名函数通过不同参数实现多种方法(动态分派)。</code></pre><h3 id="基本数据类型"><a href="#基本数据类型" class="headerlink" title="基本数据类型"></a>基本数据类型</h3><pre><code>基本类型位数,自动装箱,常量池例如byte类型是8位,可以表示的数字是-128到127,因为还有一个0,加起来一共是256,也就是2的八次方。基本数据类型的包装类只在数字范围-128到127中用到常量池,会自动拆箱装箱,其余数字范围的包装类则会新建实例</code></pre><h3 id="String及包装类"><a href="#String及包装类" class="headerlink" title="String及包装类"></a>String及包装类</h3><pre><code>String类型是final类型,在堆中分配空间后内存地址不可变。底层是final修饰的char[]数组,数组的内存地址同样不可变。但实际上可以通过修改char[n] = 'a'来进行修改,不会改变String实例的内存值,不过在jdk中,用户无法直接获取char[],也没有方法能操作该数组。所以String类型的不可变实际上也是理论上的不可变。StringBuffer和StringBuilder底层是可变的char[]数组,继承父类AbstractStringBuilder的各种成员和方法,实际上的操作都是由父类方法来完成的。</code></pre><h3 id="final关键字"><a href="#final关键字" class="headerlink" title="final关键字"></a>final关键字</h3><pre><code>final修饰基本数据类型保证不可变final修饰引用保证引用不能指向别的对象final修饰类,类的实例分配空间后地址不可变,子类不能重写所有父类方法。final修饰方法,子类不能重写该方法。</code></pre><h3 id="抽象类和接口"><a href="#抽象类和接口" class="headerlink" title="抽象类和接口"></a>抽象类和接口</h3><pre><code>1 抽象类可以有方法实现。抽象类有非final成员变量。抽象方法要用abstract修饰。抽象类可以有构造方法,但是只能由子类进行实例化。2 接口可以用extends加多个接口实现多继承。接口只能有public final类型的成员变量。接口只能由抽象方法,不能有方法体、接口不能实例化,但是可以作为引用类型。</code></pre><h3 id="代码块和加载顺序"><a href="#代码块和加载顺序" class="headerlink" title="代码块和加载顺序"></a>代码块和加载顺序</h3><pre><code>假设该类是第一次进行实例化。那么有如下加载顺序静态总是比非静态优先,从早到晚的顺序是:1 静态代码块 和 静态成员变量的顺序根据代码位置前后来决定。2 代码块和成员变量的顺序也根据代码位置来决定3 最后才调用构造方法构造方法</code></pre><h3 id="包、内部类、外部类"><a href="#包、内部类、外部类" class="headerlink" title="包、内部类、外部类"></a>包、内部类、外部类</h3><pre><code>1 Java项目一般从src目录开始有com.*.*.A.java这样的目录结构。这就是包结构。所以一般编译后的结构是跟包结构一模一样的,这样的结构保证了import时能找到正确的class引用包访问权限就是指同包下的类可见。import 一般加上全路径,并且使用.*时只包含当前目录的所有类文件,不包括子目录。2 外部类只有public和default两种修饰,要么包内可访问,要么全局可访问。3 内部类可以有全部访问权限,因为它的概念就是一个成员变量,所以访问权限设置与一般的成员变量相同。非静态内部类是外部类的一个成员变量,只跟外部类的实例有关。静态内部类是独立于外部类存在的一个类,与外部类实例无关,可以通过外部类.内部类直接获取Class类型。</code></pre><h3 id="异常"><a href="#异常" class="headerlink" title="异常"></a>异常</h3><pre><code>1 异常体系的最上层是Throwable类子类有Error和ExceptionException的子类又有RuntimeException和其他具体的可检查异常。2 Error是jvm完全无法处理的系统错误,只能终止运行。运行时异常指的是编译正确但运行错误的异常,如数组越界异常,一般是人为失误导致的,这种异常不用try catch,而是需要程序员自己检查。可检查异常一般是jvm处理不了的一些异常,但是又经常会发生,比如Ioexception,Sqlexception等,是外部实现带来的异常。3 多线程的异常流程是独立的,互不影响。大型模块的子模块异常一般需要重新封装成外部异常再次抛出,否则只能看到最外层异常信息,难以进行调试。</code></pre><h3 id="泛型"><a href="#泛型" class="headerlink" title="泛型"></a>泛型</h3><pre><code>Java中的泛型是伪泛型,只在编译期生效,运行期自动进行泛型擦除,将泛型替换为实际上传入的类型。泛型类用class <T> A {}这样的形式表示,里面的方法和成员变量都可以用T来表示类型。泛型接口也是类似的,不过泛型类实现泛型接口时可以选择注入实际类型或者是继续使用泛型。泛型方法可以自带泛型比如void <E> E go();泛型可以使用?通配符进行泛化 Object<?>可以接受任何类型也可以使用 <? extends Number> <? super Integer>这种方式进行上下边界的限制。</code></pre><h3 id="Class类和Object类"><a href="#Class类和Object类" class="headerlink" title="Class类和Object类"></a>Class类和Object类</h3><pre><code>Java反射的基础是Class类,该类封装所有其他类的类型信息,并且在每个类加载后在堆区生成每个类的一个Class<类名>实例,用于该类的实例化。Java中可以通过多种方式获取Class类型,比如.class,getClass方法以及Class.forName方法。Object是所有类的父类,有着自己的一些私有方法,以及被所有类继承的9大方法。</code></pre><h3 id="javac和java"><a href="#javac和java" class="headerlink" title="javac和java"></a>javac和java</h3><pre><code>javac 是编译一个java文件的基本命令,通过不同参数可以完成各种配置,比如导入其他类,指定编译路径等。java是执行一个java文件的基本命令,通过参数配置可以以不同方式执行一个java程序或者是一个jar包。javap是一个class文件的反编译程序,可以获取class文件的反编译结果,甚至是jvm执行程序的每一步字节码实现。</code></pre><h3 id="反射"><a href="#反射" class="headerlink" title="反射"></a>反射</h3><pre><code>Java反射包reflection提供对Class,Method,field,constructor等信息的封装类型。通过这些api可以轻易获得一个类的各种信息并且可以进行实例化,方法调用等。类中的private参数可以通过setaccessible方法强制获取。</code></pre><h3 id="枚举类"><a href="#枚举类" class="headerlink" title="枚举类"></a>枚举类</h3><pre><code>枚举类继承Enum并且每个枚举类的实例都是唯一的。枚举类可以用于封装一组常量,取值从这组常量中取,比如一周的七天,一年的十二个月。枚举类的底层实现其实是语法糖,每个实例可以被转化成内部类。并且使用静态代码块进行初始化,同时保证内部成员变量不可变。</code></pre><h3 id="序列化"><a href="#序列化" class="headerlink" title="序列化"></a>序列化</h3><pre><code>序列化的类要实现serializable接口transient修饰符可以保证某个成员变量不被序列化readObject和writeOject来实现实例的写入和读取。待更新。</code></pre><h3 id="动态代理"><a href="#动态代理" class="headerlink" title="动态代理"></a>动态代理</h3><pre><code>jdk自带的动态代理可以代理一个已经实现接口的类。cglib代理可以代理一个普通的类。动态代理的基本实现原理都是通过字节码框架动态生成字节码,并且在用defineclass加载类后,获取代理类的实例。一般需要实现一个代理处理器,用来处理被代理类的前置操作和后置操作。</code></pre><h3 id="多线程"><a href="#多线程" class="headerlink" title="多线程"></a>多线程</h3><pre><code>这里先不讲juc包里的多线程类。juc相关内容会在juc专题讲解。Java中的线程有7种状态,new runable running blocked waiting time_waiting terminateblocked是线程等待其他线程锁释放。waiting是wait以后线程无限等待其他线程使用notify唤醒time_wating是有限时间地等待被唤醒,也可能是sleep固定时间。线程的实现可以通过继承Thread类和实现Runable接口也可以使用线程池。callable配合future可以实现线程中的数据获取。一个线程实例连续start两次会抛异常。</code></pre><h3 id="网络编程"><a href="#网络编程" class="headerlink" title="网络编程"></a>网络编程</h3><pre><code>主要是Socket编程,相关类比较多。待更新</code></pre><h3 id="Java8"><a href="#Java8" class="headerlink" title="Java8"></a>Java8</h3><pre><code>接口中的默认方法,接口终于可以有方法实现了lambda表达式实现了函数式编程Option类实现了非空检验新的日期API各种api的更新,包括chm,hashmap的实现等Stream流概念,实现了集合类的流式访问待更新</code></pre><h3 id="未完待续"><a href="#未完待续" class="headerlink" title="未完待续"></a>未完待续</h3>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java基础15:深入剖析Java枚举类</title>
<link href="/2018/05/02/javase15/"/>
<url>/2018/05/02/javase15/</url>
<content type="html"><![CDATA[<p>本文介绍了枚举类的基本概念,使用方法,以及底层实现原理。帮助你更好地使用枚举类并且理解枚举类的内部实现细节。</p><p>具体代码在我的GitHub中可以找到</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>喜欢的话麻烦点一下星哈谢谢。</p><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/05/02/javase15">https://h2pl.github.io/2018/05/02/javase15</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><blockquote><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p></blockquote><a id="more"></a><p>枚举(enum)类型是Java 5新增的特性,它是一种新的类型,允许用常量来表示特定的数据片断,而且全部都以类型安全的形式来表示。</p><h2 id="初探枚举类"><a href="#初探枚举类" class="headerlink" title="初探枚举类"></a>初探枚举类</h2><blockquote><p> 在程序设计中,有时会用到由若干个有限数据元素组成的集合,如一周内的星期一到星期日七个数据元素组成的集合,由三种颜色红、黄、绿组成的集合,一个工作班组内十个职工组成的集合等等,程序中某个变量取值仅限于集合中的元素。此时,可将这些数据集合定义为枚举类型。</p></blockquote><blockquote><p>因此,枚举类型是某类数据可能取值的集合,如一周内星期可能取值的集合为:<br> { Sun,Mon,Tue,Wed,Thu,Fri,Sat}<br> 该集合可定义为描述星期的枚举类型,该枚举类型共有七个元素,因而用枚举类型定义的枚举变量只能取集合中的某一元素值。由于枚举类型是导出数据类型,因此,必须先定义枚举类型,然后再用枚举类型定义枚举型变量。 </p></blockquote><pre><code>enum <枚举类型名> { <枚举元素表> }; 其中:关键词enum表示定义的是枚举类型,枚举类型名由标识符组成,而枚举元素表由枚举元素或枚举常量组成。例如: enum weekdays { Sun,Mon,Tue,Wed,Thu,Fri,Sat }; 定义了一个名为 weekdays的枚举类型,它包含七个元素:Sun、Mon、Tue、Wed、Thu、Fri、Sat。 </code></pre><blockquote><p>在编译器编译程序时,给枚举类型中的每一个元素指定一个整型常量值(也称为序号值)。若枚举类型定义中没有指定元素的整型常量值,则整型常量值从0开始依次递增,因此,weekdays枚举类型的七个元素Sun、Mon、Tue、Wed、Thu、Fri、Sat对应的整型常量值分别为0、1、2、3、4、5、6。<br> 注意:在定义枚举类型时,也可指定元素对应的整型常量值。</p></blockquote><pre><code>例如,描述逻辑值集合{TRUE、FALSE}的枚举类型boolean可定义如下:enum boolean { TRUE=1 ,FALSE=0 };该定义规定:TRUE的值为1,而FALSE的值为0。 而描述颜色集合{red,blue,green,black,white,yellow}的枚举类型colors可定义如下:enum colors {red=5,blue=1,green,black,white,yellow}; 该定义规定red为5 ,blue为1,其后元素值从2 开始递增加1。green、black、white、yellow的值依次为2、3、4、5。 </code></pre><p> 此时,整数5将用于表示二种颜色red与yellow。通常两个不同元素取相同的整数值是没有意义的。枚举类型的定义只是定义了一个新的数据类型,只有用枚举类型定义枚举变量才能使用这种数据类型。 </p><h2 id="枚举类-语法"><a href="#枚举类-语法" class="headerlink" title="枚举类-语法"></a>枚举类-语法</h2><blockquote><p>enum 与 class、interface 具有相同地位;<br>可以继承多个接口;<br>可以拥有构造器、成员方法、成员变量;<br>1.2 枚举类与普通类不同之处</p><p>默认继承 java.lang.Enum 类,所以不能继承其他父类;其中 java.lang.Enum 类实现了 java.lang.Serializable 和 java.lang.Comparable 接口;</p></blockquote><blockquote><p>使用 enum 定义,默认使用 final 修饰,因此不能派生子类;</p></blockquote><blockquote><p>构造器默认使用 private 修饰,且只能使用 private 修饰;</p></blockquote><blockquote><p>枚举类所有实例必须在第一行给出,默认添加 public static final 修饰,否则无法产生实例;</p></blockquote><h2 id="枚举类的具体使用"><a href="#枚举类的具体使用" class="headerlink" title="枚举类的具体使用"></a>枚举类的具体使用</h2><p>这部分内容参考<a href="https://blog.csdn.net/qq_27093465/article/details/52180865" target="_blank" rel="noopener">https://blog.csdn.net/qq_27093465/article/details/52180865</a></p><h3 id="常量"><a href="#常量" class="headerlink" title="常量"></a>常量</h3><pre><code>public class 常量 {}enum Color { Red, Green, Blue, Yellow}</code></pre><h3 id="switch"><a href="#switch" class="headerlink" title="switch"></a>switch</h3><p>JDK1.6之前的switch语句只支持int,char,enum类型,使用枚举,能让我们的代码可读性更强。</p><pre><code>public static void showColor(Color color) { switch (color) { case Red: System.out.println(color); break; case Blue: System.out.println(color); break; case Yellow: System.out.println(color); break; case Green: System.out.println(color); break; } }</code></pre><h3 id="向枚举中添加新方法"><a href="#向枚举中添加新方法" class="headerlink" title="向枚举中添加新方法"></a>向枚举中添加新方法</h3><p>如果打算自定义自己的方法,那么必须在enum实例序列的最后添加一个分号。而且 Java 要求必须先定义 enum 实例。</p><pre><code>enum Color { //每个颜色都是枚举类的一个实例,并且构造方法要和枚举类的格式相符合。 //如果实例后面有其他内容,实例序列结束时要加分号。 Red("红色", 1), Green("绿色", 2), Blue("蓝色", 3), Yellow("黄色", 4); String name; int index; Color(String name, int index) { this.name = name; this.index = index; } public void showAllColors() { //values是Color实例的数组,在通过index和name可以获取对应的值。 for (Color color : Color.values()) { System.out.println(color.index + ":" + color.name); } }}</code></pre><h3 id="覆盖枚举的方法"><a href="#覆盖枚举的方法" class="headerlink" title="覆盖枚举的方法"></a>覆盖枚举的方法</h3><p>所有枚举类都继承自Enum类,所以可以重写该类的方法<br>下面给出一个toString()方法覆盖的例子。 </p><pre><code>@Overridepublic String toString() { return this.index + ":" + this.name;}</code></pre><h3 id="实现接口"><a href="#实现接口" class="headerlink" title="实现接口"></a>实现接口</h3><p>所有的枚举都继承自java.lang.Enum类。由于Java 不支持多继承,所以枚举对象不能再继承其他类。</p><pre><code>enum Color implements Print{ @Override public void print() { System.out.println(this.name); }}</code></pre><h3 id="使用接口组织枚举"><a href="#使用接口组织枚举" class="headerlink" title="使用接口组织枚举"></a>使用接口组织枚举</h3><p> 搞个实现接口,来组织枚举,简单讲,就是分类吧。如果大量使用枚举的话,这么干,在写代码的时候,就很方便调用啦。 </p><pre><code>public class 用接口组织枚举 { public static void main(String[] args) { Food cf = chineseFood.dumpling; Food jf = Food.JapaneseFood.fishpiece; for (Food food : chineseFood.values()) { System.out.println(food); } for (Food food : Food.JapaneseFood.values()) { System.out.println(food); } }}interface Food { enum JapaneseFood implements Food { suse, fishpiece }}enum chineseFood implements Food { dumpling, tofu}</code></pre><h3 id="枚举类集合"><a href="#枚举类集合" class="headerlink" title="枚举类集合"></a>枚举类集合</h3><p>java.util.EnumSet和java.util.EnumMap是两个枚举集合。EnumSet保证集合中的元素不重复;EnumMap中的 key是enum类型,而value则可以是任意类型。</p><p>EnumSet在JDK中没有找到实现类,这里写一个EnumMap的例子</p><pre><code>public class 枚举类集合 { public static void main(String[] args) { EnumMap<Color, String> map = new EnumMap<Color, String>(Color.class); map.put(Color.Blue, "Blue"); map.put(Color.Yellow, "Yellow"); map.put(Color.Red, "Red"); System.out.println(map.get(Color.Red)); }}</code></pre><h2 id="使用枚举类的注意事项"><a href="#使用枚举类的注意事项" class="headerlink" title="使用枚举类的注意事项"></a>使用枚举类的注意事项</h2><p><img src="https://img-blog.csdn.net/20170112172420090" alt="image"></p><p>枚举类型对象之间的值比较,是可以使用==,直接来比较值,是否相等的,不是必须使用equals方法的哟。</p><p>因为枚举类Enum已经重写了equals方法</p><pre><code>/** * Returns true if the specified object is equal to this * enum constant. * * @param other the object to be compared for equality with this object. * @return true if the specified object is equal to this * enum constant. */public final boolean equals(Object other) { return this==other;}</code></pre><h2 id="枚举类的底层原理"><a href="#枚举类的底层原理" class="headerlink" title="枚举类的底层原理"></a>枚举类的底层原理</h2><p>这部分参考<a href="https://blog.csdn.net/mhmyqn/article/details/48087247" target="_blank" rel="noopener">https://blog.csdn.net/mhmyqn/article/details/48087247</a></p><blockquote><p>Java从JDK1.5开始支持枚举,也就是说,Java一开始是不支持枚举的,就像泛型一样,都是JDK1.5才加入的新特性。通常一个特性如果在一开始没有提供,在语言发展后期才添加,会遇到一个问题,就是向后兼容性的问题。</p><p>像Java在1.5中引入的很多特性,为了向后兼容,编译器会帮我们写的源代码做很多事情,比如泛型为什么会擦除类型,为什么会生成桥接方法,foreach迭代,自动装箱/拆箱等,这有个术语叫“语法糖”,而编译器的特殊处理叫“解语法糖”。那么像枚举也是在JDK1.5中才引入的,又是怎么实现的呢?</p></blockquote><blockquote><p>Java在1.5中添加了java.lang.Enum抽象类,它是所有枚举类型基类。提供了一些基础属性和基础方法。同时,对把枚举用作Set和Map也提供了支持,即java.util.EnumSet和java.util.EnumMap。</p></blockquote><p>接下来定义一个简单的枚举类</p><pre><code>public enum Day { MONDAY { @Override void say() { System.out.println("MONDAY"); } } , TUESDAY { @Override void say() { System.out.println("TUESDAY"); } }, FRIDAY("work"){ @Override void say() { System.out.println("FRIDAY"); } }, SUNDAY("free"){ @Override void say() { System.out.println("SUNDAY"); } }; String work; //没有构造参数时,每个实例可以看做常量。 //使用构造参数时,每个实例都会变得不一样,可以看做不同的类型,所以编译后会生成实例个数对应的class。 private Day(String work) { this.work = work; } private Day() { } //枚举实例必须实现枚举类中的抽象方法 abstract void say ();}</code></pre><p>反编译结果</p><pre><code>D:\MyTech\out\production\MyTech\com\javase\枚举类>javap Day.classCompiled from "Day.java"public abstract class com.javase.枚举类.Day extends java.lang.Enum<com.javase.枚举类.Day> { public static final com.javase.枚举类.Day MONDAY; public static final com.javase.枚举类.Day TUESDAY; public static final com.javase.枚举类.Day FRIDAY; public static final com.javase.枚举类.Day SUNDAY; java.lang.String work; public static com.javase.枚举类.Day[] values(); public static com.javase.枚举类.Day valueOf(java.lang.String); abstract void say(); com.javase.枚举类.Day(java.lang.String, int, com.javase.枚举类.Day$1); com.javase.枚举类.Day(java.lang.String, int, java.lang.String, com.javase.枚举类.Day$1); static {};}</code></pre><blockquote><p>可以看到,一个枚举在经过编译器编译过后,变成了一个抽象类,它继承了java.lang.Enum;而枚举中定义的枚举常量,变成了相应的public static final属性,而且其类型就抽象类的类型,名字就是枚举常量的名字.</p><p>同时我们可以在Operator.class的相同路径下看到四个内部类的.class文件com/mikan/Day$1.class、com/mikan/Day$2.class、com/mikan/Day$3.class、com/mikan/Day$4.class,也就是说这四个命名字段分别使用了内部类来实现的;同时添加了两个方法values()和valueOf(String);我们定义的构造方法本来只有一个参数,但却变成了三个参数;同时还生成了一个静态代码块。这些具体的内容接下来仔细看看。</p></blockquote><p>下面分析一下字节码中的各部分,其中:</p><pre><code>InnerClasses: static #23; //class com/javase/枚举类/Day$4 static #18; //class com/javase/枚举类/Day$3 static #14; //class com/javase/枚举类/Day$2 static #10; //class com/javase/枚举类/Day$1</code></pre><p>从中可以看到它有4个内部类,这四个内部类的详细信息后面会分析。</p><pre><code>static {}; descriptor: ()V flags: ACC_STATIC Code: stack=5, locals=0, args_size=0 0: new #10 // class com/javase/枚举类/Day$1 3: dup 4: ldc #11 // String MONDAY 6: iconst_0 7: invokespecial #12 // Method com/javase/枚举类/Day$1."<init>":(Ljava/lang/String;I)V 10: putstatic #13 // Field MONDAY:Lcom/javase/枚举类/Day; 13: new #14 // class com/javase/枚举类/Day$2 16: dup 17: ldc #15 // String TUESDAY 19: iconst_1 20: invokespecial #16 // Method com/javase/枚举类/Day$2."<init>":(Ljava/lang/String;I)V //后面类似,这里省略}</code></pre><p>其实编译器生成的这个静态代码块做了如下工作:分别设置生成的四个公共静态常量字段的值,同时编译器还生成了一个静态字段$VALUES,保存的是枚举类型定义的所有枚举常量<br>编译器添加的values方法:</p><pre><code>public static com.javase.Day[] values(); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=0, args_size=0 0: getstatic #2 // Field $VALUES:[Lcom/javase/Day; 3: invokevirtual #3 // Method "[Lcom/mikan/Day;".clone:()Ljava/lang/Object; 6: checkcast #4 // class "[Lcom/javase/Day;" 9: areturn 这个方法是一个公共的静态方法,所以我们可以直接调用该方法(Day.values()),返回这个枚举值的数组,另外,这个方法的实现是,克隆在静态代码块中初始化的$VALUES字段的值,并把类型强转成Day[]类型返回。</code></pre><p>造方法为什么增加了两个参数?</p><p>有一个问题,构造方法我们明明只定义了一个参数,为什么生成的构造方法是三个参数呢?</p><pre><code>从Enum类中我们可以看到,为每个枚举都定义了两个属性,name和ordinal,name表示我们定义的枚举常量的名称,如FRIDAY、TUESDAY,而ordinal是一个顺序号,根据定义的顺序分别赋予一个整形值,从0开始。在枚举常量初始化时,会自动为初始化这两个字段,设置相应的值,所以才在构造方法中添加了两个参数。即:另外三个枚举常量生成的内部类基本上差不多,这里就不重复说明了。</code></pre><blockquote><p>我们可以从Enum类的代码中看到,定义的name和ordinal属性都是final的,而且大部分方法也都是final的,特别是clone、readObject、writeObject这三个方法,这三个方法和枚举通过静态代码块来进行初始化一起。</p></blockquote><blockquote><p>它保证了枚举类型的不可变性,不能通过克隆,不能通过序列化和反序列化来复制枚举,这能保证一个枚举常量只是一个实例,即是单例的,所以在effective java中推荐使用枚举来实现单例。</p></blockquote><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>枚举本质上是通过普通的类来实现的,只是编译器为我们进行了处理。<strong>每个枚举类型都继承自java.lang.Enum,并自动添加了values和valueOf方法。</strong></p><p>而每个枚举常量是一个静态常量字段,<strong>使用内部类实现</strong>,该内部类继承了枚举类。<strong>所有枚举常量都通过静态代码块来进行初始化,即在类加载期间就初始化</strong>。</p><p>另外通过把clone、readObject、writeObject这三个方法定义为final的,同时实现是抛出相应的异常。这样保证了每个枚举类型及枚举常量都是不可变的。<strong>可以利用枚举的这两个特性来实现线程安全的单例。</strong></p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java基础14:离开IDE,使用java和javac构建项目</title>
<link href="/2018/05/01/javase14/"/>
<url>/2018/05/01/javase14/</url>
<content type="html"><![CDATA[<p>前言:本文教你怎么用javac和java命令,讲解了classpath的原理,以及如何利用脚本(shell或bat)进行项目部署,离开ide,还原最本质的Java编译运行过程,并用简单的实例展示这些用法。 </p><p>具体代码在我的GitHub中可以找到</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/05/01/javase14">https://h2pl.github.io/2018/05/01/javase14</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><blockquote><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p></blockquote><a id="more"></a><blockquote><p>IDE是把双刃剑,它可以什么都帮你做了,你只要敲几行代码,点几下鼠标,程序就跑起来了,用起来相当方便。</p><p>你不用去关心它后面做了些什么,执行了哪些命令,基于什么原理。然而也是这种过分的依赖往往让人散失了最基本的技能,当到了一个没有IDE的地方,你便觉得无从下手,给你个代码都不知道怎么去跑。好比给你瓶水,你不知道怎么打开去喝,然后活活给渴死。</p><p>之前用惯了idea,Java文件编译运行的命令基本忘得一干二净。</p><p>现在项目出了原型,放到服务器上去测试,SSH一登陆上服务器就傻眼了,都是命令行,以前程序图标什么的都成了浮云,程序放上去了不知道怎么去编译运行,只能补补课了,下面做下补课笔记。</p></blockquote><h2 id="javac命令初窥"><a href="#javac命令初窥" class="headerlink" title="javac命令初窥"></a>javac命令初窥</h2><p>注:以下红色标记的参数在下文中有所讲解。</p><p>本部分参考<a href="https://www.cnblogs.com/xiazdong/p/3216220.html" target="_blank" rel="noopener">https://www.cnblogs.com/xiazdong/p/3216220.html</a></p><p>用法: javac <options> <source files=""></options></p><p>其中, 可能的选项包括:</p><blockquote><p> -g 生成所有调试信息</p><p> -g:none 不生成任何调试信息</p><p> -g:{lines,vars,source} 只生成某些调试信息</p><p> -nowarn 不生成任何警告</p><p> -verbose 输出有关编译器正在执行的操作的消息</p><p> -deprecation 输出使用已过时的 API 的源位置</p><p> -classpath <路径> 指定查找用户类文件和注释处理程序的位置</p><p> -cp <路径> 指定查找用户类文件和注释处理程序的位置</p><p> -sourcepath <路径> 指定查找输入源文件的位置</p><p> -bootclasspath <路径> 覆盖引导类文件的位置</p><p> -extdirs <目录> 覆盖所安装扩展的位置</p><p> -endorseddirs <目录> 覆盖签名的标准路径的位置</p><p> -proc:{none,only} 控制是否执行注释处理和/或编译。</p><p> -processor <class1>[,<class2>,<class3>…] 要运行的注释处理程序的名称; 绕过默认的搜索进程</class3></class2></class1></p><p> -processorpath <路径> 指定查找注释处理程序的位置</p><p> -d <目录> 指定放置生成的类文件的位置</p><p> -s <目录> 指定放置生成的源文件的位置</p><p> -implicit:{none,class} 指定是否为隐式引用文件生成类文件</p><p> -encoding <编码> 指定源文件使用的字符编码</p><p> -source <发行版> 提供与指定发行版的源兼容性</p><p> -target <发行版> 生成特定 VM 版本的类文件</p><p> -version 版本信息</p><p> -help 输出标准选项的提要</p><p> -A关键字[=值] 传递给注释处理程序的选项</p><p> -X 输出非标准选项的提要</p><p> -J<标记> 直接将 <标记> 传递给运行时系统</p><p> -Werror 出现警告时终止编译</p><p> @<文件名> 从文件读取选项和文件名</p></blockquote><p>在详细介绍javac命令之前,先看看这个classpath是什么</p><h2 id="classpath是什么"><a href="#classpath是什么" class="headerlink" title="classpath是什么"></a>classpath是什么</h2><p>在dos下编译java程序,就要用到classpath这个概念,尤其是在没有设置环境变量的时候。classpath就是存放.class等编译后文件的路径。</p><p>javac:如果当前你要编译的java文件中引用了其它的类(比如说:继承),但该引用类的.class文件不在当前目录下,这种情况下就需要在javac命令后面加上-classpath参数,通过使用以下三种类型的方法 来指导编译器在编译的时候去指定的路径下查找引用类。</p><blockquote><p>(1).绝对路径:javac -classpath c:/junit3.8.1/junit.jar Xxx.java</p><p>(2).相对路径:javac -classpath ../junit3.8.1/Junit.javr Xxx.java</p><p>(3).系统变量:javac -classpath %CLASSPATH% Xxx.java (注意:%CLASSPATH%表示使用系统变量CLASSPATH的值进行查找,这里假设Junit.jar的路径就包含在CLASSPATH系统变量中)</p></blockquote><h2 id="IDE中的classpath"><a href="#IDE中的classpath" class="headerlink" title="IDE中的classpath"></a>IDE中的classpath</h2><p>对于一个普通的Javaweb项目,一般有这样的配置:</p><blockquote><p>1 WEB-INF/classes,lib才是classpath,WEB-INF/ 是资源目录, 客户端不能直接访问。</p><p>2、WEB-INF/classes目录存放src目录java文件编译之后的class文件,xml、properties等资源配置文件,这是一个定位资源的入口。</p><p>3、引用classpath路径下的文件,只需在文件名前加classpath:</p><p><param-value>classpath:applicationContext-*.xml</param-value><br><!-- 引用其子目录下的文件,如 --></p><p><param-value>classpath:context/conf/controller.xml</param-value></p><p>4、lib和classes同属classpath,两者的访问优先级为: lib>classes。</p><p>5、classpath 和 classpath* 区别:</p><p>classpath:只会到你的class路径中查找找文件;<br>classpath*:不仅包含class路径,还包括jar文件中(class路径)进行查找。</p></blockquote><p>总结:</p><p>(1).何时需要使用-classpath:当你要编译或执行的类引用了其它的类,但被引用类的.class文件不在当前目录下时,就需要通过-classpath来引入类</p><p>(2).何时需要指定路径:当你要编译的类所在的目录和你执行javac命令的目录不是同一个目录时,就需要指定源文件的路径(CLASSPATH是用来指定.class路径的,不是用来指定.java文件的路径的) </p><h2 id="Java项目和Java-web项目的本质区别"><a href="#Java项目和Java-web项目的本质区别" class="headerlink" title="Java项目和Java web项目的本质区别"></a>Java项目和Java web项目的本质区别</h2><p>(看清IDE及classpath本质)</p><blockquote><p>现在只是说说Java Project和Web Project,那么二者有区别么?回答:没有!都是Java语言的应用,只是应用场合不同罢了,那么他们的本质到底是什么?</p></blockquote><blockquote><p>回答:编译后路径!虚拟机执行的是class文件而不是java文件,那么我们不管是何种项目都是写的java文件,怎么就不一样了呢?分成java和web两种了呢?</p></blockquote><blockquote><p>从.classpath文件入手来看,这个文件在每个项目目录下都是存在的,很少有人打开看吧,那么我们就来一起看吧。这是一个XML文件,使用文本编辑器打开即可。</p></blockquote><p>这里展示一个web项目的.classpath</p><p>Xml代码</p><pre><code><?xml version="1.0" encoding="UTF-8"?><classpath><classpathentry kind="src" path="src"/><classpathentry kind="src" path="resources"/><classpathentry kind="src" path="test"/><classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/><classpathentry kind="lib" path="lib/servlet-api.jar"/><classpathentry kind="lib" path="webapp/WEB-INF/lib/struts2-core-2.1.8.1.jar"/> ……<classpathentry kind="output" path="webapp/WEB-INF/classes"/></classpath></code></pre><blockquote><p>XML文档包含一个根元素,就是classpath,类路径,那么这里面包含了什么信息呢?子元素是classpathentry,kind属性区别了种 类信息,src源码,con你看看后面的path就知道是JRE容器的信息。lib是项目依赖的第三方类库,output是src编译后的位置。</p></blockquote><blockquote><p>既然是web项目,那么就是WEB-INF/classes目录,可能用MyEclipse的同学会说他们那里是WebRoot或者是WebContext而不是webapp,有区别么?回答:完全没有!</p></blockquote><blockquote><p>既然看到了编译路径的本来面目后,还区分什么java项目和web项目么?回答:不区分!普通的java 项目你这样写就行了:<classpathentry kind="output" path="bin">,看看Eclipse是不是这样生成的?这个问题解决了吧。</classpathentry></p></blockquote><blockquote><p>再说说webapp目录命名的问题,这个无所谓啊,web项目是要发布到服务器上的对吧,那么服务器读取的是类文件和页面文件吧,它不管源文件,它也无法去理解源文件。那么webapp目录的命名有何关系呢?只要让服务器找到不就行了。</p></blockquote><h2 id="Javac命令详解"><a href="#Javac命令详解" class="headerlink" title="Javac命令详解"></a>Javac命令详解</h2><h3 id="g、-g-none、-g-lines-vars-source"><a href="#g、-g-none、-g-lines-vars-source" class="headerlink" title="-g、-g:none、-g:{lines,vars,source}"></a>-g、-g:none、-g:{lines,vars,source}</h3><blockquote><p>•-g:在生成的class文件中包含所有调试信息(行号、变量、源文件)<br>•-g:none :在生成的class文件中不包含任何调试信息。</p><p>这个参数在javac编译中是看不到什么作用的,因为调试信息都在class文件中,而我们看不懂这个class文件。</p><p>为了看出这个参数的作用,我们在eclipse中进行实验。在eclipse中,我们经常做的事就是“debug”,而在debug的时候,我们会<br>•加入“断点”,这个是靠-g:lines起作用,如果不记录行号,则不能加断点。<br>•在“variables”窗口中查看当前的变量,如下图所示,这是靠-g:vars起作用,否则不能查看变量信息。<br>•在多个文件之间来回调用,比如 A.java的main()方法中调用了B.java的fun()函数,而我想看看程序进入fun()后的状态,这是靠-g:source,如果没有这个参数,则不能查看B.java的源代码。</p></blockquote><h3 id="bootclasspath、-extdirs"><a href="#bootclasspath、-extdirs" class="headerlink" title="-bootclasspath、-extdirs"></a>-bootclasspath、-extdirs</h3><blockquote><p>-bootclasspath和-extdirs 几乎不需要用的,因为他是用来改变 “引导类”和“扩展类”。<br>•引导类(组成Java平台的类):Java\jdk1.7.0_25\jre\lib\rt.jar等,用-bootclasspath设置。<br>•扩展类:Java\jdk1.7.0_25\jre\lib\ext目录中的文件,用-extdirs设置。<br>•用户自定义类:用-classpath设置。</p><p>我们用-verbose编译后出现的“类文件的搜索路径”,就是由上面三个路径组成,如下:</p></blockquote><pre><code>[类文件的搜索路径: C:\Java\jdk1.7.0_25\jre\lib\resources.jar,C:\Java\jdk1.7.0_25\jre\lib\rt.jar,C:\Java\jdk1.7.0_25\jre\lib\sunrsasign.jar,C:\Java\jdk1.7.0_25\jre\lib\jsse.jar,C:\Java\jdk1.7.0_25\jre\lib\jce.jar,C:\Java\jdk1.7.0_25\jre\lib\charsets.jar,C:\Java\jdk1.7.0_25\jre\lib\jfr.jar,C:\Java\jdk1.7.0_25\jre\classes,C:\Java\jdk1.7.0_25\jre\lib\ext\access-bridge-32.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\dnsns.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\jaccess.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\localedata.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\sunec.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\sunjce_provider.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\sunmscapi.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\sunpkcs11.jar,C:\Java\jdk1.7.0_25\jre\lib\ext\zipfs.jar,..\bin] </code></pre><p>如果利用 -bootclasspath 重新定义: javac -bootclasspath src Xxx.java,则会出现下面错误:</p><p>致命错误: 在类路径或引导类路径中找不到程序包 java.lang</p><h3 id="sourcepath和-classpath(-cp)"><a href="#sourcepath和-classpath(-cp)" class="headerlink" title="-sourcepath和-classpath(-cp)"></a>-sourcepath和-classpath(-cp)</h3><p>•-classpath(-cp)指定你依赖的类的class文件的查找位置。在Linux中,用“:”分隔classpath,而在windows中,用“;”分隔。<br>•-sourcepath指定你依赖的类的java文件的查找位置。</p><p>举个例子,</p><pre><code>public class A{ public static void main(String[] args) { B b = new B(); b.print(); }}public class B{ public void print() { System.out.println("old"); }}</code></pre><p>目录结构如下:</p><p>sourcepath //此处为当前目录</p><figure class="highlight 1c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="string">|-src</span></span><br><span class="line"> <span class="string">|-com</span></span><br><span class="line"> <span class="string">|- B.java</span></span><br><span class="line"> <span class="string">|- A.java</span></span><br><span class="line"> <span class="string">|-bin</span></span><br><span class="line"> <span class="string">|- B.class //是 B.java</span></span><br></pre></td></tr></table></figure><p> 编译后的类文件</p><p>如果要编译 A.java,则必须要让编译器找到类B的位置,你可以指定B.class的位置,也可以是B.java的位置,也可以同时都存在。</p><pre><code>javac -classpath bin src/A.java //查找到B.classjavac -sourcepath src/com src/A.java //查找到B.javajavac -sourcepath src/com -classpath bin src/A.java //同时查找到B.class和B.java</code></pre><p>如果同时找到了B.class和B.java,则:<br>•如果B.class和B.java内容一致,则遵循B.class。<br>•如果B.class和B.java内容不一致,则遵循B.java,并编译B.java。</p><p>以上规则可以通过 -verbose选项看出。</p><h3 id="d"><a href="#d" class="headerlink" title="-d"></a>-d</h3><p>•d就是 destination,用于指定.class文件的生成目录,在eclipse中,源文件都在src中,编译的class文件都是在bin目录中。</p><p>这里我用来实现一下这个功能,假设项目名称为project,此目录为当前目录,且在src/com目录中有一个Main.java文件。‘</p><pre><code>package com;public class Main{ public static void main(String[] args) { System.out.println("Hello"); }}javac -d bin src/com/Main.java</code></pre><p>上面的语句将Main.class生成在bin/com目录下。</p><h3 id="implicit-none-class"><a href="#implicit-none-class" class="headerlink" title="-implicit:{none,class}"></a>-implicit:{none,class}</h3><p>•如果有文件为A.java(其中有类A),且在类A中使用了类B,类B在B.java中,则编译A.java时,默认会自动编译B.java,且生成B.class。<br>•implicit:none:不自动生成隐式引用的类文件。<br>•implicit:class(默认):自动生成隐式引用的类文件。</p><pre><code>public class A{ public static void main(String[] args) { B b = new B(); }}public class B{}如果使用: javac -implicit:none A.java</code></pre><p>则不会生成 B.class。</p><h3 id="source和-target"><a href="#source和-target" class="headerlink" title="-source和-target"></a>-source和-target</h3><p>•-source:使用指定版本的JDK编译,比如:-source 1.4表示用JDK1.4的标准编译,如果在源文件中使用了泛型,则用JDK1.4是不能编译通过的。<br>•-target:指定生成的class文件要运行在哪个JVM版本,以后实际运行的JVM版本必须要高于这个指定的版本。</p><p>javac -source 1.4 Xxx.java</p><p>javac -target 1.4 Xxx.java</p><h2 id="encoding"><a href="#encoding" class="headerlink" title="-encoding"></a>-encoding</h2><p>默认会使用系统环境的编码,比如我们一般用的中文windows就是GBK编码,所以直接javac时会用GBK编码,而Java文件一般要使用utf-8,如果用GBK就会出现乱码。 </p><p>•指定源文件的编码格式,如果源文件是UTF-8编码的,而-encoding GBK,则源文件就变成了乱码(特别是有中文时)。</p><p>javac -encoding UTF-8 Xxx.java</p><h3 id="verbose"><a href="#verbose" class="headerlink" title="-verbose"></a>-verbose</h3><p>输出详细的编译信息,包括:classpath、加载的类文件信息。</p><p>比如,我写了一个最简单的HelloWorld程序,在命令行中输入:</p><p>D:\Java>javac -verbose -encoding UTF-8 HelloWorld01.java</p><p>输出:</p><pre><code>[语法分析开始时间 RegularFileObject[HelloWorld01.java]][语法分析已完成, 用时 21 毫秒][源文件的搜索路径: .,D:\大三下\编译原理\cup\java-cup-11a.jar,E:\java\jflex\lib\J //-sourcepathFlex.jar][类文件的搜索路径: C:\Java\jdk1.7.0_25\jre\lib\resources.jar,C:\Java\jdk1.7.0_25 //-classpath、-bootclasspath、-extdirs省略............................................[正在加载ZipFileIndexFileObject[C:\Java\jdk1.7.0_25\lib\ct.sym(META-INF/sym/rt.jar/java/lang/Object.class)]][正在加载ZipFileIndexFileObject[C:\Java\jdk1.7.0_25\lib\ct.sym(META-INF/sym/rt.jar/java/lang/String.class)]][正在检查Demo]省略............................................[已写入RegularFileObject[Demo.class]][共 447 毫秒]</code></pre><p>编写一个程序时,比如写了一句:System.out.println(“hello”),实际上还需要加载:Object、PrintStream、String等类文件,而上面就显示了加载的全部类文件。</p><h3 id="其他命令"><a href="#其他命令" class="headerlink" title="其他命令"></a>其他命令</h3><p>-J <标记><br>•传递一些信息给 Java Launcher.</p><pre><code>javac -J-Xms48m Xxx.java //set the startup memory to 48M.</code></pre><p>-@<文件名></p><blockquote><p>如果同时需要编译数量较多的源文件(比如1000个),一个一个编译是不现实的(当然你可以直接 javac *.java ),比较好的方法是:将你想要编译的源文件名都写在一个文件中(比如sourcefiles.txt),其中每行写一个文件名,如下所示:</p><p>HelloWorld01.java<br>HelloWorld02.java<br>HelloWorld03.java</p></blockquote><p>则使用下面的命令:</p><p>javac @sourcefiles.txt</p><p>编译这三个源文件。</p><h2 id="使用javac构建项目"><a href="#使用javac构建项目" class="headerlink" title="使用javac构建项目"></a>使用javac构建项目</h2><p>这部分参考:<br><a href="https://blog.csdn.net/mingover/article/details/57083176" target="_blank" rel="noopener">https://blog.csdn.net/mingover/article/details/57083176</a></p><p>一个简单的javac编译</p><p>新建两个文件夹,src和 build<br>src/com/yp/test/HelloWorld.java<br>build/</p><figure class="highlight armasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">├─<span class="keyword">build</span></span><br><span class="line"><span class="keyword">└─src</span></span><br><span class="line"><span class="keyword"> </span> └─com</span><br><span class="line"> └─yp</span><br><span class="line"> └─test</span><br><span class="line"> HelloWorld.java</span><br></pre></td></tr></table></figure><p>java文件非常简单</p><pre><code>package com.yp.test;public class HelloWorld { public static void main(String[] args) { System.out.println("helloWorld"); }}</code></pre><p>编译:<br>javac src/com/yp/test/HelloWorld.java -d build</p><p>-d 表示编译到 build文件夹下</p><figure class="highlight stata"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">查看build文件夹</span><br><span class="line">├─build</span><br><span class="line">│ └─com</span><br><span class="line">│ └─yp</span><br><span class="line">│ └─<span class="keyword">test</span></span><br><span class="line">│ HelloWorld.<span class="keyword">class</span></span><br><span class="line">│</span><br><span class="line">└─src</span><br><span class="line"> └─com</span><br><span class="line"> └─yp</span><br><span class="line"> └─<span class="keyword">test</span></span><br><span class="line"> HelloWorld.java</span><br></pre></td></tr></table></figure><p>运行文件</p><blockquote><p>E:\codeplace\n_learn\java\javacmd> java com/yp/test/HelloWorld.class<br>错误: 找不到或无法加载主类 build.com.yp.test.HelloWorld.class</p><p>运行时要指定main<br>E:\codeplace\n_learn\java\javacmd\build> java com.yp.test.HelloWorld<br>helloWorld</p></blockquote><p>如果引用到多个其他的类,应该怎么做呢 ?</p><blockquote><p>编译</p><p>E:\codeplace\n_learn\java\javacmd>javac src/com/yp/test/HelloWorld.java -sourcepath src -d build -g<br>1<br>-sourcepath 表示 从指定的源文件目录中找到需要的.java文件并进行编译。<br>也可以用-cp指定编译好的class的路径<br>运行,注意:运行在build目录下</p><p>E:\codeplace\n_learn\java\javacmd\build>java com.yp.test.HelloWorld</p></blockquote><p>怎么打成jar包?</p><blockquote><p>生成:<br>E:\codeplace\n_learn\java\javacmd\build>jar cvf h.jar *<br>运行:<br>E:\codeplace\n_learn\java\javacmd\build>java h.jar<br>错误: 找不到或无法加载主类 h.jar</p></blockquote><blockquote><p>这个错误是没有指定main类,所以类似这样来指定:<br>E:\codeplace\n_learn\java\javacmd\build>java -cp h.jar com.yp.test.HelloWorld</p></blockquote><p>生成可以运行的jar包</p><p>需要指定jar包的应用程序入口点,用-e选项:</p><pre><code>E:\codeplace\n_learn\java\javacmd\build> jar cvfe h.jar com.yp.test.HelloWorld *已添加清单正在添加: com/(输入 = 0) (输出 = 0)(存储了 0%)正在添加: com/yp/(输入 = 0) (输出 = 0)(存储了 0%)正在添加: com/yp/test/(输入 = 0) (输出 = 0)(存储了 0%)正在添加: com/yp/test/entity/(输入 = 0) (输出 = 0)(存储了 0%)正在添加: com/yp/test/entity/Cat.class(输入 = 545) (输出 = 319)(压缩了 41%)正在添加: com/yp/test/HelloWorld.class(输入 = 844) (输出 = 487)(压缩了 42%)</code></pre><p>直接运行</p><pre><code>java -jar h.jar额外发现 指定了Main类后,jar包里面的 META-INF/MANIFEST.MF 是这样的, 比原来多了一行Main-Class….Manifest-Version: 1.0Created-By: 1.8.0 (Oracle Corporation)Main-Class: com.yp.test.HelloWorld</code></pre><p>如果类里有引用jar包呢?</p><p>先下一个jar包 这里直接下 log4j </p><pre><code>* main函数改成import com.yp.test.entity.Cat;import org.apache.log4j.Logger;public class HelloWorld { static Logger log = Logger.getLogger(HelloWorld.class); public static void main(String[] args) { Cat c = new Cat("keyboard"); log.info("这是log4j"); System.out.println("hello," + c.getName()); }}</code></pre><p>现的文件是这样的</p><figure class="highlight crystal"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">├─build</span><br><span class="line">├─<span class="class"><span class="keyword">lib</span></span></span><br><span class="line">│ log4j-<span class="number">1.2</span>.<span class="number">17</span>.jar</span><br><span class="line">│</span><br><span class="line">└─src</span><br><span class="line"> └─com</span><br><span class="line"> └─yp</span><br><span class="line"> └─test</span><br><span class="line"> │ HelloWorld.java</span><br><span class="line"> │</span><br><span class="line"> └─entity</span><br><span class="line"> Cat.java</span><br></pre></td></tr></table></figure><pre><code>这个时候 javac命令要接上 -cp ./lib/*.jarE:\codeplace\n_learn\java\javacmd>javac -encoding "utf8" src/com/yp/test/HelloWorld.java -sourcepath src -d build -g -cp ./lib/*.jar运行要加上-cp, -cp 选项貌似会把工作目录给换了, 所以要加上 ;../buildE:\codeplace\n_learn\java\javacmd\build>java -cp ../lib/log4j-1.2.17.jar;../build com.yp.test.HelloWorld</code></pre><p>结果:</p><pre><code>log4j:WARN No appenders could be found for logger(com.yp.test.HelloWorld).log4j:WARN Please initialize the log4j system properly.log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.hello,keyboard</code></pre><p>由于没有 log4j的配置文件,所以提示上面的问题,往 build 里面加上 log4j.xml</p><pre><code><?xml version="1.0" encoding="UTF-8" ?><!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"><log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'> <appender name="stdout" class="org.apache.log4j.ConsoleAppender"> <layout class="org.apache.log4j.PatternLayout"> <param name="ConversionPattern" value="%d{ABSOLUTE} %-5p [%c{1}] %m%n" /> </layout> </appender> <root> <level value="info" /> <appender-ref ref="stdout" /> </root></log4j:configuration></code></pre><p>再运行</p><pre><code>E:\codeplace\n_learn\java\javacmd>java -cp lib/log4j-1.2.17.jar;build com.yp.tes t.HelloWorld15:19:57,359 INFO [HelloWorld] 这是log4jhello,keyboard</code></pre><p>说明:<br>这个log4j配置文件,习惯的做法是放在src目录下, 在编译过程中 copy到build中的,但根据ant的做法,不是用javac的,而是用来处理,我猜测javac是不能copy的,如果想在命令行直接 使用,应该是用cp命令主动去执行 copy操作</p><p>ok 一个简单的java 工程就运行完了<br>但是 貌似有些繁琐, 需要手动键入 java文件 以及相应的jar包 很是麻烦,<br>so 可以用 shell 来脚本来简化相关操作<br>shell 文件整理如下:</p><pre><code>#!/bin/bash echo "build start" JAR_PATH=libs BIN_PATH=bin SRC_PATH=src # java文件列表目录 SRC_FILE_LIST_PATH=src/sources.list #生所有的java文件列表 放入列表文件中 rm -f $SRC_PATH/sources find $SRC_PATH/ -name *.java > $SRC_FILE_LIST_PATH #删除旧的编译文件 生成bin目录 rm -rf $BIN_PATH/ mkdir $BIN_PATH/ #生成依赖jar包 列表 for file in ${JAR_PATH}/*.jar; do jarfile=${jarfile}:${file} done echo "jarfile = "$jarfile #编译 通过-cp指定所有的引用jar包,将src下的所有java文件进行编译javac -d $BIN_PATH/ -cp $jarfile @$SRC_FILE_LIST_PATH #运行 通过-cp指定所有的引用jar包,指定入口函数运行java -cp $BIN_PATH$jarfile com.zuiapps.danmaku.server.Main </code></pre><blockquote><p>有一点需要注意的是, javac -d $BIN_PATH/ -cp $jarfile @$SRC_FILE_LIST_PATH<br>在要编译的文件很多时候,一个个敲命令会显得很长,也不方便修改,</p></blockquote><blockquote><p>可以把要编译的源文件列在文件中,在文件名前加@,这样就可以对多个文件进行编译,</p></blockquote><blockquote><p>以上就是吧java文件放到 $SRC_FILE_LIST_PATH 中去了</p></blockquote><pre><code>编译 : 1. 需要编译所有的java文件 2. 依赖的java 包都需要加入到 classpath 中去 3. 最后设置 编译后的 class 文件存放目录 即 -d bin/ 4. java文件过多是可以使用 @$SRC_FILE_LIST_PATH 把他们放到一个文件中去运行: 1.需要吧 编译时设置的bin目录和 所有jar包加入到 classpath 中去</code></pre><h2 id="javap"><a href="#javap" class="headerlink" title="javap"></a>javap</h2><blockquote><p>javap是jdk自带的一个工具,可以对代码反编译,也可以查看java编译器生成的字节码。</p><p>情况下,很少有人使用javap对class文件进行反编译,因为有很多成熟的反编译工具可以使用,比如jad。但是,javap还可以查看java编译器为我们生成的字节码。通过它,可以对照源代码和字节码,从而了解很多编译器内部的工作。</p><p>javap命令分解一个class文件,它根据options来决定到底输出什么。如果没有使用options,那么javap将会输出包,类里的protected和public域以及类里的所有方法。javap将会把它们输出在标准输出上。来看这个例子,先编译(javac)下面这个类。</p></blockquote><pre><code>import java.awt.*;import java.applet.*;public class DocFooter extends Applet { String date; String email; public void init() { resize(500,100); date = getParameter("LAST_UPDATED"); email = getParameter("EMAIL"); }}</code></pre><p>在命令行上键入javap DocFooter后,输出结果如下</p><p>Compiled from “DocFooter.java”</p><pre><code>public class DocFooter extends java.applet.Applet { java.lang.String date; java.lang.String email; public DocFooter(); public void init();}</code></pre><p>如果加入了-c,即javap -c DocFooter,那么输出结果如下</p><p>Compiled from “DocFooter.java”</p><pre><code>public class DocFooter extends java.applet.Applet { java.lang.String date; java.lang.String email; public DocFooter(); Code: 0: aload_0 1: invokespecial #1 // Method java/applet/Applet."<init>":()V 4: return public void init(); Code: 0: aload_0 1: sipush 500 4: bipush 100 6: invokevirtual #2 // Method resize:(II)V 9: aload_0 10: aload_0 11: ldc #3 // String LAST_UPDATED 13: invokevirtual #4 // Method getParameter:(Ljava/lang/String;)Ljava/lang/String; 16: putfield #5 // Field date:Ljava/lang/String; 19: aload_0 20: aload_0 21: ldc #6 // String EMAIL 23: invokevirtual #4 // Method getParameter:(Ljava/lang/String;)Ljava/lang/String; 26: putfield #7 // Field email:Ljava/lang/String; 29: return }</code></pre><p>上面输出的内容就是字节码。</p><p>用法摘要</p><p>-help 帮助<br>-l 输出行和变量的表<br>-public 只输出public方法和域<br>-protected 只输出public和protected类和成员<br>-package 只输出包,public和protected类和成员,这是默认的<br>-p -private 输出所有类和成员<br>-s 输出内部类型签名<br>-c 输出分解后的代码,例如,类中每一个方法内,包含java字节码的指令,<br>-verbose 输出栈大小,方法参数的个数<br>-constants 输出静态final常量<br>总结</p><p>javap可以用于反编译和查看编译器编译后的字节码。平时一般用javap -c比较多,该命令用于列出每个方法所执行的JVM指令,并显示每个方法的字节码的实际作用。可以通过字节码和源代码的对比,深入分析java的编译原理,了解和解决各种Java原理级别的问题。</p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java基础13:反射详解</title>
<link href="/2018/05/01/javase13/"/>
<url>/2018/05/01/javase13/</url>
<content type="html"><![CDATA[<p>本节主要介绍Java反射的原理,使用方法以及相关的技术细节,并且介绍了关于Class类,注解等内容。</p><p>具体代码在我的GitHub中可以找到</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/05/01/javase13">https://h2pl.github.io/2018/05/01/javase13</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><blockquote><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p></blockquote><a id="more"></a><h2 id="回顾:什么是反射?"><a href="#回顾:什么是反射?" class="headerlink" title="回顾:什么是反射?"></a>回顾:什么是反射?</h2><p>反射(Reflection)是Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。<br>Oracle官方对反射的解释是</p><blockquote><p>Reflection enables Java code to discover information about the fields, methods and constructors of loaded classes, and to use reflected fields, methods, and constructors to operate on their underlying counterparts, within security restrictions.</p></blockquote><blockquote><p>The API accommodates applications that need access to either the public members of a target object (based on its runtime class) or the members declared by a given class. It also allows programs to suppress default reflective access control.</p><p> 简而言之,通过反射,我们可以在运行时获得程序或程序集中每一个类型的成员和成员的信息。</p><p>程序中一般的对象的类型都是在编译期就确定下来的,而Java反射机制可以动态地创建对象并调用其属性,这样的对象的类型在编译期是未知的。所以我们可以通过反射机制直接创建对象,即使这个对象的类型在编译期是未知的。</p><p> 反射的核心是JVM在运行时才动态加载类或调用方法/访问属性,它不需要事先(写代码的时候或编译期)知道运行对象是谁。</p></blockquote><p>Java反射框架主要提供以下功能:</p><blockquote><p>1.在运行时判断任意一个对象所属的类;</p></blockquote><blockquote><p>2.在运行时构造任意一个类的对象;</p></blockquote><blockquote><p>3.在运行时判断任意一个类所具有的成员变量和方法(通过反射甚至可以调用private方法);</p></blockquote><blockquote><p>4.在运行时调用任意一个对象的方法</p></blockquote><blockquote><p>重点:是运行时而不是编译时</p></blockquote><h2 id="反射的主要用途"><a href="#反射的主要用途" class="headerlink" title="反射的主要用途"></a>反射的主要用途</h2><blockquote><p> 很多人都认为反射在实际的Java开发应用中并不广泛,其实不然。</p></blockquote><blockquote><p> 当我们在使用IDE(如Eclipse,IDEA)时,当我们输入一个对象或类并想调用它的属性或方法时,一按点号,编译器就会自动列出它的属性或方法,这里就会用到反射。</p></blockquote><blockquote><p> 反射最重要的用途就是开发各种通用框架。</p></blockquote><blockquote><p> 很多框架(比如Spring)都是配置化的(比如通过XML文件配置JavaBean,Action之类的),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。</p></blockquote><blockquote><p> 举一个例子,在运用Struts 2框架的开发中我们一般会在struts.xml里去配置Action,比如:</p></blockquote><pre><code><action name="login" class="org.ScZyhSoft.test.action.SimpleLoginAction" method="execute"> <result>/shop/shop-index.jsp</result> <result name="error">login.jsp</result> </action></code></pre><p>配置文件与Action建立了一种映射关系,当View层发出请求时,请求会被StrutsPrepareAndExecuteFilter拦截,然后StrutsPrepareAndExecuteFilter会去动态地创建Action实例。</p><p>——比如我们请求login.action,那么StrutsPrepareAndExecuteFilter就会去解析struts.xml文件,检索action中name为login的Action,并根据class属性创建SimpleLoginAction实例,并用invoke方法来调用execute方法,这个过程离不开反射。</p><blockquote><p>对与框架开发人员来说,反射虽小但作用非常大,它是各种容器实现的核心。而对于一般的开发者来说,不深入框架开发则用反射用的就会少一点,不过了解一下框架的底层机制有助于丰富自己的编程思想,也是很有益的。</p></blockquote><h2 id="反射的基础:关于Class类"><a href="#反射的基础:关于Class类" class="headerlink" title="反射的基础:关于Class类"></a>反射的基础:关于Class类</h2><p>更多关于Class类和Object类的原理和介绍请见上一节</p><blockquote><p>1、Class是一个类,一个描述类的类(也就是描述类本身),封装了描述方法的Method,描述字段的Filed,描述构造器的Constructor等属性</p><p>2、对象照镜子后(反射)可以得到的信息:某个类的数据成员名、方法和构造器、某个类到底实现了哪些接口。</p><p>3、对于每个类而言,JRE 都为其保留一个不变的 Class 类型的对象。一个Class对象包含了特定某个类的有关信息。</p><p>4、Class 对象只能由系统建立对象</p><p>5、一个类在 JVM 中只会有一个Class实例</p></blockquote><pre><code>//总结一下就是,JDK有一个类叫做Class,这个类用来封装所有Java类型,包括这些类的所有信息,JVM中类信息是放在方法区的。//所有类在加载后,JVM会为其在堆中创建一个Class<类名称>的对象,并且每个类只会有一个Class对象,这个类的所有对象都要通过Class<类名称>来进行实例化。//上面说的是JVM进行实例化的原理,当然实际上在Java写代码时只需要用 类名称就可以进行实例化了。public final class Class<T> implements java.io.Serializable, GenericDeclaration, Type, AnnotatedElement {虚拟机会保持唯一一 //通过类名.class获得唯一的Class对象。 Class<UserBean> cls = UserBean.class; //通过integer.TYPEl来获取Class对象 Class<Integer> inti = Integer.TYPE; //接口本质也是一个类,一样可以通过.class获取 Class<User> userClass = User.class;</code></pre><h2 id="反射的基本运用"><a href="#反射的基本运用" class="headerlink" title="反射的基本运用"></a>反射的基本运用</h2><p>上面我们提到了反射可以用于判断任意对象所属的类,获得Class对象,构造任意一个对象以及调用一个对象。这里我们介绍一下基本反射功能的实现(反射相关的类一般都在java.lang.relfect包里)。</p><p>1、获得Class对象方法有三种</p><p>(1)使用Class类的forName静态方法:</p><pre><code> public static Class<?> forName(String className)``` 在JDBC开发中常用此方法加载数据库驱动:要使用全类名来加载这个类,一般数据库驱动的配置信息会写在配置文件中。加载这个驱动前要先导入jar包```java Class.forName(driver);</code></pre><p>(2)直接获取某一个对象的class,比如:</p><pre><code>//Class<?>是一个泛型表示,用于获取一个类的类型。Class<?> klass = int.class;Class<?> classInt = Integer.TYPE;</code></pre><p>(3)调用某个对象的getClass()方法,比如:</p><pre><code>StringBuilder str = new StringBuilder("123");Class<?> klass = str.getClass();</code></pre><h2 id="判断是否为某个类的实例"><a href="#判断是否为某个类的实例" class="headerlink" title="判断是否为某个类的实例"></a>判断是否为某个类的实例</h2><p>一般地,我们用instanceof关键字来判断是否为某个类的实例。同时我们也可以借助反射中Class对象的isInstance()方法来判断是否为某个类的实例,它是一个Native方法:</p><p>==public native boolean isInstance(Object obj);==</p><h2 id="创建实例"><a href="#创建实例" class="headerlink" title="创建实例"></a>创建实例</h2><p>通过反射来生成对象主要有两种方式。</p><p>(1)使用Class对象的newInstance()方法来创建Class对象对应类的实例。</p><p>注意:利用newInstance创建对象:调用的类必须有无参的构造器</p><pre><code>//Class<?>代表任何类的一个类对象。//使用这个类对象可以为其他类进行实例化//因为jvm加载类以后自动在堆区生成一个对应的*.Class对象//该对象用于让JVM对进行所有*对象实例化。Class<?> c = String.class;//Class<?> 中的 ? 是通配符,其实就是表示任意符合泛类定义条件的类,和直接使用 Class//效果基本一致,但是这样写更加规范,在某些类型转换时可以避免不必要的 unchecked 错误。Object str = c.newInstance();</code></pre><p>(2)先通过Class对象获取指定的Constructor对象,再调用Constructor对象的newInstance()方法来创建实例。这种方法可以用指定的构造器构造类的实例。</p><pre><code>//获取String所对应的Class对象Class<?> c = String.class;//获取String类带一个String参数的构造器Constructor constructor = c.getConstructor(String.class);//根据构造器创建实例Object obj = constructor.newInstance("23333");System.out.println(obj);</code></pre><h2 id="获取方法"><a href="#获取方法" class="headerlink" title="获取方法"></a>获取方法</h2><p>获取某个Class对象的方法集合,主要有以下几个方法:</p><p>getDeclaredMethods()方法返回类或接口声明的所有方法,==包括公共、保护、默认(包)访问和私有方法,但不包括继承的方法==。</p><pre><code>public Method[] getDeclaredMethods() throws SecurityException</code></pre><p>getMethods()方法返回某个类的所有公用(public)方法,==包括其继承类的公用方法。==</p><pre><code>public Method[] getMethods() throws SecurityException</code></pre><p>getMethod方法返回一个特定的方法,其中第一个参数为方法名称,后面的参数为方法的参数对应Class的对象</p><pre><code>public Method getMethod(String name, Class<?>... parameterTypes)</code></pre><p>只是这样描述的话可能难以理解,我们用例子来理解这三个方法:<br>本文中的例子用到了以下这些类,用于反射的测试。</p><pre><code>//注解类,可可用于表示方法,可以通过反射获取注解的内容。 //Java注解的实现是很多注框架实现注解配置的基础@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Invoke {}</code></pre><p>userbean的父类personbean</p><pre><code>public class PersonBean {private String name;int id;public String getName() { return name;}public void setName(String name) { this.name = name;}</code></pre><p>}</p><p>接口user</p><pre><code>public interface User { public void login ();}</code></pre><p>userBean实现user接口,继承personbean</p><pre><code>public class UserBean extends PersonBean implements User{ @Override public void login() { } class B { } public String userName; protected int i; static int j; private int l; private long userId; public UserBean(String userName, long userId) { this.userName = userName; this.userId = userId; } public String getName() { return userName; } public long getId() { return userId; } @Invoke public static void staticMethod(String devName,int a) { System.out.printf("Hi %s, I'm a static method", devName); } @Invoke public void publicMethod() { System.out.println("I'm a public method"); } @Invoke private void privateMethod() { System.out.println("I'm a private method"); }}</code></pre><p>1 getMethods和getDeclaredMethods的区别</p><pre><code>public class 动态加载类的反射 { public static void main(String[] args) { try { Class clazz = Class.forName("com.javase.反射.UserBean"); for (Field field : clazz.getDeclaredFields()) {// field.setAccessible(true); System.out.println(field); } //getDeclaredMethod*()获取的是类自身声明的所有方法,包含public、protected和private方法。 System.out.println("------共有方法------");// getDeclaredMethod*()获取的是类自身声明的所有方法,包含public、protected和private方法。// getMethod*()获取的是类的所有共有方法,这就包括自身的所有public方法,和从基类继承的、从接口实现的所有public方法。 for (Method method : clazz.getMethods()) { String name = method.getName(); System.out.println(name); //打印出了UserBean.java的所有方法以及父类的方法 } System.out.println("------独占方法------"); for (Method method : clazz.getDeclaredMethods()) { String name = method.getName(); System.out.println(name); } } catch (ClassNotFoundException e) { e.printStackTrace(); } }}</code></pre><p>2 打印一个类的所有方法及详细信息:</p><pre><code>public class 打印所有方法 { public static void main(String[] args) { Class userBeanClass = UserBean.class; Field[] fields = userBeanClass.getDeclaredFields(); //注意,打印方法时无法得到局部变量的名称,因为jvm只知道它的类型 Method[] methods = userBeanClass.getDeclaredMethods(); for (Method method : methods) { //依次获得方法的修饰符,返回类型和名称,外加方法中的参数 String methodString = Modifier.toString(method.getModifiers()) + " " ; // private static methodString += method.getReturnType().getSimpleName() + " "; // void methodString += method.getName() + "("; // staticMethod Class[] parameters = method.getParameterTypes(); Parameter[] p = method.getParameters(); for (Class parameter : parameters) { methodString += parameter.getSimpleName() + " " ; // String } methodString += ")"; System.out.println(methodString); } //注意方法只能获取到其类型,拿不到变量名/* public String getName() public long getId() public static void staticMethod(String int ) public void publicMethod() private void privateMethod()*/ }}</code></pre><h2 id="获取构造器信息"><a href="#获取构造器信息" class="headerlink" title="获取构造器信息"></a>获取构造器信息</h2><p>获取类构造器的用法与上述获取方法的用法类似。主要是通过Class类的getConstructor方法得到Constructor类的一个实例,而Constructor类有一个newInstance方法可以创建一个对象实例:</p><pre><code>public class 打印构造方法 { public static void main(String[] args) { // constructors Class<?> clazz = UserBean.class; Class userBeanClass = UserBean.class; //获得所有的构造方法 Constructor[] constructors = userBeanClass.getDeclaredConstructors(); for (Constructor constructor : constructors) { String s = Modifier.toString(constructor.getModifiers()) + " "; s += constructor.getName() + "("; //构造方法的参数类型 Class[] parameters = constructor.getParameterTypes(); for (Class parameter : parameters) { s += parameter.getSimpleName() + ", "; } s += ")"; System.out.println(s); //打印结果//public com.javase.反射.UserBean(String, long, ) } }}</code></pre><h2 id="获取类的成员变量(字段)信息"><a href="#获取类的成员变量(字段)信息" class="headerlink" title="获取类的成员变量(字段)信息"></a>获取类的成员变量(字段)信息</h2><p>主要是这几个方法,在此不再赘述:</p><p>getFiled: 访问公有的成员变量<br>getDeclaredField:所有已声明的成员变量。但不能得到其父类的成员变量<br>getFileds和getDeclaredFields用法同上(参照Method)</p><pre><code>public class 打印成员变量 { public static void main(String[] args) { Class userBeanClass = UserBean.class; //获得该类的所有成员变量,包括static private Field[] fields = userBeanClass.getDeclaredFields(); for(Field field : fields) { //private属性即使不用下面这个语句也可以访问// field.setAccessible(true); //因为类的私有域在反射中默认可访问,所以flag默认为true。 String fieldString = ""; fieldString += Modifier.toString(field.getModifiers()) + " "; // `private` fieldString += field.getType().getSimpleName() + " "; // `String` fieldString += field.getName(); // `userName` fieldString += ";"; System.out.println(fieldString); //打印结果// public String userName;// protected int i;// static int j;// private int l;// private long userId; } }}</code></pre><h2 id="调用方法"><a href="#调用方法" class="headerlink" title="调用方法"></a>调用方法</h2><p>当我们从类中获取了一个方法后,我们就可以用invoke()方法来调用这个方法。invoke方法的原型为:</p><pre><code>public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException, InvocationTargetExceptionpublic class 使用反射调用方法 { public static void main(String[] args) throws InvocationTargetException, IllegalAccessException, InstantiationException, NoSuchMethodException { Class userBeanClass = UserBean.class; //获取该类所有的方法,包括静态方法,实例方法。 //此处也包括了私有方法,只不过私有方法在用invoke访问之前要设置访问权限 //也就是使用setAccessible使方法可访问,否则会抛出异常// // IllegalAccessException的解释是// * An IllegalAccessException is thrown when an application tries// * to reflectively create an instance (other than an array),// * set or get a field, or invoke a method, but the currently// * executing method does not have access to the definition of// * the specified class, field, method or constructor.// getDeclaredMethod*()获取的是类自身声明的所有方法,包含public、protected和private方法。// getMethod*()获取的是类的所有共有方法,这就包括自身的所有public方法,和从基类继承的、从接口实现的所有public方法。 //就是说,当这个类,域或者方法被设为私有访问,使用反射调用但是却没有权限时会抛出异常。 Method[] methods = userBeanClass.getDeclaredMethods(); // 获取所有成员方法 for (Method method : methods) { //反射可以获取方法上的注解,通过注解来进行判断 if (method.isAnnotationPresent(Invoke.class)) { // 判断是否被 @Invoke 修饰 //判断方法的修饰符是是static if (Modifier.isStatic(method.getModifiers())) { // 如果是 static 方法 //反射调用该方法 //类方法可以直接调用,不必先实例化 method.invoke(null, "wingjay",2); // 直接调用,并传入需要的参数 devName } else { //如果不是类方法,需要先获得一个实例再调用方法 //传入构造方法需要的变量类型 Class[] params = {String.class, long.class}; //获取该类指定类型的构造方法 //如果没有这种类型的方法会报错 Constructor constructor = userBeanClass.getDeclaredConstructor(params); // 获取参数格式为 String,long 的构造函数 //通过构造方法的实例来进行实例化 Object userBean = constructor.newInstance("wingjay", 11); // 利用构造函数进行实例化,得到 Object if (Modifier.isPrivate(method.getModifiers())) { method.setAccessible(true); // 如果是 private 的方法,需要获取其调用权限// Set the {@code accessible} flag for this object to// * the indicated boolean value. A value of {@code true} indicates that// * the reflected object should suppress Java language access// * checking when it is used. A value of {@code false} indicates// * that the reflected object should enforce Java language access checks. //通过该方法可以设置其可见或者不可见,不仅可以用于方法 //后面例子会介绍将其用于成员变量 //打印结果// I'm a public method// Hi wingjay, I'm a static methodI'm a private method } method.invoke(userBean); // 调用 method,无须参数 } } } }}</code></pre><h2 id="利用反射创建数组"><a href="#利用反射创建数组" class="headerlink" title="利用反射创建数组"></a>利用反射创建数组</h2><p>数组在Java里是比较特殊的一种类型,它可以赋值给一个Object Reference。下面我们看一看利用反射创建数组的例子:</p><pre><code>public class 用反射创建数组 { public static void main(String[] args) { Class<?> cls = null; try { cls = Class.forName("java.lang.String"); } catch (ClassNotFoundException e) { e.printStackTrace(); } Object array = Array.newInstance(cls,25); //往数组里添加内容 Array.set(array,0,"hello"); Array.set(array,1,"Java"); Array.set(array,2,"fuck"); Array.set(array,3,"Scala"); Array.set(array,4,"Clojure"); //获取某一项的内容 System.out.println(Array.get(array,3)); //Scala }}</code></pre><p>其中的Array类为java.lang.reflect.Array类。我们通过Array.newInstance()创建数组对象,它的原型是:</p><pre><code>public static Object newInstance(Class<?> componentType, int length) throws NegativeArraySizeException { return newArray(componentType, length); }</code></pre><p>而newArray()方法是一个Native方法,它在Hotspot JVM里的具体实现我们后边再研究,这里先把源码贴出来</p><pre><code>private static native Object newArray(Class<?> componentType, int length) throws NegativeArraySizeException;</code></pre><h2 id="Java的注解"><a href="#Java的注解" class="headerlink" title="Java的注解"></a>Java的注解</h2><p>9、注解(Annotation)</p><p>Java提供的注解,实际上可以通过反射的方式得到注解的内容 </p><blockquote><p>•从 JDK5.0 开始,Java 增加了对元数据(MetaData)的支持,也就是Annotation(注释)</p></blockquote><blockquote><p>•Annotation其实就是代码里的特殊标记,这些标记可以在编译,类加载, 运行时被读取,并执行相应的处理.通过使用Annotation,程序员可以在不改变原有逻辑的情况下,在源文件中嵌入一些补充信息.</p></blockquote><blockquote><p>•Annotation 可以像修饰符一样被使用,可用于修饰包,类,构造器, 方法,成员变量, 参数,局部变量的声明,这些信息被保存在Annotation的 “name=value”对中.</p></blockquote><blockquote><p>•Annotation能被用来为程序元素(类,方法,成员变量等)设置元数据</p></blockquote><p>基本的 Annotation</p><pre><code>•使用 Annotation时要在其前面增加@符号,并把该Annotation 当成一个修饰符使用.用于修饰它支持的程序元素•三个基本的Annotation: –@Override:限定重写父类方法,该注释只能用于方法 –@Deprecated:用于表示某个程序元素(类,方法等)已过时 –@SuppressWarnings:抑制编译器警告.</code></pre><p>自定义 Annotation</p><blockquote><p>•定义新的 Annotation类型使用@interface关键字</p><p>•Annotation 的成员变量在Annotation</p><p>定义中以无参数方法的形式来声明.其方法名和返回值定义了该成员的名字和类型.</p><p>•可以在定义Annotation的成员变量时为其指定初始值,指定成员变量的初始值可使用default关键字</p><p>•没有成员定义的Annotation称为标记;包含成员变量的Annotation称为元数据Annotation</p></blockquote><pre><code>@Retention(RetentionPolicy.RUNTIME) //运行时检验 @Target(value = {ElementType.METHOD}) //作用在方法上 public @interface AgeValidator { int min(); int max(); </code></pre><p>注解的获取方法</p><pre><code>/** * 通过反射才能获取注解 */ @Test public void testAnnotation() throws Exception { //这样的方式不能使用注解 Person3 person3 = new Person3(); person3.setAge(10);*/ String className = "com.java.reflection.Person3"; Class clazz = Class.forName(className); Object obj = clazz.newInstance(); Method method = clazz.getDeclaredMethod("setAge",Integer.class); int val =40; //获取注解 Annotation annotation = method.getAnnotation(AgeValidator.class); if (annotation != null){ if (annotation instanceof AgeValidator){ AgeValidator ageValidator = (AgeValidator) annotation; if (val< ageValidator.min() || val>ageValidator.max()){ throw new RuntimeException("数值超出范围"); } } } method.invoke(obj, val); System.out.println(obj); } </code></pre><p>提取 Annotation信息</p><pre><code>•JDK5.0 在 java.lang.reflect包下新增了 AnnotatedElement接口,该接口代表程序中可以接受注释的程序元素•当一个 Annotation类型被定义为运行时Annotation后,该注释才是运行时可见,当 class文件被载入时保存在 class文件中的 Annotation才会被虚拟机读取•程序可以调用AnnotationElement对象的如下方法来访问 Annotation信息–获取 Annotation实例:•getAnnotation(Class<T> annotationClass)•getDeclaredAnnotations()•getParameterAnnotations()JDK 的元Annotation•JDK 的元Annotation 用于修饰其他Annotation 定义•@Retention:只能用于修饰一个 Annotation定义,用于指定该 Annotation可以保留多长时间,@Rentention包含一个RetentionPolicy类型的成员变量,使用 @Rentention时必须为该 value成员变量指定值: –RetentionPolicy.CLASS:编译器将把注释记录在 class文件中.当运行 Java程序时,JVM 不会保留注释.这是默认值 –RetentionPolicy.RUNTIME:编译器将把注释记录在class文件中. 当运行 Java 程序时, JVM 会保留注释. 程序可以通过反射获取该注释 –RetentionPolicy.SOURCE:编译器直接丢弃这种策略的注释•@Target: 用于修饰Annotation 定义,用于指定被修饰的 Annotation能用于修饰哪些程序元素.@Target 也包含一个名为 value的成员变量.•@Documented:用于指定被该元 Annotation修饰的 Annotation类将被 javadoc工具提取成文档.•@Inherited:被它修饰的 Annotation将具有继承性.如果某个类使用了被@Inherited 修饰的Annotation, 则其子类将自动具有该注释</code></pre>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java基础12:深入理解Class类和Object类</title>
<link href="/2018/04/30/javase12/"/>
<url>/2018/04/30/javase12/</url>
<content type="html"><![CDATA[<p>本文对java的Class类和Object类的概念和原理做了详尽的介绍,并且详细介绍了Object的各种方法,以及这两个类之间的关系。</p><p>Class类和Object类是Java中最根本最重要的两个类,理解它们是理解Java面向对象技术的基础,也是学习所有进阶Java技术的基石。</p><p>具体代码在我的GitHub中可以找到</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/04/30/javase12">https://h2pl.github.io/2018/04/30/javase12</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><blockquote><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p></blockquote><a id="more"></a><blockquote><p>注意这里说的Class是Java中的java.lang.Class类。这个类用于记录Java中每个类的类型信息,并且jvm在类加载时会为每个类生成一个Class<a>的Class对象在Java堆中,每个A类型的实例都要通过这个Class对象来进行实例化。</a></p></blockquote><p>这部分参考<a href="https://blog.csdn.net/s10461/article/details/53941091" target="_blank" rel="noopener">https://blog.csdn.net/s10461/article/details/53941091</a></p><h2 id="Java中Class类及用法"><a href="#Java中Class类及用法" class="headerlink" title="Java中Class类及用法"></a>Java中Class类及用法</h2><p>Java程序在运行时,Java运行时系统一直对所有的对象进行所谓的运行时类型标识,即所谓的RTTI。</p><blockquote><p>这项信息纪录了每个对象所属的类。虚拟机通常使用运行时类型信息选准正确方法去执行,用来保存这些类型信息的类是Class类。Class类封装一个对象和接口运行时的状态,当装载类时,Class类型的对象自动创建。</p></blockquote><p>说白了就是:</p><blockquote><p>Class类也是类的一种,只是名字和class关键字高度相似。Java是大小写敏感的语言。</p></blockquote><blockquote><p>Class类的对象内容是你创建的类的类型信息,比如你创建一个shapes类,那么,Java会生成一个内容是shapes的Class类的对象</p></blockquote><blockquote><p>Class类的对象不能像普通类一样,以 new shapes() 的方式创建,它的对象只能由JVM创建,因为这个类没有public构造函数</p></blockquote><pre><code>/* * Private constructor. Only the Java Virtual Machine creates Class objects. * This constructor is not used and prevents the default constructor being * generated. */ //私有构造方法,只能由jvm进行实例化private Class(ClassLoader loader) { // Initialize final field for classLoader. The initialization value of non-null // prevents future JIT optimizations from assuming this final field is null. classLoader = loader;}</code></pre><blockquote><p>Class类的作用是运行时提供或获得某个对象的类型信息,和C++中的typeid()函数类似。这些信息也可用于反射。</p></blockquote><h3 id="Class类原理"><a href="#Class类原理" class="headerlink" title="Class类原理"></a>Class类原理</h3><p>看一下Class类的部分源码</p><pre><code>//Class类中封装了类型的各种信息。在jvm中就是通过Class类的实例来获取每个Java类的所有信息的。public class Class类 { Class aClass = null;// private EnclosingMethodInfo getEnclosingMethodInfo() {// Object[] enclosingInfo = getEnclosingMethod0();// if (enclosingInfo == null)// return null;// else {// return new EnclosingMethodInfo(enclosingInfo);// }// } /**提供原子类操作 * Atomic operations support. */// private static class Atomic {// // initialize Unsafe machinery here, since we need to call Class.class instance method// // and have to avoid calling it in the static initializer of the Class class...// private static final Unsafe unsafe = Unsafe.getUnsafe();// // offset of Class.reflectionData instance field// private static final long reflectionDataOffset;// // offset of Class.annotationType instance field// private static final long annotationTypeOffset;// // offset of Class.annotationData instance field// private static final long annotationDataOffset;//// static {// Field[] fields = Class.class.getDeclaredFields0(false); // bypass caches// reflectionDataOffset = objectFieldOffset(fields, "reflectionData");// annotationTypeOffset = objectFieldOffset(fields, "annotationType");// annotationDataOffset = objectFieldOffset(fields, "annotationData");// } //提供反射信息 // reflection data that might get invalidated when JVM TI RedefineClasses() is called// private static class ReflectionData<T> {// volatile Field[] declaredFields;// volatile Field[] publicFields;// volatile Method[] declaredMethods;// volatile Method[] publicMethods;// volatile Constructor<T>[] declaredConstructors;// volatile Constructor<T>[] publicConstructors;// // Intermediate results for getFields and getMethods// volatile Field[] declaredPublicFields;// volatile Method[] declaredPublicMethods;// volatile Class<?>[] interfaces;//// // Value of classRedefinedCount when we created this ReflectionData instance// final int redefinedCount;//// ReflectionData(int redefinedCount) {// this.redefinedCount = redefinedCount;// }// } //方法数组// static class MethodArray {// // Don't add or remove methods except by add() or remove() calls.// private Method[] methods;// private int length;// private int defaults;//// MethodArray() {// this(20);// }//// MethodArray(int initialSize) {// if (initialSize < 2)// throw new IllegalArgumentException("Size should be 2 or more");//// methods = new Method[initialSize];// length = 0;// defaults = 0;// } //注解信息 // annotation data that might get invalidated when JVM TI RedefineClasses() is called// private static class AnnotationData {// final Map<Class<? extends Annotation>, Annotation> annotations;// final Map<Class<? extends Annotation>, Annotation> declaredAnnotations;//// // Value of classRedefinedCount when we created this AnnotationData instance// final int redefinedCount;//// AnnotationData(Map<Class<? extends Annotation>, Annotation> annotations,// Map<Class<? extends Annotation>, Annotation> declaredAnnotations,// int redefinedCount) {// this.annotations = annotations;// this.declaredAnnotations = declaredAnnotations;// this.redefinedCount = redefinedCount;// }// }}</code></pre><blockquote><p>我们都知道所有的java类都是继承了object这个类,在object这个类中有一个方法:getclass().这个方法是用来取得该类已经被实例化了的对象的该类的引用,这个引用指向的是Class类的对象。</p><p>我们自己无法生成一个Class对象(构造函数为private),而 这个Class类的对象是在当各类被调入时,由 Java 虚拟机自动创建 Class 对象,或通过类装载器中的 defineClass 方法生成。</p></blockquote><pre><code>//通过该方法可以动态地将字节码转为一个Class类对象protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ return defineClass(name, b, off, len, null);}</code></pre><blockquote><p>我们生成的对象都会有个字段记录该对象所属类在CLass类的对象的所在位置。如下图所示:</p></blockquote><p><img src="http://dl.iteye.com/upload/picture/pic/101542/0047a6e9-6608-3c3c-a67c-d8ee95e7fcb8.jpg" alt="image"></p><h3 id="如何获得一个Class类对象"><a href="#如何获得一个Class类对象" class="headerlink" title="如何获得一个Class类对象"></a>如何获得一个Class类对象</h3><p>请注意,以下这些方法都是值、指某个类对应的Class对象已经在堆中生成以后,我们通过不同方式获取对这个Class对象的引用。而上面说的DefineClass才是真正将字节码加载到虚拟机的方法,会在堆中生成新的一个Class对象。</p><p>第一种办法,Class类的forName函数</p><blockquote><p>public class shapes{}<br>Class obj= Class.forName(“shapes”);<br>第二种办法,使用对象的getClass()函数</p></blockquote><blockquote><p>public class shapes{}<br>shapes s1=new shapes();<br>Class obj=s1.getClass();<br>Class obj1=s1.getSuperclass();//这个函数作用是获取shapes类的父类的类型</p></blockquote><p>第三种办法,使用类字面常量</p><blockquote><p>Class obj=String.class;<br>Class obj1=int.class;<br>注意,使用这种办法生成Class类对象时,不会使JVM自动加载该类(如String类)。==而其他办法会使得JVM初始化该类。==</p></blockquote><h3 id="使用Class类的对象来生成目标类的实例"><a href="#使用Class类的对象来生成目标类的实例" class="headerlink" title="使用Class类的对象来生成目标类的实例"></a>使用Class类的对象来生成目标类的实例</h3><blockquote><p>生成不精确的object实例</p></blockquote><p>==获取一个Class类的对象后,可以用 newInstance() 函数来生成目标类的一个实例。然而,该函数并不能直接生成目标类的实例,只能生成object类的实例==</p><blockquote><p>Class obj=Class.forName(“shapes”);<br>Object ShapesInstance=obj.newInstance();<br>使用泛化Class引用生成带类型的目标实例</p></blockquote><blockquote><p>Class<shapes> obj=shapes.class;<br>shapes newShape=obj.newInstance();<br>因为有了类型限制,所以使用泛化Class语法的对象引用不能指向别的类。</shapes></p></blockquote><pre><code>Class obj1=int.class;Class<Integer> obj2=int.class;obj1=double.class;//obj2=double.class; 这一行代码是非法的,obj2不能改指向别的类然而,有个灵活的用法,使得你可以用Class的对象指向基类的任何子类。Class<? extends Number> obj=int.class;obj=Number.class;obj=double.class;因此,以下语法生成的Class对象可以指向任何类。Class<?> obj=int.class;obj=double.class;obj=shapes.class;最后一个奇怪的用法是,当你使用这种泛型语法来构建你手头有的一个Class类的对象的基类对象时,必须采用以下的特殊语法public class shapes{}class round extends shapes{}Class<round> rclass=round.class;Class<? super round> sclass= rclass.getSuperClass();//Class<shapes> sclass=rclass.getSuperClass();我们明知道,round的基类就是shapes,但是却不能直接声明 Class < shapes >,必须使用特殊语法Class < ? super round ></code></pre><p>这个记住就可以啦。</p><h2 id="Object类"><a href="#Object类" class="headerlink" title="Object类"></a>Object类</h2><p>这部分主要参考<a href="http://ihenu.iteye.com/blog/2233249" target="_blank" rel="noopener">http://ihenu.iteye.com/blog/2233249</a></p><p>Object类是Java中其他所有类的祖先,没有Object类Java面向对象无从谈起。作为其他所有类的基类,Object具有哪些属性和行为,是Java语言设计背后的思维体现。</p><p>Object类位于java.lang包中,java.lang包包含着Java最基础和核心的类,在编译时会自动导入。Object类没有定义属性,一共有13个方法,13个方法之中并不是所有方法都是子类可访问的,一共有9个方法是所有子类都继承了的。</p><p>先大概介绍一下这些方法</p><pre><code>1.clone方法保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。2.getClass方法final方法,获得运行时类型。3.toString方法该方法用得比较多,一般子类都有覆盖。4.finalize方法该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用。5.equals方法该方法是非常重要的一个方法。一般equals和==是不一样的,但是在Object中两者是一样的。子类一般都要重写这个方法。6.hashCode方法该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有哈希功能的Collection中用到。一般必须满足obj1.equals(obj2)==true。可以推出obj1.hash- Code()==obj2.hashCode(),但是hashCode相等不一定就满足equals。不过为了提高效率,应该尽量使上面两个条件接近等价。7.wait方法wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或者被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回。调用该方法后当前线程进入睡眠状态,直到以下事件发生。(1)其他线程调用了该对象的notify方法。(2)其他线程调用了该对象的notifyAll方法。(3)其他线程调用了interrupt中断该线程。(4)时间间隔到了。此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。8.notify方法该方法唤醒在该对象上等待的某个线程。9.notifyAll方法该方法唤醒在该对象上等待的所有线程。</code></pre><h3 id="类构造器public-Object"><a href="#类构造器public-Object" class="headerlink" title="类构造器public Object();"></a>类构造器public Object();</h3><blockquote><p>大部分情况下,Java中通过形如 new A(args..)形式创建一个属于该类型的对象。其中A即是类名,A(args..)即此类定义中相对应的构造函数。通过此种形式创建的对象都是通过类中的构造函数完成。</p></blockquote><blockquote><p>为体现此特性,Java中规定:在类定义过程中,对于未定义构造函数的类,默认会有一个无参数的构造函数,作为所有类的基类,Object类自然要反映出此特性,在源码中,未给出Object类构造函数定义,但实际上,此构造函数是存在的。</p><p>当然,并不是所有的类都是通过此种方式去构建,也自然的,并不是所有的类构造函数都是public。</p></blockquote><h3 id="registerNatives-方法"><a href="#registerNatives-方法" class="headerlink" title="registerNatives()方法;"></a>registerNatives()方法;</h3><p>private static native void registerNatives();</p><blockquote><p>registerNatives函数前面有native关键字修饰,Java中,用native关键字修饰的函数表明该方法的实现并不是在Java中去完成,而是由C/C++去完成,并被编译成了.dll,由Java去调用。</p><p>方法的具体实现体在dll文件中,对于不同平台,其具体实现应该有所不同。用native修饰,即表示操作系统,需要提供此方法,Java本身需要使用。</p><p>具体到registerNatives()方法本身,其主要作用是将C/C++中的方法映射到Java中的native方法,实现方法命名的解耦。</p><p>既然如此,可能有人会问,registerNatives()修饰符为private,且并没有执行,作用何以达到?其实,在Java源码中,此方法的声明后有紧接着一段静态代码块:</p></blockquote><pre><code>private static native void registerNatives(); static { registerNatives(); } </code></pre><h3 id="Clone-方法实现浅拷贝"><a href="#Clone-方法实现浅拷贝" class="headerlink" title="Clone()方法实现浅拷贝"></a>Clone()方法实现浅拷贝</h3><pre><code>protected native Object clone() throwsCloneNotSupportedException;</code></pre><blockquote><p>看,clode()方法又是一个被声明为native的方法,因此,我们知道了clone()方法并不是Java的原生方法,具体的实现是有C/C++完成的。clone英文翻译为”克隆”,其目的是创建并返回此对象的一个副本。</p></blockquote><blockquote><p>形象点理解,这有一辆科鲁兹,你看着不错,想要个一模一样的。你调用此方法即可像变魔术一样变出一辆一模一样的科鲁兹出来。配置一样,长相一样。但从此刻起,原来的那辆科鲁兹如果进行了新的装饰,与你克隆出来的这辆科鲁兹没有任何关系了。</p><p>你克隆出来的对象变不变完全在于你对克隆出来的科鲁兹有没有进行过什么操作了。Java术语表述为:clone函数返回的是一个引用,指向的是新的clone出来的对象,此对象与原对象分别占用不同的堆空间。</p></blockquote><p>明白了clone的含义后,接下来看看如果调用clone()函数对象进行此克隆操作。</p><p>首先看一下下面的这个例子:</p><pre><code>package com.corn.objectsummary; import com.corn.Person; public class ObjectTest { public static void main(String[] args) { Object o1 = new Object(); // The method clone() from the type Object is not visible Object clone = o1.clone(); } } </code></pre><blockquote><p>例子很简单,在main()方法中,new一个Oject对象后,想直接调用此对象的clone方法克隆一个对象,但是出现错误提示:”The method clone() from the type Object is not visible”</p><p>why? 根据提示,第一反应是ObjectTest类中定义的Oject对象无法访问其clone()方法。回到Object类中clone()方法的定义,可以看到其被声明为protected,估计问题就在这上面了,protected修饰的属性或方法表示:在同一个包内或者不同包的子类可以访问。</p><p>显然,Object类与ObjectTest类在不同的包中,但是ObjectTest继承自Object,是Object类的子类,于是,现在却出现子类中通过Object引用不能访问protected方法,原因在于对”不同包中的子类可以访问”没有正确理解。</p><p>“不同包中的子类可以访问”,是指当两个类不在同一个包中的时候,继承自父类的子类内部且主调(调用者)为子类的引用时才能访问父类用protected修饰的成员(属性/方法)。 在子类内部,主调为父类的引用时并不能访问此protected修饰的成员。!(super关键字除外)</p></blockquote><p>于是,上例改成如下形式,我们发现,可以正常编译:</p><pre><code> public class clone方法 { public static void main(String[] args) { } public void test1() { User user = new User();// User copy = user.clone(); } public void test2() { User user = new User();// User copy = (User)user.clone(); }}</code></pre><p>是的,因为此时的主调已经是子类的引用了。</p><blockquote><p>上述代码在运行过程中会抛出”java.lang.CloneNotSupportedException”,表明clone()方法并未正确执行完毕,问题的原因在与Java中的语法规定:</p><p>clone()的正确调用是需要实现Cloneable接口,如果没有实现Cloneable接口,并且子类直接调用Object类的clone()方法,则会抛出CloneNotSupportedException异常。</p><p>Cloneable接口仅是一个表示接口,接口本身不包含任何方法,用来指示Object.clone()可以合法的被子类引用所调用。</p><p>于是,上述代码改成如下形式,即可正确指定clone()方法以实现克隆。</p></blockquote><pre><code>public class User implements Cloneable{public int id;public String name;public UserInfo userInfo;public static void main(String[] args) { User user = new User(); UserInfo userInfo = new UserInfo(); user.userInfo = userInfo; System.out.println(user); System.out.println(user.userInfo); try { User copy = (User) user.clone(); System.out.println(copy); System.out.println(copy.userInfo); } catch (CloneNotSupportedException e) { e.printStackTrace(); }}//拷贝的User实例与原来不一样,是两个对象。// com.javase.Class和Object.Object方法.用到的类.User@4dc63996// com.javase.Class和Object.Object方法.用到的类.UserInfo@d716361 //而拷贝后对象的userinfo引用对象是同一个。 //所以这是浅拷贝// com.javase.Class和Object.Object方法.用到的类.User@6ff3c5b5// com.javase.Class和Object.Object方法.用到的类.UserInfo@d716361}</code></pre><p>总结:<br>clone方法实现的是浅拷贝,只拷贝当前对象,并且在堆中分配新的空间,放这个复制的对象。但是对象如果里面有其他类的子对象,那么就不会拷贝到新的对象中。</p><p>==深拷贝和浅拷贝的区别==</p><blockquote><p>浅拷贝<br>浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。</p><p>深拷贝<br>深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。<br>现在为了要在clone对象时进行深拷贝, 那么就要Clonable接口,覆盖并实现clone方法,除了调用父类中的clone方法得到新的对象, 还要将该类中的引用变量也clone出来。如果只是用Object中默认的clone方法,是浅拷贝的。</p></blockquote><p>那么这两种方式有什么相同和不同呢?</p><blockquote><p>new操作符的本意是分配内存。程序执行到new操作符时, 首先去看new操作符后面的类型,因为知道了类型,才能知道要分配多大的内存空间。</p><p>分配完内存之后,再调用构造函数,填充对象的各个域,这一步叫做对象的初始化,构造方法返回后,一个对象创建完毕,可以把他的引用(地址)发布到外部,在外部就可以使用这个引用操纵这个对象。</p><p>而clone在第一步是和new相似的, 都是分配内存,调用clone方法时,分配的内存和源对象(即调用clone方法的对象)相同,然后再使用原对象中对应的各个域,填充新对象的域,</p><p>填充完成之后,clone方法返回,一个新的相同的对象被创建,同样可以把这个新对象的引用发布到外部。</p></blockquote><p>==也就是说,一个对象在浅拷贝以后,只是把对象复制了一份放在堆空间的另一个地方,但是成员变量如果有引用指向其他对象,这个引用指向的对象和被拷贝的对象中引用指向的对象是一样的。当然,基本数据类型还是会重新拷贝一份的。==</p><h3 id="getClass-方法"><a href="#getClass-方法" class="headerlink" title="getClass()方法"></a>getClass()方法</h3><p>4.public final native Class<?> getClass();</p><blockquote><p>getClass()也是一个native方法,返回的是此Object对象的类对象/运行时类对象Class<?>。效果与Object.class相同。</p><p>首先解释下”类对象”的概念:在Java中,类是是对具有一组相同特征或行为的实例的抽象并进行描述,对象则是此类所描述的特征或行为的具体实例。</p><p>作为概念层次的类,其本身也具有某些共同的特性,如都具有类名称、由类加载器去加载,都具有包,具有父类,属性和方法等。</p><p>于是,Java中有专门定义了一个类,Class,去描述其他类所具有的这些特性,因此,从此角度去看,类本身也都是属于Class类的对象。为与经常意义上的对象相区分,在此称之为”类对象”。</p></blockquote><pre><code>public class getClass方法 { public static void main(String[] args) { User user = new User(); //getclass方法是native方法,可以取到堆区唯一的Class<User>对象 Class<?> aClass = user.getClass(); Class bClass = User.class; try { Class cClass = Class.forName("com.javase.Class和Object.Object方法.用到的类.User"); } catch (ClassNotFoundException e) { e.printStackTrace(); } System.out.println(aClass); System.out.println(bClass);// class com.javase.Class和Object.Object方法.用到的类.User// class com.javase.Class和Object.Object方法.用到的类.User try { User a = (User) aClass.newInstance(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } }} </code></pre><p>此处主要大量涉及到Java中的反射知识</p><h3 id="equals-方法"><a href="#equals-方法" class="headerlink" title="equals()方法"></a>equals()方法</h3><p>5.public boolean equals(Object obj);</p><blockquote><p>与equals在Java中经常被使用,大家也都知道与equals的区别:</p><p>==表示的是变量值完成相同(对于基础类型,地址中存储的是值,引用类型则存储指向实际对象的地址);</p><p>equals表示的是对象的内容完全相同,此处的内容多指对象的特征/属性。</p></blockquote><p>实际上,上面说法是不严谨的,更多的只是常见于String类中。首先看一下Object类中关于equals()方法的定义:</p><pre><code>public boolean equals(Object obj) { return (this == obj); } </code></pre><blockquote><p>由此可见,Object原生的equals()方法内部调用的正是==,与==具有相同的含义。既然如此,为什么还要定义此equals()方法?</p><p>equals()方法的正确理解应该是:判断两个对象是否相等。那么判断对象相等的标尺又是什么?</p><p>如上,在object类中,此标尺即为==。当然,这个标尺不是固定的,其他类中可以按照实际的需要对此标尺含义进行重定义。如String类中则是依据字符串内容是否相等来重定义了此标尺含义。如此可以增加类的功能型和实际编码的灵活性。当然了,如果自定义的类没有重写equals()方法来重新定义此标尺,那么默认的将是其父类的equals(),直到object基类。</p><p>如下场景的实际业务需求,对于User bean,由实际的业务需求可知当属性uid相同时,表示的是同一个User,即两个User对象相等。则可以重写equals以重定义User对象相等的标尺。</p></blockquote><p>ObjectTest中打印出true,因为User类定义中重写了equals()方法,这很好理解,很可能张三是一个人小名,张三丰才是其大名,判断这两个人是不是同一个人,这时只用判断uid是否相同即可。</p><blockquote><p>如上重写equals方法表面上看上去是可以了,实则不然。因为它破坏了Java中的约定:重写equals()方法必须重写hasCode()方法。</p></blockquote><h3 id="hashCode-方法"><a href="#hashCode-方法" class="headerlink" title="hashCode()方法;"></a>hashCode()方法;</h3><ol start="6"><li>public native int hashCode()</li></ol><p>hashCode()方法返回一个整形数值,表示该对象的哈希码值。</p><p>hashCode()具有如下约定:</p><blockquote><p>1).在Java应用程序程序执行期间,对于同一对象多次调用hashCode()方法时,其返回的哈希码是相同的,前提是将对象进行equals比较时所用的标尺信息未做修改。在Java应用程序的一次执行到另外一次执行,同一对象的hashCode()返回的哈希码无须保持一致;</p><p>2).如果两个对象相等(依据:调用equals()方法),那么这两个对象调用hashCode()返回的哈希码也必须相等;</p><p>3).反之,两个对象调用hasCode()返回的哈希码相等,这两个对象不一定相等。</p></blockquote><pre><code>即严格的数学逻辑表示为: 两个对象相等 <=> equals()相等 => hashCode()相等。因此,重写equlas()方法必须重写hashCode()方法,以保证此逻辑严格成立,同时可以推理出:hasCode()不相等 => equals()不相等 <=> 两个对象不相等。可能有人在此产生疑问:既然比较两个对象是否相等的唯一条件(也是冲要条件)是equals,那么为什么还要弄出一个hashCode(),并且进行如此约定,弄得这么麻烦?其实,这主要体现在hashCode()方法的作用上,其主要用于增强哈希表的性能。以集合类中,以Set为例,当新加一个对象时,需要判断现有集合中是否已经存在与此对象相等的对象,如果没有hashCode()方法,需要将Set进行一次遍历,并逐一用equals()方法判断两个对象是否相等,此种算法时间复杂度为o(n)。通过借助于hasCode方法,先计算出即将新加入对象的哈希码,然后根据哈希算法计算出此对象的位置,直接判断此位置上是否已有对象即可。(注:Set的底层用的是Map的原理实现)</code></pre><blockquote><p>在此需要纠正一个理解上的误区:对象的hashCode()返回的不是对象所在的物理内存地址。甚至也不一定是对象的逻辑地址,hashCode()相同的两个对象,不一定相等,换言之,不相等的两个对象,hashCode()返回的哈希码可能相同。</p><p>因此,在上述代码中,重写了equals()方法后,需要重写hashCode()方法。</p></blockquote><pre><code>public class equals和hashcode方法 { @Override //修改equals时必须同时修改hashcode方法,否则在作为key时会出问题 public boolean equals(Object obj) { return (this == obj); } @Override //相同的对象必须有相同hashcode,不同对象可能有相同hashcode public int hashCode() { return hashCode() >> 2; }}</code></pre><h3 id="toString-方法"><a href="#toString-方法" class="headerlink" title="toString()方法"></a>toString()方法</h3><p>7.public String toString();</p><pre><code>toString()方法返回该对象的字符串表示。先看一下Object中的具体方法体: public String toString() { return getClass().getName() + "@" + Integer.toHexString(hashCode()); } </code></pre><blockquote><p>toString()方法相信大家都经常用到,即使没有显式调用,但当我们使用System.out.println(obj)时,其内部也是通过toString()来实现的。</p><p>getClass()返回对象的类对象,getClassName()以String形式返回类对象的名称(含包名)。Integer.toHexString(hashCode())则是以对象的哈希码为实参,以16进制无符号整数形式返回此哈希码的字符串表示形式。</p><p>如上例中的u1的哈希码是638,则对应的16进制为27e,调用toString()方法返回的结果为:com.corn.objectsummary.User@27e。</p><p>因此:toString()是由对象的类型和其哈希码唯一确定,同一类型但不相等的两个对象分别调用toString()方法返回的结果可能相同。</p></blockquote><h3 id="wait-notify-notifAll"><a href="#wait-notify-notifAll" class="headerlink" title="wait() notify() notifAll()"></a>wait() notify() notifAll()</h3><p>8/9/10/11/12. wait(…) / notify() / notifyAll()</p><blockquote><p>一说到wait(…) / notify() | notifyAll()几个方法,首先想到的是线程。确实,这几个方法主要用于java多线程之间的协作。先具体看下这几个方法的主要含义:</p><p>wait():调用此方法所在的当前线程等待,直到在其他线程上调用此方法的主调(某一对象)的notify()/notifyAll()方法。</p><p>wait(long timeout)/wait(long timeout, int nanos):调用此方法所在的当前线程等待,直到在其他线程上调用此方法的主调(某一对象)的notisfy()/notisfyAll()方法,或超过指定的超时时间量。</p><p>notify()/notifyAll():唤醒在此对象监视器上等待的单个线程/所有线程。</p><p>wait(…) / notify() | notifyAll()一般情况下都是配套使用。下面来看一个简单的例子:</p></blockquote><p>这是一个生产者消费者的模型,只不过这里只用flag来标识哪个线程需要工作</p><pre><code>public class wait和notify { //volatile保证线程可见性 volatile static int flag = 1; //object作为锁对象,用于线程使用wait和notify方法 volatile static Object o = new Object(); public static void main(String[] args) { new Thread(new Runnable() { @Override public void run() { //wait和notify只能在同步代码块内使用 synchronized (o) { while (true) { if (flag == 0) { try { Thread.sleep(2000); System.out.println("thread1 wait"); //释放锁,线程挂起进入object的等待队列,后续代码运行 o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("thread1 run"); System.out.println("notify t2"); flag = 0; //通知等待队列的一个线程获取锁 o.notify(); } } } }).start(); //解释同上 new Thread(new Runnable() { @Override public void run() { while (true) { synchronized (o) { if (flag == 1) { try { Thread.sleep(2000); System.out.println("thread2 wait"); o.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("thread2 run"); System.out.println("notify t1"); flag = 1; o.notify(); } } } }).start(); } //输出结果是// thread1 run// notify t2// thread1 wait// thread2 run// notify t1// thread2 wait// thread1 run// notify t2//不断循环}</code></pre><blockquote><p> 从上述例子的输出结果中可以得出如下结论:</p><p>1、wait(…)方法调用后当前线程将立即阻塞,且适当其所持有的同步代码块中的锁,直到被唤醒或超时或打断后且重新获取到锁后才能继续执行;</p><p>2、notify()/notifyAll()方法调用后,其所在线程不会立即释放所持有的锁,直到其所在同步代码块中的代码执行完毕,此时释放锁,因此,如果其同步代码块后还有代码,其执行则依赖于JVM的线程调度。</p></blockquote><p>在Java源码中,可以看到wait()具体定义如下:</p><pre><code>public final void wait() throws InterruptedException { wait(0); } </code></pre><blockquote><p>且wait(long timeout, int nanos)方法定义内部实质上也是通过调用wait(long timeout)完成。而wait(long timeout)是一个native方法。因此,wait(…)方法本质上都是native方式实现。</p></blockquote><p>notify()/notifyAll()方法也都是native方法。</p><p>Java中线程具有较多的知识点,是一块比较大且重要的知识点。后期会有博文专门针对Java多线程作出详细总结。此处不再细述。</p><h3 id="finalize-方法"><a href="#finalize-方法" class="headerlink" title="finalize()方法"></a>finalize()方法</h3><ol start="13"><li>protected void finalize();</li></ol><p>finalize方法主要与Java垃圾回收机制有关。首先我们看一下finalized方法在Object中的具体定义:</p><pre><code>protected void finalize() throws Throwable { } </code></pre><blockquote><p>我们发现Object类中finalize方法被定义成一个空方法,为什么要如此定义呢?finalize方法的调用时机是怎么样的呢?</p><p>首先,Object中定义finalize方法表明Java中每一个对象都将具有finalize这种行为,其具体调用时机在:JVM准备对此对形象所占用的内存空间进行垃圾回收前,将被调用。由此可以看出,此方法并不是由我们主动去调用的(虽然可以主动去调用,此时与其他自定义方法无异)。</p></blockquote><h2 id="CLass类和Object类的关系"><a href="#CLass类和Object类的关系" class="headerlink" title="CLass类和Object类的关系"></a>CLass类和Object类的关系</h2><blockquote><p>Object类和Class类没有直接的关系。</p><p>Object类是一切java类的父类,对于普通的java类,即便不声明,也是默认继承了Object类。典型的,可以使用Object类中的toString()方法。</p><p>Class类是用于java反射机制的,一切java类,都有一个对应的Class对象,他是一个final类。Class 类的实例表示,正在运行的 Java 应用程序中的类和接口。</p></blockquote><p>转一个知乎很有趣的问题<br><a href="https://www.zhihu.com/question/30301819" target="_blank" rel="noopener">https://www.zhihu.com/question/30301819</a></p><pre><code>Java的对象模型中:1 所有的类都是Class类的实例,Object是类,那么Object也是Class类的一个实例。2 所有的类都最终继承自Object类,Class是类,那么Class也继承自Object。3 这就像是先有鸡还是先有蛋的问题,请问实际中JVM是怎么处理的?</code></pre><blockquote><p>这个问题中,第1个假设是错的:java.lang.Object是一个Java类,但并不是java.lang.Class的一个实例。后者只是一个用于描述Java类与接口的、用于支持反射操作的类型。这点上Java跟其它一些更纯粹的面向对象语言(例如Python和Ruby)不同。</p><p>而第2个假设是对的:java.lang.Class是java.lang.Object的派生类,前者继承自后者。虽然第1个假设不对,但“鸡蛋问题”仍然存在:在一个已经启动完毕、可以使用的Java对象系统里,必须要有一个java.lang.Class实例对应java.lang.Object这个类;而java.lang.Class是java.lang.Object的派生类,按“一般思维”前者应该要在后者完成初始化之后才可以初始化…</p><p>事实是:这些相互依赖的核心类型完全可以在“混沌”中一口气都初始化好,然后对象系统的状态才叫做完成了“bootstrap”,后面就可以按照Java对象系统的一般规则去运行。JVM、JavaScript、Python、Ruby等的运行时都有这样的bootstrap过程。</p><p>在“混沌”(boostrap过程)里,JVM可以为对象系统中最重要的一些核心类型先分配好内存空间,让它们进入[已分配空间]但[尚未完全初始化]状态。此时这些对象虽然已经分配了空间,但因为状态还不完整所以尚不可使用。</p><p>然后,通过这些分配好的空间把这些核心类型之间的引用关系串好。到此为止所有动作都由JVM完成,尚未执行任何Java字节码。然后这些核心类型就进入了[完全初始化]状态,对象系统就可以开始自我运行下去,也就是可以开始执行Java字节码来进一步完成Java系统的初始化了。</p></blockquote>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java基础11:Java泛型详解</title>
<link href="/2018/04/29/javase11/"/>
<url>/2018/04/29/javase11/</url>
<content type="html"><![CDATA[<p>本文对java的泛型的概念和使用做了详尽的介绍。</p><p>具体代码在我的GitHub中可以找到</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>喜欢的话麻烦点下星哈</p><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/04/29/javase11">https://h2pl.github.io/2018/04/29/javase11</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><blockquote><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p></blockquote><a id="more"></a><p>本文参考<a href="https://blog.csdn.net/s10461/article/details/53941091" target="_blank" rel="noopener">https://blog.csdn.net/s10461/article/details/53941091</a></p><h2 id="泛型概述"><a href="#泛型概述" class="headerlink" title="泛型概述"></a>泛型概述</h2><p>泛型在java中有很重要的地位,在面向对象编程及各种设计模式中有非常广泛的应用。</p><p>什么是泛型?为什么要使用泛型?</p><blockquote><p>泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。</p><p>泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。</p></blockquote><h2 id="一个栗子"><a href="#一个栗子" class="headerlink" title="一个栗子"></a>一个栗子</h2><p>一个被举了无数次的例子:</p><pre><code>List arrayList = new ArrayList();arrayList.add("aaaa");arrayList.add(100);for(int i = 0; i< arrayList.size();i++){ String item = (String)arrayList.get(i); Log.d("泛型测试","item = " + item);}</code></pre><p>毫无疑问,程序的运行结果会以崩溃结束:</p><p>java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String</p><p>ArrayList可以存放任意类型,例子中添加了一个String类型,添加了一个Integer类型,再使用时都以String的方式使用,因此程序崩溃了。为了解决类似这样的问题(在编译阶段就可以解决),泛型应运而生。</p><p>我们将第一行声明初始化list的代码更改一下,编译器会在编译阶段就能够帮我们发现类似这样的问题。</p><p>List<string> arrayList = new ArrayList<string>();<br>…<br>//arrayList.add(100); 在编译阶段,编译器就会报错</string></string></p><h2 id="特性"><a href="#特性" class="headerlink" title="特性"></a>特性</h2><p>泛型只在编译阶段有效。看下面的代码:</p><pre><code>List<String> stringArrayList = new ArrayList<String>();List<Integer> integerArrayList = new ArrayList<Integer>();Class classStringArrayList = stringArrayList.getClass();Class classIntegerArrayList = integerArrayList.getClass();if(classStringArrayList.equals(classIntegerArrayList)){ Log.d("泛型测试","类型相同");}</code></pre><blockquote><p>通过上面的例子可以证明,在编译之后程序会采取去泛型化的措施。也就是说Java中的泛型,只在编译阶段有效。在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。</p></blockquote><p>对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的基本类型。</p><p>泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法</p><h2 id="泛型类"><a href="#泛型类" class="headerlink" title="泛型类"></a>泛型类</h2><blockquote><p>泛型类型用于类的定义中,被称为泛型类。通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。</p><p>泛型类的最基本写法(这么看可能会有点晕,会在下面的例子中详解):</p></blockquote><pre><code>class 类名称 <泛型标识:可以随便写任意标识号,标识指定的泛型的类型>{ private 泛型标识 /*(成员变量类型)*/ var; ..... }</code></pre><p>一个最普通的泛型类:</p><p>//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型</p><pre><code>//在实例化泛型类时,必须指定T的具体类型public class Generic<T>{ //在类中声明的泛型整个类里面都可以用,除了静态部分,因为泛型是实例化时声明的。 //静态区域的代码在编译时就已经确定,只与类相关 class A <E>{ T t; } //类里面的方法或类中再次声明同名泛型是允许的,并且该泛型会覆盖掉父类的同名泛型T class B <T>{ T t; } //静态内部类也可以使用泛型,实例化时赋予泛型实际类型 static class C <T> { T t; } public static void main(String[] args) { //报错,不能使用T泛型,因为泛型T属于实例不属于类// T t = null; } //key这个成员变量的类型为T,T的类型由外部指定 private T key; public Generic(T key) { //泛型构造方法形参key的类型也为T,T的类型由外部指定 this.key = key; } public T getKey(){ //泛型方法getKey的返回值类型为T,T的类型由外部指定 return key; }}</code></pre><blockquote><p>12-27 09:20:04.432 13063-13063/? D/泛型测试: key is 123456</p></blockquote><blockquote><p>12-27 09:20:04.432 13063-13063/? D/泛型测试: key is key_vlaue</p></blockquote><blockquote><p>定义的泛型类,就一定要传入泛型类型实参么?并不是这样,在使用泛型的时候如果传入泛型实参,则会根据传入的泛型实参做相应的限制,此时泛型才会起到本应起到的限制作用。如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。</p></blockquote><p>看一个例子:</p><pre><code>Generic generic = new Generic("111111");Generic generic1 = new Generic(4444);Generic generic2 = new Generic(55.55);Generic generic3 = new Generic(false);Log.d("泛型测试","key is " + generic.getKey());Log.d("泛型测试","key is " + generic1.getKey());Log.d("泛型测试","key is " + generic2.getKey());Log.d("泛型测试","key is " + generic3.getKey());D/泛型测试: key is 111111D/泛型测试: key is 4444D/泛型测试: key is 55.55D/泛型测试: key is false</code></pre><p>注意:<br>泛型的类型参数只能是类类型,不能是简单类型。<br>不能对确切的泛型类型使用instanceof操作。如下面的操作是非法的,编译时会出错。<br> if(ex_num instanceof Generic<number>){<br> } </number></p><h2 id="泛型接口"><a href="#泛型接口" class="headerlink" title="泛型接口"></a>泛型接口</h2><p>泛型接口与泛型类的定义及使用基本相同。泛型接口常被用在各种类的生产器中,可以看一个例子:</p><pre><code>//定义一个泛型接口public interface Generator<T> { public T next();}</code></pre><p>当实现泛型接口的类,未传入泛型实参时:</p><pre><code>/** * 未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中 * 即:class FruitGenerator<T> implements Generator<T>{ * 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class" */class FruitGenerator<T> implements Generator<T>{ @Override public T next() { return null; }}</code></pre><p>当实现泛型接口的类,传入泛型实参时:</p><pre><code>/** * 传入泛型实参时: * 定义一个生产器实现这个接口,虽然我们只创建了一个泛型接口Generator<T> * 但是我们可以为T传入无数个实参,形成无数种类型的Generator接口。 * 在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型 * 即:Generator<T>,public T next();中的的T都要替换成传入的String类型。 */public class FruitGenerator implements Generator<String> { private String[] fruits = new String[]{"Apple", "Banana", "Pear"}; @Override public String next() { Random rand = new Random(); return fruits[rand.nextInt(3)]; }}</code></pre><h2 id="泛型通配符"><a href="#泛型通配符" class="headerlink" title="泛型通配符"></a>泛型通配符</h2><p>我们知道Ingeter是Number的一个子类,同时在特性章节中我们也验证过Generic<ingeter>与Generic<number>实际上是相同的一种基本类型。那么问题来了,在使用Generic<number>作为形参的方法中,能否使用Generic<ingeter>的实例传入呢?在逻辑上类似于Generic<number>和Generic<ingeter>是否可以看成具有父子关系的泛型类型呢?</ingeter></number></ingeter></number></number></ingeter></p><p>为了弄清楚这个问题,我们使用Generic<t>这个泛型类继续看下面的例子:</t></p><pre><code>public void showKeyValue1(Generic<Number> obj){ Log.d("泛型测试","key value is " + obj.getKey());}Generic<Integer> gInteger = new Generic<Integer>(123);Generic<Number> gNumber = new Generic<Number>(456);showKeyValue(gNumber);// showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer> // cannot be applied to Generic<java.lang.Number>// showKeyValue(gInteger);</code></pre><p>通过提示信息我们可以看到Generic<integer>不能被看作为`Generic<number>的子类。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。</number></integer></p><p>回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Generic<integer>类型的类,这显然与java中的多台理念相违背。因此我们需要一个在逻辑上可以表示同时是Generic<integer>和Generic<number>父类的引用类型。由此类型通配符应运而生。</number></integer></integer></p><p>我们可以将上面的方法改一下:</p><pre><code>public void showKeyValue1(Generic<?> obj){ Log.d("泛型测试","key value is " + obj.getKey());</code></pre><p>类型通配符一般是使用?代替具体的类型实参,注意, 此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。</p><p>可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型</p><p>public void showKeyValue(Generic<number> obj){<br> System.out.println(obj);<br> }</number></p><pre><code>Generic<Integer> gInteger = new Generic<Integer>(123);Generic<Number> gNumber = new Generic<Number>(456);public void test () {// showKeyValue(gInteger);该方法会报错 showKeyValue1(gInteger);}public void showKeyValue1(Generic<?> obj) { System.out.println(obj);}// showKeyValue这个方法编译器会为我们报错:Generic<java.lang.Integer>// cannot be applied to Generic<java.lang.Number>// showKeyValue(gInteger);</code></pre><p>。</p><h2 id="泛型方法"><a href="#泛型方法" class="headerlink" title="泛型方法"></a>泛型方法</h2><p>在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。</p><p>尤其是我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样在初学者中非常容易将泛型方法理解错了。<br>泛型类,是在实例化类的时候指明泛型的具体类型;泛型方法,是在调用方法的时候指明泛型的具体类型 。</p><pre><code>/** * 泛型方法的基本介绍 * @param tClass 传入的泛型实参 * @return T 返回值为T类型 * 说明: * 1)public 与 返回值中间<T>非常重要,可以理解为声明此方法为泛型方法。 * 2)只有声明了<T>的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。 * 3)<T>表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。 * 4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。 */ public <T> T genericMethod(Class<T> tClass)throws InstantiationException , IllegalAccessException{ T instance = tClass.newInstance(); return instance; }Object obj = genericMethod(Class.forName("com.test.test"));</code></pre><h2 id="泛型方法的基本用法"><a href="#泛型方法的基本用法" class="headerlink" title="泛型方法的基本用法"></a>泛型方法的基本用法</h2><p>光看上面的例子有的同学可能依然会非常迷糊,我们再通过一个例子,把我泛型方法再总结一下。</p><pre><code>/** * 这才是一个真正的泛型方法。 * 首先在public与返回值之间的<T>必不可少,这表明这是一个泛型方法,并且声明了一个泛型T * 这个T可以出现在这个泛型方法的任意位置. * 泛型的数量也可以为任意多个 * 如:public <T,K> K showKeyName(Generic<T> container){ * ... * } */ public class 泛型方法 { @Test public void test() { test1(); test2(new Integer(2)); test3(new int[3],new Object()); //打印结果// null// 2// [I@3d8c7aca// java.lang.Object@5ebec15 } //该方法使用泛型T public <T> void test1() { T t = null; System.out.println(t); } //该方法使用泛型T //并且参数和返回值都是T类型 public <T> T test2(T t) { System.out.println(t); return t; } //该方法使用泛型T,E //参数包括T,E public <T, E> void test3(T t, E e) { System.out.println(t); System.out.println(e); }}</code></pre><h2 id="类中的泛型方法"><a href="#类中的泛型方法" class="headerlink" title="类中的泛型方法"></a>类中的泛型方法</h2><p>当然这并不是泛型方法的全部,泛型方法可以出现杂任何地方和任何场景中使用。但是有一种情况是非常特殊的,当泛型方法出现在泛型类中时,我们再通过一个例子看一下</p><pre><code>//注意泛型类先写类名再写泛型,泛型方法先写泛型再写方法名//类中声明的泛型在成员和方法中可用class A <T, E>{ { T t1 ; } A (T t){ this.t = t; } T t; public void test1() { System.out.println(this.t); } public void test2(T t,E e) { System.out.println(t); System.out.println(e); }}@Testpublic void run () { A <Integer,String > a = new A<>(1); a.test1(); a.test2(2,"ds");// 1// 2// ds}static class B <T>{ T t; public void go () { System.out.println(t); }}</code></pre><h2 id="泛型方法与可变参数"><a href="#泛型方法与可变参数" class="headerlink" title="泛型方法与可变参数"></a>泛型方法与可变参数</h2><p>再看一个泛型方法和可变参数的例子:</p><pre><code>public class 泛型和可变参数 { @Test public void test () { printMsg("dasd",1,"dasd",2.0,false); print("dasdas","dasdas", "aa"); } //普通可变参数只能适配一种类型 public void print(String ... args) { for(String t : args){ System.out.println(t); } } //泛型的可变参数可以匹配所有类型的参数。。有点无敌 public <T> void printMsg( T... args){ for(T t : args){ System.out.println(t); } } //打印结果: //dasd //1 //dasd //2.0 //false}</code></pre><h2 id="静态方法与泛型"><a href="#静态方法与泛型" class="headerlink" title="静态方法与泛型"></a>静态方法与泛型</h2><p>静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。</p><p>即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。</p><pre><code>public class StaticGenerator<T> { .... .... /** * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法) * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。 * 如:public static void show(T t){..},此时编译器会提示错误信息: "StaticGenerator cannot be refrenced from static context" */ public static <T> void show(T t){ }}</code></pre><h2 id="泛型方法总结"><a href="#泛型方法总结" class="headerlink" title="泛型方法总结"></a>泛型方法总结</h2><p>泛型方法能使方法独立于类而产生变化,以下是一个基本的指导原则:</p><p>无论何时,如果你能做到,你就该尽量使用泛型方法。也就是说,如果使用泛型方法将整个类泛型化,那么就应该使用泛型方法。另外对于一个static的方法而已,无法访问泛型类型的参数。所以如果static方法要使用泛型能力,就必须使其成为泛型方法。</p><h2 id="泛型上下边界"><a href="#泛型上下边界" class="headerlink" title="泛型上下边界"></a>泛型上下边界</h2><p>在使用泛型的时候,我们还可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。</p><p>为泛型添加上边界,即传入的类型实参必须是指定类型的子类型</p><pre><code>public class 泛型通配符与边界 { public void showKeyValue(Generic<Number> obj){ System.out.println("key value is " + obj.getKey()); } @Test public void main() { Generic<Integer> gInteger = new Generic<Integer>(123); Generic<Number> gNumber = new Generic<Number>(456); showKeyValue(gNumber); //泛型中的子类也无法作为父类引用传入// showKeyValue(gInteger); } //直接使用?通配符可以接受任何类型作为泛型传入 public void showKeyValueYeah(Generic<?> obj) { System.out.println(obj); } //只能传入number的子类或者number public void showKeyValue1(Generic<? extends Number> obj){ System.out.println(obj); } //只能传入Integer的父类或者Integer public void showKeyValue2(Generic<? super Integer> obj){ System.out.println(obj); } @Test public void testup () { //这一行代码编译器会提示错误,因为String类型并不是Number类型的子类 //showKeyValue1(generic1); Generic<String> generic1 = new Generic<String>("11111"); Generic<Integer> generic2 = new Generic<Integer>(2222); Generic<Float> generic3 = new Generic<Float>(2.4f); Generic<Double> generic4 = new Generic<Double>(2.56); showKeyValue1(generic2); showKeyValue1(generic3); showKeyValue1(generic4); } @Test public void testdown () { Generic<String> generic1 = new Generic<String>("11111"); Generic<Integer> generic2 = new Generic<Integer>(2222); Generic<Number> generic3 = new Generic<Number>(2);// showKeyValue2(generic1);本行报错,因为String并不是Integer的父类 showKeyValue2(generic2); showKeyValue2(generic3); }}</code></pre><p>== 关于泛型数组要提一下 ==</p><p>看到了很多文章中都会提起泛型数组,经过查看sun的说明文档,在java中是”不能创建一个确切的泛型类型的数组”的。</p><pre><code>也就是说下面的这个例子是不可以的:List<String>[] ls = new ArrayList<String>[10]; 而使用通配符创建泛型数组是可以的,如下面这个例子:List<?>[] ls = new ArrayList<?>[10]; 这样也是可以的:List<String>[] ls = new ArrayList[10];</code></pre><p>下面使用Sun的一篇文档的一个例子来说明这个问题:</p><pre><code>List<String>[] lsa = new List<String>[10]; // Not really allowed. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Unsound, but passes run time store check String s = lsa[1].get(0); // Run-time error: ClassCastException.</code></pre><blockquote><p>这种情况下,由于JVM泛型的擦除机制,在运行时JVM是不知道泛型信息的,所以可以给oa[1]赋上一个ArrayList而不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现ClassCastException,如果可以进行泛型数组的声明,上面说的这种情况在编译期将不会出现任何的警告和错误,只有在运行时才会出错。</p><p>而对泛型数组的声明进行限制,对于这样的情况,可以在编译期提示代码有类型安全问题,比没有任何提示要强很多。<br>下面采用通配符的方式是被允许的:数组的类型不可以是类型变量,除非是采用通配符的方式,因为对于通配符的方式,最后取出数据是要做显式的类型转换的。</p></blockquote><pre><code>List<?>[] lsa = new List<?>[10]; // OK, array of unbounded wildcard type. Object o = lsa; Object[] oa = (Object[]) o; List<Integer> li = new ArrayList<Integer>(); li.add(new Integer(3)); oa[1] = li; // Correct. Integer i = (Integer) lsa[1].get(0); // OK </code></pre><p>最后</p><p>本文中的例子主要是为了阐述泛型中的一些思想而简单举出的,并不一定有着实际的可用性。另外,一提到泛型,相信大家用到最多的就是在集合中,其实,在实际的编程过程中,自己可以使用泛型去简化开发,且能很好的保证代码质量。</p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java基础10:全面解读Java异常</title>
<link href="/2018/04/27/javase10/"/>
<url>/2018/04/27/javase10/</url>
<content type="html"><![CDATA[<p>本文非常详尽地介绍了Java中的异常,几乎360度无死角。</p><p>从异常的概念,分类,使用方法,注意事项和设计等方面全面地介绍了Java异常。</p><p>具体代码在我的GitHub中可以找到</p><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p><p>喜欢的话麻烦点下星哈</p><p>文章首发于我的个人博客:</p><p><a href="https://h2pl.github.io/2018/04/27/javase10">https://h2pl.github.io/2018/04/27/javase10</a></p><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:<a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p><a id="more"></a><h2 id="为什么要使用异常"><a href="#为什么要使用异常" class="headerlink" title="为什么要使用异常"></a>为什么要使用异常</h2><blockquote><p> 首先我们可以明确一点就是异常的处理机制可以确保我们程序的健壮性,提高系统可用率。虽然我们不是特别喜欢看到它,但是我们不能不承认它的地位,作用。</p></blockquote><p>在没有异常机制的时候我们是这样处理的:通过函数的返回值来判断是否发生了异常(这个返回值通常是已经约定好了的),调用该函数的程序负责检查并且分析返回值。虽然可以解决异常问题,但是这样做存在几个缺陷:</p><blockquote><p> 1、 容易混淆。如果约定返回值为-11111时表示出现异常,那么当程序最后的计算结果真的为-1111呢?</p><p> 2、 代码可读性差。将异常处理代码和程序代码混淆在一起将会降低代码的可读性。</p><p> 3、 由调用函数来分析异常,这要求程序员对库函数有很深的了解。</p></blockquote><p> 在OO中提供的异常处理机制是提供代码健壮的强有力的方式。使用异常机制它能够降低错误处理代码的复杂度,如果不使用异常,那么就必须检查特定的错误,并在程序中的许多地方去处理它。</p><p>而如果使用异常,那就不必在方法调用处进行检查,因为异常机制将保证能够捕获这个错误,并且,只需在一个地方处理错误,即所谓的异常处理程序中。</p><p>这种方式不仅节约代码,而且把“概述在正常执行过程中做什么事”的代码和“出了问题怎么办”的代码相分离。总之,与以前的错误处理方法相比,异常机制使代码的阅读、编写和调试工作更加井井有条。(摘自《Think in java 》)。</p><p>该部分内容选自<a href="http://www.cnblogs.com/chenssy/p/3438130.html" target="_blank" rel="noopener">http://www.cnblogs.com/chenssy/p/3438130.html</a></p><h2 id="异常基本定义"><a href="#异常基本定义" class="headerlink" title="异常基本定义"></a>异常基本定义</h2><blockquote><p> 在《Think in java》中是这样定义异常的:异常情形是指阻止当前方法或者作用域继续执行的问题。在这里一定要明确一点:异常代码某种程度的错误,尽管Java有异常处理机制,但是我们不能以“正常”的眼光来看待异常,异常处理机制的原因就是告诉你:这里可能会或者已经产生了错误,您的程序出现了不正常的情况,可能会导致程序失败!</p></blockquote><blockquote><p> 那么什么时候才会出现异常呢?只有在你当前的环境下程序无法正常运行下去,也就是说程序已经无法来正确解决问题了,这时它所就会从当前环境中跳出,并抛出异常。抛出异常后,它首先会做几件事。</p></blockquote><blockquote><p>首先,它会使用new创建一个异常对象,然后在产生异常的位置终止程序,并且从当前环境中弹出对异常对象的引用,这时。异常处理机制就会接管程序,并开始寻找一个恰当的地方来继续执行程序,这个恰当的地方就是异常处理程序。</p></blockquote><blockquote><p> 总的来说异常处理机制就是当程序发生异常时,它强制终止程序运行,记录异常信息并将这些信息反馈给我们,由我们来确定是否处理异常。</p></blockquote><h2 id="异常体系"><a href="#异常体系" class="headerlink" title="异常体系"></a>异常体系</h2><p><img src="https://images0.cnblogs.com/blog/381060/201311/22185952-834d92bc2bfe498f9a33414cc7a2c8a4.png" alt="image"></p><p>从上面这幅图可以看出,Throwable是java语言中所有错误和异常的超类(万物即可抛)。它有两个子类:Error、Exception。</p><p>Java标准库内建了一些通用的异常,这些类以Throwable为顶层父类。</p><p>Throwable又派生出Error类和Exception类。</p><p>错误:Error类以及他的子类的实例,代表了JVM本身的错误。错误不能被程序员通过代码处理,Error很少出现。因此,程序员应该关注Exception为父类的分支下的各种异常类。</p><p>异常:Exception以及他的子类,代表程序运行时发送的各种不期望发生的事件。可以被Java异常处理机制使用,是异常处理的核心。</p><p>总体上我们根据Javac对异常的处理要求,将异常类分为2类。</p><blockquote><p>非检查异常(unckecked exception):Error 和 RuntimeException 以及他们的子类。javac在编译时,不会提示和发现这样的异常,不要求在程序处理这些异常。所以如果愿意,我们可以编写代码处理(使用try…catch…finally)这样的异常,也可以不处理。</p></blockquote><blockquote><p>对于这些异常,我们应该修正代码,而不是去通过异常处理器处理 。这样的异常发生的原因多半是代码写的有问题。如除0错误ArithmeticException,错误的强制类型转换错误ClassCastException,数组索引越界ArrayIndexOutOfBoundsException,使用了空对象NullPointerException等等。</p></blockquote><blockquote><p>检查异常(checked exception):除了Error 和 RuntimeException的其它异常。javac强制要求程序员为这样的异常做预备处理工作(使用try…catch…finally或者throws)。在方法中要么用try-catch语句捕获它并处理,要么用throws子句声明抛出它,否则编译不会通过。</p></blockquote><blockquote><p>这样的异常一般是由程序的运行环境导致的。因为程序可能被运行在各种未知的环境下,而程序员无法干预用户如何使用他编写的程序,于是程序员就应该为这样的异常时刻准备着。如SQLException , IOException,ClassNotFoundException 等。</p></blockquote><p>需要明确的是:检查和非检查是对于javac来说的,这样就很好理解和区分了。</p><p>这部分内容摘自<a href="http://www.importnew.com/26613.html" target="_blank" rel="noopener">http://www.importnew.com/26613.html</a></p><h2 id="初识异常"><a href="#初识异常" class="headerlink" title="初识异常"></a>初识异常</h2><p>异常是在执行某个函数时引发的,而函数又是层级调用,形成调用栈的,因为,只要一个函数发生了异常,那么他的所有的caller都会被异常影响。当这些被影响的函数以异常信息输出时,就形成的了异常追踪栈。</p><p>异常最先发生的地方,叫做异常抛出点。</p><pre><code>public class 异常 { public static void main (String [] args ) { System . out. println( "----欢迎使用命令行除法计算器----" ) ; CMDCalculate (); } public static void CMDCalculate () { Scanner scan = new Scanner ( System. in ); int num1 = scan .nextInt () ; int num2 = scan .nextInt () ; int result = devide (num1 , num2 ) ; System . out. println( "result:" + result) ; scan .close () ; } public static int devide (int num1, int num2 ){ return num1 / num2 ; }// ----欢迎使用命令行除法计算器----// 1// 0// Exception in thread "main" java.lang.ArithmeticException: / by zero// at com.javase.异常.异常.devide(异常.java:24)// at com.javase.异常.异常.CMDCalculate(异常.java:19)// at com.javase.异常.异常.main(异常.java:12)// ----欢迎使用命令行除法计算器----// r// Exception in thread "main" java.util.InputMismatchException// at java.util.Scanner.throwFor(Scanner.java:864)// at java.util.Scanner.next(Scanner.java:1485)// at java.util.Scanner.nextInt(Scanner.java:2117)// at java.util.Scanner.nextInt(Scanner.java:2076)// at com.javase.异常.异常.CMDCalculate(异常.java:17)// at com.javase.异常.异常.main(异常.java:12)</code></pre><p><img src="http://incdn1.b0.upaiyun.com/2017/09/0b3e4ca2f4cf8d7116c7ad354940601f.png" alt="image"></p><p>从上面的例子可以看出,当devide函数发生除0异常时,devide函数将抛出ArithmeticException异常,因此调用他的CMDCalculate函数也无法正常完成,因此也发送异常,而CMDCalculate的caller——main 因为CMDCalculate抛出异常,也发生了异常,这样一直向调用栈的栈底回溯。</p><p>这种行为叫做异常的冒泡,异常的冒泡是为了在当前发生异常的函数或者这个函数的caller中找到最近的异常处理程序。由于这个例子中没有使用任何异常处理机制,因此异常最终由main函数抛给JRE,导致程序终止。</p><blockquote><p>上面的代码不使用异常处理机制,也可以顺利编译,因为2个异常都是非检查异常。但是下面的例子就必须使用异常处理机制,因为异常是检查异常。</p></blockquote><p>代码中我选择使用throws声明异常,让函数的调用者去处理可能发生的异常。但是为什么只throws了IOException呢?因为FileNotFoundException是IOException的子类,在处理范围内。</p><h2 id="异常和错误"><a href="#异常和错误" class="headerlink" title="异常和错误"></a>异常和错误</h2><p>下面看一个例子</p><pre><code>//错误即error一般指jvm无法处理的错误//异常是Java定义的用于简化错误处理流程和定位错误的一种工具。public class 错误和错误 { Error error = new Error(); public static void main(String[] args) { throw new Error(); } //下面这四个异常或者错误有着不同的处理方法 public void error1 (){ //编译期要求必须处理,因为这个异常是最顶层异常,包括了检查异常,必须要处理 try { throw new Throwable(); } catch (Throwable throwable) { throwable.printStackTrace(); } } //Exception也必须处理。否则报错,因为检查异常都继承自exception,所以默认需要捕捉。 public void error2 (){ try { throw new Exception(); } catch (Exception e) { e.printStackTrace(); } } //error可以不处理,编译不报错,原因是虚拟机根本无法处理,所以啥都不用做 public void error3 (){ throw new Error(); } //runtimeexception众所周知编译不会报错 public void error4 (){ throw new RuntimeException(); }// Exception in thread "main" java.lang.Error// at com.javase.异常.错误.main(错误.java:11)}</code></pre><h2 id="异常的处理方式"><a href="#异常的处理方式" class="headerlink" title="异常的处理方式"></a>异常的处理方式</h2><p>在编写代码处理异常时,对于检查异常,有2种不同的处理方式:</p><blockquote><p>使用try…catch…finally语句块处理它。</p></blockquote><blockquote><p>或者,在函数签名中使用throws 声明交给函数调用者caller去解决。</p></blockquote><p>下面看几个具体的例子,包括error,exception和throwable</p><p>上面的例子是运行时异常,不需要显示捕获。<br>下面这个例子是可检查异常需,要显示捕获或者抛出。</p><pre><code>@Testpublic void testException() throws IOException{ //FileInputStream的构造函数会抛出FileNotFoundException FileInputStream fileIn = new FileInputStream("E:\\a.txt"); int word; //read方法会抛出IOException while((word = fileIn.read())!=-1) { System.out.print((char)word); } //close方法会抛出IOException fileIn.close();}</code></pre><p>一般情况下的处理方式 try catch finally</p><pre><code>public class 异常处理方式 {@Testpublic void main() { try{ //try块中放可能发生异常的代码。 InputStream inputStream = new FileInputStream("a.txt"); //如果执行完try且不发生异常,则接着去执行finally块和finally后面的代码(如果有的话)。 int i = 1/0; //如果发生异常,则尝试去匹配catch块。 throw new SQLException(); //使用1.8jdk同时捕获多个异常,runtimeexception也可以捕获。只是捕获后虚拟机也无法处理,所以不建议捕获。 }catch(SQLException | IOException | ArrayIndexOutOfBoundsException exception){ System.out.println(exception.getMessage()); //每一个catch块用于捕获并处理一个特定的异常,或者这异常类型的子类。Java7中可以将多个异常声明在一个catch中。 //catch后面的括号定义了异常类型和异常参数。如果异常与之匹配且是最先匹配到的,则虚拟机将使用这个catch块来处理异常。 //在catch块中可以使用这个块的异常参数来获取异常的相关信息。异常参数是这个catch块中的局部变量,其它块不能访问。 //如果当前try块中发生的异常在后续的所有catch中都没捕获到,则先去执行finally,然后到这个函数的外部caller中去匹配异常处理器。 //如果try中没有发生异常,则所有的catch块将被忽略。 }catch(Exception exception){ System.out.println(exception.getMessage()); //... }finally{ //finally块通常是可选的。 //无论异常是否发生,异常是否匹配被处理,finally都会执行。 //finally主要做一些清理工作,如流的关闭,数据库连接的关闭等。 }</code></pre><p>一个try至少要跟一个catch或者finally</p><pre><code> try { int i = 1; }finally { //一个try至少要有一个catch块,否则, 至少要有1个finally块。但是finally不是用来处理异常的,finally不会捕获异常。 }}</code></pre><p>异常出现时该方法后面的代码不会运行,即使异常已经被捕获。这里举出一个奇特的例子,在catch里再次使用try catch finally</p><pre><code>@Testpublic void test() { try { throwE(); System.out.println("我前面抛出异常了"); System.out.println("我不会执行了"); } catch (StringIndexOutOfBoundsException e) { System.out.println(e.getCause()); }catch (Exception ex) { //在catch块中仍然可以使用try catch finally try { throw new Exception(); }catch (Exception ee) { }finally { System.out.println("我所在的catch块没有执行,我也不会执行的"); } }}//在方法声明中抛出的异常必须由调用方法处理或者继续往上抛,// 当抛到jre时由于无法处理终止程序public void throwE (){// Socket socket = new Socket("127.0.0.1", 80); //手动抛出异常时,不会报错,但是调用该方法的方法需要处理这个异常,否则会出错。// java.lang.StringIndexOutOfBoundsException// at com.javase.异常.异常处理方式.throwE(异常处理方式.java:75)// at com.javase.异常.异常处理方式.test(异常处理方式.java:62) throw new StringIndexOutOfBoundsException(); }</code></pre><p>其实有的语言在遇到异常后仍然可以继续运行</p><blockquote><p>有的编程语言当异常被处理后,控制流会恢复到异常抛出点接着执行,这种策略叫做:resumption model of exception handling(恢复式异常处理模式 )</p><p>而Java则是让执行流恢复到处理了异常的catch块后接着执行,这种策略叫做:termination model of exception handling(终结式异常处理模式)</p></blockquote><h2 id="“不负责任”的throws"><a href="#“不负责任”的throws" class="headerlink" title="“不负责任”的throws"></a>“不负责任”的throws</h2><p>throws是另一种处理异常的方式,它不同于try…catch…finally,throws仅仅是将函数中可能出现的异常向调用者声明,而自己则不具体处理。</p><p>采取这种异常处理的原因可能是:方法本身不知道如何处理这样的异常,或者说让调用者处理更好,调用者需要为可能发生的异常负责。</p><pre><code>public void foo() throws ExceptionType1 , ExceptionType2 ,ExceptionTypeN{ //foo内部可以抛出 ExceptionType1 , ExceptionType2 ,ExceptionTypeN 类的异常,或者他们的子类的异常对象。}</code></pre><h2 id="纠结的finally"><a href="#纠结的finally" class="headerlink" title="纠结的finally"></a>纠结的finally</h2><p>finally块不管异常是否发生,只要对应的try执行了,则它一定也执行。只有一种方法让finally块不执行:System.exit()。因此finally块通常用来做资源释放操作:关闭文件,关闭数据库连接等等。</p><p>良好的编程习惯是:在try块中打开资源,在finally块中清理释放这些资源。</p><p>需要注意的地方:</p><p>1、finally块没有处理异常的能力。处理异常的只能是catch块。</p><p>2、在同一try…catch…finally块中 ,如果try中抛出异常,且有匹配的catch块,则先执行catch块,再执行finally块。如果没有catch块匹配,则先执行finally,然后去外面的调用者中寻找合适的catch块。</p><p>3、在同一try…catch…finally块中 ,try发生异常,且匹配的catch块中处理异常时也抛出异常,那么后面的finally也会执行:首先执行finally块,然后去外围调用者中寻找合适的catch块。</p><pre><code>public class finally使用 { public static void main(String[] args) { try { throw new IllegalAccessException(); }catch (IllegalAccessException e) { // throw new Throwable(); //此时如果再抛异常,finally无法执行,只能报错。 //finally无论何时都会执行 //除非我显示调用。此时finally才不会执行 System.exit(0); }finally { System.out.println("算你狠"); } }}</code></pre><h2 id="throw-JRE也使用的关键字"><a href="#throw-JRE也使用的关键字" class="headerlink" title="throw : JRE也使用的关键字"></a>throw : JRE也使用的关键字</h2><p>throw exceptionObject</p><p>程序员也可以通过throw语句手动显式的抛出一个异常。throw语句的后面必须是一个异常对象。</p><p>throw 语句必须写在函数中,执行throw 语句的地方就是一个异常抛出点,==它和由JRE自动形成的异常抛出点没有任何差别。==</p><pre><code>public void save(User user){ if(user == null) throw new IllegalArgumentException("User对象为空"); //......}</code></pre><p>后面开始的大部分内容都摘自<a href="http://www.cnblogs.com/lulipro/p/7504267.html" target="_blank" rel="noopener">http://www.cnblogs.com/lulipro/p/7504267.html</a></p><p>该文章写的十分细致到位,令人钦佩,是我目前为之看到关于异常最详尽的文章,可以说是站在巨人的肩膀上了。</p><h2 id="异常调用链"><a href="#异常调用链" class="headerlink" title="异常调用链"></a>异常调用链</h2><p>异常的链化</p><p>在一些大型的,模块化的软件开发中,一旦一个地方发生异常,则如骨牌效应一样,将导致一连串的异常。假设B模块完成自己的逻辑需要调用A模块的方法,如果A模块发生异常,则B也将不能完成而发生异常。</p><p>==但是B在抛出异常时,会将A的异常信息掩盖掉,这将使得异常的根源信息丢失。异常的链化可以将多个模块的异常串联起来,使得异常信息不会丢失。==</p><blockquote><p>异常链化:以一个异常对象为参数构造新的异常对象。新的异对象将包含先前异常的信息。这项技术主要是异常类的一个带Throwable参数的函数来实现的。这个当做参数的异常,我们叫他根源异常(cause)。</p></blockquote><p>查看Throwable类源码,可以发现里面有一个Throwable字段cause,就是它保存了构造时传递的根源异常参数。这种设计和链表的结点类设计如出一辙,因此形成链也是自然的了。</p><pre><code>public class Throwable implements Serializable { private Throwable cause = this; public Throwable(String message, Throwable cause) { fillInStackTrace(); detailMessage = message; this.cause = cause; } public Throwable(Throwable cause) { fillInStackTrace(); detailMessage = (cause==null ? null : cause.toString()); this.cause = cause; } //........}</code></pre><p>下面看一个比较实在的异常链例子哈</p><pre><code>public class 异常链 { @Test public void test() { C(); } public void A () throws Exception { try { int i = 1; i = i / 0; //当我注释掉这行代码并使用B方法抛出一个error时,运行结果如下// 四月 27, 2018 10:12:30 下午 org.junit.platform.launcher.core.ServiceLoaderTestEngineRegistry loadTestEngines// 信息: Discovered TestEngines with IDs: [junit-jupiter]// java.lang.Error: B也犯了个错误// at com.javase.异常.异常链.B(异常链.java:33)// at com.javase.异常.异常链.C(异常链.java:38)// at com.javase.异常.异常链.test(异常链.java:13)// at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)// Caused by: java.lang.Error// at com.javase.异常.异常链.B(异常链.java:29) }catch (ArithmeticException e) { //这里通过throwable类的构造方法将最底层的异常重新包装并抛出,此时注入了A方法的信息。最后打印栈信息时可以看到caused by A方法的异常。 //如果直接抛出,栈信息打印结果只能看到上层方法的错误信息,不能看到其实是A发生了错误。 //所以需要包装并抛出 throw new Exception("A方法计算错误", e); } } public void B () throws Exception,Error { try { //接收到A的异常, A(); throw new Error(); }catch (Exception e) { throw e; }catch (Error error) { throw new Error("B也犯了个错误", error); } } public void C () { try { B(); }catch (Exception | Error e) { e.printStackTrace(); } } //最后结果// java.lang.Exception: A方法计算错误// at com.javase.异常.异常链.A(异常链.java:18)// at com.javase.异常.异常链.B(异常链.java:24)// at com.javase.异常.异常链.C(异常链.java:31)// at com.javase.异常.异常链.test(异常链.java:11)// 省略// Caused by: java.lang.ArithmeticException: / by zero// at com.javase.异常.异常链.A(异常链.java:16)// ... 31 more}</code></pre><h2 id="自定义异常"><a href="#自定义异常" class="headerlink" title="自定义异常"></a>自定义异常</h2><p>如果要自定义异常类,则扩展Exception类即可,因此这样的自定义异常都属于检查异常(checked exception)。如果要自定义非检查异常,则扩展自RuntimeException。</p><p>按照国际惯例,自定义的异常应该总是包含如下的构造函数:</p><p>一个无参构造函数<br>一个带有String参数的构造函数,并传递给父类的构造函数。<br>一个带有String参数和Throwable参数,并都传递给父类构造函数<br>一个带有Throwable 参数的构造函数,并传递给父类的构造函数。<br>下面是IOException类的完整源代码,可以借鉴。</p><pre><code>public class IOException extends Exception{ static final long serialVersionUID = 7818375828146090155L; public IOException() { super(); } public IOException(String message) { super(message); } public IOException(String message, Throwable cause) { super(message, cause); } public IOException(Throwable cause) { super(cause); }}</code></pre><h2 id="异常的注意事项"><a href="#异常的注意事项" class="headerlink" title="异常的注意事项"></a>异常的注意事项</h2><p>异常的注意事项</p><blockquote><p>当子类重写父类的带有 throws声明的函数时,其throws声明的异常必须在父类异常的可控范围内——用于处理父类的throws方法的异常处理器,必须也适用于子类的这个带throws方法 。这是为了支持多态。</p><p>例如,父类方法throws 的是2个异常,子类就不能throws 3个及以上的异常。父类throws IOException,子类就必须throws IOException或者IOException的子类。</p></blockquote><p>至于为什么?我想,也许下面的例子可以说明。</p><pre><code>class Father{ public void start() throws IOException { throw new IOException(); }}class Son extends Father{ public void start() throws Exception { throw new SQLException(); }}</code></pre><p>/<strong><strong><strong><strong><strong>**</strong></strong></strong></strong></strong>假设上面的代码是允许的(实质是错误的)<strong><strong><strong><strong><strong>***</strong></strong></strong></strong></strong>/</p><pre><code>class Test{ public static void main(String[] args) { Father[] objs = new Father[2]; objs[0] = new Father(); objs[1] = new Son(); for(Father obj:objs) { //因为Son类抛出的实质是SQLException,而IOException无法处理它。 //那么这里的try。。catch就不能处理Son中的异常。 //多态就不能实现了。 try { obj.start(); }catch(IOException) { //处理IOException } } }}</code></pre><p>==Java的异常执行流程是线程独立的,线程之间没有影响==</p><blockquote><p>Java程序可以是多线程的。每一个线程都是一个独立的执行流,独立的函数调用栈。如果程序只有一个线程,那么没有被任何代码处理的异常 会导致程序终止。如果是多线程的,那么没有被任何代码处理的异常仅仅会导致异常所在的线程结束。</p><p>也就是说,Java中的异常是线程独立的,线程的问题应该由线程自己来解决,而不要委托到外部,也不会直接影响到其它线程的执行。</p></blockquote><p>下面看一个例子</p><pre><code>public class 多线程的异常 { @Test public void test() { go(); } public void go () { ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i = 0;i <= 2;i ++) { int finalI = i; try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } executorService.execute(new Runnable() { @Override //每个线程抛出异常时并不会影响其他线程的继续执行 public void run() { try { System.out.println("start thread" + finalI); throw new Exception(); }catch (Exception e) { System.out.println("thread" + finalI + " go wrong"); } } }); }// 结果:// start thread0// thread0 go wrong// start thread1// thread1 go wrong// start thread2// thread2 go wrong }}</code></pre><h2 id="当finally遇上return"><a href="#当finally遇上return" class="headerlink" title="当finally遇上return"></a>当finally遇上return</h2><p>首先一个不容易理解的事实:</p><p>在 try块中即便有return,break,continue等改变执行流的语句,finally也会执行。</p><pre><code>public static void main(String[] args){ int re = bar(); System.out.println(re);}private static int bar() { try{ return 5; } finally{ System.out.println("finally"); }}/*输出:finally*/</code></pre><p>很多人面对这个问题时,总是在归纳执行的顺序和规律,不过我觉得还是很难理解。我自己总结了一个方法。用如下GIF图说明。</p><p><img src="http://incdn1.b0.upaiyun.com/2017/09/0471c2805ebd5a463211ced478eaf7f8.gif" alt="image"></p><p>也就是说:try…catch…finally中的return 只要能执行,就都执行了,他们共同向同一个内存地址(假设地址是0×80)写入返回值,后执行的将覆盖先执行的数据,而真正被调用者取的返回值就是最后一次写入的。那么,按照这个思想,下面的这个例子也就不难理解了。</p><p>finally中的return 会覆盖 try 或者catch中的返回值。</p><pre><code>public static void main(String[] args) { int result; result = foo(); System.out.println(result); /////////2 result = bar(); System.out.println(result); /////////2 } @SuppressWarnings("finally") public static int foo() { trz{ int a = 5 / 0; } catch (Exception e){ return 1; } finally{ return 2; } } @SuppressWarnings("finally") public static int bar() { try { return 1; }finally { return 2; } }</code></pre><p>finally中的return会抑制(消灭)前面try或者catch块中的异常</p><pre><code>class TestException{ public static void main(String[] args) { int result; try{ result = foo(); System.out.println(result); //输出100 } catch (Exception e){ System.out.println(e.getMessage()); //没有捕获到异常 } try{ result = bar(); System.out.println(result); //输出100 } catch (Exception e){ System.out.println(e.getMessage()); //没有捕获到异常 } } //catch中的异常被抑制 @SuppressWarnings("finally") public static int foo() throws Exception { try { int a = 5/0; return 1; }catch(ArithmeticException amExp) { throw new Exception("我将被忽略,因为下面的finally中使用了return"); }finally { return 100; } } //try中的异常被抑制 @SuppressWarnings("finally") public static int bar() throws Exception { try { int a = 5/0; return 1; }finally { return 100; } }}</code></pre><p>finally中的异常会覆盖(消灭)前面try或者catch中的异常</p><pre><code>class TestException{ public static void main(String[] args) { int result; try{ result = foo(); } catch (Exception e){ System.out.println(e.getMessage()); //输出:我是finaly中的Exception } try{ result = bar(); } catch (Exception e){ System.out.println(e.getMessage()); //输出:我是finaly中的Exception } } //catch中的异常被抑制 @SuppressWarnings("finally") public static int foo() throws Exception { try { int a = 5/0; return 1; }catch(ArithmeticException amExp) { throw new Exception("我将被忽略,因为下面的finally中抛出了新的异常"); }finally { throw new Exception("我是finaly中的Exception"); } } //try中的异常被抑制 @SuppressWarnings("finally") public static int bar() throws Exception { try { int a = 5/0; return 1; }finally { throw new Exception("我是finaly中的Exception"); } }}</code></pre><p>上面的3个例子都异于常人的编码思维,因此我建议:</p><blockquote><p>不要在fianlly中使用return。</p></blockquote><blockquote><p>不要在finally中抛出异常。</p></blockquote><blockquote><p>减轻finally的任务,不要在finally中做一些其它的事情,finally块仅仅用来释放资源是最合适的。</p></blockquote><blockquote><p>将尽量将所有的return写在函数的最后面,而不是try … catch … finally中。</p></blockquote>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础系列 </tag>
</tags>
</entry>
<entry>
<title>Java基础9:解读Java回调机制</title>
<link href="/2018/04/26/javase9/"/>
<url>/2018/04/26/javase9/</url>
<content type="html"><![CDATA[<p>本文主要介绍了Java中的回调机制,以及Java多线程中类似回调的机制。</p><p>具体代码在我的GitHub中可以找到</p><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p><p>喜欢的话麻烦点下星哈</p><p>文章首发于我的个人博客:</p><p><a href="https://h2pl.github.io/2018/04/26/javase9">https://h2pl.github.io/2018/04/26/javase9</a></p><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:<a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p><a id="more"></a><h2 id="模块间的调用"><a href="#模块间的调用" class="headerlink" title="模块间的调用"></a>模块间的调用</h2><p>本部分摘自<a href="https://www.cnblogs.com/xrq730/p/6424471.html" target="_blank" rel="noopener">https://www.cnblogs.com/xrq730/p/6424471.html</a></p><p>在一个应用系统中,无论使用何种语言开发,必然存在模块之间的调用,调用的方式分为几种:</p><p>(1)同步调用</p><blockquote><p>同步调用是最基本并且最简单的一种调用方式,类A的方法a()调用类B的方法b(),一直等待b()方法执行完毕,a()方法继续往下走。这种调用方式适用于方法b()执行时间不长的情况,因为b()方法执行时间一长或者直接阻塞的话,a()方法的余下代码是无法执行下去的,这样会造成整个流程的阻塞。</p></blockquote><p><img src="https://images2015.cnblogs.com/blog/801753/201702/801753-20170221201001413-1766758208.png" alt="image"></p><p>(2)异步调用</p><p><img src="https://images2015.cnblogs.com/blog/801753/201702/801753-20170221201512429-1532730453.png" alt="image"></p><blockquote><p>异步调用是为了解决同步调用可能出现阻塞,导致整个流程卡住而产生的一种调用方式。类A的方法方法a()通过新起线程的方式调用类B的方法b(),代码接着直接往下执行,这样无论方法b()执行时间多久,都不会阻塞住方法a()的执行。</p><p>但是这种方式,由于方法a()不等待方法b()的执行完成,在方法a()需要方法b()执行结果的情况下(视具体业务而定,有些业务比如启异步线程发个微信通知、刷新一个缓存这种就没必要),必须通过一定的方式对方法b()的执行结果进行监听。</p><p>在Java中,可以使用Future+Callable的方式做到这一点,具体做法可以参见我的这篇文章Java多线程21:多线程下其他组件之CyclicBarrier、Callable、Future和FutureTask。</p></blockquote><p>(3)回调</p><p><img src="https://images2015.cnblogs.com/blog/801753/201702/801753-20170221205712070-824897248.png" alt="image"></p><p>最后是回调,回调的思想是: </p><p>类A的a()方法调用类B的b()方法<br>类B的b()方法执行完毕主动调用类A的callback()方法<br>这样一种调用方式组成了上图,也就是一种双向的调用方式。</p><h2 id="回调实例:Tom做题"><a href="#回调实例:Tom做题" class="headerlink" title="回调实例:Tom做题"></a>回调实例:Tom做题</h2><p>数学老师让Tom做一道题,并且Tom做题期间数学老师不用盯着Tom,而是在玩手机,等Tom把题目做完后再把答案告诉老师。</p><blockquote><p>1 数学老师需要Tom的一个引用,然后才能将题目发给Tom。</p><p>2 数学老师需要提供一个方法以便Tom做完题目以后能够将答案告诉他。</p><p>3 Tom需要数学老师的一个引用,以便Tom把答案给这位老师,而不是隔壁的体育老师。</p></blockquote><p>回调接口,可以理解为老师接口</p><pre><code>//回调指的是A调用B来做一件事,B做完以后将结果告诉给A,这期间A可以做别的事情。//这个接口中有一个方法,意为B做完题目后告诉A时使用的方法。//所以我们必须提供这个接口以便让B来回调。//回调接口,public interface CallBack { void tellAnswer(int res);}</code></pre><p>数学老师类</p><pre><code> //老师类实例化回调接口,即学生写完题目之后通过老师的提供的方法进行回调。 //那么学生如何调用到老师的方法呢,只要在学生类的方法中传入老师的引用即可。 //而老师需要指定学生答题,所以也要传入学生的实例。public class Teacher implements CallBack{ private Student student; Teacher(Student student) { this.student = student; } void askProblem (Student student, Teacher teacher) { //main方法是主线程运行,为了实现异步回调,这里开启一个线程来操作 new Thread(new Runnable() { @Override public void run() { student.resolveProblem(teacher); } }).start(); //老师让学生做题以后,等待学生回答的这段时间,可以做别的事,比如玩手机.\ //而不需要同步等待,这就是回调的好处。 //当然你可以说开启一个线程让学生做题就行了,但是这样无法让学生通知老师。 //需要另外的机制去实现通知过程。 // 当然,多线程中的future和callable也可以实现数据获取的功能。 for (int i = 1;i < 4;i ++) { System.out.println("等学生回答问题的时候老师玩了 " + i + "秒的手机"); } } @Override public void tellAnswer(int res) { System.out.println("the answer is " + res); }}</code></pre><p>学生接口</p><pre><code> //学生的接口,解决问题的方法中要传入老师的引用,否则无法完成对具体实例的回调。 //写为接口的好处就是,很多个学生都可以实现这个接口,并且老师在提问题时可以通过 //传入List<Student>来聚合学生,十分方便。public interface Student { void resolveProblem (Teacher teacher);}</code></pre><p>学生Tom</p><pre><code>public class Tom implements Student{ @Override public void resolveProblem(Teacher teacher) { try { //学生思考了3秒后得到了答案,通过老师提供的回调方法告诉老师。 Thread.sleep(3000); System.out.println("work out"); teacher.tellAnswer(111); } catch (InterruptedException e) { e.printStackTrace(); } }</code></pre><p>测试类</p><pre><code>public class Test { public static void main(String[] args) { //测试 Student tom = new Tom(); Teacher lee = new Teacher(tom); lee.askProblem(tom, lee); //结果// 等学生回答问题的时候老师玩了 1秒的手机// 等学生回答问题的时候老师玩了 2秒的手机// 等学生回答问题的时候老师玩了 3秒的手机// work out// the answer is 111 }}</code></pre><h2 id="多线程中的“回调”"><a href="#多线程中的“回调”" class="headerlink" title="多线程中的“回调”"></a>多线程中的“回调”</h2><p>Java多线程中可以通过callable和future或futuretask结合来获取线程执行后的返回值。实现方法是通过get方法来调用callable的call方法获取返回值。</p><p>其实这种方法本质上不是回调,回调要求的是任务完成以后被调用者主动回调调用者的接口。而这里是调用者主动使用get方法阻塞获取返回值。</p><pre><code>public class 多线程中的回调 { //这里简单地使用future和callable实现了线程执行完后 public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executor = Executors.newCachedThreadPool(); Future<String> future = executor.submit(new Callable<String>() { @Override public String call() throws Exception { System.out.println("call"); TimeUnit.SECONDS.sleep(1); return "str"; } }); //手动阻塞调用get通过call方法获得返回值。 System.out.println(future.get()); //需要手动关闭,不然线程池的线程会继续执行。 executor.shutdown(); //使用futuretask同时作为线程执行单元和数据请求单元。 FutureTask<Integer> futureTask = new FutureTask(new Callable<Integer>() { @Override public Integer call() throws Exception { System.out.println("dasds"); return new Random().nextInt(); } }); new Thread(futureTask).start(); //阻塞获取返回值 System.out.println(futureTask.get());}@Testpublic void test () { Callable callable = new Callable() { @Override public Object call() throws Exception { return null; } }; FutureTask futureTask = new FutureTask(callable);}}</code></pre>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础系列 </tag>
</tags>
</entry>
<entry>
<title>Java基础8:深入理解内部类</title>
<link href="/2018/04/25/javase8/"/>
<url>/2018/04/25/javase8/</url>
<content type="html"><![CDATA[<p>本文主要介绍了Java内部类的基本原理,使用方法和各种细节。</p><p>有关内部类实现回调,事件驱动和委托机制的文章将在后面发布。</p><p>具体代码在我的GitHub中可以找到</p><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p><p>喜欢的话麻烦点下星哈</p><p>文章首发于我的个人博客:</p><p><a href="https://h2pl.github.io/2018/04/25/javase8">https://h2pl.github.io/2018/04/25/javase8</a></p><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a><br><a id="more"></a></p><h2 id="内部类初探"><a href="#内部类初探" class="headerlink" title="内部类初探"></a>内部类初探</h2><p>一、什么是内部类?</p><p> 内部类是指在一个外部类的内部再定义一个类。内部类作为外部类的一个成员,并且依附于外部类而存在的。内部类可为静态,可用protected和private修饰(而外部类只能使用public和缺省的包访问权限)。内部类主要有以下几类:成员内部类、局部内部类、静态内部类、匿名内部类</p><p>二、内部类的共性</p><blockquote><p>(1)内部类仍然是一个独立的类,在编译之后内部类会被编译成独立的.class文件,但是前面冠以外部类的类名和$符号 。</p><p>(2)内部类不能用普通的方式访问。</p><p>(3)内部类声明成静态的,就不能随便的访问外部类的成员变量了,此时内部类只能访问外部类的静态成员变量 。</p><p>(4)外部类不能直接访问内部类的的成员,但可以通过内部类对象来访问</p></blockquote><p> 内部类是外部类的一个成员,因此内部类可以自由地访问外部类的成员变量,无论是否是private的。</p><p> 因为当某个外围类的对象创建内部类的对象时,此内部类会捕获一个隐式引用,它引用了实例化该内部对象的外围类对象。通过这个指针,可以访问外围类对象的全部状态。</p><p>通过反编译内部类的字节码,分析之后主要是通过以下几步做到的: </p><blockquote><p> 1 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象的引用; </p></blockquote><blockquote><p> 2 编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为1中添加的成员变量赋值;</p></blockquote><blockquote><p> 3 在调用内部类的构造函数初始化内部类对象时, 会默认传入外部类的引用。</p></blockquote><p>二、使用内部类的好处:</p><blockquote><p>静态内部类的作用:</p></blockquote><blockquote><p>1 只是为了降低包的深度,方便类的使用,静态内部类适用于包含类当中,但又不依赖与外在的类。</p><p>2 由于Java规定静态内部类不能用使用外在类的非静态属性和方法,所以只是为了方便管理类结构而定义。于是我们在创建静态内部类的时候,不需要外部类对象的引用。</p></blockquote><blockquote><p>非静态内部类的作用:</p></blockquote><blockquote><p>1 内部类继承自某个类或实现某个接口,内部类的代码操作创建其他外围类的对象。所以你可以认为内部类提供了某种进入其外围类的窗口。</p><p>2 使用内部类最吸引人的原因是:每个内部类都能独立地继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响</p></blockquote><blockquote><p>3 如果没有内部类提供的可以继承多个具体的或抽象的类的能力,一些设计与编程问题就很难解决。<br> 从这个角度看,内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效地实现了”多重继承”。</p></blockquote><p>三、<br>那静态内部类与普通内部类有什么区别呢?问得好,区别如下:</p><blockquote><p>(1)静态内部类不持有外部类的引用<br>在普通内部类中,我们可以直接访问外部类的属性、方法,即使是private类型也可以访问,这是因为内部类持有一个外部类的引用,可以自由访问。而静态内部类,则只可以访问外部类的静态方法和静态属性(如果是private权限也能访问,这是由其代码位置所决定的),其他则不能访问。</p><p>(2)静态内部类不依赖外部类<br>普通内部类与外部类之间是相互依赖的关系,内部类实例不能脱离外部类实例,也就是说它们会同生同死,一起声明,一起被垃圾回收器回收。而静态内部类是可以独立存在的,即使外部类消亡了,静态内部类还是可以存在的。</p><p>(3)普通内部类不能声明static的方法和变量<br>普通内部类不能声明static的方法和变量,注意这里说的是变量,常量(也就是final static修饰的属性)还是可以的,而静态内部类形似外部类,没有任何限制。</p></blockquote><p>==为什么普通内部类不能有静态变量呢?==</p><blockquote><p>1 成员内部类 之所以叫做成员 就是说他是类实例的一部分 而不是类的一部分 </p></blockquote><blockquote><p>2 结构上来说 他和你声明的成员变量是一样的地位 一个特殊的成员变量 而静态的变量是类的一部分和实例无关</p></blockquote><blockquote><p>3 你若声明一个成员内部类 让他成为主类的实例一部分 然后又想在内部类声明和实例无关的静态的东西 你让JVM情何以堪啊</p></blockquote><blockquote><p>4 若想在内部类内声明静态字段 就必须将其内部类本身声明为静态 </p></blockquote><p>非静态内部类有一个很大的优点:可以自由使用外部类的所有变量和方法</p><p>下面的例子大概地介绍了</p><p>1 非静态内部类和静态内部类的区别。</p><p>2 不同访问权限的内部类的使用。</p><p>3 外部类和它的内部类之间的关系</p><pre><code>//本节讨论内部类以及不同访问权限的控制//内部类只有在使用时才会被加载。//外部类Bpublic class B{ int i = 1; int j = 1; static int s = 1; static int ss = 1; A a; AA aa; AAA aaa; //内部类A public class A {// static void go () {//// }// static {//// }// static int b = 1;//非静态内部类不能有静态成员变量和静态代码块和静态方法, // 因为内部类在外部类加载时并不会被加载和初始化。 //所以不会进行静态代码的调用 int i = 2;//外部类无法读取内部类的成员,而内部类可以直接访问外部类成员 public void test() { System.out.println(j); j = 2; System.out.println(j); System.out.println(s);//可以访问类的静态成员变量 } public void test2() { AA aa = new AA(); AAA aaa = new AAA(); } } //静态内部类S,可以被外部访问 public static class S { int i = 1;//访问不到非静态变量。 static int s = 0;//可以有静态变量 public static void main(String[] args) { System.out.println(s); } @Test public void test () {// System.out.println(j);//报错,静态内部类不能读取外部类的非静态变量 System.out.println(s); System.out.println(ss); s = 2; ss = 2; System.out.println(s); System.out.println(ss); } } //内部类AA,其实这里加protected相当于default //因为外部类要调用内部类只能通过B。并且无法直接继承AA,所以必须在同包 //的类中才能调用到(这里不考虑静态内部类),那么就和default一样了。 protected class AA{ int i = 2;//内部类之间不共享变量 public void test (){ A a = new A(); AAA aaa = new AAA(); //内部类之间可以互相访问。 } } //包外部依然无法访问,因为包没有继承关系,所以找不到这个类 protected static class SS{ int i = 2;//内部类之间不共享变量 public void test (){ //内部类之间可以互相访问。 } } //私有内部类A,对外不可见,但对内部类和父类可见 private class AAA { int i = 2;//内部类之间不共享变量 public void test() { A a = new A(); AA aa = new AA(); //内部类之间可以互相访问。 } } @Test public void test(){ A a = new A(); a.test(); //内部类可以修改外部类的成员变量 //打印出 1 2 B b = new B(); }}//另一个外部类class C { @Test public void test() { //首先,其他类内部类只能通过外部类来获取其实例。 B.S s = new B.S(); //静态内部类可以直接通过B类直接获取,不需要B的实例,和静态成员变量类似。 //B.A a = new B.A(); //当A不是静态类时这行代码会报错。 //需要使用B的实例来获取A的实例 B b = new B(); B.A a = b.new A(); B.AA aa = b.new AA();//B和C同包,所以可以访问到AA// B.AAA aaa = b.new AAA();AAA为私有内部类,外部类不可见 //当A使用private修饰时,使用B的实例也无法获取A的实例,这一点和私有变量是一样的。 //所有普通的内部类与类中的一个变量是类似的。静态内部类则与静态成员类似。 }}</code></pre><h2 id="内部类的加载"><a href="#内部类的加载" class="headerlink" title="内部类的加载"></a>内部类的加载</h2><p>可能刚才的例子中没办法直观地看到内部类是如何加载的,接下来用例子展示一下内部类加载的过程。</p><blockquote><p>1 内部类是延时加载的,也就是说只会在第一次使用时加载。不使用就不加载,所以可以很好的实现单例模式。</p><p>2 不论是静态内部类还是非静态内部类都是在第一次使用时才会被加载。</p><p>3 对于非静态内部类是不能出现静态模块(包含静态块,静态属性,静态方法等)</p><p>4 非静态类的使用需要依赖于外部类的对象,详见上述对象innerClass 的初始化。</p></blockquote><p>简单来说,类的加载都是发生在类要被用到的时候。内部类也是一样</p><blockquote><p>1 普通内部类在第一次用到时加载,并且每次实例化时都会执行内部成员变量的初始化,以及代码块和构造方法。</p></blockquote><blockquote><p>2 静态内部类也是在第一次用到时被加载。但是当它加载完以后就会将静态成员变量初始化,运行静态代码块,并且只执行一次。当然,非静态成员和代码块每次实例化时也会执行。</p></blockquote><p>总结一下Java类代码加载的顺序,万变不离其宗。</p><blockquote><p>规律一、初始化构造时,先父后子;只有在父类所有都构造完后子类才被初始化</p><p>规律二、类加载先是静态、后非静态、最后是构造函数。</p><p>静态构造块、静态类属性按出现在类定义里面的先后顺序初始化,同理非静态的也是一样的,只是静态的只在加载字节码时执行一次,不管你new多少次,非静态会在new多少次就执行多少次</p><p>规律三、java中的类只有在被用到的时候才会被加载</p><p>规律四、java类只有在类字节码被加载后才可以被构造成对象实例</p></blockquote><h2 id="成员内部类"><a href="#成员内部类" class="headerlink" title="成员内部类"></a>成员内部类</h2><p>在方法中定义的内部类称为局部内部类。与局部变量类似,局部内部类不能有访问说明符,因为它不是外围类的一部分,但是它可以访问当前代码块内的常量,和此外围类所有的成员。</p><p>需要注意的是:<br>局部内部类只能在定义该内部类的方法内实例化,不可以在此方法外对其实例化。</p><pre><code>public class 局部内部类 { class A {//局部内部类就是写在方法里的类,只在方法执行时加载,一次性使用。 public void test() { class B { public void test () { class C { } } } } } @Test public void test () { int i = 1; final int j = 2; class A { @Test public void test () { System.out.println(i); System.out.println(j); } } A a = new A(); System.out.println(a); } static class B { public static void test () { //static class A报错,方法里不能定义静态内部类。 //因为只有在方法调用时才能进行类加载和初始化。 } }}</code></pre><h2 id="匿名内部类"><a href="#匿名内部类" class="headerlink" title="匿名内部类"></a>匿名内部类</h2><p>简单地说:匿名内部类就是没有名字的内部类,并且,匿名内部类是局部内部类的一种特殊形式。什么情况下需要使用匿名内部类?如果满足下面的一些条件,使用匿名内部类是比较合适的:<br>只用到类的一个实例。<br>类在定义后马上用到。<br>类非常小(SUN推荐是在4行代码以下)<br>给类命名并不会导致你的代码更容易被理解。<br>在使用匿名内部类时,要记住以下几个原则:</p><blockquote><p>1 匿名内部类不能有构造方法。</p><p>2 匿名内部类不能定义任何静态成员、方法和类。</p><p>3 匿名内部类不能是public,protected,private,static。</p><p>4 只能创建匿名内部类的一个实例。</p><p>5 一个匿名内部类一定是在new的后面,用其隐含实现一个接口或实现一个类。</p><p>6 因匿名内部类为局部内部类,所以局部内部类的所有限制都对其生效。</p></blockquote><p>一个匿名内部类的例子:</p><pre><code> public class 匿名内部类 {}interface D{ void run ();}abstract class E{ E (){ } abstract void work();}class A { @Test public void test (int k) { //利用接口写出一个实现该接口的类的实例。 //有且仅有一个实例,这个类无法重用。 new Runnable() { @Override public void run() {// k = 1;报错,当外部方法中的局部变量在内部类使用中必须改为final类型。 //因为方外部法中即使改变了这个变量也不会反映到内部类中。 //所以对于内部类来讲这只是一个常量。 System.out.println(100); System.out.println(k); } }; new D(){ //实现接口的匿名类 int i =1; @Override public void run() { System.out.println("run"); System.out.println(i); System.out.println(k); } }.run(); new E(){ //继承抽象类的匿名类 int i = 1; void run (int j) { j = 1; } @Override void work() { } }; }}</code></pre><h2 id="匿名内部类里的final"><a href="#匿名内部类里的final" class="headerlink" title="匿名内部类里的final"></a>匿名内部类里的final</h2><p>使用的形参为何要为final</p><p>参考文件:<a href="http://android.blog.51cto.com/268543/384844" target="_blank" rel="noopener">http://android.blog.51cto.com/268543/384844</a></p><blockquote><p>我们给匿名内部类传递参数的时候,若该形参在内部类中需要被使用,那么该形参必须要为final。也就是说:当所在的方法的形参需要被内部类里面使用时,该形参必须为final。</p><p>为什么必须要为final呢?</p><p>首先我们知道在内部类编译成功后,它会产生一个class文件,该class文件与外部类并不是同一class文件,仅仅只保留对外部类的引用。当外部类传入的参数需要被内部类调用时,从java程序的角度来看是直接被调用:</p></blockquote><pre><code>public class OuterClass { public void display(final String name,String age){ class InnerClass{ void display(){ System.out.println(name); } } }}</code></pre><p>从上面代码中看好像name参数应该是被内部类直接调用?其实不然,在java编译之后实际的操作如下:</p><pre><code>public class OuterClass$InnerClass { public InnerClass(String name,String age){ this.InnerClass$name = name; this.InnerClass$age = age; } public void display(){ System.out.println(this.InnerClass$name + "----" + this.InnerClass$age ); }}</code></pre><p> 所以从上面代码来看,内部类并不是直接调用方法传递的参数,而是利用自身的构造器对传入的参数进行备份,自己内部方法调用的实际上时自己的属性而不是外部方法传递进来的参数。</p><blockquote><p> 直到这里还没有解释为什么是final</p></blockquote><blockquote><p>在内部类中的属性和外部方法的参数两者从外表上看是同一个东西,但实际上却不是,所以他们两者是可以任意变化的,也就是说在内部类中我对属性的改变并不会影响到外部的形参,而然这从程序员的角度来看这是不可行的。</p><p>毕竟站在程序的角度来看这两个根本就是同一个,如果内部类该变了,而外部方法的形参却没有改变这是难以理解和不可接受的,所以为了保持参数的一致性,就规定使用final来避免形参的不改变。</p></blockquote><p> 简单理解就是,拷贝引用,为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。</p><p> 故如果定义了一个匿名内部类,并且希望它使用一个其外部定义的参数,那么编译器会要求该参数引用是final的。</p><h2 id="内部类初始化"><a href="#内部类初始化" class="headerlink" title="内部类初始化"></a>内部类初始化</h2><p>我们一般都是利用构造器来完成某个实例的初始化工作的,但是匿名内部类是没有构造器的!那怎么来初始化匿名内部类呢?使用构造代码块!利用构造代码块能够达到为匿名内部类创建一个构造器的效果。</p><pre><code>public class OutClass { public InnerClass getInnerClass(final int age,final String name){ return new InnerClass() { int age_ ; String name_; //构造代码块完成初始化工作 { if(0 < age && age < 200){ age_ = age; name_ = name; } } public String getName() { return name_; } public int getAge() { return age_; } }; }</code></pre><h2 id="内部类的重载"><a href="#内部类的重载" class="headerlink" title="内部类的重载"></a>内部类的重载</h2><p> 如果你创建了一个内部类,然后继承其外围类并重新定义此内部类时,会发生什么呢?也就是说,内部类可以被重载吗?这看起来似乎是个很有用的点子,但是“重载”内部类就好像它是外围类的一个方法,其实并不起什么作用:</p><pre><code>class Egg { private Yolk y; protected class Yolk { public Yolk() { System.out.println("Egg.Yolk()"); } } public Egg() { System.out.println("New Egg()"); y = new Yolk(); }}public class BigEgg extends Egg { public class Yolk { public Yolk() { System.out.println("BigEgg.Yolk()"); } } public static void main(String[] args) { new BigEgg(); }}复制代码输出结果为:New Egg()Egg.Yolk()</code></pre><p>缺省的构造器是编译器自动生成的,这里是调用基类的缺省构造器。你可能认为既然创建了BigEgg 的对象,那么所使用的应该是被“重载”过的Yolk,但你可以从输出中看到实际情况并不是这样的。<br>这个例子说明,当你继承了某个外围类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,各自在自己的命名空间内。</p><h2 id="内部类的继承"><a href="#内部类的继承" class="headerlink" title="内部类的继承"></a>内部类的继承</h2><p>因为内部类的构造器要用到其外围类对象的引用,所以在你继承一个内部类的时候,事情变得有点复杂。问题在于,那个“秘密的”外围类对象的引用必须被初始化,而在被继承的类中并不存在要联接的缺省对象。要解决这个问题,需使用专门的语法来明确说清它们之间的关联:</p><pre><code>class WithInner { class Inner { Inner(){ System.out.println("this is a constructor in WithInner.Inner"); }; }}public class InheritInner extends WithInner.Inner { // ! InheritInner() {} // Won't compile InheritInner(WithInner wi) { wi.super(); System.out.println("this is a constructor in InheritInner"); } public static void main(String[] args) { WithInner wi = new WithInner(); InheritInner ii = new InheritInner(wi); }}</code></pre><p>输出结果为:<br>this is a constructor in WithInner.Inner<br>this is a constructor in InheritInner</p><p>可以看到,InheritInner 只继承自内部类,而不是外围类。但是当要生成一个构造器时,缺省的构造器并不算好,而且你不能只是传递一个指向外围类对象的引用。此外,你必须在构造器内使用如下语法:<br>enclosingClassReference.super();<br>这样才提供了必要的引用,然后程序才能编译通过。</p><p>有关匿名内部类实现回调,事件驱动,委托等机制的文章将在下一节讲述。</p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础系列 </tag>
</tags>
</entry>
<entry>
<title>Java基础7:关于Java类和包的那些事</title>
<link href="/2018/04/24/javase7/"/>
<url>/2018/04/24/javase7/</url>
<content type="html"><![CDATA[<p>本文主要介绍了Java外部类和包的一些基本知识</p><p>内部类与匿名内部类的文章将在后面发布。</p><p>具体代码在我的GitHub中可以找到</p><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p><p>喜欢的话麻烦点下星哈</p><p>文章首发于我的个人博客:</p><p><a href="https://h2pl.github.io/2018/04/24/javase7">https://h2pl.github.io/2018/04/24/javase7</a></p><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a><br><a id="more"></a></p><h2 id="Java文件"><a href="#Java文件" class="headerlink" title="*.Java文件"></a>*.Java文件</h2><p>问题:一个”.java”源文件中是否可以包括多个类(不是内部类)?有什么限制?</p><blockquote><p> 答案:可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致。一个文件中可以只有非public类,如果只有一个非public类,此类可以跟文件名不同。</p></blockquote><p>为什么一个java源文件中只能有一个public类?</p><p> 在java编程思想(第四版)一书中有这样3段话(6.4 类的访问权限):</p><blockquote><p> 1.每个编译单元(文件)都只能有一个public类,这表示,每个编译单元都有单一的公共接口,用public类来表现。该接口可以按要求包含众多的支持包访问权限的类。如果在某个编译单元内有一个以上的public类,编译器就会给出错误信息。</p><p> 2.public类的名称必须完全与含有该编译单元的文件名相同,包含大小写。如果不匹配,同样将得到编译错误。</p><p> 3.虽然不是很常用,但编译单元内完全不带public类也是可能的。在这种情况下,可以随意对文件命名。</p></blockquote><p>总结相关的几个问题:</p><p>1、一个”.java”源文件中是否可以包括多个类(不是内部类)?有什么限制?</p><blockquote><p> 答:可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致。</p></blockquote><p>2、为什么一个文件中只能有一个public的类</p><blockquote><p> 答:编译器在编译时,针对一个java源代码文件(也称为“编译单元”)只会接受一个public类。否则报错。</p></blockquote><p>3、在java文件中是否可以没有public类</p><blockquote><p> 答:public类不是必须的,java文件中可以没有public类。</p></blockquote><p>4、为什么这个public的类的类名必须和文件名相同</p><blockquote><p> 答: 是为了方便虚拟机在相应的路径中找到相应的类所对应的字节码文件。</p></blockquote><h2 id="Main方法"><a href="#Main方法" class="headerlink" title="Main方法"></a>Main方法</h2><p>主函数:是一个特殊的函数,作为程序的入口,可以被JVM调用</p><p>主函数的定义:</p><blockquote><p>public:代表着该函数访问权限是最大的</p></blockquote><blockquote><p>static:代表主函数随着类的加载就已经存在了</p></blockquote><blockquote><p>void:主函数没有具体的返回值</p></blockquote><blockquote><p>main:不是关键字,但是一个特殊的单词,能够被JVM识别</p></blockquote><blockquote><p>(String[] args):函数的参数,参数类型是一个数组,该数组中的元素师字符串,字符串数组。main(String[] args) 字符串数组的 此时空数组的长度是0,但也可以在 运行的时候向其中传入参数。</p></blockquote><p>主函数时固定格式的,JVM识别</p><p>主函数可以被重载,但是JVM只识别main(String[] args),其他都是作为一般函数。这里面的args知识数组变量可以更改,其他都不能更改。</p><p>一个java文件中可以包含很多个类,每个类中有且仅有一个主函数,但是每个java文件中可以包含多个主函数,在运行时,需要指定JVM入口是哪个。例如一个类的主函数可以调用另一个类的主函数。不一定会使用public类的主函数。</p><h2 id="外部类的访问权限"><a href="#外部类的访问权限" class="headerlink" title="外部类的访问权限"></a>外部类的访问权限</h2><p>外部类只能用public和default修饰。</p><p>为什么要对外部类或类做修饰呢?</p><blockquote><p>1.存在包概念:public 和 default 能区分这个外部类能对不同包作一个划分 (default修饰的类,其他包中引入不了这个类,public修饰的类才能被import) </p><p>2.protected是包内可见并且子类可见,但是当一个外部类想要继承一个protected修饰的非同包类时,压根找不到这个类,更别提几层了</p><p>3.private修饰的外部类,其他任何外部类都无法导入它。</p></blockquote><pre><code>//Java中的文件名要和public修饰的类名相同,否则会报错//如果没有public修饰的类,则文件可以随意命名public class Java中的类文件 {}//非公共开类的访问权限默认是包访问权限,不能用private和protected//一个外部类的访问权限只有两种,一种是包内可见,一种是包外可见。//如果用private修饰,其他类根本无法看到这个类,也就没有意义了。//如果用protected,虽然也是包内可见,但是如果有子类想要继承该类但是不同包时,//压根找不到这个类,也不可能继承它了,所以干脆用default代替。class A{}</code></pre><h2 id="Java包的命名规则"><a href="#Java包的命名规则" class="headerlink" title="Java包的命名规则"></a>Java包的命名规则</h2><blockquote><p>以 java.* 开头的是Java的核心包,所有程序都会使用这些包中的类;</p></blockquote><blockquote><p>以 javax.<em> 开头的是扩展包,x 是 extension 的意思,也就是扩展。虽然 javax.</em> 是对 java.<em> 的优化和扩展,但是由于 javax.</em> 使用的越来越多,很多程序都依赖于 javax.<em>,所以 javax.</em> 也是核心的一部分了,也随JDK一起发布。</p></blockquote><blockquote><p>以 org.* 开头的是各个机构或组织发布的包,因为这些组织很有影响力,它们的代码质量很高,所以也将它们开发的部分常用的类随JDK一起发布。</p></blockquote><blockquote><p>在包的命名方面,为了防止重名,有一个惯例:大家都以自己域名的倒写形式作为开头来为自己开发的包命名,例如百度发布的包会以 com.baidu.<em> 开头,w3c组织发布的包会以 org.w3c.</em> 开头,微学苑发布的包会以 net.weixueyuan.* 开头……</p></blockquote><blockquote><p>组织机构的域名后缀一般为 org,公司的域名后缀一般为 com,可以认为 org.<em> 开头的包为非盈利组织机构发布的包,它们一般是开源的,可以免费使用在自己的产品中,不用考虑侵权问题,而以 com.</em> 开头的包往往由盈利性的公司发布,可能会有版权问题,使用时要注意。</p></blockquote><h2 id="import的使用"><a href="#import的使用" class="headerlink" title="import的使用"></a>import的使用</h2><p>Java import以及Java类的搜索路径<br>如果你希望使用Java包中的类,就必须先使用import语句导入<br>语法为:</p><pre><code>import package1[.package2…].classname;package 为包名,classname 为类名。例如:import java.util.Date; // 导入 java.util 包下的 Date 类import java.util.Scanner; // 导入 java.util 包下的 Scanner 类import javax.swing.*; // 导入 javax.swing 包下的所有类,* 表示所有类</code></pre><p>注意:</p><blockquote><p>import 只能导入包所包含的类,而不能导入包。<br>为方便起见,我们一般不导入单独的类,而是导入包下所有的类,例如 import java.util.*;。</p></blockquote><blockquote><p>Java 编译器默认为所有的 Java 程序导入了 JDK 的 java.lang 包中所有的类(import java.lang.*;),其中定义了一些常用类,如 System、String、Object、Math 等,因此我们可以直接使用这些类而不必显式导入。但是使用其他类必须先导入。</p></blockquote><blockquote><p>前面讲到的”Hello World“程序使用了System.out.println(); 语句,System 类位于 java.lang 包,虽然我们没有显式导入这个包中的类,但是Java 编译器默认已经为我们导入了,否则程序会执行失败。</p></blockquote><p>java类的搜索路径<br>Java程序运行时要导入相应的类,也就是加载 .class 文件的过程。<br>假设有如下的 import 语句:</p><p>import p1.Test;</p><blockquote><p>该语句表明要导入 p1 包中的 Test 类。<br>安装JDK时,我们已经设置了环境变量 CLASSPATH 来指明类库的路径,它的值为 .;%JAVA_HOME%\lib,而 JAVA_HOME 又为 D:\Program Files\jdk1.7.0_71,所以 CLASSPATH 等价于 .;D:\Program Files\jdk1.7.0_71\lib。</p></blockquote><blockquote><p>如果在第一个路径下找到了所需的类文件,则停止搜索,否则继续搜索后面的路径,如果在所有的路径下都未能找到所需的类文件,则编译或运行出错。</p><p>你可以在CLASSPATH变量中增加搜索路径,例如 .;%JAVA_HOME%\lib;C:\javalib,那么你就可以将类文件放在 C:\javalib 目录下,Java运行环境一样会找到。</p></blockquote><blockquote><p>用户自己写的类可以通过IDE指定编译后的class文件的输出目录,appclassloader会到指定目录进行类的加载</p></blockquote><p>下面是一个import两种访问权限的类的实例:</p><pre><code>package com.javase.Java中的类.一个包;public class 全局访问 {}package com.javase.Java中的类.一个包;class 包访问权限 {}package com.javase.Java中的类;//import可以导入基础包以及公开的类,需要使用类名的全路径//并且在导入某个包.*时,是不会把子包的类给导进来的,这样可以避免导入错误。//注意//import com.javase.Java中的类.一个包.包访问权限;//这个导入会报错,因为这个类没有用public修饰,无法用import导入。import com.javase.Java中的类.一个包.全局访问;//可以导入。</code></pre>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础系列 </tag>
</tags>
</entry>
<entry>
<title>Java基础6:代码块与代码加载顺序</title>
<link href="/2018/04/24/javase6/"/>
<url>/2018/04/24/javase6/</url>
<content type="html"><![CDATA[<p>本文主要介绍了三种代码块的特性和使用方法。</p><p>具体代码在我的GitHub中可以找到</p><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p><p>喜欢的话麻烦点下星哈</p><p>文章首发于我的个人博客:</p><p><a href="https://h2pl.github.io/2018/04/24/javase6">https://h2pl.github.io/2018/04/24/javase6</a></p><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a><br><a id="more"></a></p><p>代码块:用{}包围的代码</p><p>java中的代码块按其位置划分为四种:</p><h2 id="局部代码块"><a href="#局部代码块" class="headerlink" title="局部代码块"></a>局部代码块</h2><blockquote><p>位置:局部位置(方法内部)</p></blockquote><blockquote><p>作用:限定变量的生命周期,尽早释放,节约内存</p></blockquote><blockquote><p>调用:调用其所在的方法时执行</p></blockquote><pre><code> public class 局部代码块 {@Testpublic void test (){ B b = new B(); b.go();}}class B { B(){} public void go() { //方法中的局部代码块,一般进行一次性地调用,调用完立刻释放空间,避免在接下来的调用过程中占用栈空间 //因为栈空间内存是有限的,方法调用可能会会生成很多局部变量导致栈内存不足。 //使用局部代码块可以避免这样的情况发生。 { int i = 1; ArrayList<Integer> list = new ArrayList<>(); while (i < 1000) { list.add(i ++); } for (Integer j : list) { System.out.println(j); } System.out.println("gogogo"); } System.out.println("hello"); }}</code></pre><h2 id="构造代码块"><a href="#构造代码块" class="headerlink" title="构造代码块"></a>构造代码块</h2><blockquote><p>位置:类成员的位置,就是类中方法之外的位置</p></blockquote><blockquote><p>作用:把多个构造方法共同的部分提取出来,共用构造代码块</p></blockquote><blockquote><p>调用:每次调用构造方法时,都会优先于构造方法执行,也就是每次new一个对象时自动调用,对 对象的初始化</p></blockquote><pre><code>class A{ int i = 1; int initValue;//成员变量的初始化交给代码块来完成 { //代码块的作用体现于此:在调用构造方法之前,用某段代码对成员变量进行初始化。 //而不是在构造方法调用时再进行。一般用于将构造方法的相同部分提取出来。 // for (int i = 0;i < 100;i ++) { initValue += i; } } { System.out.println(initValue); System.out.println(i);//此时会打印1 int i = 2;//代码块里的变量和成员变量不冲突,但会优先使用代码块的变量 System.out.println(i);//此时打印2 //System.out.println(j);//提示非法向后引用,因为此时j的的初始化还没开始。 // } { System.out.println("代码块运行"); } int j = 2; { System.out.println(j); System.out.println(i);//代码块中的变量运行后自动释放,不会影响代码块之外的代码 } A(){ System.out.println("构造方法运行"); }}public class 构造代码块 { @Test public void test() { A a = new A(); }}</code></pre><h2 id="静态代码块"><a href="#静态代码块" class="headerlink" title="静态代码块"></a>静态代码块</h2><pre><code> 位置:类成员位置,用static修饰的代码块 作用:对类进行一些初始化 只加载一次,当new多个对象时,只有第一次会调用静态代码块,因为,静态代码块 是属于类的,所有对象共享一份 调用: new 一个对象时自动调用 public class 静态代码块 {@Testpublic void test() { C c1 = new C(); C c2 = new C(); //结果,静态代码块只会调用一次,类的所有对象共享该代码块 //一般用于类的全局信息初始化 //静态代码块调用 //代码块调用 //构造方法调用 //代码块调用 //构造方法调用}}class C{ C(){ System.out.println("构造方法调用"); } { System.out.println("代码块调用"); } static { System.out.println("静态代码块调用"); }}</code></pre><p>执行顺序 静态代码块 —–> 构造代码块 ——-> 构造方法</p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java基础5:抽象类和接口</title>
<link href="/2018/04/24/javase5/"/>
<url>/2018/04/24/javase5/</url>
<content type="html"><![CDATA[<p>本文主要介绍了抽象类和接口的使用方法和区别</p><p>具体代码在我的GitHub中可以找到</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>喜欢的话麻烦点下星哈</p><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/04/24/javase5">https://h2pl.github.io/2018/04/24/javase5</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a><br><a id="more"></a></p><blockquote><p>1 抽象类一般会实现一部分操作,并且留一些抽象方法让子类自己实现,比如Stringbuffer和Stringbuilder的父类abstractStringbuilder。</p></blockquote><blockquote><p>2 接口一般指一种规定,比如一个map接口中,可能需要实现增删改查等功能,如果你想实现一个具体map,这些方法就必须按照规定去实现。</p></blockquote><blockquote><p>3 另外,一个类可以实现多个接口,但是不能继承多个类。<br>然而接口却可以继承多个其他接口。这一点很神奇。</p></blockquote><p>下面看一下具体的例子,有一些小细节平时可能不会注意。</p><pre><code>class A {}interface M extends N,L{}interface N{}interface L{}interface 接口 { public final int i = 1;//变量默认都为public final修饰 final A a = null;//基本数据类型和引用都一样 //protected void a();//报错 //private //报错 public abstract void a();// 方法都是public abstract修饰的。 //void b(){} 报错,接口里的方法不能有方法体,也不能有{},只能有(); // final void b(); // 注意,抽象方法不能加final。因为final方法不能被重写。 //但如果抽象方法不被重写那就没有意义了,因为他根本没有代码体。}abstract class 抽象类 { public final int i = 1;//变量并没有被pulic和final修饰,只是一般的成员变量 public final A a = null; private void A(){}//抽象类可以有具体方法 abstract void AA();//抽象方法没有方法体 //private abstract void B();//报错,组合非法 // 因为private修饰的方法无法被子类重写,所以和final一样,使抽象方法无法被实现。}//抽象类也可以被实例化,举例说明abstract class B{ B() { System.out.println("b init"); }}class C extends B{ C(){ super(); System.out.println("c init"); }}public class 接口对比抽象类 { @Test public void test() { C c = new C(); //结果先实例化B,再实例化C。 //因为会调用到父类的构造方法。 }}</code></pre>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>java基础4:深入理解final关键字</title>
<link href="/2018/04/23/javase4/"/>
<url>/2018/04/23/javase4/</url>
<content type="html"><![CDATA[<p>本文主要介绍了final关键字的使用方法及原理</p><p>具体代码在我的GitHub中可以找到</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>喜欢的话麻烦点下星哈</p><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/04/23/javase4">https://h2pl.github.io/2018/04/23/javase4</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a><br><a id="more"></a></p><blockquote><p>final关键字可以修饰类、方法和引用。</p><p>修饰类,该类不能被继承。并且这个类的对象在堆中分配内存后地址不可变。</p><p>修饰方法,方法不能被子类重写。</p><p>修饰引用,引用无法改变,对于基本类型,无法修改值,对于引用,虽然不能修改地址值,但是可以对指向对象的内部进行修改。</p></blockquote><p>比如char[0] = ‘a’。不改变对象内存地址,只改变了值。</p><p>具体看一下下面的栗子:</p><p>final class Fi {<br> int a;<br> final int b = 0;<br> Integer s;</p><pre><code>}class Si{ //一般情况下final修饰的变量一定要被初始化。 //只有下面这种情况例外,要求该变量必须在构造方法中被初始化。 //并且不能有空参数的构造方法。 //这样就可以让每个实例都有一个不同的变量,并且这个变量在每个实例中只会被初始化一次 //于是这个变量在单个实例里就是常量了。 final int s ; Si(int s) { this.s = s; }}class Bi { final int a = 1; final void go() { //final修饰方法无法被继承 }}class Ci extends Bi { final int a = 1;// void go() {// //final修饰方法无法被继承// }}final char[]a = {'a'};final int[]b = {1};</code></pre><h2 id="final修饰类"><a href="#final修饰类" class="headerlink" title="final修饰类"></a>final修饰类</h2><pre><code>@Testpublic void final修饰类() { //引用没有被final修饰,所以是可变的。 //final只修饰了Fi类型,即Fi实例化的对象在堆中内存地址是不可变的。 //虽然内存地址不可变,但是可以对内部的数据做改变。 Fi f = new Fi(); f.a = 1; System.out.println(f); f.a = 2; System.out.println(f); //改变实例中的值并不改变内存地址。 Fi ff = f; //让引用指向新的Fi对象,原来的f对象由新的引用ff持有。 //引用的指向改变也不会改变原来对象的地址 f = new Fi(); System.out.println(f); System.out.println(ff);}</code></pre><h2 id="final修饰方法"><a href="#final修饰方法" class="headerlink" title="final修饰方法"></a>final修饰方法</h2><pre><code>@Testpublic void final修饰方法() { Bi bi = new Bi(); bi.go();//该方法无法被子类Ci重写}</code></pre><h2 id="final修饰基本数据类型变量和引用"><a href="#final修饰基本数据类型变量和引用" class="headerlink" title="final修饰基本数据类型变量和引用"></a>final修饰基本数据类型变量和引用</h2><pre><code>@Testpublic void final修饰基本类型变量和引用() { final int a = 1; final int[] b = {1}; final int[] c = {1};// b = c;报错 b[0] = 1; final String aa = "a"; final Fi f = new Fi(); //aa = "b";报错 // f = null;//报错 f.a = 1;}</code></pre><p>关于字符串的内容可以在上一节查看:</p><p><a href="https://blog.csdn.net/a724888/article/details/80042298" target="_blank" rel="noopener">https://blog.csdn.net/a724888/article/details/80042298</a></p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java基础3:深入理解String及包装类</title>
<link href="/2018/04/23/javase3/"/>
<url>/2018/04/23/javase3/</url>
<content type="html"><![CDATA[<p>本节主要介绍字符串类型和相关包装类的使用和原理。</p><p>具体代码在我的GitHub中可以找到</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>喜欢的话麻烦点下星哈</p><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/04/23/javase3">https://h2pl.github.io/2018/04/23/javase3</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a><br><a id="more"></a></p><h2 id="String的连接"><a href="#String的连接" class="headerlink" title="String的连接"></a>String的连接</h2><pre><code>@Testpublic void contact () { //1连接方式 String s1 = "a"; String s2 = "a"; String s3 = "a" + s2; String s4 = "a" + "a"; String s5 = s1 + s2; //表达式只有常量时,编译期完成计算 //表达式有变量时,运行期才计算,所以地址不一样 System.out.println(s3 == s4); //f System.out.println(s3 == s5); //f System.out.println(s4 == "aa"); //t}</code></pre><h2 id="String类型的intern"><a href="#String类型的intern" class="headerlink" title="String类型的intern"></a>String类型的intern</h2><pre><code>public void intern () { //2:string的intern使用 //s1是基本类型,比较值。s2是string实例,比较实例地址 //字符串类型用equals方法比较时只会比较值 String s1 = "a"; String s2 = new String("a"); //调用intern时,如果s2中的字符不在常量池,则加入常量池并返回常量的引用 String s3 = s2.intern(); System.out.println(s1 == s2); System.out.println(s1 == s3);}</code></pre><h2 id="String类型的equals"><a href="#String类型的equals" class="headerlink" title="String类型的equals"></a>String类型的equals</h2><pre><code>//字符串的equals方法// public boolean equals(Object anObject) {// if (this == anObject) {// return true;// }// if (anObject instanceof String) {// String anotherString = (String)anObject;// int n = value.length;// if (n == anotherString.value.length) {// char v1[] = value;// char v2[] = anotherString.value;// int i = 0;// while (n-- != 0) {// if (v1[i] != v2[i])// return false;// i++;// }// return true;// }// }// return false;// }</code></pre><h2 id="StringBuffer和Stringbuilder"><a href="#StringBuffer和Stringbuilder" class="headerlink" title="StringBuffer和Stringbuilder"></a>StringBuffer和Stringbuilder</h2><p>底层是继承父类的可变字符数组value</p><pre><code>/** * The value is used for character storage. */char[] value;初始化容量为16/** * Constructs a string builder with no characters in it and an * initial capacity of 16 characters. */public StringBuilder() { super(16);}这两个类的append方法都是来自父类AbstractStringBuilder的方法public AbstractStringBuilder append(String str) { if (str == null) return appendNull(); int len = str.length(); ensureCapacityInternal(count + len); str.getChars(0, len, value, count); count += len; return this;}@Overridepublic StringBuilder append(String str) { super.append(str); return this;}@Overridepublic synchronized StringBuffer append(String str) { toStringCache = null; super.append(str); return this;}</code></pre><h3 id="append"><a href="#append" class="headerlink" title="append"></a>append</h3><pre><code>Stringbuffer在大部分涉及字符串修改的操作上加了synchronized关键字来保证线程安全,效率较低。String类型在使用 + 运算符例如String a = "a"a = a + a;时,实际上先把a封装成stringbuilder,调用append方法后再用tostring返回,所以当大量使用字符串加法时,会大量地生成stringbuilder实例,这是十分浪费的,这种时候应该用stringbuilder来代替string。</code></pre><h3 id="扩容"><a href="#扩容" class="headerlink" title="扩容"></a>扩容</h3><pre><code>#注意在append方法中调用到了一个函数ensureCapacityInternal(count + len);该方法是计算append之后的空间是否足够,不足的话需要进行扩容public void ensureCapacity(int minimumCapacity) { if (minimumCapacity > 0) ensureCapacityInternal(minimumCapacity);}private void ensureCapacityInternal(int minimumCapacity) { // overflow-conscious code if (minimumCapacity - value.length > 0) { value = Arrays.copyOf(value, newCapacity(minimumCapacity)); }}如果新字符串长度大于value数组长度则进行扩容扩容后的长度一般为原来的两倍 + 2;假如扩容后的长度超过了jvm支持的最大数组长度MAX_ARRAY_SIZE。考虑两种情况如果新的字符串长度超过int最大值,则抛出异常,否则直接使用数组最大长度作为新数组的长度。private int hugeCapacity(int minCapacity) { if (Integer.MAX_VALUE - minCapacity < 0) { // overflow throw new OutOfMemoryError(); } return (minCapacity > MAX_ARRAY_SIZE) ? minCapacity : MAX_ARRAY_SIZE;}</code></pre><h3 id="删除"><a href="#删除" class="headerlink" title="删除"></a>删除</h3><pre><code>这两个类型的删除操作:都是调用父类的delete方法进行删除public AbstractStringBuilder delete(int start, int end) { if (start < 0) throw new StringIndexOutOfBoundsException(start); if (end > count) end = count; if (start > end) throw new StringIndexOutOfBoundsException(); int len = end - start; if (len > 0) { System.arraycopy(value, start+len, value, start, count-end); count -= len; } return this;}事实上是将剩余的字符重新拷贝到字符数组value。</code></pre><p>这里用到了system.arraycopy来拷贝数组,速度是比较快的</p><h2 id="system-arraycopy方法"><a href="#system-arraycopy方法" class="headerlink" title="system.arraycopy方法"></a>system.arraycopy方法</h2><pre><code>转自知乎:在主流高性能的JVM上(HotSpot VM系、IBM J9 VM系、JRockit系等等),可以认为System.arraycopy()在拷贝数组时是可靠高效的——如果发现不够高效的情况,请报告performance bug,肯定很快就会得到改进。java.lang.System.arraycopy()方法在Java代码里声明为一个native方法。所以最naïve的实现方式就是通过JNI调用JVM里的native代码来实现。</code></pre><h2 id="String的不可变性"><a href="#String的不可变性" class="headerlink" title="String的不可变性"></a>String的不可变性</h2><p>关于String的不可变性,这里转一个不错的回答</p><h3 id="什么是不可变?"><a href="#什么是不可变?" class="headerlink" title="什么是不可变?"></a>什么是不可变?</h3><p>String不可变很简单,如下图,给一个已有字符串”abcd”第二次赋值成”abcedl”,不是在原内存地址上修改数据,而是重新指向一个新对象,新地址。<br><img src="https://pic1.zhimg.com/80/46c03ae5abf6111879423f38375207cc_hd.jpg" alt="image"> </p><h3 id="String为什么不可变?"><a href="#String为什么不可变?" class="headerlink" title="String为什么不可变?"></a>String为什么不可变?</h3><p>翻开JDK源码,java.lang.String类起手前三行,是这样写的:</p><pre><code>public final class String implements java.io.Serializable, Comparable<String>, CharSequence { /** String本质是个char数组. 而且用final关键字修饰.*/ private final char value[]; ... ... } </code></pre><p>首先String类是用final关键字修饰,这说明String不可继承。再看下面,String类的主力成员字段value是个char[]数组,而且是用final修饰的。</p><p>final修饰的字段创建以后就不可改变。 有的人以为故事就这样完了,其实没有。因为虽然value是不可变,也只是value这个引用地址不可变。挡不住Array数组是可变的事实。</p><p>Array的数据结构看下图。<br><img src="https://pic2.zhimg.com/80/356d116d3fd43b622fc9721d399f5631_hd.jpg" alt="image"> </p><p>也就是说Array变量只是stack上的一个引用,数组的本体结构在heap堆。</p><p>String类里的value用final修饰,只是说stack里的这个叫value的引用地址不可变。没有说堆里array本身数据不可变。看下面这个例子, </p><pre><code>final int[] value={1,2,3} ;int[] another={4,5,6}; value=another; //编译器报错,final不可变 value用final修饰,编译器不允许我把value指向堆区另一个地址。但如果我直接对数组元素动手,分分钟搞定。 final int[] value={1,2,3}; value[2]=100; //这时候数组里已经是{1,2,100} 所以String是不可变,关键是因为SUN公司的工程师。 在后面所有String的方法里很小心的没有去动Array里的元素,没有暴露内部成员字段。private final char value[]这一句里,private的私有访问权限的作用都比final大。而且设计师还很小心地把整个String设成final禁止继承,避免被其他人继承后破坏。所以String是不可变的关键都在底层的实现,而不是一个final。考验的是工程师构造数据类型,封装数据的功力。 </code></pre><h3 id="不可变有什么好处?"><a href="#不可变有什么好处?" class="headerlink" title="不可变有什么好处?"></a>不可变有什么好处?</h3><p>这个最简单地原因,就是为了安全。看下面这个场景(有评论反应例子不够清楚,现在完整地写出来),一个函数appendStr( )在不可变的String参数后面加上一段“bbb”后返回。appendSb( )负责在可变的StringBuilder后面加“bbb”。</p><p>总结以下String的不可变性。</p><blockquote><p>1 首先final修饰的类只保证不能被继承,并且该类的对象在堆内存中的地址不会被改变。</p><p>2 但是持有String对象的引用本身是可以改变的,比如他可以指向其他的对象。</p></blockquote><blockquote><p>3 final修饰的char数组保证了char数组的引用不可变。但是可以通过char[0] = ‘a’来修改值。不过String内部并不提供方法来完成这一操作,所以String的不可变也是基于代码封装和访问控制的。</p></blockquote><p>举个例子</p><pre><code>final class Fi { int a; final int b = 0; Integer s;}final char[]a = {'a'};final int[]b = {1};@Testpublic void final修饰类() { //引用没有被final修饰,所以是可变的。 //final只修饰了Fi类型,即Fi实例化的对象在堆中内存地址是不可变的。 //虽然内存地址不可变,但是可以对内部的数据做改变。 Fi f = new Fi(); f.a = 1; System.out.println(f); f.a = 2; System.out.println(f); //改变实例中的值并不改变内存地址。 Fi ff = f; //让引用指向新的Fi对象,原来的f对象由新的引用ff持有。 //引用的指向改变也不会改变原来对象的地址 f = new Fi(); System.out.println(f); System.out.println(ff);}这里的对f.a的修改可以理解为char[0] = 'a'这样的操作。只改变数据值,不改变内存值。</code></pre><p>有关常量池和intern的内容在上一节讲到了。</p><p>具体参考:<a href="https://blog.csdn.net/a724888/article/details/80041698" target="_blank" rel="noopener">https://blog.csdn.net/a724888/article/details/80041698</a></p><p>下一节重讲一下final关键字。</p><p>具体参考:<a href="https://blog.csdn.net/a724888/article/details/80045107" target="_blank" rel="noopener">https://blog.csdn.net/a724888/article/details/80045107</a></p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java基础2:基本数据类型与常量池</title>
<link href="/2018/04/23/javase2/"/>
<url>/2018/04/23/javase2/</url>
<content type="html"><![CDATA[<p>本节主要介绍基本数据类型的大小,自动拆箱装箱,基本数据类型的存储方式,以及常量池的原理。</p><p>具体代码在我的GitHub中可以找到</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>喜欢的话麻烦点下星哈</p><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/04/23/javase2">https://h2pl.github.io/2018/04/23/javase2</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a><br><a id="more"></a></p><h2 id="基本数据类型的大小"><a href="#基本数据类型的大小" class="headerlink" title="基本数据类型的大小"></a>基本数据类型的大小</h2><pre><code>int 32位 4字节 short 16位float 32位double 64位long 64位char 16位byte 8位boolean 1位自动拆箱和装箱的意思就是,计算数值时,integer会自动转为int进行计算。而当int传入类型为integer的引用时,int数值又会被包装为integer。</code></pre><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//8位</span></span><br><span class="line"><span class="keyword">byte</span> bx = <span class="keyword">Byte</span>.MAX_VALUE;</span><br><span class="line"><span class="keyword">byte</span> bn = <span class="keyword">Byte</span>.MIN_VALUE;</span><br><span class="line"><span class="comment">//16位</span></span><br><span class="line"><span class="keyword">short</span> sx = <span class="keyword">Short</span>.MAX_VALUE;</span><br><span class="line"><span class="keyword">short</span> sn = <span class="keyword">Short</span>.MIN_VALUE;</span><br><span class="line"><span class="comment">//32位</span></span><br><span class="line"><span class="keyword">int</span> ix = Integer.MAX_VALUE;</span><br><span class="line"><span class="keyword">int</span> in = Integer.MIN_VALUE;</span><br><span class="line"><span class="comment">//64位</span></span><br><span class="line"><span class="keyword">long</span> lx = <span class="keyword">Long</span>.MAX_VALUE;</span><br><span class="line"><span class="keyword">long</span> ln = <span class="keyword">Long</span>.MIN_VALUE;</span><br><span class="line"><span class="comment">//32位</span></span><br><span class="line"><span class="keyword">float</span> fx = <span class="keyword">Float</span>.MAX_VALUE;</span><br><span class="line"><span class="keyword">float</span> fn = <span class="keyword">Float</span>.MIN_VALUE;</span><br><span class="line"><span class="comment">//64位</span></span><br><span class="line"><span class="keyword">double</span> dx = <span class="keyword">Double</span>.MAX_VALUE;</span><br><span class="line"><span class="keyword">double</span> dn = <span class="keyword">Double</span>.MIN_VALUE;</span><br><span class="line"><span class="comment">//16位</span></span><br><span class="line"><span class="keyword">char</span> cx = Character.MAX_VALUE;</span><br><span class="line"><span class="keyword">char</span> cn = Character.MIN_VALUE;</span><br><span class="line"><span class="comment">//1位</span></span><br><span class="line"><span class="keyword">boolean</span> bt = <span class="keyword">Boolean</span>.<span class="keyword">TRUE</span>;</span><br><span class="line"><span class="keyword">boolean</span> bf = <span class="keyword">Boolean</span>.<span class="keyword">FALSE</span>;</span><br></pre></td></tr></table></figure><h2 id="自动拆箱和装箱"><a href="#自动拆箱和装箱" class="headerlink" title="自动拆箱和装箱"></a>自动拆箱和装箱</h2><pre><code>//基本数据类型的常量池是-128到127之间。// 在这个范围中的基本数据类的包装类可以自动拆箱,比较时直接比较数值大小。public static void main(String[] args) { //int的自动拆箱和装箱只在-128到127范围中进行,超过该范围的两个integer的 == 判断是会返回false的。 Integer a1 = 128; Integer a2 = -128; Integer a3 = -128; Integer a4 = 128; System.out.println(a1 == a4); System.out.println(a2 == a3); Byte b1 = 127; Byte b2 = 127; Byte b3 = -128; Byte b4 = -128; //byte都是相等的,因为范围就在-128到127之间 System.out.println(b1 == b2); System.out.println(b3 == b4); // Long c1 = 128L; Long c2 = 128L; Long c3 = -128L; Long c4 = -128L; System.out.println(c1 == c2); System.out.println(c3 == c4); //char没有负值 //发现char也是在0到127之间自动拆箱 Character d1 = 128; Character d2 = 128; Character d3 = 127; Character d4 = 127; System.out.println(d1 == d2); System.out.println(d3 == d4); Integer i = 10; Byte b = 10; //比较Byte和Integer.两个对象无法直接比较,报错 //System.out.println(i == b); System.out.println("i == b " + i.equals(b)); //答案是false,因为包装类的比较时先比较是否是同一个类,不是的话直接返回false. int ii = 128; short ss = 128; long ll = 128; char cc = 128; System.out.println("ii == bb " + (ii == ss)); System.out.println("ii == ll " + (ii == ll)); System.out.println("ii == cc " + (ii == cc)); //这时候都是true,因为基本数据类型直接比较值,值一样就可以。</code></pre><p>总结:注意基本数据类型的拆箱装箱,以及对常量池的理解。</p><h2 id="基本数据类型的存储方式"><a href="#基本数据类型的存储方式" class="headerlink" title="基本数据类型的存储方式"></a>基本数据类型的存储方式</h2><pre><code>上面自动拆箱和装箱的原理其实与常量池有关。3.1存在栈中:public void(int a){int i = 1;int j = 1;}方法中的i 存在虚拟机栈的局部变量表里,i是一个引用,j也是一个引用,它们都指向局部变量表里的整型值 1.int a是传值引用,所以a也会存在局部变量表。3.2存在堆里:class A{int i = 1;A a = new A();}i是类的成员变量。类实例化的对象存在堆中,所以成员变量也存在堆中,引用a存的是对象的地址,引用i存的是值,这个值1也会存在堆中。可以理解为引用i指向了这个值1。也可以理解为i就是1.3.3包装类对象怎么存其实我们说的常量池也可以叫对象池。比如String a= new String("a").intern()时会先在常量池找是否有“a"对象如果有的话直接返回“a"对象在常量池的地址,即让引用a指向常量”a"对象的内存地址。public native String intern();Integer也是同理。</code></pre><p>下图是Integer类型在常量池中查找同值对象的方法。</p><pre><code>public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i);}private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {}}</code></pre><p>所以基本数据类型的包装类型可以在常量池查找对应值的对象,找不到就会自动在常量池创建该值的对象。</p><p>而String类型可以通过intern来完成这个操作。</p><p>JDK1.7后,常量池被放入到堆空间中,这导致intern()函数的功能不同,具体怎么个不同法,且看看下面代码,这个例子是网上流传较广的一个例子,分析图也是直接粘贴过来的,这里我会用自己的理解去解释这个例子:</p><figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">[<span class="keyword">java] </span>view plain copy</span><br><span class="line">String s = new String(<span class="string">"1"</span>)<span class="comment">; </span></span><br><span class="line">s.intern()<span class="comment">; </span></span><br><span class="line">String <span class="built_in">s2</span> = <span class="string">"1"</span><span class="comment">; </span></span><br><span class="line">System.out.println(s == <span class="built_in">s2</span>)<span class="comment">; </span></span><br><span class="line"> </span><br><span class="line">String <span class="built_in">s3</span> = new String(<span class="string">"1"</span>) + new String(<span class="string">"1"</span>)<span class="comment">; </span></span><br><span class="line"><span class="built_in">s3</span>.intern()<span class="comment">; </span></span><br><span class="line">String <span class="built_in">s4</span> = <span class="string">"11"</span><span class="comment">; </span></span><br><span class="line">System.out.println(<span class="built_in">s3</span> == <span class="built_in">s4</span>)<span class="comment">; </span></span><br><span class="line">输出结果为:</span><br><span class="line"></span><br><span class="line">[<span class="keyword">java] </span>view plain copy</span><br><span class="line"><span class="keyword">JDK1.6以及以下:false </span>false </span><br><span class="line"><span class="keyword">JDK1.7以及以上:false </span>true</span><br></pre></td></tr></table></figure><p><img src="https://img-blog.csdn.net/20180422231916788?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2E3MjQ4ODg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70" alt="image"></p><p><img src="https://img-blog.csdn.net/20180422231929413?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2E3MjQ4ODg=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70" alt="image"><br>JDK1.6查找到常量池存在相同值的对象时会直接返回该对象的地址。</p><p>JDK 1.7后,intern方法还是会先去查询常量池中是否有已经存在,如果存在,则返回常量池中的引用,这一点与之前没有区别,区别在于,如果在常量池找不到对应的字符串,则不会再将字符串拷贝到常量池,而只是在常量池中生成一个对原字符串的引用。</p><p>那么其他字符串在常量池找值时就会返回另一个堆中对象的地址。</p><p>下一节详细介绍String以及相关包装类。</p><p>具体请见:<a href="https://blog.csdn.net/a724888/article/details/80042298" target="_blank" rel="noopener">https://blog.csdn.net/a724888/article/details/80042298</a></p><p>关于Java面向对象三大特性,请参考:</p><p><a href="https://blog.csdn.net/a724888/article/details/80033043" target="_blank" rel="noopener">https://blog.csdn.net/a724888/article/details/80033043</a></p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>Java基础1:理解Java面向对象三大特性</title>
<link href="/2018/04/22/javase1/"/>
<url>/2018/04/22/javase1/</url>
<content type="html"><![CDATA[<p>具体代码在我的GitHub中可以找到:</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>喜欢的话麻烦点下星哈</p><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/04/22/javase1">https://h2pl.github.io/2018/04/22/javase1</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><blockquote><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p></blockquote><p>本节主要介绍Java面向对象三大特性:继承 封装 多态,以及其中的原理。</p><p>本文会结合虚拟机对引用和对象的不同处理来介绍三大特性的原理。</p><a id="more"></a><h2 id="继承"><a href="#继承" class="headerlink" title="继承"></a>继承</h2><p>Java中的继承只能单继承,但是可以通过内部类继承其他类来实现多继承。</p><pre><code>public class Son extends Father{public void go () {System.out.println("son go");}public void eat () {System.out.println("son eat");}public void sleep() {System.out.println("zzzzzz");}public void cook() {//匿名内部类实现的多继承new Mother().cook();//内部类继承第二个父类来实现多继承Mom mom = new Mom();mom.cook();}private class Mom extends Mother {@Overridepublic void cook() {System.out.println("mom cook");}}}</code></pre><h2 id="封装"><a href="#封装" class="headerlink" title="封装"></a>封装</h2><p>封装主要是因为Java有访问权限的控制。public > protected > package = default > private。封装可以保护类中的信息,只提供想要被外界访问的信息。</p><p>类的访问范围</p><pre><code> A、public 包内、包外,所有类中可见B、protected 包内所有类可见,包外有继承关系的子类可见(子类对象可调用)C、(default)表示默认,不仅本类访问,而且是同包可。D、private 仅在同一类中可见 </code></pre><h2 id="多态"><a href="#多态" class="headerlink" title="多态"></a>多态</h2><p>多态一般可以分为两种,一个是重写overwrite,一个是重载override。</p><pre><code>重写是由于继承关系中的子类有一个和父类同名同参数的方法,会覆盖掉父类的方法。重载是因为一个同名方法可以传入多个参数组合。注意,同名方法如果参数相同,即使返回值不同也是不能同时存在的,编译会出错。从jvm实现的角度来看,重写又叫运行时多态,编译时看不出子类调用的是哪个方法,但是运行时操作数栈会先根据子类的引用去子类的类信息中查找方法,找不到的话再到父类的类信息中查找方法。而重载则是编译时多态,因为编译期就可以确定传入的参数组合,决定调用的具体方法是哪一个了。</code></pre><h3 id="向上转型和向下转型:"><a href="#向上转型和向下转型:" class="headerlink" title="向上转型和向下转型:"></a>向上转型和向下转型:</h3><pre><code>public static void main(String[] args) { Son son = new Son(); //首先先明确一点,转型指的是左侧引用的改变。 //father引用类型是Father,指向Son实例,就是向上转型,既可以使用子类的方法,也可以使用父类的方法。 //向上转型,此时运行father的方法 Father father = son; father.smoke(); //不能使用子类独有的方法。 // father.play();编译会报错 father.drive(); //Son类型的引用指向Father的实例,所以是向下转型,不能使用子类非重写的方法,可以使用父类的方法。 //向下转型,此时运行了son的方法 Son son1 = (Son) father; //转型后就是一个正常的Son实例 son1.play(); son1.drive(); son1.smoke(); //因为向下转型之前必须先经历向上转型。 //在向下转型过程中,分为两种情况: //情况一:如果父类引用的对象如果引用的是指向的子类对象, //那么在向下转型的过程中是安全的。也就是编译是不会出错误的。 //因为运行期Son实例确实有这些方法 Father f1 = new Son(); Son s1 = (Son) f1; s1.smoke(); s1.drive(); s1.play(); //情况二:如果父类引用的对象是父类本身,那么在向下转型的过程中是不安全的,编译不会出错, //但是运行时会出现java.lang.ClassCastException错误。它可以使用instanceof来避免出错此类错误。 //因为运行期Father实例并没有这些方法。 Father f2 = new Father(); Son s2 = (Son) f2; s2.drive(); s2.smoke(); s2.play(); //向下转型和向上转型的应用,有些人觉得这个操作没意义,何必先向上转型再向下转型呢,不是多此一举么。其实可以用于方法参数中的类型聚合,然后具体操作再进行分解。 //比如add方法用List引用类型作为参数传入,传入具体类时经历了向下转型 add(new LinkedList()); add(new ArrayList()); //总结 //向上转型和向下转型都是针对引用的转型,是编译期进行的转型,根据引用类型来判断使用哪个方法 //并且在传入方法时会自动进行转型(有需要的话)。运行期将引用指向实例,如果是不安全的转型则会报错。 //若安全则继续执行方法。}public static void add(List list) { System.out.println(list); //在操作具体集合时又经历了向上转型// ArrayList arr = (ArrayList) list;// LinkedList link = (LinkedList) list;}</code></pre><p>总结:<br>向上转型和向下转型都是针对引用的转型,是编译期进行的转型,根据引用类型来判断使用哪个方法。并且在传入方法时会自动进行转型(有需要的话)。运行期将引用指向实例,如果是不安全的转型则会报错,若安全则继续执行方法。</p><h3 id="编译期的静态分派"><a href="#编译期的静态分派" class="headerlink" title="编译期的静态分派"></a>编译期的静态分派</h3><p>其实就是根据引用类型来调用对应方法。</p><pre><code>public static void main(String[] args) { Father father = new Son(); 静态分派 a= new 静态分派(); //编译期确定引用类型为Father。 //所以调用的是第一个方法。 a.play(father); //向下转型后,引用类型为Son,此时调用第二个方法。 //所以,编译期只确定了引用,运行期再进行实例化。 a.play((Son)father); //当没有Son引用类型的方法时,会自动向上转型调用第一个方法。 a.smoke(father); //}public void smoke(Father father) { System.out.println("father smoke");}public void play (Father father) { System.out.println("father"); //father.drive();}public void play (Son son) { System.out.println("son"); //son.drive();}</code></pre><h3 id="方法重载优先级匹配"><a href="#方法重载优先级匹配" class="headerlink" title="方法重载优先级匹配"></a>方法重载优先级匹配</h3><pre><code>public static void main(String[] args) { 方法重载优先级匹配 a = new 方法重载优先级匹配(); //普通的重载一般就是同名方法不同参数。 //这里我们来讨论当同名方法只有一个参数时的情况。 //此时会调用char参数的方法。 //当没有char参数的方法。会调用int类型的方法,如果没有int就调用long //即存在一个调用顺序char -> int -> long ->double -> ..。 //当没有基本类型对应的方法时,先自动装箱,调用包装类方法。 //如果没有包装类方法,则调用包装类实现的接口的方法。 //最后再调用持有多个参数的char...方法。 a.eat('a'); a.eat('a','c','b');}public void eat(short i) { System.out.println("short");}public void eat(int i) { System.out.println("int");}public void eat(double i) { System.out.println("double");}public void eat(long i) { System.out.println("long");}public void eat(Character c) { System.out.println("Character");}public void eat(Comparable c) { System.out.println("Comparable");}public void eat(char ... c) { System.out.println(Arrays.toString(c)); System.out.println("...");}// public void eat(char i) {// System.out.println("char");// }</code></pre><p>下一节具体介绍了基本数据类型以及常量池,具体请见:</p><p><a href="https://blog.csdn.net/a724888/article/details/80041698" target="_blank" rel="noopener">https://blog.csdn.net/a724888/article/details/80041698</a></p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> Java基础 </tag>
</tags>
</entry>
<entry>
<title>我和技术博客的这一年</title>
<link href="/2018/04/20/blog/"/>
<url>/2018/04/20/blog/</url>
<content type="html"><![CDATA[<p>本文记录了我从Java初学者到专注于Java后端开发技术栈的成长历程,主要是与写博客相关的内容,其他内容还包括<br>实习历程,后端技术学习历程,校招计划等内容,我会陆续发表并且提供链接。</p><p>我的GitHub:</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>喜欢的话麻烦点下星哈</p><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/04/20/blog">https://h2pl.github.io/2018/04/20/blog</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p><p>Java后端学习之路 <a href="https://blog.csdn.net/a724888/article/details/60879893" target="_blank" rel="noopener">https://blog.csdn.net/a724888/article/details/60879893</a></p><a id="more"></a><p>下面是正文:</p><h2 id="梦开始的地方"><a href="#梦开始的地方" class="headerlink" title="梦开始的地方"></a>梦开始的地方</h2><p>  2017年初开了这个博客,转眼也一年多时间了。最早在博客园开的博客,后来感觉csdn生态更好一点,于是转移到csdn。恰逢这段时间在做学校的课题,于是最开始的时候记录了一些项目搭建以及开发中遇到的题以及解决方案。当时技术还比较稚嫩,属于刚刚入门Java web的阶段。所以博客内容也比较一般。</p><h2 id="博客记录我的成长"><a href="#博客记录我的成长" class="headerlink" title="博客记录我的成长"></a>博客记录我的成长</h2><p>  去年的春天我投入到浩瀚的春招大军中去了,也是那个时候确定了做Java开发的方向,当时对后台技术还不是太了解,主要从Java以及Java web入手,开始了一系列的学习和准备。这篇文章主要讲博客的历程,如果对我的学习历程有兴趣的朋友可以查看最上方的链接。<br>  项目结束以后,主要在复习Java基础,于是看了不少相关博客,记录了很多Java的基础知识点,比如异常,反射,序列化,集合类等等内容的一些总结,现在看来确实有点幼稚了。所以我最近也在删除一些低质量的文章,以便让大家能看到更好的内容。<br>  在准备春招实习面试期间,我花了大量时间阅读技术书籍以及博客,并且总结了一部分面经,同时将一些比较好的总结发在了博客上,以便我在复习期间能够阅读和复习,所以有一段时间发了大量的博文,多得连我自己都怕。当然我并不推荐这种做法,在后来的日子里,基本上是定期地发一些有一定质量的文章,尽量自己理清文章内容后再进行发布,否则可能有会滥竽充数的情况。<br>  除此之外也记录了一些工程方面的内容,例Maven,git,Tomcat,以及IDE的使用,以及MySQL的一些使用经验,由于有段时间在W厂实习,所以当时主要记录的是实习过程中用的技术栈以及相关开发tips。<br>  离开W厂之后,我来到了B厂,部门做的主要是云计算,于是记录了一些云计算相关的文章,比如OpenStack,docker,kubenetes等内容。B厂是技术为主导的公司,内部经常举办技术交流会以及分享会,我通常都会报名参加,了解了一部分AI和大数据的应用以及实现原理。所以这段时间主要会发一些AI以及Hadoop的文章,让我更全面了解相关技术。</p><h2 id="最好的总结就是读书笔记"><a href="#最好的总结就是读书笔记" class="headerlink" title="最好的总结就是读书笔记"></a>最好的总结就是读书笔记</h2><p>  在百度的这段时间里,我意识到了我的基础可能还是不够牢固。因为是非科班出身,虽然是硕士,但是基础还是有一些欠缺,这段时间我看了许多更加底层的东西,比如网络,操作系统,Linux内核,其中那一本《深入理解计算机系统》确实是不错的总结性书籍,基本可以带你概览计算机系统的全貌。<br>  因此,在这段时间里我写了不少的读书笔记等总结性文章,主要囊括了操作系统,计算机网络,Linux等内容。我发现写读书笔记是加深对原书理解的很好的途径,于是我把以前看过的一些书拿出来又翻了几遍,例如JVM虚拟机,java并发实战,大型网站架构滴滴,所以我干脆把其他书的读书笔记也整理出来了,不过有一些书过于晦涩或者是太厚,也借鉴了一些博友的读书笔记。当然有很多文章还不够成熟。</p><h2 id="不积跬步无以至千里"><a href="#不积跬步无以至千里" class="headerlink" title="不积跬步无以至千里"></a>不积跬步无以至千里</h2><p>  大公司面试时,会给你一种感觉,就是无孔不入,细节决定成败,往往粗浅的总结难以让你理解技术深层次的原理,缺乏实践或者是深入思考,可能会让你错过很多重要的知识点,而往往这些知识点是大厂面试官喜欢问的。<br>  就拿Java来说,jvm虚拟机垃圾回收器的具体回收过程,可以问的很深入,问到gcRoots,停顿多少次,是否并发回收等,这些问题可能不是对gc的浅显总结可以概括的。<br>  再比如,JUC中的Lock,平时可能只了解到lock的用法,condition,并发工具类的使用,但是Lock底层的AQS实现,可能很少去关注,AQS的相关源码晦涩难懂,推荐看大牛的解析,可以让你更好地理解lock类的实现。<br>  其实这个想法也是前阵子我才想到的,因为看到阿里的实习面经,Java相关的原理问的特别深,没有深入到源码去理解的话,往往就会被问住。结果可想而知。所以这段时间主要的想法是只记录高质量的内容,并且尽量覆盖重要的知识点。</p><h2 id="纸上得来终觉浅,实践为王"><a href="#纸上得来终觉浅,实践为王" class="headerlink" title="纸上得来终觉浅,实践为王"></a>纸上得来终觉浅,实践为王</h2><p>  文章写得再好,毕竟是纸面上的东西,一旦上手,可能又是另一种情况,我虽然看了不少书,也阅读了许多优质的博客,但是对于有些技术细节总觉得还是差了点,或者说,书上看来的东西,很快就忘了。其实记忆本身就是这种特点,只有实战可以让书上的知识变成你自己的。用过这个技术并且能了解其原理,才能对这个技术有发言权。所以在未来的计划里,我打算更多地写一些实战性的文章。</p><h2 id="回到原点,重新出发"><a href="#回到原点,重新出发" class="headerlink" title="回到原点,重新出发"></a>回到原点,重新出发</h2><p>  从第一次写博客到现在,经历了很多事,有了诸多感悟,与君共勉,至于对我的观点认同与否,那就见仁见智了。脚踏实地也不要忘了仰望星空。建议做开发的朋友们都要写博客,写博客的好处很多,方便记忆,便于交流,也是打造个人品牌的一种方式,有时间自己搭博客,效果更好。<br>  最近用b3log solo搭了博客,接下来打算用github pages + hexo来写博客。等到工作以后,可能会只用个人博客了。这可能也象征着学生时代的结束吧,新的博客不仅会有技术文章,还会分享人生感悟,csdn的话,还是主要发布技术文章。就说到这里了。希望有更多人看到。</p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> 心路历程 </tag>
</tags>
</entry>
<entry>
<title>JAVA后端开发学习之路</title>
<link href="/2018/04/20/java/"/>
<url>/2018/04/20/java/</url>
<content type="html"><![CDATA[<p>本文主要记录了我从Java初学者到专注于Java后端技术栈的开发者的学习历程。主要分享了学习过程中的一些经验和教训,让后来人看到,少走弯路,与君共勉,共同进步。如有错误,还请见谅。</p><p>我的GitHub:</p><blockquote><p><a href="https://github.com/h2pl/MyTech" target="_blank" rel="noopener">https://github.com/h2pl/MyTech</a></p></blockquote><p>喜欢的话麻烦点下星哈</p><p>文章首发于我的个人博客:</p><blockquote><p><a href="https://h2pl.github.io/2018/04/20/java">https://h2pl.github.io/2018/04/20/java</a></p></blockquote><p>更多关于Java后端学习的内容请到我的CSDN博客上查看:</p><p><a href="https://blog.csdn.net/a724888" target="_blank" rel="noopener">https://blog.csdn.net/a724888</a></p><p>相关链接:我和技术博客的这一年:<a href="https://blog.csdn.net/a724888/article/details/60879893" target="_blank" rel="noopener">https://blog.csdn.net/a724888/article/details/60879893</a></p><blockquote><p>  不论你是不是网民,无论你远离互联网,还是沉浸其中;你的身影,都在这场伟大的迁徙洪流中。超越人类经验的大迁徙,温暖而无情地,开始了。<br>                                 —–《互联网时代》</p></blockquote><a id="more"></a><h2 id="选择方向"><a href="#选择方向" class="headerlink" title="选择方向"></a>选择方向</h2><p>  0上大学前的那些事,让它们随风逝去吧。</p><p>  1 个人对计算机和互联网有情怀,有兴趣,本科时在专业和学校里选择了学校,当时专业不是计算机,只能接触到一点点计算机专业课程,所以选择了考研,花半年时间复习考进了一个还不错的985,考研经历有空会发到博客上。</p><p>  2 本科阶段接触过Java和Android,感觉app蛮有趣的,所以研一的时候想做Android,起初花大量时间看了计算机专业课的教材,效果很差。但也稍微了解了一些计算机基础,如网络,操作系统,组成原理,数据库,软工等。</p><p>  3 在没确定方向的迷茫时期看了大量视频和科普性文章,帮助理清头绪和方向。期间了解了诸如游戏开发,c++开发,Android,Java甚至前端等方向,其中还包含游戏策划岗。</p><p>  4 后来综合自身条件以及行业发展等因素,开始锁定自己的目标在Java后台方向。于是乎各种百度,知乎,查阅该学什么该怎么学如此类的问题,学习别人的经验。当然只靠搜索引擎很难找到精品内容,那段时间可谓是病急乱投医,走了不少弯路。</p><hr><h2 id="夯实基础"><a href="#夯实基础" class="headerlink" title="夯实基础"></a>夯实基础</h2><p>  1 研一的工程实践课让我知道了我的基础不够扎实,由于并非科班,需要比别人更加勤奋,古语有云,天道酬勤,勤能补拙。赶上了17年的春招实习招聘,期间开始各种海投,各种大厂面试一问三不知,才知道自身差距很大,开始疯狂复习面试题,刷面经,看经验等。死记硬背,之乎者也,倒也是能应付一些小公司,可谓是临阵磨枪不快也光。</p><p>  2 不过期间的屡屡受挫让我冷静思考了一段时间,我再度调研了岗位需求,学习方法,以及需要看的书等资料。再度开工时,我的桌上开始不断出现新的经典书籍。这还要归功于我的启蒙导师:江南白衣,在知乎上看到了他的一篇文章,我的Java后端书架。在这个书架里我找寻到了很多我想看的书,以及我需要学习的技术。</p><p>  3 遥想研一我还在看的书:教材就不提了,脱离实际并且年代久远,而我选的入门书籍竟然还有Java web从入门到精通这种烂大街的书籍,然后就是什么Java编程思想啦,深入理解计算机系统,算法导论这种高深莫测的书,感觉有点高不成低不就的意思。要么太过难懂要么过于粗糙,这些书在当时基本上没能帮到我。</p><hr><h2 id="书籍选择"><a href="#书籍选择" class="headerlink" title="书籍选择"></a>书籍选择</h2><p>  1 江南白衣的后端书架真是救我于水火。他的书架里收录了许多Java后端需要用到的技术书籍,并且十分经典,虽不说每本都适合入门,但是只要你用心去看都会有收获,高质量的书籍给人的启发要优于普通书籍。</p><p>  2 每个门类的书我都挑了一些。比如网络的两本(《tcp ip卷一》以及《计算机网络自顶向下》),操作系统两本(一本《Linux内核设计与实现》,一本高级操作系统,推荐先看完《深入理解计算机系统》再来看这两本),算法看的是《数据结构与算法(Java版)》,Java的四大件(《深入理解jvm虚拟机》,《java并发编程艺术》,《深入java web技术内幕》,《Java核心技术 卷一》这本没看)。</p><p>  3 当然还有像《Effective Java》,《Java编程思想》,《Java性能调优指南》这种,不过新手不推荐,太不友好。接着是spring的两本《Spring实战》和《Spring源码剖析》。当然也包括一些redis,mq之类的书,还有就是一些介绍分布式组件的书籍,如zk等。</p><p>  4 接下来就是扩展的内容了,比如分布式的三大件,《大型网站架构设计与实践》,《分布式网站架构设计与实践》,《Java中间件设计与实践》,外加一本《分布式服务框架设计与实践》。这几本书一看,绝对让你打开新世界的大门,醍醐灌顶,三月不知肉味。</p><p>  5 你以为看完这些书你就无敌了,就满足了?想得倒是挺美。这些书最多就是把我从悬崖边拉回正途,能让我在正确的道路上行走了。毕竟技术书籍这种东西还是有门槛的,没有一定的知识储备,看书的过程也绝对是十分痛苦的。</p><p>    6 比如《深入理解jvm虚拟机》和《java并发编程艺术》这两本书,我看了好几遍,第一遍基本当天书来看,第二遍挑着章节看,第三遍能把全部章节都看了。所以有时候你觉得你看完了一本书,对,你确实看完了,但过段时间是你能记得多少呢。可以说是很少了。</p><hr><h2 id="谈一谈学习方法"><a href="#谈一谈学习方法" class="headerlink" title="谈一谈学习方法"></a>谈一谈学习方法</h2><p>  1 人们在刚开始接触自己不熟悉的领域时,往往都会犯很多错误。刚开始学习Java时,就是摸着石头过河。从在极客学院慕课上看视频,到看书,再到看博客,再到工程实践,也是学习方式转变的一个过程。</p><p>  2 看视频:适合0基础小白,视频给你构建一个世界观,让你对你要做的东西有个大概的了解,想要深入理解其中的技术原理,只看视频的话很难。</p><p>  3 看书:就如上面一节所说,看书是一个很重要的环节。当你对技术只停留在大概的了解和基本会用的阶段时,经典书籍能够让你深入这些技术的原理,你可能会对书里的内容感到惊叹,也可能只是一知半解。所以第一遍的阅读一般读个大概就可以。一本书要吃透,不仅要看好几遍,还要多上手实践,才能变成自己的东西。</p><p>  4 看博客,光看一些总结性的博客或者是科普性的博客可能还不够,一开始我也经常看这样的博客,后来只看这些东西,发现对技术的理解只能停留在表面。高质量的博客一般会把一个知识点讲得很透彻,比你看十篇总结都强,例如讲jdk源码的博文,可以很好地帮助你理解其原理,避免自己看的时候一脸懵逼。这里先推荐几个博客和网站,后面写复习计划的时候,会详细写出。<br>博客:江南白衣、酷壳、战小狼。<br>网站:并发编程网,importnew。</p><p>  5 实践为王,Java后端毕竟还是工程方向,只是通过文字去理解技术点,可能有点纸上谈兵的感觉了。还有一个问题就是,没有进行上手实践的技术,一般很快就会忘了,做一些实践可以更好地巩固知识点。如果有项目中涉及不到的知识点,可以单独拿出来做一些demo,实在难以进行实践的技术点,可以参考别人的实践过程。</p><hr><h2 id="实习,提高工程能力的好机会"><a href="#实习,提高工程能力的好机会" class="headerlink" title="实习,提高工程能力的好机会"></a>实习,提高工程能力的好机会</h2><p>  1 这段时间以后就是实习期了,三个月的W厂实习经历。半年的B厂实习,让我着实过了一把大厂的瘾。但是其中做的工作无非就是增删改查写写业务逻辑,很难接触到比较核心的部分。</p><p>  2 于是乎我花了许多时间学习部门的核心技术。比如在W厂参与数据平台的工作时,我学习了hadoop以及数据仓库的架构,也写了一些博客,并且向负责后端架构的导师请教了许多知识,收获颇丰。</p><p>  3 在B厂实习期间则接触了许多云计算相关的技术。因为部门做的是私有云,所以业务代码和底层的服务也是息息相关的,比如平时的业务代码也会涉及到底层的接口调用,比如新建一个虚拟机或者启动一台虚拟机,需要通过多级的服务调用,首先是HTTP服务调用,经过多级的服务调用,最终完成流程。在这期间我花了一些时间学习了OpenStack的架构以及部门的实际应用情况,同时也玩了一下docker,看了kubenetes的一些书籍,算是入门。</p><p>  4 但是这些东西其实离后台开发还是有一定距离的,比如后台开发的主要问题就是高并发,分布式,Linux服务器开发等。而我做的东西,只能稍微接触到这一部门的内容,因为主要是to b的内部业务。所以这段时间其实我的进步有限,虽然扩大了知识面并且积累了开发经验,但是对于后台岗位来说还是有所欠缺的。</p><p>  5 不过将近一年的实习也让我收获了很多东西,大厂的实习体验很好,工作高效,团队合作,版本的快速迭代,技术氛围很不错。特别是在B厂了可以解到很多前沿的技术,对自己的视野扩展很有帮助。</p><hr><h2 id="实习转正,还是准备秋招?"><a href="#实习转正,还是准备秋招?" class="headerlink" title="实习转正,还是准备秋招?"></a><strong>实习转正,还是准备秋招?</strong></h2><p>  1 离职以后,在考虑是否还要找实习,因为有两份实习经历了,在考虑要不要静下心来刷刷题,复习一下基础,并且回顾一下实习时用到的技术。同一时期,我了解到腾讯和阿里等大厂的实习留用率不高,并且可能影响到秋招,所以当时的想法是直接复习等到秋招内推。因此,那段时间比较放松,没什么复习状态,也导致了我在今年春招内推的阶段比较艰难。</p><p>  2 因为当时想着沉住气准备秋招,所以一开始对实习内推不太在意。但是由于AT招人的实习生转正比例较大,考虑到秋招的名额可能更少,所以还是不愿意错过这个机会。因为开始系统复习的时间比较晚,所以投的比较晚,担心准备不充分被刷。这次找实习主要是奔着转正去的,所以只投了bat和滴滴,京东,网易游戏等大厂。</p><p>  3 由于投递时间原因,所以面试的流程特别慢。并且在笔试方面还是有所欠缺,刷题刷的比较少,在线编程的算法题还是屡屡受挫。这让我有点后悔实习结束后的那段时间没有好好刷题了。</p><hr><h2 id="调整心态,重新上路"><a href="#调整心态,重新上路" class="headerlink" title="调整心态,重新上路"></a><strong>调整心态,重新上路</strong></h2><p>  1 目前的状态是,一边刷题,一边复习基础,投了几家大厂的实习内推,打算选一个心仪的公司准备转正,但是事情总是没那么顺利,微软,头条等公司的笔试难度超过了我的能力范围,没能接到面试电话。腾讯投了一个自己比较喜欢的部门,可惜岗位没有匹配上,后台开发被转成了运营开发,最终没能通过。阿里面试的也不顺利,当时投了一个牛客上的蚂蚁金服内推,由于投的太晚,部门已经招满,只面了一面就没了下文,前几天接到了菜鸟的面试,这个未完待续。</p><p>  2 目前的想法是,因为我不怎么需要实习经历来加分了,所以想多花些时间复习基础,刷题,并且巩固之前的项目经历。当然如果有好的岗位并且转正机会比较大的话,也是会考虑去实习的,那样的话可能需要多挤点时间来复习基础和刷题了。</p><p>  3 在这期间,我会重新梳理一下自己的复习框架,有针对性地看一些高质量的博文,同时多做些项目实践,加深对知识的理解。当然这方面还会通过写博客进行跟进,写博客,做项目。前阵子在牛客上看到一位牛友CyC2018做的名为interview notebook的GitHub仓库,内容非常好,十分精品,我全部看完了,并且参考其LeetCode题解进行刷题。</p><p>  4 受到这位大佬的启发,我也打算做一个类似的代码仓库或者是博客专栏,尽量在秋招之前把总结做完,并且把好的文章都放进去。上述内容只是本人个人的心得体会,如果有错误或者说的不合理的地方,还请谅解和指正。希望与广大牛友共勉,一起进步。</p>]]></content>
<categories>
<category> 后端 </category>
<category> Java </category>
</categories>
<tags>
<tag> 心路历程 </tag>
</tags>
</entry>
</search>