自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Java中的七種函數(shù)式編程技巧

開發(fā) 前端
在Java中限制數(shù)據(jù)變異的方法并不多。然而,通過使用純函數(shù),并明確避免數(shù)據(jù)變異和重新賦值(使用我們之前討論過的其他概念),可以實現(xiàn)這一目標(biāo)。對于變量,我們可以使用final關(guān)鍵字,它是一個非訪問修飾符,用于防止通過重新賦值來改變變量的值。

環(huán)境:Java21

1. 簡介

函數(shù)式編程是一種編程范式,以函數(shù)為核心,避免改變狀態(tài)與可變數(shù)據(jù),強調(diào)函數(shù)的第一公民地位。它通過使用高階函數(shù)和純函數(shù),實現(xiàn)代碼的模塊化和重用性,提升可讀性和可維護(hù)性,常用于并發(fā)編程和數(shù)學(xué)計算等領(lǐng)域。

在函數(shù)式編程中,有兩條非常重要的規(guī)則:

  • 無數(shù)據(jù)變異
    這意味著一旦數(shù)據(jù)對象被創(chuàng)建后就不應(yīng)該再被更改。任何對該對象的操作都應(yīng)該返回一個新的對象,而不是修改原始對象。
  • 無隱式狀態(tài)
    應(yīng)避免隱藏或隱式狀態(tài)。這樣理解:在傳統(tǒng)編程中,一個函數(shù)可能依賴于一些外部或隱藏的狀態(tài),比如全局變量、靜態(tài)變量或者類成員變量等,這些狀態(tài)不是通過參數(shù)傳遞給函數(shù)的。在函數(shù)式編程中,提倡避免這種隱式的依賴關(guān)系,而是將所有需要的狀態(tài)都作為參數(shù)顯式地傳遞給函數(shù)。這樣做的結(jié)果是提高了代碼的透明度和可測試性,因為你清楚地知道函數(shù)依賴哪些輸入來產(chǎn)生輸出,同時也減少了副作用的發(fā)生,即函數(shù)執(zhí)行時除了返回值外不改變其他任何東西。

除了上述內(nèi)容外,還有以下可以在Java中應(yīng)用的函數(shù)式編程概念:

  • 高階函數(shù)(Higher-order functions)
  • 閉包(Closures)
  • 柯里化(Currying)
  • 遞歸(Recursion)
  • 惰性求值(Lazy evaluations)
  • 引用透明性(Referential transparency)

使用函數(shù)式編程并不意味著必須全盤采用,你可以始終使用函數(shù)式編程概念來補充面向?qū)ο蟮母拍?,尤其是在Java中。無論你使用的范式或語言是什么,都可以盡可能地利用函數(shù)式編程的優(yōu)點。

接下來,我們將詳細(xì)介紹函數(shù)式編程在Java中的應(yīng)用

2. 實戰(zhàn)案例

2.1 一等函數(shù)和高階函數(shù)

在一等函數(shù)的上下文中,函數(shù)被視為頭等公民,意味著它們可以被賦值給變量、作為參數(shù)傳遞給其他函數(shù)、從函數(shù)中返回,以及包含在數(shù)據(jù)結(jié)構(gòu)中。遺憾的是,Java并不完全支持這一特性,因此像閉包、柯里化和高階函數(shù)這樣的概念在Java中實現(xiàn)起來不如在其他語言中那么方便。

在Java中最接近一等函數(shù)的概念是Lambda表達(dá)式。此外,在java.util.function包下還有一些內(nèi)置的函數(shù)式接口,如Function、Consumer、Predicate、Supplier等,可以用于函數(shù)式編程。

只有當(dāng)一個函數(shù)接受一個或多個函數(shù)作為參數(shù),或者返回另一個函數(shù)作為結(jié)果時,它才能被視為高階函數(shù)。在Java中,我們最接近高階函數(shù)的方式是使用Lambda表達(dá)式和內(nèi)置的函數(shù)式接口。

public class Test {
  public static void main(String[] args) {
    var list = Arrays.asList("Orange", "Apple", "Banana", "Grape", "XPack", "AKF");


    var ret = calcLength(list, new FnFactory<String, Object>() {
      public Object execute(final String it) {
        return it.length();
      }
    });
    System.err.printf("Length: %s%n", ret);
  }


  static <T, S> ArrayList<S> calcLength(List<T> arr, FnFactory<T, S> fn) {
    var list = new ArrayList<S>();
    arr.forEach(t -> list.add(fn.execute(t)));
    return list;
  }


  @FunctionalInterface
  public interface FnFactory<T, S> {
    S execute(T it);
  }

輸出結(jié)果:

Length: [6, 5, 6, 5, 5, 3]

接下來,我們使用內(nèi)置的Function接口和Lambda表達(dá)式語法來簡化上面的示例:

public class Test1 {
  public static void main(String[] args) {
    var list = Arrays.asList("Orange", "Apple", "Banana", "Grape", "XPack", "AKF") ;
    var ret = calcLength(list, it -> it.length()) ;
    System.err.printf("Length: %s%n", ret) ;
  }


