Java8—一万字的Lambda表达式的详细介绍与应用案例

Wesley13
• 阅读 951

  基于Java8详细介绍了lambda表达式的语法与使用,以及方法引用、函数式接口、lambda复合等Java8的新特性!

文章目录

  • 1 Lambda的概述

  • 2 函数式接口

  • 2.1 Consumer消费型接口

  • 2.2 Supplier供给型接口

  • 2.3 Function< T, R >函数型接口

  • 2.4 Predicate断言型接口

  • 2.5 其他接口以及功能

  • 3 Lambda的语法

  • 3.1 具体格式

  • 3.2 使用要求

  • 4 方法引用

  • 5 默认方法和静态方法

  • 5.1 概述

  • 5.2 问题及解决

  • 6 Lambda的复合

  • 6.1 Comparator比较器复合

  • 6.2 Function函数复合

  • 6.3 Consumer消费复合

  • 6.4 Predicate断言复合

  • 7 Lambda与匿名内部类

  • 8 总结

1 Lambda的概述

  面向对象的语言强调“必须通过对象的形式来做事情”,做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情。
  无论什么情况,当我们在一个方法中需要调用另一个方法的时候,传递的参数必须是一个含有该方法的对象,对象作为一等公民!对于某些可以独立的单个方法(行为),比如比较的方法,在面向对象的程序设计中,同样必须使用一个对象来进行封装,比如Comparable、Comparator,虽然Java中已经使用接口这种更加抽象的类型来封装“比较”这种方法(行为),但是在一个方法中调用比较的方法的时候,我们仍然需要传递一个接口的实现类的对象,然后再方法中调用这个对象的方法,就会很麻烦!因为实际上我们只需要进行比较的这个方法(行为),它却必须要传递一个对象进来!

/**
 * 比较对象的方法
 *
 * @param comparator 比较器
 * @param i          i
 * @param j          j
 */
public static int cmp(Comparator comparator, int i, int j) {
    return comparator.compare(i, j);
}


@Test
public void testJava() {
    //传递一个对象
    int cmp = cmp(new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            return o1 - o2;
        }
    }, 1, 2);
    System.out.println(cmp);
}

  如上图,我们有一个比较对象的方法cmp,在使用传统Java代码编程时,即使最简单的方式,也需要传递一个匿名内部类对象进去,但是我们实际上需要的只是比较的行为而已,并不需要对象!
  面向对象的编程思想自有它的好处,比如封装性,可重用性,多态性。但是程序设计的世界里,想要依靠一种方法打遍天下并且还是最优的解,那几乎是不可能的!Java 8开始,支持lambda表达式,就是为了解决面向对象编程思想在某些时候(比如单个方法的调用)显得很笨重而又啰嗦!
  lambda表达式是一种函数式的编程思想,尽量忽略面向对象的复杂语法。函数式的编程思想中,函数作为一等公民,这里的函数可以类比Java中的方法,描述的是一种行为!当一个方法(函数)中调用另一个方法时,直接将该方法(行为)作为参数传递即可。这样相比于面向对象的编程思想来说,可以让编程更加简单!
  对于上面的代码,我们使用lambda表达式改造之后,如下所示:

@Test
public void testLambda() {
    int cmp = cmp((Comparator<Integer>) (o1, o2) -> o1 - o2, 1, 2);
    System.out.println(cmp);
}

  可以看到,lambda表达式的应用让代码编程非常简单明了,我们直接将比较的行为作为参数传递给了cmp方法,连匿名对象都没有了!

2 函数式接口

  简单的说,函数式接口(Functional Interface)就是只定义一个抽象方法的接口。并且只有在函数式接口中才能使用lambda表达式。为此,Java 8的时候新增加了一个@FunctionalInterface注解,用来表明某个接口是函数式接口。注意一个函数式接口可以选择加上该注解也可以不加上该注解,这个注解简单的说可以作为一种检验!
  lambda表达式实际上就是对函数式接口的唯一抽象方法起作用的,即相当于可以把抽象方法的实现作为函数式接口的具体实现的实例来当作参数传递!这类似于匿名内部类!
  Java8之前就有许多函数式接口,比如Runnable、Callable、Comparator、Comparable……,在Java8的时候,为了更好的支持lambda,新增了一个java.util.function包,这个包下面的有很多的接口,这些接口全部都是函数式接口,它们都用于描述某个行为,方便lambda的使用!
  下面我们将介绍常用的四种接口:Consumer消费型接口、Supplier供给型接口、Function函数型接口、Predicate断言型接口,最后会附上大部分函数式接口的不同行为和功能!可能某些案例的lambda表达式看不太懂,不过没关系,下一节将会讲解lambda的语法!

2.1 Consumer消费型接口

@FunctionalInterface
public interface Consumer<T> {

    /**
     * @param t 输入参数
     */
    void accept(T t);

    //……
}

  Consumer接口中有一个accept抽象方法,它用于接收一个泛型参数T,然而并没有返回值,顾名思义,就是对传递的参数进行“消费”,没有输出,就像消费者一样!
  我们可以将其应用在对某些输入数据的处理但是不需要输出的情况中!下面的案例中,我们需要对集合中的所有int元素进行+1然后输出的操作:

/**
 * @author lx
 */
public class ConsumerTest {

    public static void main(String[] args) {
        //一个初始化集合
        List<Integer> objects = new ArrayList<>();
        objects.add(1);
        objects.add(2);
        objects.add(3);

        //对集合数据进行  加1然后输出的操作
        consume(objects, i -> System.out.println(i + 1));
    }

