15個易遺忘的java知識點

想要了解更專業的互聯網知識,歡迎加入官方技術交流Q羣:547147889

1、java中的基本數據類型以及所佔內存大小

(1)整形
byte 1字節
short 2字節
int 4字節
long 8字節
(2)浮點型
float 4字節
double 8字節
(3)字符類型
char 2字節(Unicode-16)
(4)布爾類型

布爾類型boolean比較特殊,儘管Java虛擬機定義了boolean類型,但虛擬機對boolean類型的支持是有限的,沒有爲boolean值單獨設計JVM指令。操作布爾值的表達式在編譯之後,它使用的是JVM的int數據類型,也就是佔用4個字節。
JVM也不會直接支持boolean數組,boolean數組在編譯之後,它的元素採用byte數據類型,用0表示false,1表示true,也就是boolean數組的元素只佔用一個字節。

2、UTF-8與Unicode的關係

Unicode是一個統一的編碼標準,將現有的所有字符進行唯一編碼。在第一個Unicode版本中,使用兩個字節(16位)來表示一個字符,注意這裏的字節並非指的是計算機內存中的存儲單元,而是一個數學長度單位而已。然而,一個Unicode字符在內存中存儲所佔用的長度,就需要一個具體的編碼規則來實現,比如UTF-8。因此,Unicode只是一個編碼標準,而UTF-8是對這個標準的一個實現,UTF-8規定了一個Unicode字符在內存中佔用的空間(英文和中文所佔空間是不同的,有興趣的讀者可以查閱相關資料)。

代碼點指的是可以用於對字符集進行編碼的那些數字,比如在16位的Unicode編碼字符集中,字符“A”的編碼是U+0041,那麼0041就是一個代碼點。

代碼單元指的是字符所佔空間的單元。例如在UTF-32中,一個代碼單元爲32位,一個字符佔用32位,恰好使用一個代碼單元,這種方式會耗費大量內存。在UTF-16中,一個代碼單元爲16位,值 U+0000 至 U+FFFF 編碼對應一個字符,每個字符佔用一個代碼單元,但是,對於超過這個範圍的那些增補字符的編碼,需要兩個這樣的單元(即32位)。而在UTF-8中,一個代碼單元爲8位,UTF-8 使用一至四個字節的序列對編碼 Unicode 代碼點進行編碼,原理同UTF-32和UTF-16。

3、String字符串常量

在Java語言中,一個String字符串常量對應着一個String對象,並且是不可更改和繼承的(因爲String類被final關鍵字修飾)。Java語言這樣設計,主要是爲了使得字符串常量(注意是字符串常量,字符串變量不符合這裏所講的規則)可以共享,因爲JVM將字符串常量放入公共的存儲池中,不同的變量可以引用相同的字符串常量。

public static void main(String[] args) {
String a = “hello”;
String b = “hello”;
System.out.println(a == b);
}

以上代碼運行結果爲:true。這就說明a和b引用的是同一String對象。

4、基本數據類型轉換規則

在雙操作數運算中,會根據操作數類型將低級的一個類型轉換爲高級的一個類型。

1)只要兩個操作數中有一個是double類型的,另一個將會被轉換成double類型,並且結果也是double類型;

2)只要兩個操作數中有一個是float類型的,另一個將會被轉換成float類型,並且結果也是float類型;

3)只要兩個操作數中有一個是long類型的,另一個將會被轉換成long類型,並且結果也是long類型;

4)兩個操作數(包括byte、short、int、char)都將會被轉換成int類型,並且結果也是int類型。

5.按值調用與按引用調用

按值調用表示方法接收的是調用者提供的參數值。按引用調用表示方法接收的是調用者提供的是調用者提供的參數地址。Java程序設計語言總是按值調用的。下面是反例代碼:

public void swap(Person a, Person b) {
Person temp = a;
a = b;
b = tem;
}

以上代碼啓動交換a和b所引用的對象,但實際編譯執行會發現沒有成功交換。這也就證明Java不是按引用調用的,a和b僅代表兩個Person對象的值,而不是代表兩個對象的引用,在參數傳遞上與int等基本類型的值沒有區別。

6、對象初始化

在使用構造器初始化對象時,首先運行初始化塊,然後運行構造器的主題部分。調用構造器的具體初始化步驟如下:

1)類的所有數據域被初始化爲默認值(0、false或null)。
2)按照在類中聲明的次序,依次執行所有初始化語句和初始化塊。
3)如果構造器第一行調用了第二個構造器,則執行第二個構造器。
4)執行構造器的主體。

類第一次加載的時候,將會進行靜態域的初始化。所有的靜態初始化語句以及靜態初始化塊都將依照定義的順序進行。

使用super調用構造器的語句必須是子類構造器的第一條語句。

7、數組

在Java中,子類數組的引用可以轉換成父類數組的引用,而不需要採用強制類型轉換。