  static <T, S> ArrayList<S> calcLength(List<T> arr, Function<T, S> fn) {
    var list = new ArrayList<S>() ;
    arr.forEach(t -> list.add(fn.apply(t))) ;
    return list ;
  }
}

使用這些概念加上Lambda表達(dá)式,我們可以像下面這樣編寫閉包和柯里化。

public class ClosureTest {
  Function<Integer, Integer> add(final int x) {
    Function<Integer, Integer> add(final int x) {
    // 普通寫法
//    var partial = new Function<Integer, Integer>() {
//      public Integer apply(Integer y) {
//        return x + y;
//      }
//    };
    // 使用Lambda表達(dá)式語法;注意這里不能使用var
    Function<Integer, Integer> partial = y -> x + y ;
    return partial;
  }
    return partial;
  }


  public static void main(String[] args) {
    ClosureTest closure = new ClosureTest();


    var c1 = closure.add(100) ;
    var c2 = closure.add(200) ;


    System.out.println(c1.apply(66));
    System.out.println(c2.apply(66));
  }
}

運行結(jié)果

166
266

以上是關(guān)于閉包的應(yīng)用。

Java中也有許多內(nèi)置的高階函數(shù),如java.util.Collections#sort方法:

public static void main(String[] args) {
  var list = Arrays.asList("Apple", "Orange", "Banana", "Grape");


  Collections.sort(list, (String a, String b) -> {
    return a.compareTo(b);
  });


  System.err.printf("%s%n", list) ; 
}

Java Stream相關(guān)API中也提供了許多高階函數(shù),比如forEach、map等。

2.2 純函數(shù)

函數(shù)式編程傾向于使用遞歸而不是循環(huán)。在Java中,這可以通過使用流API或編寫遞歸函數(shù)來實現(xiàn)。讓我們來看一個計算數(shù)字階乘的例子。還使用JMH對這些方法進(jìn)行了基準(zhǔn)測試,并在下方列出了每操作的納秒數(shù)。

在傳統(tǒng)的迭代方法中:

@State(Scope.Thread)
public class FactorialTest {
  // 我們要使用JMH進(jìn)行測試,所以通過@Param定義入?yún)?  @Param({"20"})
  private long num ;
  @Benchmark
  public long factorial() {
    long result = 1;
    for (; num > 0; num--) {
      result *= num;
    }
    return result;
  }


  public static void main(String[] args) throws Exception {
    Options options = new OptionsBuilder()
        .include(FactorialTest.class.getSimpleName())
        .forks(1)
        .build() ;
    new Runner(options).run() ;
  }
}

測試結(jié)果

Benchmark                (num)  Mode  Cnt  Score   Error  Units
FactorialTest.factorial     20  avgt    5  0.475 ± 0.013  ns/op

同樣的功能也可以使用遞歸來實現(xiàn),如下所示,這在函數(shù)式編程中更為青睞。

@State(Scope.Thread)
public class FactorialTest2 {
  @Param({ "20" })
  private long num;


