亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

Java 8怎么了之二:函數(shù)和原語

asoren / 3556人閱讀

摘要:本文主要介紹了中的函數(shù)與原語,由國內(nèi)管理平臺編譯呈現(xiàn)。原語與對象語言毫無關系。對象函數(shù)有個方法叫,返回數(shù)字化原語的方法被稱為,或。你可以創(chuàng)建函數(shù)的特殊形式,使用原語,而不是對象。

【編者按】本文作者為專注于自然語言處理多年的 Pierre-Yves Saumont,Pierre-Yves 著有30多本主講 Java 軟件開發(fā)的書籍,自2008開始供職于 Alcatel-Lucent 公司,擔任軟件研發(fā)工程師。

本文主要介紹了 Java 8 中的函數(shù)與原語,由國內(nèi) ITOM 管理平臺 OneAPM 編譯呈現(xiàn)。

Tony Hoare 把空引用的發(fā)明稱為“億萬美元的錯誤”。也許在 Java 中使用原語可以被稱為“百萬美元的錯誤”。創(chuàng)造原語的原因只有一個:性能。原語與對象語言毫無關系。引入自動裝箱和拆箱是件好事,不過還有很多有待發(fā)展??赡芤院髸崿F(xiàn)(據(jù)說已經(jīng)列入 Java 10的發(fā)展藍圖)。與此同時,我們需要對付原語,這可是個麻煩,尤其是在使用函數(shù)的時候。

Java 5/6/7的函數(shù)

在 Java 8之前,使用者可以創(chuàng)建下面這樣的函數(shù):

public interface Function {   
   U apply(T t); 
   }  
Function addTax = new Function() {  
 @Override   
 public Integer apply(Integer x) {    
 return x / 100 * (100 + 10);   } 
  }; 
 System.out.println(addTax.apply(100));

這些代碼會產(chǎn)生以下結(jié)果:

110

Java 8 帶來了 Function接口和 lambda 語法。我們不再需要界定自己的功能接口, 而且可以使用下面這樣的語法:

Function addTax = x -> x / 100 * (100 + 10);  
System.out.println(addTax.apply(100));

注意在第一個例子中,筆者用了一個匿名類文件來創(chuàng)建一個命名函數(shù)。在第二個例子中,使用 lambda 語法對結(jié)果并沒有任何影響。依然存在匿名類文件, 和一個命名函數(shù)。

一個有意思的問題是:“x 是什么類型?”第一個例子中的類型很明顯??梢愿鶕?jù)函數(shù)類型推斷出來。Java 知道函數(shù)參數(shù)類型是 Integer,因為函數(shù)類型明顯是 Function。第一個 Integer 是參數(shù)的類型,第二個 Integer 是返回類型。

裝箱被自動用于按照需要將 intInteger 來回轉(zhuǎn)換。下文會詳談這一點。

可以使用匿名函數(shù)嗎?可以,不過類型就會有問題。這樣行不通:

System.out.println((x -> x / 100 * (100 + 10)).apply(100));

這意味著我們無法用標識符的值來替代標識符 addTax 本身( addTax 函數(shù))。在本案例中,需要恢復現(xiàn)在缺失的類型信息,因為 Java 8 無法推斷類型。

最明顯缺乏類型的就是標識符 x??梢宰鲆韵聡L試:

System.out.println((Integer x) -> x / 100 * 100 + 10).apply(100));

畢竟在第一個例子中,本可以這樣寫:

Function addTax = (Integer x) -> x / 100 * 100 + 10;

這樣應該足夠讓 Java 推測類型,但是卻沒有成功。需要做的是明確函數(shù)的類型。明確函數(shù)參數(shù)的類型并不夠,即使已經(jīng)明確了返回類型。這么做還有一個很嚴肅的原因:Java 8對函數(shù)一無所知??梢哉f函數(shù)就是普通對象加上普通方法,僅此而已。因此需要像下面這樣明確類型:

System.out.println(((Function) x -> x / 100 * 100 + 10).apply(100));

否則,就會被解讀為:

System.out.println(((Whatever) x -> x / 100 * 100 + 10).whatever(100));

因此 lambda 只是在語法上起到簡化匿名類在 Function(或 Whatever)接口執(zhí)行的作用。它實際上跟函數(shù)毫不相關。