8、繼承

在覆蓋一個方法的時候,子類方法不能低於父類方法的可見性。即父類方法是protected的,子類覆蓋的方法只能是protected或public的。

9、final修飾的類

如果將一個類聲明爲final的,只有其中的方法自動成爲final,而不包括域。

10、equals方法

Object類的equals方法用於檢測一個對象與另一個對象是否相等,即判斷兩個變量的引用是否相同。如果重新定義了equals方法,就必須重新定義hashCode方法,因爲向散列表中添加數據時會根據hashCode和equals方法來確定插入位置。如果x.equals(y)返回true,那麼x.hashCode()與y.hashCode()的返回值必須相同。

由於枚舉值具有固定的實例,因此直接使用“==”就可以判定兩個枚舉值是否相同,而不需使用equals方法。

11、Class類

JVM會爲每個加載的類生成一個Class類型的實例,用於跟蹤對象所屬的類,獲取Class類型實例的方法如下:

1)Object類中的getClass()方法將會返回一個Class類型的實例。
2)Class.forName(className)可以返回className指定類的Class實例。
3)MyClass.class可以返回MyClass類的Class實例。

12、局部類

在方法中聲明的類稱爲局部類(也屬於內部類),局部類不僅能夠訪問包含它們的外部類,還可以訪問局部變量。不過,可以被訪問的局部變量必須被聲明爲final。

13、代理

利用代理可以在運行時創建一個實現了一組給定接口的新類,這種功能只有在編譯時無法確定需要實現哪個接口時纔有必要使用。我們需要提供一個實現InvocationHandler接口的類來處理調用過程。

public class SaneseeProxyDemo{
    public static void main(String[] args) {
            //配置第一個代理aProxyInstance,用於代理Integer類型的a。
            Integer a = 1;
            InvocationHandler aHandler = new SaneseeHandler (a);
            Object aProxyInstance = Proxy.newProxyInstance(null, new Class[]{Comparable.class}, aHandler); //其中第一個參數null表示使用默認的類加載器,             //第二個參數表明需要代理類實現的接口,第三個參數爲調用處理器類
            //配置第一個代理bProxyInstance,用於代理Integer類型的b。
            Integer b = 2;
            InvocationHandler bHandler = new SaneseeHandler (b);
            Object bProxyInstance = Proxy.newProxyInstance(null, new Class[]{Comparable.class}, bHandler);
            //輸出兩個代理類的比較結果
            Comparable aComparable = (Comparable)aProxyInstance;
            Comparable bComparable = (Comparable)bProxyInstance;
            System.out.println(aComparable.compareTo(b));
        }
    }
    class SaneseeHandler implements InvocationHandler
    {
        //需要代理的對象
        private Object target; 
        public SaneseeHandler (Object t)
        {
            target = t;
        }
        public Object invoke(Object proxy, Method m, Object[] args) throws Throwable
        {
            //打印被代理的對象
            System.out.print(target);
            // 打印方法名
            System.out.print("." + m.getName() + "(");
            // 打印參數
            if (args != null)
            {
                for (int i = 0; i < args.length; i++)
                {
                    System.out.print(args[i]);
                    if (i < args.length - 1) System.out.print(", ");
                }
            }
            System.out.println(")");
            // 調用方法
            return m.invoke(target, args);
        }
}

14、限定泛型變量

關於類型變量限定的問題,一直以來是不少程序員感覺困惑的地方,本條就這個問題簡要討論一下。Java泛型一面有以下兩種類型限定的泛型:


<T extends Comparable>
<T super Comparable>

限定類型可以有多個,使用“&”分隔。

無論何時定義一個泛型類型,都自動提供了一個相應的原始類型。程序運行時,擦除類型變量,並替換爲第一個限定類型(比如<T extends Comparable>被替換爲Comparable),無限定的變量使用Object。

SaneseeDemo <T>

不能用類型參數代替基本類型。因此沒有SaneseeDemo<int>,只有SaneseeDemo<Integer>。

不能實例化參數化類型的數組,例如:

SaneseeDemo<Integer>[] array = new SaneseeDemo<Integer>[10]; //這種寫法是錯誤的,不能通過編譯。

這是因爲array被擦除類型之後,它的類型爲SaneseeDemo [],元素類型爲SaneseeDemo,則相應的原始類型爲Object[],那麼就可以往裏面添加任何類型的元素,也就會出現類型錯誤。

如果需要使用參數化類型對象,只有一種安全而有效的方法:使用ArrayList。

同時,也不能在靜態域或方法中引用類型變量,無法通過編譯,因爲類型擦除之後,就變爲原始類型Object了。如果它可以正常執行,那麼任何類型的T最終都會變爲Object,那麼不管傳入什麼類型的T,最終只能獲取相同的單例,這與我們想要的功能是不一致的。