  @Benchmark
  public long factorialRec() {
    return factorial(num);
  }
  private long factorial(long n) {
    return n == 1 ? 1 : n * factorial(n - 1);
  }
  public static void main(String[] args) throws Exception {
    Options options = new OptionsBuilder()
        .include(FactorialTest2.class.getSimpleName())
        .forks(1)
        .build();
    new Runner(options).run();
  }
}

測試結(jié)果

Benchmark                    (num)  Mode  Cnt   Score   Error  Units
FactorialTest2.factorialRec     20  avgt    5  17.316 ± 0.792  ns/op

遞歸方法的缺點是,它通常會比迭代方法更慢(我們追求的優(yōu)勢在于代碼的簡潔性和可讀性),并且由于每次函數(shù)調(diào)用都需要作為棧幀保存到堆棧中,可能會導(dǎo)致棧溢出錯誤。

我們還可以使用Stream進(jìn)行遞歸調(diào)用

@Param({ "20" })
private long num;


@Benchmark
public long factorialRec() {
  return LongStream.rangeClosed(1, num)
      .reduce(1, (n1, n2) -> n1 * n2);
}

運行結(jié)果

Benchmark                    (num)  Mode  Cnt   Score   Error  Units
FactorialTest2.factorialRec     20  avgt    5  17.618 ± 1.414  ns/op

與遞歸算法差不多。

在編寫Java代碼時,考慮到可讀性和不可變性,可以考慮使用流API或遞歸;但如果性能至關(guān)重要,或者迭代次數(shù)將非常大,則應(yīng)使用標(biāo)準(zhǔn)循環(huán)。

2.3 惰性求值(Lazy evaluations)

惰性求值(Lazy evaluation)或非嚴(yán)格求值是指推遲表達(dá)式的計算,直到其結(jié)果真正被需要時才進(jìn)行計算。一般來說,Java執(zhí)行的是嚴(yán)格求值,但對于像&&、||和?:這樣的運算符,它會進(jìn)行惰性求值。我們可以利用這一點在編寫Java代碼時實現(xiàn)惰性求值。

考慮下面這個例子,在這個例子中Java會急切地(eagerly)計算所有內(nèi)容:

public static void main(String[] args) {
  System.out.println(addOrMultiply(true, add(4), multiply(4))); // 8
  System.out.println(addOrMultiply(false, add(4), multiply(4))); // 16
}


public static int add(int x) {
  System.out.println("executing add");
  return x + x;
}


public static int multiply(int x) {
  System.out.println("executing multiply");
  return x * x;
}


public static int addOrMultiply(boolean add, int onAdd, int onMultiply) {
  return (add) ? onAdd : onMultiply;
}

執(zhí)行結(jié)果

executing add
executing multiply
8
executing add
executing multiply
16

函數(shù)一早就被執(zhí)行了。

我們可以使用Lambda表達(dá)式和高階函數(shù)將此重寫為惰性求值的版本:

public static void main(String[] args) {
  UnaryOperator<Integer> add = t -> {
    System.out.println("executing add");
    return t + t;
  };
  UnaryOperator<Integer> multiply = t -> {
    System.out.println("executing multiply");
    return t * t;
  };
  System.out.println(addOrMultiply(true, add, multiply, 4));
  System.out.println(addOrMultiply(false, add, multiply, 4));
}


public static <T, R> R addOrMultiply(
    boolean add, Function<T, R> onAdd, 
    Function<T, R> onMultiply, T t) {
  return (add ? onAdd.apply(t) : onMultiply.apply(t));
}

執(zhí)行結(jié)果

executing add
8
executing multiply
16

我們可以看到只執(zhí)行了所需的功能。

2.4 引用透明性(Referential transparency)

表示在程序中,一個函數(shù)調(diào)用可以用它的返回值來替換,而不改變程序的行為。換句話說,對于相同的輸入,函數(shù)總是產(chǎn)生相同的結(jié)果,沒有副作用。

遺憾的是,在Java中限制數(shù)據(jù)變異的方法并不多。然而,通過使用純函數(shù),并明確避免數(shù)據(jù)變異和重新賦值(使用我們之前討論過的其他概念),可以實現(xiàn)這一目標(biāo)。對于變量,我們可以使用final關(guān)鍵字,它是一個非訪問修飾符,用于防止通過重新賦值來改變變量的值。

例如,下面的代碼將在編譯時產(chǎn)生錯誤:

final var list = Arrays.asList("Apple", "Orange") ;
// 你不能重新賦值
list = Arrays.asList("Pack", "XXXOOO") ;

但是,當(dāng)變量持有對其他對象的引用時,這并不會起到作用。例如,即使使用了final關(guān)鍵字,下面的對象變異仍然會發(fā)生:

final var list = new ArrayList<>() ;
// 我們還是可以添加數(shù)據(jù)
list.add("XXX") ;
list.add("OOO") ;

final 關(guān)鍵字允許引用變量的內(nèi)部狀態(tài)被修改,因此從函數(shù)式編程的角度來看,final 關(guān)鍵字僅對常量和捕獲重新賦值有用。

責(zé)任編輯:武曉燕 來源: Spring全家桶實戰(zhàn)案例源碼
相關(guān)推薦

2011-02-22 16:09:53

Eclipse調(diào)試

2021-10-19 14:51:33

說服力IT主管CIO

2022-07-01 08:00:44

異步編程FutureTask

2015-09-02 12:12:13

2019-11-11 16:44:20

機器學(xué)習(xí)Python算法

2013-01-07 10:14:06

JavaJava枚舉

2025-02-10 08:43:31

Java異步編程

2022-07-25 10:15:29

垃圾收集器Java虛擬機

2022-05-10 08:08:01

find命令Linux

2020-01-14 08:00:00

.NET緩存編程語言

2025-01-21 08:00:00

限流微服務(wù)算法

2022-03-14 07:40:14

RibbonSpringNacos

2016-03-16 10:39:30

數(shù)據(jù)分析數(shù)據(jù)科學(xué)可視化

2010-10-15 10:02:01

Mysql表類型

2023-02-14 08:32:41

Ribbon負(fù)載均衡

2019-09-06 09:00:00

開發(fā)技能代碼

2020-01-14 11:09:36

CIO IT技術(shù)

2025-01-15 10:46:23

開發(fā)JavaScript集合

2017-08-31 14:57:53

數(shù)據(jù)庫MySQLJOIN

2017-06-14 16:44:15

JavaScript原型模式對象
點贊
收藏

51CTO技術(shù)棧公眾號