文章目錄
說說Java中的8大基本類型 & 內存中佔有的字節 & 初始值?
bit(位):表示信息的最小單位,是二進制數的一位包含的信息;
byte(字節):用來計量存儲容量的一種計量單位;
1 byte = 8 bit(1個字節等於8位);
基本類型 | 佔據空間大小 | 取值範圍 | 默認值 |
---|---|---|---|
布爾型——boolean | 不確定 | true/false | false |
字節型——byte | 1個字節 | -128~127 | 0 |
整型——int | 4個字節 | -2^31 ~ 2^31-1(-2147483648~2147483647) | 0 |
短整型——short | 2個字節 | -2^15 ~ 2^15-1(-32768~32767) | 0 |
長整型——long | 8個字節 | -2^63 ~ 2^63-1(-9223372036854775808 ~ 9223372036854775807) | 0 |
字符型——char | 2個字節 | 0~2^16-1(0 ~ 65535無符號) | \u0000 |
單精度浮點型——float | 4個字節 | -2^128 ~ 2^128 | 0.0F |
雙精度浮點型——double | 8個字節 | -2^1024 ~ 2^1024 | 0.0D |
(1)帶符號數(正數/負數)在計算機中存儲方式?
原碼:原碼就是符號位加上真值的絕對值, 即用第一位表示符號, 其餘位表示值。
補碼:正數的補碼就是其本身。負數的補碼是在其原碼的基礎上, 符號位不變, 其餘各位取反, 最後+1. (即在反碼的基礎上+1)
補碼是計算機存儲帶符號數的方式,可以解決0的符號問題以及兩個編碼問題。
爲什麼byte類型(8位二進制表示)的取值範圍爲-128~127?
0用[0000 0000]表示,-128用[1000 0000]表示。byte類型1個字節,8位二進制,範圍爲[1000 0000]~[0111 1111]
(2)浮點數(小數)在計算機中存儲方式?
- 小數(浮點數)的二進制轉換
78.375 的整數部分:
小數部分:
所以,78.375 的二進制形式就是 1001110.011
然後,使用二進制科學記數法,有
注意,轉換後用二進制科學記數法表示的這個數,有底有指數有小數部分,這個就叫做浮點數。 - 浮點數在計算機中存儲
在計算機中,保存這個數使用的是浮點表示法,分爲三大部分:
第一部分用來存儲符號位(sign),用來區分正負,這裏是 0,表示正數
第二部分用來存儲指數(exponent),這裏的指數是十進制的 6
第三部分用來存儲小數(fraction),這裏的小數部分是 001110011
指數位決定了大小範圍,因爲指數位能表示的數越大則能表示的數越大,而小數位決定了計算精度,因爲小數位能表示的數越大,則能計算的精度越大。
- float類型是32位,是單精度浮點表示法:
符號位佔用1位,指數位佔用 8 位,小數位佔用 23 位。
float 的小數位只有 23 位,即二進制的 23 位,能表示的最大的十進制數爲 2 的 23 次方,即 8388608,即十進制的 7 位,嚴格點,精度只能百分百保證十進制的 6 位運算。 - double 類型是 64 位,是雙精度浮點表示法:
符號位佔用 1 位,指數位佔用 11 位,小數位佔用 52 位。
double 的小數位有 52 位,對應十進制最大值爲 4 503 599 627 370 496,這個數有 16 位,所以計算精度只能百分百保證十進制的 15 位運算。
- 指數位的偏移與無符號表示
float 的指數部分是 8 位,則指數的取值範圍是 -126 到 +127,爲了消除負數帶來的實際計算上的影響(比如比較大小,加減法等),可以在實際存儲的時候,需要把指數轉換爲無符號整數,即給指數做一個簡單的映射,加上一個偏移量,比如float的指數偏移量爲 127,這樣就不會有負數出現了。比如:指數如果是 6,則實際存儲的是 6+127=133,即把 133 轉換爲二進制之後再存儲。
對應的 double 類型,存儲的時候指數偏移量是 1023。 - 舉例求78.375浮點數表示
所以用float類型來保存十進制小數78.375的話,需要先轉換成浮點數,得到符號位和指數和小數部分。符號位是0,指數位是6+127=133,二進制表示爲10 000 101,小數部分是001110011,不足部分請自動補0。
連起來用 float 表示,加粗部分是指數位,最左邊是符號位 0,代表正數:
0 10000101 001110011 00000 00000 0000
知道float和double類型爲什麼會出現精度丟失的情況嗎?
(1)浮點型數據精度丟失的原因
將十進制浮點數轉換爲二進制浮點數時,小數的二進制有時也是不可能精確的。
就如同十進制不能準確表示1/3,二進制也無法準確表示1/10,而double類型存儲尾數部分最多隻能存儲52位,於是,計算機在存儲該浮點型數據時,便出現了精度丟失。
例:十進制小數如何轉化爲二進制數
算法是乘以2直到沒有了小數爲止。舉個例子,0.9表示成二進制數
0.9*2=1.8 取整數部分 1
0.8(1.8的小數部分)*2=1.6 取整數部分 1
0.6*2=1.2 取整數部分 1
0.2*2=0.4 取整數部分 0
0.4*2=0.8 取整數部分 0
0.8*2=1.6 取整數部分 1
0.6*2=1.2 取整數部分 0
.........
0.9二進制表示爲(從上往下): 1100100100100......
注意:上面的計算過程循環了,也就是說*2永遠不可能消滅小數部分,這樣算法將無限下去。很顯然,小數的二進制表示有時是不可能精確的 。
因此將11.9化爲二進制後大約是” 1011. 1110011001100110011001100…”。
(2)浮點型數據精度丟失的解決方法
商業運算中應用場景:例如某用戶有10塊錢,買了一件商品花了8.8,理應剩下1.2元。但卻無法繼續購買價格爲1.2元的商品。
double d1 = 10;
double d2 = 8.8;
double c = d1 - d2;
System.out.println("d1 - d2 = "+c);
// 輸出
d1 - d2 = 1.1999999999999993
- 解決方法1
在設計數據庫表的時候可以將price字段類型設置爲int(oracle應設置爲number)類型,而在實體中對應的屬性單位應該表示爲分(即精確到0.00)或者角(即0.0),但一般情況下money會精確到分。
如:商品的價格爲12.53元(精確到分),在數據庫中price字段對應的數據爲應該爲1253。使用這種方法需要編程人員自己在程序中收懂轉換,當然也可以封裝爲一個工具類。 - 解決方法2
使用java提供的BigDecimal類。該類封裝在java.math.BigDecimal中。該類的構造器有很多,但在使用浮點類型計算時一定要使用String構造器來實例BigDecimal對象。
//加法
public static BigDecimal add(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));//這裏使用的是String構造器,將double轉換爲String類型
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.add(b2);
}
//減法
public static BigDecimal sub(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));//同上
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.subtract(b2);//這是b1-b2,可以理解爲從b1截取b2
}
//乘法
public static BigDecimal mul(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));//同上
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.multiply(b2);
}
//除法
public static BigDecimal div(double v1,double v2){
BigDecimal b1 = new BigDecimal(Double.toString(v1));
BigDecimal b2 = new BigDecimal(Double.toString(v2));
return b1.divide(b2,2,BigDecimal.ROUND_HALF_UP); //四捨五入,保留2位小數,除不盡的情況
}
JAVA基本數據類型與封裝類型的區別?
封裝類(如Integer)是基本數據類型(如int)的包裝類。
封裝類(Integer) | 基本類型(int) | |
---|---|---|
存儲數據 | 封裝類本質是對象的引用,需實例化後使用。實際上是生成一個指向該對象的引用(存儲對象地址) | 值(基本數據類型是一個變量,直接存放數值) |
屬性和方法 | 封裝類有屬性和方法,利用這些方法和屬性來處理數據,如Integer.parseInt(Strings) | 基本數據類型都是final修飾的,不能繼承擴展新的類、新的方法 |
默認值 | null | 0 |
存儲位置 | 封裝類的對象引用存儲在棧中,實際的對象存儲在堆中 | 棧 |
使用場景 | 更好地處理數據之間的轉換 | 速度快(不涉及對象的構造與回收) |
什麼是拆箱 & 裝箱,能給我舉栗子嗎?
封裝類(如Integer)是基本數據類型(如int)的包裝類。裝箱就是 自動將基本數據類型轉換爲包裝器類型;拆箱就是 自動將包裝器類型轉換爲基本數據類型。
- 裝箱(基本數據類型->封裝類)
Integer i = 10; // 實際上執行Integer.valueOf(10);
- 拆箱(封裝類->基本數據類型)
Integer i = 10; //裝箱
int t = i; //拆箱,實際上執行了 int t = i.intValue();
能說說多維數組在內存上是怎麼存儲的嗎?
在java中數組也是對象。因此,對象存放在內存中的原理同樣適用於數組。
當創建一個數組時,在堆中會爲數組對象分配一段內存空間,並返回一個引用。數組對象的引用存放在棧中,實際的數組對象存放在堆中。
多維數組在內存中存儲方式:
你對數組二次封裝過嗎?說說封裝了什麼?
使用Java一維數組,仿照ArrayList源碼,封裝相關構造、獲取元素個數、容量大小、判空、增刪查改等功能。
對Java一維數組E[]自定義ArrayList集合
下面是部分實現:
/**
* 通過對數組封裝實現自己的Array類
*/
public class MyArrayList<E> {
private E[] data; // 定義一個整型的一維數組的成員變量
private int size; // 數組中元素個數
// 獲取數組中元素的個數
public int getSize() {
return size;
}
// 判斷數組是否爲空
public boolean isEmpty() {
return size == 0;
}
// 向數組的第index位置插入元素e
public void add(int index, E e) {
if (index < 0 || index > size)
throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size.");
if (size - data.length >= 0) {
int newCapacity = data.length + (data.length >> 1); // 擴容1.5倍
resize(newCapacity);
}
for (int i = size - 1; i >= index; i--)
data[i + 1] = data[i];
data[index] = e;
size++;
}
// 動態數組擴容 newCapacity 擴容長度
private void resize(int newCapactity) {
E[] newData = (E[]) new Object[newCapactity];
for (int i = 0; i < size; i++) {
newData[i] = data[i];
}
data = newData;
newData = null;
}
}
String
原理 & 不可變性
- 內部
在 Java 8 中,String 內部使用 char 數組存儲數據。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
}
在 Java 9 之後,String 類的實現改用 byte 數組存儲字符串,同時使用 coder 來標識使用了哪種編碼。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final byte[] value;
/** The identifier of the encoding used to encode the bytes in {@code value}. */
private final byte coder;
}
- 不可變性
String對象是不可變的,即對象的狀態(成員變量)在對象創建之後不再改變。
(一)不可變性實現
由String內部構造:
(1)String 被聲明爲 final,因此它不可被繼承。(Integer 等包裝類也不能被繼承)
(2)value 數組被聲明爲 final,這意味着 value 數組初始化之後就不能再引用其它數組。
(3)String 內部沒有改變 value 數組的方法。
可知String是不可變的。
補充:不可變的實現:
String類被final修飾,保證類不被繼承。
String內部所有成員都設置爲私有變量,並且用final修飾符修飾,保證成員變量初始化後不被修改。
不提供setter方法改變成員變量,即避免外部通過其他接口修改String的值。
通過構造器初始化所有成員(value[])時,對傳入對象進行深拷貝(deep copy),避免用戶在String類以外通過改變這個對象的引用來改變其內部的值。
在getter方法中,不要直接返回對象引用,而時返回對象的深拷貝,防止對象外泄。
(二)不可變的好處
- 滿足字符串常量池的需要(有助於共享)
可以將字符串對象保存在字符串常量池中以供與字面值相同字符串對象共享。
如果一個 String 對象已經被創建過了,那麼就會從 String Pool 中取得引用。只有 String 是不可變的,纔可能使用 String Pool。
如果String對象是可變的,那就不能這樣共享,因爲一旦對某一個String類型變量引用的對象值改變,將同時改變一起共享字符串對象的其他 String類型變量所引用的對象的值。 - 線程安全考慮
同一個字符串實例可以被多個線程共享。字符串的不變性保證字符串本身便是線程安全的。 - 支持hash映射和緩存
因爲字符串是不可變的,所以在它創建的時候hashcode就被緩存了,不需要重新計算。這就使得String很適合作爲Map中的鍵,字符串的處理速度要快過其它的鍵對象。這就是HashMap中的鍵往往都使用字符串。
缺點:String對象不適用於經常發生修改的場景,會創建大量的String對象。
(三)String 的 “改變”?
public static void main(String[] args) {
String s = "ABCDEF";
System.out.println("s = " + s);
s = "123456";
System.out.println("s = " + s);
}
String的改變實際上是創建了一個新的String對象"123456",並將引用指向了這個新的對象,同時原來的String對象"ABCDEF"並沒有發生改變,仍保存在內存中。
(四)String 的不可變 真的不可變?
通過反射獲取value數組直接改變內存數組中的數據是可以修改所謂的"不可變"對象的。
public static void reflectString() throws Exception{
// 創建字符串"ABCDEF"並賦給引用s
String s = "ABCDEF";
System.out.println("s = " + s); // s = ABCDEF
Field valueField = s.getClass().getDeclaredField("value"); // 獲取String類中value字段
valueField.setAccessible(true); // 改變value屬性的訪問權限
char[] value = (char[]) valueField.get(s); // 獲取s對象上的value屬性的值
value[0] = 'a'; // 改變value所引用的數組中的某個位置字符
value[2] = 'c';
value[4] = 'e';
System.out.println("s = " + s); // s = aBcDeF
}
String && StringBuilder && StringBuffer
- 可變性
String 不可變
StringBuffer 和 StringBuilder 可變 - 線程安全
String 不可變,因此是線程安全的
StringBuilder 不是線程安全的
StringBuffer 是線程安全的,內部使用 synchronized 進行同步
String | StringBuffer | StringBuilder | |
可變性 | String | StringBuffer | StringBuilder |
線程安全 | 安全(不可變) | 安全(Synchronized) | 不安全 |
執行效率 | 高 | 低(Synchronized) | 高 |
適用場景 | 操作少量的數據,不需要頻繁拼接 | 多線程操作大量數據 只有在對線程安全要求高的情況下使用StringBuffer | 單線程操作大量數據 |
備註 | 在字符串修改/拼接時,String是不可變的對象, 因此在每次對String 類型進行改變的時候,都會生成一個新的 String 對象,然後將指針指向新的 String 對象。不僅效率低下,還會大量浪費內存空間。 使用 StringBuffer/StringBuilder 類時,每次都會對 StringBuffer/StringBuilder 對象本身進行修改操作,而不產生新的未使用對象。 |
內存中存儲
對於String,其對象的引用都是存儲在棧中的。
java中對String對象特殊對待,所以在heap區域分成了兩塊,一塊是字符串常量池(String constant pool),用於存儲java字符串常量對象,另一塊用於存儲普通對象及字符串對象。
- "abc"字符串常量/s.intern()——StringPool
編譯期已經創建好(直接用雙引號定義的"abc")的就存儲在字符串常量池中。即jvm會在String constant pool中創建對象。字符串常量池(String Pool)保存着所有字符串字面量(literal strings),這些字面量在編譯時期就確定。不僅如此,還可以使用 String 的 intern() 方法在運行過程中將字符串添加到 String Pool 中。String Pool用於共享字符串字面量,防止產生大量String對象導致OOM。
jvm會首先在String constant pool 中尋找是否已經存在(equals)“abc"常量,如果沒有則創建該常量,並且將此常量的引用返回給String a;如果已有"abc” 常量,則直接返回String constant pool 中“abc” 的引用給String a。
當一個字符串調用 intern() 方法時,如果 String Pool 中已經存在一個字符串和該字符串值相等(使用 equals() 方法進行確定),那麼就會返回 String Pool 中字符串的引用;否則,就會在 String Pool 中添加一個新的字符串,並返回這個新字符串的引用。
equals相等(指向同一引用)的字符串在常量池中永遠只有一份。
intern() 方法返回字符串對象的規範化表示形式,即一個字符串,內容與此字符串相同,但一定取自具有唯一字符串的池。
它遵循以下規則:對於任意兩個字符串 s 和 t,當且僅當 s.equals(t) 爲 true 時,s.intern() == t.intern() 才爲 true。
- new String(“abc”)
運行期(new出來的 new String(s))才能確定的就存儲在堆中。即jvm會直接在heap中非String constant pool 中創建字符串對象,然後把該對象引用返回給String b(並且不會把"abc” 加入到String constant pool中)。
new就是在堆中創建一個新的String對象,不管"abc"在內存中是否存在,都會在堆中開闢新空間。
equals相等的字符串在堆中可能有多份。
對於 new String(“abc”),使用這種方式一共會創建兩個字符串對象(前提是 String Pool 中還沒有 “abc” 字符串對象)。這兩個字符串對象指向同一個value數組。
- “abc” 屬於字符串字面量,因此編譯時期會在 String Pool 中創建一個字符串對象,指向這個 “abc” 字符串字面量;
- 而使用 new 的方式會在堆中創建一個字符串對象。
String s1 = new String("aaa");
String s2 = new String("aaa");
System.out.println(s1 == s2); // false,指向堆內不同引用
String s3 = s1.intern();
String s4 = s1.intern();
System.out.println(s3 == s4); // true,指向字符串常量池中相同引用
String s5 = "bbb";
String s6 = "bbb";
System.out.println(s5 == s6); // true,指向字符串常量池中相同引用
字符串拼接方式 & 比較
- 拼接方式
- "+" 拼接
加號拼接字符串jvm底層其實是調用StringBuilder來實現的,也就是說”a” + “b” + "c"等效於下面的代碼片。
// String d = "a"+"b"+"c";等效於
String d = new StringBuilder().append("a").append("b").append("c").toString();
但並不是說直接用“+”號拼接就可以達到StringBuilder的效率了,因爲每次使用 "+"拼接 都會新建一個StringBuilder對象,並且最後toString()方法還會生成一個String對象。在循環拼接十萬次的時候,就會生成十萬個StringBuilder對象,會產生大量內存消耗。
- concat 拼接
concat其實就是申請一個char類型的buf數組,將需要拼接的字符串都放在這個數組裏,最後再創建並返回一個新的String對象。
public String concat(String str) {
int otherLen = str.length();
if (otherLen == 0) {
return this;
}
int len = value.length;
char buf[] = Arrays.copyOf(value, len + otherLen);
str.getChars(buf, len);
return new String(buf, true);
}
- StringBuilder/StringBuffer append
這兩個類實現append的方法都是調用父類AbstractStringBuilder的append方法,只不過StringBuffer是的append方法加了sychronized關鍵字,因此是線程安全的。append代碼如下,他主要也是利用char數組保存字符,通過ensureCapacityInternal方法來保證數組容量可用還有擴容。
public AbstractStringBuilder append(String str) {
if (str == null)
return appendNull();
int len = str.length();
ensureCapacityInternal(count + len);
str.getChars(0, len, value, count);
count += len;
return this;
}
他擴容的方法的代碼如下,可見,當容量不夠的時候,數組容量右移1位(也就是翻倍)再加2。
private int newCapacity(int minCapacity) {
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0)
? hugeCapacity(minCapacity)
: newCapacity;
}
- 拼接比較
拼接方式 | + | concat | StringBuilder/StringBuffer |
---|---|---|---|
原理 | jvm採用append優化,每次執行都會新建一個StringBuilder和String對象 | 申請一個char類型的buf數組,將需要拼接的字符串都放在這個數組裏,最後再創建並返回一個新的String對象 | 利用char數組保存字符,對Stringbuilder/StringBuffer直接修改,不生成新的String對象 |
比較 | 最慢且效率最低,適用於書寫方便場景 | 適用於少量字符串拼接(會新建String對象) | 適用於多個字符串拼接,當不考慮線程的情況下,StringBuilder效率比StringBuffer(Synchronized)高 |
String a = “a”+“b”+“c”;在內存中創建了幾個對象?
- String a=“a”+“b”+"c"在內存中創建幾個對象?——1個對象
String a = “a”+“b”+"c"經過編譯器優化後得到的效果爲String a = “abc”
java編譯期會進行常量摺疊,全字面量字符串相加是可以摺疊爲一個字面常量,而且是進入常量池的。
在JAVA虛擬機(JVM)中存在着一個字符串池,其中保存着很多String對象,並且可以被共享使用,因此它提高了效率。由於String類是final的,它的值一經創建就不可改變,因此我們不用擔心String對象共享而帶來程序的混亂。字符串池由String類維護,我們可以調用intern()方法來訪問字符串池。
對於String a=“abc”;,這行代碼被執行的時候,JAVA虛擬機首先在字符串池中查找是否已經存在了值爲"abc"的這麼一個對象,它的判斷依據是String類equals(Object obj)方法的返回值。如果有,則不再創建新的對象,直接返回已存在對象的引用;如果沒有,則先創建這個對象,然後把它加入到字符串池中,再將它的引用返回。
字符串內部拼接:只有使用引號包含文本的方式創建的String對象之間使用“+”連接產生的新對象纔會被加入字符串池中。對於所有包含new方式新建對象(包括null)的“+”連接表達式,它所產生的新對象都不會被加入字符串池中, - String s=new String(“abc”)創建了幾個對象?——2個對象
new String(“abc”)可看成"abc"(創建String對象)和new String(String original)(String構造器,創建String對象)2個對象。
我們正是使用new調用了String類的上面那個構造器方法創建了一個對象,並將它的引用賦值給了str變量。同時我們注意到,被調用的構造器方法接受的參數也是一個String對象,這個對象正是"abc"。