不能拋出或捕獲泛型類型的異常。

Sanesee<String>不是Sanesee<Object>的子類,因此不能將Sanesee<String>類型的值賦給Sanesee<Object>類型的值。

不能向extends Employee類型的變量調用set方法的。假設它可以執行,因爲程序無法知道這個變量的具體類型,它的類型可能是Manager,也可能是Executive,那麼就會出現類型轉換錯誤。只能向這個變量調用get方法,因爲程序把獲得的值自動轉換爲Employee類型,子類型可以自動轉換爲父類型。

不能向super Employee類型的變量調用get方法,因爲程序無法知道返回的具體類型,它的類型可能是Person,也有可能是Object。只能調用set方法,因爲不管是傳入Employee還是其子類,都可以成功執行。

總之,帶有super限定的通配符可以向泛型對象寫入,帶有extends限定的通配符可以從泛型對象讀取。而Sanesee<?>和Sanesee類型的不同點在於,Sanesee<?>類型擦除以後就是Object了,所以根本無法使用Object類型的對象去調用它,只有將它放在靜態方法中執行一些簡單的操作。

15、多線程

當對一個線程調用interupt方法時,線程的中斷狀態將被置位。每個線程都具有一個boolean類型的中斷標誌,通過它來判斷線程是否被中斷:

Thread.currentThread().isInterupted()

線程的run方法不能拋出任何受檢異常,而受檢異常會導致程序終止。然而,不需要使用catch語句來處理可以被傳播的異常。相反,就在線程死亡之前,異常被傳遞到一個用於未捕獲異常的處理器。可以使用setUncaughtExceptionHandler爲任何線程安裝一個處理器。

可以調用ReentrantLock的lock和unlock方法獲取鎖或解除鎖,也可以調用條件對象Condition(由ReentrantLock的newCondition方法獲取)的await和singal實現等待或通知功能。兩種方式的不同點在於,lock獲取到的鎖,會等到程序執行結束調用unlock時纔會釋放,哪怕正在執行耗時比較長的任務,或者處於等待狀態。而Condition在需要等待的地方調用await方法,進入等待集,當鎖可用時,該線程繼續保持阻塞狀態,直到另一個線程調用同一條件上的singalAll方法爲止(singal方法可以隨機解除一個線程的阻塞狀態)。然而,最好不使用Lock/Condition或synchronized關鍵字,可以使用java.util.concurrent包中的任何一種機制,它會爲你處理所有的加鎖。如果一定得使用關鍵字,優先使用synchronized,這樣可以減少代碼的數量,其次纔是Lock/Condition。

ThreadLocal類型可以使得每個線程擁有自己的獨立的變量。volatile類型爲實例域的訪問提供了一種免鎖機制,可以使得線程每次都可以訪問到最新的值(volatile變量不能保證原子性)。

ReentrantReadWriteLock類適合讀數據多而寫數據少的情形:

ReentrantReadWriteLock.readLock();//多個線程共享,排斥寫
ReentrantReadWriteLock.writeLock();//單個線程使用,排斥讀寫

官方建議放棄使用stop和suspend方法,是因爲stop強制終止一個線程是極不安全的操作,而suspend本身容易導致死鎖。

在阻塞隊列中,生產者向隊列中插入元素,消費者向隊列獲取元素。當隊列爲空時,消費者線程會被阻塞;當隊列慢時,生產者線程將被阻塞。LinkedBlockingQueue可以不限容量,而ArrayBlockingQueue需要指定容量。PriorityBlockingQueue是一個帶有優先級的隊列,DelayQueue需要在指定延遲慢之後才能移除元素。LinkedTransferQueue的transfer(item)方法允許生產者線程等待,直到消費者準備就緒並移除這個item元素。

ava.util.concurrent包提供了Map、集合等的併發實現,ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet和ConcurrentLinkedQueue,允許併發訪問數據結構。

任何集合類型可以通過使用同步包裝器變成線程安全的(當然最好使用java.util.concurrent包中定義的集合,因爲它們經過了精心的設計):

List<E> list = Collections.synchronizedList(new ArrayList<E>);

Map<K, V> map = Collections.synchronizedMap(new HashMap<K, V>());

Runnable封裝了一個異步運行的任務,可以看做沒有參數和返回值的異步方法。Callable與Runnable類似,但它有返回值。

Executors是一個線程執行器,用於管理線程的創建和執行,使用它可以創建多種線程池,常見線程池如下:

Executors.newCachedThreadPool:必要時創建新線程,空閒線程會保留60秒。

Executors.newFixedThreadPool:創建固定容量的線程池。

Executors.newSingleThreadPool:創建只有一個線程的線程池。

Executors.newScheduledThreadPool:用於預定指定的線程池。

想要了解更專業的互聯網知識,歡迎加入官方技術交流Q羣:547147889

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