歡迎閱讀原文:www.yuque.com/dobbykim/java-basic/...
一:Java語言的跨平臺性與位元組碼概述
JVM,機器碼與位元組碼
JVM 即: Java Virtual Machine 也就是 Java 虛擬機器。
Java 語言有一個特點:平臺無關性 。JVM 就是實現這一個特點的關鍵。
我們知道,軟體執行依賴於作業系統(Operating System)。早期的開發者使用的程式語言並不具備良好的移植性,如果想在不同的作業系統平臺上面執行功能相同的應用,需要為每一種平臺都編寫可以被平臺識別認知的程式碼。一般編譯器會直接將程式的原始碼編譯成計算機可以直接執行的 機器碼。
Java 語言則具有平臺無關性,也就是所謂的 Write Once,Run Anywhere (一次編譯,到處執行)。Java 編譯器並不是將 Java 原始碼編譯為由 0,1 序列構成的計算機可直接執行的機器碼,而是將其編譯為副檔名為 .class 的位元組碼。如果想要執行位元組碼檔案,平臺上面必須安裝 JVM,JVM 直譯器會將位元組碼解釋成依賴於平臺的機器碼。
從上圖也可以看出,不同作業系統需要安裝基於該作業系統的 JVM。JVM 遮蔽了作業系統之間的差異,實現了 Java 語言的跨平臺性。
二:Java語言的基本單元——類與包
類(class)
在 Java 語言中,類是最小的基本單元
一個最簡單的類
public class Cat {
}
包(package)
為了更好地組織類,Java 語言提供了包的機制,用於區別類名的名稱空間。
示例:
一個屬於my.cute 包下的 Cat 類
package my.cute;
public class Cat {
}
在 Java 語言中,包一般會用域名的反序來命名。
例如:
package com.alibaba.fastjson;
這樣可以避免類名衝突。
三:Java語言的基本結構——包的意義
包的意義與作用:
把功能相似或相關的類組織在同一個包中,方便查詢與管理
同一個包中類名要求不能相同,但是不同包中的類名可以相同;當同時呼叫兩個不同包中相同類名的類時,應該加上包名加以區別。因此,包也可以避免類名衝突。 示例: 在不同包下有著相同類名的類,我們可以使用全限定類名(Full Qualified Name)加以區分。
package com.github.hcsp; public class Home { com.github.hcsp.pet1.Cat cat1; com.github.hcsp.pet2.Cat cat2; }
包限定了訪問許可權
四:在Java中引入第三方包
示例:在程式中引入一個第三方包中的類:org.apache.commons.langs.StringUtils
。
如果使用 Maven 進行專案管理,我們首先需要在 pom 檔案中引入 Apache Commons Lang 包的依賴
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.11</version>
</dependency>
然後回到我們的程式碼中,使用 import 關鍵字即可引入第三方包
如下所示:
package com.github.hcsp;
import org.apache.commons.lang3.StringUtils;
public class Main {
public static void main(String[] args) {
System.out.println("Empty string is empty: " + StringUtils.isEmpty(""));
}
}
程式輸入結果:
Empty string is empty: true
我們發現,上述程式,無論是 String 還是 System 類都沒有通過 import 和書寫全限定類名,而是直接使用。
那是因為 String 和 System 類放在 java.lang 包下。
Java 語言規定:如果一個類放在 java.lang 包下,我們就可以不用寫 import 和全限定類名,而是直接使用。
五:方法,靜態方法與靜態成員變數
main 方法
Java 程式執行的入口是 main 方法
程式示例:
package com.github.hcsp;
public class Main {
public static void main(String[] args) {
}
}
main 方法簽名:
public 修飾符:public 代表公開的類,沒有限制可以自由呼叫
static 修飾符:static 代表靜態的,用 static 修飾的方法和變數不和任何物件繫結,代表我們不用建立任何物件就可以呼叫
void:說明該方法沒有返回值
String[] args:傳遞給 main 方法的命令列引數,表示為字串陣列
靜態方法與靜態成員變數
程式示例一:
package com.github.hcsp;
public class Main {
public static void main(String[] args) {
int i = 0;
add(i);
add(i);
add(i);
System.out.println(i);
}
public static void add(int i){
i++;
}
}
程式輸出結果為:
0
原因在於,add 方法中傳遞的引數 i 僅作用在 add 方法塊內,所以無法對 main 方法內的變數 i 產生任何影響。
程式示例二:
package com.github.hcsp;
public class Main {
public static int i = 0;
public static void main(String[] args) {
add();
add();
add();
}
public static void add() {
i++;
}
}
該程式執行的結果為:
3
static 修飾的方法或成員變數都獨立於該類的任何物件,或是說不依賴於任何物件,它是存在於 JVM 中的一塊記憶體,是一個全域性的儲存單元,可以被所有物件所共享。所以 add 方法會對其產生影響。
六:物件,構造器與成員變數
Java 是一個物件導向的語言。
類是一種抽象的概念,物件則是類的例項,是一種具體的概念。
示例:建立一個物件
Cat
package com.github.hcsp;
public class Cat {
private String name;
public Cat(){
}
public Cat(String name) {
this.name = name;
}
}
Main
package com.github.hcsp;
public class Main {
public static void main(String[] args) {
Cat cat = new Cat("Tom");
}
}
建立物件最簡單的一種方式就是:使用 new 關鍵字
在本示例中,我們建立了一個名字叫 Tom 的 Cat 物件,呼叫了有參的構造器。
如果我們不在 Cat 類中宣告任何構造器,那麼編譯器會自動為我們宣告一個無參的構造器;相反,如果我們宣告瞭任何有參的構造器,編譯器都不會再為我們自動宣告這個無參的構造器了,需要我們自己進行宣告。
七:例項方法與空指標異常
示例程式:
Cat
package com.github.hcsp;
public class Cat {
private String name;
public Cat() {
}
public Cat(String name) {
this.name = name;
}
public void meow() {
System.out.println("喵,我是 " + name);
}
}
Main
package com.github.hcsp;
public class Main {
public static void main(String[] args) {
Cat cat1 = new Cat("Tom");
Cat cat2 = new Cat("Harry");
cat1.meow();
cat2.meow();
}
}
程式輸出結果:
喵,我是 Tom
喵,我是 Harry
我們接下來看這個程式:
Cat
package com.github.hcsp;
public class Cat {
private String name;
public Cat() {
}
public Cat(String name) {
this.name = name;
}
public void meow() {
System.out.println("喵,我是 " + name + ", 我的名字的長度是:" + name.length());
}
}
package com.github.hcsp;
public class Main {
public static void main(String[] args) {
Cat cat1 = new Cat();
Cat cat2 = new Cat("Tom");
cat1.meow();
cat2.meow();
}
}
執行程式:
Exception in thread "main" java.lang.NullPointerException
at com.github.hcspTest.Cat.meow(Cat.java:15)
at com.github.hcspTest.Main.main(Main.java:8)
我們會發現,該程式執行出現了異常,這個異常是 NullPointerException 即:空指標異常
原因在於 cat1 的 name 為 null,對於一個空的物件,我們呼叫這個物件的方法時,就會產生空指標異常。
規避空指標的方法很簡單,我們在可能會產生空指標的地方加入判空的邏輯處理即可:
public void meow(){
if(name == null){
System.out.println("我還沒有名字!");
}else {
System.out.println("喵,我是 " + name + ", 我的名字的長度是:" + name.length());
}
}
八:物件與引用詳解
引用(Reference)
舉個例子:
A a = new A();
a 就是引用,它指向了一個 A 物件。我們通過操作 a 這個引用來間接地操作它指向的物件。
示例程式:
Cat
package com.github.hcsp;
public class Cat {
public String name;
public Cat() {
}
public Cat(String name) {
this.name = name;
}
public void meow() {
System.out.println("喵,我是 " + name + ", 我的名字的長度是:" + name.length());
}
}
Home
package com.github.hcsp;
public class Home {
Cat cat;
public static void main(String[] args) {
Home home = new Home();
Cat mimi = new Cat();
home.cat = mimi;
mimi.name = "mimi";
}
}
該程式的記憶體圖分析如下:
深拷貝與淺拷貝
淺拷貝和深拷貝最根本的區別就是,拷貝出的東西是否是一個物件的複製實體,而不是引用。
舉個例子來形容下:
假設B是A的一個拷貝
在我們修改A的時候,如果B也跟著發生了變化,那麼就是淺拷貝,說明修改的是堆記憶體中的同一個值;
在我們修改A的時候,如果B沒有發生改變,那麼就是深拷貝,說明修改的是堆記憶體中不同的值
實現Cloneable
介面,重寫clone()
方法並呼叫,我們獲得的是一個物件的淺拷貝,如示例程式:
Cat
package com.github.hcsp;
public class Cat implements Cloneable {
public String name;
public Cat() {
}
public Cat(String name) {
this.name = name;
}
@Override
protected Object clone() {
Cat cat = null;
try {
cat = (Cat) super.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
return cat;
}
}
Main
package com.github.hcsp;
public class Main {
public static void main(String[] args) {
Cat cat = new Cat();
Cat newCat = (Cat) cat.clone(); // clone方法為淺拷貝
}
}
那麼如何實現深拷貝呢?
深拷貝示例程式:
Cat
package com.github.hcsp;
public class Cat {
public String name;
public Cat() {
}
public Cat(String name) {
this.name = name;
}
}
Home
package com.github.hcsp;
public class Home {
Cat cat;
}
DeepCopy
public class DeepCopy {
public static void main(String[] args) {
Home home = new Home();
Cat cat = new Cat();
cat.name = "mimi";
home.cat = cat;
Home newHome = deepCopy(home);
}
public static Home deepCopy(Home home) {
Home newHome = new Home();
Cat newCat = new Cat();
String newName = new String(home.cat.name);
newHome.cat = newCat;
newCat.name = newName;
return newHome;
}
}
這樣就可以實現一個深拷貝
九:方法的傳值 vs 傳引用
我們先來看兩個程式
程式一:
package com.github.hcsp;
public class Main {
public static void main(String[] args) {
int i = 0;
addOne(i);
System.out.println(i);
}
static void addOne(int i) {
i++;
}
}
該程式輸出的結果為:
0
因為 addOne 方法中傳遞的 i 只是 main 方法中的 i 的值的拷貝,所以不會對其產生任何影響。在執行完 addOne 方法以後,該方法空間會被銷燬。
程式二:
package com.github.hcsp;
public class Main {
public static void main(String[] args) {
Cat cat = new Cat();
cat.name = "haha";
renameCat(cat);
System.out.println(cat.name);
}
static void renameCat(Cat cat){
cat.name = "mimi";
}
}
該程式執行的結果為:
mimi
為什該程式就會改變 cat 的名字呢?因為方法中傳遞的是 Cat 變數引用(地址)的拷貝,所以,在 rename 方法中的 cat 指向的也是記憶體中同一只 “貓”。
什麼是值傳遞? 值傳遞(pass by value)是指在呼叫函式時將實際引數複製一份傳遞到函式中,這樣在函式中如果對引數進行修改,將不會影響到實際引數。
什麼是引用傳遞? 引用傳遞(pass by reference)是指在呼叫函式時將實際引數的地址直接傳遞到函式中,那麼在函式中對引數所進行的修改,將影響到實際引數。
Java 中有兩種資料型別:
原生資料型別
int
char
byte
boolean
float
double
short
long
引用資料型別
在 Java 中,對於方法的引數傳遞,無論是原生資料型別,還是引用資料型別,本質上是一樣的。
如果是傳值,那就將值複製一份,如果是傳引用(地址),就將引用(地址)複製一份。
所以,對於基本型別,Java 會將數值直接複製一份並傳遞到方法中,所以,方法裡面僅僅是對複製後的數值進行修改,並沒有影響到原數值;對於一個引用型別,Java 會將引用的地址複製一份,把它當作值傳遞到方法中,方法中傳遞的是指向堆記憶體中的那個地址,等同於對堆記憶體的同一物件進行操作,所以會改變物件的資訊。
本作品採用《CC 協議》,轉載必須註明作者和本文連結