一、多線程相關
1、ThreadLocal
ThreadLocal適用於變量在線程間隔離,而在方法或類間共享的場景
程序運行在Tomcat中,執行程序的線程是Tomcat的工作線程,而Tomcat的工作線程是基於線程池的。線程池會重用固定的幾個線程,所以使用ThreadLocal來存放一些數據時,需要特別注意在代碼運行完後,需要在代碼的finally代碼塊中,顯式清除ThreadLocal中的數據
2、ConcurrentHashMap
ConcurrentHashMap只能保證提供的原子性讀寫操作是線程安全的
- 使用了ConcurrentHashMap,不代表對它的多個操作之間的狀態是一致的,如果需要確保需要手動加鎖
- 諸如
size()
、isEmpty()
和containsValue()
等聚合方法,在併發情況下可能會反映ConcurrentHashMap的中間狀態。因此在併發情況下,這些方法的返回值只能用作參考,而不能用於流程控制 - 諸如
putAll()
這樣的聚合方法也不能確保原子性,在putAll()
的過程中去獲取數據可能會獲取到部分數據
3、CopyOnWriteArrayList
CopyOnWriteArrayList雖然是一個線程安全的ArrayList,但因爲其實現方式是,每次修改數據時都會複製一份數據出來,適用於讀多寫少或者說希望無鎖讀的場景。如果讀寫比例均衡或者有大量寫操作的話,使用CopyOnWriteArrayList的性能會非常糟糕
二、Spring事務
1、@Transactional生效策略
- 除非特殊配置(比如使用AspectJ靜態織入實現AOP),否則只有定義在public方法上的
@Transactional
才能生效。原因是,Spring默認通過動態代理的方式實現AOP,對目標方法進行增強,private方法無法代理到,Spring自然也無法動態增強事務處理邏輯 - 必須通過代理過的類從外部調用目標方法才能生效
2、事務回滾
- 只有異常傳播出了標記了
@Transactional
註解的方法,事務才能回滾 - 默認情況下,出現
RuntimeException
或Error
的時候,Spring纔會回滾事務
三、判等問題
- 對基本類型,比如int、long,進行判等,只能使用==,比較的是直接值。因爲基本類型的值就是其數值
- 對引用類型,比如Integer、Long和String,進行判等,需要使用
equals()
進行內容判等。因爲引用類型的直接值是指針,使用==的話,比較的是指針,也就是兩個對象在內存中的地址,即比較它們是不是同一個對象,而不是比較對象的內容
比較值的內容,除了基本類型只能使用==外,其他類型都需要使用equals()
1、Integer與int
//案例一
Integer a = 127; //Integer.valueOf(127)
Integer b = 127; //Integer.valueOf(127)
System.out.println("\nInteger a = 127;\n" + "Integer b = 127;\n" + "a == b ? " + (a == b)); //true
//案例二
Integer c = 128; //Integer.valueOf(128)
Integer d = 128; //Integer.valueOf(128)
System.out.println("\nInteger c = 128;\n" + "Integer d = 128;\n" + "c == d ? " + (c == d)); //false
//案例三
Integer e = 127; //Integer.valueOf(127)
Integer f = new Integer(127); //new instance
System.out.println("\nInteger e = 127;\n" + "Integer f = new Integer(127);\n" + "e == f ? " + (e == f)); //false
//案例四
Integer g = new Integer(127); //new instance
Integer h = new Integer(127); //new instance
System.out.println("\nInteger g = new Integer(127);\n" + "Integer h = new Integer(127);\n" + "g == h ? " + (g == h)); //false
//案例五
Integer i = 128; //unbox
int j = 128;
System.out.println("\nInteger i = 128;\n" + "int j = 128;\n" + "i == j ? " + (i == j)); //true
案例一,編譯器會把Integer a = 127
轉換爲Integer.valueOf(127)
,轉換在內部其實做了緩存,使得兩個Integer指向同一個對象,所以==
返回true,默認會緩存[-128, 127]
的數值,所以案例二==
返回false
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
案例三和案例四中,new出來的Integer始終是不走緩存的新對象。比較兩個新對象,或者比較一個新對象和一個來自緩存的對象,結果肯定不是相同的對象,因此返回false
案例五中,把裝箱的Integer和基本類型int比較,前者會先拆箱再比較,比較的肯定是數值而不是引用,因此返回true
2、String
String a = "1";
String b = "1";
System.out.println("\nString a = \"1\";\n" + "String b = \"1\";\n" + "a == b ? " + (a == b)); //true
String c = new String("2");
String d = new String("2");
System.out.println("\nString c = new String(\"2\");\n" + "String d = new String(\"2\");\n" + "c == d ? " + (c == d)); //false
String e = new String("3").intern();
String f = new String("3").intern();
System.out.println("\nString e = new String(\"3\").intern();\n" + "String f = new String(\"3\").intern();\n" + "e == f ? " + (e == f)); //true
String g = new String("4");
String h = new String("4");
System.out.println("\nString g = new String(\"4\");\n" + "String h = new String(\"4\");\n" + "g == h ? " + g.equals(h)); //true
Java的字符串常量池機制設計初衷是節省內存。當代碼中出現雙引號形式創建字符串對象時,JVM會先對這個字符串進行檢查,如果字符串常量池中存在相同內容的字符串對象的引用,則將這個引用返回;否則,創建新的字符串對象,然後將這個引用放入字符串常量池,並返回該引用。這種機制,就是字符串駐留或池化
案例一返回 true,因爲Java的字符串駐留機制,直接使用雙引號聲明出來的兩個String對象指向常量池中的相同字符串
案例二,new出來的兩個String是不同對象,引用當然不同,所以得到false的結果
案例三,使用String提供的intern()
方法也會走常量池機制,所以同樣能得到true
案例四,通過equals()
對值內容判等,是正確的處理方式,當然會得到true
雖然使用new聲明的字符串調用intern()
方法,也可以讓字符串進行駐留,但在業務代碼中濫用intern()
,可能會產生性能問題
3、實現equals方法
對於自定義類型,如果不重寫equals()
的話,默認就是使用Object基類的按引用的比較方式
String的equals()
的實現:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
重寫equals()
的步驟:
- 考慮到性能,可以先進行指針判等,如果對象是同一個那麼直接返回true
- 需要對另一方進行判空,空對象和自身進行比較,結果一定是fasle
- 需要判斷兩個對象的類型,如果類型都不同,那麼直接返回false
- 確保類型相同的情況下再進行類型強制轉換,然後逐一判斷所有字段
重寫equals方法時總要重寫hashCode
public class Point {
private int x;
private int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Point that = (Point) o;
return x == that.x && y == that.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
}
4、Lombok使用
Lombok的@Data
註解會幫我們實現equals()
和hashcode()
方法
@Data
public class Person {
private String name; //姓名
private String identity; //身份證
public Person(String name, String identity) {
this.name = name;
this.identity = identity;
}
}
對於身份證相同、姓名相同的兩個Person對象:
Person person1 = new Person("xiaoming", "001");
Person person2 = new Person("xiaoming", "001");
System.out.println("person1.equals(person2) ? " + person1.equals(person2)); //true
如果只要身份證一致就認爲是同一個人的話,可以使用@EqualsAndHashCode.Exclude
註解來修飾name字段,從equals()
和hashCode()
的實現中排除name字段:
@Data
public class Person {
@EqualsAndHashCode.Exclude
private String name; //姓名
private String identity; //身份證
public Person(String name, String identity) {
this.name = name;
this.identity = identity;
}
}
Person person1 = new Person("xiaoming", "001");
Person person2 = new Person("xiaohong", "001");
System.out.println("person1.equals(person2) ? " + person1.equals(person2)); //true
Employee類繼承Person,並新定義一個公司屬性
@Data
public class Employee extends Person {
private String company;
public Employee(String name, String identity, String company) {
super(name, identity);
this.company = company;
}
}
聲明兩個Employee實例,它們具有相同的公司名稱,但姓名和身份證均不同,結果返回爲true
Employee employee1 = new Employee("zhuye", "001", "bkjk.com");
Employee employee2 = new Employee("Joseph", "002", "bkjk.com");
System.out.println("employee1.equals(employee2) ? " + employee1.equals(employee2)); //true
@EqualsAndHashCode
默認實現沒有使用父類屬性,可以手動設置callSuper開關爲true
@Data
@EqualsAndHashCode(callSuper = true)
public class Employee extends Person {
四、數值計算
1、BigDecimal使用
小數點的加減乘除都使用BigDecimal來解決,因爲double或者float會丟失精度
- 使用BigDecimal表示和計算浮點數,且務必使用字符串的構造方法來初始化BigDecimal
- 如果一定要用Double來初始化BigDecimal的話,可以使用
BigDecimal.valueOf()
方法
2、丟失精度原因
double a = 0.3;
double b = 0.1;
System.out.println(a - b); //0.19999999999999998
BigDecimal bigDecimal = new BigDecimal(0.3);
System.out.println(bigDecimal); //0.299999999999999988897769753748434595763683319091796875
對於十進制的小數轉換成二進制採用乘2取整法進行計算,取掉整數部分後,剩下的小數繼續乘以2,直到小數部分全爲0
將0.3轉成二進制的過程:
0.3 * 2 = 0.6 => .0 (.6)取0剩0.6
0.6 * 2 = 1.2 => .01 (.2)取1剩0.2
0.2 * 2 = 0.4 => .010 (.4)取0剩0.4
0.4 * 2 = 0.8 => .0100 (.8) 取0剩0.8
0.8 * 2 = 1.6 => .01001 (.6)取1剩0.6
.............
由於double不能精確表示爲0.3,因此用double構造函數傳遞的值不完全等於0.3。使用BigDecimal時,必須使用String字符串參數構造方法來創建它。BigDecimal是不可變的,在進行每一步運算時,都會產生一個新的對象。double的問題是從小數點轉換到二進制丟失精度,二進制丟失精度。而BigDecimal在處理的時候把十進制小數擴大N倍讓它在整數上進行計算,並保留相應的精度信息
3、equals做判等
System.out.println(new BigDecimal("1.0").equals(new BigDecimal("1"))); //false
BigDecimal的equals()
方法比較的是BigDecimal的value和scale,1.0的scale是1,1的scale是0,所以結果是false
如果希望只比較BigDecimal的value,可以使用compareTo()
方法
System.out.println(new BigDecimal("1.0").compareTo(new BigDecimal("1")) == 0); //true
BigDecimal的equals()
和hashCode()
方法會同時考慮value和scale,如果結合HashSet或HashMap使用的話就可能會出現麻煩。比如,把值爲1.0的BigDecimal加入HashSet,然後判斷其是否存在值爲1的BigDecimal,得到的結果是false:
Set<BigDecimal> hashSet1 = new HashSet<>();
hashSet1.add(new BigDecimal("1.0"));
System.out.println(hashSet1.contains(new BigDecimal("1"))); //false
解決這個問題的辦法有兩個:
1)使用TreeSet替換HashSet。TreeSet不使用hashCode()
方法,也不使用equals()
比較元素,而是使用compareTo()
方法,所以不會有問題
Set<BigDecimal> treeSet = new TreeSet<>();
treeSet.add(new BigDecimal("1.0"));
System.out.println(treeSet.contains(new BigDecimal("1"))); //true
2)把BigDecimal存入HashSet或HashMap前,先使用stripTrailingZeros()
方法去掉尾部的零,比較的時候也去掉尾部的0,確保value相同的BigDecimal,scale也是一致的
Set<BigDecimal> hashSet2 = new HashSet<>();
hashSet2.add(new BigDecimal("1.0").stripTrailingZeros());
System.out.println(hashSet2.contains(new BigDecimal("1.000").stripTrailingZeros())); //true
五、Arrays.asList把數據轉換爲List
1、不能直接使用Arrays.asList來轉換基本類型數組
int[] arr = {1, 2, 3};
List<int[]> list = Arrays.asList(arr);
System.out.println(list.size()); //1
只能是把int裝箱爲Integer,不可能把int數組裝箱爲Integer數組。Arrays.asList()
方法傳入的是一個泛型T類型可變參數,最終int數組整體作爲了一個對象成爲了泛型類型T
2、Arrays.asList返回的List不支持增刪操作
Arrays.asList()
返回的List並不是java.util.ArrayList
,而是Arrays的內部類ArrayList。ArrayList內部類繼承自AbstractList類,並沒有覆寫父類的add()
方法,而父類中add()
方法的實現,就是拋出UnsupportedOperationException
public static <T> List<T> asList(T... a) {
return new ArrayList<>(a);
}
private static class ArrayList<E> extends AbstractList<E>
implements RandomAccess, java.io.Serializable
{
private static final long serialVersionUID = -2764017481108945198L;
private final E[] a;
ArrayList(E[] array) {
a = Objects.requireNonNull(array);
}
@Override
public E get(int index) {
return a[index];
}
@Override
public E set(int index, E element) {
E oldValue = a[index];
a[index] = element;
return oldValue;
}
//...
}
3、對原始數組的修改會影響到通過Arrays.asList獲得的那個List
ArrayList的實現是直接使用了原始的數組。所以,把通過Arrays.asList()
獲得的List交給其他方法處理,很容易因爲共享了數組,相互修改產生Bug
修復方式比較簡單,重新new一個ArrayList初始化Arrays.asList()
返回的List即可
String[] arr = {"1", "2", "3"};
List list = new ArrayList(Arrays.asList(arr));
arr[1] = "4";
list.add("5");