    /**
     * 使用Consumer对集合元素进行操作的方法
     *
     * @param list     需要操作的集合
     * @param consumer 对元素的具体的操作,在调用的时候传递某个动作就行了
     */
    private static <T> void consume(List<T> list, Consumer<T> consumer) {
        for (T t : list) {
            consumer.accept(t);
        }
    }
}

2.2 Supplier供给型接口

@FunctionalInterface
public interface Supplier<T> {

    /**
     * @return 获取一个返回结果
     */
    T get();
}

  Supplier接口中有一个get抽象方法,它不接收任何参数,但是返回一个T类型的结果,顾名思义,就是没有输入,只有输出,就像生产者一样!
  我们可以将其应用在创建某些对象、获取数据数据的情况中。下面的案例中,我们需要用集合收集10个随机数:

/**
 * @author lx
 */
public class SupplierTest {

    public static void main(String[] args) {
        //一个初始化集合
        List<Integer> objects = new ArrayList<>();
        //我们需要填充10个随机数,Supplier是一个获取随机数的动作
        Random random = new Random();
        supplier(objects, 10, () -> random.nextInt(10));
        //输出集合数据
        System.out.println(objects);
    }

    /**
     * 填充集合数据的方法
     *
     * @param list     需要填充的集合
     * @param count    需要填充的数量
     * @param supplier 获取数据的的操作,在调用的时候传递某个动作就行了
     */
    private static <T> void supplier(List<T> list, int count, Supplier<T> supplier) {
        for (int i = 0; i < count; i++) {
            list.add(supplier.get());
        }
    }
}

2.3 Function< T, R >函数型接口

@FunctionalInterface
public interface Function<T, R> {

    /**
     * 将指定函数应用于给定的参数。
     *
     * @param t 函数参数
     * @return 函数结果
     */
    R apply(T t);
}

  Function接口中有一个apply抽象方法,它接收T类型的参数,返回一个R类型的结果,顾名思义,就是一个参数T到R的映射操作,就像一个函数一样!
  我们可以将其应用在对某个输入对象进行变换、操作然后输出另一个对象(也可以是自己)的情况中。下面的案例中,我们需要对集合中的所有int元素进行自增1的操作:

/**
 * @author lx
 */
public class FunctionTest {

    public static void main(String[] args) {
        //一个初始化集合
        List<Integer> objects = new ArrayList<>();
        objects.add(1);
        objects.add(2);
        objects.add(3);

        //对集合中的数据进行 自增1的操作
        function(objects, i -> ++i);
        //输出集合数据
        System.out.println(objects);
    }


    /**
     * 使用Function对集合元素进行操作的方法
     *
     * @param list     需要操作的集合
     * @param function 对元素的具体的函数操作,在调用的时候传递某个动作就行了
     */
    private static <T> void function(List<T> list, Function<T, T> function) {
        for (int i = 0; i < list.size(); i++) {
            //将通过传入的函数操作获取的结果替换原来的集合对应的数据
            list.set(i, function.apply(list.get(i)));
        }
    }
}

2.4 Predicate断言型接口

@FunctionalInterface
public interface Predicate<T> {

    /**
     * 对给定的参数进行断言的方法
     *
     * @param t 输入参数
     * @return 如果参数符合规则,那么返回true,否则返回false
     */
    boolean test(T t);
}

2.5 其他接口以及功能

  java.util.function包中的大多数其他函数式接口都是一个特性化的接口,即它们的功能和上面的四大接口都差不多,区别可能是参数数量和类型以及返回值类型!
  函数描述符:用来描述函数的参数以及返回值的类型,()表示无参,void表示无返回值,中间使用->连接。

函数式接口

函数描述符

特性化接口

Predicate< T >

T->boolean

IntPredicate,LongPredicate, DoublePredicate

Consumer< T >

T->void

IntConsumer,LongConsumer, DoubleConsumer

Function< T,R >

T->R

IntFunction< R >,IntToDoubleFunction,IntToLongFunction,LongFunction< R >,LongToDoubleFunction,LongToIntFunction,DoubleFunction< R >,ToIntFunction< T >,ToDoubleFunction< T >,ToLongFunction< T >

Supplier< T >

()->T

BooleanSupplier,IntSupplier, LongSupplier

UnaryOperator< T >

T->T

IntUnaryOperator,LongUnaryOperator,DoubleUnaryOperator

BinaryOperator< T >

(T,T)->T

IntBinaryOperator,LongBinaryOperator,DoubleBinaryOperator

BiPredicate< L,R >

(L,R)->boolean

BiConsumer< T,U >

(T,U)->void

ObjIntConsumer< T >,ObjLongConsumer< T >,ObjDoubleConsumer< T >

BiFunction< T,U,R >

(T,U)->R

ToIntBiFunction< T,U >,ToLongBiFunction< T,U >,ToDoubleBiFunction< T,U >

  前四个都介绍了,后面的其实都差不多,只是参数数量和类型以及返回值类型有差异:

  1. UnaryOperator:一元操作器,一个参数一个返回值,类似于Function,不过参数和返回值类型一致。
  2. BinaryOperator:二元操作器,两个参数一个返回值,类型一致。
  3. BiPredicate:二元断言,传递两个可以不同类型的参数,返回一个boolean类型。
  4. BiConsumer:二元消费,传递两个可以不同类型的参数,无返回值。
  5. BiFunction:二元函数,传递两个可以不同类型的参数,一个返回值可以是不同类型。

下面我们正式学习lambda的语法!

3 Lambda的语法

3.1 具体格式

  lambda表达式的标准格式为

(参数类型 参数名称, 参数类型 参数名称) ‐> { 代码语句 }

  小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。-> 是新引入的语法格式,代表指向动作。大括号内的语法与传统方法体要求基本一致。
  比如对Comparator接口使用匿名内部类对象和lambda表达式:

//使用传统匿名内部类
Comparator<Integer> comparable1 = new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1 - o2;
    }
};

//使用lambda表达式标准语法
Comparator<Integer> comparable2 = (Integer o1, Integer o2) -> {
    return o1 - o2;
};

  当然,如果使用idea,那么可能会提示你这个lambda表达式还有更精简的写法。

//使用lambda表达式优化语法
Comparator<Integer> comparable3 = (o1, o2) -> o1 - o2;

  可以看到,此时我们的lambda表达式更加精简了,同时也更加通俗易懂,那就是通过比较两个数的差值来比较大小!
  在lambda标准格式的基础上,使用省略写法的规则为:

  1. 小括号内参数的类型可以省略;
  2. 如果小括号内有且仅有一个参,则小括号可以省略;
  3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号和return关键字及语句的分号,即无论有没有返回值,都可以省略return。

  由此,我们可以知道,同一个lambda表达式可以对应不同的实际目标类型,比如下面的例子,同样的lambda表达式,却可以赋值给不同的类型!

//对应Comparator目标类型
Comparator<Integer> comparable3 = (o1, o2) -> o1 - o2;

//对应BinaryOperator目标类型
BinaryOperator<Integer> binaryOperator = (o1, o2) -> o1 - o2;

  实际上,lambda表达式也有自己的类型,但是它的类型是通过上下文(参数类型、返回值类型、包括泛型类型)推断得来的。在上面的Comparator的精简写法中,参数类型被省略了,因为可以通过返回值的泛型类型Integer推断出来,参数的类型也一定是Integer类型,另外通过返回的类型可以推断出这个lambda一定是Comparator类型。
  在下面的lambda表达式中,参数类型同样被省略了,因为可以通过cmp方法的第一个参数可以推断出,参数类型一定是Byte类型,并且根据第一个参数的目标类型可以推断出这个lambda表达式一定是Comparator类型!

@Test
public void test1() {
    cmp((o1, o2) -> (o1 - o2), (byte) 1, (byte) 1);
}

public int cmp(Comparator<Byte> comparator, byte l1, byte l2) {
    return comparator.compare(l1, l2);
}

  这里的上下文推断就类似于JDK1.7出现的针对集合的类型推断<>符号:

//JDK1.7开始,右侧构造器可以使用<>当作泛型推断
List<String> strings = new ArrayList<>();

  只不过Java8的时候对类型推断做了进一步增强,使用上下文推断可以在使用Lambda表达式时用来推断合法的Lambda表达式的类型的上下文,而不必在代码中强制转型或者注明类型!可推导即可省略!

3.2 使用要求

  Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
  2. 使用Lambda必须具有上下文推断。也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
  3. 在 Lambda 表达式中不允许声明一个与局部变量同名的参数或者局部变量。
  4. 在 Lambda 表达式中,允许引用最终变量、静态变量、局部变量,但是只允许修改静态变量,以及对象类型的局部变量的属性(这要求后面的代码不会修改这个局部变量的引用指向),对于局部变量本身的引用指向以及基本类型的变量则不允许修改
  5. 对应第四条的另一种解释,lambda表达式的局部变量可以不用声明为final,但是实际上是具有隐式的final的的语义,即必须不可被后面的代码修改,否则会编译错误。

  为什么会对局部变量有这些限制呢? 主要是因为对象类型局部变量的引用以及基本类型的局部变量都保存栈上,存在某一个线程之中,如果Lambda可以直接访问并修改栈上的变量,并且Lambda是在另一个线程中使用的,那么使用Lambda的线程可能会在分配该变量的线程将这个变量收回之后,继续去访问该变量。因此,Java在访问栈上的局部变量时,实际上是在访问它的副本,而不是访问原始变量,从而造成线程不安全的可能,特别是并行运算的时候。但是如果局部变量仅仅被最开始赋值一次,以后不会再次变动,那就没有这种隐患了——因此就有了这个限制,即局部变量除了最开始的赋值之后都是读操作,而没有写操作,那么可以读取这个局部变量,相当于final的语义了。
  由于对局部变量的限制,Lambda表达式在 Java 中又称为闭包或匿名函数。它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但是它们不能修改定义Lambda的方法的局部变量的内容,这些变量必须是隐式最终的。因此可以认为Lambda是对值封闭,而不是对变量封闭,因为可以访问局部变量,但不可修改值。为什么对象类型的额局部变量的属性可以修改呢?因为它们保存在堆中,而堆是在线程之间共享的!因此我们如果需要在lambda中修改某个基本变量,那么可以使用该变量的包装类。然后再修改属性值即可。
  关于变量的测试案例如下:

//静态全局变量
static int k = 1;
static AtomicInteger stinteger = new AtomicInteger(1);

@Test
public void test3() {
    int i = 1;
    Object o = new Object();
    AtomicInteger integer = new AtomicInteger(1);
    //使用lambda表达式标准语法
    Comparator<Integer> comparable = (Integer o1, Integer o2) -> {
        //在后面的语句中不会修改这个局部变量的值时,可以在lambda中访问基本局部变量,但是不可操作值
        int b = i;
        System.out.println(i);

        //在后面的语句中不会修改这个对象局部变量的引用指向时,可以操作或者访问这个对象的属性
        integer.addAndGet(1);
        integer.get();
        integer.set(10);

        //静态变量的引用指向可以修改
        stinteger = new AtomicInteger(2);
        //静态变量的值可以修改
        k = 2;
        return o1 - o2;
    };

    //在后面的语句中改变基本局部变量的值之后,lambda中对该变量的任何访问操作都将编译不通过
    // i = 2;

    //在后面的语句中改变对象局部变量的引用指向之后,lambda中对该变量的任何访问操作都将编译不通过
    //integer= new AtomicInteger(1);

    //在后面的语句中可以操作或者访问这个对象的属性
    integer.set(15);

    //静态变量的引用指向可以修改
    stinteger = new AtomicInteger(3);
    //静态变量的值可以修改
    k = 3;
}

static class Run implements Runnable {

    @Override
    public void run() {

    }
}

4 方法引用

  到此之前,我们已经会使用Lambda表达式创建匿名方法,自己实现方法体,但是有时候,我们的Lambda表达式可能仅仅调用一个已存在的方法,而不做任何其它事,对于这种情况,通过一个方法名字来引用这个已存在的方法会更加清晰,Java 8的方法引用允许我们这样做。方法引用简单的格式通过引用一个已经存在的方法,同时实现了代码的复用,进一步简化了lambda的复杂度。
  方法引用的目标很明显,因为方法可以看作一个已经存在的定义好的函数,当我们要传递的函数已经被某个方法实现了的时候,那么则可以通过双冒号"::"操作符来引用该方法作为 Lambda 的替代者。

/**
 1. @author lx
 */
public class User {
    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }


    public static void main(String[] args) {
        //获取User的age
        //lambda标准格式的优化写法,内部只是引用了一个方法
        Function<User, Integer> userIntegerFunction1 = o -> o.getAge();

        //获取User的age
        //使用方法引用之后的写法,更加精简
        Function<User, Integer> userIntegerFunction2 = User::getAge;
    }
}

  方法引用同样可以使用传递的参数类型和参数个数进行推导。比如上面的lambda表达式可知道参数o的类型为User,同时它调用了getAge方法,返回一个Inteer。因此可以直接使用方法引用User::getAge,简化了方法的调用与参数的传递。这些都是可以推倒的,lambda遵循” 可推导即可省略”原则,或者说方法引用可以看作针对lambda的语法糖!
  怎么才能将lambda表达式转换为方法引用呢?或者说什么情况才能使用方法引用呢?

  1. 要求Lambda 表达式的方法体中只有一句话,并且这句话就是调用另一个方法,此时就可能使用方法引用代替手动调用该方法。
  2. 特殊情况下,如果抽象方法的第一个参数就是内部调用该方法的实例,那么被调用的方法与函数式接口中的抽象方法的参数个数可以不相同,但是要求后面的参数和方法参数的顺序一致,类型相同(或者兼容)。如果不是这种特殊情况,那么还要求被调用的方法与函数式接口中的抽象方法的参数个数和顺序一致,类型相同(或者兼容)。
  3. 被调用的方法与函数式接口中的抽象方法返回值类型相同(或者兼容),与方法名无关。

  方法引用有很多种,它们的语法如下,都需要遵循上面的原则:

  1. 类型上的静态方法引用:ClassName::methodName
  2. 实例上的实例方法引用:instanceReference::methodName
  3. 类型上的实例方法引用:ClassName::methodName
  4. 超类实例上的实例方法引用:super::methodName
  5. 构造方法引用:ClassName::new
  6. 数组构造方法引用:TypeClassName [ ]::new

  方法引用案例如下:

/**
 * @author lx
 */
public class Person {
    private int age;

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }


    public static void print() {
        System.out.println("静态方法");
    }

    public static Person instance() {
        return new Person();
    }


    public Integer gett(Object str, Integer integer) {
        setAge(integer);
        return getAge();
    }


    public Integer gett(int integer, Object str) {
        setAge(integer);
        return getAge();
    }

    @Test
    public void test() {
        //一个user实例
        Person user = new Person();

        //类型上的实例方法引用:ClassName::methodName
        Function<Person, Integer> userIntegerFunction2 = Person::getAge;

        //类型上的实例方法引用:ClassName::methodName
        Consumer<Person> userConsumer2 = System.out::println;



        //实例上的实例方法引用:instanceReference::methodName
        Supplier<Integer> supplier = user::getAge;



        //类型上的静态方法引用:ClassName::methodName
        Supplier<Person> userSupplier = Person::instance;

        //类型上的静态方法引用:ClassName::methodName
        Print print = Person::print;



        //超类实例上的实例方法引用:super::methodName
        Supplier<Class> SupSupplier = super::getClass;




        //构造方法引用:ClassName::new
        Supplier<Object> newSupplier = Person::new;




        //数组构造方法引用:TypeClassName[]::new
        Function<Integer, Person[]> arrayFunction = Person[]::new;



        
        //如果内部调用gett(String str, Object integer)方法
        ThFunction<Person, Integer, Integer, String> thFunction1 = new ThFunction<Person, Integer, Integer, String>() {
            @Override
            public Integer apply(Person o, Integer o2, String o3) {
                return o.gett(o3, o2);
            }
        };
        //那么不能使用方法引用
        //因为虽然抽象方法的第一个参数就是内部调用该方法的实例,后面的参数和方法参数的顺序不一致
        //参数顺序不一致,即  o2 o3  ->  o3 o2
        ThFunction<Person, Integer, Integer, String> thFunction2 = (o, o2, o3) -> o.gett(o3, o2);


        //如果内部调用gett(Integer integer, Object str)方法
        ThFunction<Person, Integer, Integer, String> thFunction3 = new ThFunction<Person, Integer, Integer, String>() {
            @Override
            public Integer apply(Person o, Integer o2, String o3) {
                return o.gett(o2, o3);
            }
        };
        //那么能使用方法引用
        //因为抽象方法的第一个参数就是内部调用该方法的实例,后面的参数和方法参数的顺序一致,类型兼容
        //参数顺序一致,即  o2 o3  ->   o2 o3
        ThFunction<Person, Integer, Integer, String> thFunction4 = Person::gett;
    }


    /**
     * 自定义函数式接口,无参数无返回值
     */
    @FunctionalInterface
    public interface Print {
        /**
         * 输出
         */
        void print();
    }

    @FunctionalInterface
    public interface ThFunction<T, U, R, K> {
        R apply(T t, U u, K k);
    }
}

