Java8的新特性以及與C#的比較


函數式接口 VS 委托

在C中,可以使用函數指針來存儲函數的入口,從而使得函數可以像變量一樣賦值、傳遞和存儲,使得函數的調用變得十分靈活,是實現函數回調的基礎。然而函數指針不存在函數的簽名信息,甚至可以指向任何地址,使用上有諸多不安全因素,因此在很多現代語言中不存在函數指針這種類型。

在Java中,包裝一個方法的調用,需要創建一個接口類型和相應的實現類型,在實現中調用需要包裝的方法,如果需要調用的是實例方法,還需要將實例的引用傳遞進接口實現的實例中(后面再比較閉包)。這種實現方式的好處是不需要引入更多的語法概念,可以保持語言的精簡,學習曲線平緩。缺點是代碼量多,不能清晰的區分和表達“這是一個包裝了方法的對象”這種概念,同時接口的定義多樣,實例的使用者需要了解接口的細節才能很好地使用。

public interface Func<T, TResult> {

    TResult invoke(T arg);

}

 

public static void M(){

    Func<String, String> func = new Func<String, String>(){

        public String invoke(String arg){

            return method(arg);

        }

    };

    String result = func.invoke("world");

}

   

public static String method(String arg){

    return "hello " + arg;

}

 

在C#(1+)中,有一種特殊的引用類型叫做委托(Delegate),專門用於表達方法的引用。所有的委托類型都繼承於System.Delegate,同時,擁有一組特殊的語法,使得委托的定義、實例化、調用十分簡單和極具語義。委托的調用可以像調用普通方法一樣,使用者不需要了解委托的內在實現。

delegate TResult Func<T,TResult>(T arg); //定義委托

 

public static void M(string arg){

    Func<string, string> func = Method;//指向靜態方法

    Program p = new Program();

    func = p.InstanceMethod;//指向實例方法

    string result = func("world");//調用方法

}

 

public static string Method(string arg){

    return "hello " + arg;

}

 

public string InstanceMethod(string arg){

    return "Woo " + arg; ;

}

在.NET中,委托的內部實際上是封裝了一個函數指針。

 

Java8中新加的函數式接口,采用了類似於C#的委托的語法,使得一個方法定義可以直接賦值給一個“函數式接口”,所謂函數式接口就是只包含一個方法定義的接口,可以使用@FunctionalInterface加以約束。

 

@FunctionalInterface

public interface Func<T, TResult> {

    TResult invoke(T arg);

}

 

public static void M(){

    Func<String, String> func = Programe::method;

    Programe p = new Programe();

    func = p::instanceMethod;

    String result = func.invoke("world");

}

   

public static String method(String arg){

    return "hello " + arg;

}

 

public String instanceMethod(String arg){

    return "Woo " + arg;

}

可以看到,無論是指向靜態方法還是實例方法,Java8的語法都和C#十分接近,他們的內在實現也是類似的,.NET使用ldftn指令獲取方法地址,而Java則使用invokedynamic。在C#,獲取方法引用和調用方法的語法完全一致,而Java,靜態方法必須使用類名::方法名的寫法,和調用時可以只需方法名的寫法有區別,同時多了一個::符號,增加了語法的復雜度,不明白這樣設計的原因是什么,難道僅僅是為了區別C#。

 

更多

.NET的Delegate基類提供了委托的鏈表連接以實現多播,是事件機制實現的基礎;提供了運行於線程池的異步調用方法,使用起來也十分方便。

 

匿名方法和閉包

當一個方法的定義僅僅是為了被引用到一個變量中,往往會使用內聯方法定義,也就是匿名方法。

Java:

Func<String, String> func = new Func<String, String>(){

    public String invoke(String arg){

        return "hello " + arg;

    }

};

String result = func.invoke("world");

C#

Func<string, string> func = delegate(string arg){

    return "hello " + arg;

};

string result = func("world");

類似的語法,使得一個方法出現在了另一個方法體內,在編譯之后,都生成了兩個獨立的方法。一般說來兩個方法之間的臨時變量有各自的棧范圍,是不可以共享的,也就是說內部方法里不能訪問外部方法的變量。然而從代碼結構上,似乎又應該允許這樣的訪問,的確,如果內部方法如果可以訪問外部方法的臨時變量,將會帶來很多便利,代碼邏輯也更直觀。Java和C#都實現了這種變量穿越的能力,就是閉包。

Java

public static void M(int arg){

    final int v = arg;

    Func<String, String> func = new Func<String, String>(){

        public String invoke(String arg){

            return "hello " + arg + v;

        }

    };

}

C#

public static void M(){

    int v=1;

    Func<string, string> func = delegate(string arg)    {

        return "hello " + arg + v;

    };

    string result = func("world");

}

 

然而他們的實現機制則有一些的區別。

Java的實現,在“初始化接口”時將內部需要的變量作為自動生成的匿名類型的構造方法的參數傳遞進實例內部並使用字段存儲,內部方法調用外部變量實質上是調用實例的字段。這樣一來,內部訪問的變量和外部的變量在XXX實例化的那一刻開始,就獨立變化了,表現得不像是同一個變量,因此在Java中閉包變量必須加上final修飾,以解決這一問題。

在C#中,內部方法的訪問實現和Java類似,同時添加了對閉包字段訪問的屬性,在外部方法訪問變量時,同樣訪問的是閉包對象的屬性,也就是和內部訪問的相同,從而實現了內部方法和外部方法共享一個“變量”。

兩者的實現都將方法內的變量提升到了對象的成員,都將延長對象生命周期,需要注意。

 

泛型與類型推斷