假設 Java 只有 apply 方法的 Function 接口,這就不是個大問題。但是原語怎么辦呢?如果 Java 只是對象語言,Function 接口就沒關系??墒撬皇恰K皇悄:孛嫦?qū)ο蟮氖褂茫ㄒ虼吮环Q為面向?qū)ο螅?。Java 中最重要的類別是原語,而原語與面向?qū)ο缶幊倘诤系貌⒉缓谩?/p>

Java 5 中引入了自動裝箱,來協(xié)助解決這個問題,但是自動裝箱對性能產(chǎn)生了嚴重限制,這還關系到 Java 如何求值。Java 是一種嚴格的語言,遵循立即求值規(guī)則。結(jié)果就是每次有原語需要對象,都必須將原語裝箱。每次有對象需要原語,都必須將對象拆箱。如果依賴自動裝箱和拆箱,可能會產(chǎn)生多次裝箱和拆箱的大量開銷。

其他語言解決這個問題的方法有所不同,只允許對象,在后臺解決了轉(zhuǎn)化問題。他們可能會有“值類”,也就是受到原語支持的對象。在這種功能下,程序員只使用對象,編譯器只使用原語(描述過于簡化,不過反映了基本原則)。Java 允許程序員直接控制原語,這就增大了問題難度,帶來了更多安全隱患,因為程序員被鼓勵將原語用作業(yè)務類型,這在面向?qū)ο缶幊袒蚝瘮?shù)式程序設計中都沒有意義。(筆者將在另一篇文章中再談這個問題。)

不客氣地說,我們不應該擔心裝箱和拆箱的開銷。如果帶有這種特性的 Java 程序運行過慢,這種編程語言就應該進行修復。我們不應該試圖用糟糕的編程技巧來解決語言本身的不足。使用原語會讓這種語言與我們作對,而不是為我們所用。如果問題不能通過修復語言來解決,那我們就應該換一種編程語言。不過也許不能這樣做,原因有很多,其中最重要的一條是只有 Java 付錢讓我們編程,其他語言都沒有。結(jié)果就是我們不是在解決業(yè)務問題,而是在解決 Java 的問題。使用原語正是 Java 的問題,而且問題還不小。

現(xiàn)在不用對象,用原語來重寫例子。選取的函數(shù)采用類型 Integer 的參數(shù),返回 Integer。要取代這些,Java 有 IntUnaryOperator 類型。哇哦,這里不對勁兒!你猜怎么著,定義如下:

public interface IntUnaryOperator {  
 int applyAsInt(int operand);  
  ...
   }

這個問題太簡單,不值得調(diào)出方法 apply。

因此,使用原語重寫例子如下:

IntUnaryOperator addTax = x -> x / 100 * (100 + 10); 
System.out.println(addTax.applyAsInt(100));

或者采用匿名函數(shù):

System.out.println(((IntUnaryOperator) x -> x / 100 * (100 + 10)).applyAsInt(100));

如果只是為了 int 返回 int 的函數(shù),很容易實現(xiàn)。不過實際問題要更加復雜。Java 8 的 java.util.function 包中有43種(功能)接口。實際上,它們不全都代表功能,可以分類如下:

21個帶有一個參數(shù)的函數(shù),其中2個為對象返回對象的函數(shù),19個為各種類型的對象到原語或原語到對象函數(shù)。2個對象到對象函數(shù)中的1個用于參數(shù)和返回值屬于相同類型的特殊情況。

9個帶有2個參數(shù)的函數(shù),其中2個為(對象,對象)到對象,7個為各種類型的(對象,對象)到原語或(原語,原語)到原語。

7個為效果,非函數(shù),因為它們并不返回任何值,而且只被用于獲取副作用。(把這些稱為“功能接口”有些奇怪。)

5個為“供應商”,意思就是這些函數(shù)不帶參數(shù),卻會返回值。這些可以是函數(shù)。在函數(shù)世界里,有些特殊函數(shù)被稱為無參函數(shù)(表明它們的元數(shù)或函數(shù)總量為0)。作為函數(shù),它們返回的值可能永遠不變,因此它們允許將常量當做函數(shù)。在
Java 8,它們的職責是根據(jù)可變語境來返回各種值。因此,它們不是函數(shù)。

真是太亂了!而且這些接口的方法有不同的名字。對象函數(shù)有個方法叫 apply,返回數(shù)字化原語的方法被稱為 applyAsInt、applyAsLong,或 applyAsDouble。返回 boolean 的函數(shù)有個方法被稱為 test,供應商的方法叫做 getgetAsInt、getAsLong、 getAsDouble,或 getAsBoolean。(他們沒敢把帶有 test 方法、不帶函數(shù)的 BooleanSupplier 稱為“謂語”。筆者真的很好奇為什么!)

