1忆首、面向?qū)ο蟮幕疽兀悍庋b爱榔、繼承和多態(tài)。
封裝的目的是隱藏事務(wù)內(nèi)部的實現(xiàn)細(xì)節(jié)糙及,以便提高安全性和簡化編程详幽。封裝提供了合理的邊界,避免外部調(diào)用者接觸到內(nèi)部的細(xì)節(jié)浸锨。從另一個角度看唇聘,封裝這種隱藏,也提供了簡化的界面柱搜,避免太多無意義的細(xì)節(jié)浪費(fèi)調(diào)用者的精力迟郎。
繼承是代碼復(fù)用的基礎(chǔ)機(jī)制,類似于我們對于馬聪蘸、白馬和黑馬的歸納總結(jié)宪肖。但要注意,繼承可以看做是非常緊耦合的一種關(guān)系健爬,父類代碼修改控乾,子類行為也會變動。在實踐中浑劳,過度濫用繼承阱持,可能會起到反效果。
多態(tài)魔熏,你可能立即會想到重寫(override)和重載(overload)衷咽、向上轉(zhuǎn)型条篷。簡單說框舔,重寫是父子類中相同名字和參數(shù)的方法,不同的實現(xiàn);重載則是相同名字的方法兄纺,但是不同的參數(shù),本質(zhì)上這些方法的簽名是不一樣的周瞎。
多態(tài)的存在有三個前提:
1.要有繼承關(guān)系
2.子類要重寫父類的方法
3.父類引用指向子類對象
多態(tài)成員訪問的特點(diǎn):
1)成員變量
編譯看左邊(父類)箍鼓,運(yùn)行看左邊(父類)
2)成員方法
編譯看左邊(父類),運(yùn)行看右邊(子類)
3)靜態(tài)方法
編譯看左邊(父類)相寇,運(yùn)行看左邊(父類)
只有非靜態(tài)的成員方法慰于,編譯看左邊,運(yùn)行看右邊
對于創(chuàng)建的對象是向上轉(zhuǎn)型還是向下轉(zhuǎn)型:
向上轉(zhuǎn)型:只能調(diào)用與父類引用中父類相同的方法唤衫,不能調(diào)用子類中自己定義的方法婆赠;如果在子類中重寫了,則調(diào)用的是子類中的方法佳励;
向下轉(zhuǎn)型:可以調(diào)用父類中的方法休里,也可以調(diào)用子類中自己定義的方法;如果子類中重寫了父類的方法赃承,調(diào)用的是子類中重寫的方法妙黍;
那么多態(tài)有什么弊端呢?有的瞧剖,即多態(tài)后不能使用子類特有的屬性和方法拭嫁。
如果想使用子類特有的屬性和方法,則可以進(jìn)行向下轉(zhuǎn)型抓于,將父類的引用強(qiáng)制轉(zhuǎn)為子類型噩凹。
2、Java中為什么靜態(tài)方法不能被重寫毡咏?
首先理解重寫的意思驮宴,重寫就是子類中對父類的實例方法進(jìn)行重新定義功能,且返回類型呕缭、方法名以及參數(shù)列表保持一致堵泽,且對重寫方法的調(diào)用主要看實際類型。實際類型如果實現(xiàn)了該方法則直接調(diào)用該方法恢总,如果沒有實現(xiàn)迎罗,則在繼承關(guān)系中從低到高搜索有無實現(xiàn)。那么問題又來了片仿,為什么只能對實例方法才能重寫纹安?我頭好暈,這兩個問題在這互相推脫責(zé)任。
為了滿足里式替換原則厢岂,重寫有有以下兩個限制:
* 子類方法的訪問權(quán)限必須大于等于父類方法光督;
* 子類方法的返回類型必須是父類方法返回類型或為其子類型。
使用 @Override 注解塔粒,可以讓編譯器幫忙檢查是否滿足上面的兩個限制條件结借。
理解三個概念:靜態(tài)類型,實際類型卒茬,方法接受者船老。
Person student= new Student();
student.work();
靜態(tài)類型就是編譯器編譯期間認(rèn)為對象所屬的類型,這個主要根據(jù)聲明類型決定圃酵,所以上述Person就是靜態(tài)類型
實際類型就是解釋器在執(zhí)行時根據(jù)引用實際指向的對象所決定的柳畔,所以Student就是實際類型。
方法接受者就是動態(tài)綁定所找到執(zhí)行此方法的對象郭赐,比如student荸镊。
還要理解類編譯的class文件中字節(jié)碼的方法調(diào)用指令。
(1)invokestatic:調(diào)用靜態(tài)方法
(2)invokespecial:調(diào)用實例構(gòu)造器方法堪置,私有方法。
(3)invokevirtual:調(diào)用所有的虛方法张惹。
(4)invokeinterface:調(diào)用接口方法舀锨,會在運(yùn)行時再確定一個實現(xiàn)此接口的對象。
(5)invokedynamic:先在運(yùn)行時動態(tài)解析出調(diào)用點(diǎn)限定符所引用的方法宛逗,然后再執(zhí)行該方法坎匿。
非虛方法:不能被重寫或者說覆蓋的方法,指的是構(gòu)造方法雷激、靜態(tài)方法替蔬、私有方法和final 修飾的方法。
虛方法:則是能被重寫的方法屎暇,一般指的是實例方法承桥。
栗子:
class Demo01{
???public void method1(){
???????System.out.println("This is father non-static");
???}
???public static void method2(){
???????System.out.println("This is father static");
???}
}
public class Demo02 extends Demo01{
???public void method1(){
???????System.out.println("This is son non-static");
???}
???public static void method2(){
???????System.out.println("This is son static");
???}
???public static void main(String[] args){
???????Demo01 d1= new Demo01();
???????Demo02 d2= new Demo02();
???????Demo01 d3= new Demo02(); //父類引用指向子類對象
???????d1.method1();
???????d1.method2();
???????d2.method1();
???????d2.method2();
???????d3.method1();
???????d3.method2();
??? }
}
運(yùn)行結(jié)果:
對于這樣的運(yùn)行結(jié)果前5行應(yīng)該沒什么疑問,用常規(guī)的思維就能理解根悼,可是最后一條凶异,what?說好的動態(tài)綁定呢,Are you kidding
me挤巡?No,這里沒有發(fā)生動態(tài)綁定了剩彬,問題又來了,為什么靜態(tài)方法不發(fā)生動態(tài)綁定矿卑?動態(tài)綁定到底發(fā)生了什么喉恋?簡直是頭腦風(fēng)暴,說實話我也是蒙蒙的,接下來可能正確也可能不正確轻黑,但是八九不離十糊肤。
分析
首先看看上面main 方法的字節(jié)碼:
?// access flags 0x9
? public staticmain([Ljava/lang/String;)V
?? L0
??? LINENUMBER 19 L0
???NEW com/learn/pra06/Demo01
???DUP
???INVOKESPECIAL com/learn/pra06/Demo01. ()V
???ASTORE 1
??L1
???LINENUMBER 20 L1
???NEW com/learn/pra06/Demo02
???DUP
???INVOKESPECIAL com/learn/pra06/Demo02. ()V
???ASTORE 2
??L2
???LINENUMBER 21 L2
???NEW com/learn/pra06/Demo02
???DUP
???INVOKESPECIAL com/learn/pra06/Demo02. ()V
???ASTORE 3
??L3
???LINENUMBER 22 L3
???ALOAD 1
???INVOKEVIRTUAL com/learn/pra06/Demo01.method1 ()V
??L4
???LINENUMBER 23 L4
???INVOKESTATIC com/learn/pra06/Demo01.method2 ()V
??L5
???LINENUMBER 24 L5
???ALOAD 2
???INVOKEVIRTUAL com/learn/pra06/Demo02.method1 ()V
??L6
???LINENUMBER 25 L6
???INVOKESTATIC com/learn/pra06/Demo02.method2 ()V
??L7
???LINENUMBER 26 L7
???ALOAD 3
???INVOKEVIRTUAL com/learn/pra06/Demo01.method1 ()V
??L8
???LINENUMBER 27 L8
???INVOKESTATIC com/learn/pra06/Demo01.method2 ()V
??L9
???LINENUMBER 28 L9
???RETURN
??L10
L+number 對應(yīng)就是main方法體中每一行,我們可以清晰的看見代碼執(zhí)行的指令苔悦,簡直大愛轩褐,有種相見恨晚的趕腳。
Demo01 d1= new Demo01();這個語句將會在運(yùn)行期發(fā)生什么呢玖详?結(jié)合我們前期準(zhǔn)備學(xué)的那幾個指令集把介。查看以上的字節(jié)碼發(fā)現(xiàn):INVOKESPECIAL
com/learn/pra06/Demo01.<init> ()V 請問將會調(diào)用Demo01的構(gòu)造函數(shù),這個毋庸置疑蟋座。同理L1也是如此拗踢。
Demo01 d3= new Demo02();雖然聲明類型為父類,但實際new的時候是子類向臀,同樣字節(jié)碼也對應(yīng)如此巢墅。INVOKESPECIALcom/learn/pra06/Demo02. ()V
d1.method1();這個語句是應(yīng)該是對象調(diào)用其實例方法,字節(jié)碼也很好說明了這一點(diǎn):INVOKEVIRTUAL
com/learn/pra06/Demo01.method1 ()V此處用了INVOKEVIRTUAL券膀,則代表調(diào)用虛方法君纫,并且此方法的引用存在方法表中(這個待會再說),只用INVOKEVIRTUAL指令會去方法表尋找要調(diào)用方法的引用芹彬。
d1.method2();這句是對象調(diào)用靜態(tài)方法蓄髓,字節(jié)碼為:INVOKESTATIC com/learn/pra06/Demo01.method2 ()V此方法則是直接調(diào)用方法區(qū)中靜態(tài)方法,無需經(jīng)過方法表舒帮,這也就解釋了靜態(tài)方法的執(zhí)行只看靜態(tài)類型会喝,而與實際類型無關(guān),又因為重寫的方法調(diào)用看的是實際類型玩郊,所以靜態(tài)方法不能被重寫肢执。d2的兩個方法調(diào)用解釋與d1相同。
重點(diǎn)是d3方法的調(diào)用過程译红,d3.method1();字節(jié)碼:INVOKEVIRTUAL
com/learn/pra06/Demo01.method1 ()V運(yùn)用了INVOKEVIRTUAL指令预茄,說明運(yùn)行期間會到方法表中去調(diào)用真實指向的方法,因為method01可能被重寫侦厚,所以編譯器期間標(biāo)明運(yùn)行時應(yīng)調(diào)用method1所在的方法表中位置存的真正方法的引用反璃。因為method01方法被Demo02重寫,所以方法表中原先存父類method01方法的引用被改寫成子類的method01方法的引用假夺,所以在運(yùn)行時根據(jù)INVOKEVIRTUAL指令找到的method01的方法是子類的淮蜈。
那么d3.method2();通過查看字節(jié)碼發(fā)現(xiàn):INVOKESTATIC com/learn/pra06/Demo01.method2 ()V用到INVOKESTATIC ,不能訪問方法表已卷,而是直接訪問的父類的method2的梧田,所以運(yùn)行時調(diào)用就是父類的靜態(tài)方法。
編譯時把對象的靜態(tài)類型(聲明類型)作為該方法的接受者。運(yùn)行時則根據(jù)指令集再進(jìn)行更改裁眯。
INVOKEVIRTUAL指令流程:
package com.learn.pra06;
public class ClassReference {
???static class Person {
???????@Override
???????public String toString(){
??????????? return "I'm a person.";
???????}
???????public void eat(){
??????????? System.out.println("Personeat");
???????}
???????public void speak(){
??????????? System.out.println("Personspeak");
???????}
? ?}
???static class Boy extends Person{
???????@Override
???????public String toString(){
??????????? return "I'm a boy";
???????}
???????@Override
???????public void speak(){
??????????? System.out.println("Boyspeak");
???????}
???????public void fight(){
??????????? System.out.println("Boyfight");
???????}
???}
???static class Girl extends Person{
???????@Override
???????public String toString(){
??????????? return "I'm a girl";
???????}
???????@Override
???????public void speak(){
??????????? System.out.println("Girlspeak");
???????}
???????public void sing(){
??????????? System.out.println("Girlsing");
???????}
???}
???public static void main(String[] args) {
???????Person boy = new Boy();
???????Person girl = new Girl();
???????System.out.println(boy);
???????boy.eat();
???????boy.speak();
???????System.out.println(girl);
???????girl.eat();
???????girl.speak();
???}
}
執(zhí)行結(jié)果:
由于Boy 和Girl 沒有重寫父類Person eat方法鹉梨,所以會調(diào)用父類的eat方法。
字節(jié)碼:
public static main([Ljava/lang/String;)V
??L0
???LINENUMBER 47 L0
??? NEW com/learn/pra06/ClassReference$Boy
???DUP
???INVOKESPECIAL com/learn/pra06/ClassReference$Boy. ()V
???ASTORE 1
??L1
???LINENUMBER 48 L1
???NEW com/learn/pra06/ClassReference$Girl
???DUP
???INVOKESPECIAL com/learn/pra06/ClassReference$Girl. ()V
???ASTORE 2
??L2
???LINENUMBER 49 L2
???GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
???ALOAD 1
???INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
??L3
???LINENUMBER 50 L3
???ALOAD 1
???INVOKEVIRTUAL com/learn/pra06/ClassReference$Person.eat ()V
??L4
???LINENUMBER 51 L4
???ALOAD 1
???INVOKEVIRTUAL com/learn/pra06/ClassReference$Person.speak ()V
??L5
???LINENUMBER 53 L5
???GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
???ALOAD 2
???INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/Object;)V
??L6
???LINENUMBER 54 L6
???ALOAD 2
???INVOKEVIRTUAL com/learn/pra06/ClassReference$Person.eat ()V
??L7
???LINENUMBER 55 L7
???ALOAD 2
???INVOKEVIRTUAL com/learn/pra06/ClassReference$Person.speak ()V
??L8
???LINENUMBER 57 L8
???RETURN
??L9
? 很明顯在L2處穿稳,編譯器會將根類Object的toString 方法的引用寫入class文件存皂,說明編譯器會將祖先的方法引用寫入,而非近親逢艘。
L3,L4,L6,L7都用到了INVOKEVIRTUAL旦袋,到底流程是怎么樣的呢?
首先看看方法表在內(nèi)存的模型:
通過看Girl和Boy方法表可以看出繼承的方法從頭到尾開始排列它改,并且方法引用在子類的中都有固定索引疤孕,即都有相同的偏移量;若子類重寫父類某個方法央拖,就會使子類方法表原先存父類的方法引用變成重寫后方法的引用祭阀,到這就應(yīng)該理解為什么可以根據(jù)對象類型而調(diào)用到正確的方法,關(guān)鍵就在于方法表鲜戒。
下面以girl.speak為例专控,看看INVOKEVIRTUAL指令流程。
解說圖:
1. 首先INVOKEVIRTUAL
com/learn/pra06/ClassReferencePerson.speak()V中遏餐,根據(jù)com/learn/pra06/ClassReferencePerson.speak ()V在常量池中找到該方法的偏移量
2. 查看Person的方法表伦腐,得到speak方法在該方法表的偏移量(假設(shè)為15),這樣就得到該方法的直接引用境输。
3. 根據(jù)this判斷出該引用指的是Girl實例
4. 然后去找Girl實例的方法表,根據(jù)上面的偏移量在方法表中找到該方法引用颖系,因為該方法引用的值在類加載根據(jù)是否重寫了方法已經(jīng)確定了正確的方法引用嗅剖,所以我們這里就可以直接調(diào)用該方法。
為什么靜態(tài)方法不能隱藏實例方法嘁扼?
靜態(tài)方法的調(diào)用的是通過在編譯器靜態(tài)綁定的信粮,而實例方法的調(diào)用是在運(yùn)行時動態(tài)綁定的,2者的調(diào)用的方式不同趁啸,所以二者只能存在其一强缘,否則會存在歧義!
總結(jié)
總體流程就是:編譯器將類編譯成class文件不傅,其中方法會根據(jù)靜態(tài)類型從而將對應(yīng)的方法引用寫入class中旅掂,運(yùn)行時,JVM會根據(jù)INVOKEVIRTUAL 所指向的方法引用在常量池找到該方法的偏移量访娶,再根據(jù)this找到引用類型真實指向的對象商虐,訪問這個對象類型的方法表,根據(jù)偏移量找出存放目標(biāo)方法引用的位置,取出這個引用秘车,調(diào)用這個引用實際指向的方法典勇,完成多態(tài)!
3叮趴、接口和抽象類
抽象類:
要注意一個問題:在《JAVA編程思想》一書中割笙,將抽象類定義為“包含抽象方法的類”,但是后面發(fā)現(xiàn)如果一個類不包含抽象方法眯亦,只是用abstract修飾的話也是抽象類伤溉。也就是說抽象類不一定必須含有抽象方法。個人覺得這個屬于鉆牛角尖的問題吧搔驼,因為如果一個抽象類不包含任何抽象方法谈火,為何還要設(shè)計為抽象類?所以暫且記住這個概念吧舌涨,不必去深究為什么糯耍。
? ? 包含抽象方法的類稱為抽象類,但并不意味著抽象類中只能有抽象方法囊嘉,它和普通類一樣温技,同樣可以擁有成員變量和普通的成員方法。注意扭粱,抽象類和普通類的主要有三點(diǎn)區(qū)別:
1)抽象方法必須為public或者protected(因為如果為private舵鳞,則不能被子類繼承,子類便無法實現(xiàn)該方法)琢蛤,缺省情況下默認(rèn)為public蜓堕。
2)抽象類不能用來創(chuàng)建對象;
3)如果一個類繼承于一個抽象類博其,則子類必須實現(xiàn)父類的抽象方法套才。如果子類沒有實現(xiàn)父類的抽象方法,則必須將子類也定義為為abstract類慕淡。
? ? 抽象類大多用于抽取相關(guān)Java類的共用方法實現(xiàn)或者是共同成員變量背伴,然后通過繼承的方式達(dá)到代碼復(fù)用的目的。
接口:
??? 接口是對行為的抽象峰髓,它是抽象方法的集合傻寂,利用接口可以達(dá)到API定義和實現(xiàn)分離的目的。
? ? 接口不能實例化携兵;
? ? 接口中可以含有變量和方法疾掰。但是要注意,接口中的變量會被隱式地指定為public static final變量(并且只能是public static final變量徐紧,用private修飾會報編譯錯誤)个绍,而方法會被隱式地指定為public abstract方法且只能是public abstract方法(用其他關(guān)鍵字勒葱,比如private、protected巴柿、static凛虽、final等修飾會報編譯錯誤)。
? ? Java8在接口中引入了默認(rèn)方法广恢,通過在方法前加上default關(guān)鍵字就可以在接口中寫方法的默認(rèn)實現(xiàn)凯旋。這是因為不支持默認(rèn)方法的接口的維護(hù)成本太高了。在 Java 8 之前钉迷,如果一個接口想要添加新的方法至非,那么要修改所有實現(xiàn)了該接口的類。
兩者的區(qū)別:
語法層面上的區(qū)別:
1)抽象類可以提供成員方法的實現(xiàn)細(xì)節(jié)糠聪,而接口中只能存在public abstract方法荒椭;???? ---在java8中接口已經(jīng)支持默認(rèn)方法
2)抽象類中的成員變量可以是各種類型的,而接口中的成員變量只能是public static final類型的舰蟆;
3)接口中不能含有靜態(tài)代碼塊以及靜態(tài)方法趣惠,而抽象類可以有靜態(tài)代碼塊和靜態(tài)方法;
4)一個類只能繼承一個抽象類身害,使用extends關(guān)鍵字味悄;而一個類卻可以實現(xiàn)多個接口,使用implements關(guān)鍵字塌鸯。
設(shè)計層面上的區(qū)別
??? 1)抽象類是對一種事物的抽象侍瑟,即對類抽象,而接口是對行為的抽象丙猬。抽象類是對整個類整體進(jìn)行抽象涨颜,包括屬性、行為茧球,但是接口卻是對類局部(行為)進(jìn)行抽象庭瑰。
??? 2)設(shè)計層面不同,抽象類作為很多子類的父類袜腥,它是一種模板式設(shè)計见擦。而接口是一種行為規(guī)范钉汗,它是一種輻射式設(shè)計羹令。
??? 3)抽象類提供了一種 IS-A 關(guān)系,那么就必須滿足里式替換原則损痰,即子類對象必須能夠替換掉所有父類對象福侈。而接口更像是一種 LIKE-A 關(guān)系,它只是提供一種方法實現(xiàn)契約卢未,并不要求接口和實現(xiàn)接口的類具有 IS-A 關(guān)系肪凛。
使用選擇:
使用接口:
需要讓不相關(guān)的類都實現(xiàn)一個方法堰汉,例如不相關(guān)的類都可以實現(xiàn) Compareable 接口中的 compareTo() 方法;
需要使用多重繼承伟墙。
使用抽象類:
需要在幾個相關(guān)的類中共享代碼翘鸭。
需要能控制繼承來的成員的訪問權(quán)限,而不是都為 public戳葵。
需要繼承非靜態(tài)和非常量字段就乓。
在很多情況下,接口優(yōu)先于抽象類拱烁。因為接口沒有抽象類嚴(yán)格的類層次結(jié)構(gòu)要求生蚁,可以靈活地為一個類添加行為。并且從 Java 8 開始戏自,接口也可以有默認(rèn)的方法實現(xiàn)邦投,使得修改接口的成本也變的很低。
4擅笔、內(nèi)部類
廣泛意義上的內(nèi)部類一般來說包括這四種:成員內(nèi)部類志衣、靜態(tài)內(nèi)部類、局部內(nèi)部類和匿名內(nèi)部類剂娄。
? ? ? 成員內(nèi)部類的定義為位于另一個類的內(nèi)部蠢涝,作為另一個類的成員:
1)成員內(nèi)部類可以無條件訪問外部類的所有成員屬性和成員方法(包括private成員和靜態(tài)成員)。
2)當(dāng)成員內(nèi)部類擁有和外部類同名的成員變量或者方法時阅懦,會發(fā)生隱藏現(xiàn)象和二,即默認(rèn)情況下訪問的是成員內(nèi)部類的成員。如果要訪問外部類的同名成員耳胎,需要以下面的形式進(jìn)行訪問:
? ??外部類.this.成員變量
??? 外部類.this.成員方法
3)在外部類中如果要訪問成員內(nèi)部類的成員惯吕,必須先創(chuàng)建一個成員內(nèi)部類的對象,再通過指向這個對象的引用來訪問怕午,而創(chuàng)建成員內(nèi)部類的對象废登,前提是必須存在一個外部類的對象:
??? Outter outter = new Outter();
??? Outter.Inner inner = outter.newInner();? //必須通過Outter對象來創(chuàng)建
? ? inner.成員變量
??? inner.成員方法
4)內(nèi)部類可以擁有private訪問權(quán)限、protected訪問權(quán)限郁惜、public訪問權(quán)限及包訪問權(quán)限堡距。
? ? ? 靜態(tài)內(nèi)部類也是定義在另一個類里面的類,只不過在類的前面多了一個關(guān)鍵字static兆蕉。靜態(tài)內(nèi)部類是不需要依賴于外部類的羽戒,這點(diǎn)和類的靜態(tài)成員屬性有點(diǎn)類似,并且它不能使用外部類的非static成員變量或者方法虎韵。
?????? Outter.Inner inner = new Outter.Inner();
??? 局部內(nèi)部類是定義在一個方法或者一個作用域里面的類易稠,它和成員內(nèi)部類的區(qū)別在于局部內(nèi)部類的訪問僅限于方法內(nèi)或者該作用域內(nèi)。
??? 局部內(nèi)部類就像是方法里面的一個局部變量一樣包蓝,是不能有public驶社、protected企量、private以及static修飾符的。
??? 匿名內(nèi)部類基本語法:
??? 一個匿名類由以下幾個部分組成:
??? 1)new操作符
?? ?2)Runnable:接口或類名稱亡电。這里還可以填寫抽象類届巩、普通類的名稱。
?? ?3)():這個括號表示構(gòu)造函數(shù)的參數(shù)列表份乒。由于Runnable是一個接口姆泻,沒有構(gòu)造函數(shù),所以這里填一個空的括號表示沒有參數(shù)冒嫡。
?? ?4){...}:大括號中間的代碼表示這個類內(nèi)部的一些結(jié)構(gòu)拇勃。在這里可以定義變量名稱、方法孝凌。跟普通的類一樣方咆。
??? 例如在多線程實現(xiàn)Runnable接口時經(jīng)常使用:
?public static void main(String[] args) throwsInterruptedException?
?{
??????for (int i = 0; i < 4; i++) {
???????????? service.execute(new Runnable() {
??????????????? @Override
??????????????? public void run() {
??????????????????? ......
??????????????? }
???????????? }
????????? }
?}