Java Lambda表示式應用介紹,幫助大家快速掌握Lambda

soralink發表於2020-09-30

JAVA中LAMBDA的應用

本文主要著重講解LAMBDA表示式的用法,關於LAMBDA的實現可能會在後續的文章中講解。本文內容基本基於一個公眾號的內容,在此基礎上進行精簡。如果有興趣深入瞭解的同學請看連結:關於Java Lambda表示式看這一篇就夠了

Lambda表示式與匿名內部類

之所以在java中用lambda表示式其實就是可以用它來簡化代替一些匿名內部類。但是Lambda不能代替所有的匿名內部類。只能用來取代函式介面的簡寫。

我們先來看一下Java7之前如果想用匿名內部類實現一個執行緒是怎麼寫的。

Thread t = new Thread(
	new Runnable(){
		@Override
		public void run(){
			//doSomething
		}
	}
);

上面的程式碼就是我們需要給Thread類傳入一個Runnable的物件。但是這樣看起來很繁重。所以Java8通過引用Lambda表示式可以簡化為如下:

Thread t = new Thread(()->{//doSomething})

可以看到使用Lambda可以省略類名和方法名。如果過載的方法裡面只有一行程式碼還可以把{}省略。

上面講的是不帶引數的寫法,帶引數的寫法同樣簡單比如我們想排序一個Collection物件在Java8之前我們需要:

List<Integer> list = Arrays.asList(2,3,5,2,3);
Collections.sort(list, new Comparator<Integer>(){
	@Override
	public int compare(Integer a, Integer b){
		return a-b;
	}
});

如果用Lambda的話如下:

List<Integer> list = Arrays.asList(2,3,5,2,3);
Collections.sort(list,(a,b)->a-b);

Lambda可以省略引數型別主要是因為javac的型別推斷機制。當有推斷失敗的時候就要手動的指定型別了。

自定義介面

我們看到Lambda是代替介面的實現。可以被Lambda實現的介面我們叫函式介面。這種介面要保證介面裡面只有一個抽象方法。那麼我們怎麼去編寫自己想要的介面呢?其實很簡單,具體如下:

@FunctionalInterface
public interface FInterface<T>{
	void accept(T t);
}

這裡面@FunctionalInterface可以幫我們判斷我們所寫的介面是不是滿足函式介面。

注意this

雖然我們說Lambda是代替了匿名內部類,但是Lambda的實現和匿名內部類的實現是不一樣的。具體情況你可以自己寫好程式碼之後通過javac看一下生成的class檔案有什麼不同或者看我在開頭附上的連結。

在匿名內部類使用this指向的是類的本身,而用Lambda表示式this指向的還是外部的類。

Lambda與Collections

在Java8中新增類java.util.function包。裡面包含了常用的函式介面。我們看一下各個類中都新增了什麼方法:
介面名|Java8新增方法

專案Value
CollectionremoveIf(), spliterator(), stream(), parallelStream(), forEach()
ListreplaceAll(), sort()
MapgetOrDefault(), forEach(), replaceAll(), putIfAbsent(), remove(), replace(), computeIfAbsent(), computeIfPresent(), compute(), merge()

下面我們來逐一的學習這些方法。

Collection中的新方法

1. forEach()

首先是forEach方法。我們先看一下forEach方法的原始碼:

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

可以看到我們需要傳給forEach方法一個Consumer實體物件。那Consumer是什麼呢?

@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

    /**
     * Returns a composed {@code Consumer} that performs, in sequence, this
     * operation followed by the {@code after} operation. If performing either
     * operation throws an exception, it is relayed to the caller of the
     * composed operation.  If performing this operation throws an exception,
     * the {@code after} operation will not be performed.
     *
     * @param after the operation to perform after this operation
     * @return a composed {@code Consumer} that performs in sequence this
     * operation followed by the {@code after} operation
     * @throws NullPointerException if {@code after} is null
     */
    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }
}

可以看到Consumer就是我們之前提到的函式介面。所以我們在forEach裡面用Lambda表示式可以實現這個介面。這樣我們就可以輕鬆的遍歷一遍list裡面的元素了。具體如下:

List<Integer> list = Arrays.asList(1,2,3,4,5,6);
list.forEach(a->System.out.println(a));

2. removeIf()

