国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

Java 8怎么了之二:函數和原語

asoren / 3481人閱讀

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

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

本文主要介紹了 Java 8 中的函數與原語,由國內 ITOM 管理平臺 OneAPM 編譯呈現。

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

Java 5/6/7的函數

在 Java 8之前,使用者可以創建下面這樣的函數:

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));

這些代碼會產生以下結果:

110

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

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

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

一個有意思的問題是:“x 是什么類型?”第一個例子中的類型很明顯。可以根據函數類型推斷出來。Java 知道函數參數類型是 Integer,因為函數類型明顯是 Function。第一個 Integer 是參數的類型,第二個 Integer 是返回類型。

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

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

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

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

最明顯缺乏類型的就是標識符 x。可以做以下嘗試:

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

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

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

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

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)接口執行的作用。它實際上跟函數毫不相關。

假設 Java 只有 apply 方法的 Function 接口,這就不是個大問題。但是原語怎么辦呢?如果 Java 只是對象語言,Function 接口就沒關系。可是它不是。它只是模糊地面向對象的使用(因此被稱為面向對象)。Java 中最重要的類別是原語,而原語與面向對象編程融合得并不好。

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

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

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

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

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

這個問題太簡單,不值得調出方法 apply

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

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

或者采用匿名函數:

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

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

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

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

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

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

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

值得注意的一點,是并沒有對應 bytecharshortfloat 的函數,也沒有對應兩個以上元數的函數。

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

如何發現正確類型

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

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

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

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

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

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

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);

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

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

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

這顯示為:

[5,8, 11, 14, 17]

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

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

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

IntFunction>>

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

IntFunction>

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

Function>>

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

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)); }

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

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

函數復用

函數測試

函數替換

程序維護

程序文檔管理

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

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; 
    });

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

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));

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

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

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]

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

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

敬請期待

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

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

本文轉自 OneAPM 官方博客

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

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/65860.html

相關文章

  • 數據結構之二叉樹(java版)

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

    JayChen 評論0 收藏0
  • java內存模型

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

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

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

    focusj 評論0 收藏0
  • 馬士兵MCA架構師Java互聯網架構師

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

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

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

    xietao3 評論0 收藏0

發表評論

0條評論

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