Java8函數式編程


Java8函數式編程

為什么要用Java8

Java8在並行處理大型集合上有很大優勢。可以更好的利用多核處理器的優勢。Java8可以用Lambda表達式很簡便的寫出復雜的處理集合的邏輯。

函數式編程

函數式編程是一種編程范式,我們常見的編程范式有命令式編程(Imperative programming),函數式編程,邏輯式編程,常見的面向對象編程是也是一種命令式編程。

命令式編程是面向計算機硬件的抽象,有變量(對應着存儲單元),賦值語句(獲取,存儲指令),表達式(內存引用和算術運算)和控制語句(跳轉指令),一句話,命令式程序就是一個馮諾依曼機的指令序列。而函數式編程是面向數學的抽象,將計算描述為一種表達式求值,一句話,函數式程序就是一個表達式。

函數式編程中的函數這個術語不是指計算機中的函數(實際上是Subroutine),而是指數學中的函數,即自變量的映射。也就是說一個函數的值僅決定於函數參數的值,不依賴其他狀態。比如sqrt(x)函數計算x的平方根,只要x不變,不論什么時候調用,調用幾次,值都是不變的。在函數式語言中,函數作為一等公民,可以在任何地方定義,在函數內或函數外,可以作為函數的參數和返回值,可以對函數進行組合。

Java是一門面向對象的語言,對函數式編程的支持並不全面,如果想深入理解學習函數式編程可以學習Scala、Clojure,這兩種語言都是可以在jvm上運行的。

lambda表達式

Java8最大的改變就是加入了lambda表達式。什么是lambda表達式呢?

簡單來說,編程中提到的 lambda 表達式,通常是在需要一個函數,但是又不想費神去命名一個函數的場合下使用,也就是指匿名函數。

比如有些函數你只會使用一次,但是你卻給他定義一次,這個函數就成了污染函數,匿名函數就是用來干這個的。

舉個栗子:

faculties.sort(new Comparator<Faculty>() {
            @Override
            public int compare(Faculty o1, Faculty o2) {
                return o1.getName().compareTo(o2.getName());
            }
        });

為了給這個專業列表(faculties)排序,你專門寫了一個匿名內部類,來實現比較大小的方法。這樣看起來又臃腫又復雜,如果用lambda表達式就很簡單了:

faculties.sort((f1, f2) -> f1.getName().compareTo(f2.getName()));

還可以用方法引用可以更簡單:

faculties.sort(Comparator.comparing(Faculty::getName));

下面有各種不同的方法來寫lambda表達式:

Runnable noArguments = () -> System.out.println("Hello World");	
ActionListener oneArgument = event -> System.out.println("button clicked");
Runnable multiStatement = () -> {
System.out.print("Hello");
System.out.println(" World");
};
BinaryOperator<Long> add = (x, y) -> x + y;
BinaryOperator<Long> addExplicit = (Long x, Long y) -> x + y;

lambda表達式可以接受一個或者多個參數,lambda表達式可以轉換成一個接口不能轉化成類,轉化成接口要保證接口中只有一個可以復寫的方法。不然會報錯。

Streams

Stream可以讓我們在更高級別的抽象上寫集合的處理代碼,stream有一系列的方法來讓我們使用。

現在有一需求,需要篩選出來自倫敦的藝術家,通常我們會這么寫:

int count = 0;
for (Artist artist : allArtists) {
	if (artist.isFrom("London")) { 
		count++;
	} 
}

用stream可以這么寫:

long count = allArtists.stream()
						.filter(artist -> artist.isFrom("London"))
						.count();

這樣看起來就清晰很多,而且效率也是差不多的,用stream不會循環兩遍的,stream的方法分為eager和lazy兩種,比如下面這段代碼:

allArtists.stream()
      .filter(artist -> {
          System.out.println(artist.getName());
			 return artist.isFrom("London"); 
		});

它不會打印任何東西,filter就是一個lazy的方法,那怎么區分lazy和eager方法呢?如果返回的還是一個stream那么就是一個lazy的方法,如果返回的不是stream就是eager方法。例如上面的count()方法返回的是int,那么就會被立刻執行。