值得注意的一點,是并沒有對應 bytechar、 shortfloat 的函數(shù),也沒有對應兩個以上元數(shù)的函數(shù)。

不用說,這樣真是太荒謬了,然而我們又不得不堅持下去。只要 Java 能推斷類型,我們就會覺得一切順利。然而,一旦試圖通過功能方式控制函數(shù),你將會很快面對 Java 無法推斷類型的難題。最糟糕的是,有時候 Java 能夠推斷類型,卻會保持沉默,繼續(xù)使用另外一個類型,而不是我們想用的那一個。

如何發(fā)現(xiàn)正確類型

假設筆者想使用三個參數(shù)的函數(shù)。由于 Java 8沒有現(xiàn)成可用的功能接口,筆者只有一個選擇:創(chuàng)建自己的功能接口,或者如前文(Java 8 怎么了之一)中所說,采取柯里化。創(chuàng)建三個對象參數(shù)、并返回對象的功能接口直截了當:

interface Function {  
 R apply(T, t, U, u, V, v); 
 }

不過,可能出現(xiàn)兩種問題。第一種,可能需要處理原語。參數(shù)類型也幫不上忙。你可以創(chuàng)建函數(shù)的特殊形式,使用原語,而不是對象。最后,算上8類原語、3個參數(shù)和1個返回值,只不過得到6561中該函數(shù)的不同版本。你以為甲骨文公司為什么沒有在 Java 8中包含 TriFunction?(準確來說,他們只放了有限數(shù)量的 BiFunction,參數(shù)為 Object,返回類型為 int、longdouble,或者參數(shù)和返回類型同為 int、long 或 Object,產(chǎn)生729種可能性中的9種結(jié)果。)

更好的解決辦法是使用拆箱。只需要使用 IntegerLong、Boolean 等等,接下來就讓 Java 去處理。任何其他行動都會成為萬惡之源,例如過早優(yōu)化(詳見 http://c2.com/cgi/wiki?PrematureOptimization)。

另外一個辦法(除了創(chuàng)建三個參數(shù)的功能接口之外)就是采取柯里化。如果參數(shù)不在同一時間求值,就會強制柯里化。而且它還允許只用一種參數(shù)的函數(shù),將可能的函數(shù)數(shù)量限制在81之內(nèi)。如果只使用 boolean、intlongdouble,這個數(shù)字就會降到25(4個原語類型加上兩個位置的 Object 相當于5 x 5)。

問題在于在對返回原語,或?qū)⒃Z作為參數(shù)的函數(shù)來說,使用柯里化可能有些困難。以下是前文(Java 8怎么了之一)中使用的同一例子,不過現(xiàn)在用了原語:

IntFunction> 
   intToIntCalculation = x -> y -> z -> x + y * z;  
   private IntStream calculate(IntStream stream, int a) {   
      return stream.map(intToIntCalculation.apply(b).apply(a)); 
      }  
      
    IntStream stream = IntStream.of(1, 2, 3, 4, 5); 
    IntStream newStream = calculate(stream, 3);

注意結(jié)果不是“包含值5、8、11、14和17的流”,一開始的流也不會包含值1、2、3、4和5。newStream 在這個階段并沒有求值,因此不包含值。(下篇文章將討論這個問題)。

為了查看結(jié)果,就要對這個流求值,也許通過綁定一個終端操作來強制執(zhí)行。可以通過調(diào)用 collect 方法。不過在這個操作之前,筆者要利用 boxed 方法將結(jié)果與一個非終端函數(shù)綁定在一起。boxed 方法將流與一個能夠把原語轉(zhuǎn)為對應對象的函數(shù)綁定在一起。這可以簡化求值過程:

System.out.println(newStream.boxed().collect(toList()));

這顯示為:

[5,8, 11, 14, 17]

也可以使用匿名函數(shù)。不過,Java 不能推斷類型,所以筆者必須提供協(xié)助:

private IntStream calculate(IntStream stream, int a) {   
  return stream.map(((IntFunction>) x -> y -> z -> x + y * z).apply(b).apply(a)); 
  }  
  
  IntStream stream = IntStream.of(1, 2, 3, 4, 5); 
  IntStream newStream = calculate(stream, 3);

柯里化本身很簡單,只要別忘了筆者在其他文章中提到過的一點:

(x, y, z) -> w

解讀為:

x -> y -> z -> w