這個方法作用是刪除滿足filter指定條件的元素。依然是先看一下原始碼:

    default boolean removeIf(Predicate<? super E> filter) {
        Objects.requireNonNull(filter);
        boolean removed = false;
        final Iterator<E> each = iterator();
        while (each.hasNext()) {
            if (filter.test(each.next())) {
                each.remove();
                removed = true;
            }
        }
        return removed;
    }

邏輯很簡單就是滿足這個filter條件就remove這個元素。而這個filter是一個Predicate介面的例項。我們看一下Predicate介面:

@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);

    /**
     * Returns a composed predicate that represents a short-circuiting logical
     * AND of this predicate and another.  When evaluating the composed
     * predicate, if this predicate is {@code false}, then the {@code other}
     * predicate is not evaluated.
     *
     * <p>Any exceptions thrown during evaluation of either predicate are relayed
     * to the caller; if evaluation of this predicate throws an exception, the
     * {@code other} predicate will not be evaluated.
     *
     * @param other a predicate that will be logically-ANDed with this
     *              predicate
     * @return a composed predicate that represents the short-circuiting logical
     * AND of this predicate and the {@code other} predicate
     * @throws NullPointerException if other is null
     */
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    /**
     * Returns a predicate that represents the logical negation of this
     * predicate.
     *
     * @return a predicate that represents the logical negation of this
     * predicate
     */
    default Predicate<T> negate() {
        return (t) -> !test(t);
    }

    /**
     * Returns a composed predicate that represents a short-circuiting logical
     * OR of this predicate and another.  When evaluating the composed
     * predicate, if this predicate is {@code true}, then the {@code other}
     * predicate is not evaluated.
     *
     * <p>Any exceptions thrown during evaluation of either predicate are relayed
     * to the caller; if evaluation of this predicate throws an exception, the
     * {@code other} predicate will not be evaluated.
     *
     * @param other a predicate that will be logically-ORed with this
     *              predicate
     * @return a composed predicate that represents the short-circuiting logical
     * OR of this predicate and the {@code other} predicate
     * @throws NullPointerException if other is null
     */
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }

    /**
     * Returns a predicate that tests if two arguments are equal according
     * to {@link Objects#equals(Object, Object)}.
     *
     * @param <T> the type of arguments to the predicate
     * @param targetRef the object reference with which to compare for equality,
     *               which may be {@code null}
     * @return a predicate that tests if two arguments are equal according
     * to {@link Objects#equals(Object, Object)}
     */
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }

    /**
     * Returns a predicate that is the negation of the supplied predicate.
     * This is accomplished by returning result of the calling
     * {@code target.negate()}.
     *
     * @param <T>     the type of arguments to the specified predicate
     * @param target  predicate to negate
     *
     * @return a predicate that negates the results of the supplied
     *         predicate
     *
     * @throws NullPointerException if target is null
     *
     * @since 11
     */
    @SuppressWarnings("unchecked")
    static <T> Predicate<T> not(Predicate<? super T> target) {
        Objects.requireNonNull(target);
        return (Predicate<T>)target.negate();
    }
}

可以看到我們這個介面裡面只有一個test函式需要實現。所以我們依然可以用Lambda表示式實現:

List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3,4,5,6));
list.removeIf(a -> a>1);

我們還可以看到在Predicate介面裡面還有一些default函式,這些函式可以用來配合多個filter一起使用。這裡就不再贅述。大家自己試一試就可以明白,不是很難理解。

3. replaceAll()

這個方法就是對每個元素進行一個操作,之後操作的返回結果替換原來的元素。方法原始碼:

default void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final ListIterator<E> li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }

邏輯依然很簡單。我們看一下UnaryOperator這個介面:

@FunctionalInterface
public interface UnaryOperator<T> extends Function<T, T> {
    static <T> UnaryOperator<T> identity() {
        return t -> t;
    }
}

我們發現這個介面並沒有apply方法。其實這個方法是放在了Function介面裡面:

@FunctionalInterface
public interface Function<T, R> {

    R apply(T t);

    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }

    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    static <T> Function<T, T> identity() {
        return t -> t;
    }
}

我們發現這兩個介面裡面都有一個identity靜態方法。這個方法其實就是返回一個apply函式為返回輸入本身的Function物件。所以有的時候我們可以用identity()代替t->t。

4. sort()

這個就不多做講解了,之前已經給出了例子,大家返回上面看一下就可以了。

5. spliterator()

這個方法和Lambda沒什麼關係,有興趣的同學可以看一下這個部落格:Spliterator深入解讀

6. Stream和parallelStream

這兩個方法會在後面詳細解釋,所以暫時先跳過。

Map中的新方法

在這一部分,我基於HashMap來講解新增的函式。如果對HashMap的實現沒有基礎的同學請自己看一下HashMap的實現原理。

1. forEach()

先上forEach()原始碼:

@Override
    public void forEach(BiConsumer<? super K, ? super V> action) {
        Node<K,V>[] tab;
        if (action == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (Node<K,V> e : tab) {
                for (; e != null; e = e.next)
                    action.accept(e.key, e.value);
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }

基本上就是用兩個for迴圈遍歷所有的Node。之後把每個node的key和value放入BiConsumer裡面的accept方法。BiConsumer的原始碼就不放了,感興趣的同學可以自己看一下。
使用forEach的程式碼如下:

HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.forEach((k, v) -> System.out.println(k + "=" + v));

2. getOrDefault()和putIfAbsent()

這兩個方法跟Lambda沒什麼關係就不詳細介紹了,但是這個方法一定掌握,因為經常會用到它,所以在這裡提及一下。

3. replaceAll()

先看原始碼:

    public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
        Node<K,V>[] tab;
        if (function == null)
            throw new NullPointerException();
        if (size > 0 && (tab = table) != null) {
            int mc = modCount;
            for (Node<K,V> e : tab) {
                for (; e != null; e = e.next) {
                    e.value = function.apply(e.key, e.value);
                }
            }
            if (modCount != mc)
                throw new ConcurrentModificationException();
        }
    }

這個方法就是根據BiFunction裡面apply函式的返回值更改對應的value。簡單的使用就是:

HashMap<Integer, String> map = new HashMap<>();
map.put(1, "one");
map.put(2, "two");
map.put(3, "three");
map.replaceAll((k, v) -> v.toUpperCase());

4. merge()

原始碼:

    @Override
    public V merge(K key, V value,
                   BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
        if (value == null || remappingFunction == null)
            throw new NullPointerException();
        int hash = hash(key);
        Node<K,V>[] tab; Node<K,V> first; int n, i;
        int binCount = 0;
        TreeNode<K,V> t = null;
        Node<K,V> old = null;
        if (size > threshold || (tab = table) == null ||
            (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((first = tab[i = (n - 1) & hash]) != null) {
            if (first instanceof TreeNode)
                old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
            else {
                Node<K,V> e = first; K k;
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) {
                        old = e;
                        break;
                    }
                    ++binCount;
                } while ((e = e.next) != null);
            }
        }
        if (old != null) {
            V v;
            if (old.value != null) {
                int mc = modCount;
                v = remappingFunction.apply(old.value, value);
                if (mc != modCount) {
                    throw new ConcurrentModificationException();
                }
            } else {
                v = value;
            }
            if (v != null) {
                old.value = v;
                afterNodeAccess(old);
            }
            else
                removeNode(hash, key, null, false, true);
            return v;
        } else {
            if (t != null)
                t.putTreeVal(this, tab, hash, key, value);
            else {
                tab[i] = newNode(hash, key, value, first);
                if (binCount >= TREEIFY_THRESHOLD - 1)
                    treeifyBin(tab, hash);
            }
            ++modCount;
            ++size;
            afterNodeInsertion(true);
            return value;
        }
    }

原始碼稍微有點兒長。首先merge需要提供三個引數。k,v和BiFunction。之後根據提供的k找到在hashmap裡面對應的value我們叫v2。如果v2不存在就把v和k關聯。如果v2存在則把v2和v放到BiFunction的apply函式裡面。返回的結果作為新的v。
使用如下:

map.merge(key, value, (v1, v2) -> v1+v2);

5. compute()

原始碼:

@Override
    public V compute(K key,
                     BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
        if (remappingFunction == null)
            throw new NullPointerException();
        int hash = hash(key);
        Node<K,V>[] tab; Node<K,V> first; int n, i;
        int binCount = 0;
        TreeNode<K,V> t = null;
        Node<K,V> old = null;
        if (size > threshold || (tab = table) == null ||
            (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((first = tab[i = (n - 1) & hash]) != null) {
            if (first instanceof TreeNode)
                old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
            else {
                Node<K,V> e = first; K k;
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k)))) {
                        old = e;
                        break;
                    }
                    ++binCount;
                } while ((e = e.next) != null);
            }
        }
        V oldValue = (old == null) ? null : old.value;
        int mc = modCount;
        V v = remappingFunction.apply(key, oldValue);
        if (mc != modCount) { throw new ConcurrentModificationException(); }
        if (old != null) {
            if (v != null) {
                old.value = v;
                afterNodeAccess(old);
            }
            else
                removeNode(hash, key, null, false, true);
        }
        else if (v != null) {
            if (t != null)
                t.putTreeVal(this, tab, hash, key, v);
            else {
                tab[i] = newNode(hash, key, v, first);
                if (binCount >= TREEIFY_THRESHOLD - 1)
                    treeifyBin(tab, hash);
            }
            modCount = mc + 1;
            ++size;
            afterNodeInsertion(true);
        }
        return v;
    }

