JavaSE學習筆記二
第一階段——模組二
一、類的執行流程和記憶體分析
當類被執行的時候,會載入到記憶體中的方法區。
二、可變長引數
- 格式:返回值型別 方法名(資料型別... 引數名)
- 需要放到引數列表的末尾
三、引數傳遞的注意事項
-
基本資料型別的變數作為方法的引數傳遞時,形參變數資料的改變不會影響實參變數的數值,因為兩個變數有各自獨立的記憶體空間。示例程式碼如下:
public class ArgumentTest { public void show(int a) { a = 200; System.out.println("a的值是:" + a); } public static void main(String[] args) { int b = 10; ArgumentTest at = new ArgumentTest(); at.show(b); System.out.println("b的值是:" + b); } } //a的值是:200 //b的值是:10 // 從列印的結果來看,b的值並沒有改變。所以在show方法內部,無論對形參a的值做什麼改變,都不會改變實參b的值。 // 相當於是把b的值複製了一份給形參a,然你不管怎麼改a的值都不會影響到b的值。
-
引用資料型別的變數作為方法的引數傳遞時,形參變數資料的改變會影響實參變數的數值,因為兩個變數指向同一塊記憶體空間。示例程式碼如下:
public class ArgumentTest { public void show2(int[] args) { args[0] = 200; System.out.println("show方法中:args[0] = " + args[0]); } public static void main(String[] args) { int[] arr2 = new int[]{10, 20}; at.show2(arr2); System.out.println("main方法中:args[0] = " + arr2[0]); } } //show方法中:args[0] = 200 //main方法中:args[0] = 200 //可以看到在show2方法中對陣列中的值改變之後,原始實引數組的值也發生改變了,因為他們指向同一塊記憶體空間,都在堆記憶體
四、構造方法的注意事項
-
當一個類中沒有手動定義任何構造方法時,編譯器會自動新增一個無參構造方法,叫做預設/預設構造方法。
-
如類中手動定義了構造方法,那麼編譯器則不再提供任何形式的構造方法。
五、遞迴(重點)
遞迴能簡化程式碼,但是不容易理解。當遞迴會影響程式的效能時,這時候就需要使用遞推了。
1. 費氏數列
程式設計實現費氏數列(斐波那契數列)中第n項的數值並返回
示例程式碼
/** * TODO 使用遞推的方式實現返回斐波那契列中第n項的數值 * 1, 1, 2, 3, 5, 8, 13,... * ia ib ic * @author 凱爾王 * @date 2020年9月18日 下午8:58:45 */ public class FeiBoNaQieTest { public int show(int n) { // 先讓ia指向第一個位置,即ia = 1 int ia = 1; // 再讓ib指向第二個位置,即ib = 1 int ib = 1; for (int i = 3; i <= n; i++) { // ic的值就是前兩項的值的和 int ic = ia + ib; // 然後更新ia和ib的位置,即向ia和ib分別向前移動一個位置 ia = ib; ib = ic; } return ib; } }
六、封裝
1. 封裝的概念
- 通常情況下,可以在測試類中給成員變數賦值一些合法但是不合理的數值,無論是在編譯階段還是在執行階段都不會報錯或者給出提示,此時與現實生活不符。示例如下:
public class Student {
int id;
String name;
public void show() {
System.out.println("我的名字是:" + this.name);
}
}
public class StudentTest {
public static void main(String[] args) {
Student s1 = new Student();
s1.id = 101;
// 這裡給名字的賦值就不合理了
s1.name = "sddadasdda";
}
}
- 為了避免錯誤的發生,這時候就需要對成員變數進行密封包裝處理,來隱藏成員變數的細節以及保證成員變數數值的合理性,該機制就叫做封裝。示例程式碼如下:
public class Student {
private int id;
private String name;
public void setId(int id) {
// 這裡面就可以進行合理值判斷了
this.id = id;
}
public void setName(String name) {
// 這裡面就可以進行合理值判斷了
// 如果名字的是英文,則提示使用者重新輸入。s
this.name = name;
}
public void show() {
System.out.println("我的名字是:" + this.name);
}
}
七、static關鍵字(重點)
- 使用static關鍵字修飾的成員變量表示靜態含義,此時成員變數由物件層提升為類層級,也就是整個類只有一份並被所有物件共享。
- 該成員變數隨著類的載入準備就緒,與是否建立物件無關。
- 靜態變數存在於記憶體中的方法區
1. 使用方式
- 在非靜態成員方法中既能訪問非靜態成員又能訪問靜態的成員。(成員:成員變數 + 成員方法,靜態成員被所有物件共享)
- 在靜態成員方法中只能訪問靜態成員不能訪問非靜態成員。(成員:成員變數 + 成員方法,因為此時還沒有建立物件)
- 在開發過程中,只隸屬於類層級並被所有物件共享的內容才可以使用static關鍵字修飾。
八、構造塊和靜態程式碼塊(重點)
1. 構造塊
在類體中直接使用{}括起來的程式碼塊
public class BlockTest {
{
Syetem.out.println("構造塊")
}
public BlockTest() {
System.out.println("=====構造方法體")
}
}
注意事項:每一次建立一個物件都會執行構造塊,會優先於構造方法體執行。
2. 靜態程式碼塊
public class BlockTest {
static {
Syetem.out.println("靜態程式碼塊")
}
public BlockTest() {
System.out.println("=====構造方法體")
}
}
注意事項:靜態程式碼塊會隨著類的載入而準備就緒,只執行一次,會優先於構造塊執行。
九、單例類的實現
惡漢式和懶漢式
示例程式碼如下:
public class Singleton {
// 1.對構造方法私有化,讓外界無法隨意構造物件。
private Singleton() {};
// 2.對構造方法私有化了之後,就必須在本類提供一個成員變數來接收Singleton物件的一個例項,
// 為了防止外部獲取 sn 時,對sn 就行隨意修改,所以這裡的成員變數也需要私有化
private static Singleton sn = new Singleton(); // 餓漢式
//private static Singleton sn = null; // 懶漢式
// 3.需要提供公有的get方法來獲取sn,為了外部能夠通過類名直接訪問,所以該get方法需要用static修飾。
public static Singleton getInstance() {
return sn;
// 懶漢式
//if(null == sn){
// sn = new Singleton();
//}
//return sn;
}
}
public class SingletonTest {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2); // true
}
}
開發中推薦使用餓漢式
十、繼承
1. 繼承的特點
-
子類不能繼承父類的構造方法和私有方法。但私有成員變數可以被繼承,只是不能直接訪問。
-
無論用什麼方式構造子類的物件時,都會自動呼叫父類的無參構造方法來初始化從父類中繼承的成員變數,相當於在構造方法的第一行增加程式碼super()的效果。
2. 方法的重寫
當從父類中繼承下來的方法不滿足子類的需求時,就需要在子類中重寫一個和父類一樣的方法來覆蓋從父類中繼承下來的版本。
3. 方法重寫的原則
- 要求方法名相同,引數列表相同以及返回值型別相同,從java5開始允許返回子型別。
- 要求訪問的許可權不能變小,可以相同或者變大。
- 要求方法不能丟擲更大的異常
十一、再談構造塊與靜態程式碼塊
有繼承關係時,各個程式碼塊的執行順序。
示例程式碼
public class SuperTest {
{
System.out.println("SuperTest類中的構造塊");
}
static {
System.out.println("SuperTest類中的靜態程式碼塊");
}
public SuperTest() {
System.out.println("SuperTest類中的構造方法體");
}
}
public class SubSuperTest extends SuperTest{
{
System.out.println("SubSuperTest類中的構造塊");
}
static {
System.out.println("SubSuperTest類中的靜態程式碼塊");
}
public SubSuperTest() {
System.out.println("SubSuperTest類中的構造方法體");
}
public static void main(String[] args) {
SuperTest st = new SubSuperTest();
}
}
//執行結果如下:
//SuperTest類中的靜態程式碼塊
//SubSuperTest類中的靜態程式碼塊
//SuperTest類中的構造塊
//SuperTest類中的構造方法體
//SubSuperTest類中的構造塊
//SubSuperTest類中的構造方法體
總結
- 先執行父類的靜態程式碼塊,再執行子類的靜態程式碼塊。
- 先執行父類的構造塊,再執行父類的構造方法體。
- 先執行子類的構造塊,再執行子類的構造方法體。
通過程式碼說明類中成員變數、構造塊、構造方法體的執行順序
示例程式碼
public class SuperTest {
private final int a;
//private static final int a = 3; 如果是static final修飾·,則必須在定義的時候初始化。
{
System.out.println("SuperTest類中的構造塊");
a = 2;
}
static {
System.out.println("SuperTest類中的靜態程式碼塊");
}
public SuperTest() {
System.out.println("SuperTest類中的構造方法體");
// this.a= 2; 這裡賦值也是可以的
}
public static void main(String[] args) {
SuperTest st = new SuperTest();
System.out.println(st.a);
}
}
//執行結果如下:
//SuperTest類中的靜態程式碼塊
//SuperTest類中的構造塊
//SuperTest類中的構造方法體
//2
對以上程式碼的說明
- final修飾的成員變數必須給初始值,可以在定義的時候給初始值,也可以在構造方法體中或者構造程式碼塊中給初始值。
- 說明成員變數的初始化會優先於構造程式碼塊的初始化,如果成員變數都沒有初始化好,又怎麼能給其賦值呢。
十二、常用的訪問控制符
修飾符 | 本類 | 同一個包中的類 | 子類 | 其他類 |
---|---|---|---|---|
public | 可以訪問 | 可以訪問 | 可以訪問 | 可以訪問 |
protected | 可以訪問 | 可以訪問 | 可以訪問 | 不能訪問 |
預設 | 可以訪問 | 可以訪問 | 不能訪問 | 不能訪問 |
private | 可以訪問 | 不能訪問 | 不能訪問 | 不能訪問 |
十三、final關鍵字(重點)
1. 使用方式
- final關鍵字修飾類體現在該類不能被繼承
- final關鍵字修飾成員方法體現在該方法不能被重寫,但是可以被繼承。
- final關鍵字修飾成員變數體現在該成員變數必須初始化,並且不能被修改。
2. 注意事項
-
final修飾的變量表示賦值之後不能再進行更改,系統賦預設值也算賦值,因此係統也不會賦預設值。
-
final修飾的成員變數必須給初始值,可以在定義的時候給初始值,也可以在構造方法體中或者構造程式碼塊中給初始值。
-
如果用static final同時修飾變數的話,則變數必須在定義的時候進行初始化。因為static變數屬於類,在呼叫建構函式之前就已經被系統賦予預設值了。
private static final int a = 3; //如果是static final修飾·,則必須在定義的時候初始化。
-
final修飾的引用型別變數,引用指向的物件不能改變,但是引用指向的物件裡的內容可以改變。
final A a = new A(); a.value = 2; //這樣是可以的,但是如果再從新賦值a就報錯了:a = new A();
-
final修飾的成員變數一般都和static使用,因為 final修改的成員變數是無法被修改的,沒有必要在每個物件裡儲存一份相同的成員變數。
-
final 不能和 abstract修飾類,抽象是用來實現的,final 不能被繼承。
十四、類的初始化過程(重點)
- 先載入類的class檔案到方法區
- 然後將靜態屬性和方法載入到資料共享區
- 在堆記憶體中開闢空間(new物件的時候)
- 將變數載入到堆記憶體,並賦預設值。final修飾的成員變數系統不會賦預設值,需要手動顯示賦值或者隱式賦值。
- 構造塊的初始化
- 構造方法初始化
十五、類的初始化時機(擴充套件)
只有以下6種方式被看作程式對類或介面的主動使用。
- 建立類的例項。包括new關鍵字來建立,或者通過反射、克隆及反序列化方式來建立例項。
- 呼叫類的靜態方法。
- 訪問某個類或介面的靜態變數(不是靜態常量),或者對該靜態變數賦值。
- 使用反射機制來建立某個類或介面對應的java.lang.Class物件。例如Class.forName("Test")操作,如果系統還未初始化Test類,這波操作會導致該Test類被初始化,並返回Test類對應的java.lang.Class物件。
- 初始化一個類的子類,該子類所有的父類都會被初始化。
- JVM啟動時被標明為啟動類的類(直接使用java.exe命令執行某個主類)。例如對於“java Test”命令,Test類就是啟動類(主類),JVM會先初始化這個主類。
除了以上6種情況,其他方式都被看作成是被動使用,不會導致類的初始化。下面通過接個例子來驗證:
public class A {
public static final int a = 2*3;//a為編譯時常量
public static final String str = "haha";//str為編譯時常量
public static final int b = (int)(Math.random()*5);//b不是編譯時常量
static {
System.out.println("init A");
}
}
public class ATest {
public static void main(String[] args) {
System.out.println(A.a);
}
}
// 輸出結果為6
// 並沒有輸出init A,說明A類並沒有被初始化,當JVM載入並連線A類時,不會在方法區內為它的編譯時常量a分配記憶體。
public class ATest {
public static void main(String[] args) {
System.out.println(A.b);
}
}
// 輸出結果為init A, 4
// 說明這時A類被初始化了
十六、多型
1. 多型的特點
-
當父類型別的引用指向子類型別的物件時,父類型別的引用可以直接呼叫父類獨有的方法。
Person p = new Student(); // p可以呼叫Person類中獨有的方法
-
當父類型別的引用指向子類型別的物件時,父類型別的引用不可以直接呼叫子類獨有的方法。
Person p = new Student(); // p不可以直接呼叫Student類中獨有的方法,可以強轉型別呼叫子類中特有的方法。
-
對於父子類都有的非靜態方法來說,編譯階段呼叫父類版本,執行階段呼叫子類重寫的版本。
Person p = new Student(); p.show(); // 編譯階段看Person類中是否有show方法,如果沒有,則編譯器報錯。 // 執行階段,執行Student類中的show方法,如果Student類中沒有show方法,就取Person類中找。 // 總結一句話:編譯看左邊,執行看右邊。
-
對於父子類都有的非靜態成員變數時,編譯階段呼叫父類中的變數,執行階段也是呼叫父類中的變數。
class Fu { int num = 4; } class Zi extends Fu { int num = 5; } class Demo { public static void main(String[] args) { Fu f = new Zi(); System.out.println(f.num); Zi z = new Zi(); System.out.println(z.num); } } // 總結一句話:編譯和執行看左邊
-
對於父子類都有的靜態方法時,編譯階段呼叫父類中的方法,執行階段也是呼叫父類中的方法。
Person p = new Student(); p.test(); // test為靜態方法 // 編譯的時候看Person類中是否有test方法,執行的時候也是走的父類中的test方法。
十七、抽象類
示例程式碼
public abstract class Employee {
private String id; // 員工編號
private String name; // 員工姓名
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
//工作方法(抽象方法)
public abstract void work();
}
面試考點
- 即使一個類中不含抽象方法,它也可以宣告為抽象類;
- 如果子類只覆蓋了部分抽象方法,那麼該子類還是一個抽象類。
- 可以有構造方法的,由子類的super語句來呼叫,用於給抽象類中的成員初始化。
- abstract不能和private共存
- abstract不能和final共存
- abstract不能static共存,因為一旦加static我們就可以通過類名直接訪問抽象方法,由於抽象方法沒有方法體,沒有任何意義,也不允許這樣做。
- 抽象類可以包含main方法,它只是一個靜態方法,你可以使用main方法執行抽象類,但不可以建立任何例項。
十八、介面
1. 成員方法
全都是抽象方法,並且有固定的格式。
public interface MyAInterface{
public abstract void show();
}
2. 成員變數
介面中無法定義普通的成員變數,必須定義為常量。
public interface MyAInterface{
public static final int NAME = "zhangsan";
}
為什麼必須是static final修飾的常量呢?
static:必須。因為介面是可以多繼承的。如果一個類實現了兩個介面,且兩個介面都具有相同名字的變數,此時這個變數可以被實現類使用,那麼如果不是static的,這個變數來自哪一個介面就會產生歧義,所以實現類使用介面中的變數必須通過介面名指定,也就只能定為static的。
看下面的例子:
public interface iface1 {
int a = 10;
}
public interface iface2 {
int a = 9;
}
public class impl implements iface1, iface2 {
public static void main(String args[]){
System.out.println(a);
}
}
此時,會報編譯錯誤,因為a有歧義。
final:我認為因為必須是static的,那麼所有子類共享,而介面是一種抽象, 所以一個子類修改了值會影響到其他所有子類,因此就不應該允許子類修改這個值,所以定義為final。
十九、類和介面之間的關係
名稱 | 關鍵字 | 關係 |
---|---|---|
類和類之間的關係 | 使用extends關鍵字表達繼承關係 | 支援單繼承 |
類和介面之間的關係 | 使用 implements關鍵字表達實現關係 | 支援多實現 |
介面和介面之間的關係 | 使用extends關鍵字表達繼承關係 | 支援多繼承 |
二十、內部類
1. 內部類的分類
-
普通內部類:直接將一個類的定義放在另一個類的類體中
// 定義格式 修飾符 class 外部類 { 修飾符 class 內部類 { //其他程式碼 } } // 訪問方式:外部類名.內部類名 變數名 = new 外部類名().new 內部類名();
說明:普通內部類和普通類一樣可以使用private、protected、final、abstract修飾
-
靜態內部類:使用static關鍵字修飾的內部類,隸屬於類層級。
// 定義格式 修飾符 class 外部類 { 修飾符 static class 內部類 { //其他程式碼 } } // 訪問方式:外部類名.內部類名 變數名 = new 外部類名.內部類名();
總結:
-
靜態內部類不能直接訪問外部類的非靜態成員;
-
靜態內部類可以直接建立物件
-
如果靜態內部類訪問外部類中與本類內同名的成員變數或方法時,需要使用類名.的方式訪問。
-
-
區域性內部類:直接將一個類的定義放在方法體的內部。
public class Party { // 外部類,聚會 public void puffBall(){ // 吹氣球方法 class Ball { // 內部類,氣球,不能使用修飾符修飾 public void puff(){ System.out.println("氣球膨脹了"); } } //建立內部類物件,呼叫puff方法 new Ball().puff(); } }
總結:
-
區域性內部類不能使用public、private、protected訪問控制符和static關鍵字修飾符,跟在成員方法中的區域性變數一樣。
public class AreaOuter { private int cnt = 1; public void show() { // 區域性變數 int ib = 2; // public int ib = 2; 報錯 // private int ib = 2; 報錯 // 內部類 //public class AreaInner { 報錯 //private class AreaInner { 報錯 final class AreaInner { private int ia = 1; public AreaInner() { System.out.println("區域性內部類的構造方法"); } public void test() { // 如果用到外部的變數ib,則ib為final修飾的 System.out.println("ib = " + ib); System.out.println("ia = " + ia); System.out.println("cnt = " + cnt); } } //呼叫方法:宣告區域性內部類的引用指向區域性內部類的物件 AreaInner areaInner = new AreaInner(); areaInner.test(); } } // 因為區域性變數 本身就是 一個訪問許可權 的設定。 只能在區域性呼叫,也就是說區域性變數的生命週期在{}之中除了這個方法外界是沒辦法訪問你這個變數,所以不需要用任何修飾符修飾,比如private ,public protected,等但是能加final。 // 也不能加static,靜態的關鍵詞,因為static只能修飾成員變數和成員方法,在區域性變數中用static修飾,又不能直接被類呼叫,而static關鍵字就是不直接過物件而用類就可以直接呼叫的,所以區域性變數前不能加static關鍵字。
-
內部類引用外部變數必須是final修飾的
-
-
匿名內部類:沒有名字的內部類
格式
new 父類或介面(){ //進行方法重寫 };
-
程式碼演示
//已經存在的父類: public abstract class Person{ public abstract void eat(); } //方式一:使用Person型別的引用來接收匿名物件 Person p = new Person(){ public void eat() { System.out.println(“我吃了”); } }; p.eat(); // 方式二:不使用變數引用 new Person(){ public void eat() { System.out.println(“我吃了”); } }.eat(); // 注意這一行.eat()之前都是在建立了介面的實現類物件
總結:匿名內部類可以理解為不用重新寫一個類去實現抽象類,直接new,然後實現抽象方法即可。
二十一、回撥模式
1. 概念
回撥模式是指:如果一個方法的引數是介面型別,則在呼叫該方法時,需要建立並傳遞一個實現此介面型別的物件。而該方法在執行的時候會呼叫到引數物件中所實現的方法。
2. 示例程式碼
public class AnonymousInterfaceTest {
// 假設已有下面的方法,請問如何呼叫下面的方法?
// AnonymousInterface ai = new AnonymousInterfaceImpl();
// 介面型別的引用指向實現型別的物件,形成了多型
public static void test(AnonymousInterface ai) {
// 編譯階段呼叫父類版本,執行呼叫實現類重寫的版本
ai.show();
}
public static void main(String[] args) {
//AnonymousInterfaceTest.test(new AnonymousInterface()); // Error:介面不能例項化
AnonymousInterfaceTest.test(new AnonymousInterfaceImpl());
}
}
public class AnonymousInterfaceImpl implements AnonymousInterface {
@Override
public void show() {
System.out.println("這裡是介面的實現類!");
}
}
public interface AnonymousInterface {
// 自定義抽象方法
public abstract void show();
}
二十二、列舉類
1. 列舉的定義
-
使用public static final修飾的常量描述較為繁瑣,使用enum關鍵字來 定義列舉型別取代常量。
-
列舉值就是當前類的型別,也就是指向本類的物件,預設是使用public static final 修飾的,因此採用列舉型別.的方式呼叫。
-
列舉類可以自定義構造方法,但是構造方法的修飾符必須是private的,預設也是私有的。
-
列舉類可以實現介面,不能繼承類。
二十三、註解(陌生)
註解又叫標註,是從Java5開始增加的一種引用型別。
註解本質上就是程式碼中的特殊標記,通過這些標記可以在編譯、類載入以及執行時執行指定的處理。
1. 註解的語法格式
訪問修飾符 @interface 註解名稱{
註解成員;
}
通過@註解名稱的方式可以修飾包、類、 成員方法、成員變數、構造方 法、引數、區域性變數的宣告等。
2. 註解的使用方式
- 註解體中只有成員變數沒有成員方法,而註解的成員變數以“無形參的方法”形式來宣告,其方法名定義了該成員變數的名字,其返回值定義了該成員變數的型別。
public @interface MyAnnotation {
// 宣告一個String型別的成員變數,變數名為value
public String value() default "haha"; // 給預設值
public String value2();
}
- 如果註解只有一個引數成員,建議使用引數名為value,而型別只能是八種基本資料型別、String型別、Class型別、enum型別及Annotation型別。
3. 元註解的概念
-
元註解是可以註解到註解上的註解,或者說元註解是一種基本註解,但 是它能夠應用到其它的註解上面。
-
元註解主要有 @Retention、@Documented、@Target、@Inherited、 @Repeatable。
3.1 元註解@Retention
@Retention 應用到一個註解上用於說明該註解的的生命週期,取值如下:
- RetentionPolicy.SOURCE 註解只在原始碼階段保留,在編譯器進行編譯時 它將被丟棄忽視。
- RetentionPolicy.CLASS 註解只被保留到編譯進行的時候,它並不會被加 載到 JVM 中,預設方式。
- RetentionPolicy.RUNTIME 註解可以保留到程式執行的時候,它會被載入 進入到 JVM 中,所以在程式執行時可以獲取到它們。
3.2 元註解@Documented
定義為@Documented的註解必須設定Retention值為RUNTIME。
3.3 元註解@Target
-
@Target用於指定被修飾的註解能用於哪些元素的修飾,取值如下:
ElementType.ANNOTATION_TYPE 可以給一個註解進行註解 ElementType.CONSTRUCTOR 可以給構造方法進行註解 ElementType.FIELD 可以給屬性進行註解 ElementType.LOCAL_VARIABLE 可以給區域性變數進行註解 ElementType.METHOD 可以給方法進行註解 ElementType.PACKAGE 可以給一個包進行註解 ElementType.PARAMETER 可以給一個方法內的引數進行註解 ElementType.TYPE 可以給型別進行註解,比如類、介面、列舉
3.4 元註解@Inherited
@Inherited並不是說註解本身可以繼承,而是說如果一個超類被該註解標 記過的註解進行註解時,如果子類沒有被任何註解應用時,則子類就繼 承超類的註解。