尋找正確類型稍微復雜一些。要記住,每次使用一個參數(shù),都會返回一個函數(shù),因此你需要一個從參數(shù)類型到對象類型的函數(shù)(因為函數(shù)就是對象)。在本例中,每個參數(shù)類型都是 int,因此需要使用經(jīng)過返回函數(shù)類型參數(shù)化的 IntFunction。由于最終類型為 IntUnaryOperator(這是 IntStream 類的 map 方法的要求),結(jié)果如下:

IntFunction>>

筆者采用了三個參數(shù)中的兩種,所有參數(shù)類型都是 int ,因此類型如下:

IntFunction>

可以與使用自動裝箱版本進行比較:

Function>>

如果你無法決定正確類型,可以從使用自動裝箱開始,只要替換上你需要的最終類型(因為它就是 map 參數(shù)的類型):

Function>

注意,你可能正好在你的程序中使用了這種類型:

private IntStream calculate(IntStream stream, int a) {   
    return stream.map(((Function>) x -> y -> z -> x + y * z).apply(b).apply(a)); 
    }  
    
    IntStream stream = IntStream.of(1, 2, 3, 4, 5); 
    IntStream newStream = calculate(stream, 3);

接下來可以用你使用的原語版本來替換每個 Function,如下所示:

private IntStream calculate(IntStream stream, int a) {   
   return stream.map(((Function>) x -> y -> z -> x + y * z).apply(b).apply(a)); }

然后是:

private IntStream calculate(IntStream stream, int a) {   return stream.map(((IntFunction>) x -> y -> z -> x + y * z).apply(b).apply(a)); }

注意,三個版本都可編譯運行,唯一的區(qū)別在于是否使用了自動裝箱。

何時匿名
在以上例子中可見,lambdas 很擅長簡化匿名類的創(chuàng)建,但是不給創(chuàng)建的范例命名實在沒有理由。命名函數(shù)的用處包括:

函數(shù)復用

函數(shù)測試

函數(shù)替換

程序維護

程序文檔管理

命名函數(shù)加上柯里化能夠讓函數(shù)完全獨立于環(huán)境(“引用透明性”),讓程序更安全、更模塊化。不過這也存在難度。使用原語增加了辨別柯里化函數(shù)類別的難度。更糟糕的是,原語并不是可使用的正確業(yè)務類型,因此編譯器也幫不上忙。具體原因請看以下例子:

double tax = 10.24; 
double limit = 500.0; 
double delivery = 35.50; 
DoubleStream stream3 = DoubleStream.of(234.23, 567.45, 344.12, 765.00); 
DoubleStream stream4 = stream3.map(x -> {   
    double total = x / 100 * (100 + tax);   
      if ( total > limit) {     
        total = total + delivery;   
        }   
        return total; 
    });

要用命名的柯里化函數(shù)來替代匿名“捕捉”函數(shù),確定正確類型并不難。有4個參數(shù),返回 DoubleUnaryOperator,那么類型應該是 DoubleFunction>>。不過,很容易錯放參數(shù)位置:

DoubleFunction>> computeTotal = x -> y -> z -> w -> {   
    double total = w / 100 * (100 + x);   
    if (total > y) {     
      total = total + z;   
      }   
      return total; 
    };  
    DoubleStream stream2 = stream.map(computeTotal.apply(tax).apply(limit).apply(delivery));

你怎么確定 xy、zw 是什么?實際上有個簡單的規(guī)則:通過直接使用方法求值的參數(shù)在第一位,按照使用方法的順序,例如,taxlimit、delivery 對應的就是 xyz。來自流的參數(shù)最后使用,因此它對應的是 w。

不過還存在一個問題:如果函數(shù)通過測試,我們知道它是正確的,但是沒有辦法確保它被正確使用。舉個例子,如果我們使用參數(shù)的順序不對:

DoubleStream stream2 = stream.map(computeTotal.apply(limit).apply(tax).apply(delivery));

就會得到:

[1440.8799999999999, 3440.2000000000003, 2100.2200000000003, 4625.5]

而不是:

[258.215152, 661.05688, 379.357888, 878.836]

這就意味著不僅需要測試函數(shù),還要測試它的每次使用。如果能夠確保使用順序不對的參數(shù)不會被編譯,豈不是很好?

這就是使用正確類型體系的所有內(nèi)容。將原語用于業(yè)務類型并不好,從來就沒有好結(jié)果。但是現(xiàn)在有了函數(shù),就更多了一條不要這么做的理由。這個問題將在其他文章中詳細討論。

