在初遇章節我們就談到過Java是一門面向對象的語言,那么什么是面向對象呢?既然有面向對象語言,是否就有其他的語言?面向對象又能給我么帶來什么好處呢?接下來,我們將在這個章節探討下面向對象。
面向過程和面向對象
在目前的軟件開發領域有兩種主流的開發方法:結構化開發方法(面向過程)和面向對象開發方法。早期的編程語言C、Basic、Pascal等都是結構化編程語言,隨著時代的變遷,軟件的發展,人們發現了一種更好的可復用、可擴展和可維護的的方法,即面向對象,代表語言有C++,C#,Ruby,Java等。
- 面向過程
主張按功能來設計程序,特點是:自上而下,逐步求精,模塊化等。結構化程序設計的最小單元是函數,每個函數都負責完成一個功能。局限性有兩點:一,設計不夠直觀,與人類習慣思維不一致;二,適應性差,可擴展性不強。 - 面向對象
更優秀的程序設計思想,基本思想是使用類、對象、繼承、封裝、消息等基本概念進行程序設計。最小單位是類,由類可以生成系統中多個對象。
面向對象的基本特征
- 封裝
隱藏細節,通過公共方法暴露出該對象的功能。比如說一臺電腦,我們在不拆機的情況下看不到里面的主板,cpu,內存條,這些好比是私有方法,我們無法直接訪問,但是我們可以訪問它的鍵盤,開機鍵,顯示器,這些就是公共方法。 - 繼承
軟件復用的重要手段,子類繼承父類,可以直接復用父類的屬性和方法。 - 多態
子類對象直接賦給父類變量,運行的時候表現為子類的特性。
抽象也是面向對象的重要組成之一,但是不是基本特征。抽象是抽取我們當前目標所需要的東西,排除一些無關的信息。
Java面向對象特征
在初遇章節我們就談過Java的面向對象特征,我們這里再次談談Java面向對象特征。
-
一切皆是對象
除了8個基本數據類型,一切皆是對象。對象實現了數據和操作的結合,是Java的核心,具備唯一性,每個對象都有一個標識來引用,如果失去這個引用,那么這個對象將會變成垃圾,然后會被虛擬機回收掉。Java中不允許直接訪問對象,而是通過一個引用(也有一種稱呼為句柄)來操作對象。就如同設計一臺電視機,電視機上沒有任何按鈕,只能通過遙控來操作電視機。而這個遙控就是引用(句柄),電視機就是對象。
如 Person p = new Person();
image.png
p就是一個引用變量,其實就是C語言中的指針,只是Java友好的將這個指針封裝起來了,不需要繁瑣的去操作它。p中存儲的是Person的地址,當訪問p引用變量的成員變量和方法,其實就是訪問Person的成員變量和方法。
- 類和對象
對象也稱為實例instance,對象的抽象化是類,類的具體化是對象。Java語言使用class來定義對象,通過成員變量來描述對象的數據,通過方法來描述對象的行為特征。類之間的關系一般有兩種:- 一般->特殊關系(is a),Java中使用extends來表示這種特殊的關系,即繼承關系。
發生在繼承關系常見的一個概念是重寫(Overrride),重寫必須符合規則式:兩同兩小一大。即,方法名,形參列表相同;返回值類型要比父類的返回值類型更小或者相等;子類拋出的異常必須比父類的異常更小或者相等(不能一代不如一代);子類的訪問權限必須比父類的相等或者更大。
這里需要注意的是當父類的方法是private修飾時,子類是不能訪問的。 - 整體->部分關系(has a),組合關系,即Java中一個類里面保存了另一個類的引用來實現這種關系。
- 一般->特殊關系(is a),Java中使用extends來表示這種特殊的關系,即繼承關系。
修飾符
- private 私有的(類訪問權限)
- default 默認(包訪問權限)
- protected 子類訪問權限
- public 公共訪問權限
this和super
面向對象離不開this和super,這里我們分析下這兩個關鍵字
- this
this關鍵字指向調用該方法的對象,一般會出現在構造器和方法中。我們知道一種特殊的方法static修飾的,就是靜態方法,調用靜態方法可以使用類對象,所以this無法指向調用該方法的對象,所以靜態方法里面不能使用this,同樣,靜態方法中不能使用非靜態成員變量。 - super
super是用來子類調用父類的方法或者構造方法的。和this一樣,super也不能應用在靜態方法中。
子類調用父類構造器過程是:- 子類構造器執行體的第一行使用super顯式調用父類構造器,系統會根據super傳入的實例列表調用父類對應的構造器。
- 子類構造器執行體的第一行使用this顯式的調用本類的重載構造器,執行本類的另一個構造器時即會調用父類構造器。
- 子類構造器既沒有super,也沒有this,系統將會執行子類構造器之前,隱式的調用父類的無參構造器
final修飾符
final用來修飾類、變量、方法表示該類、變量、方法不可改變。
- final修飾變量
final修飾變量一旦獲得初始值后是不能改變的。如下圖,我們編譯器在編譯過程中就會報錯The final local variable a may already have been assigned
。
image.png
關于final修飾成員變量,必須顯式的初始化。
1.普通成員變量,必須在初始化塊(代碼塊)、聲明時或者構造器中初始化。- 靜態成員變量,必須在靜態代碼塊、聲明時初始化。
其實final的不可改變也不是絕對的,這就是final修飾基本類型變量和引用類型變量的區別,修飾引用類型時,只要保證引用類型的地址不變,而引用的這個對象完全可以改變。
- 靜態成員變量,必須在靜態代碼塊、聲明時初始化。
- final方法
final方法不能被重寫,如果父類不想讓子類繼承某個方法,可以定義為final類型。 - final類
final類不能有子類
聊聊Lambda表達式
Lambda表達式是Java8新增的一個重要功能,是大家期待已久的,它使得代碼更為的簡潔、直觀,接下來讓我們了解下Lambda表達式的功能。
- 組成部分
- 形參列表。允許省略參數類型,如果是一個參數甚至可以省略圓括號
- 箭頭。(->)必須是英文的劃線號和大于號組成
- 代碼塊。 如果代碼塊只有一條語句,可以省略花括號。如果只有一條返回語句,return關鍵字也可以省略。
比如說,我們可以創建一個線程類
Thread thread = new Thread((Runnable) ()->{
System.out.println(Thread.currentThread().getName()+"-run--");
});
這樣寫也是可以的
Thread thread = new Thread(()->System.out.println(Thread.currentThread().getName()+"-run--"));
- 方法引用和構造器引用
@FunctionalInterface
interface Converter{
Integer converter(String from);
}
Converter converter = from ->Integer.valueOf(from);
上面代碼其實就是對接口Converter的一個實現,然后把實現的地址賦給了引用變量converter。上面的代碼還可以簡寫成
Converter converter = Integer::valueOf;
調用converter.converter("5");也就是調用Integer.valueOf("5");
Lambda還有很多有意思的寫法,這就需要通過實踐中去探索了。
實戰
沒有實戰的概念就是耍流氓。
- 一個比較坑的問題
public class StaticThreadDemo implements Runnable{
public static Integer i = new Integer(0);
@Override
public void run() {
while(true){
synchronized (i) {
if(i<100){
i++;
System.out.println("i="+i);
}else{
break;
}
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new StaticThreadDemo());
Thread t2 = new Thread(new StaticThreadDemo());
t1.start();
t2.start();
}
}
問題輸出的結果是啥?按順序1-100?重復輸出1-100?無序的1-100?
運行的結果是:無序的,有重復,有確實的打印1-100。
就是說,這是個線程不安全的程序。那么為什么會導致這種情況呢?
分析:
synchronized 鎖對象的問題。我們知道,靜態變量和類信息(區分類對象)都是存放在我們的方法區中(因此靜態變量屬于類本身而不屬于實例),我們可以認為是線程共享的,唯一的。那,我們應該要理解的是引用i對應的對象是否被偷換的問題,如果沒有變化,那么,i肯定是線程安全的。我們編譯下這段代碼。
public class StaticThreadDemo implements Runnable {
public static Integer i = new Integer(0);
public StaticThreadDemo() {
}
public void run() {
while(true) {
Integer var1 = i;
synchronized(i) {
if(i.intValue() >= 100) {
return;
}
Integer e = i;
i = Integer.valueOf(i.intValue() + 1);
System.out.println("i=" + i);
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new StaticThreadDemo());
Thread t2 = new Thread(new StaticThreadDemo());
t1.start();
t2.start();
}
}
我們發現:Integer要獲取它的數據需要通過intValue() 方法,那么intValue()方法干了件什么事呢?查看Integer對象源碼
private final int value;
public int intValue() {
return value;
}
我們上面說過,對象的數據使用成員變量來描述,而這個成員變量是私有的,我們只能通過它的方法來獲取。
i++分解成了兩句
Integer e = i;
i = Integer.valueOf(i.intValue() + 1);
第一句我們比較好理解,就是用一個新的對象保存舊的數據,而第二句才是重點,我們先看下Interger的靜態方法valueOf
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
解釋一下這段代碼,就是當i的字段在-128和127之間的話,從IntegerCache緩存里面獲取,如果在區間之外的話,重新new一個對象,當然,緩存里面其實也是new Integer(i);所以說i的對象發生了改變了,因此,synchronized鎖不住對象了。
我們可以這樣理解這個流程,線程t1獲取鎖對象,進入run方法,執行i++后,鎖對象發生了改變,這個時候線程t1,t2一起爭取新的鎖對象,由于這一步和打印語句并行,所以存在線程安全問題。
這里提一下Integer內部類IntegerCache緩存對象問題,在Java5加入了自動裝箱和自動拆箱后(實現原理就是valueOf方法),如果int值在-128和127之間,Java不會new一個對象,而是直接從緩存里面獲取了,這就有了面試題Integer a =127;Integer b = 127;Integer c =128;Integer d = 128;
System.out.println(a==b); System.out.println(c==d);
尾聲
通過本章節,我們說到了面向對象的基本特性與面向過程的優勢所在,然后闡述了Java面向對象的特征,引出了引用數據類型。后面我們說到了一些修飾符,如訪問權限修飾符,關鍵字等。還提到了Java8新增的Lambda表達式的應用。總之,Java面向對象博大精深,不是一篇文章就能說得清楚的,如果要深入學習,我們還需要閱讀相關的書籍。在最后,我舉了一個多線程安全問題的案例,詳細分析了Integer對象在i++過程中的實際操作以及對象之間的變化,希望能幫到大家進一步了解面向對象思想。