Java學習筆記(語言基礎及面向對象)

Java語言概述

  • Java虛擬機(JVM : Java Virtual Machine)
    java語言裏負責解釋執行字節碼文件,是運行Java字節碼文件的虛擬計算機。
    當使用Java編譯器編譯Java程序時,生成的是與平臺無關的字節碼,這些字節碼不面向任何具體平臺,只面向JVM。
    不同平臺上的JVM都是不同的,但他們都提供了相同的接口。
  • Java標準化開發包(JDK : Java SE Development Kit)
    提供了編譯、運行Java程序所需的各種工具和資源(包括Java編譯器、Java運行時環境以及常用的Java類庫等)
  • Java運行時環境(JRE : Java Runtime Enviroment)
    JRE與JVM的關係:JRE包含JVM,JVM是運行Java程序的核心虛擬機,而運行Java程序不僅需要核心虛擬機,還需要其他的類的加載器、字節碼校驗器以及基礎類庫。JRE除包含JVM之外,還包含運行Java程序的其他環境支持。
  • 計算機如何查找命令?(設置環境變量的原因)
    Windows根據Path環境變量來查找命令,Path環境變量的值是一系列路徑,系統會根據這一系列路徑中依次查找命令。如果能找到這個命令,則執行;如果找不到這個命令,就會出現:'xxx’不是內部或外部命令,也不是可運行的程序或批處理文件。
  • 關於CLASSPATH
    如果使用1.4以前版本的JDK,需要設置CLASSPATH環境變量的值爲
    .;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar
    一旦設置了該環境變量,JRE將會按照該環境變量指定的路徑來搜索Java類。
    點( . )代表當前路徑,用以強制Java解釋器在當前路徑下搜索Java類。
  • Java源文件的命名規則
    如果Java源程序的源代碼裏定義了一個public類,則該源文件的主文件名必須與public類的類名相同。(因此,一個Java源文件內最多隻能有一個public類)
    如果一個源文件內有3個類,則使用javac命令編譯會生成3個.class文件,每一個類對應一個.class文件。
  • 提高可讀性的建議
  1. 一個Java源文件通常只定義一個類,不同的類使用不同的源文件定義。
  2. 讓Java源文件的主文件名與該源文件中定義的public類同名。
  • 垃圾回收(Garbage Collection , GC)
    JRE回收無用內存的機制。通常JRE會提供一個後臺線程來進行檢測和控制,一般都是在CPU空閒或內存不足時自動進行垃圾回收。
  • Java的堆內存
    是一個運行時數據區,用以保存類的實例。堆內存中存儲着在運行的應用程序所建立的所有對象,這些對象不需要程序通過代碼來顯式地釋放。堆內存的回收由垃圾回收器來負責,所有的JVM實現都有一個由垃圾回收器管理的堆內存。

數據類型和運算符

自動提升規則

byte -> short&char -> int -> long -> float -> double

  1. 所有的byte、short和char類型都將被提升到int類型。
short value = 5;
value = value - 2;
//這裏的value-2被自動提升到int類型,int賦給short可能會報錯
  1. 整個算數表達式的數據類型自動提升到與表達式中最高等級操作數相同的類型
byte b = 40;
var c = 'a';
var i = 23;
var d = .314;
double ans = b + c + i * d;
System.out.print(ans);//輸出7

直接量

是指在程序中通過源代碼直接給出的值。

int a = 1; //1是直接量
double b = 2.0; //2.0是直接量
String name = "MJT"; //"MJT"是直接量

String類的直接量不能賦給其他類型的變量。
null類型的直接量可以直接賦給任何引用類型的變量,包括String類型。
boolean類型的直接量只能賦給boolean類型的變量,不能賦給其他任何類型的變量。

常量池

指的是在編譯器被確定,並保存在已編譯的.class文件中的一些數據。
包括關於類、方法、接口中的常量,也包括字符串中的直接量。

//下面的"hello"都在常量池中儲存
var s1 = "hello"; 
var s2 = "hello";
var s3 = "he" + "llo";

Java確保每個字符串常量只有一個,不會產生多個副本。
因此,上面那三個"hello"在常量池中是同一塊數據。

位運算

運算符 名稱 功能
& 按位與 同時爲1時返回1
| 按位或 只要有一位爲1則返回1
~ 按位非 單目運算符,用來取反。
^ 按位異或 當兩位相同時返回0,不同時返回1
<< 左移運算符 右側補0。左側超出截斷
>> 右移運算符 原來是正數,則左邊補零;原來是複數,則左邊補1。右側超出截斷
>>> 無符號右移運算符 左側總是補零。右側超出截斷

n>>x相當於n乘以2的x次方。
n<<x相當於n除以2的x次方。

