Java物件和多型 (面向物件)
Java物件和多型 (面向物件)
面向物件基礎
面向物件程式設計(Object Oriented Programming)
物件基於類建立,類相當於一個模板,物件就是根據模板創建出來的實體(就像做月餅,我們要做一個月餅首先需要一個模具,模具就是我們的類,而做出來的月餅,就是類的實現,也叫做物件),類是抽象的資料型別,並不能代表某一個具體的事物,類是物件的一個模板。類具有自己的屬性,包括成員變數、成員方法等,我們可以呼叫類的成員方法來讓類進行一些操作。
Scanner sc = new Scanner(System.in); String str = sc.nextLine(); System.out.println("你輸入了:"+str); sc.close();
所有的物件,都需要通過new
關鍵字建立,基本資料型別不是物件!Java不是純面對物件語言!
不是基本型別的變數,都是引用型別,引用型別變數代表一個物件,而基本資料型別變數,儲存的是基本資料型別的值,我們可以通過引用來對物件進行操作。(最好不要理解為引用指向物件的地址,初學者不要談記憶體,學到JVM時再來討論)
物件佔用的記憶體由JVM統一管理,不需要手動釋放記憶體,當一個物件不再使用時(比如失去引用或是離開了作用域)會被JVM自動清理,記憶體管理更方便!
類的基本結構
為了快速掌握,我們自己建立一個自己的類,建立的類檔名稱應該和類名一致。
成員變數
在類中,可以包含許多的成員變數,也叫成員屬性,成員欄位(field)通過.
public class Test {
int age;
String name;
}
public static void main(String[] args) {
Test test = new Test();
test.name = "奧利給";
System.out.println(test.name);
}
成員變數預設帶有初始值,也可以自己定義初始值。
成員方法
我們之前的學習中接觸過方法(Method)嗎?主方法!
public static void main(String[] args) {
//Body
}
方法是語句的集合,是為了完成某件事情而存在的。完成某件事情,可以有結果,也可以做了就做了,不返回結果。比如計算兩個數字的和,我們需要得到計算後的結果,所以說方法需要有返回值;又比如,我們只想吧數字列印在控制檯,只需要列印就行,不用給我結果,所以說方法不需要有返回值。
方法的定義和使用
在類中,我們可以定義自己的方法,格式如下:
[返回值型別] 方法名稱([引數]){
//方法體
return 結果;
}
- 返回值型別:可以是引用型別和基本型別,還可以是void,表示沒有返回值
- 方法名稱:和識別符號的規則一致,和變數一樣,規範小寫字母開頭!
- 引數:例如方法需要計算兩個數的和,那麼我們就要把兩個數到底是什麼告訴方法,那麼它們就可以作為引數傳入方法
- 方法體:方法具體要乾的事情
- 結果:方法執行的結果通過return返回(如果返回型別為void,可以省略return)
非void方法中,return
關鍵字不一定需要放在最後,但是一定要保證方法在任何情況下都具有返回值!
int test(int a){
if(a > 0){
//缺少retrun語句!
}else{
return 0;
}
}
return
也能用來提前結束整個方法,無論此時程式執行到何處,無論return位於哪裡,都會立即結束個方法!
void main(String[] args) {
for (int i = 0; i < 10; i++) {
if(i == 1) return; //在迴圈內返回了!和break區別?
}
System.out.println("淦"); //還會到這裡嗎?
}
傳入方法的引數,如果是基本型別,會在呼叫方法的時候,對引數的值進行復制,方法中的引數變數,不是我們傳入的變數本身!
public static void main(String[] args) {
int a = 10, b = 20;
new Test().swap(a, b);
System.out.println("a="+a+", b="+b);
}
public class Test{
void swap(int a, int b){ //傳遞的僅僅是值而已!
int temp = a;
a = b;
b = temp;
}
}
傳入方法的引數,如果是引用型別,那麼傳入的依然是該物件的引用!(類似於C語言的指標)
public class B{
String name;
}
public class A{
void test(B b){ //傳遞的是物件的引用,而不是值
System.out.println(b.name);
}
}
public static void main(String[] args) {
int a = 10, b = 20;
B b = new B();
b.name = "lbw";
new A().test(b);
System.out.println("a="+a+", b="+b);
}
方法之間可以相互呼叫
void a(){
//xxxx
}
void b(){
a();
}
當方法在自己內部呼叫自己時,稱為遞迴呼叫(遞迴很危險,慎重!)
int a(){
return a();
}
成員方法和成員變數一樣,是屬於物件的,只能通過物件去呼叫!
物件設計練習
- 學生應該具有以下屬性:名字、年齡
- 學生應該具有以下行為:學習、運動、說話
方法的過載
一個類中可以包含多個同名的方法,但是需要的形式引數不一樣。(補充:形式引數就是定義方法需要的引數,實際引數就傳入的引數)方法的返回型別,可以相同,也可以不同,但是僅返回型別不同,是不允許的!
public class Test {
int a(){ //原本的方法
return 1;
}
int a(int i){ //ok,形參不同
return i;
}
void a(byte i){ //ok,返回型別和形參都不同
}
void a(){ //錯誤,僅返回值型別名稱不同不能過載
}
}
現在我們就可以使用不同的引數,但是支援呼叫同樣的方法,執行一樣的邏輯:
public class Test {
int sum(int a, int b){ //只有int支援,不靈活!
return a+b;
}
double sum(double a, double b){ //重寫一個double型別的,就支援小數計算了
return a+b;
}
}
現在我們有很多種重寫的方法,那麼傳入實參後,到底進了哪個方法呢?
public class Test {
void a(int i){
System.out.println("呼叫了int");
}
void a(short i){
System.out.println("呼叫了short");
}
void a(long i){
System.out.println("呼叫了long");
}
void a(char i){
System.out.println("呼叫了char");
}
void a(double i){
System.out.println("呼叫了double");
}
void a(float i){
System.out.println("呼叫了float");
}
public static void main(String[] args) {
Test test = new Test();
test.a(1); //直接輸入整數
test.a(1.0); //直接輸入小數
short s = 2;
test.a(s); //會對號入座嗎?
test.a(1.0F);
}
}
構造方法
構造方法(構造器)沒有返回值,也可以理解為,返回的是當前物件的引用!每一個類都預設自帶一個無參構造方法。
//反編譯結果
package com.test;
public class Test {
public Test() { //即使你什麼都不編寫,也自帶一個無參構造方法,只是預設是隱藏的
}
}
反編譯其實就是把我們編譯好的class檔案變回Java原始碼。
Test test = new Test(); //實際上存在Test()這個的方法,new關鍵字就是用來建立並得到引用的
// new + 你想要使用的構造方法
這種方法沒有寫明返回值,但是每個類都必須具有這個方法!只有呼叫類的構造方法,才能建立類的物件!
類要在一開始準備的所有東西,都會在構造方法裡面執行,完成構造方法的內容後,才能創建出物件!
一般最常用的就是給成員屬性賦初始值:
public class Student {
String name;
Student(){
name = "傘兵一號";
}
}
我們可以手動指定有參構造,當遇到名稱衝突時,需要用到this關鍵字
public class Student {
String name;
Student(String name){ //形參和類成員變數衝突了,Java會優先使用形式引數定義的變數!
this.name = name; //通過this指代當前的物件屬性,this就代表當前物件
}
}
//idea 右鍵快速生成!
注意,this只能用於指代當前物件的內容,因此,只有屬於物件擁有的部分才可以使用this,也就是說,只能在類的成員方法中使用this,不能在靜態方法中使用this關鍵字。
在我們定義了新的有參構造之後,預設的無參構造會被覆蓋!
//反編譯後依然只有我們定義的有參構造!
如果同時需要有參和無參構造,那麼就需要用到方法的過載!手動再去定義一個無參構造。
public class Student {
String name;
Student(){
}
Student(String name){
this.name = name;
}
}
成員變數的初始化始終在構造方法執行之前
public class Student {
String a = "sadasa";
Student(){
System.out.println(a);
}
public static void main(String[] args) {
Student s = new Student();
}
}
靜態變數和靜態方法
靜態變數和靜態方法是類具有的屬性(後面還會提到靜態類、靜態程式碼塊),也可以理解為是所有物件共享的內容。我們通過使用static
關鍵字來宣告一個變數或一個方法為靜態的,一旦被宣告為靜態,那麼通過這個類建立的所有物件,操作的都是同一個目標,也就是說,物件再多,也只有這一個靜態的變數或方法。那麼,一個物件改變了靜態變數的值,那麼其他的物件讀取的就是被改變的值。
public class Student {
static int a;
}
public static void main(String[] args) {
Student s1 = new Student();
s1.a = 10;
Student s2 = new Student();
System.out.println(s2.a);
}
不推薦使用物件來呼叫,被標記為靜態的內容,可以直接通過類名.xxx
的形式訪問
public static void main(String[] args) {
Student.a = 10;
System.out.println(Student.a);
}
簡述類載入機制
類並不是在一開始就全部載入好,而是在需要時才會去載入(提升速度)以下情況會載入類:
- 訪問類的靜態變數,或者為靜態變數賦值
- new 建立類的例項(隱式載入)
- 呼叫類的靜態方法
- 子類初始化時
- 其他的情況會在講到反射時介紹
所有被標記為靜態的內容,會在類剛載入的時候就分配,而不是在物件建立的時候分配,所以說靜態內容一定會在第一個物件初始化之前完成載入。
public class Student {
static int a = test(); //直接呼叫靜態方法,只能呼叫靜態方法
Student(){
System.out.println("構造類物件");
}
static int test(){ //靜態方法剛載入時就有了
System.out.println("初始化變數a");
return 1;
}
}
思考:下面這種情況下,程式能正常執行嗎?如果能,會輸出什麼內容?
public class Student {
static int a = test();
static int test(){
return a;
}
public static void main(String[] args) {
System.out.println(Student.a);
}
}
定義和賦值是兩個階段,在定義時會使用預設值(上面講的,類的成員變數會有預設值)定義出來之後,如果發現有賦值語句,再進行賦值,而這時,呼叫了靜態方法,所以說會先去載入靜態方法,靜態方法呼叫時拿到a,而a這時僅僅是剛定義,所以說還是初始值,最後得到0
程式碼塊和靜態程式碼塊
程式碼塊在物件建立時執行,也是屬於類的內容,但是它在構造方法執行之前執行(和成員變數初始值一樣),且每建立一個物件時,只執行一次!(相當於構造之前的準備工作)
public class Student {
{
System.out.println("我是程式碼塊");
}
Student(){
System.out.println("我是構造方法");
}
}
靜態程式碼塊和上面的靜態方法和靜態變數一樣,在類剛載入時就會呼叫;
public class Student {
static int a;
static {
a = 10;
}
public static void main(String[] args) {
System.out.println(Student.a);
}
}
String和StringBuilder類
字串類是一個比較特殊的類,他是Java中唯一過載運算子的類!(Java不支援運算子過載,String是特例)
String的物件直接支援使用+
或+=
運算子來進行拼接,並形成新的String物件!(String的字串是不可變的!)
String a = "dasdsa", b = "dasdasdsa";
String l = a+b;
System.out.println(l);
大量進行字串的拼接似乎不太好,編譯器是很聰明的,String的拼接有可能會被編譯器優化為StringBuilder來減少物件建立(物件頻繁建立時很費時間同時佔記憶體的!)
String result="String"+"and"; //會被優化成一句!
String str1="String";
String str2="and";
String result=str1+str2;
//變數隨時可變,在編譯時無法確定result的值,那麼只能在執行時再去確定
String str1="String";
String str2="and";
String result=(new StringBuilder(String.valueOf(str1))).append(str2).toString();
//使用StringBuilder,會採用類似於第一種實現,顯然會更快!
StringBuilder也是一個類,但是它能夠儲存可變長度的字串!
StringBuilder builder = new StringBuilder();
builder
.append("a")
.append("bc")
.append("d"); //鏈式呼叫
String str = builder.toString();
System.out.println(str);
包和訪問控制
包宣告和匯入
包其實就是用來區分類位置的東西,也可以用來將我們的類進行分類,類似於C++中的namespace!
package com.test;
public class Test{
}
包其實是資料夾,比如com.test就是一個com資料夾中包含一個test資料夾,再包含我們Test類。
一般包按照個人或是公司域名的規則倒過來寫 頂級域名.一級域名.二級域名
com.java.xxxx
如果需要使用其他包裡面的類,那麼我們需要import
(類似於C/C++中的include)
import com.test.Student;
也可以匯入包下的全部(一般匯入會由編譯器自帶幫我們補全,但是一定要記得我們需要導包!)
import com.test.*
Java預設為我們匯入了以下的包,不需要去宣告
import java.lang.*
靜態匯入
靜態匯入可以直接匯入某個類的靜態方法或者是靜態變數,匯入後,相當於這個方法或是類在定義在當前類中,可以直接呼叫該方法。
import static com.test.ui.Student.test;
public class Main {
public static void main(String[] args) {
test();
}
}
靜態匯入不會進行類的初始化!
訪問控制
Java支援對類屬性訪問的保護,也就是說,不希望外部類訪問類中的屬性或是方法,只允許內部呼叫,這種情況下我們就需要用到許可權控制符。
![image-20210819160939950](/Users/nagocoler/Library/Application Support/typora-user-images/image-20210819160939950.png)
許可權控制符可以宣告在方法、成員變數、類前面,一旦宣告private,只能類內部訪問!
public class Student {
private int a = 10; //具有私有訪問許可權,只能類內部訪問
}
public static void main(String[] args) {
Student s = new Student();
System.out.println(s.a); //還可以訪問嗎?
}
和檔名稱相同的類,只能是public,並且一個java檔案中只能有一個public class!
// Student.java
public class Student {
}
class Test{ //不能新增許可權修飾符!只能是default
}
陣列型別
假設出現一種情況,我想記錄100個數字,定義100個變數還可行嗎?
我們可以使用到陣列,陣列是相同型別資料的有序集合。陣列可以代表任何相同型別的一組內容(包括引用型別和基本型別)其中存放的每一個數據稱為陣列的一個元素,陣列的下標是從0開始,也就是第一個元素的索引是0!
int[] arr = new int[10]; //需要new關鍵字來建立!
String[] arr2 = new String[10];
陣列本身也是類(程式設計不可見,C++寫的),不是基本資料型別!
int[] arr = new int[10];
System.out.println(arr.length); //陣列有成員變數!
System.out.println(arr.toString()); //陣列有成員方法!
一維陣列
一維陣列中,元素是依次排列的(線性),每個陣列元素可以通過下標來訪問!宣告格式如下:
型別[] 變數名稱 = new 型別[陣列大小];
型別 變數名稱n = new 型別[陣列大小]; //支援C語言樣式,但不推薦!
型別[] 變數名稱 = new 型別[]{...}; //靜態初始化(直接指定值和大小)
型別[] 變數名稱 = {...}; //同上,但是隻能在定義時賦值
創建出來的陣列每個元素都有預設值(規則和類的成員變數一樣,C語言建立的陣列需要手動設定預設值),我們可以通過下標去訪問:
int[] arr = new int[10];
arr[0] = 626;
System.out.println(arr[0]);
System.out.println(arr[1]);
我們可以通過陣列變數名稱.length
來獲取當前陣列長度:
int[] arr = new int[]{1, 2, 3};
System.out.println(arr.length); //列印length成員變數的值
陣列在建立時,就固定長度,不可更改!訪問超出陣列長度的內容,會出現錯誤!
String[] arr = new String[10];
System.out.println(arr[10]); //出現異常!
//Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: 11
// at com.test.Application.main(Application.java:7)
思考:能不能直接修改length的值來實現動態擴容呢?
int[] arr = new int[]{1, 2, 3};
arr.length = 10;
陣列做實參,因為陣列也是類,所以形參得到的是陣列的引用而不是複製的陣列,操作的依然是陣列物件本身
public static void main(String[] args) {
int[] arr = new int[]{1, 2, 3};
test(arr);
System.out.println(arr[0]);
}
private static void test(int[] arr){
arr[0] = 2934;
}
陣列的遍歷
如果我們想要快速列印陣列中的每一個元素,又怎麼辦呢?
傳統for迴圈
我們很容易就聯想到for迴圈
int[] arr = new int[]{1, 2, 3};
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
foreach
傳統for迴圈雖然可控性高,但是不夠省事,要寫一大堆東西,有沒有一種省事的寫法呢?
int[] arr = new int[]{1, 2, 3};
for (int i : arr) {
System.out.println(i);
}
foreach屬於增強型的for迴圈,它使得程式碼更簡潔,同時我們能直接拿到陣列中的每一個數字。
二維陣列
二維陣列其實就是存放陣列的陣列,每一個元素都存放一個數組的引用,也就相當於變成了一個平面。
//三行兩列
int[][] arr = { {1, 2},
{3, 4},
{5, 6}};
System.out.println(arr[2][1]);
二維陣列的遍歷同一維陣列一樣,只不過需要巢狀迴圈!
int[][] arr = new int[][]{ {1, 2},
{3, 4},
{5, 6}};
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 2; j++) {
System.out.println(arr[i][j]);
}
}
多維陣列
不止二維陣列,還存在三維陣列,也就是存放陣列的陣列的陣列,原理同二維陣列一樣,逐級訪問即可。
可變長引數
可變長引數其實就是陣列的一種應用,我們可以指定方法的形參為一個可變長引數,要求實參可以根據情況動態填入0個或多個,而不是固定的數量
public static void main(String[] args) {
test("AAA", "BBB", "CCC"); //可變長,最後都會被自動封裝成一個數組
}
private static void test(String... test){
System.out.println(test[0]); //其實引數就是一個數組
}
由於是陣列,所以說只能使用一種型別的可變長引數,並且可變長引數只能放在最後一位!
實戰:三大基本排序演算法
現在我們有一個數組,但是數組裡面的資料是亂序排列的,如何使它變得有序?
int[] arr = {8, 5, 0, 1, 4, 9, 2, 3, 6, 7};
排序是程式設計的一個重要技能,掌握排序演算法,你的技術才能更上一層樓,很多的專案都需要用到排序!三大排序演算法:
- 氣泡排序
氣泡排序就是冒泡,其實就是不斷使得我們無序陣列中的最大數向前移動,經歷n輪迴圈逐漸將每一個數推向最前。
- 插入排序
插入排序其實就跟我們打牌是一樣的,我們在摸牌的時候,牌堆是亂序的,但是我們一張一張摸到手中進行排序,使得它變成了有序的!
- 選擇排序
選擇排序其實就是每次都選擇當前陣列中最大的數排到最前面!
封裝、繼承和多型
封裝、繼承和多型是面向物件程式設計的三大特性。
封裝
封裝的目的是為了保證變數的安全性,使用者不必在意具體實現細節,而只是通過外部介面即可訪問類的成員,如果不進行封裝,類中的例項變數可以直接檢視和修改,可能給整個程式碼帶來不好的影響,因此在編寫類時一般將成員變數私有化,外部類需要同getter和setter方法來檢視和設定變數。
設想:學生小明已經建立成功,正常情況下能隨便改他的名字和年齡嗎?
public class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
}
也就是說,外部現在只能通過呼叫我定義的方法來獲取成員屬性,而我們可以在這個方法中進行一些額外的操作,比如小明可以修改名字,但是名字中不能包含"小"這個字。
public void setName(String name) {
if(name.contains("小")) return;
this.name = name;
}
單獨給外部開放設定名稱的方法,因為我還需要做一些額外的處理,所以說不能給外部直接操作成員變數的許可權!
封裝思想其實就是把實現細節給隱藏了,外部只需知道這個方法是什麼作用,而無需關心實現。
封裝就是通過訪問許可權控制來實現的。
繼承
繼承屬於非常重要的內容,在定義不同類的時候存在一些相同屬性,為了方便使用可以將這些共同屬性抽象成一個父類,在定義其他子類時可以繼承自該父類,減少程式碼的重複定義,子類可以使用父類中非私有的成員。
現在學生分為兩種,藝術生和體育生,他們都是學生的分支,但是他們都有自己的方法:
public class SportsStudent extends Student{ //通過extends關鍵字來繼承父類
public SportsStudent(String name, int age) {
super(name, age); //必須先通過super關鍵字(指代父類),實現父類的構造方法!
}
public void exercise(){
System.out.println("我超勇的!");
}
}
public class ArtStudent extends Student{
public ArtStudent(String name, int age) {
super(name, age);
}
public void art(){
System.out.println("隨手畫個畢加索!");
}
}
子類具有父類的全部屬性,protected可見但外部無法使用(包括private
屬性,不可見,無法使用),同時子類還能有自己的方法。繼承只能繼承一個父類,不支援多繼承!
每一個子類必須定義一個實現父類構造方法的構造方法,也就是需要在構造方法開始使用super()
,如果父類使用的是預設構造方法,那麼子類不用手動指明。
所有類都預設繼承自Object類,除非手動指定型別,但是依然改變不了最頂層的父類是Object類。所有類都包含Object類中的方法,比如:
public static void main(String[] args) {
Object obj = new Object;
System.out.println(obj.hashCode()); //求物件的hashcode,預設是物件的記憶體地址
System.out.println(obj.equals(obj)); //比較物件是否相同,預設比較的是物件的記憶體地址,也就是等同於 ==
System.out.println(obj.toString()); //將物件轉換為字串,預設生成物件的類名稱+hashcode
}
關於Object類的其他方法,我們會在Java多執行緒中再來提及。
多型
多型是同一個行為具有多個不同表現形式或形態的能力。也就是同樣的方法,由於實現類不同,執行的結果也不同!
方法的重寫
我們之前學習了方法的過載,方法的重寫和過載是不一樣的,過載是原有的方法邏輯不變的情況下,支援更多引數的實現,而重寫是直接覆蓋原有方法!
//父類中的study
public void study(){
System.out.println("學習");
}
//子類中的study
@Override //宣告這個方法是重寫的,但是可以不要,我們現階段不接觸
public void study(){
System.out.println("給你看點好康的");
}
再次定義同樣的方法後,父類的方法就被覆蓋!子類還可以給父類方法提升訪問許可權!
public static void main(String[] args) {
SportsStudent student = new SportsStudent("lbw", 20);
student.study(); //輸出子類定義的內容
}
思考:靜態方法能被重寫嗎?
當我們在重寫方法時,不僅想使用我們自己的邏輯,同時還希望執行父類的邏輯(也就是呼叫父類的方法)怎麼辦呢?
public void study(){
super.study();
System.out.println("給你看點好康的");
}
同理,如果想訪問父類的成員變數,也可以使用super關鍵字來訪問,注意,子類可以具有和父類相同的成員變數!而在方法中訪問的預設是 形參列表中 > 當前類的成員變數 > 父類成員變數
public void setTest(int test){
test = 1;
this.test = 1;
super.test = 1;
}
再談型別轉換
我們曾經學習過基本資料型別的型別轉換,支援一種資料型別轉換為另一種資料型別,而我們的類也是支援型別轉換的(僅限於存在親緣關係的類之間進行轉換)比如子類可以直接向上轉型:
Student student = new SportsStudent("lbw", 20); //父類變數引用子類例項
student.study(); //得到依然是具體實現的結果,而不是當前型別的結果
我們也可以把已經明確是由哪個類實現的父類引用,強制轉換為對應的型別:
Student student = new SportsStudent("lbw", 20); //是由SportsStudent進行實現的
//... do something...
SportsStudent ps = (SportsStudent)student; //讓它變成一個具體的子類
ps.sport(); //呼叫具體實現類的方法
這樣的型別轉換稱為向下轉型。
instanceof關鍵字
那麼我們如果只是得到一個父類引用,但是不知道它到底是哪一個子類的實現怎麼辦?我們可以使用instanceof關鍵字來實現,它能夠進行型別判斷!
private static void test(Student student){
if (student instanceof SportsStudent){
SportsStudent sportsStudent = (SportsStudent) student;
sportsStudent.sport();
}else if (student instanceof ArtStudent){
ArtStudent artStudent = (ArtStudent) student;
artStudent.art();
}
}
通過進行型別判斷,我們就可以明確類的具體實現到底是哪個類!
思考:student instanceof Student
的結果是什麼?
再談final關鍵字
我們目前只知道final
關鍵字能夠使得一個變數的值不可更改,那麼如果在類前面宣告final,會發生什麼?
public final class Student { //類被宣告為終態,那麼它還能被繼承嗎
}
類一旦被宣告為終態,將無法再被繼承,不允許子類的存在!而方法被宣告為final呢?
public final void study(){ //還能重寫嗎
System.out.println("學習");
}
如果類的成員屬性被宣告為final,那麼必須在構造方法中或是在定義時賦初始值!
private final String name; //引用型別不允許再指向其他物件
private final int age; //基本型別值不允許發生改變
public Student(String name, int age) {
this.name = name;
this.age = age;
}
學習完封裝繼承和多型之後,我們推薦在不會再發生改變的成員屬性上新增final關鍵字,JVM會對添加了final關鍵字的屬性進行優化!
抽象類
類本身就是一種抽象,而抽象類,把類還要抽象,也就是說,抽象類可以只保留特徵,而不保留具體呈現形態,比如方法可以定義好,但是我可以不去實現它,而是交由子類來進行實現!
public abstract class Student { //抽象類
public abstract void test(); //抽象方法
}
通過使用abstract
關鍵字來表明一個類是一個抽象類,抽象類可以使用abstract
關鍵字來表明一個方法為抽象方法,也可以定義普通方法,抽象方法不需要編寫具體實現(無方法體)但是必須由子類實現(除非子類也是一個抽象類)!
抽象類由於不是具體的類定義,因此無法直接通過new關鍵字來建立物件!
Student s = new Student(){ //只能直接建立帶實現的匿名內部類!
public void test(){
}
}
因此,抽象類一般只用作繼承使用!抽象類使得繼承關係之間更加明確:
public void study(){ //現在只能由子類編寫,父類沒有定義,更加明確了多型的定義!同一個方法多種實現!
System.out.println("給你看點好康的");
}
介面
介面甚至比抽象類還抽象,他只代表某個確切的功能!也就是隻包含方法的定義,甚至都不是一個類!介面包含了一些列方法的具體定義,類可以實現這個介面,表示類支援介面代表的功能(類似於一個外掛,只能作為一個附屬功能加在主體上,同時具體實現還需要由主體來實現)
public interface Eat {
void eat();
}
通過使用interface
關鍵字來表明是一個介面(注意,這裡class關鍵字被替換為了interface)介面只能包含public
許可權的抽象方法!(Java8以後可以有預設實現)我們可以通過宣告default
關鍵字來給抽象方法一個預設實現:
public interface Eat {
default void eat(){
//do something...
}
}
介面中定義的變數,預設為public static final
public interface Eat {
int a = 1;
void eat();
}
一個類可以實現很多個介面,但是不能理解為多繼承!(實際上實現介面是附加功能,和繼承的概念有一定出入,頂多說是多繼承的一種替代方案)一個類可以附加很多個功能!
public class SportsStudent extends Student implements Eat, ...{
@Override
public void eat() {
}
}
類通過implements
關鍵字來宣告實現的介面!每個介面之間用逗號隔開!
實現介面的類也能通過instanceof關鍵字判斷,也支援向上和向下轉型!
內部類
類中可以存在一個類!各種各樣的長相怪異的程式碼就是從這裡開始出現的!
成員內部類
我們的類中可以在巢狀一個類:
public class Test {
class Inner{ //類中定義的一個內部類
}
}
成員內部類和成員變數和成員方法一樣,都是屬於物件的,也就是說,必須存在外部物件,才能建立內部類的物件!
public static void main(String[] args) {
Test test = new Test();
Test.Inner inner = test.new Inner(); //寫法有那麼一絲怪異,但是沒毛病!
}
靜態內部類
靜態內部類其實就和類中的靜態變數和靜態方法一樣,是屬於類擁有的,我們可以直接通過類名.
去訪問:
public class Test {
static class Inner{
}
}
public static void main(String[] args) {
Test.Inner inner = new Test.Inner(); //不用再建立外部類物件了!
}
區域性內部類
對,你沒猜錯,就是和區域性變數一樣噠~
public class Test {
public void test(){
class Inner{
}
Inner inner = new Inner();
}
}
反正我是沒用過!內部類 -> 累不累 -> 反正我累了!
匿名內部類
匿名內部類才是我們的重點,也是實現lambda表示式的原理!匿名內部類其實就是在new的時候,直接對介面或是抽象類的實現:
public static void main(String[] args) {
Eat eat = new Eat() {
@Override
public void eat() {
//DO something...
}
};
}
我們不用單獨去建立一個類來實現,而是可以直接在new的時候寫對應的實現!但是,這樣寫,無法實現複用,只能在這裡使用!
lambda表示式
讀作λ
表示式,它其實就是我們介面匿名實現的簡化,比如說:
public static void main(String[] args) {
Eat eat = new Eat() {
@Override
public void eat() {
//DO something...
}
};
}
public static void main(String[] args) {
Eat eat = () -> {}; //等價於上述內容
}
lambda表示式(匿名內部類)只能訪問外部的final型別或是隱式final型別的區域性變數!
為了方便,JDK預設就為我們提供了專門寫函式式的介面,這裡只介紹Consumer
列舉類
假設現在我們想給小明新增一個狀態(跑步、學習、睡覺),外部可以實時獲取小明的狀態:
public class Student {
private final String name;
private final int age;
private String status;
//...
public void setStatus(String status) {
this.status = status;
}
public String getStatus() {
return status;
}
}
但是這樣會出現一個問題,如果我們僅僅是儲存字串,似乎外部可以不按照我們規則,傳入一些其他的字串。這顯然是不夠嚴謹的!
有沒有一種辦法,能夠更好地去實現這樣的狀態標記呢?我們希望開發者拿到使用的就是我們定義好的狀態,我們可以使用列舉類!
public enum Status {
RUNNING, STUDY, SLEEP //直接寫每個狀態的名字即可,分號可以不打,但是推薦打上
}
使用列舉類也非常方便,我們只需要直接訪問即可
public class Student {
private final String name;
private final int age;
private Status status;
//...
public void setStatus(Status status) { //不再是String,而是我們指定的列舉型別
this.status = status;
}
public Status getStatus() {
return status;
}
}
public static void main(String[] args) {
Student student = new Student("小明", 18);
student.setStatus(Status.RUNNING);
System.out.println(student.getStatus());
}
列舉型別使用起來就非常方便了,其實列舉型別的本質就是一個普通的類,但是它繼承自Enum
類,我們定義的每一個狀態其實就是一個public static final
的Status型別成員變數!
// Compiled from "Status.java"
public final class com.test.Status extends java.lang.Enum<com.test.Status> {
public static final com.test.Status RUNNING;
public static final com.test.Status STUDY;
public static final com.test.Status SLEEP;
public static com.test.Status[] values();
public static com.test.Status valueOf(java.lang.String);
static {};
}
既然列舉型別是普通的類,那麼我們也可以給列舉型別新增獨有的成員方法
public enum Status {
RUNNING("睡覺"), STUDY("學習"), SLEEP("睡覺"); //無參構造方法被覆蓋,建立列舉需要新增引數(本質就是呼叫的構造方法!)
private final String name; //列舉的成員變數
Status(String name){ //覆蓋原有構造方法(預設private,只能內部使用!)
this.name = name;
}
public String getName() { //獲取封裝的成員變數
return name;
}
}
public static void main(String[] args) {
Student student = new Student("小明", 18);
student.setStatus(Status.RUNNING);
System.out.println(student.getStatus().getName());
}
列舉類還自帶一些繼承下來的實用方法
Status.valueOf("") //將名稱相同的字串轉換為列舉
Status.values() //快速獲取所有的列舉
基本型別包裝類
Java並不是純面向物件的語言,雖然Java語言是一個面向物件的語言,但是Java中的基本資料型別卻不是面向物件的。在學習泛型和集合之前,基本型別的包裝類是一定要講解的內容!
我們的基本型別,如果想通過物件的形式去使用他們,Java提供的基本型別包裝類,使得Java能夠更好的體現面向物件的思想,同時也使得基本型別能夠支援物件操作!
- byte -> Byte
- boolean -> Boolean
- short -> Short
- char -> Character
- int -> Integer
- long -> Long
- float -> Float
- double -> Double
包裝類實際上就行將我們的基本資料型別,封裝成一個類(運用了封裝的思想)
private final int value; //Integer內部其實本質還是存了一個基本型別的資料,但是我們不能直接操作
public Integer(int value) {
this.value = value;
}
現在我們操作的就是Integer物件而不是一個int基本型別了!
public static void main(String[] args) {
Integer i = 1; //包裝型別可以直接接收對應型別的資料,並變為一個物件!
System.out.println(i + i); //包裝型別可以直接被當做一個基本型別進行操作!
}
自動裝箱和拆箱
那麼為什麼包裝型別能直接使用一個具體值來賦值呢?其實依靠的是自動裝箱和拆箱機制
Integer i = 1; //其實這裡只是簡寫了而已
Integer i = Integer.valueOf(1); //編譯後真正的樣子
呼叫valueOf來生成一個Integer物件!
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high) //注意,Java為了優化,有一個快取機制,如果是在-128~127之間的數,會直接使用已經快取好的物件,而不是再去建立新的!(面試常考)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i); //返回一個新建立好的物件
}
而如果使用包裝類來進行運算,或是賦值給一個基本型別變數,會進行自動拆箱:
public static void main(String[] args) {
Integer i = Integer.valueOf(1);
int a = i; //簡寫
int a = i.intValue(); //編譯後實際的程式碼
long c = i.longValue(); //其他型別也有!
}
既然現在是包裝型別了,那麼我們還能使用==
來判斷兩個數是否相等嗎?
public static void main(String[] args) {
Integer i1 = 28914;
Integer i2 = 28914;
System.out.println(i1 == i2); //實際上判斷是兩個物件是否為同一個物件(記憶體地址是否相同)
System.out.println(i1.equals(i2)); //這個才是真正的值判斷!
}
注意IntegerCache帶來的影響!
思考:下面這種情況結果會是什麼?
public static void main(String[] args) {
Integer i1 = 28914;
Integer i2 = 28914;
System.out.println(i1+1 == i2+1);
}
在集合類的學習中,我們還會繼續用到我們的包裝型別!