Java SE 不得不注意的地方

0. 前言

       以下內容,是我認爲SE部分比較有意思的地方 ,故特此編寫。如果有其他重要的點被遺漏, 歡迎評論區補充,補充後期。

1. 面向過程和麪向對象

       面向對象和麪向過程是一種思想,其並不是由編程語言所實現的。比如我們通常說,C語言是面向過程的,Java是面向對象的。難道C語言就一定不能實現面向對象的思想嗎?答案是否定的。

       這裏以我個人的簡潔闡述一下面向過程和對象對象的區別。面向過程,通常是站在程序員即我們的角度去看待問題,去一步步想我們應該怎麼做才能解決問題。而面向對象的思想中,程序員通常是第三人稱,通過對需求進行抽象,抽象出一個個的對象,通過這些對象的協作來解決問題。但是每個對象去完成其工作依然屬於面向過程。可以這麼理解,面向對象在宏觀上是多個對象相互合作,微觀上依然是面向過程的。

2. JDK、JRE和JVM的區別

        JDKJava程序的開發包,而JRE只是Java程序的運行環境,JDK中包含了JRE。對於程序員來說,我們需要JDK開發相關的程序,對於程序的使用者來說,僅需要JRE即可。

       並且整個Java程序都是運行在JVM之上,我們知道Java語言是跨平臺的,其跨平臺的原因正是由於JVM的存在,我們的Java程序在Javac編譯後,會生成.class文件,該程序一次編譯,到處運行,指需要對應的計算機中按照有合適的JVM即可。

在這裏插入圖片描述

3. 基本數據類型和包裝類型

基本數據類型 包裝類型 字節
boolean Boolean -
byte Byte 1
short Short 2
char Character 2
int Integer 4
float Float 4
double Double 8
long Long 8

       一個boolean變量在編譯後以一個int代替,一個boolean數組編譯後,數組中每個成員是一個byte

       Java語言號稱一切皆對象,但是基本數據類型並不屬於對象,由此便產生了包裝類型。包裝類型的作用除了爲了表示對象,還可以實現與基本數據類型的自動轉換。

       既然包裝類型屬於對象,那麼其與基本數據類型也存在很大的區別:

  1. 初值不同,基本數據類型有其各自的數據類型,而包裝類型的初值爲null(這裏的初值是成員變量而不是方法中的變量,方法中的變量必須賦初值才能使用);
  2. 存儲位置不同,基本類型存儲位置在棧中,而包裝類型的存儲位置位於堆中(隨着棧逃逸技術的出現,對象不在侷限於出現在堆中);
  3. 泛型不能被指定爲基本數據類型,只能爲引用類型。

4. 自動拆裝箱

       自動裝箱指的是,基本類型可以自動轉換爲包裝類型,不需要通過new關鍵字。

       自動拆箱指的是,包裝類型可以自動轉換爲基本數據類型,參與算術運算。

4.1 實現原理

       自動裝箱的實現原理:

	public static void main(String[] args) {
        Integer a = 1;
    }

       上述代碼中,我們將一個基本數據類型直接賦值給了一個Integer的類型,如此便實現了自動裝箱。其編譯後的字節碼文件如下所示:

在這裏插入圖片描述

       可以看到,編譯器會自動調用Integer.valueOf()方法,那麼該方法的作用是什麼?

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

       在該方法中,實現了new的過程。

       自動拆箱的實現原理:

    public static void main(String[] args) {
        Integer a = 1;

        int b = a;
    }

       與自動拆箱一樣,編譯器同樣會會自動調用Integer.Intvalue()方法,實現包裝類型到基本類型的自動轉換。
在這裏插入圖片描述

	private final int value;

    public int intValue() {
        return value;
    }

4.2 緩存池

       我們知道對於基本類型==的作用是比較兩個變量的值,對於引用類型則是比較對象的地址,如果要比較對象的值則可以通過重寫equal方法。

    public static void main(String[] args) {
        Integer a = 1;
        Integer b = 1;
        Integer c = new Integer(1);

        System.out.println("a == b, " + (a == b ));
        System.out.println("a == c, " + (a == c ));
    }

       該程序的運行結果如下:

在這裏插入圖片描述

       首先可以明確的是,上述兩個==的作用比較的都是地址。但是爲什麼會出現不同的結果,答案正是由於緩存池的作用。

       在包裝類型中,緩存池的作用是在進行自動裝箱時,直接從緩存池中獲取對應的對象,否則則是在堆中創建對象。

       我們回到自動裝箱的代碼中,如下:

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

       首先判斷i的值是否在指定的緩存池的範圍內,如果在則返回緩存池中的引用,否則去創建對象。那麼緩存池是什麼?緩存池的大小是多大?

	static final int low = -128;
	static final int high;
	static final Integer cache[];
	static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }

       可以看到緩存池就是該包裝類型的一個數組,其可表示的數據範圍在-128,127之間。

       在介紹完緩存池後,那麼是否是所有的包裝類型都存在緩衝池,答案是否定的。