其實和merge很像。區別就是這次放到BiFunction的apply函式裡面的引數是key。

可以用compute方法實現merge,程式碼如下:

map.compute(key, (k,v) -> v==null ? newValue : v.concat(newValue));

6. computeIfAbsent() 和 computeIfPresent()

這兩個方法和compute有一點點區別。其實你們看名字就明白了。一個是如果key存在再執行apply方法,之後把k和v放入到map中。另一個就是當map中不存在key的時候呼叫apply方法,之後更改對應的value值。

Stream API(很牛X)

為什麼我們要用Stream API呢?主要原因有亮點

  1. 簡潔(後面看程式碼就能體會到)
  2. 對於多核友好。呼叫parallel()方法就可以。
    圖片是從部落格開頭的連結借用過來的
    這個圖片展示了java中的四種stream,他們均繼承自BaseStream類。IntStream,LongStream,DoubleStream分別對應三種基本型別。Stream對應剩餘的型別。

大部分情況我們可以通過呼叫Collection.stream()得到。但是他與collection本身還是有區別的。具體區別可以看一下原博,我就不細講了。

1. forEach()

對於這個方法其實和collection裡面的forEach很像,我們直接看一下程式碼就可以了。

Stream<String> stream = Stream.of("I", "love", "you", "too");
stream.forEach(str -> System.out.println(str));

2. filter()

故名思議,過濾。就是把符合條件的元素留下。看一下這個抽象方法:

Stream<T> filter(Predicate<? super T> predicate);

可以看到我們就是傳進去一個Predicate介面實現。這個幾口我們之前已經講過。用法如下:

Stream<String> stream= Stream.of("I", "love", "you", "too");
stream.filter(str -> str.length()==3)
    .forEach(str -> System.out.println(str));

3. distinct()

去除重複的元素。用法如下:

Stream<String> stream= Stream.of("I", "love", "you", "too", "too");
stream.distinct()
    .forEach(str -> System.out.println(str));

4. sorted()

排序。傳入一個Comparator。用法如下

Stream<String> stream= Stream.of("I", "love", "you", "too");
stream.sorted((str1, str2) -> str1.length()-str2.length())
    .forEach(str -> System.out.println(str));

5. map()

把所有元素經過處理後返回。抽象函式為:

<R> Stream<R> map(Function<? super T,? extends R> mapper)

用法為:

Stream<String> stream = Stream.of("I", "love", "you", "too");
stream.map(str -> str.toUpperCase())
    .forEach(str -> System.out.println(str));

6. flatMap()

把多個集合物件拆開變成一個stream。使用樣例:

Stream<List<Integer>> stream = Stream.of(Arrays.asList(1,2), Arrays.asList(3, 4, 5));
stream.flatMap(list -> list.stream())
    .forEach(i -> System.out.println(i));

Stream的reduce操作

reduce操作就是從眾多元素中篩選出一個你想要的值。sum()、max()、min()、count()等都是reduce操作,將他們單獨設為函式只是因為常用。

reduce在Stream裡面有三種過載的方法分別是:

Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);

<U> U reduce(U identity,
                 BiFunction<U, ? super T, U> accumulator,
                 BinaryOperator<U> combiner);

下面講一下這三個方法有什麼區別。
第一個方法應用如下:

List<Integer> numList = Arrays.asList(1,2,3,4,5);
int result = numList.stream().reduce((a,b) -> a + b ).get();
System.out.println(result);