敬請期待

本文介紹了使用原語大概比使用對象更為復雜。在 Java 8中使用原語的函數(shù)一團糟,不過還有更糟糕的。在下一篇文章中,筆者將談論在流中使用原語。

OneAPM 能為您提供端到端的 Java 應用性能解決方案,我們支持所有常見的 Java 框架及應用服務器,助您快速發(fā)現(xiàn)系統(tǒng)瓶頸,定位異常根本原因。分鐘級部署,即刻體驗,Java 監(jiān)控從來沒有如此簡單。想閱讀更多技術文章,請訪問 OneAPM 官方技術博客。

本文轉(zhuǎn)自 OneAPM 官方博客

原文地址: https://dzone.com/articles/whats-wrong-java-8-part-ii

文章版權歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/65860.html

相關文章

  • 數(shù)據(jù)結(jié)構之二叉樹(java版)

    摘要:二叉樹是數(shù)據(jù)結(jié)構中很重要的結(jié)構類型,學習數(shù)據(jù)結(jié)構也是深入學習編程的必由之路,這里我們簡單介紹下我對于二叉樹的理解,水平有限,如有錯誤還請不吝賜教。 二叉樹是數(shù)據(jù)結(jié)構中很重要的結(jié)構類型,學習數(shù)據(jù)結(jié)構也是深入學習編程的必由之路,這里我們簡單介紹下我對于二叉樹的理解,水平有限,如有錯誤還請不吝賜教。 首先照例定義一個二叉樹的節(jié)點類 class Node { private int ...

    JayChen 評論0 收藏0
  • java內(nèi)存模型

    摘要:順序一致性內(nèi)存模型有兩大特性一個線程中所有操作必須按照程序的順序執(zhí)行。這里的同步包括對常用同步原語的正確使用通過以下程序說明與順序一致性兩種內(nèi)存模型的對比順序一致性模型中所有操作完全按程序的順序串行執(zhí)行。 java內(nèi)存模型 java內(nèi)存模型基礎 happen-before模型 JSR-133使用happen-before的概念來闡述操作之間的內(nèi)存可見性。在JMM中,如果一個操作執(zhí)行的結(jié)...

    2i18ns 評論0 收藏0
  • 實戰(zhàn)Java虛擬機之二“虛擬機的工作模式”

    摘要:今天開始實戰(zhàn)虛擬機之二虛擬機的工作模式。總計有個系列實戰(zhàn)虛擬機之一堆溢出處理實戰(zhàn)虛擬機之二虛擬機的工作模式實戰(zhàn)虛擬機之三的新生代實戰(zhàn)虛擬機之四禁用實戰(zhàn)虛擬機之五開啟編譯目前的虛擬機支持和兩種運行模式。 今天開始實戰(zhàn)Java虛擬機之二:虛擬機的工作模式。 總計有5個系列實戰(zhàn)Java虛擬機之一堆溢出處理實戰(zhàn)Java虛擬機之二虛擬機的工作模式實戰(zhàn)Java虛擬機之三G1的新生代GC實戰(zhàn)Jav...

    focusj 評論0 收藏0
  • 馬士兵MCA架構師Java互聯(lián)網(wǎng)架構師

    摘要:如果是這樣,你一定要拿出個小時的時間,參加一次馬士兵老師的多線程與高并發(fā)訓練營。橫掃一切關于多線程的問題,吊打所有敢于提問并發(fā)問題的面試官。 如果你平時只有CRUD的經(jīng)驗,從來不會了解多線程與高并發(fā),相信你一定一頭霧水。如果是這樣,你一定要拿出4個小時的時間,參加一次馬士兵老師的《多線程與高并發(fā)》訓練營。讓骨灰級掃地神僧馬...

    dantezhao 評論0 收藏0
  • 樂字節(jié)Java反射之二:實例化對象、接口與父類、修飾符屬性

    摘要:大家好,小樂繼續(xù)接著上集樂字節(jié)反射之一反射概念與獲取反射源頭這次是之二實例化對象接口與父類修飾符和屬性一實例化對象之前我們講解過創(chuàng)建對象的方式有克隆反序列化,再加一種,根據(jù)對象,使用或者構造器實例化對象。 大家好,小樂繼續(xù)接著上集:樂字節(jié)Java反射之一:反射概念與獲取反射源頭Class 這次是之二:實例化對象、接口與父類、修飾符和屬性 一:實例化對象 之前我們講解過創(chuàng)建對象的方式,有...

    xietao3 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<