Java8新特性:函数式接口

1. 函数式接口

函数式接口:有且仅有一个抽象方法的接口。
函数式接口用于函数式编程的场景,对Java来说就是lambda表达式,只有确保接口中有且仅有一个抽象方法,lambda表达式才能正确推导。
函数式接口的lambda表达式,就像是匿名内部类的另一种表现形式。

1.1 函数式接口格式

interface FunctionalInterface1 {
	void test();
}

1.2 @FunctionalInterface

该注解可以作用在一个接口上,用于检测接口是否符合函数式接口的规范,即是否有且仅有一个抽象方法。如果不符合要求,就会报错。
该注解仅用于检查编写规范,但是只要是符合“有且仅有一个抽象方法”要求的接口,即使不使用该注解,它也是一个函数式接口。

1.3 自定义函数式接口

public class Demo01 {
    public static void main(String[] args) {
        // 使用函数式接口,后面的lambda表达式,就是对接口唯一抽象方法的实现
        Demo01Interface lambda = () -> System.out.println("functional interface test");
        lambda.test();
    }
}
// 函数式接口
@FunctionalInterface
interface Demo01Interface {
    void test();
}

2. 函数式编程

上面的例子可以看到,我们将lambda表达式看做一个对象,并且定义一个变量引用它。在面向对象的基础上,Java通过lambda表达式和方法引用,进行函数式编程。

2.1 lambda表达式的延迟执行

有些语句被执行后,结果不一定会被使用,造成了性能浪费。
lambda表达式是延迟执行的,从这方面,可以提高利用率,提升性能。

性能浪费日志案例

打印日志信息可以快速定位问题的位置,记录程序运行的状况。
如果我们在日志的打印的方法中,进行信息的拼接,在满足条件的情况下再进行输出

public class Demo02 {
	public static void main(String[] args) {
		String pre = "Debug: ";
		String content = "this is logger demo. ";
		String suf = "currentTime: " + LocalDateTime.now();
		// 调用日志打印方法
		log(1, pre + content + suf);
	}
	// 日志打印方法
	public static void log(int level, String msg) {
		if (level == 1) {
			System.out.println(msg);
		}
	}
}

这段代码的问题:无论level是否符合要求,log方法的第二个参数都会进行字符串的拼接。如果级别不符合要求,那么字符串拼接操作就白做了,存在性能浪费。

解决方式1 – 可变参数

/**
* 日志打印方法-可变参数
* 将字符串作为可变参数传入,在符合条件的情况下再进行遍历拼接
* /
public static void log(int level, String... msgs) {
	if (level == 1) {
		StringBuilder sb = new StringBuilder();
		for (String msg : msgs) {
			sb.append(msg);
		}
		System.out.println(sb);
	}
}

解决方式2 – labmda表达式写法

使用lambda表达式,需要一个函数式接口

public class Demo03LambdaLogger {
    public static void main(String[] args) {
        String pre = "Debug: ";
        String content = "this is logger demo. currentTime: ";
        String suf = LocalDateTime.now().toString();
        // 使用函数式接口,构造消息
        MessageBuilder builder = () -> pre + content + suf;
        // 日志打印
        log(1, builder);
    }

    public static void log(int level, MessageBuilder builder) {
        if (level == 1) {
            String msg = builder.build();
            System.out.println(msg);
        }
    }
}

/**
 * 用于构建日志信息的函数式接口
 */
interface MessageBuilder {
    // 构建日志消息方法
    String build();
}

验证lambda表达式的延迟执行: 在编写lambda表达式时,添加打印语句,并将level调整为非1的值,看是否进行打印

MessageBuilder builder = () -> {
	System.out.println("进行消息构建...");
	return pre + content + suf;
};
log(0, builder);

2.2 使用lambda表达式作为参数和返回值

抛开原理,Java中的lambda表达式可以被当做匿名内部类的替代。如果方法的参数是一个函数式接口类型,那么就可以使用lambda表达式替代;

lambda表达式作为参数
例如Runnable接口只有一个run方法,就是一个函数式接口。如果有一个方法的参数为Runnable类型,那么就可以传入一个lambda表达式作为参数。

