書讀的多而不思考,你會以爲本身知道的不少。java
書讀的多而思考,你會以爲本身不懂的愈來愈多。 -----江疏影程序員
在面向對象編程(Object-Oriented Programming, OOP)的世界裏,類和對象是真實世界的描述工具,方法是行爲和動做的展現形式,封裝、繼承、多態則是其多姿多彩的主要實現方式,本章主要講述關於Java對象,方法的種種規則,限制和建議。算法
建議31:在接口中不要存在實現代碼編程
建議32:靜態變量必定要先聲明後賦值session
建議33:不要重寫靜態方法多線程
實例對象訪問靜態方法或靜態屬性不是好習慣,直接類名調用就好了。模塊化
建議34:構造函數儘可能簡化函數
建議35:避免在構造函數中初始化其餘類工具
建議36:使用構造函代碼塊精簡程序性能
一、代碼塊基本概念:
什麼叫作代碼塊(Code Block)?用大括號把多行代碼封裝在一塊兒,造成一個獨立的數據體,實現特定算法的代碼集合即爲代碼塊,通常來講代碼快不能單獨運行的,必需要有運行主體。在Java中一共有四種類型的代碼塊:
普通代碼塊:就是在方法後面使用"{}"括起來的代碼片斷,它不能單獨運行,必須經過方法名調用執行;
靜態代碼塊:在類中使用static修飾,並用"{}"括起來的代碼片斷,用於靜態變量初始化或對象建立前的環境初始化。
同步代碼塊:使用synchronized關鍵字修飾,並使用"{}"括起來的代碼片斷,它表示同一時間只能有一個線程進入到該方法塊中,是一種多線程保護機制。
構造代碼塊:在類中沒有任何前綴和後綴,並使用"{}"括起來的代碼片斷;
二、代碼實例展現:
package OSChina.Client; public class Base { static { System.out.println("我是父類靜態代碼塊1"); } { System.out.println("我是父類構造代碼塊2"); } Base() { System.out.println("我是父類無參構造3"); } public static void doSomething(){ System.out.println("我是父類靜態函數"); } }
package OSChina.Client; public class Sub extends Base{ static { System.out.println("我是子類靜態代碼塊1"); } { System.out.println("我是子類構造代碼塊2"); } Sub(){ System.out.println("我是子類無參構造3"); } }
當實例化時:
Base base = new Sub();
當實例化時,執行順序:靜態代碼塊>>構造代碼塊>>構造函數>>普通方法。
當類名調用靜態方法,不實例化時:
只執行靜態代碼塊和對應的靜態函數,構造函數不執行!
三、構造代碼塊應用場景:
① 初始化實例變量
若是每一個構造函數都要初始化變量,能夠經過構造代碼塊來實現。
注:不是每一個構造函數都要加載的,而構造代碼塊必定加載。
② 初始化實例變量
一個對象必須在適當的場景下才能存在,若是沒有適當的場景,就須要在建立該對象的時候建立場景。例如HTTP request必須首先創建HTTP session,此時就能夠經過構造代碼塊來檢查HTTP session是否已經存在,不存在則建立。
注:構造函數要實現複雜邏輯的時候,能夠經過編寫多個構造代碼塊來實現,每一個代碼塊完成不一樣的業務邏輯(構造函數儘可能簡單的原則),按照業務順序依次存放,這樣在建立實例對象時JVM會按照順序依次執行,實現複雜對象的模塊化建立。
建議37:構造代碼塊會想你所想
建議38:使用靜態內部類提升封裝性
一、Java中的嵌套類分爲兩種:靜態內部類和內部類。
public class Person { // 姓名 private String name; // 家庭 private Home home; public Person(String _name) { name = _name; } /* home、name的setter和getter方法略 */ public static class Home { // 家庭地址 private String address; // 家庭電話 private String tel; public Home(String _address, String _tel) { address = _address; tel = _tel; } /* address、tel的setter和getter方法略 */ } }
其中,Person類中定義了一個靜態內部類Home,它表示的意思是「人的家庭信息」,因爲Home類封裝了家庭信息,不用在Person類中定義homeAddr,homeTel等屬性,這就提升了封裝性,可讀性也提升了。
public static void main(String[] args) { // 定義張三這我的 Person p = new Person("張三"); // 設置張三的家庭信息 p.setHome(new Home("北京", "010")); }
二、靜態內部類的優點:
提升封裝性
提升代碼的可讀性
形似內部,神似外部
靜態內部類雖然存在於外部類中,並且編譯後的類文件也包含外部類(格式是:外部類+$+內部類),可是它能夠擺脫外部類存在,也就是說能夠經過new Home()聲明一個home對象,只是須要導入Person.Home而已。
三、靜態內部類和普通內部類的區別:
① 靜態內部類不持有外部類的引用:
普通內部類能夠訪問外部類的全部屬性和方法。
靜態內部類只能訪問外部類的靜態屬性和靜態方法。
② 靜態內部類不依賴外部類:
普通內部類與外部類同生共死,一塊兒被垃圾回收。
靜態內部類能夠獨立存在。
③ 普通內部類不能聲明static的方法和變量,final static修飾的常量除外。
建議39:使用匿名類的構造函數?
建議40:匿名類的構造函數很特殊?
建議39和建議40暫時沒看出有啥用,再說吧!若有須要請閱讀原著《編寫高質量代碼:改善Java程序的151個建議》!
建議41:讓多繼承成爲現實
在Java中一個類能夠多重實現,但不能多重繼承,也就是說一個類可以同時實現多個接口,但不能同時繼承多個類。
Java中提供的內部類能夠曲折的解決此問題。
建議42:讓工具類不可實例化
建議43:避免對象的淺拷貝
咱們知道一個類實現了cloneable接口就表示它具有了被拷貝的能力,若是重寫clone()方法就會徹底具有拷貝能力。拷貝是在內存中進行的,因此性能方面比直接new生成的對象要快不少,特別在大對象的生成上,性能提高很是顯著。可是淺拷貝存在對象屬性拷貝不完全的問題。
淺拷貝:
一、概念
建立一個新對象,而後將當前對象的非靜態字段複製到新對象,若是是值類型,對該字段進行復制;若是是引用類型,複製引用但不復制引用的對象。所以,原始對象及其副本引用同一個對象。
二、實現方法
調用對象的 clone 方法,必需要讓類實現 Cloneable 接口,而且覆寫 clone 方法。
建議44:推薦使用序列化對象的拷貝
深拷貝:
一、概念
建立一個新對象,而後將當前對象的非靜態字段複製到該新對象,不管該字段是值類型的仍是引用類型,都複製獨立的一份。當你修改其中一個對象的任何內容時,都不會影響另外一個對象的內容。
二、實現方法
① 讓每一個引用類型屬性內部都重寫clone()方法
② 利用序列化
序列化是將對象寫到流中便於傳輸,而反序列化則是把對象從流中讀取出來。這裏寫到流中的對象是原始對象的一個拷貝,由於原始對象還存在在JVM中,因此咱們能夠利用對象的序列化產生克隆對象,而後經過反序列化獲取這個對象。
注意每一個須要序列化的類都要實現serializable接口,若是有某個屬性不須要序列化,能夠將其聲明爲transient,即將其排除在克隆屬性以外。
三、利用序列化完成深拷貝的代碼實例
import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Serializable; public final class CloneUtils { private CloneUtils() { throw new Error(CloneUtils.class + " cannot instance "); } // 拷貝一個對象 public static <T extends Serializable> T clone(T obj) { // 拷貝產生的對象 T cloneObj = null; try { // 讀取對象字節數據 ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(cloneObj); oos.close(); // 分配內存空間,寫入原始對象,生成新對象 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); // 返回新對象, 並作類型轉換 cloneObj = (T) ois.readObject(); ois.close(); } catch (ClassNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } return cloneObj; } }
此工具類要求被拷貝的對象實現serializable接口。
注:Apache下的Commons工具包中SerializationUtils類,直接使用更加簡潔。
建議45:重寫equals方法時不要識別不出本身
重寫的equals方法作了多個校驗,考慮到Web上傳遞過來的對象有可能輸入了先後空格,因此用trim方法剪切了一下。
建議46:equals應該考慮null值情景
建議47:在equals中使用getClass進行類型判斷
建議48:重寫equals方法必須重寫hashcode方法
爲啥?官方解釋沒看懂。
建議49:推薦重寫toString方法
Java提供的默認toString方法不友好,打印出來看不懂,不重寫不行!
建議50:使用package-info類爲包服務
package-info簡稱包文檔,爲包級文檔和包級別註釋提供一個地方,並且該文件惟一必須包含的是包的聲明。
一、package-info的建立
二、package-info類不能繼承,沒有接口,沒有類間關係等。
但package-info有啥用呢?只是對這個包的註釋說明?
① 聲明友好類和包內訪問常量:
這個比較簡單,並且很實用,好比一個包中有不少內部訪問的類或常量,就能夠統一放到package-info類中,這樣很方便,便於集中管理,能夠減小友好類處處遊走的狀況,代碼以下:
class PkgClazz { public void test() { } } class PkgConstant { static final String PACKAGE_CONST = "ABC"; }
注意以上代碼是放在package-info.java中的,雖然它沒有編寫package-info的實現,可是package-info.class類文件仍是會生成。經過這樣的定義,咱們把一個包須要的常量和類都放置在本包下,在語義上和習慣上都能讓程序員更適應。
② 爲在包上提供註解提供便利:
好比咱們要寫一個註解(Annotation),查一下包下的對象,只要把註解標註到package-info文件中便可,並且不少開源項目中也採用了此方法,好比Struts2的@namespace、hibernate的@filterDef等。
③ 提供包的總體註釋說明:
若是是分包開發,也就是說一個包實現了一個業務邏輯或功能點或模塊或組件,則該包須要一個很好的說明文檔,說明這個包是作什麼用的,版本變遷歷史,與其餘包的邏輯關係等,package-info文件的做用在此就發揮出來了,這些均可以直接定義到此文件中,經過javadoc生成文檔時,會吧這些說明做爲包文檔的首頁,讓讀者更容易對該包有一個總體的認識。
建議51:不要主動進行垃圾回收
System.gc要中止全部的響應,才能檢查內存中是否存在能夠回收的對象,這對一個應用系統來講風險極大。