Java 核心知識摘錄

Basic Knowledge

JVM中類加載的過程

加載過程圖解

 

 

1. 加載

指Java虛擬機查找字節流(查找.class文件),並且根據字節流創建java.lang.Class對象的過程。

過程:將類的.class文件中的二進制數據讀入內存,放在運行時區域的方法區內。然後,在堆中創建java.lang.Class對象,用來封裝類在方法區的數據結構。

2. 鏈接

鏈接包括驗證、準備以及解析三個階段。

(1)驗證階段。主要的目的是確保被加載的類(.class文件的字節流)滿足Java虛擬機規範,不會造成安全錯誤。

(2)準備階段。負責爲類的靜態成員分配內存,並設置默認初始值。

(3)解析階段。將類的二進制數據中的符號引用替換爲直接引用。

說明:

符號引用。即一個字符串,但是這個字符串給出了一些能夠唯一性識別一個方法,一個變量,一個類的相關信息。直接引用。可以理解爲一個內存地址,或者一個偏移量。比如類方法,類變量的直接引用是指向方法區的指針;而實例方法,實例變量的直接引用則是從實例的頭指針開始算起到這個實例變量位置的偏移量。

舉個例子來說,現在調用方法hello(),這個方法的地址是0xaabbccdd,那麼hello就是符號引用,0xaabbccdd就是直接引用。在解析階段,虛擬機會把所有的類名,方法名,字段名這些符號引用替換爲具體的內存地址或偏移量,也就是直接引用。

3. 初始化

初始化,則是爲標記爲常量值的字段賦值的過程。換句話說,只對static修飾的變量或語句塊進行初始化。如果初始化一個類的時候,其父類尚未初始化,則優先初始化其父類。

如果同時包含多個靜態變量和靜態代碼塊,則按照自上而下的順序依次執行。

類加載過程只是一個類生命週期的一部分,在其前,有編譯的過程,只有對源代碼編譯之後,才能獲得能夠被虛擬機加載的字節碼文件;在其後還有具體的類使用過程,當使用完成之後,還會在方法區垃圾回收的過程中進行卸載(垃圾回收)。

Reflect 反射

原理

在運行狀態中,對於任意一個類,它的所有屬性和方法都能夠被知道;對於任意一個對象,它的任意一個方法和屬性都能夠被調用。這種動態獲取的信息以及動態調用對象的方法的功能稱爲java語言的反射機制。

作用

能把Java類中的各種成分映射成Java對象。

解讀

對類進行操作,前提是需要首先獲得該類的字節碼對象Class。然後通過Class提供的方法對類進行相關操作。相關的操作屬性,As follow:Class, Constructor, Method, Field, Instance, Invoke。

僞代碼Demo:

Student.java

@Setter
@Getter
public class Student implements Serializable {
    private static final long serialVersionUID = 3323956009409102413L;
    private String name;
    private Integer age;
    private Student(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
    }

    public Student() {}
    public static Builder builder() {
        return new Builder();
    }

    static class Builder {
        private String name = null;
        private Integer age = null;

        public Builder() {}
        public Builder name(String name) {
            this.name = name;
            return this;
        }
        public Builder age(Integer age) {
            this.age = age;
            return this;
        }
        public Student build() {
            return new Student(this);
        }

    }
}
ReflectTest.java
@Slf4j
public class ReflectTest {
    public static void main(String[] args) {
        Student student = Student.builder()
                .name("zs")
                .age(20)
                .build();

        Class clazz = student.getClass();
        Field[] fieldArr = clazz.getDeclaredFields();
        List<Field> fieldList = Arrays.stream(fieldArr).filter( field -> !"serialVersionUID".equals(field.getName()))
                .collect(Collectors.toList());
        fieldList.forEach(field -> {
            String key = field.getName();

            PropertyDescriptor descriptor;
            Object value = null;
            try {
                descriptor = new PropertyDescriptor(key, clazz);
                Method method = descriptor.getReadMethod();
                value = method.invoke(student);
            } catch (Exception e) {
                log.error(e.getMessage());
            }

            System.out.println(key + ":" + value);
        });

    }
}

獲取類的三種方法

僞代碼Demo:

        //Method 1:getClass(),When you don't know the name of this class
        Student student = new Student();
        Class clazz = student.getClass();
        System.out.println(clazz);

        //Method 2: class name.class
        Class clazz2 = Student.class;
        System.out.println(clazz2);

        //Method 3:forName()
        Class clazz3 = null;
        try {
            clazz3 = Class.forName("com.xxx.xxx.basic.reflect.Student");
            System.out.println(clazz3);
        } catch (ClassNotFoundException e) {
            log.error(e.getMessage());
        }

        System.out.println(clazz == clazz2 && clazz == clazz3); //true

Dynamic Proxy 動態代理

僞代碼Demo:

public interface StudentService {
    void study();
    void eat();
}



public class StudentServiceImpl implements StudentService {
    @Override
    public void study() {
        System.out.println("Student need to eat food");
    }

    @Override
    public void eat() {
        System.out.println("Student must be study");
    }
}



public class DynamicProxyHandler implements InvocationHandler {
    private Object proxyed;

    public DynamicProxyHandler(Object proxyed) {
        this.proxyed = proxyed;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("The proxy is working");
        return method.invoke(proxyed, args);
    }
}


public class Main {
    public static void main(String[] args) {
        StudentService studentService = new StudentServiceImpl();

        StudentService proxy = (StudentService) Proxy.newProxyInstance(StudentService.class.getClassLoader(),
                new Class[]{StudentService.class}, new DynamicProxyHandler(studentService));
        proxy.eat();
        proxy.study();
    }
}

 

三目運算符

三目運算符是我們經常在代碼中使用的,a= (b==null?0:1);這樣一行代碼可以代替一個if-else,可以使代碼變得清爽易讀。

但是,三目運算符也是有一定的語言規範的。在運用不恰當的時候會導致意想不到的問題。本文就介紹一個我自己曾經踩過的坑。

一、三目運算符

對於條件表達式b?x:y,先計算條件b,然後進行判斷。如果b的值爲true,計算x的值,運算結果爲x的值;否則,計算y的值,運算結果爲y的值。一個條件表達式從不會既計算x,又計算y。條件運算符是右結合的,也就是說,從右向左分組計算。例如,a?b:c?d:e將按a?b:(c?d:e)執行。

 

二、自動裝箱與自動拆箱

基本數據類型的自動裝箱(autoboxing)、拆箱(unboxing)是自J2SE 5.0開始提供的功能。

 一般我們要創建一個類的對象實例的時候,我們會這樣: Class a = new Class(parameters); 當我們創建一個Integer對象時,卻可以這樣: Integer i = 100;(注意:和 int i = 100;是有區別的 ) 

 

實際上,執行上面那句代碼的時候,系統爲我們執行了: Integer i = Integer.valueOf(100); 這裏暫且不討論這個原理是怎麼實現的(何時拆箱、何時裝箱),也略過普通數據類型和對象類型的區別。

我們可以理解爲,當我們自己寫的代碼符合裝(拆)箱規範的時候,編譯器就會自動幫我們拆(裝)箱。那麼,這種不被程序員控制的自動拆(裝)箱會不會存在什麼問題呢?

 

三、問題回顧

首先,通過你已有的經驗看一下下面這段代碼。如果你得到的結果和後文分析的結果一致(並且你知道原理),那麼請忽略本文。如果不一致,請跟我探索下去。

public static void main(String[] args) {
    Map<String, Boolean> map = new HashMap<>();
    Boolean b = map != null ? map.get("test") : false;
    System.out.println(b);
}

以上這段代碼,是我們在不注意的情況下有可能經常會寫的一類代碼(在很多時候我們都愛使用三目運算符)。

一般情況下,我們會認爲以上代碼Boolean b的最終得到的值應該是null。因爲map.get("test")的值是null,而b又是一個對象,所以得到結果會是null。

但是,以上代碼會拋出NPE:

Exception in thread "main" java.lang.NullPointerException

首先可以明確的是,既然報了空指針,那麼一定是有些地方調用了一個null的對象的某些方法。在這短短的兩行代碼中,看上去只有一處方法調用map.get("test"),但是我們也都是知道,map已經事先初始化過了,不會是Null,那麼到底是哪裏有空指針呢。

我們接下來反編譯一下該代碼。看看我們寫的代碼在經過編譯器處理之後變成了什麼樣。反編譯後代碼如下:

public static void main(String args[]){
   Map map = new HashMap();
   Boolean b = Boolean.valueOf(map == null ? false : ((Boolean)map.get("test")).booleanValue());
   System.out.println(b);
}

 