static void startWork(Runnable runnable) {
    new Thread(runnable).start();
}

public static void main(String[] args) {
    startWork(()-> System.out.println("task is running..."));
}

lambda表达式作为返回值
如果一个方法的返回值是一个函数式接口,那么就可以直接返回一个lambda表达式。当需要通过一个方法获取java.util.Comparator接口类型的对象作为排序器时,就可以调该方法获取。

public static void main(String[] args) {
   // 创建list集合并使用比较器进行排序
   List<String> list = new ArrayList<>();
   list.add("aaaa");
   list.add("aaa");
   list.add("aa");
   list.add("a");
   Collections.sort(list, getComparator());
   System.out.println(list);
}
// 获取比较器,这里直接返回一个lambda表达式
static Comparator<String> getComparator() {
   // 比较字符串的长度进行排序
   return (a, b) -> a.length() - b.length();
}

3. Java提供的常用函数式接口

JDK提供了很多的函数式接口以及丰富的使用场景,它们主要在 java.util.function 包中。下面是最简单的几个接口及使用示例。

3.1 Supplier

java.util.function.Supplier<T> 接口仅包含一个无参方法 T get()。用来获取一个泛型参数指定类型的对象数据。由于这是一个函数式接口,也就意味着这个方法需要使用lambda表达式向外提供一个T类型的对象。

public class Demo06Supplier {
    public static void main(String[] args) {
        String s = getStringFromSupplier(() -> "string from supplier");
        System.out.println(s);
    }

    static String getStringFromSupplier(Supplier<String> supplier) {
        return supplier.get();
    }
}

练习:求数组元素的最大值

public class Demo07SupplierPractice {
    public static void main(String[] args) {
        int[] nums = {1, 4, 2, 7, 5};
        // 调用getMaxValue方法,传递一个lambda表达式作为参数,lambda表达式定义如何产生这个最大值
        int max = getMaxValue(() -> {
           int maxTemp = nums[0];
           for (int num : nums) {
                if (num > maxTemp) {
                    maxTemp = num;
                }
           }
           return maxTemp;
        });
        System.out.println(max);
    }
    // 定义一个方法,参数为 Supplier
    static Integer getMaxValue(Supplier<Integer> supplier) {
        return supplier.get();
    }
}

3.3 Comsumer

java.util.function.Comsumer<T> 接口和Supplier接口相反,它不是生产一个数据,而是消费一个数据,数据类型由泛型决定。

抽象方法: accept(T t)

Consumer接口中包含一个抽象方法 void accept(T t),意为消费一个指定泛型类型的数据。如:

public class Demo08Consumer {
    public static void main(String[] args) {
        // 调用方法,传递lambda表达式,定义如何消费这一字符串
        consumeString( s -> {
            System.out.println("消费方式--将其分割为数组,并打印数组中的元素...");
            String[] strings = s.split(" ");
            for (String s1 : strings) {
                System.out.println(s1);
            }
        });
    }
    // 定义一个消费字符串的方法,传入Consumer作为参数
    static void consumeString(Consumer<String> consumer) {
        consumer.accept("Hello consumer");
    }
}

默认方法 andThen

如果一个方法的参数和返回值类型全是Consumer<T>类型,那么就可以实现这种效果:消费数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是Consumer接口的andThen,JDK源代码:

default Consumer<T> andThen(Consumer<? super T> after) {
    Objects.requireNonNull(after);
    return (T t) -> { accept(t); after.accept(t); };
}

实现该组合,需要两个或两个以上的lambda表达式,andThen的含义就是一步接着一步,两个步骤组合的情况:

public class Demo09ConsumerAndThen {
    public static void main(String[] args) {
        // 编写两个lambda表达式,定义对数据不同的消费方式
        Consumer<String> consumer1 = (s) -> System.out.println(s.toUpperCase() + "FIRST UPPERCASE");
        Consumer<String> consumer2 = (s) -> System.out.println(s.toLowerCase() + "second lowercase");
        consumerString(consumer1.andThen(consumer2));
    }
    // 定义一个消费字符串的方法, 参数传入Consumer对象
    static void consumerString(Consumer<String> consumer) {
        consumer.accept("two consumer:");
    }
}