包裝類型 是否存在緩存池 範圍
Boolean 不存在,但是存在兩個常量
Byte 存在 -127 - 128
Short 存在 -127 - 128
Charcater 存在 0 - 127
Integer 存在 -127 - 128
Float 不存在
Double 不存在
Long 存在 -127 - 128

       不知道你有沒有寫過這樣的代碼?將一個int直接自動裝箱爲Double。如果沒有的話,可以去嘗試一下,結果可能會出乎你的意料。

    public static void main(String[] args) {

        Double a = 1;

        Double b = new Double(1);

    }

       如上所示,你可能會認爲Double自動裝箱的話調vlaueOf(double d)那麼的話,我的1可以自動類型轉換爲1.0,那我當然可以實現自動裝箱了,然而編譯結果如下:
在這裏插入圖片描述
            可以這樣理解,自動裝箱的過程中關閉了自動類型轉換。

5. Object

       面向對象的三大特徵是:封裝、繼承、多態。

       在Java中,所有的類都是繼承自Object類,同時該類的方法在併發中也被經常使用到。

權限 方法名 作用
public getClass() 獲得Class對象
public hashCode() 獲得哈希值
public equals(Object obj) 比較兩個對象是否相等
public toString() 打印該對象的信息
public notify() 喚醒一個等待中的線程
public notifyAll() 喚醒多個等待中的線程
public wait() 讓當前線程等待,並釋放鎖
public wait(long timeout) 讓當前線程等待,直到指定時間或被喚醒
public wait(long timeout, int nanos) 同wait(long timeout),time = 1000000*timeout+nanos
protected clone() 克隆對象
protected finalize() 被垃圾回收器清除之前執行
private registerNatives() 註冊本地方法

       通常,我們比較兩個對象是否相等時,不僅會重寫equal方法,也會重寫hashCode()方法。這樣做的意義是,hashCode相同,兩個對象的值不一定完全相同。但是hashCode不同,兩個對象的值一定不相同。通過hashCode的值,可以減少對對象成員變量的比較。

6. String

       String是經常被使用到的一個數據類型,需要注意的是,其是引用類型,並不是基本類型。

       我們都知道String是一個不可變字符串,那麼爲什麼是不可變字符串?是因爲String數組內部封裝了一個final數組。

	private final char value[];

       我們知道對於被final修飾的引用類型,只是其地址值不可以改變,那麼其數組下標中的元素可以改變嗎?答案是可以的,如下所示:

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        String s = "abcd";
        //獲得Class對象
        Class c = s.getClass();
        //讓s1和s的地址相同,判斷反射改變String的值後地址有沒有發生變化
        String s1 = s;
        //輸出true
        System.out.println(s1 == s);
        //獲得String的成員變量,即final數組
        Field value = c.getDeclaredField("value");
        //因爲數組爲private,所以需要取消語法檢查
        value.setAccessible(true);
        char[] array = (char[])value.get(s);
        //輸出[a, b, c, d]
        System.out.println(Arrays.toString(array));
        //輸出abcd
        System.out.println(s);
        array[3] = 'a';
        //[a, b, c, a]
        System.out.println(Arrays.toString(array));
        //輸出abca
        System.out.println(s);
        //輸出true
        System.out.println(s1 == s);
    }

       那麼事情這麼簡單嗎?如果我們打印hashCode會有變化嗎?運行後發現hashCode的值並沒有發生變化,這是爲什麼呢?

       要知道"abcd"abca這兩個字符串的hashCode並不相同。這是因爲在我們創建String對象的時候,便已經計算了hashCode的值。如下:

    public String(String original) {
        this.value = original.value;
        this.hash = original.hash;
    }

       而我們在反射的過程中,並沒有進行hashCode值的更新。所以反射對字符串中的數組元素修改後,還需要重新計算hashCode

       存在如下一段代碼:

	String s1 = "abc";
    String s2 = "abc";
    String s3 = new String("abc");
    System.out.println(s1 == s2);
    System.out.println(s1 == s3);

       輸出如下:

	true
	false

       這時因爲每次new都是在堆中分配一塊內存,然後將該內存的引用返回。

       如果字符串的常量池中已經存在對應的常量,直接返回對應的地址即可。

       String s3 = new String(“abc”),因爲字符串常量池已經存在對用的字符串常量,所以內存圖如下:

在這裏插入圖片描述

       在String中,有一個方法即爲有趣,即intern(),該方法的作用是如果常量池中存在和目標串相同的值,則返回常量池中的地址。否則,直接返回引用,並且將目標串加入到字符串常量池中

       示例如下:

	String s1 = new String("abc");
    String s2 = s1.intern();
    System.out.println(s1 == s2);
    // 輸出:false

在這裏插入圖片描述

	String s1 = "abc";
    String s2 = s1.intern();
    System.out.println(s1 == s2);
    // 輸出:true

6.1 StringBuilder

       在說到String這個不可變字符串時,不得不說到可變字符串StringBuildStringBuffer。因爲我們有的時候並不想爲了修改字符串而頻繁的去創建String對象,由此便有了可變字符串。

       StringBuild的源碼如下所示:

abstract class AbstractStringBuilder implements Appendable, CharSequence {
    /**
     * The value is used for character storage.
     */
    char[] value;

    /**
     * The count is the number of characters used.
     */
    int count;

       可以看到其value屬性並不是由final所修飾。

       在我們創建StringBuild對象時,默認創建一個長度爲16的字符數組。

 	public StringBuilder() {
        super(16);
    }

6.2 StringBuffer

       StringBufferStringBuilder的區別是StringBuffer是線程安全的,其方法基本都是由synchronized所修飾。

在這裏插入圖片描述

7. 抽象類與接口

       抽象類的作用往往是定義一個模板,然後其子類照着這個模板去實現相關模塊,如前面說到的StringBuilder,其便是繼承自一個AbstractStringBuilder。接口的作用往往是定義有哪些行爲。

       接口更多的是在系統架構設計方法發揮作用,主要用於定義模塊之間的通信契約。而抽象類在代碼實現方面發揮作用,可以實現代碼的重用。

       抽象類與普通類的區別:

  1. 類名由abstract所修飾
public abstract class Person {
}
  1. 類名不能被final修飾

  2. 可以不包含方法的實現

public abstract class Person {

	//如果方法被abstract修飾,說明這是一個抽象方法
    public abstract void sayHello();
}
  1. 不能實例化

       這裏有一點值得注意,便是抽象類不能被實例化,那麼其構造方法存在的意義是什麼?

public abstract class Person {
    public abstract void sayHello();

    public Person(){
        System.out.println("執行抽象類的構造方法");
    }
}

class Student extends Person{

    @Override
    public void sayHello() {
        System.out.println(getClass().getName()+":sayHello");
    }

    public Student(){
        System.out.println("執行非抽象類的構造方法");
    }

    public static void main(String[] args) {
        Student student = new Student();
        student.sayHello();
    }
}

       上述代碼的輸入結果如下:

	執行抽象類的構造方法
	執行非抽象類的構造方法
	Student:sayHello

       可以看出,在執行子類的構造器時,會執行抽象類的構造方法,所以抽象類的構造方法可以完成一些初始化操作。

       接口和抽象類的區別:

  1. 接口可視作比抽象類更加抽象的類。
  2. 接口中的方法只能爲public,對於抽象方法而言,其只是不能被private修飾。
  3. 接口沒有構造方法。
  4. 接口中的變量默認被public static final所修飾。
  5. JDK1.8及以後,接口允許有方法體,因爲當我們給接口拓展方法時,其每一個實現類都要重寫該方法,這樣會很不便。
public interface MyInterface {

    void a();

	//如果方法具有方法體,且非靜態方法,必須被default修飾
    default void b(){
        System.out.println("hello interface");
    }

}

8. 重載和重寫

       重載發生在同一個類中,表示方法名相同的情況下,允許參數列表的類型不同。

interface Language{

}

class Java implements Language {

}

class Python implements Language {

}

class Go implements Language {

}

class Stu{

    public void Study(Java java){
        System.out.println("Study Java");
    }

    public void Study(Python python){
        System.out.println("Study Python");
    }
    
    private void Study(Go go){
        System.out.println("Study Go");
    }
}

       重寫則是發生在父子類之間,子類可以重寫父類的方法。

class Father{
    public void print(){
        System.out.println("Father");
    }
}

class Son extends Father{
    public void print(){
        System.out.println("Son");
    }
}

9. 多態

       多態指的是一個對象可以有多種類型,分爲編譯時類型和運行時類型。編譯時類型是在編譯階段就可以確定的,運行時則只能在程序運行的過程中確定。

public class Test {
    public static void main(String[] args) {
        Language language = null;
        language = new Java();
        language = new Python();
        language = new Go();
    }
}

interface Language{

}

class Java implements Language {

}

class Python implements Language {

}

class Go implements Language {

}

       上述代碼中,language編譯時類型爲Language,運行時類型則爲JavaPython以及Go

       多態下,成員訪問的規則如下:

       成員變量:編譯看左邊,運行看左邊
       成員方法:編譯看左邊,運行看右邊

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