泛型是C#2添加的新特性,給C#帶來了十分靈動的類型定義方式。例如,不再需要為各種含有不同類型的函數簽名的方法定義委托,只需要根據不同參數個數以及有無返回值定義一批泛型委托就可以表示絕大部分函數了。

    delegate TResult Func<TResult>();

    delegate TResult Func<T, TResult>(T arg);

    delegate TResult Func<T1, T2, TResult>(T1 arg1, T2 arg2);

    ……

    delegate void Action();

    delegate void Action<T>(T arg);

delegate void Action<T1, T2>(T1 arg1, T2 arg2);

……

再例如,復雜的數據結構可以用嵌套的泛型定義:如Dictionary<Tuple<int, KeyValuePair<string, long>>, List<int>>。這樣的數據結構定義比自定義類型更為直觀和統一。但是代碼則較為冗長,在new這樣的實例時,需要重復寫兩遍。在C#3中,添加了對匿名類型的類型推導能力,自動根據等號右邊的表達式推導左邊變量的類型。

var data = new Dictionary<Tuple<int, KeyValuePair<string, long>>, List<int>>();

var data = new Func<int>(delegate()    {

        return 0;

    });

在C#中,各種開泛型和閉泛型在編譯后都是區別存在的,Action<>,Action<int>,Action<string>都是不同的類型,從而真正實現了強類型約束以及運行時獲取類型參數的能力,同時類型參數可以是值類型,提高了泛型集合的性能。

 

Lambda表達式

Lambda表達式是C#3添加的新特性。用於簡化委托的定義。

對於以下匿名方法的定義:

Func<string, string> func = delegate(string arg)    {

    return "hello " + arg;

};

去掉delegate關鍵字,並用符號=>連接參數與方法體:

Func<string, string> func = (string arg) =>   {

        return "hello " + arg;

    };

通過類型推斷,可以省去參數的類型:

Func<string, string> func = (arg) =>   {

        return "hello " + arg;

};

對於只有一個參數的方法,可以省去參數的括號:

Func<string, string> func = arg =>   {

        return "hello " + arg;

};

對於只有一行表達式的方法,可以省去方法體括號和return關鍵字:

Func<string, string> func = arg => "hello " + arg;

Java8中的lambda表達式幾乎和C#完全一致,只不過連接符號使用->而不是=>。

Func<String, String> func = arg -> "hello " + arg;

 

顯然,lambda表達式大大化簡了匿名方法的定義,方法內聯到任何需要匿名方法的地方。

如,定義一個可枚舉類型,提供一個篩選元素的方法Where:

class Enumerable<T> : IEnumerable<T>

{

    public IEnumerable<T> Where(Func<T, bool> predicat)

    {

        foreach (var item in this)

        {

            if (predicat(item))

            {

                yield return item;

            }

        }

    }

    ……

}

當我需要對已存在的集合進行按條件篩選,則需要提供一個篩選條件的委托類型,這里可以使用lambda表達式來定義,就顯得十分方便和直觀。

Enumerable<int> collection;

var result = collection.Where(item => item > 100);

 

高階函數

Lambda表達式除了簡化匿名方法的定義以外,由於其強大的表達能力,賦於了語言更多的函數式表達能力。

將參數或者返回類型為函數的函數稱為高階函數。

如斐波那契數列函數定義:

f(0) = 1;

f(1) = 1;

f(n) = f(n-1) + f(n-2);

用C#可以寫成:

Func<int, int> f = null;

f = x => x <= 1 ? 1 : f(x - 1) + f(x - 2);

int result = f(5);

 

 

Streams VS linq

在前面的例子中,定了一個Where方法用於對集合元素的篩選,事實上.NET4內置提供了多種類似的集合操作方法(這些方法都是通過擴展方法(C#3新特性)添加的,可以在不修改類定義的情況下為類添加類似於實例方法的效果)。利用這些方法和lambda表達式,可以為集合進行多種操作。

var result = Enumerable.Range(0, 100)//遍歷1到100

    .Where(n => n % 2 == 0)//篩選能被2整除的數

    .Where(n => n % 3 == 0)//篩選能被2整除的數

    .Select(n => n.ToString())//轉換成字符串

.Reverse();//反轉順序

通過這樣的鏈式編程,可以表達各種數據集合操作,而這些都只是操作定義,真正的數據操作在使用時才進行,達到了延時操作效果。

更進一步,C#實現了linq,一種語言級查詢語言,將查詢簡化到了極致,達到了類似於SQL的效果。

上面的查詢可以寫成:

var reuslt = from n in Enumerable.Range(0, 100)

                where n % 2 == 0 && n % 3 == 0

                select n.ToString();

用一行語句表達了純粹的查詢,不存在任何方法、委托的痕跡。

Java8中添加java.util.Stream包,用於實現類似於C#鏈式查詢的效果。

List<String> stringCollection = null;

String[] result = stringCollection.stream()

.filter(s -> s.startsWith("a"))

.map(s -> s.toString())

.toArray();

 

表達式樹

在C#中,lambda表達式可以被編譯為一種數據結構,稱為表達式樹,而這個數據結構可以在運行時被分析、處理或者編譯,做到很多靈活、有趣且高效的效果,其中最為矚目的就是將linq作為ORM的查詢語言,將數據庫的查詢融入到語言中,接受編譯時強類型檢查。

 

Annotation VS Attribute

注解(annotation)是Java6添加的新特性,在Java8中,可以對同一個元素添加重復的注解。

注解十分類似於.NET(1+)中的特性(attribute)。不同的是.NET Attribute是類而不是接口,可以帶有方法邏輯而不僅僅是數據,從而可以利用各種設計模式,得到更多的設計可能性。

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM