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