<rt id="bn8ez"></rt>
<label id="bn8ez"></label>

  • <span id="bn8ez"></span>

    <label id="bn8ez"><meter id="bn8ez"></meter></label>

    精彩的人生

    好好工作,好好生活

    BlogJava 首頁 新隨筆 聯系 聚合 管理
      147 Posts :: 0 Stories :: 250 Comments :: 0 Trackbacks

    #

    作者:cleverpig





    版權聲明:本文可以自由轉載,轉載時請務必以超鏈接形式標明文章原始出處和作者信息及本聲明
    作者:cleverpig(作者的Blog:http://blog.matrix.org.cn/page/cleverpig)
    原文:http://www.matrix.org.cn/resource/article/44/44055_Java+Annotation+Reflect.html
    關鍵字:java,annotation,reflect

    前言:
    在上篇文章《Java Annotation入門》中概要性的介紹了Annotation的定義、使用,范圍涵蓋較廣,但是深度不夠。所以作者在《Java Annotation入門》后,繼續整理了Annotation的概念和知識點,與喜歡research的朋友們共享。

    閱讀提示:文中提到的程序成員或者程序元素是一個概念,指組成程序代碼的單元:如類、方法、成員變量。

    一、Annotation究竟是什么?

    Annotation提供了一條與程序元素關聯任何信息或者任何元數據(metadata)的途徑。從某些方面看,annotation就像修飾符一樣被使用,并應用于包、類型、構造方法、方法、成員變量、參數、本地變量的聲明中。這些信息被存儲在annotation的“name=value”結構對中。annotation類型是一種接口,能夠通過java反射API的方式提供對其信息的訪問。

    annotation能被用來為某個程序元素(類、方法、成員變量等)關聯任何的信息。需要注意的是,這里存在著一個基本的潛規則:annotaion不能影響程序代碼的執行,無論增加、刪除annotation,代碼都始終如一的執行。另外,盡管一些annotation通過java的反射api方法在運行時被訪問,而java語言解釋器在工作時忽略了這些annotation。正是由于java虛擬機忽略了annotation,導致了annotation類型在代碼中是“不起作用”的;只有通過某種配套的工具才會對annotation類型中的信息進行訪問和處理。本文中將涵蓋標準的annotation和meta-annotation類型,陪伴這些annotation類型的工具是java編譯器(當然要以某種特殊的方式處理它們)。

    由于上述原因,annotation在使用時十分簡便。一個本地變量可以被一個以NonNull命名的annotation類型所標注,來作為對這個本地變量不能被賦予null值的斷言。而我們可以編寫與之配套的一個annotation代碼分析工具,使用它來對具有前面變量的代碼進行解析,并且嘗試驗證這個斷言。當然這些代碼并不必自己編寫。在JDK安裝后,在JDK/bin目錄中可以找到名為“apt”的工具,它提供了處理annotation的框架:它啟動后掃描源代碼中的annotation,并調用我們定義好的annotation處理器完成我們所要完成的工作(比如驗證前面例子中的斷言)。說到這里,annotation的強大功能似乎可以替代XDoclet這類的工具了,隨著我們的深入,大家會更加堅信這一點。
    注:詳細描述請參看jsr250規范:
    http://www.jcp.org/aboutJava/communityprocess/pfd/jsr250/

    二、Annotation的定義:

    這段文字開始介紹annotation相關技術。在此大家將看到java5.0的標準annotation類型,這種標準類型就是前文中所說的“內建”類型,它們可以直接被javac支持。可喜的是,在java6.0beta版中的javac已經加入了對自定義annotation的支持。

    1。Annotation的概念和語法:

    首先,關鍵的概念是理解annotation是與一個程序元素相關聯信息或者元數據的標注。它從不影響java程序的執行,但是對例如編譯器警告或者像文檔生成器等輔助工具產生影響。

    下面是常用的annotation列表,我們應該注意在annotation和annotation類型之間的不同:

    A.annotation:
    annotation使用了在java5.0所帶來的新語法,它的行為十分類似public、final這樣的修飾符。每個annotation具有一個名字和成員個數>=0。每個annotation的成員具有被稱為name=value對的名字和值(就像javabean一樣),name=value裝載了annotation的信息。

    B.annotation類型:
    annotation類型定義了annotation的名字、類型、成員默認值。一個annotation類型可以說是一個特殊的java接口,它的成員變量是受限制的,而聲明annotation類型時需要使用新語法。當我們通過java反射api訪問annotation時,返回值將是一個實現了該annotation類型接口的對象,通過訪問這個對象我們能方便的訪問到其annotation成員。后面的章節將提到在java5.0的java.lang包里包含的3個標準annotation類型。

    C.annotation成員:
    annotation的成員在annotation類型中以無參數的方法的形式被聲明。其方法名和返回值定義了該成員的名字和類型。在此有一個特定的默認語法:允許聲明任何annotation成員的默認值:一個annotation可以將name=value對作為沒有定義默認值的annotation成員的值,當然也可以使用name=value對來覆蓋其它成員默認值。這一點有些近似類的繼承特性,父類的構造函數可以作為子類的默認構造函數,但是也可以被子類覆蓋。

    D.marker annotation類型:
    一個沒有成員定義的annotation類型被稱為marker annotation。這種annotation類型僅使用自身的存在與否來為我們提供信息。如后面要說的Override。

    E.meta-annotation:
    meta-annotation也稱為元annotation,它是被用來聲明annotation類型的annotation。Java5.0提供了一些標準的元-annotation類型。下面介紹的target、retention就是meta-annotation。

    F.target:
    annotation的target是一個被標注的程序元素。target說明了annotation所修飾的對象范圍:annotation可被用于packages、types(類、接口、枚舉、annotation類型)、類型成員(方法、構造方法、成員變量、枚舉值)、方法參數和本地變量(如循環變量、catch參數)。在annotation類型的聲明中使用了target可更加明晰其修飾的目標。

    G.retention:
    annotation的retention定義了該annotation被保留的時間長短:某些annotation僅出現在源代碼中,而被編譯器丟棄;而另一些卻被編譯在class文件中;編譯在class文件中的annotation可能會被虛擬機忽略,而另一些在class被裝載時將被讀取(請注意并不影響class的執行,因為annotation與class在使用上是被分離的)。使用這個meta-annotation可以對annotation的“生命周期”限制。

    H.metadata:
    由于metadata被廣泛使用于各種計算機開發過程中,所以當我們在這里談論的metadata即元數據通常指被annotation裝載的信息或者annotation本身。

    2。使用標準Annotation:
    java5.0在java.lang包中定義了3種標準的annotation類型:

    A.Override:
    java.lang.Override是一個marker annotation類型,它被用作標注方法。它說明了被標注的方法重載了父類的方法,起到了斷言的作用。如果我們使用了這種annotation在一個沒有覆蓋父類方法的方法時,java編譯器將以一個編譯錯誤來警示。
    這個annotaton常常在我們試圖覆蓋父類方法而確又寫錯了方法名時發揮威力。

    使用方法極其簡單:在使用此annotation時只要在被修飾的方法前面加上@Override。
    下面的代碼是一個使用@Override修飾一個企圖重載父類的toString方法,而又存在拼寫錯誤的sample:
    清單1:

    @Override
    public String toSting() {?? // 注意方法名拼寫錯了
    ????return "[" + super.toString() + "]";
    }


    B.Deprecated:
    同樣Deprecated也是一個marker annotation。當一個類型或者類型成員使用@Deprecated修飾的話,編譯器將不鼓勵使用這個被標注的程序元素。而且這種修飾具有一定的“延續性”:如果我們在代碼中通過繼承或者覆蓋的方式使用了這個過時的類型或者成員,雖然繼承或者覆蓋后的類型或者成員并不是被聲明為@Deprecated,但編譯器仍然要報警。
    值得注意,@Deprecated這個annotation類型和javadoc中的@deprecated這個tag是有區別的:前者是java編譯器識別的,而后者是被javadoc工具所識別用來生成文檔(包含程序成員為什么已經過時、它應當如何被禁止或者替代的描述)。
    在java5.0,java編譯器仍然象其從前版本那樣尋找@deprecated這個javadoc tag,并使用它們產生警告信息。但是這種狀況將在后續版本中改變,我們應在現在就開始使用@Deprecated來修飾過時的方法而不是@deprecated javadoc tag。
    清單2:

    下面是一段使用@Deprecated的代碼:
    /**
    * 這里是javadoc的@deprecated聲明.
    * @deprecated No one has players for this format any more.??Use VHS instead.
    */
    @Deprecated public class Betamax { ... }


    C.SuppressWarnings:
    @SuppressWarnings被用于有選擇的關閉編譯器對類、方法、成員變量、變量初始化的警告。在java5.0,sun提供的javac編譯器為我們提供了-Xlint選項來使編譯器對合法的程序代碼提出警告,此種警告從某種程度上代表了程序錯誤。例如當我們使用一個generic collection類而又沒有提供它的類型時,編譯器將提示出"unchecked warning"的警告。

    通常當這種情況發生時,我們就需要查找引起警告的代碼。如果它真的表示錯誤,我們就需要糾正它。例如如果警告信息表明我們代碼中的switch語句沒有覆蓋所有可能的case,那么我們就應增加一個默認的case來避免這種警告。
    相仿,有時我們無法避免這種警告,例如,我們使用必須和非generic的舊代碼交互的generic collection類時,我們不能避免這個unchecked warning。此時@SuppressWarning就要派上用場了,在調用的方法前增加@SuppressWarnings修飾,告訴編譯器停止對此方法的警告。
    SuppressWarning不是一個marker annotation。它有一個類型為String[]的成員,這個成員的值為被禁止的警告名。對于javac編譯器來講,被-Xlint選項有效的警告名也同樣對@SuppressWarings有效,同時編譯器忽略掉無法識別的警告名。

    annotation語法允許在annotation名后跟括號,括號中是使用逗號分割的name=value對用于為annotation的成員賦值:
    清單3:

    @SuppressWarnings(value={"unchecked","fallthrough"})
    public void lintTrap() { /* sloppy method body omitted */ }


    在這個例子中SuppressWarnings annotation類型只定義了一個單一的成員,所以只有一個簡單的value={...}作為name=value對。又由于成員值是一個數組,故使用大括號來聲明數組值。

    注意:我們可以在下面的情況中縮寫annotation:當annotation只有單一成員,并成員命名為"value="。這時可以省去"value="。比如將上面的SuppressWarnings annotation進行縮寫:
    清單4:

    @SuppressWarnings({"unchecked","fallthrough"})

    如果SuppressWarnings所聲明的被禁止警告個數為一個時,可以省去大括號:

    @SuppressWarnings("unchecked")


    3。Annotation語法:

    在上一個章節中,我們看到書寫marker annotation和單一成員annotation的語法。下面本人來介紹一下完整的語法:

    annotation由“@+annotation類型名稱+(..逗號分割的name-value對...)”組成。其中成員可以按照任何的順序。如果annotation類型定義了某個成員的默認值,則這個成員可以被省略。成員值必須為編譯時常量、內嵌的annotation或者數組。

    下面我們將定義一個annotation類型名為Reviews,它有一個由@Review annotation數組構成的成員。這個@Review annotation類型有三個成員:"reviewer"是一個字符串,"comment" 是一個具有默認值的可選的字符串,"grade"是一個Review.Grade枚舉類型值。
    清單5:

    @Reviews({??// Single-value annotation, so "value=" is omitted here
    ????@Review(grade=Review.Grade.EXCELLENT,
    ????????????reviewer="df"),
    ????@Review(grade=Review.Grade.UNSATISFACTORY,
    ????????????reviewer="eg",
    ????????????comment="This method needs an @Override annotation")
    })

    annotation語法的另一個重要規則是沒有程序成員可以有多于一個的同一annotation實例。例如在一個類中簡單的放置多個@Review annotation。這也是在上面代碼中定義@Reviews annotation類型數組的原因。

    4。Annotation成員類型和值:

    annotation成員必須是非空的編譯時常量表達式。可用的成員類型為:primitive類型、, String, Class, enumerated類型, annotation類型, 和前面類型的數組。

    下面我們定義了一個名為UncheckedExceptions 的annotation類型,它的成員是一個擴展了RuntimeException類的類數組。
    清單6:

    @UncheckedExceptions({
    ????IllegalArgumentException.class, StringIndexOutOfBoundsException.class
    })


    5。Annotation的目標:

    annotation通常被放在類型定義和成員定義的前面。然而它也出現在package、方法參數、本地變量的前面。下面,我們來討論一下這些不大常用的寫法:

    package annotation出現在package聲明的前面。
    下面的例子package-info.java中不包含任何的公共類型定義,卻包含一個可選的javadoc注釋。
    清單7:

    /**
    * This package holds my custom annotation types.
    */
    @com.davidflanagan.annotations.Author("David Flanagan")
    package com.davidflanagan.annotations;

    當package-info.java文件被編譯時,它將產生名為包含annotation(特殊的接口)聲明的package-info.class的類。這個接口沒有成員,它的名字package-info不是一個合法的java標識,所以它不能用在java源代碼中。這個接口的存在只是簡單的被看作一個為package annotation準備的占位符。

    用于修飾方法參數、catch參數、本地變量的annotation只是簡單的出現在這些程序成員的修飾符位置。java類文件格式沒有為本地變量或者catch參數存儲annotation作準備,所以這些annotation總是保留在源代碼級別(source retention);方法參數annotation能夠保存在類文件中,也可以在保留到運行時。

    最后,請注意,枚舉類型定義中不允許任何的修飾符修飾其枚舉值。

    6。Annotation和默認值:
    在Annotation中,沒有默認值的成員必須有一個成員值。而如何理解默認值是如何被處理就是一個很重要的細節:annotation類型所定義的成員默認值被存儲在class文件中,不被編譯到annotation里面。如果我們修改一個annotation類型使其成員的默認值發生了改變,這個改變對于所有此類型的annotation中沒有明確提供成員值的成員產生影響(即修改了該成員的成員值)。即使在annotation類型使其成員的默認值被改變后annotation從沒被重新編譯過,該類型的annotation(改變前已經被編譯的)也受到影響。

    三、Annotation工作原理:

    Annotation與反射
    在java5.0中Java.lang.reflect提供的反射API被擴充了讀取運行時annotation的能力。讓我們回顧一下前面所講的:一個annotation類型被定義為runtime retention后,它才是在運行時可見,當class文件被裝載時被保存在class文件中的annotation才會被虛擬機讀取。那么reflect是如何幫助我們訪問class中的annotation呢?

    下文將在java.lang.reflect用于annotation的新特性,其中java.lang.reflect.AnnotatedElement是重要的接口,它代表了提供查詢annotation能力的程序成員。這個接口被java.lang.Package、java.lang.Class實現,并間接地被Method類、Constructor類、java.lang.reflect的Field類實現。而annotation中的方法參數可以通過Method類、Constructor類的getParameterAnnotations()方法獲得。

    下面的代碼使用了AnnotatedElement類的isAnnotationPresent()方法判斷某個方法是否具有@Unstable annotation,從而斷言此方法是否穩定:
    清單8:

    import java.lang.reflect.*;

    Class c = WhizzBangClass.class;??????????????????????????
    Method m = c.getMethod("whizzy", int.class, int.class);??
    boolean unstable = m.isAnnotationPresent(Unstable.class);

    isAnnotationPresent()方法對于檢查marker annotation是十分有用的,因為marker annotation沒有成員變量,所以我們只要知道class的方法是否使用了annotation修飾就可以了。而當處理具有成員的annotation時,我們通過使用getAnnotation()方法來獲得annotation的成員信息(成員名稱、成員值)。這里我們看到了一套優美的java annotation系統:如果annotation存在,那么實現了相應的annotation類型接口的對象將被getAnnotation()方法返回,接著調用定義在annotation類型中的成員方法可以方便地獲得任何成員值。

    回想一下,前面介紹的@Reviews annotation,如果這個annotation類型被聲明為runtime retention的話,我們通過下面的代碼來訪問@Reviews annotation的成員值:
    清單9:

    AnnotatedElement target = WhizzBangClass.class; //獲得被查詢的AnnotatedElement
    // 查詢AnnotatedElement的@Reviews annotation信息
    Reviews annotation = target.getAnnotation(Reviews.class);
    // 因為@Reviews annotation類型的成員為@Review annotation類型的數組,
    // 所以下面聲明了Review[] reviews保存@Reviews annotation類型的value成員值。
    Review[] reviews = annotation.value();
    // 查詢每個@Review annotation的成員信息
    for(Review r : reviews) {
    ????Review.Grade grade = r.grade();
    ????String reviewer = r.reviewer();
    ????String comment = r.comment();
    ????System.out.printf("%s assigned a grade of %s and comment '%s'%n",
    ??????????????????????reviewer, grade, comment);
    }


    四、如何自定義Annotation?

    1.詳解annotation與接口的異同:
    因為annotation類型是一個非凡的接口,所以兩者之間存在著某些差異:

    A.Annotation類型使用關鍵字@interface而不是interface。
    這個關鍵字聲明隱含了一個信息:它是繼承了java.lang.annotation.Annotation接口,并非聲明了一個interface。

    B.Annotation類型、方法定義是獨特的、受限制的。
    Annotation類型的方法必須聲明為無參數、無異常拋出的。這些方法定義了annotation的成員:方法名成為了成員名,而方法返回值成為了成員的類型。而方法返回值類型必須為primitive類型、Class類型、枚舉類型、annotation類型或者由前面類型之一作為元素的一維數組。方法的后面可以使用default和一個默認數值來聲明成員的默認值,null不能作為成員默認值,這與我們在非annotation類型中定義方法有很大不同。
    Annotation類型和它的方法不能使用annotation類型的參數、成員不能是generic。只有返回值類型是Class的方法可以在annotation類型中使用generic,因為此方法能夠用類轉換將各種類型轉換為Class。

    C.Annotation類型又與接口有著近似之處。
    它們可以定義常量、靜態成員類型(比如枚舉類型定義)。Annotation類型也可以如接口一般被實現或者繼承。

    2.實例:
    下面,我們將看到如何定義annotation類型的example。它展示了annotation類型聲明以及@interface與interface之間的不同:
    清單10:

    package com.davidflanagan.annotations;
    import java.lang.annotation.*;

    /**
    * 使用annotation來描述那些被標注的成員是不穩定的,需要更改
    */
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Unstable {}


    下面的另一個example只定義了一個成員。并通過將這個成員命名為value,使我們可以方便的使用這種annotation的快捷聲明方式:
    清單11:

    /**
    * 使用Author這個annotation定義在程序中指出代碼的作者
    */
    public @interface Author {
    ????/** 返回作者名 */
    ????String value();
    }


    以下的example更加復雜。Reviews annotation類型只有一個成員,但是這個成員的類型是復雜的:由Review annotation組成的數組。Review annotation類型有3個成員:枚舉類型成員grade、表示Review名稱的字符串類型成員Reviewer、具有默認值的字符串類型成員Comment。
    清單12:

    import java.lang.annotation.*;
    ????????
    /**
    * Reviews annotation類型只有一個成員,
    * 但是這個成員的類型是復雜的:由Review annotation組成的數組
    */
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Reviews {
    ????Review[] value();
    }

    /**
    * Review annotation類型有3個成員:
    * 枚舉類型成員grade、
    ??* 表示Review名稱的字符串類型成員Reviewer、
    ??* 具有默認值的字符串類型成員Comment。
    */
    public @interface Review {
    ????// 內嵌的枚舉類型
    ????public static enum Grade { EXCELLENT, SATISFACTORY, UNSATISFACTORY };

    ????// 下面的方法定義了annotation的成員
    ????Grade grade();????????????????
    ????String reviewer();??????????
    ????String comment() default "";??
    }


    最后,我們來定義一個annotation方法用于羅列出類運行中所有的unchecked異常(上文已經提到這種情況不一定是錯誤)。這個annotation類型將一個數組作為了唯一的成員。數組中的每個元素都是異常類。為了加強對未檢查的異常(此類異常都是在運行時拋出)進行報告,我們可以在代碼中對異常的類型進行限制:
    清單13:

    public @interface UncheckedExceptions {
    ????Class<? extends RuntimeException>[] value();
    }


    五、Meta-Annotation

    Annotation類型可以被它們自己所標注。Java5.0定義了4個標準的meta-annotation類型,它們被用來提供對其它annotation類型作說明。這些類型和它們所支持的類在java.lang.annotation包中可以找到。如果需要更詳細的信息可以參考jdk5.0手冊。

    1.再談Target
    作為meta-annotation類型的Target,它描述了annotation所修飾的程序成員的類型。當一個annotation類型沒有Target時,它將被作為普通的annotation看待。當將它修飾一個特定的程序成員時,它將發揮其應用的作用,例如:Override用于修飾方法時,增加了@Target這個meta-annotation就使編譯器對annotation作檢查,從而去掉修飾錯誤類型的Override。

    Target meta-annotation類型有唯一的value作為成員。這個成員的類型是java.lang.annotation.ElementType[]類型的,ElementType類型是可以被標注的程序成員的枚舉類型。

    2.Retention的用法
    我們在文章的開頭曾經提到過Retention,但是沒有詳細講解。Retention描述了annotation是否被編譯器丟棄或者保留在class文件;如果保留在class文件中,是否在class文件被裝載時被虛擬機讀取。默認情況下,annotation被保存在class文件中,但在運行時并不能被反射訪問。Retention具有三個取值:source、class、runtime,這些取值來自java.lang.annotation.RetentionPolicy的枚舉類型值。

    Retention meta-annotation類型有唯一的value作為成員,它的取值來自java.lang.annotation.RetentionPolicy的枚舉類型值。

    3.Documented
    Documented是一個meta-annotation類型,用于描述其它類型的annotation應該被作為被標注的程序成員的公共API,因此可以被例如javadoc此類的工具文檔化。

    Documented是一個marker annotation,沒有成員。

    4.Inherited
    @Inherited meta-annotation也是一個marker annotation,它闡述了某個被標注的類型是被繼承的。如果一個使用了@Inherited修飾的annotation類型被用于一個class,則這個annotation將被用于該class的子類。

    注意:@Inherited annotation類型是被標注過的class的子類所繼承。類并不從它所實現的接口繼承annotation,方法并不從它所重載的方法繼承annotation。

    值得思考的是,當@Inherited annotation類型標注的annotation的Retention是RetentionPolicy.RUNTIME,則反射API增強了這種繼承性。如果我們使用java.lang.reflect去查詢一個@Inherited annotation類型的annotation時,反射代碼檢查將展開工作:檢查class和其父類,直到發現指定的annotation類型被發現,或者到達類繼承結構的頂層。

    六、總結:

    本文幾乎覆蓋了所有的Annotation的概念和知識點,從annotation的定義、語法到工作原理、如何自定義annotation,直至meta-annotation。其中也具有一些配套的代碼片斷可參考,雖然不是很多,但是可謂言簡意賅、著其重點,本人認為用好annotation的關鍵還在于使用。希望本手冊能夠幫助大家用好annotation,這也是本人的最大快樂。

    posted @ 2006-04-03 13:33 hopeshared 閱讀(2534) | 評論 (0)編輯 收藏

    版權聲明:本文可以自由轉載,轉載時請務必以超鏈接形式標明文章原始出處和作者信息及本聲明
    作者:cleverpig(作者的Blog:
    http://blog.matrix.org.cn/page/cleverpig
    )
    原文:[http://www.matrix.org.cn/resourc ... ava+Annotation.html]http://www.matrix.org.cn/resource/article/44/44048_Java+Annotation.html[/url]
    關鍵字:Java,annotation,標注
    摘要:
    本文針對java初學者或者annotation初次使用者全面地說明了annotation的使用方法、定義方式、分類。初學者可以通過以上的說明制作簡單的annotation程序,但是對于一些高級的annotation應用(例如使用自定義annotation生成javabean映射xml文件)還需要進一步的研究和探討。涉及到深入annotation的內容,作者將在后文《Java Annotation高級應用》中談到。
    同時,annotation運行存在兩種方式:運行時、編譯時。上文中討論的都是在運行時的annotation應用,但在編譯時的annotation應用還沒有涉及,
    一、為什么使用Annotation:
    在JAVA應用中,我們常遇到一些需要使用模版代碼。例如,為了編寫一個JAX-RPC web service,我們必須提供一對接口和實現作為模版代碼。如果使用annotation對遠程訪問的方法代碼進行修飾的話,這個模版就能夠使用工具自動生成。
    另外,一些API需要使用與程序代碼同時維護的附屬文件。例如,JavaBeans需要一個BeanInfo Class與一個Bean同時使用/維護,而EJB則同樣需要一個部署描述符。此時在程序中使用annotation來維護這些附屬文件的信息將十分便利而且減少了錯誤。
    二、Annotation工作方式:
    在5.0版之前的Java平臺已經具有了一些ad hoc annotation機制。比如,使用transient修飾符來標識一個成員變量在序列化子系統中應被忽略。而@deprecated這個javadoc tag也是一個ad hoc annotation用來說明一個方法已過時。從Java5.0版發布以來,5.0平臺提供了一個正式的annotation功能:允許開發者定義、使用自己的annoatation類型。此功能由一個定義annotation類型的語法和一個描述annotation聲明的語法,讀取annotaion的API,一個使用annotation修飾的class文件,一個annotation處理工具(apt)組成。
    annotation并不直接影響代碼語義,但是它能夠工作的方式被看作類似程序的工具或者類庫,它會反過來對正在運行的程序語義有所影響。annotation可以從源文件、class文件或者以在運行時反射的多種方式被讀取。
    當然annotation在某種程度上使javadoc tag更加完整。一般情況下,如果這個標記對java文檔產生影響或者用于生成java文檔的話,它應該作為一個javadoc tag;否則將作為一個annotation。
    三、Annotation使用方法:
    1。類型聲明方式:
    通常,應用程序并不是必須定義annotation類型,但是定義annotation類型并非難事。Annotation類型聲明于一般的接口聲明極為類似,區別只在于它在interface關鍵字前面使用“@”符號。
    annotation類型的每個方法聲明定義了一個annotation類型成員,但方法聲明不必有參數或者異常聲明;方法返回值的類型被限制在以下的范圍:primitives、String、Class、enums、annotation和前面類型的數組;方法可以有默認值。
    下面是一個簡單的annotation類型聲明:
    清單1:
    ? ? /**
    ? ???* Describes the Request-For-Enhancement(RFE) that led
    ? ???* to the presence of the annotated API element.
    ? ???*/
    ? ? public @interface RequestForEnhancement {
    ? ?? ???int? ? id();
    ? ?? ???String synopsis();
    ? ?? ???String engineer() default "[unassigned]";
    ? ?? ???String date();? ? default "[unimplemented]";
    ? ? }
    代碼中只定義了一個annotation類型RequestForEnhancement。
    2。修飾方法的annotation聲明方式:
    annotation是一種修飾符,能夠如其它修飾符(如public、static、final)一般使用。習慣用法是annotaions用在其它的修飾符前面。annotations由“@+annotation類型+帶有括號的成員-值列表”組成。這些成員的值必須是編譯時常量(即在運行時不變)。
    A:下面是一個使用了RequestForEnhancement annotation的方法聲明:
    清單2:
    ? ? @RequestForEnhancement(
    ? ?? ???id? ?? ? = 2868724,
    ? ?? ???synopsis = "Enable time-travel",
    ? ?? ???engineer = "Mr. Peabody",
    ? ?? ???date? ???= "4/1/3007"
    ? ? )
    ? ? public static void travelThroughTime(Date destination) { ... }
    B:當聲明一個沒有成員的annotation類型聲明時,可使用以下方式:
    清單3:
    ? ? /**
    ? ???* Indicates that the specification of the annotated API element
    ? ???* is preliminary and subject to change.
    ? ???*/
    ? ? public @interface Preliminary { }
    作為上面沒有成員的annotation類型聲明的簡寫方式:
    清單4:
    ? ? @Preliminary public class TimeTravel { ... }
    C:如果在annotations中只有唯一一個成員,則該成員應命名為value:
    清單5:
    ? ? /**
    ? ???* Associates a copyright notice with the annotated API element.
    ? ???*/
    ? ? public @interface Copyright {
    ? ?? ???String value();
    ? ? }
    更為方便的是對于具有唯一成員且成員名為value的annotation(如上文),在其使用時可以忽略掉成員名和賦值號(=):
    清單6:
    ? ? @Copyright("2002 Yoyodyne Propulsion Systems")
    ? ? public class OscillationOverthruster { ... }
    3。一個使用實例:
    結合上面所講的,我們在這里建立一個簡單的基于annotation測試框架。首先我們需要一個annotation類型來表示某個方法是一個應該被測試工具運行的測試方法。
    清單7:
    ? ? import java.lang.annotation.*;
    ? ? /**
    ? ???* Indicates that the annotated method is a test method.
    ? ???* This annotation should be used only on parameterless static methods.
    ? ???*/
    ? ? @Retention(RetentionPolicy.RUNTIME)
    ? ? @Target(ElementType.METHOD)
    ? ? public @interface Test { }
    值得注意的是annotaion類型聲明是可以標注自己的,這樣的annotation被稱為“meta-annotations”。
    在上面的代碼中,@Retention(RetentionPolicy.RUNTIME)這個meta-annotation表示了此類型的annotation將被虛擬機保留使其能夠在運行時通過反射被讀取。而@Target(ElementType.METHOD)表示此類型的annotation只能用于修飾方法聲明。
    下面是一個簡單的程序,其中部分方法被上面的annotation所標注:
    清單8:
    ? ? public class Foo {
    ? ?? ???@Test public static void m1() { }
    ? ?? ???public static void m2() { }
    ? ?? ???@Test public static void m3() {
    ? ?? ?? ?? ?throw new RuntimeException("Boom");
    ? ?? ???}
    ? ?? ???public static void m4() { }
    ? ?? ???@Test public static void m5() { }
    ? ?? ???public static void m6() { }
    ? ?? ???@Test public static void m7() {
    ? ?? ?? ?? ?throw new RuntimeException("Crash");
    ? ?? ???}
    ? ?? ???public static void m8() { }
    ? ? }
    Here is the testing tool:
    ? ? import java.lang.reflect.*;
    ? ? public class RunTests {
    ? ?? ? public static void main(String[] args) throws Exception {
    ? ?? ?? ? int passed = 0, failed = 0;
    ? ?? ?? ? for (Method m : Class.forName(args[0]).getMethods()) {
    ? ?? ?? ?? ? if (m.isAnnotationPresent(Test.class)) {
    ? ?? ?? ?? ?? ? try {
    ? ?? ?? ?? ?? ?? ? m.invoke(null);
    ? ?? ?? ?? ?? ?? ? passed++;
    ? ?? ?? ?? ?? ? } catch (Throwable ex) {
    ? ?? ?? ?? ?? ?? ? System.out.printf("Test %s failed: %s %n", m, ex.getCause());
    ? ?? ?? ?? ?? ?? ? failed++;
    ? ?? ?? ?? ?? ? }
    ? ?? ?? ?? ? }
    ? ?? ?? ? }
    ? ?? ?? ? System.out.printf("Passed: %d, Failed %d%n", passed, failed);
    ? ?? ? }
    ? ? }
    這個程序從命令行參數中取出類名,并且遍歷此類的所有方法,嘗試調用其中被上面的測試annotation類型標注過的方法。在此過程中為了找出哪些方法被annotation類型標注過,需要使用反射的方式執行此查詢。如果在調用方法時拋出異常,此方法被認為已經失敗,并打印一個失敗報告。最后,打印運行通過/失敗的方法數量。
    下面文字表示了如何運行這個基于annotation的測試工具:
    清單9:
    ? ? $ java RunTests Foo
    ? ? Test public static void Foo.m3() failed: java.lang.RuntimeException: Boom
    ? ? Test public static void Foo.m7() failed: java.lang.RuntimeException: Crash
    ? ? Passed: 2, Failed 2
    四、Annotation分類:
    根據annotation的使用方法和用途主要分為以下幾類:
    1。內建Annotation——Java5.0版在java語法中經常用到的內建Annotation:
    @Deprecated用于修飾已經過時的方法;
    @Override用于修飾此方法覆蓋了父類的方法(而非重載);
    @SuppressWarnings用于通知java編譯器禁止特定的編譯警告。
    下面代碼展示了內建Annotation類型的用法:
    清單10:
    package com.bjinfotech.practice.annotation;
    /**
    * 演示如何使用java5內建的annotation
    * 參考資料:
    * http://java.sun.com/docs/books/t ... OO/annotations.html
    * http://java.sun.com/j2se/1.5.0/d ... ge/annotations.html
    * http://mindprod.com/jgloss/annotations.html
    * @author cleverpig
    *
    */
    import java.util.List;
    public class UsingBuiltInAnnotation {
    ? ?? ???//食物類
    ? ?? ???class Food{}
    ? ?? ???//干草類
    ? ?? ???class Hay extends Food{}
    ? ?? ???//動物類
    ? ?? ???class Animal{
    ? ?? ?? ?? ?? ? Food getFood(){
    ? ?? ?? ?? ?? ?? ?? ?? ?return null;
    ? ?? ?? ?? ?? ? }
    ? ?? ?? ?? ?? ? //使用Annotation聲明Deprecated方法
    ? ?? ?? ?? ?? ? @Deprecated
    ? ?? ?? ?? ?? ? void deprecatedMethod(){
    ? ?? ?? ?? ?? ? }
    ? ?? ???}
    ? ?? ???//馬類-繼承動物類
    ? ?? ???class Horse extends Animal{
    ? ?? ?? ?? ?? ? //使用Annotation聲明覆蓋方法
    ? ?? ?? ?? ?? ? @Override
    ? ?? ?? ?? ?? ? Hay getFood(){
    ? ?? ?? ?? ?? ?? ?? ?? ?return new Hay();
    ? ?? ?? ?? ?? ? }
    ? ?? ?? ?? ?? ? //使用Annotation聲明禁止警告
    ? ?? ?? ?? ?? ? @SuppressWarnings({"deprecation","unchecked"})
    ? ?? ?? ?? ?? ? void callDeprecatedMethod(List horseGroup){
    ? ?? ?? ?? ?? ?? ?? ?? ?Animal an=new Animal();
    ? ?? ?? ?? ?? ?? ?? ?? ?an.deprecatedMethod();
    ? ?? ?? ?? ?? ?? ?? ?? ?horseGroup.add(an);
    ? ?? ?? ?? ?? ? }
    ? ?? ???}
    }
    2。開發者自定義Annotation:由開發者自定義Annotation類型。
    下面是一個使用annotation進行方法測試的sample:
    AnnotationDefineForTestFunction類型定義如下:
    清單11:
    package com.bjinfotech.practice.annotation;
    import java.lang.annotation.*;
    /**
    * 定義annotation
    * @author cleverpig
    *
    */
    //加載在VM中,在運行時進行映射
    @Retention(RetentionPolicy.RUNTIME)
    //限定此annotation只能標示方法
    @Target(ElementType.METHOD)
    public @interface AnnotationDefineForTestFunction{}
    測試annotation的代碼如下:
    清單12:
    package com.bjinfotech.practice.annotation;
    import java.lang.reflect.*;
    /**
    * 一個實例程序應用前面定義的Annotation:AnnotationDefineForTestFunction
    * @author cleverpig
    *
    */
    public class UsingAnnotation {
    ? ?? ???@AnnotationDefineForTestFunction public static void method01(){}
    ? ?? ???
    ? ?? ???public static void method02(){}
    ? ?? ???
    ? ?? ???@AnnotationDefineForTestFunction public static void method03(){
    ? ?? ?? ?? ?? ? throw new RuntimeException("method03");
    ? ?? ???}
    ? ?? ???
    ? ?? ???public static void method04(){
    ? ?? ?? ?? ?? ? throw new RuntimeException("method04");
    ? ?? ???}
    ? ?? ???
    ? ?? ???public static void main(String[] argv) throws Exception{
    ? ?? ?? ?? ?? ? int passed = 0, failed = 0;
    ? ?? ?? ?? ?? ? //被檢測的類名
    ? ?? ?? ?? ?? ? String className="com.bjinfotech.practice.annotation.UsingAnnotation";
    ? ?? ?? ?? ?? ? //逐個檢查此類的方法,當其方法使用annotation聲明時調用此方法
    ? ?? ?? ?? ?for (Method m : Class.forName(className).getMethods()) {
    ? ?? ?? ?? ?? ?if (m.isAnnotationPresent(AnnotationDefineForTestFunction.class)) {
    ? ?? ?? ?? ?? ?? ?try {
    ? ?? ?? ?? ?? ?? ?? ?m.invoke(null);
    ? ?? ?? ?? ?? ?? ?? ?passed++;
    ? ?? ?? ?? ?? ?? ?} catch (Throwable ex) {
    ? ?? ?? ?? ?? ?? ?? ?System.out.printf("測試 %s 失敗: %s %n", m, ex.getCause());
    ? ?? ?? ?? ?? ?? ?? ?failed++;
    ? ?? ?? ?? ?? ?? ?}
    ? ?? ?? ?? ?? ?}
    ? ?? ?? ?? ?}
    ? ?? ?? ?? ?System.out.printf("測試結果: 通過: %d, 失敗: %d%n", passed, failed);
    ? ?? ???}
    }
    3。使用第三方開發的Annotation類型
    這也是開發人員所常常用到的一種方式。比如我們在使用Hibernate3.0時就可以利用Annotation生成數據表映射配置文件,而不必使用Xdoclet。
    五、總結:
    1。前面的文字說明了annotation的使用方法、定義方式、分類。初學者可以通過以上的說明制作簡單的annotation程序,但是對于一些高級的annotation應用(例如使用自定義annotation生成javabean映射xml文件)還需要進一步的研究和探討。
    2。同時,annotation運行存在兩種方式:運行時、編譯時。上文中討論的都是在運行時的annotation應用,但在編譯時的annotation應用還沒有涉及,因為編譯時的annotation要使用annotation processing tool。
    涉及以上2方面的深入內容,作者將在后文《Java Annotation高級應用》中談到。
    六、參考資源:
    ·Matrix-Java開發者社區:
    http://www.matrix.org.cn
    ·
    http://java.sun.com/docs/books/tutorial/java/javaOO/annotations.html
    ·
    http://java.sun.com/j2se/1.5.0/docs/guide/apt/GettingStarted.html
    ·
    http://java.sun.com/j2se/1.5.0/docs/guide/apt/GettingStarted.html
    ·
    http://java.sun.com/j2se/1.5.0/docs/guide/apt/GettingStarted.html
    ·作者的Blog:
    http://blog.matrix.org.cn/page/cleverpig
    posted @ 2006-04-03 13:30 hopeshared 閱讀(896) | 評論 (0)編輯 收藏

    級別: 中級

    Brian Goetz , 首席顧問, Quiotix

    2005 年 12 月 19 日

    雖然用 Java? 語言編寫的程序在理論上是不會出現“內存泄漏”的,但是有時對象在不再作為程序的邏輯狀態的一部分之后仍然不被垃圾收集。本月,負責保障應用程序健康的工程師 Brian Goetz 探討了無意識的對象保留的常見原因,并展示了如何用弱引用堵住泄漏。

    要讓垃圾收集(GC)回收程序不再使用的對象,對象的邏輯 生命周期(應用程序使用它的時間)和對該對象擁有的引用的實際 生命周期必須是相同的。在大多數時候,好的軟件工程技術保證這是自動實現的,不用我們對對象生命周期問題花費過多心思。但是偶爾我們會創建一個引用,它在內存中包含對象的時間比我們預期的要長得多,這種情況稱為無意識的對象保留(unintentional object retention)

    全局 Map 造成的內存泄漏

    無意識對象保留最常見的原因是使用 Map 將元數據與臨時對象(transient object)相關聯。假定一個對象具有中等生命周期,比分配它的那個方法調用的生命周期長,但是比應用程序的生命周期短,如客戶機的套接字連接。需要將一些元數據與這個套接字關聯,如生成連接的用戶的標識。在創建 Socket 時是不知道這些信息的,并且不能將數據添加到 Socket 對象上,因為不能控制 Socket 類或者它的子類。這時,典型的方法就是在一個全局 Map 中存儲這些信息,如清單 1 中的 SocketManager 類所示:


    清單 1. 使用一個全局 Map 將元數據關聯到一個對象

    public class SocketManager {
    ??? private Map<Socket,User> m = new HashMap<Socket,User>();
    ???
    ??? public void setUser(Socket s, User u) {
    ??????? m.put(s, u);
    ??? }
    ??? public User getUser(Socket s) {
    ??????? return m.get(s);
    ??? }
    ??? public void removeUser(Socket s) {
    ??????? m.remove(s);
    ??? }
    }

    SocketManager socketManager;
    ...
    socketManager.setUser(socket, user);


    這種方法的問題是元數據的生命周期需要與套接字的生命周期掛鉤,但是除非準確地知道什么時候程序不再需要這個套接字,并記住從 Map 中刪除相應的映射,否則,SocketUser 對象將會永遠留在 Map 中,遠遠超過響應了請求和關閉套接字的時間。這會阻止 SocketUser 對象被垃圾收集,即使應用程序不會再使用它們。這些對象留下來不受控制,很容易造成程序在長時間運行后內存爆滿。除了最簡單的情況,在幾乎所有情況下找出什么時候 Socket 不再被程序使用是一件很煩人和容易出錯的任務,需要人工對內存進行管理。

    -----------------------------------------------------------------------------------

    找出內存泄漏

    程序有內存泄漏的第一個跡象通常是它拋出一個 OutOfMemoryError,或者因為頻繁的垃圾收集而表現出糟糕的性能。幸運的是,垃圾收集可以提供能夠用來診斷內存泄漏的大量信息。如果以 -verbose:gc 或者 -Xloggc 選項調用 JVM,那么每次 GC 運行時在控制臺上或者日志文件中會打印出一個診斷信息,包括它所花費的時間、當前堆使用情況以及恢復了多少內存。記錄 GC 使用情況并不具有干擾性,因此如果需要分析內存問題或者調優垃圾收集器,在生產環境中默認啟用 GC 日志是值得的。

    有工具可以利用 GC 日志輸出并以圖形方式將它顯示出來,JTune 就是這樣的一種工具(請參閱 參考資料)。觀察 GC 之后堆大小的圖,可以看到程序內存使用的趨勢。對于大多數程序來說,可以將內存使用分為兩部分:baseline 使用和 current load 使用。對于服務器應用程序,baseline 使用就是應用程序在沒有任何負荷、但是已經準備好接受請求時的內存使用,current load 使用是在處理請求過程中使用的、但是在請求處理完成后會釋放的內存。只要負荷大體上是恒定的,應用程序通常會很快達到一個穩定的內存使用水平。如果在應用程序已經完成了其初始化并且負荷沒有增加的情況下,內存使用持續增加,那么程序就可能在處理前面的請求時保留了生成的對象。

    清單 2 展示了一個有內存泄漏的程序。MapLeaker 在線程池中處理任務,并在一個 Map 中記錄每一項任務的狀態。不幸的是,在任務完成后它不會刪除那一項,因此狀態項和任務對象(以及它們的內部狀態)會不斷地積累。


    清單 2. 具有基于 Map 的內存泄漏的程序


    public class MapLeaker {
    ??? public ExecutorService exec = Executors.newFixedThreadPool(5);
    ??? public Map<Task, TaskStatus> taskStatus
    ??????? = Collections.synchronizedMap(new HashMap<Task, TaskStatus>());
    ??? private Random random = new Random();

    ??? private enum TaskStatus { NOT_STARTED, STARTED, FINISHED };

    ??? private class Task implements Runnable {
    ??????? private int[] numbers = new int[random.nextInt(200)];

    ??????? public void run() {
    ??????????? int[] temp = new int[random.nextInt(10000)];
    ??????????? taskStatus.put(this, TaskStatus.STARTED);
    ??????????? doSomeWork();
    ??????????? taskStatus.put(this, TaskStatus.FINISHED);
    ??????? }
    ??? }

    ??? public Task newTask() {
    ??????? Task t = new Task();
    ??????? taskStatus.put(t, TaskStatus.NOT_STARTED);
    ??????? exec.execute(t);
    ??????? return t;
    ??? }
    }
    ?

    圖 1 顯示 MapLeaker GC 之后應用程序堆大小隨著時間的變化圖。上升趨勢是存在內存泄漏的警示信號。(在真實的應用程序中,坡度不會這么大,但是在收集了足夠長時間的 GC 數據后,上升趨勢通常會表現得很明顯。)


    圖 1. 持續上升的內存使用趨勢

    確信有了內存泄漏后,下一步就是找出哪種對象造成了這個問題。所有內存分析器都可以生成按照對象類進行分解的堆快照。有一些很好的商業堆分析工具,但是找出內存泄漏不一定要花錢買這些工具 —— 內置的 hprof 工具也可完成這項工作。要使用 hprof 并讓它跟蹤內存使用,需要以 -Xrunhprof:heap=sites 選項調用 JVM。

    清單 3 顯示分解了應用程序內存使用的 hprof 輸出的相關部分。(hprof 工具在應用程序退出時,或者用 kill -3 或在 Windows 中按 Ctrl+Break 時生成使用分解。)注意兩次快照相比,Map.EntryTaskint[] 對象有了顯著增加。

    請參閱 清單 3

    清單 4 展示了 hprof 輸出的另一部分,給出了 Map.Entry 對象的分配點的調用堆棧信息。這個輸出告訴我們哪些調用鏈生成了 Map.Entry 對象,并帶有一些程序分析,找出內存泄漏來源一般來說是相當容易的。


    清單 4. HPROF 輸出,顯示 Map.Entry 對象的分配點


    TRACE 300446:
    java.util.HashMap$Entry.<init>(<Unknown Source>:Unknown line)
    java.util.HashMap.addEntry(<Unknown Source>:Unknown line)
    java.util.HashMap.put(<Unknown Source>:Unknown line)
    java.util.Collections$SynchronizedMap.put(<Unknown Source>:Unknown line)
    com.quiotix.dummy.MapLeaker.newTask(MapLeaker.java:48)
    com.quiotix.dummy.MapLeaker.main(MapLeaker.java:64)


    -------------------------------------------------------------------------------------------

    弱引用來救援了

    SocketManager 的問題是 Socket-User 映射的生命周期應當與 Socket 的生命周期相匹配,但是語言沒有提供任何容易的方法實施這項規則。這使得程序不得不使用人工內存管理的老技術。幸運的是,從 JDK 1.2 開始,垃圾收集器提供了一種聲明這種對象生命周期依賴性的方法,這樣垃圾收集器就可以幫助我們防止這種內存泄漏 —— 利用弱引用

    弱引用是對一個對象(稱為 referent)的引用的持有者。使用弱引用后,可以維持對 referent 的引用,而不會阻止它被垃圾收集。當垃圾收集器跟蹤堆的時候,如果對一個對象的引用只有弱引用,那么這個 referent 就會成為垃圾收集的候選對象,就像沒有任何剩余的引用一樣,而且所有剩余的弱引用都被清除。(只有弱引用的對象稱為弱可及(weakly reachable)。)

    WeakReference 的 referent 是在構造時設置的,在沒有被清除之前,可以用 get() 獲取它的值。如果弱引用被清除了(不管是 referent 已經被垃圾收集了,還是有人調用了 WeakReference.clear()),get() 會返回 null。相應地,在使用其結果之前,應當總是檢查 get() 是否返回一個非 null 值,因為 referent 最終總是會被垃圾收集的。

    用一個普通的(強)引用拷貝一個對象引用時,限制 referent 的生命周期至少與被拷貝的引用的生命周期一樣長。如果不小心,那么它可能就與程序的生命周期一樣 —— 如果將一個對象放入一個全局集合中的話。另一方面,在創建對一個對象的弱引用時,完全沒有擴展 referent 的生命周期,只是在對象仍然存活的時候,保持另一種到達它的方法。

    弱引用對于構造弱集合最有用,如那些在應用程序的其余部分使用對象期間存儲關于這些對象的元數據的集合 —— 這就是 SocketManager 類所要做的工作。因為這是弱引用最常見的用法,WeakHashMap 也被添加到 JDK 1.2 的類庫中,它對鍵(而不是對值)使用弱引用。如果在一個普通 HashMap 中用一個對象作為鍵,那么這個對象在映射從 Map 中刪除之前不能被回收,WeakHashMap 使您可以用一個對象作為 Map 鍵,同時不會阻止這個對象被垃圾收集。清單 5 給出了 WeakHashMapget() 方法的一種可能實現,它展示了弱引用的使用:


    清單 5. WeakReference.get() 的一種可能實現


    public class WeakHashMap<K,V> implements Map<K,V> {
    
        private static class Entry<K,V> extends WeakReference<K> 
          implements Map.Entry<K,V> {
            private V value;
            private final int hash;
            private Entry<K,V> next;
            ...
        }
    
        public V get(Object key) {
            int hash = getHash(key);
            Entry<K,V> e = getChain(hash);
            while (e != null) {
                K eKey= e.get();
                if (e.hash == hash && (key == eKey || key.equals(eKey)))
                    return e.value;
                e = e.next;
            }
            return null;
        }
    


    調用 WeakReference.get() 時,它返回一個對 referent 的強引用(如果它仍然存活的話),因此不需要擔心映射在 while 循環體中消失,因為強引用會防止它被垃圾收集。WeakHashMap 的實現展示了弱引用的一種常見用法 —— 一些內部對象擴展 WeakReference。其原因在下面一節討論引用隊列時會得到解釋。

    在向 WeakHashMap 中添加映射時,請記住映射可能會在以后“脫離”,因為鍵被垃圾收集了。在這種情況下,get() 返回 null,這使得測試 get() 的返回值是否為 null 變得比平時更重要了。

    用 WeakHashMap 堵住泄漏

    SocketManager 中防止泄漏很容易,只要用 WeakHashMap 代替 HashMap 就行了,如清單 6 所示。(如果 SocketManager 需要線程安全,那么可以用 Collections.synchronizedMap() 包裝 WeakHashMap)。當映射的生命周期必須與鍵的生命周期聯系在一起時,可以使用這種方法。不過,應當小心不濫用這種技術,大多數時候還是應當使用普通的 HashMap 作為 Map 的實現。


    清單 6. 用 WeakHashMap 修復 SocketManager


    public class SocketManager {
        private Map<Socket,User> m = new WeakHashMap<Socket,User>();
        
        public void setUser(Socket s, User u) {
            m.put(s, u);
        }
        public User getUser(Socket s) {
            return m.get(s);
        }
    }
    


    引用隊列

    WeakHashMap 用弱引用承載映射鍵,這使得應用程序不再使用鍵對象時它們可以被垃圾收集,get() 實現可以根據 WeakReference.get() 是否返回 null 來區分死的映射和活的映射。但是這只是防止 Map 的內存消耗在應用程序的生命周期中不斷增加所需要做的工作的一半,還需要做一些工作以便在鍵對象被收集后從 Map 中刪除死項。否則,Map 會充滿對應于死鍵的項。雖然這對于應用程序是不可見的,但是它仍然會造成應用程序耗盡內存,因為即使鍵被收集了,Map.Entry 和值對象也不會被收集。

    可以通過周期性地掃描 Map,對每一個弱引用調用 get(),并在 get() 返回 null 時刪除那個映射而消除死映射。但是如果 Map 有許多活的項,那么這種方法的效率很低。如果有一種方法可以在弱引用的 referent 被垃圾收集時發出通知就好了,這就是引用隊列 的作用。

    引用隊列是垃圾收集器向應用程序返回關于對象生命周期的信息的主要方法。弱引用有兩個構造函數:一個只取 referent 作為參數,另一個還取引用隊列作為參數。如果用關聯的引用隊列創建弱引用,在 referent 成為 GC 候選對象時,這個引用對象(不是 referent)就在引用清除后加入 到引用隊列中。之后,應用程序從引用隊列提取引用并了解到它的 referent 已被收集,因此可以進行相應的清理活動,如去掉已不在弱集合中的對象的項。(引用隊列提供了與 BlockingQueue 同樣的出列模式 —— polled、timed blocking 和 untimed blocking。)

    WeakHashMap 有一個名為 expungeStaleEntries() 的私有方法,大多數 Map 操作中會調用它,它去掉引用隊列中所有失效的引用,并刪除關聯的映射。清單 7 展示了 expungeStaleEntries() 的一種可能實現。用于存儲鍵-值映射的 Entry 類型擴展了 WeakReference,因此當 expungeStaleEntries() 要求下一個失效的弱引用時,它得到一個 Entry。用引用隊列代替定期掃描內容的方法來清理 Map 更有效,因為清理過程不會觸及活的項,只有在有實際加入隊列的引用時它才工作。


    清單 7. WeakHashMap.expungeStaleEntries() 的可能實現


    private void expungeStaleEntries() {
    Entry<K,V> e;
    ??????? while ( (e = (Entry<K,V>) queue.poll()) != null) {
    ??????????? int hash = e.hash;

    ??????????? Entry<K,V> prev = getChain(hash);
    ??????????? Entry<K,V> cur = prev;
    ??????????? while (cur != null) {
    ??????????????? Entry<K,V> next = cur.next;
    ??????????????? if (cur == e) {
    ??????????????????? if (prev == e)
    ??????????????????????? setChain(hash, next);
    ??????????????????? else
    ??????????????????????? prev.next = next;
    ??????????????????? break;
    ??????????????? }
    ??????????????? prev = cur;
    ??????????????? cur = next;
    ??????????? }
    ??????? }
    ??? }


    ------------------------------------------------------------------------------------------------

    結束語

    弱引用和弱集合是對堆進行管理的強大工具,使得應用程序可以使用更復雜的可及性方案,而不只是由普通(強)引用所提供的“要么全部要么沒有”可及性。下個月,我們將分析與弱引用有關的軟引用,將分析在使用弱引用和軟引用時,垃圾收集器的行為。

    -----------------------------------------------------------------------------------------------------


    原文地址:http://www-128.ibm.com/developerworks/cn/java/j-jtp11225/index.html

    posted @ 2006-04-03 11:31 hopeshared 閱讀(598) | 評論 (0)編輯 收藏

    不知道這個標題是否讓讀者產生一種想打我的沖動。至少今天我的主管被我用這個小把戲詫異了一把,當他看到"hi there".equals("cheers !") 的結果居然是true時,臉上的表情實在是可愛。

    OK,言歸正傳。System.out.println("hi there".equals("cheers !")); 這個看來再顯然不過的句子,輸出的結果居然是true。聰明的讀者,你知道是為什么嗎?如果一時還猜不出來,給你一點提示:

    1、Java語言規范規定,同一個程序中任何相同的字符串常量(literal string)都只是同一個String對象的不同引用,不論它們是否在同一個類、同一個包中。

    2、Java語言規范規定,由常量表達式計算得到的String對象將在編譯期被求值,并在運行時被作為字符串常量對待;在運行時計算得到的String對象將是一個完全獨立的新對象。

    如果你仍然不明就里,或者想知道這個把戲實現的細節,請看下面這篇來自artima的webLog

    ——————————————————

    Artima Weblogs
    "hi there".equals("cheers !") == true
    by Heinz Kabutz
    May 21, 2003
    Summary
    Java Strings are strange animals. They are all kept in one pen, especially the constant strings. This can lead to bizarre behaviour when we intentionally modify the innards of the constant strings through reflection. Join us, as we take apart one of Java's most prolific beasts.

    Whenever we used to ask our dad a question that he could not possibly have known the answer to (such as: what's the point of school, dad?) he would ask back: "How long is a piece of string?"

    Were he to ask me that now, I would explain to him that String is immutable (supposedly) and that it contains its length, all you have to do is ask the String how long it is. This you can do by calling length().

    OK, so the first thing we learn about Java is that String is immutable. It is like when we first learn about the stork that brings the babies? There are some things you are not supposed to know until you are older! Secrets so dangerous that merely knowing them would endanger the fibres of electrons pulsating through your Java Virtual Machine.

    So, are Strings immutable?

    Playing with your sanity - Strings

    Have a look at the following code:

    public ? class ?MindWarp? {
    ??
    public ? static ? void ?main(String[]?args)? {
    ????System.out.println(
    ??????
    " Romeo,?Romeo,?wherefore?art?thou?oh?Romero? " );
    ??}

    ??
    private ? static ? final ?String?OH_ROMEO? =
    ????
    " Romeo,?Romeo,?wherefore?art?thou?oh?Romero? " ;
    ??
    private ? static ? final ?Warper?warper? = ? new ?Warper();
    }


    If we are told that the class Warper does not produce any visible output when you construct it, what is the output of this program? The most correct answer is, "you don't know, depends on what Warper does". Now THERE's a nice question for the Sun Certified Java Programmer Examination.

    In my case, running "java MindWarp" produces the following output

    C:> java MindWarp <ENTER>
    Stop this romance nonsense, or I'll be sick
    

    And here is the code for Warper:

    												
    import ?java.lang.reflect. * ;
    public ? class ?Warper? {
    ??
    private ? static ?Field?stringValue;
    ??
    static ? {
    ????
    // ?String?has?a?private?char?[]?called?"value"
    ????
    // ?if?it?does?not,?find?the?char?[]?and?assign?it?to?valuetry?{
    ??????stringValue? = ?String. class .getDeclaredField( " value " );
    ????}
    ? catch (NoSuchFieldException?ex)? {
    ??????
    // ?safety?net?in?case?we?are?running?on?a?VM?with?a
    ??????
    // ?different?name?for?the?char?array.
    ??????Field[]?all? = ?String. class .getDeclaredFields();
    ??????
    for ?( int ?i = 0 ;?stringValue? == ? null ? && ?i < all.length;?i ++ )? {
    ????????
    if ?(all[i].getType().equals( char []. class ))? {
    ??????????stringValue?
    = ?all[i];
    ????????}

    ??????}

    ????}

    ????
    if ?(stringValue? != ? null )? {
    ??????stringValue.setAccessible(
    true );? // ?make?field?public
    ????}

    ??}

    ??
    public ?Warper()? {
    ????
    try ? {
    ??????stringValue.set(
    ????????
    " Romeo,?Romeo,?wherefore?art?thou?oh?Romero? " ,
    ????????
    " Stop?this?romance?nonsense,?or?I'll?be?sick " .
    ??????????toCharArray());
    ??????stringValue.set(
    " hi?there " ,? " cheers?! " .toCharArray());
    ????}
    ? catch (IllegalAccessException?ex)? {} ? // ?shhh
    ??}

    }

    How is this possible? How can String manipulation in a completely different part of the program affect our class MindWarp?

    To understand that, we have to look under the hood of Java. In the language specification it says in ?3.10.5:

    "Each string literal is a reference (?4.3) to an instance (?4.3.1, ?12.5) of class String (?4.3.3). String objects have a constant value. String literals-or, more generally, strings that are the values of constant expressions (?15.28)-are "interned" so as to share unique instances, using the method String.intern."

    The usefulness of this is quite obvious, we will use less memory if we have two Strings which are equivalent pointing at the same object. We can also manually intern Strings by calling the intern() method.

    The language spec goes a bit further:

    1. Literal strings within the same class (?8) in the same package (?7) represent references to the same String object (?4.3.1).
    2. Literal strings within different classes in the same package represent references to the same String object.
    3. Literal strings within different classes in different packages likewise represent references to the same String object.
    4. Strings computed by constant expressions (?15.28) are computed at compile time and then treated as if they were literals.
    5. Strings computed at run time are newly created and therefore distinct.
    6. The result of explicitly interning a computed string is the same string as any pre-existing literal string with the same contents.

    This means that if a class in another package "fiddles" with an interned String, it can cause havoc in your program. Is this a good thing? (You don't need to answer ;-)

    Consider this example

    												
    public ? class ?StringEquals? {
    public ? static ? void ?main(String[]?args)? {
    ??System.out.println(
    " hi?there " .equals( " cheers?! " ));
    }

    private ? static ? final ?String?greeting? = ? " hi?there " ;
    private ? static ? final ?Warper?warper? = ? new ?Warper();
    }

    Running this against the Warper produces a result of true, which is really weird, and in my opinion, quite mind-bending. Hey, you can SEE the values there right in front of you and they are clearly NOT equal!

    BTW, for simplicity, the Strings in my examples are exactly the same length, but you can change the length quite easily as well.

    Last example concerns the HashCode of String, which is now cached for performance reasons mentioned in "Java Idiom and Performance Guide", ISBN 0130142603. (Just for the record, I was never and am still not convinced that caching the String hash code in a wrapper object is a good idea, but caching it in String itself is almost acceptable, considering String literals.)

    												
    public ? class ?CachingHashcode? {
    ??
    public ? static ? void ?main(String[]?args)? {
    ????java.util.Map?map?
    = ? new ?java.util.HashMap();
    ????map.put(
    " hi?there " ,? " You?found?the?value " );
    ????
    new ?Warper();
    ????System.out.println(map.get(
    " hi?there " ));
    ????System.out.println(map);
    ??}

    ??
    private ? static ? final ?String?greeting? = ? " hi?there " ;
    }

    The output under JDK 1.3 is:

    You found the value
    {cheers !=You found the value}
    

    Under JDK 1.2 it is

    null
    {cheers !=You found the value}
    

    This is because in the JDK 1.3 SUN is caching the hash code so if it once is calculated, it doesn't get recalculated, so if the value field changes, the hashcode stays the same.

    Imagine trying to debug this program where SOMEWHERE, one of your hackers has done a "workaround" by modifying a String literal. The thought scares me.

    The practical application of this blog? Let's face it, none.

    This is my first blog ever, I would be keen to hear what you thought of it?



    摘自:http://www.daima.com.cn/Info/55/Info14695/

    posted @ 2006-04-03 11:23 hopeshared 閱讀(523) | 評論 (1)編輯 收藏

    有很多介紹基本的Java應用性能調整的文章。他們都討論些簡單的技術,諸如使用StringBuffer而不用String,使用synchronized關鍵字的開銷等等。
      
      這篇文章不再介紹這些東西。相反,我們關注能幫助你的基于Web的應用更快、可升級型更好的技巧。一些技巧很詳細,其他的相對簡短,但所有的都很有用。最后以一些你可提供給你的管理者的建議結束。
      
      我寫這篇文章的靈感來自于當我的同事和我一起回憶我們的.com(dot-com)時代的時候――我們如何設計能支持成千上萬的用戶和擁有緊密代碼的系統,我們如何對有侵略性的致命打擊。有時在為復用設計和為性能設計之間有一個權衡。基于我的情況,性能每次都獲勝。即使你的商務顧客無需理解代碼復用,但是他們知道快速(fast-performing)的系統是怎么回事。讓我們開始看看我們的技巧。
      
      如何使用Exception
      Exception降低性能。一個異常拋出首先需要創建一個新的對象。Throwable接口中的構造器調用名為fillInStackTrace()的本地方法。這個方法負責巡檢棧的整個框架來收集跟蹤信息。這樣無論何時有異常拋出,它要求虛擬機裝載調用棧,因為一個新的對象在中部被創建。
      
      異常應當僅用于有錯誤發生時,而不要控制流。
      
      我有機會在一個專門用于無線內容市場的網站(名字故意隱去了)看到一段代碼,其中開發者完全可以使用一個簡單的對照來查看對象是否為空。相反,他或她跳過了這個檢查而實際上拋出Null-PointerException。
      
      不要兩次初始化變量
      Java通過調用獨特的類構造器默認地初始化變量為一個已知的值。所有的對象被設置成null,integers (byte, short, int, long)被設置成0,float和double設置成0.0,Boolean變量設置成false。這對那些擴展自其它類的類尤其重要,這跟使用一個新的關鍵詞創建一個對象時所有一連串的構造器被自動調用一樣。
      
      對新的關鍵詞使用優選法則
      正如前面提到的,通過使用一個新的關鍵詞創建一個類的實例,在這個鏈中的所有構造器將被調用。如果你需要創建一個類的新實例,你可以使用一個實現了cloneable接口的對象的clone()方法。該clone方法不調用任何類的構造器。
      
      如果你已經使用了設計模式作為你的體系結構的一部分,并且使用了工廠模式創建對象,變化會很簡單。下面所列是工廠模式的典型實現。
      
      public static Account getNewAccount() {
      return new Account();
      }
      
      使用了clone方法的refactored代碼看起來可能像下面這樣:
      
      private static Account BaseAccount = new Account();
      public static Account getNewAccount() {
        return (Account) BaseAccount.clone();
      }
      
      以上的思路對實現數組同樣有用。
      
      如果你在應用中沒有使用設計模式,我建議你停止讀這篇文章,趕快跑到(不要走)書店挑一本四人著的《設計模式》。
      
      在任何可能的地方讓類為Final
      標記為final的類不能被擴展。在《核心Java API》中有大量這個技術的例子,諸如java.lang.String。將String類標記為final阻止了開發者創建他們自己實現的長度方法。
      
      更深入點說,如果類是final的,所有類的方法也是final的。Java編譯器可能會內聯所有的方法(這依賴于編譯器的實現)。在我的測試里,我已經看到性能平均增加了50%。
      
      在任何可能的地方使用局部變量
      屬于方法調用部分的自變量和聲明為此調用一部分的臨時變量存儲在棧中,這比較快。諸如static,實例(instance)變量和新的對象創建在堆中,這比較慢。局部變量的更深入優化依賴于你正在使用的編譯器或虛擬機。
      
      使用Nonblocking I/O
      當前的JDK版本不支持nonblocking I/O API,很多應用試圖通過創建大量的線程(目光長遠得用在池中)來避免阻塞。正如前述,在Java中創建線程有嚴重的開銷。
      
      典型的你可能看到應用中實現的線程需要支持并發I/O流,像Web 服務器,并quote and auction components.
      
      JDK1.4介紹了一個nonblocking I/O包(java.nio)。如果你必須保留在較早版本的JDK,有添加了支持nonblocking I/O的第三方包。
      
      :www.cs.berkeley.edu/~mdw/proj/java-nbio/download.html.
      
      停止小聰明
      很多開發人員在腦子中編寫可復用和靈活的代碼,而有時候在他們的程序中就產生額外的開銷。曾經或者另外的時候他們編寫了類似這樣的代碼:
      
      public void doSomething(File file) {
      FileInputStream fileIn = new FileInputStream(file);
      // do something
      
      他夠靈活,但是同時他們也產生了更多的開銷。這個主意背后做的事情是操縱一個InputStream,而不是一個文件,因此它應該重寫如下:
      
      public void doSomething(InputStream inputStream){
      // do something
      
      乘法和除法
      我有太多的東東適用于摩爾法則――它聲明CPU功率每年成倍增長。"摩爾法則"表明每年由開發者所寫的差勁的代碼數量三倍增加,劃去了摩爾法則的任何好處。
      
      考慮下面的代碼:
      
      for (val = 0; val < 100000; val +=5) { shiftX = val * 8; myRaise = val * 2; }
      
      如果我們狡猾的利用位移(bit),性能將會六倍增加。這是重寫的代碼:
      
      for (val = 0; val < 100000; val += 5) { shiftX = val << 3; myRaise = val << 1; }
      
      代替了乘以8,我們使用同等效果的左移3位。每一個移動相當于乘以2,變量myRaise對此做了證明。同樣向右移位相當于除以2,當然這會使執行速度加快,但可能會使你的東東以后難于理解;所以這只是個建議。
      
      選擇一個基于垃圾收集實現的虛擬機
      許多人可能會對Java規范不需要實現垃圾收集感到驚訝。設想時代已經是我們都擁有無限內存計算機。總之,垃圾收集器日常事務就是負責發現和拋出(hence garbage)不再需要的對象。垃圾收集必須發現那些對象不再被程序指向,并且使被對象占用的棧內存被釋放掉。它還負責運行任何被釋放對象的finalizer。
      
      垃圾收集故意不允許你釋放并非由你分配的內存,從而幫助你確保程序完整,當JVM確定CPU時間的時間表并且當垃圾收集器運行時,這個進程也產生開銷。
      
      垃圾收集器有兩個不同的步驟執行他們的工作。
      
      實現了定位計算的垃圾收集器在棧中為每一個對象保留一個計數。當一個對象被創建并且對它的一個定位被分配給一個變量,計數增加。當對象越出范圍,定位計數被設置成0并且對象可以被垃圾收集。這個步驟允許參考計數器運行在與程序執行有關的短時間增量內。定位計數在父子彼此擁有定位的應用里運行不正常。每次一個對象刷新時也會有定位計數增加和減少的開銷。
      
      實現了跟蹤的垃圾收集器從根節點開始跟蹤一列定位。對象發現跟蹤是否被標記。在這個過程完成后,知道不可達的任何沒標記的對象可以被垃圾收集。這可能以位圖(bitmap)形式實現或者在對象中被設置標志。此技術參考"Mark and Sweep."(reference:定位,翻譯成“指向”好像更容易理解,是Java語言對在用對象的一個跟蹤指針。譯者著)
      
      給你的管理人員提建議
      其他方法可被用來使你的基于Web的應用更快并且更可升級。可實現的最簡單的技術通常是支持cluster的策略。使用cluster,一組服務器能夠一起透明的提供服務。多數應用服務器允許你獲得cluster支持而不需要改變你的應用――一個大的勝利。
      
      當然在執行此步驟之前你可能需要考慮來自你使用的應用服務器提供商附加的許可權利。
      
      當看到cluster策略會有許多額外的事情考慮。經常在體系結構中產生的一個缺點是擁有有狀態會話。如果cluster中的一個服務器或者進程當掉,cluster會舍棄整個應用。為防止此類事情發生,cluster必須給cluster中的所有成員不斷復制會話Bean的狀態。確保你也限制了存儲在會話中的對象的大小和數量,因為這些也需要被復制。
      
      Cluster也允許你分期度量你的Web站點的部分。如果你需要度量靜態部分,你可以添加Web服務器。如果你需要度量動態生成的部分,你可以添加應用服務器。
      
      在你已經把你的系統放入cluster后,下一個讓你的應用跑得更快的建議步驟是選擇一個更好的虛擬機。看看Hotspot虛擬機或者其他的飛速發展中的執行優化的虛擬機。隨同虛擬機,看看更好的編譯器是一個更好的主意。
      
      如果你使用了幾個這兒提到的行業技術插件,并且仍然不能獲得你要的可升級性和高可用性,那么我建議一個可靠的調試策略。策略的第一步是為可能的瓶頸檢查整個體系結構。通常,這在你的作為單線程組件或者有很多輔助連接線組件的UML流圖中很容易識別出來。
      
      最后的步驟是產生一個整個代碼的詳細性能估價。
      
      確保你的管理人員至少為此安排了整個項目時間的20%;否則不足的時間可能不止危及你整個成功的安全,還會導致你向系統引入新的缺點。
      
      許多組織者在適當的位置沒有嚴格意義的測試基礎而歸咎于成本考慮也是錯誤的。確保你的QA環境真實反映你的生產環境,并且你的QA測試考慮以不同的負載測試應用,包括在最大的預期并發用戶時一個基于低負載和一個完全負載的測試。
      
      性能測試,有時測試一個系統的穩定性,可能需要在每天,甚至每周的整個時期的不同關節都運行。


    轉自:http://www.1piao.net/articles/view.asp?p=2006/2/1142260628218
    posted @ 2006-04-03 11:17 hopeshared 閱讀(506) | 評論 (0)編輯 收藏

    僅列出標題
    共30頁: First 上一頁 13 14 15 16 17 18 19 20 21 下一頁 Last 
    主站蜘蛛池模板: 国产91成人精品亚洲精品| 久久久久久a亚洲欧洲aⅴ| 久久久久亚洲精品日久生情| 久久99久久成人免费播放| 免费看的一级毛片| 亚洲一区二区三区久久| 五月亭亭免费高清在线| 亚洲av无码精品网站| 91视频精品全国免费观看| 亚洲人成伊人成综合网久久久| 亚洲日韩在线观看免费视频| 一级毛片直播亚洲| 美女视频黄a视频全免费网站色| 日本一道一区二区免费看 | 成人激情免费视频| 亚洲日本久久一区二区va| 成年女人男人免费视频播放| 久久久久国色AV免费观看| 亚洲深深色噜噜狠狠网站| 日韩一级免费视频| 久久99精品免费视频| 亚洲欧洲国产综合| 最近2019中文免费字幕| 日韩免费精品视频| 亚洲五月激情综合图片区| 最近免费中文字幕mv在线电影| 亚洲av无码成h人动漫无遮挡 | 在线观看亚洲AV日韩A∨| 日本人护士免费xxxx视频| 久久亚洲免费视频| 亚洲国产精品综合久久网各| 亚洲精品A在线观看| 久久精品国产免费| 亚洲精品国产情侣av在线| 成年私人影院免费视频网站| 久久国产乱子伦精品免费不卡| av网站免费线看| 久久精品国产亚洲av天美18| 亚洲精品美女久久777777| 香蕉97超级碰碰碰免费公| 国内精品免费在线观看 |