5 默认方法和静态方法

5.1 概述

  此前,Java中的接口不能有非抽象方法,并且实现接口的类必须为接口中定义的每个方法提供一个实现,一旦类库的设计者需要更新接口,向其中加入新的方法,这种方式就会出现问题,这会导致所有的实现类必须实现新的方法,虽然我们可以提供一个骨干实现的抽象类,但是仍然不能根本的解决问题,比如其他Guava和Apache Commons提供的集合框架,会同时修改大量代码!
  Java8开始,接口中新增的方法可以不需要实现类必须实现,因为接口支持两种新类型的方法及其实现,一种是静态方法,通过static关键字标识,表示这个方法可以通过接口直接调用,这个方法时是属于该接口的;另一个就是非常重要的默认方法,通过default关键字标识,并且接口提供了方法的默认实现,实现接口的类如果不显式地提供该方法的具体实现,就会自动继承默认的实现。这就类似于继承了一个普通方法,这样每次接口新增的方法可以设置为默认方法,它的实现类也不再需要改动代码,保证新方法和源代码的兼容!
  实际上静态方法和默认方法被大量的用来支持lambda表达式的复杂写法与复合逻辑,后面我们会介绍到!

5.2 问题及解决

  在Java8之前,一个类可以实现多个接口,即使有同名方法也没关系,因此抽象方法没有具体的行为,子类必须有针对抽象方法自己的实现。Java8之后,由于接口拥有了默认方法,也就是说接口提供了方法的默认行为,子类可以不选择实现而直接使用接口提供的实现。
  实际上Java8接口允许了默认方法之后,Java已经在某种程度上实现了多继承,所以不光带来了多重继承的好处,还带来了多重继承的问题。如果一个类实现的多个接口中都具有相同方法签名的默认方法,那么这个实现类将无法通过方法签名选择具体调用哪一个接口的默认实现,此时就可能会出现问题!
  如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了非抽象方法,通过下面规则尝试判断具体调用哪一个方法:

  1. 本类重写的方法优先级最高。
  2. 否则,一个类同时实现了类或者接口,并且类和接口具有相同的签名的方法,那么父类中声明的方法的优先级高于任何接口中声明的默认方法的优先级。
  3. 否则,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B接口继承了A接口,那么B接口就比A更加具体。
  4. 否则,继承了多个接口的类必须显式指定的调用某个父接口的默认方法实现。

其他注意:

  1. 如果一个类实现了抽象类和接口,并且接口中具有和抽象类中的抽象方法同样方法签名的默认方法,此时子类任然需要实现这个抽象方法,而不能使用接口的默认方法作为实现!
  2. 如果一个类实现的接口之间存在继承关系,那么该类可以手动选择调用最低级别接口的的默认实现,但是手动选择调用其他级别接口的的默认实现。
  3. Java强大的编译机制帮助我们解决了菱形继承问题,我们自己不需要解决。什么是菱形继承问题:即有个接口A,有个默认方法a(),此时子接口B、C继承了A接口,随后实现类D同时实现了B、C接口,此时在D中调用a()方法不会有问题,但是c++就有问题!
    1. 如果接口B或者C复写了方法a(),那么在D中调用的a()方法,就是B或者C的a()方法。
    2. 如果接口B和C都复写了默认方法a(),那么就会出现冲突。
    3. 如果接口B或者C复写了默认方法a(),但是变成了抽象方法,那么那么在D中必须实现该方法!

  案例:

/**
 * @author lx
 */
public class InterfaceTest {

    /**
     * 测试1  本类重写的方法优先级最高。
     */
    static class InterfaceTest1 extends InterfaceClass4 implements Interface3, Interface {
        public static void main(String[] args) {
            InterfaceTest1 interfaceTest = new InterfaceTest1();
            System.out.println(interfaceTest.handle());
        }

        /**
         * 自己重写的方法优先级最高
         */
        @Override
        public int handle() {
            return 5;
        }
    }

    /**
     * 测试2 父类中声明的方法的优先级高于任何接口中声明的默认方法的优先级。
     */
    static class InterfaceTest2 extends InterfaceClass4 implements Interface3, Interface {
        public static void main(String[] args) {
            InterfaceTest2 interfaceTest = new InterfaceTest2();
            //继承了InterfaceClass4父类,因此父类中相同方法签名的方法优先级最高
            System.out.println(interfaceTest.handle());

        }

    }


    /**
     * 测试3 最具体实现的默认方法的接口优先级最高,即最底层的子接口
     */
    static class InterfaceTest3 implements Interface3, Interface, Interface0 {
        public static void main(String[] args) {
            InterfaceTest3 interfaceTest = new InterfaceTest3();
            //Interface3接口继承了Interface借口路,因此Interface3的默认方法优先级最高
            System.out.println(interfaceTest.handle());
        }

    }


    /**
     * 测试4 上面的方式无法判断,并且编译不通过,只能手动指定
     */
    static class InterfaceTest4 implements Interface1, Interface2 {
        public static void main(String[] args) {
            InterfaceTest4 interfaceTest = new InterfaceTest4();
            //Interface3接口继承了Interface借口路,因此Interface3的默认方法优先级最高
            System.out.println(interfaceTest.handle());
        }

        /**
         * 通过 Interface1.super.handle();指定调用某个接口的默认方法
         */
        @Override
        public int handle() {
            return Interface1.super.handle();
        }
    }