數組類型

  1. 靜態初始化 : 顯式指定初始值,系統決定數組長度。
//第一種方式
int[] arr;
arr = new int[] {1,2,3};
//第二種方式(推薦)
var arr = new  int[] {1,2,3};
  1. 動態初始化 : 顯式指定長度,系統分配初始值。
//第一種方式
int[] arr = new int[10];
//第二種方式(推薦)
var arr = new int[10];
  1. 動態初始化分配規則
數組元素類型 自動分配的值
整數類型 0
浮點類型 0.0
字符型(char) ‘\u0000’
布爾類型 false
引用類型(類,接口,數組) null

foreach循環

以遍歷數組元素爲例

var arrs = new int[] {1,2,3};
for (int arr : arrs) {
    System.out.println(arr);
}

使用foreach循環迭代數組元素時,並不能改變數組元素的值,因此不應用foreach的循環變量進行賦值。

深入數組

Java中“數組元素”和“數組變量(引用變量)”在內存中是分開存放的。舉個栗子:

int[] p;//定義一個局部變量

這裏的p是一個引用變量,作爲局部變量被存儲在棧內存中。
相當於C++中的指針,指向一個int元素的內存。

p = new int[] {1,2,3};

實際的數組元素{1,2,3}被存儲在被new開闢的一塊堆內存中,p = new int[] {1,2,3};則讓p引用變量指向該堆內存。

二維數組

//動態初始化方式
int[][] arr;
arr = new int[2][];
//靜態初始化方式
int[][] arr = new int[][] {new int[2],new int[]{1,2,3}};

這個二維數組實際上完全可以看作是一維數組:使用new int[4]初始化一維數組後,相當於定義了4個int類型的變量;
類似的,使用new int[4][]初始化這個數組後,相當於定義了4個int[]類型的變量,這些int[]類型的變量都是數組類型,因此必須再次初始化這些數組。

面向對象(上)

關於static的問題

爲什麼靜態成員(類成員)不能直接訪問非靜態成員(對象成員)?

先了解幾個書中的基本概念

主調:調用成員變量、方法的對象稱爲主調。如:主調.方法();

基本概念(1):如果調用static修飾的成員時忽略了主調,那麼默認使用該類作爲主調。

//在調用static方法時下列兩種形式相同。
static_fun();
ClassName.static_fun();

基本概念(2):如果調用沒有static修飾的成員時忽略了主調,那麼默認使用this作爲主調。

//調用普通成員時下列兩種形式相同。
fun();
this.fun();

基本概念(3):this指向本類的實例,而static成員是屬於類的,因此static成員內不能使用this。

如果在靜態成員中調用普通成員,會發生什麼? 舉個栗子:

public class test
{
    public void show()
    {
        System.out.println("HelloWorld");
    }
    public static void main(String[] args)
    {
        show();
    }
}
===============運行結果=================
錯誤: 無法從靜態上下文中引用非靜態 方法 show()

這對應基本概念(2),這裏調用的是一個普通成員函數show,則默認使用this作爲主調。
這裏相當於this.show();
但show()只能通過對象來訪問,而static方法是屬於類的,在static內的this不知道該指向哪一個實例,因此show()無法在main方法中被調用。

如果確實想在static方法中調用非static方法,可以臨時創建一個對象:

public class test
{
    public void show()
    {
        System.out.println("HelloWorld");
    }
    public static void main(String[] args)
    {
        //創建一個臨時對象來調用臨時對象中的show方法。
        new test().show();
    }
}

方法的參數傳遞機制

Java參數的傳遞機制只有一種:值傳遞

對於引用類型的參數傳遞,一樣採用的是值傳遞方式。但是這裏的“值”,是地址值。比如:

class Data{
    int num1=1;
    int num2=2;
}
public class Test
{
    public static void swap(Data p)
    {
        var temp = p.num1;
        p.num1 = p.num2;
        p.num2 = temp;
    }
    public static void main(String[] args)
    {
        Data a; //定義一個Data類型的引用
        a = new Data(); //a引用指向一塊用new新開闢的一塊Data內存空間。
        swap(a); //值傳遞,p接受a的地址值,使得p與a存儲着相同的地址值。
        System.out.println(a.num1 + " " + a.num2);
    }
}

形參個數可變的方法

方法:在最後一個形參類型後加三個點 (Typename… name)

public class Test
{
    public static void fun(int num,String... books)
    {
        for (String string : books) {
            System.out.println(string);
        }
    }
    public static void main(String[] args)
    {
        //可直接羅列參數
        fun(3,"Java","C++","Python");
        //也可將String...看做String[],傳遞一個數組進去。
        fun(3,new String[] {"Java","C++","Python"});
    }
}

