on java 8 学习笔记 2022.2.17-2022.2.18
2022.2.17
問題
正如你在第8章會看到的,當引入繼承時,通過繼承而來的類(子類)可以訪問父類的protected成員以及public成員(但不能訪問private成員)。只有當兩個類在同一個包中時,它才可以訪問父類的包訪問權限成員。但現在不必擔心繼承和protected。7.2.1
沒看懂這句話想表達什么
答:意思很簡單,就是強調默認包的訪問權限
然而,僅僅因為一個對象的引用在類中是private的,并不意味著其他對象不能擁有對同一個對象的public引用。(請參閱進階卷第2章了解別名問題。)7.2.3
這句話我也沒看懂
第七章 實現隱藏
對于僅用于實現類但不提供給客戶程序員直接使用的方法也是如此。
這里的方法對應的應該是我們的私有方法
當編譯一個.java文件時,文件中的每個類都會有一個輸出文件。輸出文件的名字就是其在.java文件中對應的類的名字,但擴展名為.class。因此,你可以從少量的.java文件中得到相當多的.class文件。如果使用編譯型語言寫過程序,你可能習慣于編譯器輸出一個中間形式(通常是obj文件),然后使用鏈接器(linker)或庫生成器(librarian,用來創建庫)將它與其他同類文件打包在一起,以創建一個可執行文件。Java不是這樣的。在Java中一個可運行程序就是一堆.class文件,可以使用jar歸檔器將它們打包并壓縮成一個Java檔案文件(JAR)。Java解釋器負責查找、加載和解釋這些文件。
這種寫法可以讓你直接調用靜態方法
fun()而不用寫成這樣
類名.方法名(),挺奇妙的,protected的使用居然是為了在包外可以使用繼承的基類的方法,這確實讓我挺意外的
不是public的類的引用,如果在包外使用了,編譯器是找不到的
還有protected修飾符不能用于類
protected class one {protected void f(){System.out.println("hello");} }這么寫編譯不過
有時候基類的創建者想要把特定成員的訪問權限賦給子類,而不是所有的類,這時候protected就可以發揮作用了。protected還提供了包訪問權,也就是說,同一包中的其他類也可以訪問protected元素。
如果你回顧文件Cookie.java,就會知道下面的類不能調用包訪問權限成員bite():
// hiding/ChocolateChip.java // 無法在另一個包里調用包訪問權限的成員 import hiding.dessert.*;public class ChocolateChip extends Cookie {public ChocolateChip() {System.out.println("ChocolateChip constructor");}public void chomp() {//- bite(); // 無法訪問bite}public static void main(String[] args) {ChocolateChip x = new ChocolateChip();x.chomp();} } /* 輸出: Cookie constructor ChocolateChip constructor */如果類Cookie中存在一個方法bite(),那么這個方法也存在于任何繼承Cookie的類中。但是bite()只具有包訪問權限并且位于另一個包中,因此無法在當前包中使用它。你可以將其修改為public,但這樣的話每個人就都可以訪問它了,這也許不是你想要的。如果按如下方式更改類Cookie:
// hiding/cookie2/Cookie.java package hiding.cookie2;public class Cookie {public Cookie() {System.out.println("Cookie constructor");}protected void bite() {System.out.println("bite");} }這樣任何繼承Cookie的類都可以訪問bite():
// hiding/ChocolateChip2.java import hiding.cookie2.*;public class ChocolateChip2 extends Cookie {public ChocolateChip2() {System.out.println("ChocolateChip2 constructor");}public void chomp() { bite(); } // protected方法public static void main(String[] args) {ChocolateChip2 x = new ChocolateChip2();x.chomp();} } /* 輸出: Cookie constructor ChocolateChip2 constructor bite */這時盡管bite()也有包訪問權限,但它不是public的。
總結一下,如果將不同的包看作使用者,那么public就是公車.如果將類看作使用者的話,那么默認類就是公車,默認類至少保證了同一個包中的引用還是可用的,不至于無法創建引用.(注意我這里沒提構造器的事)
然后,變量的訪問權限大于類的訪問權限沒有意義
請注意,類不能是private(這將使除該類之外的任何類都無法訪問它)或protected的5。因此,對于類訪問權限,只有兩種選擇:包訪問權限和public。如果想要防止對該類的訪問,可以將其所有的構造器都設為private,從而禁止其他人創建該類的對象,而你則可以在這個類的靜態方法中創建對象:
5實際上,內部類可以是private的或protected的,但這是特殊情況。這些主題在第11章中會介紹。
默認變量只在同一個包中可以隨便用,也就是它是同一個包下類的公車
package example;public class one {int i=9;public static void main(String[] args){System.out.println(new two().i);} } class two {int i=0; } package Test;import example.one;public class test {public static void main(String[] args){one a = new one();System.out.println(a.i);}}你會發現下面的代碼編譯通不過
注意,訪問權限控制側重于庫開發者和該庫的外部客戶之間的關系,這也是一種通信方式。不過有很多情況并非如此。例如,你自己編寫所有的代碼,或者你與一個小團隊密切合作,而且所有的內容都放在同一個包中。這些情況是另一種不同的通信方式,嚴格遵守訪問權限規則可能不是最佳選擇。默認的(包)訪問權限可能就夠用了。
第八章 復用
初始化引用有下列4種方式。
以下是這4種方式的示例:
// reuse/Bath.java // 使用組合進行構造器初始化class Soap {private String s;Soap() {System.out.println("Soap()");s = "Constructed";}@Override public String toString() { return s; } }public class Bath {private String // 在定義時初始化s1 = "Happy",s2 = "Happy",s3, s4;private Soap castile;private int i;private float toy;public Bath() {System.out.println("Inside Bath()");s3 = "Joy";toy = 3.14f;castile = new Soap();}// 實例初始化{ i = 47; }@Override public String toString() {if(s4 == null) // 延遲初始化s4 = "Joy";return"s1 = " + s1 + "\n" +"s2 = " + s2 + "\n" +"s3 = " + s3 + "\n" +"s4 = " + s4 + "\n" +"i = " + i + "\n" +"toy = " + toy + "\n" +"castile = " + castile;}public static void main(String[] args) {Bath b = new Bath();System.out.println(b);} } /* 輸出: Inside Bath() Soap() s1 = Happy s2 = Happy s3 = Joy s4 = Joy i = 47 toy = 3.14 castile = Constructed */?
如果省略訪問權限修飾符,則該成員的權限默認是包訪問權限,僅允許包內的成員進行訪問。因此,在這個包內,如果沒有訪問權限修飾符,任何人都可以使用這些方法。例如Detergent就沒有問題。但是,如果來自其他包的類要繼承Cleanser,那它就只能訪問public成員。因此,考慮到繼承,作為一般規則,應該將所有字段設為private,將所有方法設為public(稍后你將學到,protected成員也允許子類訪問)。在特定情況下,你必須進行調整,但一般來說這是一個有用的指導方針。
注意下,protected修飾符是專門用來針對public修飾符的,默認修飾符對他影響不大
public class test extends two {public static void main(String[] args){one a = new one();System.out.println(a.i);two b = new two();} } package example;public class one {int i=9;public static void main(String[] args){System.out.println(new two().i);} } class two {int i=0;protected void f(){System.out.println("hello");} }會顯示example.two在example中不是公共的; 無法從外部程序包中對其進行訪問,還有默認變量無法在包外訪問,還有這里光是導入two這個類就會報錯
package Test;import example.one;public class test extends one {public static void main(String[] args){one a = new one();new test().f();}} package example;public class one {int i=9;public static void main(String[] args){System.out.println(new two().i);}protected void f(){System.out.println("hello");} } class two {int i=0;protected void f(){System.out.println("hello");} }正如在scrub()中看到的那樣,可以使用基類中定義的方法并對其進行修改。在這個示例中,你可能想從新版本的方法里調用繼承來的基類方法。但是在scrub()中不能簡單地調用scrub(),因為這會產生遞歸調用。為了解決這個問題,Java提供了super關鍵字,來指代當前類繼承的“超類”(基類)。因此,表達式super.scrub()調用了基類版本的scrub()方法。
這里會遞歸調用主要是因為會寫成這樣
f(){f() }如果不是的話,其實沒必要,不過把這個堵死了也好,畢竟可能產生歧義
現在涉及兩個類:基類和子類。想象一下子類產生的對象,這可能會令人困惑。從外部看,新類與基類具有相同的接口,或許還有一些額外的方法和字段。但是繼承不只是復制基類的接口這么簡單。當創建子類對象時,它里面包含了一個基類的子對象(subobject)。這個子對象與直接通過基類創建的對象是一樣的。只是從外面看,基類的子對象被包裹在了子類的對象中。
對基類構造器的調用必須是子類構造器的第一個操作
看了半天他這個代碼,終于看懂了他的意思,就是要隱藏一個類,定義的public實際上就類似于一個空殼子,里面private的對象才是實際上掌控方法的,就怎么說呢,這個寫的是邏輯吧,境界太高,只能理解到這了
其實有那么一點感覺,委托模式好像是代理模式的前身,雖然兩者差別很大.
沒看懂8.4.1,我尋思著,你這是要按照棧的順序清理是嗎?看著太像入棧和出棧了
其他
現在想想,其實構造器作為一種靜態方法確實很神奇,它居然可以訪問到非靜態的變量,現在對構造器的理解應該是一種特殊的方法,因為它能夠修改類的private變量,會導致一定程度的封裝破壞,所以對它的使用就比較嚴格
而轉過來想一想,也正是因為構造器實際上是一種靜態方法,所以才可以在構造器方法里調用另一個構造器方法,雖然不能同時調用兩個構造器,且構造器必須出現在構造器方法的最上面
public class test {int i ,j,m,n;public test(int i){this.i=i;}public test(int i,int j,int m){this(i);this.m=m;this.j=j;}public test(int i,int j){this(i);this.j=j;}void f(){System.out.println("i="+i+"j="+j+"m="+m);}public static void main(String[] args){new test(5,3,2).f();} }這里的this我側重于理解this方法,this靜態方法.其實感覺如果理解了靜態方法的概念,也就理解this()方法的精髓了,這個構造器方法確實巧妙
雖然靜態方法可以產生內部類,不過這個內部類外部是不用想著訪問了
public class test {public static void f(){class a {public a(){System.out.println("hello");}}new a();}public static void main(String[] args){a one = new a();test.f();} }上面的代碼會顯示找不到對象a,所以即便我將a的構造器作為了public對象,外界也別想訪問這個對象,不是因為初始化做不到,而是找不到這個類
2022.2.18
第八章 復用
組合和繼承都會將子對象放置在新類中(組合是顯式執行此操作,而繼承是隱式執行)。你可能想知道兩者之間的區別,以及如何在兩者之間做出選擇。
當希望在新類中使用現有類的功能而不是其接口時,應該使用組合。也就是說,在新類中嵌入一個對象(通常是private)來實現自己的特性。新類的用戶看到的是新類定義的接口,而不是嵌入對象的接口。
對于新類里通過組合得到的成員,有時候允許類的使用者直接訪問它們是合理的。為此,可以將成員對象設為public(你可以將其視為一種“半委托”)。成員對象隱藏自己的實現,所以這種做法是安全的。當用戶了解到你正在組裝一堆組件時,會更容易理解你的接口。car對象就是一個很好的例子:
// reuse/Car.java // 使用公共對象來實現組合class Engine { public void start() {} public void rev() {} public void stop() {} }class Wheel { public void inflate(int psi) {} }class Window { public void rollup() {} public void rolldown() {} }class Door { public Window window = new Window(); public void open() {} public void close() {} }public class Car { public Engine engine = new Engine(); public Wheel[] wheel = new Wheel[4]; public Doorleft = new Door(),right = new Door(); // 雙門車 public Car() {for(int i = 0; i < 4; i++)wheel[i] = new Wheel(); } public static void main(String[] args) {Car car = new Car();car.left.window.rollup();car.wheel[0].inflate(72); } }很有意思的一種組合方式,用public確實可以讓人更加清晰的了解
還有這里學習一下調用基類的toString();
常量之所以有用,有兩個原因:
對編譯時常量來說,編譯器可以將常量值“折疊”到計算中;也就是說,計算可以在編譯時進行,這節省了一些運行時開銷。在Java里,這些常量必須是基本類型,并用final關鍵字表示。在定義常量時必須提供一個值。
當final關鍵字與對象引用而非基本類型一起使用時,其含義可能會令人困惑。對于基本類型,final使其值恒定不變,但對于對象引用,final使引用恒定不變。一旦引用被初始化為一個對象,它就永遠不能被更改為指向另一個對象了。但是,對象本身是可以修改的。Java沒有提供使對象恒定不變的方法。(但是,你可以編寫類,使對象具有恒定不變的效果。)這個限制同樣適用于數組,它們也是對象。
最后輸出的是other不過要在是s1前加this,不然就是second
不加s1,就不是two這個對象的字段被賦值,而是s1這個變量被賦值
“重寫”只有在方法是基類接口的一部分時才會發生。也就是說,必須能將一個對象向上轉型為其基類類型并能調用與其相同的方法(下一章中你會更理解這一點)。如果一個方法是private的,它就不是基類接口的一部分。它只是隱藏在類中的代碼,只不過恰好具有相同的名稱而已。即使在子類中創建了具有相同名稱的public、protected或包訪問權限的方法,它與基類中這個相同名稱的方法也沒有任何聯系。你并沒有重寫該方法,只不過是創建了一個新的方法。private方法是不可訪問的,并且可以有效地隱藏自己,因此除了定義它的類的代碼組織之外,它不會影響任何事情。
final類的字段可以是final,也可以不是,根據個人選擇而定。無論類是否定義為final,相同的規則都適用于字段的final定義。然而,由于final類禁止繼承,它的所有方法都是隱式final的,因為無法重寫它們。你可以在final類的方法中包含final修飾符,但它不會添加任何意義。
首先final類本身不能繼承,所以即便字段是public的,也不能用子類進行訪問
但是一個public的final類并不會禁止包外的類除了自己的子類外訪問自己public字段
package Test;import example.a; import example.one; import example.*; public class test extends two {public static void main(String[] args){new one().s1 = "heool";} } package example; public final class one {public String s1 = "hello"; }當你運行java Beetle時,首先會嘗試訪問靜態方法Beetle.main(),所以加載器會去Beetle.class文件中找到Beetle類的編譯代碼。在加載它的代碼時,加載器注意到有一個基類,然后它就會去加載基類。無論是否創建該基類的對象,都會發生這種情況。(可以嘗試注釋掉對象創建來驗證一下。)
如果基類又有自己的基類,那么第二個基類也將被加載,以此類推。接下來,會執行根基類(本例中為Insect)中的靜態初始化,然后是下一個子類,以此類推。這很重要,因為子類的靜態初始化可能依賴于基類成員的正確初始化。
現在所有必要的類都已加載,因此可以創建對象了。首先,該對象中的所有基本類型都被設為其默認值,并且對象引用被設為null——這通過將對象中的內存設置為二進制零來一步實現。然后調用基類構造器。這里的調用是自動的,但也可以通過super關鍵字來指定基類構造器的調用(需要作為Beetle構造器中的第一個操作)。基類構造器以與子類構造器相同的順序經歷相同的過程。基類構造器完成后,子類的實例變量按文本順序初始化。最后,執行子類構造器的其余部分。
第九章 多態
將一個方法調用和一個方法體關聯起來的動作稱為綁定。在程序運行之前執行綁定(如果存在編譯器和鏈接器的話,由它們來實現),稱為前期綁定。你之前可能沒有聽說過這個術語,因為在面向過程語言中默認就是前期綁定的。例如,在C語言中只有一種方法調用,那就是前期綁定。
上述程序之所以令人困惑,主要是由于前期綁定。這是因為當編譯器只有一個Instrument引用時,它無法知道哪個才是要調用的正確方法。
解決這個問題的方案稱為后期綁定,這意味著綁定發生在運行時,并基于對象的類型。后期綁定也稱為動態綁定或運行時綁定。當一種語言實現后期綁定時,必須有某種機制在運行時來確定對象的類型,并調用恰當的方法。也就是說,編譯器仍然不知道對象的類型,但方法調用機制能找到并調用正確的方法體。后期綁定機制因語言而異,但可以想象,必須要將某種類型信息放在對象里。
這里前期調用需要理解的最重要的一句話就是方法調用和方法體是緊密一對一的鏈接的,下面舉一個例子
f()->方法diaoyonf(){//方法體 int i =0; }然后前期綁定所無法做道的就是,無法通過一個方法名去定位有多個方法體的目標,學過c語言的就會感覺一個方法體可以有多個目標本身就很離譜
下面我串一下c語言的函數,學過c語言的應該都知道函數指針這種東西,它的解引用其實就是方法體的位置,那么其實可以很容易的推斷出來,一個指針變量所能指向的位置是唯一確定的,但問題來了,如果指針指向的方法存在多種形式(也就是多種同名方法),那么每個形式的方法體占的地址肯定也是不同的,而這就是蛋疼的地方了,這種特性導致了它能實現重載的可能性直接變0.
Java中的所有方法綁定都是后期綁定,除非方法是static或final的(private方法隱式為final)。
static的方法其實還是有c語言的遺風的,然后final方法我不是很能理解為什么可以不是后期綁定機制
final方法可能原因是不能進行重寫吧,重載是編譯時的多態,重寫才是運行時多態的表現
當Sub對象向上轉型為Super引用時,任何字段訪問都會被編譯器解析,因此不是多態的。在此示例中,Super.field和Sub.field被分配了不同的存儲空間。因此,Sub實際上包含兩個被稱為field的字段:它自己的字段和它從Super繼承的字段。然而,當你在Sub中引用field時,Super版本并不是默認的那個。要獲得Super的field,必須明確地說super.field。
如果要調用父類的字段,要顯示用super調用,當然,如果時private字段,你用super也沒用
這里層次結構中的每個類都包含類型為Characteristic和Description的成員對象,它們也必須被銷毀。處置順序應該與初始化順序相反,以防子對象依賴于其他對象。對于字段,這意味著與聲明順序相反(因為字段是按聲明順序初始化的)。對于基類(遵循C++中析構函數的形式),首先執行子類清理,然后是基類清理。這是因為子類在清理時可能會調用基類中的一些方法,這些方法可能需要基類組件處于存活狀態,因此不能過早地銷毀它們。輸出顯示了Frog對象的所有部分都是按照與創建順序相反的順序進行銷毀的。
有點能夠理解為什么要以相反的順序清理了,就是為了防止某些對象是和其他對象有關聯的,比如基類子類,刪了基類子類就殘疾了
這個其實是一件特別奇妙的事情,我用c的角度來審視這個問題就是,在創建一個對象時,該對象的全部信息應該存儲在堆中的一塊區域當中,那么,本例中的兩個對象本身其實他們的draw()方法其實就是存儲在他們對應的區域中,但是,java的后期綁定導致了一個問題,在java中,構造器和方法其實是由個隱形的參數this的,用來確定對象使用的,那么這里構造子對象時,其實是在子對象的空間中進行調用,也就是說,基類構造器構造的過程中是會被傳入一個this的隱參來進行定位,也就是這個父對象其實是在這個子對象的空間當中的,這也就導致了在調用方法時,其實是隱式調用了this.fun(),而不是super.fun().
實際的初始化過程如下所示。
這里我著重理解的是初始化為二進制0,因為我學過一點匯編,也學過一點逆向,在我的理解中,c語言是直接劃定了一邊區域作為了某個變量的值,里面不一定是什么,而java這是劃定區域以后,把里面的值先設置為0.
編寫構造器時有一個很好的準則:“用盡可能少的操作使對象進入正常狀態,如果可以避免的話,請不要調用此類中的任何其他方法。”只有基類中的final方法可以在構造器中安全調用(這也適用于private方法,它們默認就是final的)。這些方法不能被重寫,因此不會產生這種令人驚訝的問題。
Java 5添加了協變返回類型(covariant return type),這表示子類中重寫方法的返回值可以是基類方法返回值的子類型:
// polymorphism/CovariantReturn.javaclass Grain { @Override public String toString() {return "Grain"; } }class Wheat extends Grain { @Override public String toString() {return "Wheat"; } }class Mill { Grain process() { return new Grain(); } }class WheatMill extends Mill { @Override Wheat process() {return new Wheat(); } }public class CovariantReturn { public static void main(String[] args) {Mill m = new Mill();Grain g = m.process();System.out.println(g);m = new WheatMill();g = m.process();System.out.println(g); } } /* 輸出: Grain Wheat */Java 5與其之前版本的主要區別在于,其之前版本強制要求process()的重寫版本返回Grain,而不能是Wheat,即使Wheat繼承自Grain,因而也是一個合法的返回類型。協變返回類型允許更具體的Wheat返回類型。
這個其實很有意思
Stage對象包含了一個Actor的引用,它被初始化為一個HappyActor對象。這意味著performPlay()方法會產生特定的行為。因為引用可以在運行時重新綁定到不同的對象,所以可以將actor中的引用替換為對SadActor對象的引用,這樣performPlay()產生的行為也隨之改變。因此你就在運行時獲得了動態靈活性(這也稱為狀態模式)。相反,你不能在運行時決定以不同的方式來繼承,這必須在編譯期間就完全確定下來。
“使用繼承來表達行為上的差異,使用字段來表達狀態上的變化”在前面的例子中,兩者都用到了:通過繼承得到了兩個不同的類來表達act()方法的差異,而Stage使用組合來允許其狀態發生改變。在這里,狀態的改變恰好導致了行為的變化。
不得不說,真是精妙的思想,繼承只是為了接口的多態性,如果不能使接口產生多態性,應該使用組合的方式來實現狀態的多樣性.
總結
以上是生活随笔為你收集整理的on java 8 学习笔记 2022.2.17-2022.2.18的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Mysql:Access denied
- 下一篇: on java8学习笔记2022.2.1