    /**
     * 菱形继承问题,编译通过,运行正常
     * 这实际上就是c++的菱形继承问题,但是Java中帮助我们解决了,我们自己不需要解决
     * 它会自动递归向上查找,找到Interface4和Interface5的共同父接口Interface,然后调用里面的方法,而c++则会抛出异常
     */
    static class InterfaceTest5 implements Interface4, Interface5 {
        public static void main(String[] args) {
            InterfaceTest5 interfaceTest = new InterfaceTest5();
            //Interface3接口继承了Interface借口路,因此Interface3的默认方法优先级最高
            System.out.println(interfaceTest.handle());
        }
    }


    /**
     * 注意1  如果一个类实现了抽象类和接口,并且接口中具有和抽象类中的抽象方法同样方法签名的默认方法
     * 此时子类仍然需要实现这个抽象方法,而不能使用接口的默认方法作为实现,否则编译不通过!
     */
    static class InterfaceTestt1 extends InterfaceClass5 implements Interface3, Interface {
        public static void main(String[] args) {
            InterfaceTestt1 interfaceTest = new InterfaceTestt1();
            //继承了InterfaceClass4父类,因此父类中相同方法签名的方法优先级最高
            System.out.println(interfaceTest.handle());

        }


        //仍然需要实现这个抽象方法
        @Override
        public int handle() {
            return 0;
        }
    }

    /**
     * 注意2  2 如果一个类实现的接口之间存在继承关系
     * 那么该类可以手动选择调用最低级别接口的的默认实现,但是手动选择调用其他级别接口的的默认实现。
     */
    static class InterfaceTestt2 implements Interface3, Interface, Interface0 {
        public static void main(String[] args) {
            InterfaceTestt2 interfaceTest = new InterfaceTestt2();
            //继承了InterfaceClass4父类,因此父类中相同方法签名的方法优先级最高
            System.out.println(interfaceTest.handle());

        }

        /**
         * 可以手动选择调用Interface3的方法
         * 不能可以手动选择调用Interface和Interface0的方法
         */
        @Override
        public int handle() {
            return Interface3.super.handle();
//            return Interface.super.handle();
//            return Interface0.super.handle();
        }

    }


}

interface Interface0 {
    default int handle() {
        return -1;
    }
}

interface Interface extends Interface0 {
    @Override
    default int handle() {
        return 0;
    }
}

interface Interface2 {
    default int handle() {
        return 2;
    }
}

interface Interface1 {
    default int handle() {
        return 1;
    }
}

interface Interface3 extends Interface {
    @Override
    default int handle() {
        return 3;
    }
}

interface Interface4 extends Interface {

}

interface Interface5 extends Interface {

}


class InterfaceClass4 {
    public int handle() {
        return 4;
    }
}

abstract class InterfaceClass5 {
    abstract int handle();
}

6 Lambda的复合

  由于lambda相当于一个函数或者行为,因此Java8允许把多个简单的Lambda复合成复杂的表达式,将简单的函数复合成为复杂的函数,这其中就用到了上面的默认方法和静态方法。

6.1 Comparator比较器复合

返回

方法

描述

static < T,U extends Comparable<? super U >> Comparator< T >

comparing(Function< ? super T,? extends U > keyExtractor)

使用指定的keyExtractor提取需要比较的键,返回一个自然排序比较器。

static < T,U > Comparator< T >

comparing(Function< ? super T,? extends U > keyExtractor, Comparator< ? super U > keyComparator)

使用指定的keyExtractor提取需要比较的键,返回一个指定排序的比较器。

default Comparator< T >

reversed()

返回一个与调用比较器相反的比较器

static < T extends Comparable<? super T >> Comparator

reverseOrder()

返回一个与 自然排序相反的比较器。

default Comparator< T >

thenComparing(Comparator< ? super T > other)

当调用比较器比较两个对象相等时使用另一个参数副比较器进行比较。

default < U extends Comparable<? super U >> Comparator< T >

thenComparing(Function< ? super T,? extends U > keyExtractor)

当调用比较器比较两个对象相等时使用另一个参数副比较器进行比较。使用指定的keyExtractor提取需要比较的键

  Java8开始,支持Comparator比较器的复合,添加了许多静态方法和默认方法。主要有reversed方法,该方法用于返回一个与调用比较器相反排序顺序的比较器,以及thenComparing方法,该方法类似于复合比较器,调用方法的比较器作为主要比较器,参数比较器作为副比较器,如果主比较器比较相等,那么使用副比较器比较排序!

/**
 * @author lx
 */
public class CompositeTest {

    ArrayList<User> userArrayList = new ArrayList<>();

    @Before
    public void beforeTest() {
        User user4 = new User(20, "da");
        User user1 = new User(25, "张三");
        User user3 = new User(20, "张小三");
        User user2 = new User(25, "tom");

        userArrayList.add(user1);
        userArrayList.add(user2);
        userArrayList.add(user3);
        userArrayList.add(user4);
    }