必須將Typename…形參放在在最後一個位置,否則會報錯。

成員變量和局部變量

成員變量:在類內定義的變量。無需顯式初始化,系統自動初始化。
局部變量:在方法內定義的變量。需要顯式初始化。

成員變量的初始化和內存中的運行機制

當系統加載類或創建該類的實例時,系統會自動爲成員變量分配內存空間。

舉個栗子:
比如說我們有一個類

class Person{
    public static int eyeNum;
    public String name;
}

1)如果在主函數中第一次加載這個類:

new Person();

會初始化Person類,只初始化靜態變量(類變量)↑

2)然後創建一個Person類型的對象

var p1 = new Person();

創建一個Person對象時並不需要爲eyeName類變量分配內存
系統只爲name實例變量分配了內存空間

3)再創建第二個Person類對象

var p2 = new Person();
p2.name = "張三";

系統只爲第二個對象的對象成員分配了內存空間

局部變量的初始化和內存中的運行機制

int a; //系統並未分配空間
a = 1; //賦值時才分配空間
  1. 局部變量不屬於任何類或實例,因此它總是保存在其所在方法的棧內存中
  2. 因爲局部變量只保存基本類型的值或者對象的引用,因此局部變量所佔的內存區通常比較小

訪問控制符

private default protected public
同一個類中
同一個包中
子類中
全局範圍內

局部變量不能用用訪問控制符修飾

工具方法

只用於輔助該類的其他方法的方法。也應該用private修飾

package包機制

幾個知識點

  1. 包機制用於解決類的重名衝突,類文件的管理。
  2. 按照規範,包名最好小寫字母。
  3. 應該使用域名的倒寫,如com.ccatom包。
  4. package語句必須作爲源文件的第一條非註釋語句。
  5. 一個源文件只能指定一個包
  6. 同一個包下的類可以直接訪問
  7. 通常建議將class文件與java源文件分開放。

特別注意的是:父包內的類使用子包中的類,必須寫類的完整包路徑加類名。

比如com.ccatom包下有一個類A,com.ccatom.child包下有一個類B。
類A使用類B必須要有com.ccatom.child.B,就像以下形式。

com.ccatom.child.B obj = new com.ccatom.child.B();

一個使用包機制的示例

//定義一個子包的類
package com.ccatom.child;
public class B{
    public static void show(){
        System.out.println("Hello Package");
    }
}
父包類要用子包類,因此先編譯子包源文件:javac -d . B.java
發現在以下目錄出現了class文件:當前目錄\com\ccatom\child\B.class
//再定義一個使用子包類的父包類
package com.ccatom;
public class A{
    public static void main(String[] args){
        //使用完整的名稱來使用子類包
        com.ccatom.child.B obj = new com.ccatom.child.B();
        obj.show();
    }
}
編譯父包類源文件:javac -d . A.java
發現在以下目錄出現了class文件:當前目錄\com\ccatom\A.class
運行父類包:java com.ccatom.A
輸出Hello Package

如果編譯Java文件時不使用-d選項,編譯器不會爲Java源文件生成相應的文件結構。

import關鍵字

import關鍵字用來簡化上述源代碼,簡化以前:

package com.ccatom;
public class A{
    public static void main(String[] args){
        com.ccatom.child.B obj = new com.ccatom.child.B();
        obj.show();
    }
}

使用import直接導入類B,簡化之後:

package com.ccatom;
import com.ccatom.child.B;//import導入子包類B
public class A{
    public static void main(String[] args){
        B obj = new B(); //簡化了此處的操作
        obj.show();
    }

如果想導入某個包下所有的類,則可以這樣寫:

import com.ccatom.*; //導入了com.ccatom包下的所有的類

JDK 1.5以後可以進行“靜態導入”,用來導入指定類的某個靜態成員變量或方法。
如,導入com.ccatom包中類A的靜態方法fun() :

import static com.ccatom.A.fun;

也可以導入類A中所有的靜態成員:

import static com.ccatom.A.*;

1.import語句應該出現在package語句之後、類定義之前。
2.Java默認爲所有源文件導入java.lang包下所有的類,因此可以直接使用String和System類。
使用import可以省略包名;而使用import static則可以連類名都省略。

Java常用包

含有 例如
java.lang 核心類(無需導入) String、Math、System、Thread
java.util 工具類/接口和集合框架類/接口 Arrays、List、Set
java.net 網絡編程相關 -
java.text 輸入輸出的類/接口 -
java.sql JDBC數據庫編程的相關類/接口 -
java.awt 抽象窗口工具集,用於構建GUI -
java.swing Swing GUI編程的相關類 -

Java構造器

類似於C++中的構造函數

public class Test{
    public String name;
    public int age;
    //一個簡單的構造器,用來初始化name和age
    public Test(String name,int age){
        this.name = name;
        this.age = age;
    }
    public static void main(String[] args){
        Test obj = new Test("MJT",20);
        System.out.println(obj.name+" "+obj.age);
    }
}

不能說Java對象完全由構造器負責創建:在調用構造器之前,系統就已經爲對象分配內存空間,併爲這個對象執行默認初始化,這個對象就已經產生了。只是這個對象不能被外部訪問,只能通過內部構造器的this來引用。

同時,上述代碼自定義了一個有參的構造器,這時就不能再用new Test();來創建實例,因爲類中沒有無參構造器。因此需要進行構造器重載(同一個類裏具有多個構造器,各構造器的形參列表不同)

//上述類中重載一個無參的構造器
//就可以用new Test();來創建實例了
public Test(){}
  • 同一個類的構造器之間的調用

一種方法是在構造器中用new關鍵字來調用另一個構造器,但會重新創建一個對象,佔用資源。
因此,對於含有包含關係的構造器來講,就要用this調用另一個重載的構造器

public Test(String name){
    this.name = name;
}
public Test(String name,int age){
    this(name);//用this調用重載的構造器
    this.age = age;
}

這裏需要注意的是:

