Java SE
0. 前言
以下內容,是我認爲SE
部分比較有意思的地方 ,故特此編寫。如果有其他重要的點被遺漏, 歡迎評論區補充,補充後期。
1. 面向過程和麪向對象
面向對象和麪向過程是一種思想,其並不是由編程語言所實現的。比如我們通常說,C
語言是面向過程的,Java
是面向對象的。難道C
語言就一定不能實現面向對象的思想嗎?答案是否定的。
這裏以我個人的簡潔闡述一下面向過程和對象對象的區別。面向過程,通常是站在程序員即我們的角度去看待問題,去一步步想我們應該怎麼做才能解決問題。而面向對象的思想中,程序員通常是第三人稱,通過對需求進行抽象,抽象出一個個的對象,通過這些對象的協作來解決問題。但是每個對象去完成其工作依然屬於面向過程。可以這麼理解,面向對象在宏觀上是多個對象相互合作,微觀上依然是面向過程的。
2. JDK、JRE和JVM的區別
JDK
是Java
程序的開發包,而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
語言號稱一切皆對象,但是基本數據類型並不屬於對象,由此便產生了包裝類型。包裝類型的作用除了爲了表示對象,還可以實現與基本數據類型的自動轉換。
既然包裝類型屬於對象,那麼其與基本數據類型也存在很大的區別:
- 初值不同,基本數據類型有其各自的數據類型,而包裝類型的初值爲
null
(這裏的初值是成員變量而不是方法中的變量,方法中的變量必須賦初值才能使用); - 存儲位置不同,基本類型存儲位置在棧中,而包裝類型的存儲位置位於堆中(隨着棧逃逸技術的出現,對象不在侷限於出現在堆中);
- 泛型不能被指定爲基本數據類型,只能爲引用類型。
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
這個不可變字符串時,不得不說到可變字符串StringBuild
及StringBuffer
。因爲我們有的時候並不想爲了修改字符串而頻繁的去創建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
StringBuffer
與StringBuilder
的區別是StringBuffer
是線程安全的,其方法基本都是由synchronized
所修飾。
7. 抽象類與接口
抽象類的作用往往是定義一個模板,然後其子類照着這個模板去實現相關模塊,如前面說到的StringBuilder
,其便是繼承自一個AbstractStringBuilder
。接口的作用往往是定義有哪些行爲。
接口更多的是在系統架構設計方法發揮作用,主要用於定義模塊之間的通信契約。而抽象類在代碼實現方面發揮作用,可以實現代碼的重用。
抽象類與普通類的區別:
- 類名由
abstract
所修飾
public abstract class Person {
}
-
類名不能被
final
修飾 -
可以不包含方法的實現
public abstract class Person {
//如果方法被abstract修飾,說明這是一個抽象方法
public abstract void sayHello();
}
- 不能實例化
這裏有一點值得注意,便是抽象類不能被實例化,那麼其構造方法存在的意義是什麼?
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
可以看出,在執行子類的構造器時,會執行抽象類的構造方法,所以抽象類的構造方法可以完成一些初始化操作。
接口和抽象類的區別:
- 接口可視作比抽象類更加抽象的類。
- 接口中的方法只能爲
public
,對於抽象方法而言,其只是不能被private
修飾。 - 接口沒有構造方法。
- 接口中的變量默認被
public static final
所修飾。 - 在
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
,運行時類型則爲Java
、Python
以及Go
。
多態下,成員訪問的規則如下:
成員變量:編譯看左邊,運行看左邊。
成員方法:編譯看左邊,運行看右邊。