    /**
     * 比较器复合
     */
    @Test
    public void test() {

        /*1 首先是通过user的age比较大小并顺序排序,我们使用JDK8提供的comparingInt静态方法*/
        Comparator<User> userComparator = Comparator.comparingInt(User::getAge);
        userArrayList.sort(userComparator);
        System.out.println("age顺序:" + userArrayList);

        //上面的comparingInt方法就相当于下面的两个表达式
        //实际上这里的ToIntFunction用来提取需要比较的两个对象的的属性
        //只不过这两个操作被封装到了comparingInt方法中,因此我们只需要传递一个ToIntFunction的函数即可
        ToIntFunction<User> toIntFunction = value -> value.getAge();
        Comparator<User> userComparator1 = (c1, c2) -> Integer.compare(toIntFunction.applyAsInt(c1), toIntFunction.applyAsInt(c2));


        /*
         * 2 现在如果我们需要进行逆序排序
         * 此时我们只需要将以前的比较器顺序反过来就行了
         * 比较器调用reversed方法,返回的比较器将会使用和调用比较器相反的顺序。
         */
        Comparator<User> reversed = userComparator.reversed();
        userArrayList.sort(reversed);
        System.out.println("age逆序:" + userArrayList);

        /*
         * 3 有时候我们需要进行多个参数的比较
         * 现在我们需要在age相等的基础上再比较姓名长度并顺序排序
         * 此时我们可以调用thenComparing方法,传递一个比较器这表示将两个比较器复合
         * 调用方法的比较器被看作主要比较器,参数的比较器看作副比较器
         * 或者更进一步,我们类似于第一个获取比较器的方式,传递一个能共提取比较的键的ToIntFunction
         */
        Comparator<User> userComparator2 = reversed.thenComparingInt(o -> o.getName().length());
        userArrayList.sort(userComparator2);
        System.out.println("age逆序-name长度顺序:" + userArrayList);


    }


    public class User {
        private int age;
        private String name;

        public int getAge() {
            return age;
        }

        public void setAge(int age) {
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }


        public User() {
        }

        public User(int age, String name) {
            this.age = age;
            this.name = name;
        }

        @Override
        public String toString() {
            return "User{" +
                    "age=" + age +
                    ", name='" + name + '\'' +
                    '}';
        }
    }
}

6.2 Function函数复合

返回

方法

描述

default < V > Function< T,V >

andThen(Function< ? super R,? extends V > after)

返回一个复合函数,首先将该函数应用于其输入,然后将 after函数应用于结果。

default < V > Function< V,R >

compose(Function< ? super V,? extends T > before)

返回一个复合函数,首先将 before函数应用于其输入,然后将此函数应用于结果。

static < T > Function< T,T >

identity()

返回一个总是返回其输入参数的函数。

  andThen和compose的运算顺序是相反的。andThen方法中,先执行调用者函数的计算,然后将结果作为参数传给after参数函数,最后执行after参数函数的计算。compose方法中,先执行before参数函数的计算,然后将结果作为参数传给调用者函数,最后执行调用者函数的计算。

/**
 * 函数复合 andThen计算
 */
@Test
public void andThen() {
    User user1 = new User(20, "小花");
    //这个函数根据传入的user获取age
    Function<User, Integer> f1 = User::getAge;
    //这个函数根据传入的int值创建一个user
    Function<Integer, User> f2 = integer -> new User(integer++);


    //将函数f1和f2复合,即获取传入user的age创建一个新的user,获得新的函数f3
    //可以看到f3的参数就是f1的参数,f3的返回类型,就是f2的返回类型,相当于将f1和f2串联了起来
    //先执行f1,然后将结果传给f2,最后执行f2
    Function<User, User> f3 = f1.andThen(f2);
    System.out.println(f3.apply(user1));
}


/**
 * 函数复合 compose计算
 */
@Test
public void compose() {
    //这个函数根据传入的user获取age
    Function<User, Integer> f1 = User::getAge;
    //这个函数根据传入的int值创建一个user
    Function<Integer, User> f2 = integer -> new User(integer++);


    //将函数f1和f2复合,即获取传入user的age创建一个新的user,获得新的函数f3
    //可以看到f3的参数就是f2的参数,f3的返回类型就是f1的返回类型,相当于将f2和f1串联了起来
    //先执行f2,然后将结果传给f1,最后执行f1
    Function<Integer, Integer> f3 = f1.compose(f2);

    System.out.println(f3.apply(10));
}

6.3 Consumer消费复合

返回

方法

描述

default Consumer< T >

andThen(Consumer< ? super T > after)

返回一个复合的 Consumer ,按顺序执行该操作,然后执行 after操作。

  Consumer的andThen方法相当于按照顺序对最开始传递的参数进行一系列计算。先在调用者里面执行参数计算,然后将参数传给after,最后执行在after里面执行参数计算。

/**
 * 消费复合 Consumer的andThen计算
 */
@Test
public void andThenCom() {
    User user = new User(20, "小花");
    //一个Consumer设置名字
    Consumer<User> c1 = o -> o.setName("花小");
    //一个Consumer设置年龄
    Consumer<User> c2 = o -> o.setAge(10);
    //组合
    //先在调用者里面执行参数计算,然后将参数传给after,最后执行在after里面执行参数计算。
    Consumer<User> c3 = c1.andThen(c2);
    c3.accept(user);
    System.out.println(user);

    //链式编程写法
    c1.andThen(o -> o.setAge(10)).accept(user);
}

6.4 Predicate断言复合

返回

方法

描述

default Predicate< T >

and(Predicate< ? super T > other)

返回一个组合断言,表示两个断言的&&连接

default Predicate< T >

negate()

返回一个非断言,表示目前断言的!关系

default Predicate< T >

or(Predicate< ? super T > other)

返回一个组合断言,表示两个断言的||连接

  断言型接口Predicate内部提供了and、negate、or默认方法,用于“&&(与)、!(非)、||(或)”的方式连接两个断言!
  调用断言的结果将会先被计算,随后与参数断言的结果进行比较,调用断言在前,参数断言在后!

/**
 * 断言复合
 */