  1. 使用this調用另一個重載的構造器只能在構造器中使用
  2. 必須作爲構造器執行體的第一行語句

類的繼承

比如B類以public方式繼承A類:

public B extends A{......}

需要注意的一些小知識:

  1. Java摒棄了多繼承特徵,Java類只能有一個直接父類
  2. 爲顯式指定父類,則默認父類java.lang.Object,java。因此,java.lang.Object類是所有類的父類
  3. 構造器不能被繼承;
  4. 從子類角度看,子類擴展(extends)了父類;
  5. 從父類角度看,父類派生(derive)出了子類;

重寫父類的方法

方法重寫(override):子類包含父類同名方法的現象。又稱方法覆蓋

class A{
    public void fun(){
        System.out.println("A_fun");
    }
}
public class B extends A{
    //重寫父類A中的方法fun
    public void fun(){
        System.out.println("B_fun");
    }
    public static void main(String[] args){
        B obj = new B();
        obj.fun();//輸出B_fun
    }
}

靜態方法與非靜態方法不能互相重寫

如果父類中有一個private方法,則在子類中定義一個同名方法不構成重寫,如:

class A{
    //父類中的方法定義爲private
    private void fun(){...}
}
class B extends A{
    //這裏可以用static限定,因爲不屬於重寫。
    //所定義的fun是子類的新方法
    public static void fun(){...}
}

super限定

如果需要在子類中調用父類中被重寫的方法,可以用super限定作爲主調來調用被覆蓋的方法。比如

class A{
    public int a = 1;
}
public class B extends A{
    public int a = 2;
    public void show_a_in_A(){
        //在子類方法中用super來調用父類的實例變量a
        System.out.println(super.a);
    }
    public static void main(String[] args){
        //儘管父類的a被子類的a覆蓋
        //但是new B();的時候依然會爲父類中的a開闢一塊內存
        B obj = new B();
        obj.show_a_in_A();//輸出1
    }
}

需要注意的小知識:

  1. 可以將super與this相似看待,super不能出現在static修飾的方法中
  2. 並不是完全覆蓋,系統在創建子類對象時,依然會爲父類中定義的、被隱藏的變量分配內存空間;

重載(overload)與重寫(override)

重載:發生在同一個類的多個同名方法之前。
重寫:發生在子類和父類的同名方法之間。

也有例外:如果子類中定義一個與父類方法有相同的方法名,但參數列表不同的方法,就會形成父類方法和子類方法的重載。

子類調用父類的構造器

同一個類,在一個構造器中調用另一個構造器使用this調用來完成。
類似的,在子類構造器中調用父類的構造器用super調用來完成。

class A{
    private String name;
    public A(String n){
        name = n;
    }
}
public class B extends A{
    private int age;
    public B(String n,int a){
        //使用super限定來調用父類的構造器
        super(n)
        age = a;
    }
    public static void main(String[] args){
        B obj = new B("MJT",20);
    }
}

需要注意的小知識:

  1. super調用父類構造器時也必須出現在構造器執行體的第一行(因此this調用與super調用不會同時出現)
  2. 當調用子類構造器來初始化子類對象時,父類構造器總會在子類構造器之前執行。
  3. super調用不能用在static方法內(構造器不屬於類,也不可能用static修飾)

多態

