第45条:将局部变量的作用域最小化
- 最有力的方法是在第一次使用局部变量的地方声明它。如果过早的声明局部变量,当要使用时,代码阅读者可能已经忘记了变量的类型和初始值。过早的声明变量,也会扩展它的作用域。
- 几乎每个局部变量的声明都应当包含一个初始化表达式。如果声明的时候不能进行有意义的初始化,就应该推迟这个声明。对此的一个例外是try-catch的语句,如果一个变量在初始化时会抛出异常,应该在try块中初始化。如果还要在try块之外使用,就应该在try块之前声明。
- 循环中的局部变量最小化
/** * 循环中的变量最小化处理 * Created by itlivemore on 17-6-17. */ public class Loop { public static void main(String[] args) { List<Integer> list = new ArrayList<>(); list.add(1); list.add(2); // 使用foreach,tmp的作用域只在下面的循环中 for (Integer tmp : list) { System.out.println(tmp); } // 在jdk1.5之前可以使用下面的做法,iterator的作用域也只在循环中 for (Iterator iterator = list.iterator(); iterator.hasNext(); ) { System.out.println(iterator.next()); } // 如果使用while循环,下面的Iterator变量的作用域不只在循环中 Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } // 这里还可以访问iterator System.out.println(iterator.hasNext()); /* 下面是另一种对局部变量的作用域进行最小化的循环做法 * i和n具有相同的作用域*/ for (int i = 0, n = list.size(); i < n; i++) { System.out.println(i); } } }
- 最后一种“将局部变量的作用域最小化”的方法是小而集中。如果把两个操作合并到同一个方法中,与其中一个操作相关的局部变量就有可能会出现在执行另一个操作的代码范围之内,可以将这个方法分成两个方法,每个方法各执行一个操作。
第46条:for-each循环优先于传统的for循环
- 使用传统for循环嵌套遍历多个集合存在的问题。下面的示例中要遍历得到一副扑克牌。
1 // 扑克牌的花色 2 enum Suit { 3 CLUB, DIAMOND, HEART, SPADE 4 } 5 6 // 扑克牌的名称 7 enum Rank { 8 ACE, DEUCE, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, JACK, QUEEN, KING 9 } 10 11 // 扑克牌 12 class Card { 13 final Suit suit; 14 final Rank rank; 15 16 Card(Suit suit, Rank rank) { 17 this.suit = suit; 18 this.rank = rank; 19 } 20 21 @Override 22 public String toString() { 23 return "Card{" + 24 "suit=" + suit + 25 ", rank=" + rank + 26 '}'; 27 } 28 } 29 30 /** 31 * 对多个集合嵌套迭代 32 * Created by itlivemore on 17-6-17. 33 */ 34 public class NestedIteration { 35 public static void main(String[] args) { 36 // 所有扑克牌花色 37 List<Suit> suits = Arrays.asList(Suit.values()); 38 // 所有扑克牌名称 39 List<Rank> ranks = Arrays.asList(Rank.values()); 40 // 遍历获得一副扑克牌 41 List<Card> deck = null; 42 try { 43 // 传统for循环遍历,有异常 44 getDeck1(suits, ranks); 45 } catch (Exception e) { 46 e.printStackTrace(); 47 } 48 print(deck); 49 50 // 修复传统for循环问题 51 deck = getDeck2(suits, ranks); 52 print(deck); 53 54 // 使用foreach循环 55 System.out.println("=======use foreach======"); 56 deck = getDeck3(suits, ranks); 57 print(deck); 58 59 } 60 61 /*打印元素*/ 62 private static <T> void print(Collection<T> collection) { 63 if (collection == null || collection.isEmpty()) { 64 return; 65 } 66 for (T t : collection) { 67 System.out.println(t); 68 } 69 } 70 71 /*传统for循环遍历,容易犯这样的错误,因为外部的suitIterator只有四个元素,在内部调用了太多次next(), 72 * 所以会报NoSuchElementException异常*/ 73 private static List<Card> getDeck1(List<Suit> suits, List<Rank> ranks) { 74 List<Card> deck = new ArrayList<>(); 75 for (Iterator<Suit> suitIterator = suits.iterator(); suitIterator.hasNext(); ) { 76 for (Iterator<Rank> rankIterator = ranks.iterator(); rankIterator.hasNext(); ) { 77 deck.add(new Card(suitIterator.next(), rankIterator.next())); 78 } 79 } 80 return deck; 81 } 82 83 /*修复传统for循环遍历的问题,在外部循环中增加一个变量保存元素即可*/ 84 private static List<Card> getDeck2(List<Suit> suits, List<Rank> ranks) { 85 List<Card> deck = new ArrayList<>(); 86 for (Iterator<Suit> suitIterator = suits.iterator(); suitIterator.hasNext(); ) { 87 Suit suit = suitIterator.next(); 88 for (Iterator<Rank> rankIterator = ranks.iterator(); rankIterator.hasNext(); ) { 89 deck.add(new Card(suit, rankIterator.next())); 90 } 91 } 92 return deck; 93 } 94 95 /*使用foreach循环*/ 96 private static List<Card> getDeck3(List<Suit> suits, List<Rank> ranks) { 97 List<Card> deck = new ArrayList<>(); 98 for (Suit suit : suits) { 99 for (Rank rank : ranks) { 100 deck.add(new Card(suit, rank)); 101 } 102 } 103 return deck; 104 } 105 }
- 三种常见的情况下无法使用for-each循环
- 过滤:遍历过程中要删除元素,就要显式使用迭代器,以便调用remove方法。
- 转换:遍历过程中需要取代部分或全部的元素值,就需要列表迭代器或者数组索引,以便设定元素的值。
- 平行迭代:如果需要并行地遍历多个集合,就需要显式地控制迭代器或者索引变量,以便所有迭代器或者索引变量都可以得到同步前移。
第47条:了解和使用类库
- 产生位于0和某个上界之间的随机整数.
1 /** 2 * 产生位于0和某个上界之间的随机整数 3 * Created by itlivemore on 17-6-17. 4 */ 5 public class RandomBug { 6 private static final Random rnd = new Random(); 7 8 /*产生位于0和某个上界之间的随机整数,该方法有3个缺陷 9 * 1.如果n是一个比较小的2的乘方,经过一段相当短的周期之后,它产生的随机数序列会重复。 10 * 2.如果n不是2的乘方,平均起来有些数出现得更为频繁。 11 * 3.极少情况下,返回一个不在范围内的数。netxInt()返回Integer.MIN_VALUE,abs()也会返回 12 * Integer.MIN_VALUE,如果n不是2的乘方,取模将返回一个负数 13 * 可以使用Random.nextInt(n)来代替*/ 14 private static int random(int n) { 15 return Math.abs(rnd.nextInt()) % n; 16 } 17 18 public static void main(String[] args) { 19 // 针对缺陷2测试 20 int n = 2 * (Integer.MAX_VALUE / 3); 21 int low = 0; 22 for (int i = 0; i < 1000000; i++) { 23 if (random(n) < n / 2) { 24 low++; 25 } 26 } 27 System.out.println(low); 28 // 打印的结果是667179,说明大约有2/3的数字落在前半部分 29 30 low = 0; 31 for (int i = 0; i < 1000000; i++) { 32 if (rnd.nextInt(n) < n / 2) { 33 low++; 34 } 35 } 36 System.out.println(low); 37 // 打印的结果是499797,差不多正好是1/2 38 39 40 // 针对缺陷3测试 41 n = 1023; 42 Integer abs = Math.abs(Integer.MIN_VALUE); 43 System.out.println(abs.equals(Integer.MIN_VALUE) + " : " + abs.equals(Integer.MAX_VALUE)); 44 // 打印的是true:false,说明Math.abs(Integer.MIN_VALUE)=Integer.MIN_VALUE 45 System.out.println(abs % n); 46 // 打印的结果是-2,不在0-1023范围内 47 48 // 测试Math.abs()取绝对值 49 System.out.println(Math.abs(-123)); 50 // 打印123,说明Math.abs(Integer.MIN_VALUE)=Integer.MIN_VALUE属于特例 51 }
- 使用标准类库的好处:
- 无需关心实现细节。通过使用标准类库,可以充分利用这些编写标准类库的专家的知识,以及在你之前的其他人的使用经验。
- 不必浪费时间为那些与工作不太相关的问题提供特别的解决方案。
- 标准类库的性能会随着时间的推移而不断提高,功能也会随着时间的推移而增加,无需你做任何努力。
- 可以使自己的代码融入主流,这样的代码更易读,更易维护,更易被大多数开发人员重用。
- 每个java程序员应该熟悉的类库:java.lang,java.util,java.io。特别是java.util中的Collections Framework(集合框架),并发实用工具包java.util.concurrent。
第48条:如果需要精确的答案,请避免使用float和double
- float和double是为了在广泛的数值范围上提供较为精确的快速近似计算而精心设计的,它们不能提供完全精确的结果,所以不应该被用于需要精确结果的场合。
- float和double类型尤其不适合用于货币计算,因为不能精确地表示0.1(或者10的任何其他负数次方值)。
1 /** 2 * 求解问题:假设你有1元钱,去买糖果,糖果标价为1角,2角,3角等直到1元。 3 * 你打算从标价为1角的糖果开始,每种买1颗,一直买到不能支付下一个糖果价钱为止, 4 * 那么你可以买几颗糖果?还会找回多少零钱? 5 * Created by itlivemore on 17-6-17. 6 */ 7 public class Arithmetic { 8 9 /*使用double计算,可以买3颗糖果,找零:0.3999...结果不对*/ 10 private static void useDouble() { 11 double funds = 1.00; 12 int itemsBought = 0; 13 for (double price = 0.1; funds >= price; price += 0.1) { 14 funds -= price; 15 itemsBought++; 16 } 17 System.out.println("itemsBought : " + itemsBought); 18 System.out.println("Change : " + funds); 19 } 20 21 /*使用BigDecimal计算,可以买4颗糖果,找零:0*/ 22 private static void useBigDecimal() { 23 // 特别要注意这里new BigDecimal("String")使用的是字符串 24 final BigDecimal TEN_CENTS = new BigDecimal("0.1"); 25 BigDecimal funds = new BigDecimal("1"); 26 int itemsBought = 0; 27 28 for (BigDecimal price = new BigDecimal("0.1"); 29 funds.compareTo(price) >= 0; price = price.add(TEN_CENTS)) { 30 funds = funds.subtract(price); 31 itemsBought++; 32 } 33 System.out.println("itemsBought : " + itemsBought); 34 System.out.println("Change : " + funds); 35 } 36 37 /*也可以使用最小单位为分,用int类型计算*/ 38 private static void useInteger() { 39 int funds = 100; 40 int itemsBought = 0; 41 for (int price = 10; funds >= price; price += 10) { 42 funds -= price; 43 itemsBought++; 44 } 45 System.out.println("itemsBought : " + itemsBought); 46 System.out.println("Change : " + funds); 47 } 48 49 public static void main(String[] args) { 50 useDouble(); 51 useBigDecimal(); 52 useInteger(); 53 } 54 }
- BigDecimal的缺点:代码写起来没有基本运算类型方便,而且慢。好处:可以控制舍入,允许从8种舍入模式中选择。
- 如果数值范围没有超过9位数字,就可以使用int;如果不超过18位数字,就可以使用long。如果数值可能超过18位数字,就必须使用BigDecimal。
第49条:基本类型优先于装箱基本类型
- 基本类型和装箱基本类型之间的区别:
- 基本类型只有值,而装箱基本类型则具有与它们的值不同的同一性。换句话说:两个装箱基本类型可以具有相同的值和不同的同一性(值相同,引用地址不同)。
- 基本类型只有功能完备的值,而每个装箱基本类型除了它对应基本类型的所有功能值之外,还有个非功能值:null。
- 基本类型通常比装箱基本类型更节省时间和空间。
- 使用大于等于时,Integer实例被自动拆箱,提取它们的基本类型值进行比较。对装箱基本类型运用==操作符比较的是地址。
1 /** 2 * 自动拆箱与装箱 3 * Created by itlivemore on 17-6-17. 4 */ 5 public class AutoBoxed { 6 public static void main(String[] args) { 7 Integer a = new Integer(26); 8 Integer b = new Integer(26); 9 System.out.println(compare(a, b)); 10 // 返回的是1 11 12 System.out.println(compare2(a, b)); 13 // 返回的是0 14 } 15 16 private static int compare(Integer first, Integer second) { 17 // 执行first < second会自动拆箱,所以比较26时是false 18 // 执行==是比较地址,所以比较26返回的是1 19 return first < second ? 1 : (first == second ? 0 : 1); 20 } 21 22 private static int compare2(Integer first, Integer second) { 23 int f = first; // 自动拆箱 24 int s = second; // 自动拆箱 25 return f < s ? 1 : (f == s ? 0 : 1); 26 } 27 }
- 当在一项操作中混合使用基本类型和装箱类型时,装箱基本类型就会自动拆箱。
1 /** 2 * 当在一项操作中混合使用基本类型和装箱类型时,装箱基本类型就会自动拆箱。 3 * Created by itlivemore on 17-6-17. 4 */ 5 public class Unbeliveable { 6 private static Integer a; 7 8 private static int b; 9 10 public static void main(String[] args) { 11 if (b == 2) { 12 System.out.println("b == 2"); 13 } else { 14 System.out.println("b = " + b); 15 } 16 // 上面打印的是b = 0 17 System.out.println("=========="); 18 if (a == 2) { // 自动拆箱,a为null,拆箱会有NullPointerException 19 System.out.println("a == 2"); 20 } else { 21 System.out.println("a = " + a); 22 } 23 } 24 }
- 使用装箱基本类型,会使变量反复地装箱与拆箱,导致性能下降。
1 /** 2 * 使用装箱基本类型,会使变量反复地装箱与拆箱,导致性能下降。 3 * Created by itlivemore on 17-6-17. 4 */ 5 public class Sum { 6 public static void main(String[] args) { 7 Long sum = 0L; 8 for (long i = 0; i < Integer.MAX_VALUE; i++) { 9 sum += i; 10 } 11 System.out.println(sum); 12 } 13 }
- 装箱基本类型的合理用处
- 作为集合中的元素、键和值,因为不能将基本类型放在集合中。
- 在泛型中必须使用装箱基本类型作为类型参数。
- 在进行反射的方法调用时,必须使用装箱基本类型。
第50条:如果其他类型更合适,则尽量避免使用字符串
- 不应该使用字符串的情形
- 字符串不适合代替其他的类型。当一段数据从文件、网络等进入程序之后,通常是以字符串的形式存在,原来是什么类型就应该转成什么类型。如原来是Integer类型就应该转成Integer类型。
- 字符串不适合代替枚举类型。
- 字符串不适合代替聚集类型。聚集类型可以理解为一个类,类中有多个属性,将多个属性使用某个特殊字符作为分隔符拼接成一个字符串通常是不恰当的。如果用来分隔域的字符也出现在域中,结果就会出现混乱。为了访问单独的域,必须解析字符串,这个过程非常慢也很繁锁,容易出错。
- 字符串不适合代替能力表。有时候字符串被用于对某种功能进行授权访问。程序根据客户端提供的字符串键来判断权限,因为String变量是可以被共享的,所以恶意的客户端可以使用与另一个个客户端相同的键,以便非法地访问其他客户端的数据。
第51条:当心字符串连接的性能
- 字符串连接操作符"+"是把多个字符串合并为一个字符串的便利途径。由于字符串不可变,为连接n个字符串而重复地使用字符串连接操作符,需要n的平方级时间。
- 连接字符串时使用StringBuilder替代String。
第52条:通过接口引用对象
- 如果有合适的接口类型存在,那么对于参数、返回值、变量和域来说,就都应该使用接口类型进行声明。好处是当要更换实现时,只要改变构造器中类的名称(或者使用一个不同的静态工厂)。
List<Integer> list = new Vector<>(); //如果要将Vector更换为ArrayList<>();只需要修改构造器中类的名称,而其它地方不用修改。 List<Integer> list = new ArrayList<>();
- 为什么要改变实现?因为新的实现提高了性能,增加了新功能。如ThreadLocal内部有一个包级私有的Map域,1.3版本中这个域使用的是HashMap,在1.4版本中使用了新增的类IdentityHashMap。只需要将初始化域的那一行代码修改即可。
-
不存在适当接口的类型的情形:1.如果没有合适的接口存在,完全可以用类而不是接口来引用对象。比如值类(如String),具体类(如Random)。2.对象属于一个框架,而框架的基本类型是类,不是接口。应该使用相关的基类(通常是抽象类)来引用对象。3.类实现了接口,但是它提供了接口中不存在的额外方法——例如LinkedHashMap。如果程序依赖于这些额外的方法,应该引用类。
第53条:接口优先于反射机制
- 反射的缺点:
- 丧失了编译时类型检查的好处。
- 执行反射访问所需要的代码非常笨拙和冗长。
- 性能损失。
- 通常,普通应用程序在运行时不应该以反射方式访问对象。需要用反射的示例:类浏览器,对象监视器,代码分析工具,解释型的内嵌式系统,RPC(远程过程调用)系统。
- 对于有些程序,它们必须用到在编译时无法获取的类,但是在编译时存在适当的接口或者超类,通过它们可以引用这个类。如果是这种情况,就可以以反射方式创建实例,然后通过它们的接口或者超类,以正常的方式访问这些实例。如果适当的构造器不带参数,可以用Class.newInstance创建实例。
- 例:使用Set对参数去重,具体使用哪个类型的Set则由输入参数决定。
1 /** 2 * 使用Set对参数去重,具体使用哪个类型的Set则由输入参数决定 3 * Created by itlivemore on 17-6-18. 4 */ 5 public class ReflectInterface { 6 public static void main(String[] args) { 7 // 第一个参数是要使用的Set的类型,后面是要去重的参数 8 args = new String[]{"java.util.HashSet", "1", "2", "1", "2"}; 9 args = new String[]{"java.util.TreeSet", "1", "2", "1", "2"}; 10 Class<?> clazz = null; 11 try { 12 // 加载类 13 clazz = Class.forName(args[0]); 14 } catch (ClassNotFoundException e) { 15 e.printStackTrace(); 16 System.exit(1); 17 } 18 Set<String> set = null; 19 try { 20 // 创建set实例 21 set = (Set<String>) clazz.newInstance(); 22 } catch (InstantiationException e) { 23 e.printStackTrace(); 24 } catch (IllegalAccessException e) { 25 e.printStackTrace(); 26 } 27 if (set == null) { 28 return; 29 } 30 31 // 使用set对参数去重 32 set.addAll(Arrays.asList(args).subList(1, args.length)); 33 System.out.println(set); 34 } 35 }
- 如果你编写的程序必须要与编译时未知的类一起工作,如有可能,就应该仅仅使用反射机制来实例化对象,而访问对象时则使用编译时已知的某个接口或者超类。
第54条:谨慎地使用本地方法
- 本地方法是指用本地程序设计语言(比如C或者C++)来编写的特殊方法。java通过JNI调用本地方法。本地方法在本地语言中可以执行任意的计算任务,并返回到Java程序设计语言。
- 本地方法的主要用途
- 提供了“访问特定于平台的机制”的能力,如注册表、文件锁。
- 提供了访问遗留代码库的能力,从而可以访问遗留数据。
- 本地方法可以通过本地语言,编写应用程序中注重性能的部分,以提高系统的性能。
- 不推荐使用本地方法的原因:
- Java平台提供了越来越多以前只有在宿主平台上才拥有的特性。如jdk1.4的java.util.prefs包提供了注册表的功能。jdk1.6的java.awt.SystemTray增提供了访问桌面系统托盘区的功能。
- JVM实现变得越来越快,对于大多数任务,现在即使不使用本地方法也可以获得与之相当的性能。使用本地方法来提高性能的做法不值得提倡。
- 本地语言不是安全的,程序不能免受内存毁坏错误的影响。
- 本地语言是与平台相关的,程序不再是可自由移植的。
- 使用本地方法的应用程序更难调试。在进入和退出本地代码时,需要相关的固定开销。
- 需要“胶合代码”的本地方法编写起来单调乏味,并且难以阅读。
第55条:谨慎地进行优化
- 不要因为性能而牺牲合理的结构。要努力编写好的程序而不是坏的程序。如果好的程序不够快,它的结构将使它可以得到优化。实现上的问题可以通过后期的优化而得到修正,而结构几乎是不能被改正的。
- 程序设计优先于性能,努力避免那些限制性能的设计决策。API设计好后难以甚至不可能改变。一般而言,好的API设计也会带来好的性能。
- 使用性能剖析工具来决定优化的重心。每次做优化,要对优化前、优化后做性能测量。
第56条:遵守普遍接受的命名惯例
- 字面命名惯例,涉及到包、类、接口、方法、域和类型变量
- 包的名称应该是层次状的,用句号分隔每个部分。每个部分都包括小写字母和数字(很少使用数字)。任何将在你的组织之外使用的包,其名称都应该以你的组织的Internet域名开头,并且顶级域名放在前面。标准类库和一些可选的包除外,其名称以java和javax开头。包名称的其余部分应该包括一个或者多个描述该包的组成部分。鼓励使用有意义的缩写形式,如util。
- 类和接口的名称,包括枚举和注解类型的名称,都应该包含一个或者多个单词,每个单词的首字母大写,应该尽量避免用缩写,除非是一些首字母缩写和一些通用的缩写。
- 方法和域的名称与类和接口的名称一样,都遵守相同的字面惯例,只不过方法或者域的名称的第一个字母应该小写。
- 常量域的名称应该包含一个或者多个大写的单词,中间用下划线分隔。
- 局部变量的命名惯例与成员名称类似,只不过允许缩写,可以使用单个字符(如i)和短字符。
- 类型参数名称通常由单个字母组成。通常是以下5个:T表示任意的类型,E表示集合的元素类型,K和V表示映射的键和值类型,X表示异常。任何类型的的序列可以是T、U、V或者T1、T2、T3。
- 语法命名惯例
- 类(包括枚举类型)通常用一个名词或者名词短语命名。
- 接口的命名与类相似,或者用一个以"-able"或"ible"结尾的形容词来命名。
- 执行某个动作的方法通常用动词或者动词短语来命名。
- 对于返回boolean值的方法,以单词"is"开头,很少用has,后面跟名词或名词短语,或者任何具有形容词功能的单词或短语。
- 对于返回非boolean值的方法,通常用名词、名词短语或者get开头的动词短语来命名。
- 一些特殊方法的命名
-
- 转换对象类型的方法、返回不同类型的独立对象的方法,通常为toType,例如toString和toArray。
- 返回视图的方法通常为asType,例如asList。
- 返回一个与被调用对象同值的基本类型的方法通常为typeValue,例如intValue。
- 静态工厂的常用名称为valueOf、of、 getInstance、newInstance、getType和NewType。