聊一聊 Java 一些核心知識點
詳解java常見的核心基礎知識
能不能給我簡單介紹一下java
Java是1995年由sun公司推出的一門高級語言,該語言具備如下特點:
- 簡單易學,相較于C語言和C++,沒有指針的概念,所以操作和使用是會相對容易一些。
- 平臺無關性,即Java程序可以通過Java虛擬機在不同硬件不同操作系統上運行,而無需進行任何修改。
- 面向對象:面向對象是一種程序設計技術,以木匠工作為例,使用面向對象方式實現的木匠的工作關注重點永遠是制作椅子,其次才是工具。而面向過程則優先關注制作工具。與C++不同的是,Java不支持多繼承,取而代之的是更加簡單的接口的概念。
- 編譯與解釋并存:與C++,Rust,Go不同的是,java源代碼運行時需要先編譯稱為字節碼文件(.class),然后再通過解釋器翻譯成機器碼運行。
- 可靠性:Java通過早期檢測以及運行時檢測消除了容易出錯的情況。以及與C++不同的是,操作數組、字符串方式采用的是指針模型避免了重寫內存或者損壞數據的問題。
- 安全性:Java適用于網絡/分布式環境,為了達到這個目標,Java在防病毒,防篡改做出很大的努力。
- 支持網絡編程,使用非常方便,也有不錯的性能。
- 支持多線程編程。
Java和C++主要區別有哪些?能不能給我說說優缺點
Java屬于半編譯、半解釋型語言,通過編譯器javac生成JVM可識別的字節碼(.class)之后,由JVM將其解釋成計算機可執行的機器碼,所以執行效率相對低,依賴解釋器,但是跨平臺較好?;谶@種情況JVM在后端編譯階段會將那些頻繁執行的熱點代碼(hot spot code)的字節碼直接生成機器碼直接執行,由此避免頻繁編譯+解釋的步驟:
而C++則是編譯型語言,代碼通過編譯后就可以直接生成機器碼直接在服務器上運行,執行速度塊、效率高、因為代碼是直接服務于針對性的操作系統,所以為了兼容性不得不在邏輯上進行特殊處理,所以跨平臺相對差一些。
除此之外java和C++還有以下幾個區別:
- 多繼承:Java支持單繼承類,多繼承接口,而C++支持多繼承。
- 內存管理方面:java有JVM自動管理、而C++需要手動管理。
- 參數傳遞方面:Java僅僅支持值傳遞,而C++支持引用、指針、值傳遞。
- 系統資源控制方面:Java相關方法依賴于JVM底層實現沒有指針等機制操作內存空間,不夠底層,所以相對C++會弱一些。
Java中有了基本類型為什么還需要包裝類
java開發是面向對象的,很多特定的操作都需要以對象的維度運行,最典型的就是集合的泛型:
List<String> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
而且基本類型作為成員變量時,如果沒有做任何初始化則取對應類型的零值,這種情況下會導致某些業務場景的錯誤,例如我們的某個int變量要求必傳,設置為基本類型時,假如前端沒有傳值,那么這個值就會得到0而不是空,這種場景就很可能導致業務上的錯誤:
public class Main {
private int a;
public static void main(String[] args) {
System.out.println("a="+new Main().a);//a=0
}
}
Java的三種技術架構是什么
Java有多種技術架構,但以下是其中三種常見的:
- Java SE(Java Standard Edition):Java標準版是Java平臺的基礎版本,用于開發和部署桌面、嵌入式和服務器應用程序。它包含了Java編程語言、Java虛擬機(JVM)、Java類庫等核心組件。Java SE提供了廣泛的功能和API,可以用于開發各種類型的應用程序,從簡單的命令行工具到復雜的圖形用戶界面(GUI)應用。
- Java EE(Java Enterprise Edition):Java企業版是用于開發和部署企業級應用程序的Java平臺。它建立在Java SE的基礎上,提供了一組擴展和技術,用于構建分布式、可擴展和安全的企業應用。Java EE包括諸如Servlet、JavaServer Pages(JSP)、Enterprise JavaBeans(EJB)、Java Persistence API(JPA)等技術,還提供了支持事務管理、安全性、消息傳遞、Web服務等企業級功能的各種規范和API。
- Java ME(Java Micro Edition):Java微版是專門設計用于嵌入式設備和移動設備的Java平臺。它針對資源受限的環境,如智能手機、個人數字助理(PDA)、嵌入式系統等,提供了一個輕量級的Java運行時環境。Java ME包括了一組精簡的Java類庫和API,使開發人員能夠構建適用于小型設備的應用程序,如移動游戲、手機應用等。
這些Java技術架構可以根據應用程序的需求和目標進行選擇和使用。Java SE適用于通用的應用程序開發,Java EE適用于構建大型企業級應用,而Java ME適用于嵌入式和移動設備的開發。
JVM簡介
Java虛擬機(Java Virtual Machine,JVM)是Java平臺重要組成部分,它是一個在計算機上運行Java字節碼的虛擬計算機,它負責解釋并執行編譯后的Java字節碼,并將其轉換為底層操作系統能夠理解的機器碼。
JDK和JRE分別是什么
JDK(Java Development Kit)是Java開發工具包,包含了JRE所有的東西,所以作為開發人員,只需要安裝JDK即可
JRE(Java Runtime Environment)是Java運行環境,包含運行所需要的類庫以及JVM。你可能認為如果僅僅要運行Java程序,安裝JRE即,但是某些web程序例如需要將JSP 轉換為 Java servlet就需要jdk編譯了,所以保守起見,無論運行還是開發,我們都建議在操作系統上安裝jdk。
什么字節碼文件,字節碼文件的優勢是什么
首先我們了解一下從編寫Java代碼到Java代碼被執行的過程:
- 開發人員編寫java代碼。
- 通過javac編譯生成字節碼即.class結尾的文件。
- 通過解釋器翻譯計算機可以理解的機器碼,需要注意的是JIT解釋器通過運行時編譯,會將某些字節碼對應的機器碼保存下來,這就是HotSpot 的Lazy Evaluation原則,將那些熱點代碼保存起來,執行次數越多速度越快(這里參照了CPU分支預測器的工作原理)
- CPU收到機器碼執行該指令。
JDK9的AOT技術以及為什么不全部使用 AOT 呢?
JDK9的AOT(Ahead-of-Time)編譯技術使得Java字節碼可直接編譯為本地機器碼,相比于傳統JIT編譯來說在運行時將字節碼轉為機器碼,這種做法帶來了兩個好處:
- 啟動時間快,因為AOT的提前編譯,使得運行時避免了一些編譯的開銷,所以啟動的效率得以提高。
- 執行性能優化,因為AOT編譯是在運行前針對Java程序進行全局優化,而不是僅僅針對運行時的熱點代碼,所以程序執行性能相比JIT更高。
同樣的,它也帶來如下幾個缺點:
- Java是一門動態語言,需要動態加載類、反射、動態代碼生成等特性,這些特性無法在編譯器靜態決定,需要運行時才能進行決策的,所以使用AOT這些特性就無法支持了。
- Java應用程序需要長期的運行,可能需要動態加載和卸載類,或者在運行時進行動態的優化,AOT的靜態編譯就無法滿足這些特性。
- Java生態系統包含大量的第三方框架,如果使用AOT可能使得很多特性都無法支持,最典型就是Spring框架的AOP。
Java 有哪幾種基本數據類型?float 和 double 的區別是?
對應的基本類型和默認0值如下:
基本類型 位數 字節 默認值
int 32 4 0
short 16 2 0
long 64 8 0L
byte 8 1 0
char 16 2 'uo000'
float 32 4 0f
double 64 8 0d
boolean 1 false
所以float為4個字節,double為8字節。
Java有那些引用類型
除了8種基本類型以外,其余類型都是引用類型即變量所存儲的都是數據的引指向堆中某塊內存,對應的引用類型有:
- 類類型即用 class關鍵字修飾
- 接口 類型interface
- 數組類型arr[]
自動類型轉換和強制類型轉換
精度小的賦值給精度大的Java會進行自動轉換,反之就需要我們進行強制轉換了。
精度自小向大有兩種情況,一種是字符和數字的轉換,char賦值給int類型會自動轉換為int。因為char占兩個字節,存放都是0到 65535以內的整數,所以賦值給4個字節的int就會發生自動類型轉換。
char c = 'a';
int num = c;
反之int轉char就需要強制轉換了:
int num=1;
char c=(char)num;
另一種情況大家就比較熟悉了,即精度小的轉為精度大的發生自動類型轉換,反之就是強制類型轉換,如下圖所示:
什么是自動裝箱和自動拆箱
將基本數據類型賦值給包裝類就是裝箱,如下所示:
Integer i = 10; //裝箱
這一點,我們可以通過查看字節碼得以印證,可以看到底層就是通過valueOf實現裝箱。
public static main([Ljava/lang/String;)V
L0
LINENUMBER 5 L0
BIPUSH 10
//valueOf 完成類型裝箱
INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer;
ASTORE 1
L1
LINENUMBER 6 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
LOCALVARIABLE i Ljava/lang/Integer; L1 L2 1
MAXSTACK = 1
MAXLOCALS = 2
}
Integer i = 10;
int num = i; //拆箱
查看字節碼,可以看到底層是通過Integer.intValue實現拆箱:
// access flags 0x9
public static main([Ljava/lang/String;)V
L1
LINENUMBER 6 L1
ALOAD 1
INVOKEVIRTUAL java/lang/Integer.intValue ()I
ISTORE 2
L2
LINENUMBER 7 L2
RETURN
L3
LOCALVARIABLE args [Ljava/lang/String; L0 L3 0
LOCALVARIABLE i Ljava/lang/Integer; L1 L3 1
LOCALVARIABLE num I L2 L3 2
MAXSTACK = 1
MAXLOCALS = 3
}
&和&&的區別是什么
前者進行邏輯與時不會因為左邊的false發生短路即表達式左右兩邊都會執行,而后者會發生短路,即左邊得到false就不執行右邊的邏輯:
具體我們可以查看下面這個例子,&運算不會發生短路,所以第一個func()結果返回false,第二個func()還是會被執行。
public static void main(String[] args) {
boolean b1 = func() & func();
}
private static boolean func() {
System.out.println("調用了func");
return false;
}
從輸出結果我們可以得以印證:
調用了func
調用了func
同理我們再看看&&運算,因為第一次返回了false,所以&&運算符后面的func()就不會被執行。
public static void main(String[] args) {
boolean b1 = func() && func();
}
private static boolean func() {
System.out.println("調用了func");
return false;
}
輸出結果如下:
調用了func
switch 是否能作用在 byte/long/String 上?
到JDK7開始byte、String都可以都可以用在switch上,但是long還是不行的。
break、continue、return的區別
- break:結束當前循環體
- continue:跳過本次循環
- return 結束當前循環直接返回結果
最有效的計算2乘8是什么方式?
用左移運算符即 2<<3 ,這種寫法在JDK的數據結構中經常可以見到,例如Arrays中的binarySearch0二分搜索,它獲取中間索引時除2就是用右移運算符號。
private static int binarySearch0(long[] a, int fromIndex, int toIndex,
long key) {
int low = fromIndex;
int high = toIndex - 1;
while (low <= high) {
//右移運算實現除2操作
int mid = (low + high) >>> 1;
long midVal = a[mid];
if (midVal < key)
low = mid + 1;
else if (midVal > key)
high = mid - 1;
else
return mid; // key found
}
return -(low + 1); // key not found.
}
為什么不能用浮點數表示金額?
浮點數只有8個字節,作為高精度計算的金融結算場景可能存在偏差,一般情況下針對高精度的計算,我們更推薦使用BigDecimal,因為其底層針對精度和數值進行相應的處理不會造成丟失。
BigDecimal用equals進行比較可以嗎?為什么?
BigDecimal重寫的equals方法比較的是兩個值的精度的大小是否一致:
@Override
public boolean equals(Object x) {
if (!(x instanceof BigDecimal))
return false;
BigDecimal xDec = (BigDecimal) x;
if (x == this)
return true;
//先比較精度
if (scale != xDec.scale)
return false;
//再比較數值
long s = this.intCompact;
long xs = xDec.intCompact;
if (s != INFLATED) {
if (xs == INFLATED)
xs = compactValFor(xDec.intVal);
return xs == s;
} else if (xs != INFLATED)
return xs == compactValFor(this.intVal);
return this.inflated().equals(xDec.inflated());
假設我們BigDecimal聲明的分別是1.0和1.00,最終的結果就可能返回false,所以一般情況下,BigDecimal進行數值比較時我們建議使用compareTo
BigDecimal(double)和BigDecimal(String)有什么區別?
因為double只有8個字節,對于高精度的小數可能存在進度丟失問題,對應String類型因為字符串字面量的關系所以對應的小數就可以通過BigDecimal完成正確的轉換。
為什么對Java中的負數取絕對值結果不一定是正數
我們都知道integer是4個字節,即取值范圍是-2的31次方(-2147483648),2的31次方減一(2147483647),當-2147483648取絕對值時就會得到2147483648超過integer最大值導致越界得到負數。
Lambda表達式是如何實現的
本質上Lambda就是語法糖,編譯階段編譯器會將lambda表達式進行解糖轉換成對應的方法進行解析。
public static void main(String[] args) {
List<String> list = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
.filter(i -> i % 2 == 0)
.map(String::valueOf)
.collect(Collectors.toList());
System.out.println(list);
}
對應的我們給出反編譯后的結果,可以看到這些表達式最后都會轉為Predicate.test、Function.apply等調用:
83: aastore
84: invokestatic #4 // InterfaceMethod java/util/stream/Stream.of:([Ljava/lang/Object;)Ljava/util/stream/Stream;
87: invokedynamic #5, 0 // InvokeDynamic #0:test:()Ljava/util/function/Predicate;
92: invokeinterface #6, 2 // InterfaceMethod java/util/stream/Stream.filter:(Ljava/util/function/Predicate;)Ljava/util/stream/Stream;
97: invokedynamic #7, 0 // InvokeDynamic #1:apply:()Ljava/util/function/Function;
102: invokeinterface #8, 2 // InterfaceMethod java/util/stream/Stream.map:(Ljava/util/function/Function;)Ljava/util/stream/Stream;
107: invokestatic #9 // Method java/util/stream/Collectors.toList:()Ljava/util/stream/Collector;
110: invokeinterface #10, 2 // InterfaceMethod java/util/stream/Stream.collect:(Ljava/util/stream/Collector;)Ljava/lang/Object;
115: checkcast #11 // class java/util/List
finally中代碼一定會執行嗎
不會,原因如下:
- 在執行finally前調用System.exit(0);。
- try-catch塊中存在死循環。
- 虛擬機崩潰或者執行過程中服務器宕機。
- finally代碼塊由守護線程執行,主線程執行完時,守護線程可能會直接推出,不執行finally。
你覺得Java中的枚舉有什么用處?
- 語義化好,天生自帶屬性,拓展強。
- 天生就是個單例,可以直接保證線程安全,便于使用單例模式。
什么是AIO、BIO和NIO
- BIO:同步阻塞IO,線程發起請求后一直阻塞直到讀或者寫結束,傳統的socket編程就是BIO適用于那些連接數較少的服務端。
- NIO:同步非阻塞,線程發起IO請求后不阻塞立即返回,常用于解決c10k問題,這其中Netty就是最經典的NIO框架。
- AIO:異步非阻塞,線程發起IO請求后不阻塞返回,IO操作有結果后會回調通知。
UUID是什么?可以保證全局唯一?
能,UUID即全局唯一標識,是指在一臺機器上生成的id編碼,由于UUID依賴MAC地址、時間戳、隨機數等信息,所以UUID具有極高的唯一性,幾乎是不可能重復的。
Arrays.sort是使用什么排序算法實現的
查看底層源碼可知排序算法使用的是雙路快排法,一種針對數組進行遞歸分而治之再合并的O(nlogN)級別的排序算法:
public static void sort(int[] a) {
DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
}
為什么Java中的main方法必須是public static void修飾
- 設置public讓JVM可調用。
- 聲明狀態避免實例化后調用
- 調用后無需任何返回值,所以返回類型設置為void。
BigDecimal和Long哪個更適合標識金額
BigDecimal無論是使用還是進行高精度計算時都比Long類型準確。
怎么修改一個類中的private修飾的String參數的值
- 反射
- 用set方法
Stream的并行流一定比串行流更快嗎
不一定,針對一些計算密集型任務。它需要時刻活躍于CPU核心才能工作,如果使用并行流以為去提升并發度,這其中上下文切換和結果聚合的開銷導致的最終性能,可能還不如單線程串行運算。
小結
自此我們將java基礎的核心知識進行了簡單的總結,后續筆者才會針對這些題目進行進一步的迭代,希望對你有幫助。