  • 多態:相同類型的變量,調用同一個方法時呈現出多種不同的行爲特徵的現象。
  • 編譯時類型:由聲明變量的類型決定,如 String s ,那麼String就是編譯時類型。
  • 運行時類型:由實際賦給該變量的對象決定,如var s = new String(); ,那麼String就是運行時類型。
  • 向上轉型(upcasting):將一個子類對象直接賦給一個父類引用變量

如果編譯時類型和運行時類型不一致,就可能出現多態
類似的形式: 編譯時類型 變量名 = new 運行時類型();

這種類型的多態分爲兩種情況:

首先是重寫成員的情況:
先上結論:

解釋一下圖表:變量名.重寫方法(); 調用的是執行運行時類型的方法,也就相當於
變量名.執行運行時類型的方法();

以代碼爲例:

class Father{
    //這裏的num與show對應圖表中的“重寫實例變量”與“重寫方法”
    public int num = 1;
    public void show(){
        System.out.println("Father_show_override");
    }
}
public class Child extends Father{
    //子類中重寫num實例變量
    public int num = 3;
    //子類中重寫show方法
    public void show(){
        System.out.println("Child_show_override");
    }
    public static void main(final String[] args){
        final Father obj = new Child();

        obj.show();//調用重寫的show方法
        System.out.println(obj.num);//輸出重寫的num實例變量
    }
}

然後是特有成員的情況(子類有,父類沒有 或 父類有子類沒有)
結論:

代碼爲例:

class Father{
    //父類中特有的實例變量
    public int father_num = 2;
    //父類中特有的方法
    public void father_fun(){
        System.out.println("Father_fun");
    }
}
public class Child extends Father{
    //子類中特有的實例變量
    public int child_num = 4;
    //子類中特有的方法
    public void child_fun(){
        System.out.println("Child_fun");
    }
    public static void main(final String[] args){
        final Father obj = new Child();

        obj.father_fun();//調用父類中特有的方法
        System.out.println(obj.father_num);//輸出父類中特有的實例變量

        obj.child_fun();//調用子類中特有的方法,報錯
        System.out.println(obj.child_num);//輸出子類中特有的實例變量,報錯
    }
}

instanceof運算符

  • 使用:雙目運算符。前一個操作數通常是一個引用類型變量,後一個操作數通常是一個類(或接口)。

  • 作用:用來判斷前面的對象是否是後面的類或其子類(是返回true,不是則返回false)。

  • 應用:一般用於強制類型轉換(向上或向下轉型)的情景。

    例如:如果試圖將一個父類實例強制轉換爲子類類型,則這個對象必須是子類實例纔行(即編譯時類型是父類類型,而運行時類型是子類類型)否則將在運行時引發ClassCastException異常。

    通常情況下instanceof和(type)運算符搭配使用:通常先用instanceof判斷一個對象是否可以強制類型轉換,然後再使用(type)運算符進行強制類型轉換,從而保證程序不會出現錯誤。

    舉個栗子:

    //如果obj是String類或其子類
    if(obj instanceof String){
        //那麼就可以將obj對象類型強制轉換爲同類或其父類類型
        var str = (String)obj;
    }
    

使用繼承的注意點

  • 儘量隱藏父類的內部數據,用private修飾符。
  • 不要讓子類可以隨意訪問、修改父類的方法。工具方法應該用private修飾;需要被外部類調用,就用public修飾,但又不想子類重寫該方法,就再加一個final修飾符;如果希望某個方法被子類重寫,又不想被其他類訪問,就用protected修飾符。
  • 儘量不要在父類構造器中調用將要被子類重寫的方法。

組合

  • 對於繼承:子類可以直接獲得父類的public方法。
  • 而對於組合:把舊類對象作爲新類的成員變量組合起來,用以實現新類的功能。

舉個栗子:

class A{
    public void fun_A(){
        System.out.println("Hello_World");
    }
}
class B{
    //將舊類A的對象a作爲新類B的成員變量組合起來。
    //這裏用private,是因爲我們想讓用戶看到的只是舊類的方法,而不是舊類對象。
    private A a = new A();//組合
    public void fun_A_in_B(){
        //從而可以在新類方法內調用舊類的方法。
        a.fun_A();
    }
}
public class Test{
    public static void main(String[] args){
        var obj = new B();
        obj.fun_A_in_B();
    }
}

組合的內存花銷:繼承與組合設計的系統開銷不會有本質的差別。繼承爲父類(舊類)開闢空間,也爲子類(新類)開闢空間。組合也一樣,只不過比繼承多了一個引用變量來引用被嵌入的對象,一般影響不大。

初始化塊

  • 格式:
[修飾符] {
    //初始化塊的可執行性代碼
    ......
}
  • 這裏的 [修飾符] 如果是static,則稱此代碼塊爲類初始化塊
  • 沒有static修飾,則稱爲實例初始化塊

實例初始化塊