练习

下面的字符串数组当中存有多条信息,请按照格式“ 姓名:XX,性别:XX。 ”的格式将信息打印出来。要求将打印姓名的动作作为第一个 Consumer 接口的Lambda实例,将打印性别的动作作为第二个 Consumer 接口的Lambda实例,将两个 Consumer 接口按照顺序“拼接”到一起。
数组:
String[] array = { “迪丽热巴,女”, “古力娜扎,女”, “马尔扎哈,男” };

public class Demo09ConsumerPractice {
    public static void main(String[] args) {
        String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男"};
        // 传入两个lambda表达式,分别定义accept方法不同的消费方式:
        // 1.打印姓名 2.打印性别
        print(array, (s) -> System.out.print("姓名:" + s.split(",")[0]),
                    (s) -> System.out.println(",性别:" + s.split(",")[1] + "。"));

    }
	// 定义打印方法,传入两个Consumer,在方法的内部使用andThen进行组合
    static void print(String[] arr, Consumer<String> consumer1, Consumer<String> consumer2) {
        for (String str : arr) {
            consumer1.andThen(consumer2).accept(str);
        }
    }
}

3.4 Predicate

有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值的结果。这时候可以使用 java.util.function.Predicate<T> 接口。

抽象方法 test(T t)

test(T t) 用于条件判断的场景:

public class Demo10Predicate {
    public static void main(String[] args) {
        // 编写一个lambda表达式,定义test方法判断逻辑
        testStringLength(s -> s.length() > 8);
    }
    // 定义一个判断字符串长度的方法,传入一个Predicate作为参数
    static void testStringLength(Predicate<String> predicate) {
    	// 使用test方法判断字符串
        boolean bool = predicate.test("helloWorld");
        System.out.println("字符串太长了吗? " + (bool?"是的":"不是"));
    }
}

默认方法 and(Predicate<? super T> predicate)

条件判断存在常见的与、或、非三种常见的逻辑关系。其中将两个Predicate 条件使用 连接起来,实现并且效果时,可以使用default方法 and,JDK源码如下:

default Predicate<T> and(Predicate<? super T> other) {
   Objects.requireNonNull(other);
   // 可以看到,and就是将两个test方法的结果使用 && 进行了运算,并返回结果
    return (t) -> test(t) && other.test(t);
}

练习:判断一个字符串既要包含 “W” 又要包含 “H” :

public static void main(String[] args) {
boolean result = testContains(s -> s.contains("H"), s -> s.contains("W"));
    System.out.println(result?"包含W和H":"不包含或仅包含一个");
}

static boolean testContains(Predicate<String> p1, Predicate<String> p2) {
    return p1.and(p2).test("Hello World");
}

默认方法 or(Predicate<? super T> predicate)

使用此方法可进行 运算,JDK源码如下:

default Predicate<T> or(Predicate<? super T> other) {
  Objects.requireNonNull(other);
    return (t) -> test(t) || other.test(t);
}

练习:如果要实现 “字符串包含H或者W”,只需将上面的 “与” 的练习中and方法改为or即可

public static void main(String[] args) {
boolean result = testContains(s -> s.contains("H"), s -> s.contains("W"));
    System.out.println(result?"包含W和H":"不包含或仅包含一个");
}

static boolean testContains(Predicate<String> p1, Predicate<String> p2) {
    return p1.and(p2).test("Hello World");
}

默认方法 negate()

negate表示"非"的关系,即对自身的取反。默认方法negate()源码也非常简单:

default Predicate<T> negate() {
    return (t) -> !test(t);
}

从它的实现中可以看出,它是对执行过的test方法的结果进行取反,需要在test方法调用前调用negate方法。

public static void main(String[] args) {
    negateResult(s -> s.length() < 5);
}

static void negateResult(Predicate<String> predicate) {
    boolean result = predicate.negate().test("HelloWorld");
    System.out.println("字符串很长吗? " + (result?"很长":"不长"));
}

