指南)
提到Java基礎很多人第一反應是“語法簡單、背一背就行”但真到了面試和寫項目的時候才發(fā)現(xiàn)自己連環(huán)境變量為什么要配都沒搞明白。這篇文章就是寫給準備系統(tǒng)學Java、準備校招社招、或者工作中需要補基礎的人。我把這些年從零帶新人、自己面試和被面試的經(jīng)驗都揉進來不僅講語法更講“為什么”盡量讓每個知識點都能落到實際開發(fā)和面試里。1. 從零開始先搞懂Java到底是什么1.1 為什么是Java而不是直接上框架新同學最喜歡問的一句話是現(xiàn)在都SpringBoot了我直接學SpringCloud不行嗎我的回答一向是不行??蚣苁荍ava生態(tài)的一部分但框架解決的是“怎么組織代碼”Java解決的是“計算機怎么執(zhí)行你的指令”。你連JVM內存區(qū)域都說不清楚排查線上OOM就只能靠重啟連集合擴容機制都不懂寫出來的接口可能數(shù)據(jù)量一上來就卡死。Java能活這么多年核心優(yōu)勢是三點第一跨平臺靠JVM屏蔽操作系統(tǒng)差異第二生態(tài)龐大從后端到大數(shù)據(jù)到安卓到處都是Java的痕跡第三靜態(tài)強類型編譯期就能發(fā)現(xiàn)一大批低級錯誤。這些優(yōu)勢都是建立在扎實的語言基礎之上的。基礎這層不打牢后面學多線程、JVM調優(yōu)、分布式全都是空中樓閣。1.2 環(huán)境搭建JDK安裝和環(huán)境變量最好不要跳過去很多教程讓你直接下載JDK然后一路下一步連環(huán)境變量都不解釋。結果到IDEA里能跑命令行一敲java -version就報錯。原因很簡單操作系統(tǒng)不知道去哪里找java.exe。安裝JDK本身不復雜關鍵在于理解三個環(huán)境變量JAVA_HOMEJDK的安裝根目錄很多框架和工具比如Maven、Tomcat、IDEA都靠它定位JDK。PATH把%JAVA_HOME%\bin追加進去這樣操作系統(tǒng)能在任意目錄下找到java、javac這些命令。CLASSPATH老版本需要手動配置JDK 5以后其實可以不管但面試偶爾會問。它的作用是告訴JVM去哪里找類文件設置為.表示當前目錄。配置完成后打開新終端輸入java -version能看到類似“openjdk version 17.0.8”的輸出就說明成功了。這里有個坑Windows配置完環(huán)境變量必須重新打開命令行窗口才能生效不是配置完立刻就能用。順帶說一個高頻報錯錯誤: 源發(fā)行版 17 需要目標發(fā)行版 17。這個問題的本質是編譯版本和運行版本不一致。通常發(fā)生在你用JDK 17寫的代碼但IDEA里Project Structure的SDK設成了JDK 8或者Maven的maven.compiler.source/target沒配對。解決辦法是統(tǒng)一三處File - Project Structure - Project SDK、Settings - Build Tools - Maven - Runner - JRE、pom.xml里的java.version三處都改成同一個版本。2. 語法地基面向對象和數(shù)據(jù)類型2.1 基本類型和引用類型內存里的差別很大Java有8種基本類型byte、short、int、long、float、double、char、boolean。它們直接存值存在棧上用完就彈棧效率高。而String、數(shù)組、對象這些引用類型變量名存的是地址真正的內容在堆里。很多新手搞不清“基本類型變量怎么傳、對象怎么傳”面試題里經(jīng)典的“值傳遞還是引用傳遞”就是從這來的。一定要記住Java只有值傳遞。傳基本類型傳的是值的副本方法里改了不影響外面?zhèn)鲗ο髠鞯氖且玫刂返母北痉椒ɡ锿ㄟ^這個地址改對象內容外面會看到變化。但如果方法里把這個引用重新指向一個新對象外面的引用不會變。這題在面試里至少有一半人答錯。舉例說明public class PassTest { public static void main(String[] args) { StringBuilder sb new StringBuilder(hello); change(sb); System.out.println(sb.toString()); // hello world } static void change(StringBuilder s) { s.append( world); s new StringBuilder(modified); } }輸出是hello world因為s new StringBuilder(...)只改了方法內復制出來的引用原來的sb還是指向原來的對象。2.2 面向對象三件套封裝、繼承、多態(tài)面向對象是Java面試八股文里繞不開的題目。封裝的核心就是“隱藏內部細節(jié)對外暴露受控的方法”比如類的字段設成private通過getter/setter訪問。繼承解決的是“類與類之間的共性抽取”用extends實現(xiàn)。多態(tài)則要滿足三個條件有繼承、有重寫、父類引用指向子類對象。多態(tài)最典型的例子Animal a new Dog(); a.speak(); // 執(zhí)行的是Dog重寫后的speak很多人不理解為什么非要這樣寫。實際開發(fā)里多態(tài)的意義在于“面向接口編程”你可以寫一個方法接收Animal然后傳入任意子類方法不用改。比如支付場景里定義Payment接口AliPay和WeChatPay各實現(xiàn)一遍后續(xù)加新支付方式核心業(yè)務代碼不用動。還有個高頻考點是static方法能不能重寫。答案是不能。方法重寫是運行時動態(tài)綁定的而static方法屬于類編譯期就綁定好了。如果你在子類寫一個和父類同名的靜態(tài)方法那叫“隱藏”不是重寫。調用時看引用類型而不是對象類型。2.3 字符串、equals和hashCode的坑Java開發(fā)中String是最常用的類但也是坑最多的。先記住字符串常量池。用String s1 abc這種方式創(chuàng)建JVM會先去常量池找找到就復用用new String(abc)則是強制在堆里創(chuàng)建一個新對象。String a abc; String b abc; String c new String(abc); System.out.println(a b); // true同一個常量池對象 System.out.println(a c); // false堆上新建對象 System.out.println(a.equals(c)); // true比較內容所以字符串比較必須用equals判空用isEmpty()或isBlank()之前先判斷是否為null否則空指針。equals和hashCode的約定也是面試必問兩個對象用equals比較相等它們的hashCode必須相等。反過來說hashCode相等equals不一定相等。為什么因為HashMap這類集合先用hashCode定位桶再用equals找具體元素。如果你重寫了equals不重寫hashCode同一個對象放進HashSet可能被當成兩個元素這就是線上詭異Bug的來源。金額計算同樣不能忽視。用double直接做加減乘除0.10.2會出現(xiàn)一長串小數(shù)因為二進制無法精確表示部分十進制小數(shù)。正確做法是使用BigDecimal構造時用new BigDecimal(0.1)不要用new BigDecimal(0.1)后者其實也是個不精確的近似值。3. 核心API與進階概念3.1 集合框架面試重災區(qū)也是開發(fā)高頻區(qū)Java集合框架的核心接口是Collection和Map。List、Set、Queue都繼承了CollectionMap是獨立的鍵值對體系。面試必考的包括ArrayListvsLinkedListArrayList底層是數(shù)組查詢快、增刪慢尾插很快LinkedList底層是雙向鏈表頭部插入刪除快但隨機訪問要遍歷反而慢。HashMap的原理數(shù)組鏈表紅黑樹。默認容量16負載因子0.75擴容閾值12。當鏈表長度大于等于8且數(shù)組長度大于等于64時鏈表轉紅黑樹。put流程是先算hash(key)定位桶如果桶空直接放否則遍歷鏈表找相同key找到就替換值找不到就尾插最后判斷是否超過閾值超過就擴容。JDK 8以后改成尾插法就是為了避免并發(fā)擴容時鏈表成環(huán)。ConcurrentHashMapJDK 8以后用CASsynchronized鎖桶頭節(jié)點把鎖粒度從整張表降到單個桶并發(fā)度大幅提升。HashSet底層其實就是HashMapvalue統(tǒng)一用一個固定的Object占位。開發(fā)中選集合的原則很簡單單列數(shù)據(jù)用ArrayList需要去重用HashSet需要鍵值對應用HashMap需要線程安全再用CopyOnWriteArrayList或ConcurrentHashMap。別一上來就上Vector和Hashtable這兩個雖然線程安全但性能差屬于被淘汰的類。這里補充一個容易被問到的點為什么重寫equals必須重寫hashCode。還是用HashMap的get流程來說它先算key的hash找到桶再用equals比較如果兩個對象內容相同但hashCode不同它們就落在不同桶里get的時候根本找不到邏輯就錯了。3.2 異常體系別用try-catch兜底所有問題Java的異常分為受檢異常checked和運行時異常unchecked。Exception類下除了RuntimeException之外的都是受檢異常比如IOException、SQLException編譯器強制你處理。RuntimeException及其子類如NullPointerException、ArrayIndexOutOfBoundsException是運行時異常編譯器不強制但程序運行時就可能炸。正確的異常處理姿勢有三個原則能精確捕獲就精確捕獲不要只catch一個Exception不然日志里看不到真實問題。使用finally或try-with-resources釋放資源。JDK 7以后推薦try-with-resourcesAutoCloseable的類可以自動關閉代碼少且不會漏關。記錄日志時用log.error(操作失敗userId{}, userId, e)一定要把異常對象e傳進去否則打不出堆棧。不要在循環(huán)里catch異常。如果循環(huán)體里的代碼會拋異常盡量在循環(huán)內處理完繼續(xù)或者收集異常結果到列表里統(tǒng)一處理。否則一個數(shù)據(jù)出錯整個批量任務就中斷了。3.3 泛型、枚舉和注解寫優(yōu)雅代碼的基礎泛型沒有很多人想的那么神秘它本質上是“類型參數(shù)化”。寫成ListString編譯器在編譯期就幫你限定類型不用像以前那樣從List里取出來還要手動強轉。通配符? extends T和? super T是面試點extends規(guī)定了上界只能讀super規(guī)定了下界只能寫。這個規(guī)則一句話概括就是PECS生產(chǎn)者用extends消費者用super。枚舉用得好代碼能清爽很多。Java的enum不只是常量列表它可以帶字段、帶方法、帶構造器。比如狀態(tài)機里定義訂單狀態(tài)public enum OrderStatus { CREATED(1, 已創(chuàng)建), PAID(2, 已支付), SHIPPED(3, 已發(fā)貨), COMPLETED(4, 已完成); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } }需要用狀態(tài)碼反查枚舉時可以加一個static方法遍歷values()注意不要每次調用都遍歷整個枚舉可以初始化一個Map緩存性能更好。用枚舉替代魔法數(shù)字最大的好處是代碼可讀性強別人一看就知道狀態(tài)含義switch也不會寫錯case。注解則是一種元數(shù)據(jù)機制本身不改變程序行為但框架會讀取它做各種處理。最常見的Override是給編譯器看的Transactional是給Spring做事務增強的。自定義注解需要配合反射或AOP才有意義這一步學Spring的時候會理解得更深。4. JVM、并發(fā)與數(shù)據(jù)一致性4.1 JVM內存分區(qū)排查OOM的第一步JVM的內存區(qū)域是面試必問也是排查線上問題的前提。按規(guī)范分為程序計數(shù)器當前線程執(zhí)行的字節(jié)碼行號線程私有。Java虛擬機棧每個方法對應一個棧幀局部變量、操作數(shù)棧都在這里線程私有。棧深度不夠就拋StackOverflowError。本地方法棧為native方法服務。堆幾乎所有對象實例都在這里線程共享是GC的主要戰(zhàn)場。方法區(qū)存放類信息、常量、靜態(tài)變量、JIT編譯后的代碼。JDK 8以后方法區(qū)改名為元空間使用本地內存。高頻面試題是“對象在堆里的分配過程”。簡單說對象優(yōu)先在Eden區(qū)分配Eden滿了觸發(fā)Minor GC存活的對象進入Survivor的from區(qū)經(jīng)過多次GC還活著會晉升到老年代。大對象直接進老年代為了避免Eden和Survivor之間復制大對象的開銷。老年代滿了觸發(fā)Full GC如果Full GC之后還是放不下就拋OutOfMemoryError。再補充一個實戰(zhàn)思路線上應用頻繁Full GC第一步不是改代碼而是先dump堆快照。用jmap -dump:formatb,fileheap.hprof pid導出再用MAT或VisualVM分析大對象都在哪。絕大多數(shù)情況是某張表數(shù)據(jù)被一次性全查出來或者緩存沒設過期時間導致內存被占滿。4.2 多線程從線程安全到鎖的升級多線程是Java進階的分水嶺。實現(xiàn)多線程的三種方式繼承Thread、實現(xiàn)Runnable、實現(xiàn)Callable。實際開發(fā)中基本只用后兩種因為Java是單繼承繼承Thread就把唯一一次繼承機會浪費了。Callable和Runnable的區(qū)別在于Callable能返回結果、能拋異常。線程安全的核心是“可見性、原子性、有序性”。volatile解決可見性和一定程度上禁止指令重排但不保證原子性。synchronized保證原子性和可見性JDK 6以后做了鎖升級偏向鎖-輕量級鎖-重量級鎖。ReentrantLock功能更豐富支持可中斷、可超時、公平鎖但需要手動加鎖解鎖通常配try-finally使用。寫并發(fā)代碼有個經(jīng)驗優(yōu)先考慮java.util.concurrent包下的工具類而不是自己造輪子。比如用CountDownLatch等待多個線程執(zhí)行完用Semaphore做限流用BlockingQueue做生產(chǎn)者消費者模型。自己用synchronizedwait/notify實現(xiàn)生產(chǎn)消費很容易寫錯notify和wait的順序導致死鎖或假死。線程池也是一個必考點。Executors.newFixedThreadPool用起來方便但阿里規(guī)范明確不推薦因為LinkedBlockingQueue默認容量是Integer.MAX_VALUE任務積壓太多會占滿內存。推薦直接用ThreadPoolExecutor構造函數(shù)顯式指定核心線程數(shù)、最大線程數(shù)、空閑時間、隊列和拒絕策略。ThreadPoolExecutor pool new ThreadPoolExecutor( 4, 8, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue(1000), new ThreadPoolExecutor.CallerRunsPolicy() );CPU密集型任務核心線程數(shù)設為CPU核數(shù)1IO密集型任務可以設成CPU核數(shù)*2具體要根據(jù)壓測結果調整。4.3 數(shù)據(jù)一致性事務、鎖和分布式單機環(huán)境下保證數(shù)據(jù)一致性靠的是數(shù)據(jù)庫事務的ACID特性。Spring里最常用的是Transactional但用不好會踩很多坑。第一個坑Transactional默認只在RuntimeException回滾try-catch把異常吃了就永遠不會回滾。正確的做法是讓異常繼續(xù)往外拋或者在catch里手動TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。第二個坑Transactional失效。常見原因是方法被this調用而不是通過Spring代理對象調用。因為Transactional是基于AOP動態(tài)代理實現(xiàn)的this調用不會走代理。解決方法有兩種注入自身代理對象或者把需要事務的方法拆分到另一個Service里。第三個坑事務范圍過大。不要在事務里做RPC調用、文件上傳、發(fā)消息等耗時操作長事務會長時間占用數(shù)據(jù)庫連接和鎖拖垮并發(fā)性能。事務里只放數(shù)據(jù)庫操作其他事情放到事務外。到了分布式場景單機事務就不夠用了。分布式事務的常見方案有XA兩階段提交、TCC補償、MQ最終一致性、Seata的AT模式。面試問“怎么保證數(shù)據(jù)一致性”先分清場景強一致性需求用XA或TCC最終一致性需求用消息隊列加本地消息表。不要一上來就說分布式事務中間件先解釋清楚為什么不直接用數(shù)據(jù)庫事務。5. 框架、面試與實操Java后端完整成長路線5.1 從零到能干活一條少走彎路的路線圖如果讓人推薦Java學習路線我傾向于分四步走第一階段是語言基礎也就是本文前面所有內容大概需要一個月。包括基本語法、面向對象、集合、異常、IO、反射。第二階段是數(shù)據(jù)庫和Web基礎。先學MySQL的SQL編寫、索引原理、事務隔離級別再學HTTP協(xié)議、Servlet和JSP。JSP現(xiàn)在項目里用得少了但屬于Java的Web基礎理解一次有助于后續(xù)學SpringMVC。第三階段是主流框架。Spring、SpringMVC、MyBatis或MyBatis-Plus然后是SpringBoot。學框架不要只看用法要理解IOC容器和AOP思想。SpringBoot把大量配置自動化了新人用起來舒服但出了問題反而不知道從哪排查所以基礎的XML配置方式也要能看懂。第四階段是中間件和微服務。Redis、RabbitMQ或Kafka、Elasticsearch、SpringCloud Alibaba。學到這階段就可以開始模仿一個電商項目或管理后臺把框架和中間件結合起來用。5.2 Java面試八股文與其背答案不如建立知識脈絡Java面試題被戲稱為“八股文”但真正有效的復習方式不是死記硬背而是把知識點串成脈絡。比如問HashMap正常的回答應該從數(shù)據(jù)結構開始數(shù)組加鏈表再講hash函數(shù)和擴容再講紅黑樹化的條件最后講線程安全替代方案ConcurrentHashMap。這一條線講下來至少能聊五分鐘還能引出JVM、并發(fā)、集合等多個話題??梢宰鲆粋€知識卡片式的自查表主題必會核心點集合框架HashMap底層結構、擴容機制、HashSet原理、ArrayList擴容JVM內存分區(qū)、垃圾回收算法、常見收集器、類加載過程并發(fā)synchronized與ReentrantLock區(qū)別、volatile語義、線程池參數(shù)SpringIOC、AOP、Bean生命周期、事務傳播機制MySQL索引結構、最左前綴原則、事務隔離級別、MVCCRedis持久化機制、緩存穿透擊穿雪崩、分布式鎖網(wǎng)絡TCP三次握手四次揮手、HTTP與HTTPS、Cookie與Session設計模式單例、工廠、策略、模板方法、代理準備面試的時候挑至少兩個知識點能往深處展開比如“你項目里遇到過哪些并發(fā)問題”“Redis緩存一致性是怎么做的”。面試官想考察的不是你會背多少定義而是能不能把知識遷移到實際場景中。5.3 構建工具和IDE基礎之外的工程化能力基礎語法之外工程化工具也算Java基建設施。Maven和Gradle是主流構建工具Maven的核心概念是坐標、依賴、倉庫、生命周期。本地倉庫默認在.m2目錄下依賴下載慢可以配置阿里云鏡像。Gradle更靈活構建速度快但學習成本略高。IDEA是當前最主流的IDE。一些提升效率的插件如Lombok、MyBatisX、Alibaba Java Coding Guidelines值得裝。不過Lombok雖然簡化了代碼但也隱藏了equals/hashCode、構造器等細節(jié)面試的時候建議能說出Lombok的Data在編譯期做了什么事。工程化能力還包括學會用Git管理代碼學會用Maven打包學會看日志排查問題學會用jstack看線程狀態(tài)、用jstat看GC情況。這些不是面試必考但入職后是日常必備。5.4 設計模式和代碼質量基礎能力的試金石設計模式是Java進階繞不開的一塊面試常考的有單例模式、工廠模式、策略模式、模板方法模式和動態(tài)代理。單例模式的推薦寫法是枚舉或靜態(tài)內部類雙重檢查鎖要加上volatile防止指令重排。策略模式則適合處理多種if-else分支比如根據(jù)支付方式選擇不同的支付實現(xiàn)。public interface PayStrategy { void pay(BigDecimal amount); } Component(alipay) public class AliPayStrategy implements PayStrategy { Override public void pay(BigDecimal amount) { // 支付寶支付 } } Component(wechatpay) public class WeChatPayStrategy implements PayStrategy { Override public void pay(BigDecimal amount) { // 微信支付 } }如果配合Spring可以注入一個MapString, PayStrategykey就是Bean名稱這樣將來新增支付方式只需要新增實現(xiàn)類不用改核心邏輯。這就是策略模式的實戰(zhàn)價值。代碼質量方面推薦遵循阿里巴巴開發(fā)手冊類名大駝峰、方法名小駝峰、常量全大寫、equals常量放前面避免空指針、集合轉數(shù)組用list.toArray(new String[0])。規(guī)范的代碼不是給機器看的是給三個月后的自己看的。6. 避坑總結別在這些常見問題上翻車6.1 環(huán)境層面的高頻故障JDK版本選擇是個老生常談但總有人踩的坑?,F(xiàn)在很多公司用JDK 8新項目有上JDK 17的但不同版本的行為有差異。比如JDK 8的String內部是char[]JDK 9以后改成了byte[]并引入COMPACT_STRINGS。寫代碼時盡量別依賴具體JDK版本特性保證代碼可移植性。還有一個隱藏問題Windows系統(tǒng)同時裝了JDK 8和JDK 17的話JAVA_HOME改了但PATH里如果還留著舊版本的bin路徑命令行執(zhí)行的還是舊版本。排查時用where java看實際路徑比反復改環(huán)境變量高效得多。6.2 代碼層面的經(jīng)典Bug字符串判空、金額計算、集合刪除這三個是新手最容易寫錯的點。集合在遍歷時刪除元素直接list.remove(i)會拋ConcurrentModificationException。正確的是用Iterator的remove或者用JDK 8的removeIf。日期時間處理也值得單獨說。JDK 8之前用SimpleDateFormat它是線程不安全的多線程環(huán)境下同一個實例執(zhí)行format會產(chǎn)生臟數(shù)據(jù)。新代碼應該用LocalDate、LocalDateTime和DateTimeFormatter后者是線程安全的。如果和老代碼交互可以用Date.toInstant()轉成新API。對象深拷貝的問題在面試里也經(jīng)常出現(xiàn)。Object.clone()是淺拷貝BeanUtils.copyProperties也是淺拷貝如果對象里有List或者可變對象淺拷貝會共享內存。要實現(xiàn)深拷貝可以序列化后反序列化或者手動為每個字段創(chuàng)建新對象。實際項目中像訂單這種業(yè)務對象我一般直接新建對象再賦值最簡單也最可控。6.3 學習方法和心態(tài)調整最后聊點學習上的經(jīng)驗。學Java基礎最忌諱的是“只看不練”。視頻課或者書看一遍覺得都會了打開IDEA寫代碼才發(fā)現(xiàn)連import都不知道在哪。建議每學一個知識點就寫一個十幾行的小Demo驗證。比如學完HashMap自己寫一個統(tǒng)計單詞頻率的程序順便練練文件讀寫學完多線程寫一個簡單的生產(chǎn)者消費者Demo把BlockingQueue用起來。另一個親測有效的方法是費曼學習法。每個技術點用自己的話講給同事或寫進個人博客。你能把一個概念講得讓小白聽懂說明你是真懂了。這也是我這些年堅持寫文章、整理筆記的原因輸出倒逼輸入效率遠高于單純看資料。根據(jù)我個人的實際體會Java基礎的學習不需要一次追求完美第一次學能寫CRUD就夠了第二次學能理解Spring底層第三次學能定位復雜問題。每輪學習都帶著新的實踐問題回頭翻基礎每次都有新收獲。這就是Java這個語言的特點同時也是它真正值錢的地方——基礎知識學得越扎實后面的并發(fā)、分布式、調優(yōu)之路才能走得越穩(wěn)。