這段程式碼最後返回的就是元素的累加。
那麼第二個過載方法和第一基本一樣,區別就是在執行的時候第一個元素並不是list裡面的元素而是你放入的元素。

List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3));
int x = list.stream().reduce(5,(a,b)->{
	System.out.println(a+","+b);
    return a+b;
});
System.out.println(x);

這個程式碼的輸出是:

5,1
6,2
8,3
11

可以看到第一個元素是5。最後的結果也是11。

第三個方法作用特別大,就是可以進行型別轉換。程式碼如下:

List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3));
Long x = list.stream().reduce(3L,(a, b)->{
	System.out.println(a+","+b);
	return a+b;
},(a,b)->null);
System.out.println(x);

最後把int值轉換成了long型別。最後一個元素用於並行的stream。一般用不到。

Stream裡面的collect()

collect的作用就是把stream轉換成其他的類比如List,Set,Map。比如:

Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(ArrayList::new, ArrayList::add, ArrayList::addAll);// 方式1
//List<String> list = stream.collect(Collectors.toList());// 方式2
System.out.println(list);

我們看到裡面有用到[類名]::[方法名]。這種方式就是將類裡面的方法當作lambda方法傳入進去。

collect()方法有兩種過載方法分別是:

<R> R collect(Supplier<R> supplier,
                  BiConsumer<R, ? super T> accumulator,
                  BiConsumer<R, R> combiner);

<R, A> R collect(Collector<? super T, A, R> collector);

上面的樣例程式碼就是用的第一種,我們需要提供三個引數。但是如果我們轉換的是常用類的話,Collector這個類已經幫我們實現好了。所以我們用第二個過載方法會簡單很多。例如:

Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList()); // (1)
Set<String> set = stream.collect(Collectors.toSet()); // (2)

上述程式碼能夠滿足大部分需求,但由於返回結果是介面型別,我們並不知道類庫實際選擇的容器型別是什麼,有時候我們可能會想要人為指定容器的實際型別,這個需求可通過Collectors.toCollection(Supplier collectionFactory)方法完成。

ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new));// (3)
HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new));// (4)

collect()如何生成Map

雖然Map不能轉換成stream,但是反過來是可以的。有三種方法可以生成map:

  1. 使用Collectors.toMap()生成的收集器,使用者需要指定如何生成Map的key和value。
  2. 使用Collectors.partitioningBy()生成的收集器,對元素進行二分割槽操作時用到。
  3. 使用Collectors.groupingBy()生成的收集器,對元素做group操作時用到。

下面分別講述這三個方法都是什麼意思
首先第一個方法為:基於元素可以傳入兩個lambda方法,之後這兩個lambda方法的返回值就是key與value。

List<Integer> list = new ArrayList<>(Arrays.asList(1,2,3));
Map<Integer, String> map = list.stream().collect(Collectors.toMap(a->a+1,a->a.toString()));
System.out.println(map);

輸出為:

{2=1, 3=2, 4=3}

第二個方法就是將元素分為兩個部分,根據就是第二個傳入的lambda函式。我們可以把它看作一個條件。滿足條件就放到第一個集合,不滿足就是第二個。
比如:

Map<Boolean, List<Student>> passingFailing = students.stream()
         .collect(Collectors.partitioningBy(s -> s.getGrade() >= PASS_THRESHOLD));

第三個方法其實課sql裡面的groupby一樣。根據一個lambda函式返回的值不同,進行不同的分類。
程式碼如下:

Map<Department, List<Employee>> byDept = employees.stream()
            .collect(Collectors.groupingBy(Employee::getDepartment));

collect()還可以幫我們進行字串的拼接:

Stream<String> stream = Stream.of("I", "love", "you");
String joined = stream.collect(Collectors.joining());// "Iloveyou"
String joined = stream.collect(Collectors.joining(","));// "I,love,you"
String joined = stream.collect(Collectors.joining(",", "{", "}"));// "{I,love,you}"

Lambda表示式的用法介紹就全部結束了,但是他的內部原理還要更加的複雜。這裡涉及到對於Stream這個介面的實現,也就是ReferencePipeline這個類。他對Stream有一定的優化作用。具體的你們可以看我在文章開頭附上的那片文章,他在後邊有講到這些。如果有時間我也會在以後的文章繼續講解這些知識。謝謝大家~

相關文章