collect(toList())

List<String> collected = Stream.of("a", "b", "c")
                                   .collect(Collectors.toList());
    assertEquals(Arrays.asList("a", "b", "c"), collected);

collect(toList())把一個Stream轉換成List。

map

List<String> collected = Stream.of("a", "b", "hello")
                               .map(string -> string.toUpperCase())
                               .collect(toList());

map可以把一種類型的stream數據轉換成另一種。

filter

List<String> beginningWithNumbers
  = Stream.of("a", "1abc", "abc1")
		  .filter(value -> isDigit(value.charAt(0)))
		  .collect(toList());

filter可以把filter內返回為true的的數據過濾出來。

flatMap

List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
                               .flatMap(numbers -> numbers.stream())
                               .collect(toList());

看上面的圖更好理解一點,flatmap可以把stream的元素轉換成stream,然后再把這些stream合成一個新的stream,而map只能把stream中的元素從一種類型轉換成另一種類型。

max and min

List<Track> tracks = asList(new Track("Bakai", 524),
							new Track("Violets for Your Furs", 378),
							new Track("Time Was", 451));
Track shortestTrack = tracks.stream()
                            .min(Comparator.comparing(track -> track.getLength()))
                            .get();

在stream中找到最大值和最小值。

reduce

int count = Stream.of(1, 2, 3)
				  .reduce(0, (acc, element) -> acc + element);

reduce會把第一個參數作為初始的acc,遍歷stream的元素作為element,然后每次計算的結果會作為下次的acc.上面的代碼可以拓展為:

BinaryOperator<Integer> accumulator = (acc, element) -> acc + element; int count = accumulator.apply(
                    accumulator.apply(
                        accumulator.apply(0, 1),
2), 3);
綜合

把上面的各種方法結合起來用一下:

Set<String> origins = album.getMusicians()
                               .filter(artist -> artist.getName().startsWith("The"))
                               .map(artist -> artist.getNationality())
                               .collect(toSet());
  1. 過濾出名字以The為開頭的藝術家
  2. 取出藝術家的國籍
  3. 得到一個藝術家的國籍的Set(去重)

再看一個例子:

public Set<String> findLongTracks(List<Album> albums) { return albums.stream()
                 .flatMap(album -> album.getTracks())
                 .filter(track -> track.getLength() > 60)
                 .map(track -> track.getName())
                 .collect(toSet());
}
  1. 把所有專輯的曲目取出來合並成一個stream
  2. 過濾出大於60秒的曲目
  3. 把曲目的名字取出來的
  4. 轉換成一個Set

方法引用

artist -> artist.getName()
Artist::getName

(name, nationality) -> new Artist(name, nationality)
Artist::new

方法引用是一種更簡潔的寫法,而且比lambda表達式更容易理解。

對數據進行分類

一種分類方法是:partiioningBy

public Map<Boolean, List<Artist>> bandsAndSolo(Stream<Artist> artists) { 
	return artists.collect(partitioningBy(artist -> artist.isSolo()));
}

有時候我們想根據某個值對數據進行分類就可以用partitionBy,方法的返回的boolean值作為key,滿足條件的值合並成list作為value。如果不想得到list還以在后面再加一個collector。

public Map<Boolean, Integer> bandsAndSolo(Stream<Artist> artists) { 
	return artists.collect(partitioningBy(artist -> artist.isSolo(), list -> list.size()));
}

這樣改一下就是把滿足條件的值的數量作為value。

另一種是:groupingBy

    public Map<Boolean, List<String>> bandsAndSolo(Stream<String> artists) {
        return artists.collect(partitioningBy(artist -> artist.isSolo(), artist.size()));
    }

不同點在於partitioningBy只能把boolean作為key,而且永遠只有兩個entry,效率要高一點。而groupingBy可以把任意對象作為key。

總結

Java8加入的lambda表達式和stream庫,可以讓我們很方便的操作集合,聚合數據等等,熟練使用的話能很好提高編程效率,同時也提高了代碼的可讀性。當然它也有缺點,比如不方便debug,有一定的學習成本。


免責聲明!

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



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