downgoon / hello-world

hello-world for anything you want on branches or subdir
25 stars 4 forks source link

Java8 新特性集锦 #9

Open downgoon opened 6 years ago

downgoon commented 6 years ago

F1: forEach lambda expression

https://www.mkyong.com/java8/java-8-foreach-examples/

传统做法

Map<String, Integer> items = new HashMap<>();
items.put("A", 10);
items.put("B", 20);
items.put("C", 30);
items.put("D", 40);
items.put("E", 50);
items.put("F", 60);

for (Map.Entry<String, Integer> entry : items.entrySet()) {
    System.out.println("Item : " + entry.getKey() + " Count : " + entry.getValue());
}

尤其这个Map.Entry<String, Integer> 写起来很费劲?!

Java8的写法

Map<String, Integer> items = new HashMap<>();
items.put("A", 10);
items.put("B", 20);
items.put("C", 30);
items.put("D", 40);
items.put("E", 50);
items.put("F", 60);

items.forEach((k,v)->System.out.println("Item : " + k + " Count : " + v));

items.forEach((k,v)->{
    System.out.println("Item : " + k + " Count : " + v);
    if("E".equals(k)){
        System.out.println("Hello E");
    }
});
downgoon commented 6 years ago

F2: 函数指针

现有类的方法

public class UserHandler {

        public void create(Command command) throws CommandException {
            System.out.println("user create action");
        }
}
UserHandler handler = new UserHandler();
handler.create(new Command("/user/create"));

定义函数指针

函数指针只关心“函数签名”:输入参数(包括个数,顺序和类型)和返回值,相同即可。

public interface CommandHandler {

    public void exec(Command command) throws CommandException;

}

如果一个框架,需要用到某个函数,通常都是Template-Callback方式:

public CommandFramework location(String location, CommandHandler handler) {
    // do something before
    handler.exec(); // callback function pointer
   // do something after
}

传递函数指针

userHandler.create(cmd)表示调用某个函数;而userHandler::create表示引用某个函数,也就是引用函数指针。

UserHandler user = new UserHandler();
framework.location("/user/create", user::create);

为什么才有函数指针

C语言里面,到处是“函数指针”。为什么Java到Java8才有呢?因为Java从一开始就是面向对象的,有封装性:数据与对数据的操作(也就是函数或叫方法)就是封装在一起的;不像C,数据和函数是分离的,需要把数据传递到函数里面,才能执行,或者把函数传递到另一个函数。在这种情况下,“函数指针”就被理解为“只有方法,没有属性的类”,又因为Java要实现“继承与多态”机制,于是引入了interface机制,顺理成章,“函数指针”就变成了interface,尽管interface可以定义很多个方法,而“函数指针”是 只能有一个方法,那只需要我们人为的在interface里面“只定义一个方法”不就行了么?!

对于 只能有一个方法interface,JDK专门有一个约束的annotation,叫 java.lang.FunctionalInterface。 An informative annotation type used to indicate that an interface type declaration is intended to be a functional interface as defined by the Java Language Specification. Conceptually, a functional interface has exactly one abstract method.

但是通过上面的例子我们可以看到,显然还是函数指针更方便一点。不然上面的UserHandler,如果要植入到framework.location("/user/create", CommandHandler handler)里面执行,我们必须 预先UserHandler implement CommandHandler,但是UserHandler很可能在Framework之前就实现完了的,没法对UserHandler做预先要求,遇到这种情况,就得用“适配器模式”,定义一个UserHandlerCommandable的接口,把对CommandHandler的调用,转为对UserHandler的调用。形如:

framework.location("/user/remove", new CommandHandler() {

       @Override
       public void exec(Command command) throws CommandException {
                 user.remove(command); // adaptor pattern before Java8
        }
});

编程风格影响

  • 有了“函数指针”,我们可以慢慢淡忘匿名内部类适配器模式了。
  • 我们可以不再 通过传递整个对象来间接传递这个对象的一个方法,而可以直接指定只传递这个对象的一个方法了。

Java8与JavaScript越来越像

用javascript描述播放器的行为。比如暂停播放的时候,出一个广告。结束播放的时候,加载推荐内容。代码是这样的:

player.on("pause", function() {
  console.log('即将加载暂停广告...');
});

player.on("ended", function() {
  console.log('即将加载内容推荐...');
});

如果换成Java8的,函数指针描述:

player.addListener("ended", new EventListener() { @overwrite public void handleEvent(Event event) { logger.log("即将加载内容推荐..."); } });


- Java8描述

``` java
player.on("pause", () -> {
  logger.log("即将加载暂停广告...");
});

player.on("ended", event -> {
  logger.log("即将加载内容推荐...");
});

如果事件不以字符来命名,而是直接的方法,也可以:

player.pause( () -> {
  logger.log("即将加载暂停广告...");
});
downgoon commented 6 years ago

F3: lambda 表达式

本质是匿名函数

什么是lambda表达式?个人理解就是匿名函数

在Java中的函数,一般是这样的:

int sum(int x, int y) {
  return x + y;
}

可见一个函数的组成是:函数名 sum,输入参数 (int x, int y),返回值 int 和 函数体 {return x + y }

所谓匿名函数就是“去掉函数名”:

(int x, int y) -> { return x + y; }

因为去掉了函数名,返回值不知道在哪声明,就变成自动识别的隐形类型。

官方对lambda表达式的定义是:

a function (or a subroutine) defined, and possibly called, without being bound to an identifier。

函数指针与Lambda表达式

因为Lambda表达式本质是匿名函数,它自己没有名字,但本质上还是个函数,所以能赋值给函数指针。 这点非常类似javascript中的匿名函数,也能赋值给函数对象。

public class HelloLambda {

    static interface SumFunctionPointer { // 定义函数指针
        int sum(int x, int y);
    }

    public static void main(String[] args) {

    // Lambda 表达式: 本质是一个匿名函数
        SumFunctionPointer sumLambda = (int x, int y) -> { return x + y; };

        SumFunctionPointer sumMethod = HelloLambda::sum;

        System.out.println("call sum lambda: " + sumLambda.sum(1, 2));
        System.out.println("call sum method: " + sumMethod.sum(1, 2));

    }

    static int sum(int x, int y) { // 传统函数实现
        return x + y;
    }

}

各种变种

常规定义

lambda既然是一个函数,那么它的常规定义应该形如:

(Type1 param1, Type2 param2, ..., TypeN paramN) -> {
  statment1;
  statment2;
  // .....
  return statmentM;
}

类型省略

省略类型:编译器都可以从上下文环境中推断出lambda表达式的参数类型

(param1,param2, ..., paramN) -> {
  statment1;
  statment2;
  // .....
  return statmentM;
}

所以上面的代码还可以写成:

SumFunctionPointer sumLambda = (int x, int y) -> { return x + y; };
System.out.println("call sum lambda: " + sumLambda.sum(1, 2));
// hidden types lambda
SumFunctionPointer sumHiddenTypes = (x, y) -> {return x + y; };
System.out.println("call sum lambda (hidden types): " + sumHiddenTypes.sum(1, 2));

简化模式1:只有一个参数

如果只有一个参数,可以省略参数的()符号,变为:

param1 -> {
  statment1;
  statment2;
  // ...
  return statmentM;
}

简化模式2:只有一条语句

当只有一条语句的时候,可以省略函数体的{}符合,变为:

(param1,param2, ..., paramN) -> statement;

简化模式实例

public class HelloLambdaVariation {

    static interface FPOneArgument {
        int oneArgument(int x);
    }

    static interface FPOneStatement {
        int oneStatement(int x, int y, int z);
    }

    static interface FPOneArgumentStatement {
        int oneArgState(int x);
    }

    public static void main(String[] args) {

        FPOneArgument fpOneArg = x -> { x = x^2; return x + 1; };
        System.out.println("fpOneArg: " + fpOneArg.oneArgument(5));

        FPOneStatement fpOneState = (x, y, z) -> x + y + z;
        System.out.println("fpOneState: " + fpOneState.oneStatement(10, 20, 30));

        FPOneArgumentStatement fpOneArgState = x -> x^2;
        System.out.println("fpOneArgState: " + fpOneArgState.oneArgState(6));
    }

}

输出内容:

fpOneArg: 8
fpOneState: 60
fpOneArgState: 4

表达式内部访问'this'

上面看到的Lambda表达式似乎是一个 “闭环”:它只需要接受输入参数,然后自己加工,并返回。似乎它不用访问外部变量。其实它可以,还可以在表达式的函数体内使用this引用,这个this引用指向的就是定义它的类(不是它自己,而是定义它的类)。

以下代码样例,是模拟计算一个营业员的累计月薪:基本工资,再加上每笔订单给与15%的提成。

public class HelloLambdaThisRef {

    // totalToken 月薪总工资
    private int totalToken = 0;

    public HelloLambdaThisRef(int baseToken) {
        // baseToken 员工底薪
        this.totalToken += baseToken;
    }

    // 每次返回累计月薪
    public int charge(double orderCharge) {
        // 每笔订单计15%的提成
        FP fp = (x) -> { this.totalToken += (int) (x * 0.15); return this.totalToken; };
        // Lambda 表达式中返回 'this'
        return fp.hand(orderCharge);
    }

    static interface FP {
        int hand(double x);
    }

    public static void main(String[] args) {
        HelloLambdaThisRef alice = new HelloLambdaThisRef(3000);
        System.out.println("total: " + alice.charge(256.30));
        System.out.println("total: " + alice.charge(108.50));
        System.out.println("total: " + alice.charge(675.60));
    }

}

通用函数指针

上面所有例子,为了让lambda表达式(匿名函数)有个“锚点”,我们刻意定义了一些interface来作为“函数指针”。实际上完全没有必要。原因是Java本身支持泛型,这样函数概括起来就是:输入一个参数,返回一个值;输入两个参数,返回一个值;输入3个或3个以上的参数,返回一个值。当然,你或许会说,还有无返回值的呢?的确,不过你可以当做Void处理(注意是 首字母大写Void)。

这样的话,这些函数完全可以预先定义,在JDK层面就可以预定义,然后给用户直接使用就好。事实上,JDK8就是这么做的:

package java.util.function;
@FunctionalInterface
public interface Function<T, R> {
  /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */  
  R apply(T t);
}

很言简意赅,就是把输入参数T,变换成输出参数R。如果没有返回值,则R类型用Void代替,并return null

当然,没有返回值,JDK8还定义了另一种接口:

package java.util.function;
public interface Consumer<T> {
  void accept(T t);
}
package java.util.function;
public interface BiFunction<T, U, R> {
  R apply(T t, U u);
}

就是把输入T和U,变换成R。如果没有返回值,则R类型用Void代替,并return null

当然,没有返回值,JDK8还定义了另一种接口:

package java.util.function;
public interface BiConsumer<T, U> {
  void accept(T t, U u);
}

如果输入三个或多个参数呢?难道一一列出来么?当然不用。可以直接当集合看待呀,因为集合并不要求元素类型一致呀。JDK8专门为它们定义了Stream接口。Stream的功能就相当强大,可以专门的章节讲解。

@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

输入0个,返回1个的,就是Supplier,起名上对应Consumer

完整的实战代码:

import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

public class HelloLambdaFunction {

    public static void main(String[] args) {

        // CASE-1: 输入1个或2个,返回1个
        Function<Integer, Integer> fpOne = x -> {
            x = x ^ 2;
            return x + 1;
        };
        System.out.println("fp one argument: " + fpOne.apply(5));

        BiFunction<Integer, Integer, Long> fqTwo = (x, y) -> x ^ 2 + y ^ 2 + 0L;
        System.out.println("fp two arguments: " + fqTwo.apply(5, 6));

        // CASE-2: 输入1个或2个,返回Void。无返回值,用Void代替。
        //
        Function<Integer, Void> fpOneVoid = x -> {
            x = x ^ 2;
            x = x + 1;
            System.out.println("one argument void: " + x);
            return null;
        };
        fpOneVoid.apply(5);

        BiFunction<Integer, Integer, Void> fpTwoVoid = (x, y) -> {
            long s = x ^ 2 + y ^ 2 + 0L;
            System.out.println("two arguments void: " + s);
            return null;
        };
        fpTwoVoid.apply(5, 6);

        // CASE-3: 输入1个或2个,无返回。无返回值,另一种处理方式
        Consumer<Integer> fpOneNone = x -> {
            x = x ^ 2;
            x = x + 1;
            System.out.println("one argument consumer: " + x);
        };
        fpOneNone.accept(5);

        BiConsumer<Integer, Integer> fpTwoNone = (x, y) -> {
            long s = x ^ 2 + y ^ 2 + 0L;
            System.out.println("two arguments consumer: " + s);
        };
        fpTwoNone.accept(5,6);

        // CASE-4: 无输入,有输出
        Supplier<Double> fpRand = () -> { return Math.random();};
        System.out.println("none argument but one returned: " + fpRand.get());

    // CASE-5: 无输入,无输出
        Runnable fpRunnable = () -> {System.out.println("无输入,无输出");};
        fpRunnable.run();
    }

}

用个图表总结一下:

输入参数个数 返回值个数 通用函数指针名
0 1 java.util.function.Supplier
1 1 java.util.function.Function
2 1 java.util.function.BiFunction
0 0 - 或 java.lang.Runnable
1 0 java.util.function.Consumer
2 0 java.util.function.BiConsumer
3+ * java.util.stream.Stream
downgoon commented 6 years ago

备忘录

预定义函数指针:lambda表达式与预定义函数

输入参数个数 返回值个数 通用函数指针名
0 1 java.util.function.Supplier
1 1 java.util.function.Function
2 1 java.util.function.BiFunction
0 0 - 或 java.lang.Runnable
1 0 java.util.function.Consumer
2 0 java.util.function.BiConsumer
3+ * java.util.stream.Stream

函数与等效lambda表达式

函数引用 等效lambda表达式
String::valueOf | x ->String.valueOf(x)
Object::toString | x ->x.toString()
x::toString | ()->x.toString()
ArrayList::new | () -> new ArrayList<>()
downgoon commented 6 years ago

F4: Stream API & SQL Select

import java.util.stream.Collectors;
import java.util.stream.Stream;

public class StreamSQLSelect {

    public static void main(String[] args) {

        // selectAll();

        // 1. 投影运算

        // selectNameWay1();
        // selectNameWay2();
        // selectNameAge();

        // 2. where 运算
        // selectAgeLT35();

        // 3. order by
        // selectAgeLT35OrderByAge();

        // 4. group by
        // 4.1 男员工和女员工的数量分别是多少
        selectGroupByGender();
    }

    public static void selectGroupByGender() {
        /*
         * select male, count(male) as count from employees group by male order
         * by count
         */
        Stream<Employee> employees = genStream();
        employees.collect(Collectors.groupingBy(Employee::isMale)) //
                .entrySet().stream() // kv stream
                // .peek(System.out::println) // debug groupby
                .sorted((kv1, kv2) -> -(kv1.getValue().size() - kv2.getValue().size())) //
                .map(kv -> { // count on groupBy
                    String gender = kv.getKey() ? "男性" : "女性";
                    int count = kv.getValue().size();
                    return String.format("%s,%d", gender, count);
                }) // count on groupBy
                .forEach(System.out::println);

    }

    public static void selectAgeLT35OrderByAge() {
        /*
         * select name,age from employees where age <= 35 order by age desc
         */
        Stream<Employee> employees = genStream();
        employees.filter(e -> e.getAge() <= 35) // where age <= 35
                .sorted((e1, e2) -> -(e1.age - e2.age)) // order by age desc
                .map(e -> e.getName() + "," + e.getAge()) // select name&age
                .forEach(System.out::println);
    }

    public static void selectAgeLT35() {
        /*
         * select name,age from employees where age <= 35
         */
        Stream<Employee> employees = genStream();
        employees.filter(e -> e.getAge() <= 35) // where age <= 35
                .map(e -> e.getName() + "," + e.getAge()) // select name&age
                .forEach(System.out::println);
    }

    public static void selectNameAge() {
        /*
         * select name,age from employees
         */
        Stream<Employee> employees = genStream();
        employees.map(e -> e.getName() + "," + e.getAge()) // select name&age
                .forEach(System.out::println);
    }

    public static void selectNameWay2() {
        /*
         * select name from employees
         */
        Stream<Employee> employees = genStream();
        employees.map(Employee::getName) // select name
                .forEach(System.out::println);
    }

    public static void selectNameWay1() {
        /*
         * select name from employees
         */
        Stream<Employee> employees = genStream();
        employees.map(e -> e.getName()) // select name
                .forEach(System.out::println);
    }

    public static void selectAll() {
        /*
         * select * from employees
         */
        Stream<Employee> employees = genStream();
        employees.forEach(System.out::println);
    }

    public static Stream<Employee> genStream() {
        return Stream.of(new Employee(1, "wangyi", 28, true, 3000.85) //
                , new Employee(2, "liuer", 30, true, 4058.93) //
                , new Employee(3, "zhangsan", 45, false, 6000.78) //
                , new Employee(4, "lisi", 35, false, 6080.78) //
                , new Employee(5, "zhaowu", 41, true, 7050.78) //
                , new Employee(6, "chenliu", 38, true, 6080.78) //
        );
    }

    public static class Employee {
        private int id;

        private String name;

        private int age;

        private boolean male;

        private double salary;

        public int getId() {
            return id;
        }

        public void setId(int id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

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

        public int getAge() {
            return age;
        }

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

        public boolean isMale() {
            return male;
        }

        public void setMale(boolean male) {
            this.male = male;
        }

        public double getSalary() {
            return salary;
        }

        public void setSalary(double salary) {
            this.salary = salary;
        }

        public Employee(int id, String name, int age, boolean male, double salary) {
            super();
            this.id = id;
            this.name = name;
            this.age = age;
            this.male = male;
            this.salary = salary;
        }

        @Override
        public String toString() {
            return "Employee [id=" + id + ", name=" + name + ", age=" + age + ", male=" + male + ", salary=" + salary
                    + "]";
        }

    }
}

当然我们也可以按员工的年龄段来进行统计:

public static void selectGroupByAgeGap() {
        /*
         * select age, count(age) as count from employees group by gap(age, 10)
         * order by count
         */

        Stream<Employee> employees = genStream();
        String head = String.format("年龄段,员工人数");
        System.out.println(head);
        employees.collect(Collectors.groupingBy( //
                (Employee e) -> {
                    return (e.getAge()/10) * 10;
                }) // groupby age gap
        ).entrySet() // map: age -> list<Employee>
            .stream() // set to stream
            .sorted( (e1, e2) -> e2.getValue().size() - e1.getValue().size()) // orderby
            .map(kv -> { // age, count fields
                int age = kv.getKey();
                int count = kv.getValue().size();

                return String.format("%d~%d,%d", age, age+10, count);
            }) //
            .forEach(System.out::println);

    }

输出结果是:

年龄段,员工人数
30~40,3
40~50,2
20~30,1
downgoon commented 6 years ago

F5: 接口的静态方法

vertx的接口中,有非常多的接口中定义了“静态方法”:

private void setupDefaultStackHandlers(Router router) {
        router.route().handler(CookieHandler.create());
        router.route().handler(BodyHandler.create());
        router.route().handler(SessionHandler.create(LocalSessionStore.create(vertx)));
    }

其中:CookieHandler.create()是接口的静态方法

package io.vertx.ext.web.handler;

import io.vertx.ext.web.handler.impl.CookieHandlerImpl;

public interface CookieHandler extends Handler<RoutingContext> {

  static CookieHandler create() { // 接口静态方法直接返回实现类
    return new CookieHandlerImpl();
  }
}

package io.vertx.ext.web.handler.impl;
import io.vertx.ext.web.handler.CookieHandler;
public class CookieHandlerImpl implements CookieHandler {
}  

尽管很多教程都说这样有利于使用方被强制针对接口编程,但另一方面这样导致接口层与实现层相互依赖了。而且,代码中到处散布着CookieHandler.create(),然而内部是强制new CookieHandlerImpl();,也就是说如果不修改接口的static方法,代码中也无法使用其他实现。