练习:集合信息的筛选

数组当中有多条 姓名+性别 的信息如下,请通过Predicate接口的拼装将符合要求的字符串删选到一个ArrayList中,需要同时满足两个条件:
1)必须为女生;2)姓名为四个字;
数组如下:

String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男", "赵丽颖,女" };

解答:

public static void main(String[] args) {
   String[] array = { "迪丽热巴,女", "古力娜扎,女", "马尔扎哈,男", "赵丽颖,女" };
    // 定义两个Predicate的表达式,第一个筛选姓名,第二个筛选性别
    Predicate<String> p1 = s -> s.split(",")[0].length() == 4;
    Predicate<String> p2 = s -> s.split(",")[1].equals("女");
    List<String> persons = filterPerson(array, p1, p2);
    System.out.println(persons);
}

static List<String> filterPerson(String[] array, Predicate<String> p1, Predicate<String> p2) {
    List<String> personList = new ArrayList<>();
    for (String person : array) {
        if (p1.and(p2).test(person)) {
            personList.add(person);
        }
    }
    return personList;
}

3.5 Function

java.util.function.Function<T, R> 接口用来根据一个类型的数据,得到另一个类型的数据,前者称为前置条件,后者称为后置条件。

抽象方法 R apply(T t)

Function中的抽象方法为 R apply(T t),根据T类型的参数获取R类型的结果。
使用场景:如将 String 类型转换为 Integer类型。当然仅仅是将String转换为Integer不必使用此接口直接使用 Integer.valueOf(String s)方法即可,这里只是作为一个演示:

public class Demo14Function {
    public static void main(String[] args) {
        // 定义lambda表示,如何操作字符串
        Function<String, Integer> function = s -> Integer.valueOf(s);
        // 调用apply方法
        Integer i = function.apply("100");
        System.out.println(i);
    }
}

默认方法 andThen(Function<T, R> function)

Function接口也有一个默认的andThen方法,用来进行组合操作。JDK源码如下:

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

使用这个方法可以定义 "先做什么,再做什么"的顺序

public class Demo15FunctionAndThen {
    public static void main(String[] args) {
        // 定义两个lambda表达式,第一个将字符串转换为整数,第二个将整数进行加100的运算
        Function<String, Integer> f1 = s -> Integer.valueOf(s);
        Function<Integer, Integer> f2 = i -> i += 100;
        Integer result = f1.andThen(f2).apply("900");
        System.out.println(result);
    }
}

练习:自定义函数模型的拼接

题目:
请使用 Function 进行函数模型的拼接,按照顺序需要执行的多个函数操作为:

  1. 将字符串截取数字年龄部分,得到字符串;
  2. 将上一步的字符串转换成为int类型的数字;
  3. 将上一步的int数字累加100,得到结果int数字。

解答:

public class Demo16FunctionPractice {
    public static void main(String[] args) {
        // 根据题意,定义3个lambda表达式
        // 1.字符串截取表达式
        Function<String, String> f1 = s -> s.split(",")[1];
        // 2.字符串转换表达式
        Function<String, Integer> f2 = s -> Integer.valueOf(s);
        // 3.字符串累加表达式
        Function<Integer, Integer> f3 = i -> i += 100;
        
        // 执行
        String str = "bulabula,30";
        Integer result = f1.andThen(f2).andThen(f3).apply(str);
        System.out.println(result);
    }
}

默认方法 compose(Function<T, R> function)

compose(Function<T, R> function) 和 andThen方法刚好相反,它是先执行参数的apply后,将结果作为调用compose方法的Function对象执行apply方法的参数,JDK源码如下:

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

使用这个方法可以定义在做什么事情之前,先做什么:

public class Demo17FunctionCompose {
    public static void main(String[] args) {
        Function<String, Integer> f1 = s -> Integer.valueOf(s);
        Function<Integer, Integer> f2 = i -> i *= 100;
        // 在执行乘法运算之前,先将字符串转换为整数
        Integer result = f2.compose(f1).apply("10");
        System.out.println(result);
    }
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值