全棧必備 你需要了解的Java編程基礎(chǔ)
那一年,從北郵畢業(yè),同一年,在大洋的彼岸誕生了一門對軟件業(yè)將產(chǎn)生重大影響的編程語言,它就是——Java。1998年的時候,開始學(xué)習(xí)Java1.2,并在Java Orbix 上做服務(wù),而如今Java 9 已經(jīng)來了,而且 Java 10 也已經(jīng)不遠(yuǎn)了。
對一個全棧而言,Java 是必備的編程語言之一。 而談到Java,雖萬語千言卻不知從何開始,老碼農(nóng)從個人的角度看一下Java 語言的編程基礎(chǔ)。
虛擬機
Java 真正牛X的地方就在于JVM。JVM是一個抽象的計算機,具有指令集、寄存器、垃圾回收堆、棧、存儲區(qū)、類文件的格式等細(xì)節(jié)。所有平臺上的JVM向上提供給Java字節(jié)碼的接口完全相同,但向下提供適應(yīng)不同平臺的接口,規(guī)定了JVM的統(tǒng)一標(biāo)準(zhǔn)并實現(xiàn)了Java程序的平臺無關(guān)性。這就是常說的,Java的跨平臺,但跨越不同實現(xiàn)的JVM時還是有些許不同的。
JVM是運行java程序的核心虛擬機,而運行java程序不僅需要核心虛擬機,也需要其他的類加載器,字節(jié)碼校驗器以及大量的基礎(chǔ)類庫。JRE除了包含JVM之外還包含運行Java程序的其他環(huán)境支持。
當(dāng)JVM啟動時,由三個類加載器對類進(jìn)行加載:
- bootstrap classloader 是由JVM實現(xiàn)的,不是java.lang.ClassLoader的子類 ,負(fù)責(zé)加載Java的核心類,其加載的類由 sun.boot.class.path指定,或者在執(zhí)行java命令時使用-Xbootclasspath選項, 還可以使用-D選項指定sun.boot.class.path系統(tǒng)屬性值
- extension classloader ,它負(fù)責(zé)加載JRE的擴展目錄中JAR的類包,為引入除Java核心類以外的新功能提供了一個標(biāo)準(zhǔn)機制。
- system/application classloader,加載來自-classpath或者java.class.path系統(tǒng)屬性以及CLASSPATH操作系統(tǒng)屬性所指定的JAR包和類路徑??梢酝ㄟ^靜態(tài)方法ClassLoader.getSystemClassLoader()找到該類加載器。如果沒有特別指定,則用戶自定義的任何類加載器都將該類加載器作為它的父加載器。
ClassLoader加載Class的一般過程如下:
垃圾回收是JVM 中的一項重要技術(shù)。所謂垃圾回收只是針對內(nèi)存資源,而對于物理資源如數(shù)據(jù)庫連接、IO讀寫等JVM無能為力,所有程序中都需要顯式釋放。為了更快回收垃圾,可以將對象的引用變量設(shè)為null。垃圾回收具有不可預(yù)知性,即使調(diào)用了對象的finalize() ,System.gc()方法也不能確定何時回收,只是通知JVM而已。垃圾回收機制能精確標(biāo)記活著的對象,能精確定位對象之間關(guān)系,前者是完全回收的前提,后者實現(xiàn)歸并和復(fù)制等功能?,F(xiàn)在JVM有多種不同的垃圾回收算法實現(xiàn),不同的垃圾回收算法都有著典型的場景, 根據(jù)內(nèi)存和cpu使用的不同可以對垃圾回收算法進(jìn)行調(diào)整。
語法
作為一種編程語言,基本語法都是類似的,包括數(shù)據(jù)類型,操作符,語句,判斷和分支,循環(huán),遞歸等。
對于Java 的關(guān)鍵字可以做個文字游戲,排列成打油詩。
- if volatile default, catch class short,
- abstract package private, throw this protected.
- else char break, return super true,
- instanceof interface long, switch null native.
- while boolean case, try final static,
- extends false transient, throws void public.
- import new float, continue for double,
- implements int byte, do synchronized.
- finally, goto const......
如果沒有記錯的話,goto 和 const 是 java 的保留字而不是關(guān)鍵字。弄清楚每個關(guān)鍵字的意義、用法、典型場景等,才算是“磨刀不誤砍柴功”。
數(shù)據(jù)
java 中的基本類型有4類8種:整型(int, short, long, byte),浮點型( float, double),邏輯型 boolean和 文本型 char。
Java中的基本數(shù)據(jù)結(jié)構(gòu)大多在java.util 中體現(xiàn),主要分為Collection和map兩個主要接口,而程序中最終使用的數(shù)據(jù)結(jié)構(gòu)則是繼承自這些接口的數(shù)據(jù)結(jié)構(gòu)類。
- import java.util.Hashtable;
- import java.util.ArrayList;
- import java.util.HashMap;
- import java.util.HashSet;
- import java.util.LinkedHashMap;
- import java.util.LinkedHashSet;
- import java.util.LinkedList;
- import java.util.Stack;
- import java.util.TreeMap;
- import java.util.TreeSet;
- import java.util.Vector;
- .....
一般的,一個空的對象需要占用12字節(jié)的堆空間,一個空的String就要占用40字節(jié)的堆空間,這或許就是推薦用stringbuilder的一個原因吧。在Java中,類型決定行為,例如byte可以起到限制數(shù)據(jù)的作用,但是并不能節(jié)約內(nèi)存,在內(nèi)存中byte和int一樣是占用4字節(jié)的空間。一個對象的占用堆空間的多少一般與類中非static的基本數(shù)據(jù)類型和引用變量有關(guān)。每一個數(shù)組中的元素都是一個對象,每一個對象都有一個16字節(jié)的數(shù)組對象頭。
回憶一下堆棧,Java 的堆是一個運行時數(shù)據(jù)區(qū),類的對象從中分配空間。只有通過new()方法才能保證每次都創(chuàng)建一個新的對象,它們不需要程序代碼來顯式的釋放。堆是由垃圾回收來負(fù)責(zé)的。Java的棧存取速度比堆要快,棧數(shù)據(jù)可以共享,存在棧中的數(shù)據(jù)大小與生存期必須是確定的,主要存放一些基本類型的變量和對象句柄。
(圖片來自 https://www.programcreek.com/2013/09/top-8-diagrams-for-understanding-java/)
可以通過如下的方式粗略的判斷不同數(shù)據(jù)類型的內(nèi)存使用狀況:
- Runtime.getRuntime().gc();
- Thread.yield();
- iBefore = Runtime.getRuntime().freeMemory();
- 類 變量 = new 類(參數(shù)類別);
- Runtime.getRuntime().gc();
- Thread.yield();
- iAfter = Runtime.getRuntime().freeMemory();
- System.out.println(iBefore-iAfter);
另外,Java 中的引用對內(nèi)存也有著不同的影響,主要包括:
- 強引用: strong reference
- 軟引用: soft reference
- 弱引用: weak reference
- 虛引用: Phantom Reference
接口
抽象類和接口是Java 的兩大利器, 抽象類是OOP 的共性,而接口則簡單規(guī)范,提高了代碼的可維護(hù)性和可擴展性,同時是軟件松耦合的重要方式。對修改關(guān)閉,對擴展(不同的實現(xiàn)implements)開放,接口本身就是對開閉原則的一種體現(xiàn)。
Java接口是一系列方法的聲明,是一些方法特征的集合,一個接口只有方法而沒有方法的實現(xiàn)。弄一點玄虛,接口是一組規(guī)則的集合,它規(guī)定了實現(xiàn)本接口的類或接口必須擁有的一組規(guī)則,是在一定粒度上同類事物的抽象表示。
- <修飾符>interface<接口名>{
- [<常量聲明>]
- [<抽象方法聲明>]
- }
接口是類型轉(zhuǎn)換的前提和動態(tài)調(diào)用的保證。實現(xiàn)某一接口就完成了類型的轉(zhuǎn)換也就是多重繼承,一般用來作為一個類型的等級結(jié)構(gòu)的起點;動態(tài)調(diào)用則只關(guān)心類型,不關(guān)心具體類。接口可以為不同類順利交互提供標(biāo)準(zhǔn)。
Java中的類描述了一個實體,包括實體的狀態(tài),也包括實體可能發(fā)出的動作。而接口定義了一個實體可能發(fā)出的動作,但只是定義了這些動作的原型,沒有實現(xiàn),也沒有任何狀態(tài)信息。所以接口有點象一個規(guī)范、一個協(xié)議,是一個抽象的概念;而類則是實現(xiàn)了這個協(xié)議,滿足了這個規(guī)范的具體實體,是一個具體的概念。
從程序角度簡單理解,接口就是函數(shù)聲明,類就是函數(shù)實現(xiàn)。需要注意的是同一個聲明可能有很多種實現(xiàn)。
泛型
所謂“泛型”,就是寬泛的數(shù)據(jù)類型,任意的數(shù)據(jù)類型。Java 中的泛型是以C++模板為參照的,本質(zhì)是參數(shù)化類型的應(yīng)用,主要包括:
泛型類,例如:
- public class MyGeneric<T,V> {
- T obj_a;
- V obj_b;
- MyGeneric(T obj_1,V obj_2){
- this.obj_a = obj_1;
- this.obj_b = obj_2;
- }
泛型接口,例如:
- interface MyInterface<T extends Comparable<T>>{
- //...
- }
泛型方法,例如:
- <T extends Comparator<T>, V extends T> boolean MyIn(T x, V[] y)
泛型中的類型參數(shù)只能用來表示引用類型,不能用來表示基本類型,如 int、double、char 等。但是傳遞基本類型不會報錯,因為它們會自動裝箱成對應(yīng)的包裝類。類型參數(shù)必須是一個合法的標(biāo)識符,習(xí)慣上使用單個大寫字母,通常情況下,K 表示鍵,V 表示值,E 表示異?;蝈e誤,T 表示一般意義上的數(shù)據(jù)類型。
使用有界通配符,可以為參數(shù)類型指定上界和下界,從而能夠限制方法能夠操作的對象類型。最常用的是指定有界通配符上界,使用extends子句創(chuàng)建。 對于實現(xiàn)了<? extends T>的集合類只能將它視為生產(chǎn)者向外提供元素(get),而不能作為消費者來對外獲取元素(add)。
Java泛型只能用于在編譯期間的靜態(tài)類型檢查,然后編譯器生成的代碼會擦除相應(yīng)的類型信息,這樣到了運行期間實際上JVM根本就知道泛型所代表的具體類型。在Java中不允許創(chuàng)建泛型數(shù)組,無法對泛型代碼直接使用instanceof。
使用泛型,可以消除顯示的強制類型轉(zhuǎn)換,提高代碼復(fù)用,還可以提供更強的類型檢查,避免運行時的ClassCastException。
反射
JAVA反射機制是在運行狀態(tài)中,對于任意一個類,都能夠知道這個類的所有屬性和方法;對于任意一個對象,都能夠調(diào)用它的任意一個方法。普通調(diào)用需要在編譯前必須了解所有的class,包括成員變量,成員方法,繼承關(guān)系等。而反射可以于運行時加載、探知、使用編譯期間完全未知的類。也就是說,Java程序可以加載一個運行時才得知名稱的class,獲悉其完整構(gòu)造。
Java反射的方式主要分為兩類:Java.lang.reflect.*和Cg-lib工具包。
因為在反射調(diào)用中同樣要遵循java的可見性規(guī)約,因此Class.getMethod方法只能查找到該類的public方法。如果要獲取聲明為private的方法對象,則需要通過Class.getDeclaredMethod,而且在invoke前要設(shè)置setAccessable(true)才能保證調(diào)用成功。如果的確需要調(diào)用父類方法,可以通過Class.getInterface方法查找父類,再實例化一個父類對象,然后按照調(diào)用private Method的方式進(jìn)行調(diào)用。
反射的應(yīng)用廣泛,例如Spring容器的注入,就是運用了反射的方式,通過配置文件讀取欲實例化的類的名稱,屬性,然后由spring容器統(tǒng)一實例化,既達(dá)到了注入的目的,又可以通過容器統(tǒng)一控制bean的作用域、生命周期等。J
在框架和容器中,比較廣泛的就是java bean的規(guī)范,或者POJO,以及一些作為與數(shù)據(jù)庫交互載體的持久化對象,都會有要求:
每個field都要有setXxx/getXxx方法,命名符合駝峰命名法,且需要聲明為public的。
含有一個無參的構(gòu)造方法。 ***條就是為了方便反射屬性值,通過get/set方法。另一條是為了保證可以通過cls.newInstance()實例化一個新對象。 另外還有servlet(要有init、service、doGet、doPost方法),filter(要有doFilter方法)。這些組件定義的規(guī)范就是為了容器可以通過反射的方式進(jìn)行統(tǒng)一調(diào)用和管理。
ava.lang.reflect包中還自帶了代理模式的一個實現(xiàn),靜態(tài)代理和動態(tài)代理都是有意思的事, 很多插件化開發(fā)都使用了代理模式。
注解
注解這種機制允許在編寫代碼的同時可以直接編寫元數(shù)據(jù)。注解就是代碼的元數(shù)據(jù),包含了代碼自身的信息。
注解可以被用在包,類,方法,變量,參數(shù)上。自Java8開始,有一種注解幾乎可以被放在代碼的任何位置,叫做類型注解。被注解的代碼并不會直接被注解影響,只會向第三系統(tǒng)提供關(guān)于自己的信息以用于不同的需求。注解會被編譯至class文件中,而且會在運行時被處理程序提取出來用于業(yè)務(wù)邏輯。當(dāng)然,創(chuàng)建在運行時不可用的注解也是可能的,甚至可以創(chuàng)建只在源文件中可用,在編譯時不可用的注解。
Java自帶的內(nèi)建注解可以叫元注解,由JVM 對這些注解進(jìn)行執(zhí)行。常見的元注解如下:
@Retention:用來說明如何存儲已被標(biāo)記的注解,值包括:SOURCE, CLASS和RUNTIME。
@Target:這個注解用于限制某個元素可以被注解的類型。例如:
- ANNOTATION_TYPE :應(yīng)用到其他注解上
- CONSTRUCTOR:使用到構(gòu)造器上
- FIELD:使用到域或?qū)傩陨?/li>
- LOCAL_VARIABLE:使用到局部變量上。
- METHOD:使用到方法級別的注解上。
- PACKAGE:使用到包聲明上
- PARAMETER:使用到方法的參數(shù)上
- TYPE:使用到一個類的任何元素上。
@Documented:被注解的元素將會作為Javadoc產(chǎn)生的文檔中的內(nèi)容,都默認(rèn)不會成為成為文檔中的內(nèi)容。這個注解可以對其它注解使用。
@Inherited:在默認(rèn)情況下,注解不會被子類繼承。被此注解標(biāo)記的注解會被所有子類繼承。
還有 @Deprecated,@SuppressWarnings,@Override等等。
Java反射API包含了許多方法來在運行時從類、方法或者其它元素獲取注解的手段。接口AnnotatedElement包含了大部分重要的方法,如下:
- getAnnotations(): 返回該元素的所有注解,包括沒有顯式定義該元素上的注解。
- isAnnotationPresent(annotation): 檢查傳入的注解是否存在于當(dāng)前元素。
- getAnnotation(class): 按照傳入的參數(shù)獲取指定類型的注解。返回null說明當(dāng)前元素不帶有此注解。
自己寫個注解,會讓代碼變得簡潔。一些類庫如:JAXB, Spring Framework, Findbugs, Log4j, Hibernate, Junit等,使用注解來完成代碼質(zhì)量分析,單元測試,XML解析,依賴注入和許多其它的工作。
線程
一個JVM 相當(dāng)于操作系統(tǒng)的一個進(jìn)程,Java線程是進(jìn)程的一個實體,是CPU調(diào)度和分派的基本單位,JVM線程調(diào)度程序是基于優(yōu)先級的搶先調(diào)度機制。 線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程包含以下內(nèi)容:
- 一個指向當(dāng)前被執(zhí)行指令的指令指針
- 一個棧
- 一個寄存器值的集合,定義了一部分描述正在執(zhí)行線程的處理器狀態(tài)的值
- 一個私有的數(shù)據(jù)區(qū)
在 Java程序中,有兩種方法創(chuàng)建線程:對 Thread 類進(jìn)行派生并覆蓋 run方法和通過實現(xiàn)Runnable接口創(chuàng)建。獲取當(dāng)前線程的對象的方法是Thread.currentThread()。實現(xiàn)Runnable接口相對于繼承Thread類而言,更適合多個相同的程序代碼的線程去處理同一個資源,繞過單繼承限制,而且線程池只能放入實現(xiàn)Runable或callable類線程,一般不直接放入繼承Thread的類。
線程池的基本思想還是一種對象池的思想,開辟一塊內(nèi)存空間,里面存放了眾多(未死亡)的線程,池中線程執(zhí)行調(diào)度由池管理器來處理。當(dāng)有線程任務(wù)時,從池中取一個,執(zhí)行完成后線程對象歸池,這樣可以避免反復(fù)創(chuàng)建線程對象所帶來的性能開銷,節(jié)省了系統(tǒng)的資源。線程池分好多種:固定尺寸的線程池、單任務(wù)線程池、可變尺寸連接池、延遲連接池、自定義線程池等等。
理解Java線程的狀態(tài)機(新建,就緒,運行,睡眠/阻塞/等待,消亡等)對于線程的使用很有幫助。
(圖片來自http://blog.csdn.net/Evankaka/article/details/44153709)
在使用任何多線程技術(shù)的時候,都要關(guān)注線程安全。盡管線程安全類中封裝了必要的同步機制,從而客戶端無須進(jìn)一步采取同步措施,但還是要關(guān)注一下資源競爭即所謂的競態(tài)條件。競態(tài)條件成立的三個條件: 1)兩個處理共享變量 2)至少一個處理會對變量進(jìn)行修改 3)一個處理未完成前另一個處理會介入進(jìn)來 只要三個條件有一個不具備,就可以寫線程安全的程序了。 規(guī)避一,沒有共享內(nèi)存,就不存在競態(tài)條件了,例如利用獨立進(jìn)程和actor模型。 規(guī)避二,比如Java中的immutable 規(guī)避三,不介入,使用協(xié)調(diào)模式的線程如coroutine等,也可以使用表示不便介入的標(biāo)識——鎖、mutex、semaphore,實際上是使用中的狀態(tài)牌。鎖的使用問題包括死鎖和無法組合,只能寄托于事務(wù)內(nèi)存來奢望解決了。
通過Java多線程技術(shù),可以提高資源利用率,程序擁有更好的響應(yīng)。
排錯
Zero Bug 是每個程序員的目標(biāo), debug 是項繁重的工作,減少bug一般從Error Handling 開始,在Java 中主要體現(xiàn)在異常處理。
異常處理
Java 中 Exception的繼承關(guān)系如下圖:
(圖片來自https://www.programcreek.com/2013/09/top-8-diagrams-for-understanding-java/)
紅色部分為必須被捕獲,或者在函數(shù)中聲明為拋出該異常。其中,throwable 是一個有趣的東西, 在某些極端情況下, 直接catch throwable 才能得到想要的效果。
靜態(tài)代碼分析
據(jù)說,在整個軟件開發(fā)生命周期中,30% 至 70% 的代碼邏輯設(shè)計和編碼缺陷是可以通過靜態(tài)代碼分析來發(fā)現(xiàn)和修復(fù)的。但是,code review 往往要求大量的時間消耗和相關(guān)知識的積累,因此使用靜態(tài)代碼分析工具自動化執(zhí)行代碼檢查和分析,能夠極大地提高軟件可靠性并節(jié)省軟件開發(fā)和測試成本。
靜態(tài)代碼分析是指無需運行被測代碼,僅通過分析或檢查源程序的語法、結(jié)構(gòu)、過程、接口等來檢查程序的正確性,找出代碼隱藏的錯誤和缺陷,如參數(shù)不匹配,有歧義的嵌套語句,錯誤的遞歸,非法計算,可能出現(xiàn)的空指針引用等等。靜態(tài)代碼分析主要是基于缺陷模式匹配,類型推斷,模型檢查和數(shù)據(jù)流分析等。
通過靜態(tài)代碼分析工具可以自動執(zhí)行靜態(tài)代碼分析,快速定位代碼隱藏錯誤和缺陷;幫助我們更專注于分析和解決bug;顯著減少在代碼逐行檢查上花費的時間,提高軟件可靠性并節(jié)省軟件開發(fā)和測試成本。
常用的靜態(tài)代碼工具有checkstyle,findbugs,PMD等,其中Checkstyle 更加偏重于代碼編寫格式檢查,而 FindBugs,PMD,Jtest 等著重于發(fā)現(xiàn)代碼缺陷,但個人還是喜歡Sonar。
內(nèi)存泄漏
在Java中排錯的一個麻煩就是內(nèi)存泄露。內(nèi)存泄漏是指無用對象持續(xù)占用內(nèi)存或無用對象的內(nèi)存得不到及時釋放,從而造成內(nèi)存空間的浪費。內(nèi)存泄露有時不嚴(yán)重且不易察覺,這樣可能不知道存在內(nèi)存泄露,但有時也會很嚴(yán)重,會引發(fā)Out of memory。
常用的Java內(nèi)存分析工具有VisualVM、jconsole、jhat、JProfiler、Memory Analyzer (MAT)等??紤]能處理的Heapdump大小及速度,網(wǎng)絡(luò)環(huán)境,可視化分析,內(nèi)存資源限制,是否免費使用等,推薦的工具為jmap + MAT。
Java中內(nèi)存分析的一般步驟如下:
- 把Java應(yīng)用程序使用的堆dump下來,啟動時加虛擬機參數(shù):-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=path,這樣在程序發(fā)生OOM時,會自動在相關(guān)路徑下生成dump文件
- 然后使用Java heap分析工具,找出對象數(shù)量或占用內(nèi)存太多的對象 執(zhí)行jmap -dump:format=b,file=heap.bin pid 其中,format=b,表示dump出來的文件是二進(jìn)制格式,file=heap.bin,表示dump出來的文件名是heap.bin,pid是進(jìn)程號。
- 需要分析嫌疑對象和其他對象的引用關(guān)系,結(jié)合程序的源代碼,找出原因。 可以將Heapdump拉到本地,使用MAT打開進(jìn)行分析。如果Heapdump較大,本地內(nèi)存不夠,可以在服務(wù)器上執(zhí)行sh ParseHeapDump.sh Heapdumpfile,得到分解后的文件,然后拉到本地,再使用MAT打開,就可以進(jìn)一步分析了。
【本文來自51CTO專欄作者“老曹”的原創(chuàng)文章,作者微信公眾號:喔家ArchiSelf,id:wrieless-com】