[Java 基礎]Java 程式的基本結構

jrh_2333發表於2021-06-18

歡迎閱讀原文: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 的位元組碼。如果想要執行位元組碼檔案,平臺上面必須安裝 JVMJVM 直譯器會將位元組碼解釋成依賴於平臺的機器碼。

從上圖也可以看出,不同作業系統需要安裝基於該作業系統的 JVMJVM 遮蔽了作業系統之間的差異,實現了 Java 語言的跨平臺性。

二:Java語言的基本單元——類與包

類(class)

Java 語言中,類是最小的基本單元

一個最簡單的類

public class Cat {
}

包(package)

為了更好地組織類,Java 語言提供了包的機制,用於區別類名的名稱空間。

示例:

一個屬於my.cute 包下的 Cat

package my.cute;public class Cat {
}

Java 語言中,包一般會用域名的反序來命名。

例如:

package com.alibaba.fastjson;

這樣可以避免類名衝突。

三:Java語言的基本結構——包的意義

包的意義與作用:

  1. 把功能相似或相關的類組織在同一個包中,方便查詢與管理

  2. 同一個包中類名要求不能相同,但是不同包中的類名可以相同;當同時呼叫兩個不同包中相同類名的類時,應該加上包名加以區別。因此,包也可以避免類名衝突。 示例: 在不同包下有著相同類名的類,我們可以使用全限定類名(Full Qualified Name)加以區分。

    package com.github.hcsp;
    
    public class Home {
      com.github.hcsp.pet1.Cat cat1;
      com.github.hcsp.pet2.Cat cat2;
    }
  3. 包限定了訪問許可權

四:在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 和書寫全限定類名,而是直接使用。

那是因為 StringSystem 類放在 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 關鍵字

在本示例中,我們建立了一個名字叫 TomCat 物件,呼叫了有參的構造器

如果我們不在 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 即:空指標異常

原因在於 cat1namenull,對於一個空的物件,我們呼叫這個物件的方法時,就會產生空指標異常。

規避空指標的方法很簡單,我們在可能會產生空指標的地方加入判空的邏輯處理即可:

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 指向的也是記憶體中同一只 “貓”。

  1. 什麼是值傳遞? 值傳遞(pass by value)是指在呼叫函式時將實際引數複製一份傳遞到函式中,這樣在函式中如果對引數進行修改,將不會影響到實際引數。

  2. 什麼是引用傳遞? 引用傳遞(pass by reference)是指在呼叫函式時將實際引數的地址直接傳遞到函式中,那麼在函式中對引數所進行的修改,將影響到實際引數。

Java 中有兩種資料型別:

  • 原生資料型別

    • int

    • char

    • byte

    • boolean

    • float

    • double

    • short

    • long

  • 引用資料型別

Java 中,對於方法的引數傳遞,無論是原生資料型別,還是引用資料型別,本質上是一樣的。

如果是傳值,那就將值複製一份,如果是傳引用(地址),就將引用(地址)複製一份。

所以,對於基本型別,Java 會將數值直接複製一份並傳遞到方法中,所以,方法裡面僅僅是對複製後的數值進行修改,並沒有影響到原數值;對於一個引用型別,Java 會將引用的地址複製一份,把它當作值傳遞到方法中,方法中傳遞的是指向堆記憶體中的那個地址,等同於對堆記憶體的同一物件進行操作,所以會改變物件的資訊。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章