日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > windows >内容正文

windows

Java程序员应知道的十条Java优化策略,让你的系统健步如飞

發布時間:2024/4/17 windows 39 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java程序员应知道的十条Java优化策略,让你的系统健步如飞 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

1、使用StringBuilder(技術文)

StingBuilder 應該是在我們的Java代碼中默認使用的,應該避免使用 + 操作符?;蛟S你會對 StringBuilder 的語法糖(syntax sugar)持有不同意見,比如:

  • String?x?=?"a"?+?args.length?+?"b";?
  • 將會被編譯為:

  • 0?new?java.lang.StringBuilder?[16]?
  • ?
  • 3?dup?
  • ?
  • 4?ldc?<String?"a">?[18]?
  • ?
  • 6?invokespecial?java.lang.StringBuilder(java.lang.String)?[20]?
  • ?
  • 9?aload_0?[args]?
  • ?
  • 10?arraylength?
  • ?
  • 11?invokevirtual?java.lang.StringBuilder.append(int)?:?java.lang.StringBuilder?[23]?
  • ?
  • 14?ldc?<String?"b">?[27]?
  • ?
  • 16?invokevirtual?java.lang.StringBuilder.append(java.lang.String)?:?java.lang.StringBuilder?[29]?
  • ?
  • 19?invokevirtual?java.lang.StringBuilder.toString()?:?java.lang.String?[32]?
  • ?
  • 22?astore_1?[x]?
  • 但究竟發生了什么?接下來是否需要用下面的部分來對 String 進行改善呢?

  • String?x?=?"a"?+?args.length?+?"b";?
  • ?
  • if?(args.length?==?1)?
  • ?
  • xx?=?x?+?args[0];?
  • 現在使用到了第二個 StringBuilder,這個 StringBuilder 不會消耗堆中額外的內存,但卻給 GC 帶來了壓力。

  • StringBuilder?x?=?new?StringBuilder("a");?
  • ?
  • x.append(args.length);?
  • ?
  • x.append("b");?
  • ?
  • if?(args.length?==?1);?
  • ?
  • x.append(args[0]);?
  • 小結

    在上面的樣例中,如果你是依靠Java編譯器來隱式生成實例的話,那么編譯的效果幾乎和是否使用了 StringBuilder 實例毫無關系。請記住:在 N.O.P.E 分支中,每次CPU的循環的時間到白白的耗費在GC或者為 StringBuilder 分配默認空間上了,我們是在浪費 N x O x P 時間。

    一般來說,使用 StringBuilder 的效果要優于使用 + 操作符。如果可能的話請在需要跨多個方法傳遞引用的情況下選擇 StringBuilder,因為 String 要消耗額外的資源。JOOQ在生成復雜的SQL語句便使用了這樣的方式。在整個抽象語法樹(AST Abstract Syntax Tree)SQL傳遞過程中僅使用了一個 StringBuilder 。

    更加悲劇的是,如果你仍在使用 StringBuffer 的話,那么用 StringBuilder 代替 StringBuffer 吧,畢竟需要同步字符串的情況真的不多。

    2、避免使用正則表達式(好技術文)

    正則表達式給人的印象是快捷簡便。但是在 N.O.P.E 分支中使用正則表達式將是最糟糕的決定。如果萬不得已非要在計算密集型代碼中使用正則表達式的話,至少要將 Pattern 緩存下來,避免反復編譯Pattern。

  • static?final?Pattern?HEAVY_REGEX?=?
  • ?
  • Pattern.compile("(((X)*Y)*Z)*");?
  • 如果僅使用到了如下這樣簡單的正則表達式的話:

  • String[]?parts?=?ipAddress.split("\.");?
  • 這是最好還是用普通的 char[] 數組或者是基于索引的操作。比如下面這段可讀性比較差的代碼其實起到了相同的作用。

  • int?length?=?ipAddress.length();?
  • ?
  • int?offset?=?0;?
  • ?
  • int?part?=?0;?
  • ?
  • for?(int?i?=?0;?i?<?length;?i++)?{?
  • ?
  • if?(i?==?length?-?1?||?
  • ?
  • ipAddress.charAt(i?+?1)?==?'.')?{?
  • ?
  • parts[part]?=?
  • ?
  • ipAddress.substring(offset,?i?+?1);?
  • ?
  • part++;?
  • ?
  • offset?=?i?+?2;?
  • ?
  • }?
  • ?
  • }?
  • 上面的代碼同時表明了過早的優化是沒有意義的。雖然與 split() 方法相比較,這段代碼的可維護性比較差。

    挑戰:聰明的小伙伴能想出更快的算法嗎?歡迎留言。

    小結

    正則表達式是十分有用,但是在使用時也要付出代價。尤其是在 N.O.P.E 分支深處時,要不惜一切代碼避免使用正則表達式。還要小心各種使用到正則表達式的JDK字符串方法,比如 String.replaceAll() 或 String.split()??梢赃x擇用比較流行的開發庫,比如 Apache Commons Lang 來進行字符串操作。

    3、不要使用iterator()方法

    這條建議不適用于一般的場合,僅適用于在 N.O.P.E 分支深處的場景。盡管如此也應該有所了解。Java 5格式的循環寫法非常的方便,以至于我們可以忘記內部的循環方法,比如:

  • for?(String?value?:?strings)?{?
  • ?
  • //?Do?something?useful?here?
  • ?
  • }?
  • 當每次代碼運行到這個循環時,如果 strings 變量是一個 Iterable 的話,代碼將會自動創建一個Iterator 的實例。如果使用的是 ArrayList 的話,虛擬機會自動在堆上為對象分配3個整數類型大小的內存。

  • private?class?Itr?implements?Iterator<E>?{?
  • ?
  • int?cursor;?
  • ?
  • int?lastRet?=?-1;?
  • ?
  • int?expectedModCount?=?modCount;?
  • ?
  • //?...?
  • 也可以用下面等價的循環方式來替代上面的 for 循環,僅僅是在棧上“浪費”了區區一個整形,相當劃算。

  • int?size?=?strings.size();?
  • ?
  • for?(int?i?=?0;?i?<?size;?i++)?{?
  • ?
  • String?value?:?strings.get(i);?
  • ?
  • //?Do?something?useful?here?
  • ?
  • }?
  • 如果循環中字符串的值是不怎么變化,也可用數組來實現循環。

  • for?(String?value?:?stringArray)?{?
  • ?
  • //?Do?something?useful?here?
  • ?
  • }?
  • 小結

    無論是從易讀寫的角度來說,還是從API設計的角度來說迭代器、Iterable接口和 foreach 循環都是非常好用的。但代價是,使用它們時是會額外在堆上為每個循環子創建一個對象。如果循環要執行很多很多遍,請注意避免生成無意義的實例,最好用基本的指針循環方式來代替上述迭代器、Iterable接口和 foreach 循環。

    討論

    一些與上述內容持反對意見的看法(尤其是用指針操作替代迭代器)詳見Reddit上的討論。

    4、不要調用高開銷方法

    有些方法的開銷很大。以 N.O.P.E 分支為例,我們沒有提到葉子的相關方法,不過這個可以有。假設我們的JDBC驅動需要排除萬難去計算 ResultSet.wasNull() 方法的返回值。我們自己實現的SQL框架可能像下面這樣:

  • if?(type?==?Integer.class)?{?
  • ?
  • result?=?(T)?wasNull(rs,?
  • ?
  • Integer.valueOf(rs.getInt(index)));?
  • ?
  • }?
  • ?
  • //?And?then...?
  • ?
  • static?final?<T>?T?wasNull(ResultSet?rs,?T?value)?
  • ?
  • throws?SQLException?{?
  • ?
  • return?rs.wasNull()???null?:?value;?
  • ?
  • }?
  • 在上面的邏輯中,每次從結果集中取得 int 值時都要調用 ResultSet.wasNull() 方法,但是 getInt() 的方法定義為:

    返回類型:變量值;如果SQL查詢結果為NULL,則返回0。

    所以一個簡單有效的改善方法如下:

  • static?final?<T?extends?Number>?T?wasNull(?
  • ?
  • ResultSet?rs,?T?value?
  • ?
  • )?
  • ?
  • throws?SQLException?{?
  • ?
  • return?(value?==?null?||?
  • ?
  • (value.intValue()?==?0?&&?rs.wasNull()))?
  • ?
  • ??null?:?value;?
  • ?
  • }?
  • 這是輕而易舉的事情。

    小結

    將方法調用緩存起來替代在葉子節點的高開銷方法,或者在方法約定允許的情況下避免調用高開銷方法。

    5、使用原始類型和棧

    上面介紹了來自 jOOQ的例子中使用了大量的泛型,導致的結果是使用了 byte、 short、 int 和 long 的包裝類。但至少泛型在Java 10或者Valhalla項目中被專門化之前,不應該成為代碼的限制。因為可以通過下面的方法來進行替換:

  • //存儲在堆上?
  • ?
  • Integer?i?=?817598;?
  • ?
  • ……如果這樣寫的話:?
  • ?
  • //?存儲在棧上?
  • ?
  • int?i?=?817598;?
  • 在使用數組時情況可能會變得更加糟糕:

  • //在堆上生成了三個對象?
  • ?
  • Integer[]?i?=?{?1337,?424242?};?
  • ……如果這樣寫的話:

  • //?僅在堆上生成了一個對象?
  • ?
  • int[]?i?=?{?1337,?424242?};?
  • 小結

    當我們處于 N.O.P.E. 分支的深處時,應該極力避免使用包裝類。這樣做的壞處是給GC帶來了很大的壓力。GC將會為清除包裝類生成的對象而忙得不可開交。

    所以一個有效的優化方法是使用基本數據類型、定長數組,并用一系列分割變量來標識對象在數組中所處的位置。

    遵循LGPL協議的 trove4j 是一個Java集合類庫,它為我們提供了優于整形數組 int[] 更好的性能實現。

    例外

    下面的情況對這條規則例外:因為 boolean 和 byte 類型不足以讓JDK為其提供緩存方法。我們可以這樣寫:

  • Boolean?a1?=?true;?//?...?syntax?sugar?for:?
  • ?
  • Boolean?a2?=?Boolean.valueOf(true);?
  • ?
  • Byte?b1?=?(byte)?123;?//?...?syntax?sugar?for:?
  • ?
  • Byte?b2?=?Byte.valueOf((byte)?123);?
  • 其它整數基本類型也有類似情況,比如 char、short、int、long。

    不要在調用構造方法時將這些整型基本類型自動裝箱或者調用 TheType.valueOf() 方法。

    也不要在包裝類上調用構造方法,除非你想得到一個不在堆上創建的實例。

    6、避免遞歸(真技術文)

    現在,類似Scala這樣的函數式編程語言都鼓勵使用遞歸。因為遞歸通常意味著能分解到單獨個體優化的尾遞歸(tail-recursing)。如果你使用的編程語言能夠支持那是再好不過。不過即使如此,也要注意對算法的細微調整將會使尾遞歸變為普通遞歸。

    希望編譯器能自動探測到這一點,否則本來我們將為只需使用幾個本地變量就能搞定的事情而白白浪費大量的堆??蚣?#xff08;stack frames)。

    小結

    這節中沒什么好說的,除了在 N.O.P.E 分支盡量使用迭代來代替遞歸。

    7、使用entrySet()

    當我們想遍歷一個用鍵值對形式保存的 Map 時,必須要為下面的代碼找到一個很好的理由:

  • for?(K?key?:?map.keySet())?{?
  • ?
  • V?value?:?map.get(key);?
  • ?
  • }?
  • 更不用說下面的寫法:

  • for?(Entry<K,?V>?entry?:?map.entrySet())?{?
  • ?
  • K?key?=?entry.getKey();?
  • ?
  • V?value?=?entry.getValue();?
  • ?
  • }?
  • 在我們使用 N.O.P.E. 分支應該慎用map。因為很多看似時間復雜度為 O(1) 的訪問操作其實是由一系列的操作組成的。而且訪問本身也不是免費的。至少,如果不得不使用map的話,那么要用 entrySet() 方法去迭代!這樣的話,我們要訪問的就僅僅是Map.Entry的實例。

    小結

    在需要迭代鍵值對形式的Map時一定要用 entrySet() 方法。

    8、使用EnumSet或EnumMap(真是技術文)

    在某些情況下,比如在使用配置map時,我們可能會預先知道保存在map中鍵值。如果這個鍵值非常小,我們就應該考慮使用 EnumSet 或 EnumMap,而并非使用我們常用的 HashSet 或 HashMap。下面的代碼給出了很清楚的解釋:

  • private?transient?Object[]?vals;?
  • ?
  • public?V?put(K?key,?V?value)?{?
  • ?
  • //?...?
  • ?
  • int?index?=?key.ordinal();?
  • ?
  • vals[index]?=?maskNull(value);?
  • ?
  • //?...?
  • ?
  • }?
  • 上段代碼的關鍵實現在于,我們用數組代替了哈希表。尤其是向map中插入新值時,所要做的僅僅是獲得一個由編譯器為每個枚舉類型生成的常量序列號。如果有一個全局的map配置(例如只有一個實例),在增加訪問速度的壓力下,EnumMap 會獲得比 HashMap 更加杰出的表現。原因在于 EnumMap 使用的堆內存比 HashMap 要少 一位(bit),而且 HashMap 要在每個鍵值上都要調用 hashCode() 方法和 equals() 方法。

    小結

    Enum 和 EnumMap 是親密的小伙伴。在我們用到類似枚舉(enum-like)結構的鍵值時,就應該考慮將這些鍵值用聲明為枚舉類型,并將之作為 EnumMap 鍵。

    9、優化自定義hasCode()方法和equals()方法(技術好文)

    在不能使用EnumMap的情況下,至少也要優化 hashCode() 和 equals() 方法。一個好的 hashCode() 方法是很有必要的,因為它能防止對高開銷 equals() 方法多余的調用。

    在每個類的繼承結構中,需要容易接受的簡單對象。讓我們看一下jOOQ的 org.jooq.Table 是如何實現的?

    最簡單、快速的 hashCode() 實現方法如下:

  • //?AbstractTable一個通用Table的基礎實現:?
  • ?
  • @Override?
  • ?
  • public?int?hashCode()?{?
  • ?
  • //?[#1938]?與標準的QueryParts相比,這是一個更加高效的hashCode()實現?
  • ?
  • return?name.hashCode();?
  • ?
  • }?
  • name即為表名。我們甚至不需要考慮schema或者其它表屬性,因為表名在數據庫中通常是唯一的。并且變量 name 是一個字符串,它本身早就已經緩存了一個 hashCode() 值。

    這段代碼中注釋十分重要,因繼承自 AbstractQueryPart 的 AbstractTable 是任意抽象語法樹元素的基本實現。普通抽象語法樹元素并沒有任何屬性,所以不能對優化 hashCode() 方法實現抱有任何幻想。覆蓋后的 hashCode() 方法如下:

  • //?AbstractQueryPart一個通用抽象語法樹基礎實現:?
  • ?
  • @Override?
  • ?
  • public?int?hashCode()?{?
  • ?
  • //?這是一個可工作的默認實現。?
  • ?
  • //?具體實現的子類應當覆蓋此方法以提高性能。?
  • ?
  • return?create().renderInlined(this).hashCode();?
  • ?
  • }?
  • 換句話說,要觸發整個SQL渲染工作流程(rendering workflow)來計算一個普通抽象語法樹元素的hash代碼。

    equals() 方法則更加有趣:

    // AbstractTable通用表的基礎實現:

  • @Override??
  • ??
  • public?boolean?equals(Object?that)?{??
  • ??
  • if?(this?==?that)?{??
  • ??
  • return?true;??
  • ??
  • }??
  • ??
  • //?[#2144]?在調用高開銷的AbstractQueryPart.equals()方法前,??
  • ??
  • //?可以及早知道對象是否不相等。??
  • ??
  • if?(that?instanceof?AbstractTable)?{??
  • ??
  • if?(StringUtils.equals(name,??
  • ??
  • (((AbstractTable<?>)?that).name)))?{??
  • ??
  • return?super.equals(that);??
  • ??
  • }??
  • ??
  • return?false;??
  • ??
  • }??
  • ??
  • return?false;??
  • ??
  • }??

  • 首先,不要過早使用 equals() 方法(不僅在N.O.P.E.中),如果:

    • this == argument

    • this“不兼容:參數

    注意:如果我們過早使用 instanceof 來檢驗兼容類型的話,后面的條件其實包含了argument == null。

    在我們對以上幾種情況的比較結束后,應該能得出部分結論。比如jOOQ的 Table.equals() 方法說明是,用來比較兩張表是否相同。不論具體實現類型如何,它們必須要有相同的字段名。比如下面兩個元素是不可能相同的:

    • com.example.generated.Tables.MY_TABLE

    • DSL.tableByName(“MY_OTHER_TABLE”)

    如果我們能方便地判斷傳入參數是否等于實例本身(this),就可以在返回結果為 false 的情況下放棄操作。如果返回結果為 true,我們還可以進一步對父類(super)實現進行判斷。在比較過的大多數對象都不等的情況下,我們可以盡早結束方法來節省CPU的執行時間。

    一些對象的相似度比其它對象更高。

    在jOOQ中,大多數的表實例是由jOOQ的代碼生成器生成的,這些實例的 equals() 方法都經過了深度優化。而數十種其它的表類型(衍生表 (derived tables)、表值函數(table-valued functions)、數組表(array tables)、連接表(joined tables)、數據透視表(pivot tables)、公用表表達式(common table expressions)等,則保持 equals() 方法的基本實現。

    10、考慮使用set而并非單個元素(技術文)

    最后,還有一種情況可以適用于所有語言而并非僅僅同Java有關。除此以外,我們以前研究的 N.O.P.E. 分支也會對了解從 O(N3) 到 O(n log n)有所幫助。

    不幸的是,很多程序員的用簡單的、本地算法來考慮問題。他們習慣按部就班地解決問題。這是命令式(imperative)的“是/或”形式的函數式編程風格。這種編程風格在由純粹命令式編程向面對象式編程向函數式編程轉換時,很容易將“更大的場景(bigger picture)”模型化,但是這些風格都缺少了只有在SQL和R語言中存在的:

    聲明式編程。

    在SQL中,我們可以在不考慮算法影響下聲明要求數據庫得到的效果。數據庫可以根據數據類型,比如約束(constraints)、鍵(key)、索引(indexes)等不同來采取最佳的算法。

    在理論上,我們最初在SQL和關系演算(relational calculus)后就有了基本的想法。在實踐中,SQL的供應商們在過去的幾十年中已經實現了基于開銷的高效優化器CBOs (Cost-Based Optimisers) 。然后到了2010版,我們才終于將SQL的所有潛力全部挖掘出來。

    但是我們還不需要用set方式來實現SQL。所有的語言和庫都支持Sets、collections、bags、lists。使用set的主要好處是能使我們的代碼變的簡潔明了。比如下面的寫法:

  • SomeSet?INTERSECT?SomeOtherSet?
  • 而不是

  • //?Java?8以前的寫法?
  • ?
  • Set?result?=?new?HashSet();?
  • ?
  • for?(Object?candidate?:?someSet)?
  • ?
  • if?(someOtherSet.contains(candidate))?
  • ?
  • result.add(candidate);?
  • ?
  • //?即使采用Java?8也沒有很大幫助?
  • ?
  • someSet.stream()?
  • ?
  • .filter(someOtherSet::contains)?
  • ?
  • .collect(Collectors.toSet());?
  • 有些人可能會對函數式編程和Java 8能幫助我們寫出更加簡單、簡潔的算法持有不同的意見。但這種看法不一定是對的。我們可以把命令式的Java 7循環轉換成Java 8的Stream collection,但是我們還是采用了相同的算法。但SQL風格的表達式則是不同的:

  • SomeSet?INTERSECT?SomeOtherSet?
  • 上面的代碼在不同的引擎上可以有1000種不同的實現。我們今天所研究的是,在調用 INTERSECT 操作之前,更加智能地將兩個set自動的轉化為 EnumSet 。甚至我們可以在不需要調用底層的 Stream.parallel() 方法的情況下進行并行 INTERSECT 操作。

    總結

    在這篇文章中,我們討論了關于N.O.P.E.分支的優化。比如深入高復雜性的算法。作為jOOQ的開發者,我們很樂于對SQL的生成進行優化。

    • 每條查詢都用唯一的StringBuilder來生成。

    • 模板引擎實際上處理的是字符而并非正則表達式。

    • 選擇盡可能的使用數組,尤其是在對監聽器進行迭代時。

    • 對JDBC的方法敬而遠之。

    • 等等。

    jOOQ處在“食物鏈的底端”,因為它是在離開JVM進入到DBMS時,被我們電腦程序所調用的最后一個API。位于食物鏈的底端意味著任何一條線路在jOOQ中被執行時都需要 N x O x P 的時間,所以我要盡早進行優化。

    我們的業務邏輯可能沒有N.O.P.E.分支那么復雜。但是基礎框架有可能十分復雜(本地SQL框架、本地庫等)。所以需要按照我們今天提到的原則,用Java Mission Control 或其它工具進行復查,確認是否有需要優化的地方。 ?


    原文發布時間為:2017-10-31?

    本文作者:佚名

    本文來自云棲社區合作伙伴“51CTO”,了解相關信息可以關注。

    總結

    以上是生活随笔為你收集整理的Java程序员应知道的十条Java优化策略,让你的系统健步如飞的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。