  • 實例初始化塊只在創建Java對象時隱式執行,而且在構造器執行之前自動執行
public class Test{
    {
        System.out.println("第一個運行的實例初始化塊");
    }
    {
        System.out.println("初始化塊按順序執行");
    }
    public Test(){
        System.out.println("先運行完初始化塊後運行Test構造器");
    }
    public static void main(String[] args){
        new Test();//創建對象時隱式執行
    }
}
=============執行結果=============
第一個運行的初始化塊
初始化塊按順序執行
先運行完初始化塊後運行Test構造器

實例初始化塊的“假象”:其實在編譯後實例初始化塊會消失,被還原到每個構造器的所有代碼的前面

類初始化塊

  • 類初始化塊在類初始化階段執行,而不是創建對象時才執行。因此類初始化塊總是比實例初始化塊先執行
  • 類初始化塊只能訪問靜態成員。

【重點】實例初始化快、類初始化快、構造器的執行順序


class Father{
    static{
        System.out.println("父類類初始化塊");
    }
    {
        System.out.println("父類實例初始化塊");
    }
    public Father(){
        System.out.println("父類的構造器");
    }
}
class Child extends Father{
    static{
        System.out.println("子類類初始化塊");
    }
    {
        System.out.println("子類實例初始化塊");
    }
    public Child(){
        System.out.println("子類的構造器");
    }
}
public class Test{
    public static void main(String[] args){
        //第一次new Child();
        new Child();
        //第二次new Child();
        new Child();
    }
}

用流程圖來簡單描述運行流程(貧窮的我只能用試用版億圖圖示):

面向對象(下)

包裝類

Java中的基本類型功能簡單,不具備對象的特性,爲了使基本類型具備對象的特性,所以出現了包裝類,就可以像操作對象一樣操作基本類型數據。

//定義一個包裝類Integer的對象
Integer int_obj = new Integer(1024);
  • 自動裝箱:將一個基本類型變量直接賦給對應的包裝類變量,或者賦給Object變量。
//自動裝箱
int num = 1024;//一個基本類型變量
Integer int_obj=num;//直接賦給Integer包裝類變量
  • 自動拆箱:與裝箱相反,直接把包裝類對象賦給一個對應的基本類型變量。
//自動拆箱
Integer int_obj = new Integer(1024);//一個包裝類對象
int num = int_obj;//直接賦給基本類型變量
  • 基本數據類型和包裝類的對應關係
基本數據類型 包裝類
byte Byte
short Short
int Integer
long Long
char Character
float Float
double Double
boolean Boolean
  • 包裝類常用方法

打印對象和toString方法

在Java中可以直接輸出類對象。
舉個栗子:
先定義一個簡單的類,用於輸出:

class Person{
    private int num;
    public Person(int num){
        this.num = num;
    }
}

在main函數中直接輸出Person類對象:

public class Test{
    public static void main(String[] args){
        Person obj = new Person(1024);
        System.out.println(obj);//直接輸出類對象
    }
}
============輸出結果=============
Person@1f32e575

實際上這裏隱式用到了Object類裏的一個實例方法toString()
而所有的Java類都是Object類的子類,因此所有的Java對象都具有toString()方法
下面這兩種形式是等價的:

System.out.println(obj);
System.out.println(obj.toString());

Object類提供的toString()方法總是返回該對象實現類的"類名+@+hashCode"值。
如果想要輸出我們想要的結果,就可以重寫toString方法。

class Person{
    private int num;
    public Person(int num){
        this.num = num;
    }
    //重寫這個toString方法(Object類的一個實例方法)
    public String toString(){
        return "[" + "num=" + num + "]";//返回我們想要的結果
    }
}

編譯javac -d . Test.java
運行java Test
走~~~~

[num=12]

還是挺好玩的QAQ

==和equals方法

在Java中有兩種比較變量的方式:一個是利用==,另一個是利用equals()方法。

先來說說==運算符。
這玩意對於基本數據類型可以直接比較,C++中也經常使用。
但是如果用來比較引用類型變量的話,則只有兩個引用類型變量都指向同一個對象時,==判斷纔會返回true。舉個栗子:

var num1 = Integer.valueOf(1024);
var num2 = Integer.valueOf(1024);
System.out.println(num1 == num2);
============輸出結果=============
false

這裏的num1和num2是兩個引用類型變量,分別存儲着不同的地址,指向不同的位置,==直接判斷的是地址,因此輸出false

再講講equals方法
equals()方法是Object類提供的一個實例方法,因此所有的引用變量都可以調用該方法來判斷是否與其他引用變量相等。

var num1 = Integer.valueOf(1024);
var num2 = Integer.valueOf(1024);
System.out.println(num1.equals(num2));
============輸出結果==============
true

這裏的equals()方法判斷的不是地址,而是其指向的值。

自定義equals()方法
這個equals()既然是Object類的一個方法,而Object類是所有類的父類。
那麼我們也可以像自定義toString()方法那樣來自定義equals()咯~
舉個栗子:
我們先定義一個"人"類,有名字也有年齡,只要年齡相等,我們就判斷這兩個是true
即p1.equals(p2);應該返回true。

class Person{
    private int age;//年齡
    private String name;//姓名
    public Person(int age,String name){
        this.age = age;
        this.name = name;
    }
    //重寫equals方法,只判斷年齡就好了。
    //重寫要求返回值和形參都不能改變。
    public boolean equals(Object obj){
        return (this.age==obj.age?true:false);
    }
}
public class Test{
    public static void main(String[] args){
        //兩個人名字不同,但年齡相同。
        Person p1 = new Person(18, "Amy");
        Person p2 = new Person(18, "Tom");
        System.out.println(p1.equals(p2));
    }
}

編譯 運行 走~~~~~~

Exception in thread "main" java.lang.Error: Unresolved compilation problem:
    age cannot be resolved or is not a field

