您好,登錄后才能下訂單哦!
本篇文章為大家展示了怎么在java中實現內存化和函數式協同,內容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。
內存化
內存化 這個詞是 Donald Michie(一位英國人工智能研究人員)發明的,用于表示重復的值的函數級緩存。如今,內存化在函數式編程語言中很常見,它要么被用作一個內置特性,要么被用作一個相對容易實現的特性。
內存化在以下場景中很有幫助。假設您必須反復調用一個注重性能的函數。一個常見解決方案是構建內部緩存。每次計算某個參數集的值時,您都會將該值放入緩存中,作為參數值的線索。在未來,如果該函數使用以前的參數調用,那么它將會從緩存返回值,而不是重新計算它。函數緩存是一種經典的計算機科學權衡:它使用更多內存(我們常常擁有豐富的內存)來不斷實現更高的性能。
函數必須是純粹的,緩存技術才能發揮其作用。純函數 是沒有副作用的函數:它沒有引用任何其他易變的類字段,沒有設置除返回值以外的任何值,而且僅依賴于參數作為輸入。java.lang.Math 類中的所有方法都是純函數的良好示例。顯然,只有在函數可靠地為一組給定的參數返回相同值時,您才能成功地重用緩存的結果。
Groovy 中的內存化
內存化在 Groovy 中很簡單,Groovy 在 Closure 類上包含一系列 memoize() 函數。例如,假設您有一個昂貴的哈希算法,以至于您需要緩存結果來提高效率。為此,您可以使用閉包塊語法來定義方法,在返回時調用 memoize() 函數,如清單 1 所示。我并不是暗示清單 1 中使用的 ROT13 算法(即凱撒密碼 的一個版本)的性能面臨挑戰,只是假設緩存在這個示例中很重要。
清單 1. Groovy 中的內存化
class NameHash { def static hash = {name -> name.collect{rot13(it)}.join() }.memoize() public static char rot13(s) { char c = s switch (c) { case 'A'..'M': case 'a'..'m': return c + 13 case 'N'..'Z': case 'n'..'z': return c - 13 default: return c } } } class NameHashTest extends GroovyTestCase { void testHash() { assertEquals("ubzre", NameHash.hash.call("homer")) } }
正常情況下,Groovy 函數定義看起來像清單 1 中的 rot13(),方法主體位于參數列表之后。hash() 函數定義使用了稍微不同的語法,將代碼塊分配給 hash 變量。該定義的最后一部分是對 memoize() 的調用,它自動為重復的值創建一個內部緩存,與該參數建立聯系。
memoize() 方法實際上是一個方法系列,為您提供了對緩存特征的一定控制,如表 1 所示。
表 1. Groovy 的 memoize() 系列
方法 | 用途 |
---|---|
memoize() | 返回閉包的一個包含緩存的實例 |
memoizeAtMost() | 為緩存元素的數量設置一個上限 |
memoizeAtLeast(int protectedCacheSize) | 為緩存元素的數量設置一個下限,保護一定數量的元素免遭垃圾收集 |
memoizeBetween(int protectedCacheSize, int maxCacheSize) | 為緩存元素的數量設置一個下限和上限 |
表 1 中的方法為您提供了對緩存特征粗粒度的控制,這不是直接調優緩存特征的細粒度方式。內存化應是一種通用機制,您可以用它來輕松優化常見的緩存情形。
Clojure 中的內存化
內置于 Clojure 中的內存化。您可以使用 (memoize ) 函數內存化任何函數。例如,如果您已經擁有一個 (hash "homer") 函數,那么您可以通過 (memoize (hash "homer")) 針對一個緩存版本而對其進行內存化。清單 2 在 Clojure 中實現了 清單 1 中的名稱哈希示例。
清單 2. Clojure 內存化
(defn name-hash [name] (apply str (map #(rot13 %) (split name #"\d")))) (def name-hash-m (memoize name-hash)) (testing "name hash" (is (= "ubzre" (name-hash "homer")))) (testing "memoized name hash" (is (= "ubzre" (name-hash-m "homer")))))
請注意,在 清單 1 中,調用內存化的函數需要調用 call() 方法。在 Clojure 版本中,內存化的方法調用在表面上完全相同,但增加了對方法用戶不可見的間接性和緩存。
Scala 中的內存化
Scala 沒有直接實現內存化,但有一個名為 getOrElseUpdate() 的集合方法來處理實現它的大部分工作,如清單 3 所示。
清單 3. Scala 內存化
def memoize[A, B](f: A => B) = new (A => B) { val cache = scala.collection.mutable.Map[A, B]() def apply(x: A): B = cache.getOrElseUpdate(x, f(x)) } def nameHash = memoize(hash)
清單 3 中的 getOrElseUpdate() 函數是建立緩存的完美的運算符。它檢索匹配的值,或者在沒有匹配值時創建一個新條目。
組合函數特性
通過復合 (composition) 來組合
復合 在軟件開發中有許多含義。函數復合 指組合函數來獲得復合結果的能力。在數學術語中,如果您有一個 f(x) 函數和一個 g(x) 函數,那么您應能夠執行 f(g(x))。在軟件術語中,如果您有一個將字符串轉換為大寫的 a() 函數和一個刪除過量空格的 b() 函數,那么復合函數將執行這兩項任務。
在上一節和前幾期 Java 下一代 文章中,我介紹了函數式編程的許多細節,尤其是與 Java 下一代語言相關的細節。但是,函數式編程的真正強大之處在于各種特性與解決方案的執行方式的組合。
面向對象的程序員傾向于不斷創建新數據結構和附帶的運算。畢竟,構建新類和在它們之間傳遞的消息是主要的語言模式。但是構建如此多的定制結構,會使在最低層級上構建可重用代碼變得很困難。函數式編程語言引用一些核心代碼結構并構建優化的機制來理解它們。
以下是一個示例。清單 4 給出了來自 Apache Commons 框架的 indexOfAny() 方法,該框架為 Java 編程提供了大量幫助器。
清單 4. 來自 Apache Commons 的 indexOfAny()
// From Apache Commons Lang, http://commons.apache.org/lang/ public static int indexOfAny(String str, char[] searchChars) { if (isEmpty(str) || ArrayUtils.isEmpty(searchChars)) { return INDEX_NOT_FOUND; } int csLen = str.length(); int csLast = csLen - 1; int searchLen = searchChars.length; int searchLast = searchLen - 1; for (int i = 0; i < csLen; i++) { char ch = str.charAt(i); for (int j = 0; j < searchLen; j++) { if (searchChars[j] == ch) { if (i < csLast && j < searchLast && CharUtils.isHighSurrogate(ch)) { if (searchChars[j + 1] == str.charAt(i + 1)) { return i; } } else { return i; } } } } return INDEX_NOT_FOUND; }
清單 4 中 1/3 的代碼負責邊緣檢查和實現嵌套迭代所需的變量的初始化。我將逐步將此代碼轉換為 Clojure。作為第一步,我將刪除邊角情形,如清單 5 所示。
清單 5. 刪除邊角情形
public static int indexOfAny(String str, char[] searchChars) { when(searchChars) { int csLen = str.length(); int csLast = csLen - 1; int searchLen = searchChars.length; int searchLast = searchLen - 1; for (int i = 0; i < csLen; i++) { char ch = str.charAt(i); for (int j = 0; j < searchLen; j++) { if (searchChars[j] == ch) { if (i < csLast && j < searchLast && CharUtils.isHighSurrogate(ch)) { if (searchChars[j + 1] == str.charAt(i + 1)) { return i; } } else { return i; } } } } return INDEX_NOT_FOUND; } }
Clojure 會智能地處理 null 和 empty 情形,擁有 (when ...) 等智能函數,該函數僅在字符存在時返回 true。Clojure 具有動態(且強)類型,消除了在使用前聲明變量類型的需求。因此,我可以刪除類型聲明,獲得清單 6 中所示的代碼。
清單 6. 刪除類型聲明
indexOfAny(str, searchChars) { when(searchChars) { csLen = str.length(); csLast = csLen - 1; searchLen = searchChars.length; searchLast = searchLen - 1; for (i = 0; i < csLen; i++) { ch = str.charAt(i); for (j = 0; j < searchLen; j++) { if (searchChars[j] == ch) { if (i < csLast && j < searchLast && CharUtils.isHighSurrogate(ch)) { if (searchChars[j + 1] == str.charAt(i + 1)) { return i; } } else { return i; } } } } return INDEX_NOT_FOUND; } }
for 循環 (命令式語言的主要元素)允許依次訪問每個元素。函數式語言傾向于更多地依靠集合方法,這些方法已理解(或避免)了邊角情形,所以我可刪除 isHighSurrogate()(它檢查字符編碼)等方法和索引指針的操作。此轉換的結果如清單 7 所示。
清單 7. 一個用于替換最里面的 for 的 when 子句
// when clause for innermost for indexOfAny(str, searchChars) { when(searchChars) { csLen = str.length(); for (i = 0; i < csLen; i++) { ch = str.charAt(i); when (searchChars(ch)) i; } } }
在清單 7 中,我將代碼折疊到一個方法中,該方法會檢查受歡迎的字符是否存在,在找到這些字符時,它會返回其索引。盡管我既未使用 Java 也未使用 Clojure,而是提供了一段陌生的偽代碼,但這個 when 方法并不總是存在。但 Clojure 中還有 (when ) 方法,此代碼會慢慢變成該方法。
接下來,我將最頂層的 for 循環替換為一種更加簡潔的代碼,使用 for comprehension: 一個結合了集合的訪問和過濾(等)的宏。演變后的代碼如清單 8 所示。
清單 8. 添加一個 comprehension
// add comprehension indexOfAny(str, searchChars) { when(searchChars) { for ([i, ch] in indexed(str)) { when (searchChars(ch)) i; } } }
要理解清單 8 中的 for comprehension,首先您必須理解一些部件。Clojure 中的 (indexed ...) 函數接受一個 Sequence
并返回一個包含編號的元素的序列。例如,如果我調用 (indexed '(a b c)),返回值為 ([0 a] [1 b] [2 c])。(單個撇號告訴 Clojure,我想要一個字符的文字序列,但并不希望執行一個包含兩個參數的 (a )。)for comprehension 在我的搜索字符上創建這個序列,然后應用內部的 when 來查找匹配字符的索引。
此轉換的最后一步是將代碼轉換為合適的 Clojure 語法,還原真實函數和語法的外觀,如清單 9 所示。
清單 9. Clojure 化的代碼
// Clojure-ify (defn index-filter [pred coll] (when pred (for [[index element] (indexed coll) :when (pred element)] index)))
在清單 9 中的最終的 Clojure 版本中,我將語法轉換為合適的 Clojure 并添加一次升級:此函數的調用方現在可傳遞任何判定函數(一個返回布爾值結果的函數),而不只是檢查一個空字符串。Clojure 的一個目標是實現創建可讀的代碼的能力(在您理解圓括號之后),而且這個函數證實了這種能力:對于帶索引的集合,在您的判定與元素匹配時,將會返回索引。
Clojure 的另一個目標是使用最少量的字符來表達清楚目的;在這方面,Java 與 Clojure 相差甚遠。表 2 比較了 清單 4 中的 “移動部件” 和 清單 9 中的相應部件。
表 2.比較 “移動部件”
要求 | 函數 | |
---|---|---|
函數 | 1 | 1 |
類 | 1 | 0 |
內部退出點 | 2 | 0 |
變量 | 3 | 0 |
分支 | 4 | 0 |
布爾運算符 | 1 | 0 |
函數調用 | 6 | 3 |
總計 | 18 | 4 |
復雜性上的差異一目了然。盡管 Clojure 代碼更簡單,但它也更加通用。這里,我對一個硬幣翻轉序列建立了索引,建模為 Clojure :h(頭)和 :t(尾)關鍵字:
(index-filter #{:h} [:t :t :h :t :h :t :t :t :h :h]) -> (2 4 8 9)
請注意,返回值是所有匹配的索引位置的序列,而不只是第一個。Clojure 中的列表操作盡可能是 惰性的,包括這一個操作。如果我僅想要第一個值,那么我可以通過 (take 1 ) 從結果中獲得該值,或者我可以全部打印它們,就像我在這里所做的那樣。
我的 (index-filter ) 函數是通用的,所以我可在數字上使用它。例如,我可確定其斐波納契值超過 1,000 的第一個數字:
(first (index-filter #(> % 1000) (fibo))) -> 17
(fibo) 函數返回一個沒有限制但惰性的斐波納契數字序列;(index-filter ) 找到第一個超過 1,000 的值。(事實證明,18 的斐波納契值為 1,597。)函數結構、動態類型、惰性和簡潔語法相結合,得到的是更強大的功能。
惰性
惰性 — 盡可能延遲表達式計算 — 是函數式語言在不需要或只需很少開發人員成本的情況下添加功能的另一個優秀示例。請參閱 “函數式思維:探索 Java 中的惰性計算” 和 “函數式思維:深入剖析惰性計算”,了解 Java 下一代語言中的惰性討論和示例。
Java的基本數據類型分為:1、整數類型,用來表示整數的數據類型。2、浮點類型,用來表示小數的數據類型。3、字符類型,字符類型的關鍵字是“char”。4、布爾類型,是表示邏輯值的基本數據類型。
上述內容就是怎么在java中實現內存化和函數式協同,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。