最近細讀了秦小波老師的《編寫高質量程式碼改善Jaav程式的151個建議》,要說是151個建議,其實更合適的說是避免Java的一些冷門的坑,下面整理了20個比較有趣的建議重新學習了一遍。
三元操作符的型別務必一致
三元操作符運算也稱為三目運算,其表示式形如:"條件表示式 ? 表示式1 : 表示式2",在大部分語言中都有這樣的三元操作符,其目的就是為了簡化if-else,當條件表示式為真時執行表示式1,否則執行表示式2。 來分析一下下面這段程式碼:
public static void main(String[] args){
int i = 80;
String s = String.valueOf(i < 100 ? 80 : 100);
String s1 = String.valueOf(i < 100 ? 80 : 100.0);
boolean equals = s.equals(s1);
// 兩者是否相等:false, 字串s的值:80, 字串s1的值:80.0
System.out.println("兩者是否相等:" + equals);
System.out.println("字串s的值:" + s);
System.out.println("字串s1的值:" + s1);
}
說明:如果三目運算子的型別不一致,返回的結果也不一致。
避免帶有變長引數的方法過載
public class Client {
private static final Logger log = LoggerFactory.getLogger(Client.class);
public static void main(String[] args) {
Client client = new Client();
client.calPrice(5000, 80);
}
/**
* calPrice 簡單折扣計算
*
* @param price 價格
* @param discount 折扣
* @description
* @author luokangyuan
* @date 2019-4-2 14:58
* @version 1.0.0
*/
private void calPrice(int price, int discount) {
float knockdownPrice = price * discount / 100.0F;
log.info("簡單的折扣後的價格:{}", knockdownPrice);
}
/**
* calPrice
*
* @param price 價格
* @param discounts 折扣
* @description 複雜折扣計算
* @author luokangyuan
* @date 2019-4-2 15:08
* @version 1.0.0
*/
private void calPrice(int price, int... discounts) {
float knockdownPrice = price;
for (int discount : discounts) {
knockdownPrice = knockdownPrice * discount / 100;
}
log.info("複雜折扣後的價格:{}", knockdownPrice);
}
}
說明:方法過載就是方法名相同,引數型別或者引數數量不同,在上述例子中Java
編輯器會根據方法簽名找到合適的合適的方法,上述測試呼叫的就是簡單的折扣計算,而非複雜折扣計算。
不要只替換一個類
public class Constant{
public final static int MAX_AGE = 150;
}
public class Client{
public static void main(String[] args){
System.out.println("人類壽命極限是:" + Constant.MAX_AGE);
}
}
對於final修飾的基本型別和String型別,編譯器會認為它是穩定態的(Immutable Status)所以在編譯時就直接把值編譯到位元組碼中了,避免了在執行期引用(Run-time Reference),以提高程式碼的執行效率。對於我們的例子來說,Client類在編譯時位元組碼中就寫上了"150",這個常量,而不是一個地址引用,因此無論你後續怎麼修改常量類,只要不重新編譯Client類,輸出還是照舊。
對於final修飾的類(即非基本型別),編譯器會認為它不是穩定態的(Mutable Status),編譯時建立的則是引用關係(該型別也叫作Soft Final)。如果Client類引入的常量是一個類或例項,及時不重新編譯也會輸出最新值。
用偶判斷,不用奇判斷
String s = n % 2 == 1 ? "奇數" : "偶數";
String s1 = n % 2 == 0 ? "偶數" : "奇數";
說明:通常使用第二種偶數判斷,使用第一種的話。-1
也會被判斷為偶數。
用整數型別處理貨幣
// 0.40000000000000036
System.out.println(10.00 - 9.60);
說明:Java中的浮點數是不準確的,在處理貨幣上使用浮點數就會存在問題,因此使用BigDecimal
,類來進行計算,或者使用整數,將需要計算的數放大100倍,計算後在縮小。
1、使用BigDecimal
BigDecimal
是專門為彌補浮點數無法精確計算的缺憾而設計的類,並且它本身也提供了加減乘除的常用數學演算法。特別是與資料庫Decimal型別的欄位對映時,BigDecimal是最優的解決方案。
2、使用整型
把參與運算的值擴大100倍,並轉為整型,然後在展現時再縮小100倍,這樣處理的好處是計算簡單,準確,一般在非金融行業(如零售行業)應用較多。此方法還會用於某些零售POS機,他們輸入和輸出的全部是整數,那運算就更簡單。
使用String直接賦值
public static void main(String[] args) {
String str1 = "China";
String str2 = "China";
String str3 = new String("China");
String str4 = str3.intern();
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // false
System.out.println(str1 == str4); // true
}
說明:建議使用String str1 = "China";
這中方式對字串賦值,而不是通過new String("China");
這種方式,在Java中會給定義的常量存放在一個常量池中,如果池中存在就不會在重複定義,所以str1 == str2
返回true
。new
出的是一個物件,不會去檢查字串常量池是否存在,所以str1 == str3
是不同的引用,返回false
。經過intern()
處理後,返回true
,是因為intern()
會去物件常量池中檢查是否存在字面上相同得引用物件。
asList產生的list不可修改
private static void arraysTest() {
String[] week = {"Mon", "Tue", "Wed", "Thu"};
List<String> strings = Arrays.asList(week);
strings.add("Fri");
}
說明:執行報錯,asList產生的list不可修改。
別讓null值和空值威脅到變長方法
public void countSum(String type, Integer... is){}
public void countSum(String type, String... strs){}
public static void main(String[] args) {
ClientReload clientReload = new ClientReload();
clientReload.countSum("China", 0);
clientReload.countSum("China", "People");
// 編譯報錯
clientReload.countSum("China");
// 編譯報錯
clientReload.countSum("China", null);
}
說明:同樣是含有變長引數的過載方法,外部呼叫的使用使用NULL
或者空值都會出現編譯不通過的錯誤,這是應為NULL
和空值在上述過載的方法中都滿足引數條件,所以編譯器不知道調什麼方法,在過載方法的設計中違反了KISS
原則,同時在外部呼叫的時候,外部呼叫這隱藏了實參的型別,如將呼叫程式碼做如下修改,就不存在編譯報錯了。
String strs = null;
clientReload.countSum("China", strs);
警惕自增的陷阱
public static void main(String[] args) {
int count = 0;
for (int i = 0; i < 100; i++) {
int i1 = count++;
count = i1;
System.out.println("每次count++的值:" + i1);
}
System.out.println(count);
}
說明:結果是0
,而不是我們100
,這是count++
是一個表示式,返回的是自加之前count
的值。
break不可忘記
public static void main(String[] args) {
String s = toChineseNumber(2);
log.info("轉換結果:{}", s);
}
private static String toChineseNumber(int n) {
String chineseNumber = "";
switch (n) {
case 0 : chineseNumber = "零";
case 1 : chineseNumber = "壹";
case 2 : chineseNumber = "貳";
case 3 : chineseNumber = "叄";
case 4 : chineseNumber = "肆";
case 5 : chineseNumber = "伍";
case 6 : chineseNumber = "陸";
case 7 : chineseNumber = "柒";
case 8 : chineseNumber = "捌";
case 9 : chineseNumber = "玖";
case 10 : chineseNumber = "拾";
}
return chineseNumber;
}
說明:在switch
中break
一定不能少。
不要讓型別悄悄轉換
/** 光速*/
private static final int LIGHT_SPEED = 30 * 10000 * 1000;
public static void main(String[] args) {
long dis = LIGHT_SPEED * 60 * 8;
// -2028888064
System.out.println(dis);
}
說明:LIGHT_SPEED * 60 * 8
計算後是int
型別,可能存在越界問題,雖然,我們在程式碼中寫了轉換為Long
型,但是,在Java中是先運算後在進行型別轉換的,也就是LIGHT_SPEED * 60 * 8
計算後是int
型,超出了長度,從頭開始,所以為負值,修改為顯示的定義型別。如下:
/** 光速*/
private static final long LIGHT_SPEED = 30L * 10000 * 1000;
public static void main(String[] args) {
long dis = LIGHT_SPEED * 60 * 8;
System.out.println(dis);
}
避免帶有變長引數的方法過載
public class MainTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
System.out.println(PriceTool.calPrice(12, 1)); // 1
}
}
class PriceTool {
public static int calPrice(int price, int discount) {
return 1;
}
public static int calPrice(int price, int... discount) {
return 2;
}
}
說明:編譯器會從最簡單的開始猜想,只要符合編譯條件的即採用。
少用靜態匯入
import static java.lang.Math.PI;
public double calCircleArea(double r) {
return Math.PI * r * r;
}
public double calBallArea (double r) {
return 4 * PI * r * r;
}
說明:靜態匯入可以減少程式碼的量,但不易於閱讀,可讀性差。
提防包裝型別的null值
public static int f(List<Integer> list){
int count = 0;
for (Integer i : list){
count += (i != null) ? i : 0;
}
return count;
}
說明:包裝物件和拆箱物件可以自由轉換,這不假,但是要剔除null值,null值並不能轉換為基本型別。對於此問題,我們謹記一點:包裝型別參與運算時,要做null值校驗。
謹慎包裝型別的大小比較
舉個例子。i==j false
。Integer是引用型別。
public static void main(String[] args){
Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.println(i == j);
}
避免instanceof非預期結果
instanceof是一個簡單的二元操作符,它是用來判斷一個物件是否是一個類例項的,兩側操作符需要有繼承或實現關係。
public static void main(String[] args) {
// String物件是否是Object的例項 - true,"String"是要給字串,字串繼承了Object,當然是Object的例項。
boolean b1 = "String" instanceof Object;
// String物件是否是String類的例項 - true,一個類的物件當然是一個類的例項。
boolean b2 = new String() instanceof String;
// Object物件是否是String的例項,編譯報錯,Object是父類。
boolean b3 = new Object() instanceof String;
// 拆箱型別是否是裝箱型別的例項,編譯報錯,“A”是一個Char型,是一個基本型別,instanceof只能用於物件判斷。
boolean b4 = "A" instanceof Character;
// 空物件是否是String的例項 - false,instanceof的規則,左邊為null,無論右邊什麼型別,都返回false。
boolean b5 = null instanceof String;
// 型別轉換後的空物件是否是String的例項 - false,null可以說是沒有型別,型別轉換後還是null。
boolean b6 = (String) null instanceof String;
// Date物件是否是String的例項,編譯報錯,Date類和String類沒有繼承關係
boolean b7 = new Date() instanceof String;
}
不要隨便設定隨機數種子
在Java中,隨機數的產生取決於種子,隨機數和種子之間的關係遵從以下兩個原則: 種子不同,產生不同的隨機數 ;種子相同,即使例項不同也產生相同的隨機數。
public static void main(String[] args)
{
Random r = new Random();
for (int i = 1; i < 4; i++)
{
System.out.println("第" + i + "次:" + r.nextInt());
}
}
執行結果:
第1次:846339168
第2次:756051503
第3次:1696875906
程式啟動後,生成的隨機數會不同。但是每次啟動程式,生成的都會是三個隨機數。產生隨機數和種子之間的關係如下:
1)種子不同,產生不同的隨機數。
2)種子相同,即使例項不同也產生相同的隨機數。
Random的預設種子(無參構造)是System.nanoTime()
的返回值(jdk1.5以前是System.currentTimeMillis()),這個值是距離某一個固定時間點的納秒數,不同的作業系統和硬體有不同的固定時間點,隨機數自然也就不同了。
避免在建構函式中初始化其它類
public class Client35 {
public static void main(String[] args) {
Son son = new Son();
son.doSomething();
}
}
// 父類
class Father {
public Father() {
new Other();
}
}
// 相關類
class Other {
public Other() {
new Son();
}
}
// 子類
class Son extends Father {
public void doSomething() {
System.out.println("Hi, show me Something!");
}
}
說明:造成構造方法迴圈呼叫。
優先使用整型池
Integer快取了-128-127的Integer物件。所以通過裝箱(Integer.valueOf())獲得的物件可以複用。
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 int getSumForArray(int[] array) {
int sum = 0;
for (int i = 0; i < array.length; i++) {
sum += array[i];
}
return sum;
}
private static int getSumForList(List<Integer> list) {
return list.stream().mapToInt(Integer::intValue).sum();
}