    at Person.equals(Test.java:32)
    at Test.main(Test.java:40)

emmm,竟然報錯了。
報錯信息指向這一段

obj.age

obj作爲父類,並沒有age這個變量。age是Person子類的特有成員!
但是想要重寫,就必須返回值和形參都不能改變,而形參裏面必有Object obj(equals文檔說的)
那怎麼辦呢??
強制類型轉換
也就是需要進行向下轉型,((Person)obj).age。
同時也要判斷obj是不是Person類的對象。

public boolean equals(Object obj){
    //判斷obj對象是不是Person類對象
    if(obj.getClass() == Person.class){
        return (this.age==((Person)obj).age?true:false);
    }
    return false;
}

其他不變 編譯 運行 走~~

true

小細節:爲什麼是((Person)obj).age而不是(Person)obj.age ?
這是因爲成員運算符(.)的優先級要比括號高。
(Person)obj.age就等同於(Person)(obj.age)

final成員變量

  • 前置知識:類初始化時,系統會爲該類的類變量分配內存,並分配默認值;當創建對象時,系統會爲該對象的實例變量分配內存,並分配默認值。
  • 關於final:用final修飾的成員變量,一旦有了初始值,就不能被重新賦值。
  • Java語法規定:final修飾的成員變量必須由程序員顯式地指定初始值。
變量類型 能指定初始值的位置
類變量 靜態初始化塊、聲明時
實例變量 非靜態初始化塊、聲明時、構造器中
/**********類變量*********/

//聲明時初始化
final static int num = 1024;
//靜態初始化塊中初始化
final static int num;
static{
    num = 1024;
}

/**********實例變量*********/

//聲明時初始化
final int num = 1024;
//非靜態初始化塊中初始化
final int num;
{
    num = 1024;
}
//構造器中初始化
final private int num;
public Test(int num){
    this.num = num;
}

特別注意:如果打算在構造器、初始化塊中對final成員變量進行初始化,則不要在初始化之前訪問final成員變量。

final int num;
{
    //還沒有初始化num,這樣會報錯
    System.out.println(num);
    //但是,Java允許通過方法來訪問final變量。
    //因此,通過這個fun()方法來間接訪問num
    //會導致系統自動爲num賦0
    fun();
    //fun()結束後已經被系統賦初始值了
    //這裏的num=1024失效
    num = 1024;
}
public void fun(){
    System.out.println(num);
}

關於final

final局部變量

final局部變量的賦值可以往後稍稍,先聲明,過一會再初始化。比如:

public static void main(String[] args){
    final int num;//先聲明
    num = 1024;//稍後再進行初始化
    System.out.println(num);

}

final引用類型變量

對於引用類型變量來講,它保存的僅僅是一個引用,final只保證這個引用類型變量所指向的地址不會改變。

“宏替換”的final變量

對於一個final變量來說,只要滿足三個條件:

  1. 使用final修飾符修飾。
  2. 在定義的同時指定了初始值。
  3. 該初始值可以在編譯時被確定下來。

這個final變量就不再是一個變量,而是相當於一個直接量,被存儲在常量池中。

//下面兩個都是final“宏變量”
final var a = 5+2;
final var str1 = "阿騰木"+"的";
final var str2 = "小世界"
//下面的初始值不能在編譯時確定
//不被當成宏變量
final var num = Integer.valueOf(1024);

final方法

final修飾的方法不可被重寫

一個“例外”:

public class A{
    //這裏用了private限定
    //對於B類是不可見的
    final private fun(){}
}
class B extends A{
    //因此這裏相當於定義了一個新的方法
    //屬於B子類
    public fun(){}
}

final類

final修飾的類不可以有子類

不可變類

不可變(immutable)類的意思是創建該類的實例後,該實例的實例變量是不可改變的。

public class Test{
    //不可變類需要用private和final修飾成員變量
    private final String str;
    //需要提供帶參數的構造器
    public Test(String str){
        this.str = str;
    }
    //僅爲該類的類成員提供getter方法
    //不提供setter方法
}

不可變類的實例在整個生命週期中永遠處於初始化狀態。
Java的8個包裝類和java.lang.String類都是不可變類

抽象類

  1. 抽象類只能被繼承,無法使用new進行實例化。
  2. abstract不能修飾變量。
  3. 有抽象方法的類只能是抽象類,抽象類可以沒有抽象方法。
  4. abstract與private不共存,因爲在子類中private方法不可見,則無法重寫。
  5. final與abstract不能共存,否則類不能被繼承,方法不能被重寫。
//Abs裏面有一個抽象方法,Abs只能是抽象類
public abstract Abs{
    //abstract不能修飾變量
    private String name;
    //抽象類的構造器不能用於創建實例
    //主要用於被其他子類調用
    public Abs(){}
    public Abs(String name){
        this.name = name;
    }
    //抽象方法不提供具體實現,交給子類實現
    public abstract String getName();
}
  • 模板模式:如果編寫一個抽象父類,父類提供了多個子類的通用方法,並把一個或多個方法留給子類實現。這種設計模式就叫做模板模式。

抽象類就是從多個具體類中抽象出來的父類
避免了子類設計的隨意性

接口

如果說抽象類是從多個類中抽象出來的模板,那麼接口抽象的更徹底。
接口是從多個相似類中抽象出來的規範,接口不提供任何實現
接口定義了一批類所需要遵守的規範,接口不關心這些類的內部狀態數據,也不關心這些類裏方法的實現細節,它只規定這批類裏必須提供某些方法,提供這些方法的類就可以滿足實際需要。

格式:

[修飾符] interface 接口名 extends 父接口1,父接口2...
{
    各種方法和常量
}

一些知識點:

  1. 修飾符只能是public,要麼沒有。因爲接口是多個類的“公共”規範。
  2. 接口不能有初始化塊、構造器。
  3. 接口成員變量只能是靜態常量(int a = 1; 則系統自動加public static final修飾)。
  4. 接口的成員變量只能在定義時指定默認值。
  5. 接口的方法只能是類方法、抽象方法(普通方法自動加abstract)、默認方法或私有方法。
  6. 接口中的普通方法(抽象方法)不能有實現;但類方法、默認方法、私有方法必須有方法實現。
public interface Itf{

    /**********成員變量**********/
    int num = 1024;
    //系統自動加public static final修飾
    //必須在定義時指定默認值

    /**********抽象方法**********/
    void fun();
    //系統自動加abstract

    /**********默認方法**********/
    default void def_fun(){
        System.out.println("默認方法");
    }
    //使用default修飾,必須有實現
    //系統自動加public修飾符

    /**********類方法**********/
    static void sta_fun(){
        System.out.println("類方法");
    }
    //系統自動加public修飾符

    /**********私有方法**********/
    private void pri_fun(){
        System.out.println("私有方法");
    }
    //可以使用static修飾符修飾
    //作爲工具方法使用
}

一個源文件內只能有一個public接口,主文件名必須與接口名相同。

使用接口

使用格式:

[修飾符] class 類名 extends 父類 implements 接口1,接口2...
{
    ......
}

接口的主要用途:

  1. 定義接口類型的變量,也可用於進行強制類型轉換
  2. 調用接口中定義的常量
  3. 接口中的方法被其他類實現

舉個栗子,使用上述Itf接口:

public class Test implements Itf{
    //實現接口的抽象方法,需要用public
    //否則會報錯:正在嘗試分配更低的訪問權限; 以前爲public
    public void abs_fun(){
        System.out.println("實現了接口中的抽象方法fun");
    }
    public static void main(String[] args){
        Itf obj = new Test();
        System.out.println(obj.num);//調用接口中的常量
        obj.abs_fun();//使用接口的對象調用了實現類所實現的抽象方法
        obj.def_fun();//可以直接調用接口的默認方法
        Itf.sta_fun();//使用接口來調用接口的類方法
    }
}
==========輸出結果==========
1024
實現了接口中的抽象方法fun
默認方法
類方法

實現接口方法時,必須使用public修飾符,因爲接口裏的方法都是public的,而子類重寫父類方法時訪問權限只能是更大或者相等。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章