看完這段反編譯之後的代碼之後,經過分析我們大概可以知道問題出在哪裏。((Boolean)hashmap.get("test")).booleanValue() 的執行過程及結果如下:

hashmap.get("test")->null;

(Boolean)null->null;

null.booleanValue()->報錯

好,問題終於定位到了。很明顯,上面源代碼中的map.get("test")在被編譯成了

(Boolean)map.get("test").booleanValue(),這是一種自動拆箱的操作。

 

那麼,爲什麼這裏會發生自動拆箱呢?這個問題又如何解決呢?

 

四、原理分析

通過查看反編譯之後的代碼,我們準確的定位到了問題,分析之後我們可以得出這樣的結論:NPE的原因應該是三目運算符和自動拆箱導致了空指針異常。

那麼,這段代碼爲什麼會自動拆箱呢?這其實是三目運算符的語法規範。參見jls-15.25,摘要如下:

If the second and third operands have the same type (which may be the null type), then that is the type of the conditional expression.


If one of the second and third operands is of primitive type T, and the type of the other is the result of applying boxing conversion (§5.1.7) to T, then the type of the conditional expression is T.

If one of the second and third operands is of the null type and the type of the other is a reference type, then the type of the conditional expression is that reference type.

簡單的來說就是:當第二,第三位操作數分別爲基本類型和對象時,其中的對象就會拆箱爲基本類型進行操作。

所以,結果就是:由於使用了三目運算符,並且第二、第三位操作數分別是基本類型和對象。所以對對象進行拆箱操作,由於該對象爲null,所以在拆箱過程中調用null.booleanValue()的時候就報了NPE。

 

五、問題解決

如果代碼這麼寫,就不會報錯:

Map<String,Boolean> map =  new HashMap<String, Boolean>();
Boolean b = (map!=null ? map.get("test") : Boolean.FALSE);

就是保證了三目運算符的第二第三位操作數都爲對象類型。這樣就不會發生自動拆箱操作,以上代碼得到的b的結果爲null。

PS:本文中的示例,只是爲了更加方便讀者理解三目運算符會導致自動拆箱現象,可能在代碼中並不會直接這樣使用。但是,我自己的代碼確實發生過類似問題。這裏簡化一下,爲了講清楚原理。



泛型(Genericity)

 


序列化(Serialization)

1.序列化和反序列化
序列化(Serialization)是將對象的狀態信息轉化爲可以存儲或者傳輸的形式的過程,一般將一個對象存儲到一個儲存媒介,例如檔案或記憶體緩衝等,在網絡傳輸過程中,可以是字節或者XML等格式;而字節或者XML格式的可以還原成完全相等的對象,這個相反的過程又稱爲反序列化;

2.Java對象的序列化和反序列化
在Java中,我們可以通過多種方式來創建對象,並且只要對象沒有被回收我們都可以複用此對象。但是,我們創建出來的這些對象都存在於JVM中的堆(heap)內存中,只有JVM處於運行狀態的時候,這些對象纔可能存在。一旦JVM停止,這些對象也就隨之消失;

但是在真實的應用場景中,我們需要將這些對象持久化下來,並且在需要的時候將對象重新讀取出來,Java的序列化可以幫助我們實現該功能。

對象序列化機制(object serialization)是java語言內建的一種對象持久化方式,通過對象序列化,可以將對象的狀態信息保存未字節數組,並且可以在有需要的時候將這個字節數組通過反序列化的方式轉換成對象,對象的序列化可以很容易的在JVM中的活動對象和字節數組(流)之間進行轉換。

在JAVA中,對象的序列化和反序列化被廣泛的應用到RMI(遠程方法調用)及網絡傳輸中

 


  • Java的JDBC連接數據庫用的模式:橋接模式
  • (不定項選擇)下面有關java threadlocal說法正確的有?(ABCD)
    1. ​​​​​​​A. ThreadLocal存放的值是線程封閉,線程間互斥的,主要用於線程內共享一些數據,避免通過參數來傳
    2. B. 線程的角度看,每個線程都保持一個對其線程局部變量副本的隱式引用,只要線程是活動的並且 
      ThreadLocal 實例是可訪問的;在線程消失之後,其線程局部實例的所有副本都會被垃圾回收 
    3. 在Thread類中有一個Map,用於存儲每一個線程的變量的副本
    4. 對於多線程資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間
      換時間”的方式

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

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