@Test
public void predicate() {
    User user = new User(20, "小花");
    //一个断言用于判断年龄是否大于20
    Predicate<User> p1 = (User u) -> u.getAge() >= 20;
    //一个断言用于判断姓名长度是否大于等于3
    Predicate<User> p2 = (User u) -> u.getName().length() >= 3;

    //组合
    //&&
    Predicate<User> and = p1.and(p2);
    System.out.println("年龄大于等于20并且姓名长度大于等于3:---" + and.test(user));

    //||
    Predicate<User> or = p1.or(p2);
    System.out.println("年龄大于等于20或者姓名长度大于等于3:---" + or.test(user));

    //!
    Predicate<User> negate = p1.negate();
    System.out.println("年龄小于20:---" + negate.test(user));


    
    User user2 = new User(19, "花花花");
    //多重组合
    //年龄大于等于20并且年龄大于3,或者姓名等于花花花
    System.out.println(p1.and((User u) -> u.getName().length() > 3).or((User u) -> "花花花".equals(u.getName())).test(user2));
    //年龄大于等于20并且年龄大于等于3,或者姓名等于花花花
    System.out.println(p1.and((User u) -> u.getName().length() >= 3).or((User u) -> "花花花".equals(u.getName())).test(user2));
}

7 Lambda与匿名内部类

  在Java8引入lambda之前,我们使用匿名内部类来完成“避免”类的创建,之后我们使用lambda来代替函数式接口的匿名内部类以及对象的创建,它们之间从上层特性到底层原理都有很多的不同:

  1. 关键字 this

    1. 匿名内部类中的 this 就是代表当前匿名类对象。
    2. 在lambda表达中引用this关键字,和在lambda外部引用的意义一样。因为lambda不是内部类对象,那么在lambda内部引用this也就和内部类没什么关系了。

    /**

    • @author lx

    */ public class ThisTest {

    public static void main(String[] args) {
        ThisTest thisTest = new ThisTest();
        System.out.println("thisTest对象:"+thisTest);
        thisTest.thisTest();
    }
    
    public void thisTest() {
        System.out.println("----------------");
        ThisTest th=this;
        System.out.println("当前this对象:"+this);
    
    
        System.out.println("----------------");
        Comparator<Integer> comparator = new Comparator<Integer>() {
            @Override
            public int compare(Integer o1, Integer o2) {
                System.out.println("匿名内部类的this:"+this);
                return o1.compareTo(o2);
            }
        };
        comparator.compare(1, 2);
    
    
        System.out.println("----------------");
        Comparator<Integer> integerComparator = (x, y) -> {
            System.out.println("lambda的this:"+this);
            System.out.println(th==this);
            return x.compareTo(y);
        };
        integerComparator.compare(1, 2);
    }
    

    }

  2. 应用范围:

    1. 匿名内部类可以为任意接口创建实例。不管接口中包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可,也可以为抽象类甚至普通类创建实例。
    2. lambda表达式只能为函数式接口创建实例。
  3. 方法调用

    1. 匿名内部类实现抽象方法的方法体中允许调用接口中定义的默认方法;但Lambda表达式的代码块中不允许调用接口中定义的默认方法。
    2. 匿名内部类内调用与外部类有相同签名的方法时,实际调用的是该匿名内部类实例的方法。而lambda调用与外部类有相同签名的方法是,实际调用的是外部类实例的方法。
  4. 底层实现

    1. 虽然匿名内部类的使用避免了我们手动创建类,但实际上仍然会被编译器编译成一个.class文件,相当于是帮我们了一个创建一个独立的类,文件命名方式为:主类名+ + + + + +++(1.2.3…)。而在程序启动的时候,JVM会对用到的全部类进行加载、验证、准备、解析、初始化等操作,匿名内部类生成的类文件也不例外,因此大量的内部类文件的创建将会影响应用程序启动执行的性能。
    2. 对于lambda表达式,Java编译器使用Java7引入的 invokedynamic 字节码指令(为支持动态类型语言新增的指令)。invokedynamic指令不会在编译时就进行类型检查而产生新的类文件,而是将lambda表达式的字节码类型检查转换操作推迟到了第一次调用时,相当于一个调用点,仅在lambda表达式被首次调用的时候(执行到invokedynamic调用点),才会通过反射创建一个匿名的lambda实现类以及对象,之后的调用也都会跳过这一步骤,没有了程序启动时就进行的类加载过程,而是第一次用到的时候才会进行相应的类动态创建工作,自然提升了性能。

    /**

    • @author lx

    */ public class LambdaInvoke { public static void main(String[] args) { classTest(new Comparator() { @Override public int compare(Integer o1, Integer o2) { return o1.compareTo(o2); } }); lambdaTest((Comparator) Integer::compareTo); } public static void classTest(Comparator comparator) {} public static void lambdaTest(Comparator comparator) {} }

  使用Javap -v 查看class文件的字节码,可以发现匿名内部类对象的创建工作就是使用了一般的new指令而已,这说明这个匿名类在程序启动的时候就被加载进来了,而lambda表达式则使用了invokedynamic指令,对应的类以及对象会在执行时被动态的加载。
Java8—一万字的Lambda表达式的详细介绍与应用案例

8 总结

  lambda为Java这种面向对象的语言带来了函数式编程的写法,改变了Java只能面像对象的局限性,某些情况下能够极大地减少代码量。Java引入lambda的目的并不是为了完全取代面向对象编程,而是为了方便我们使用混合开发方式,在合适的情况下采用合理的编程方式,能够有效的提高开发效率!
  lambda的另一个重要应用就是同样在Java8新增的Stream API中,几乎都可以使用函数式接口与lambda作为参数完成功能强大的流式编程!
   Stream API中才是lambda大展身手的地方!

相关文章:
  Stream:Java8—两万字的Stream流的详细介绍与应用案例

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
3年前
HIVE 时间操作函数
日期函数UNIX时间戳转日期函数: from\_unixtime语法:   from\_unixtime(bigint unixtime\, string format\)返回值: string说明: 转化UNIX时间戳(从19700101 00:00:00 UTC到指定时间的秒数)到当前时区的时间格式举例:hive   selec
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这