
10分钟让你彻底了解 Java 8 的 Lambda、函数式接口、Stream 用法和原理 就在今年 Java 25周岁了,可能比在座的各位中的一些少年年龄还大,但令人遗憾的是,竟然没有我大,不禁感叹,Java 还是太小了。(难道我会说是因为我老了?) 而就在上个月,Java 15 的试验版悄悄发布了,但是在 Java 界一直有个神秘现象,那就是「你发你发任你发,我的最爱 Java 8」. 据 Snyk 和 The Java Magazine 联合推出发布的 2020 JVM 生态调查报告显示,在所有的 Java 版本中,仍然有 64% 的开发者使用 Java 8。另外一些开发者可能已经开始用 Java 9、Java 11、Java 13 了,当然还有一些神仙开发者还在坚持使用 JDK 1.6 和 1.7。 尽管 Java 8 发布多年,使用者众多,可神奇的是竟然有很多同学没有用过 Java 8 的新特性,比如 Lambda表达式、比如方法引用,再比如今天要说的 Stream。其实 Stream 就是以 Lambda 和方法引用为基础,封装的简单易用、函数式风格的 API。 Java 8 是在 2014 年发布的,实话说,风筝我也是在 Java 8 发布后很长一段时间才用的 Stream,因为 Java 8 发布的时候我还在 C# 的世界中挣扎,而使用 Lambda 表达式却很早了,因为 Python 中用 Lambda 很方便,没错,我写 Python 的时间要比 Java 的时间还长。 要讲 Stream ,那就不得不先说一下它的左膀右臂 Lambda 和方法引用,你用的 Stream API 其实就是函数式的编程风格,其中的「函数」就是方法引用,「式」就是 Lambda 表达式。 Lambda 表达式Lambda 表达式是一个匿名函数,Lambda表达式基于数学中的λ演算得名,直接对应于其中的lambda抽象,是一个匿名函数,即没有函数名的函数。Lambda表达式可以表示闭包。 在 Java 中,Lambda 表达式的格式是像下面这样 // 无参数,无返回值() -> log.info("Lambda") // 有参数,有返回值(int a, int b) -> { a+b }其等价于 log.info("Lambda"); private int plus(int a, int b){ return a+b; }最常见的一个例子就是新建线程,有时候为了省事,会用下面的方法创建并启动一个线程,这是匿名内部类的写法,new Thread需要一个 implements 自Runnable类型的对象实例作为参数,比较好的方式是创建一个新类,这个类 implements Runnable,然后 new 出这个新类的实例作为参数传给 Thread。而匿名内部类不用找对象接收,直接当做参数。 new Thread(new Runnable() { @Override public void run() { System.out.println("快速新建并启动一个线程"); } }).run();但是这样写是不是感觉看上去很乱、很土,而这时候,换上 Lambda 表达式就是另外一种感觉了。 new Thread(()->{ System.out.println("快速新建并启动一个线程"); }).run();怎么样,这样一改,瞬间感觉清新脱俗了不少,简洁优雅了不少。 Lambda 表达式简化了匿名内部类的形式,可以达到同样的效果,但是 Lambda 要优雅的多。虽然最终达到的目的是一样的,但其实内部的实现原理却不相同。 匿名内部类在编译之后会创建一个新的匿名内部类出来,而 Lambda 是调用 JVM invokedynamic指令实现的,并不会产生新类。 方法引用方法引用的出现,使得我们可以将一个方法赋给一个变量或者作为参数传递给另外一个方法。::双冒号作为方法引用的符号,比如下面这两行语句,引用 Integer类的 parseInt方法。 Function s = Integer::parseInt;Integer i = s.apply("10");或者下面这两行,引用 Integer类的 compare方法。 Comparator comparator = Integer::compare;int result = comparator.compare(100,10);再比如,下面这两行代码,同样是引用 Integer类的 compare方法,但是返回类型却不一样,但却都能正常执行,并正确返回。 IntBinaryOperator intBinaryOperator = Integer::compare;int result = intBinaryOperator.applyAsInt(10,100);相信有的同学看到这里恐怕是下面这个状态,完全不可理喻吗,也太随便了吧,返回给谁都能接盘。 先别激动,来来来,现在咱们就来解惑,解除蒙圈脸。 Q:什么样的方法可以被引用? A:这么说吧,任何你有办法访问到的方法都可以被引用。 Q:返回值到底是什么类型? A:这就问到点儿上了,上面又是 Function、又是Comparator、又是 IntBinaryOperator的,看上去好像没有规律,其实不然。 返回的类型是 Java 8 专门定义的函数式接口,这类接口用 @FunctionalInterface 注解。 比如 Function这个函数式接口的定义如下: @FunctionalInterfacepublic interface Function { R apply(T t); }还有很关键的一点,你的引用方法的参数个数、类型,返回值类型要和函数式接口中的方法声明一一对应才行。 比如 Integer.parseInt方法定义如下: public static int parseInt(String s) throws NumberFormatException { return parseInt(s,10); }首先parseInt方法的参数个数是 1 个,而 Function中的 apply方法参数个数也是 1 个,参数个数对应上了,再来,apply方法的参数类型和返回类型是泛型类型,所以肯定能和 parseInt方法对应上。 这样一来,就可以正确的接收Integer::parseInt的方法引用,并可以调用Funciton的apply方法,这时候,调用到的其实就是对应的 Integer.parseInt方法了。 用这套标准套到 Integer::compare方法上,就不难理解为什么即可以用 Comparator接收,又可以用 IntBinaryOperator接收了,而且调用它们各自的方法都能正确的返回结果。 Integer.compare方法定义如下: public static int compare(int x, int y) { return (x < y) ? -1 : ((x == y) ? 0 : 1); }返回值类型 int,两个参数,并且参数类型都是 int。 然后来看Comparator和IntBinaryOperator它们两个的函数式接口定义和其中对应的方法: @FunctionalInterfacepublic interface Comparator { int compare(T o1, T o2); } @FunctionalInterfacepublic interface IntBinaryOperator { int applyAsInt(int left, int right); }对不对,都能正确的匹配上,所以前面示例中用这两个函数式接口都能正常接收。其实不止这两个,只要是在某个函数式接口中声明了这样的方法:两个参数,参数类型是 int或者泛型,并且返回值是 int或者泛型的,都可以完美接收。 JDK 中定义了很多函数式接口,主要在 java.util.function包下,还有 java.util.Comparator 专门用作定制比较器。另外,前面说的 Runnable也是一个函数式接口。 自己动手实现一个例子 定义一个函数式接口,并添加一个方法 定义了名称为 KiteFunction 的函数式接口,使用 @FunctionalInterface注解,然后声明了具有两个参数的方法 run,都是泛型类型,返回结果也是泛型。 还有一点很重要,函数式接口中只能声明一个可被实现的方法,你不能声明了一个 run方法,又声明一个 start方法,到时候编译器就不知道用哪个接收了。而用default 关键字修饰的方法则没有影响。 @FunctionalInterfacepublic interface KiteFunction { /** * 定义一个双参数的方法 * @param t * @param s * @return */ R run(T t,S s); } 定义一个与 KiteFunction 中 run 方法对应的方法 在 FunctionTest 类中定义了方法 DateFormat,一个将 LocalDateTime类型格式化为字符串类型的方法。 public class FunctionTest { public static String DateFormat(LocalDateTime dateTime, String partten) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten); return dateTime.format(dateTimeFormatter); } }3.用方法引用的方式调用 正常情况下我们直接使用 FunctionTest.DateFormat()就可以了。 而用函数式方式,是这样的。 KiteFunction functionDateFormat = FunctionTest::DateFormat;String dateString = functionDateFormat.run(LocalDateTime.now(),"yyyy-MM-dd HH:mm:ss");而其实我可以不专门在外面定义 DateFormat这个方法,而是像下面这样,使用匿名内部类。 public static void main(String[] args) throws Exception { String dateString = new KiteFunction<LocalDateTime, String, String>() { @Override public String run(LocalDateTime localDateTime, String s) { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(s); return localDateTime.format(dateTimeFormatter); } }.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss"); System.out.println(dateString); }前面第一个 Runnable的例子也提到了,这样的匿名内部类可以用 Lambda 表达式的形式简写,简写后的代码如下: public static void main(String[] args) throws Exception { KiteFunction<LocalDateTime, String, String> functionDateFormat = (LocalDateTime dateTime, String partten) -> { DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(partten); return dateTime.format(dateTimeFormatter); }; String dateString = functionDateFormat.run(LocalDateTime.now(), "yyyy-MM-dd HH:mm:ss"); System.out.println(dateString); }使用(LocalDateTime dateTime, String partten) -> { } 这样的 Lambda 表达式直接返回方法引用。 Stream API为了说一下 Stream API 的使用,可以说是大费周章啊,知其然,也要知其所以然吗,追求技术的态度和姿势要正确。 当然 Stream 也不只是 Lambda 表达式就厉害了,真正厉害的还是它的功能,Stream 是 Java 8 中集合数据处理的利器,很多本来复杂、需要写很多代码的方法,比如过滤、分组等操作,往往使用 Stream 就可以在一行代码搞定,当然也因为 Stream 都是链式操作,一行代码可能会调用好几个方法。 Collection接口提供了 stream()方法,让我们可以在一个集合方便的使用 Stream API 来进行各种操作。值得注意的是,我们执行的任何操作都不会对源集合造成影响,你可以同时在一个集合上提取出多个 stream 进行操作。 我们看 Stream 接口的定义,继承自 BaseStream,机会所有的接口声明都是接收方法引用类型的参数,比如 filter方法,接收了一个 Predicate类型的参数,它就是一个函数式接口,常用来作为条件比较、筛选、过滤用,JPA中也使用了这个函数式接口用来做查询条件拼接。 public interface Stream extends BaseStream> { Stream filter(Predicate<? super T> predicate); // 其他接口} 下面就来看看 Stream 常用 API。 of可接收一个泛型对象或可变成泛型集合,构造一个 Stream 对象。 private static void createStream(){ Stream<String> stringStream = Stream.of("a","b","c"); }empty创建一个空的 Stream 对象。 concat连接两个 Stream ,不改变其中任何一个 Steam 对象,返回一个新的 Stream 对象。 private static void concatStream(){ Stream<String> a = Stream.of("a","b","c"); Stream<String> b = Stream.of("d","e"); Stream<String> c = Stream.concat(a,b); }max一般用于求数字集合中的最大值,或者按实体中数字类型的属性比较,拥有最大值的那个实体。它接收一个 Comparator,上面也举到这个例子了,它是一个函数式接口类型,专门用作定义两个对象之间的比较,例如下面这个方法使用了 Integer::compareTo这个方法引用。 private static void max(){ Stream<Integer> integerStream = Stream.of(2, 2, 100, 5); Integer max = integerStream.max(Integer::compareTo).get(); System.out.println(max); }当然,我们也可以自己定制一个 Comparator,顺便复习一下 Lambda 表达式形式的方法引用。 private static void max(){ Stream<Integer> integerStream = Stream.of(2, 2, 100, 5); Comparator<Integer> comparator = (x, y) -> (x.intValue() < y.intValue()) ? -1 : ((x.equals(y)) ? 0 : 1); Integer max = integerStream.max(comparator).get(); System.out.println(max); }min与 max 用法一样,只不过是求最小值。 findFirst获取 Stream 中的第一个元素。 findAny获取 Stream 中的某个元素,如果是串行情况下,一般都会返回第一个元素,并行情况下就不一定了。 count返回元素个数。 Stream a = Stream.of("a", "b", "c");long x = a.count();peek建立一个通道,在这个通道中对 Stream 的每个元素执行对应的操作,对应 Consumer的函数式接口,这是一个消费者函数式接口,顾名思义,它是用来消费 Stream 元素的,比如下面这个方法,把每个元素转换成对应的大写字母并输出。 private static void peek() { Stream<String> a = Stream.of("a", "b", "c"); List<String> list = a.peek(e->System.out.println(e.toUpperCase())).collect(Collectors.toList()); }forEach和 peek 方法类似,都接收一个消费者函数式接口,可以对每个元素进行对应的操作,但是和 peek 不同的是,forEach 执行之后,这个 Stream 就真的被消费掉了,之后这个 Stream 流就没有了,不可以再对它进行后续操作了,而 peek操作完之后,还是一个可操作的 Stream 对象。 正好借着这个说一下,我们在使用 Stream API 的时候,都是一串链式操作,这是因为很多方法,比如接下来要说到的 filter方法等,返回值还是这个 Stream 类型的,也就是被当前方法处理过的 Stream 对象,所以 Stream API 仍然可以使用。 private static void forEach() { Stream<String> a = Stream.of("a", "b", "c"); a.forEach(e->System.out.println(e.toUpperCase())); }forEachOrdered功能与 forEach是一样的,不同的是,forEachOrdered是有顺序保证的,也就是对 Stream 中元素按插入时的顺序进行消费。为什么这么说呢,当开启并行的时候,forEach和 forEachOrdered的效果就不一样了。 Stream a = Stream.of("a", "b", "c");a.parallel().forEach(e->System.out.println(e.toUpperCase()));当使用上面的代码时,输出的结果可能是 B、A、C 或者 A、C、B或者A、B、C,而使用下面的代码,则每次都是 A、 B、C Stream a = Stream.of("a", "b", "c");a.parallel().forEachOrdered(e->System.out.println(e.toUpperCase()));limit获取前 n 条数据,类似于 MySQL 的limit,只不过只能接收一个参数,就是数据条数。 private static void limit() { Stream<String> a = Stream.of("a", "b", "c"); a.limit(2).forEach(e->System.out.println(e)); }上述代码打印的结果是 a、b。 skip跳过前 n 条数据,例如下面代码,返回结果是 c。 private static void skip() { Stream<String> a = Stream.of("a", "b", "c"); a.skip(2).forEach(e->System.out.println(e)); }distinct元素去重,例如下面方法返回元素是 a、b、c,将重复的 b 只保留了一个。 private static void distinct() { Stream<String> a = Stream.of("a", "b", "c","b"); a.distinct().forEach(e->System.out.println(e)); }sorted有两个重载,一个无参数,另外一个有个 Comparator类型的参数。 无参类型的按照自然顺序进行排序,只适合比较单纯的元素,比如数字、字母等。 private static void sorted() { Stream<String> a = Stream.of("a", "c", "b"); a.sorted().forEach(e->System.out.println(e)); }有参数的需要自定义排序规则,例如下面这个方法,按照第二个字母的大小顺序排序,最后输出的结果是 a1、b3、c6。 private static void sortedWithComparator() { Stream<String> a = Stream.of("a1", "c6", "b3"); a.sorted((x,y)->Integer.parseInt(x.substring(1))>Integer.parseInt(y.substring(1))?1:-1).forEach(e->System.out.println(e)); }为了更好的说明接下来的几个 API ,我模拟了几条项目中经常用到的类似数据,10条用户信息。 private static List getUserData() { Random random = new Random(); List<User> users = new ArrayList<>(); for (int i = 1; i <= 10; i++) { User user = new User(); user.setUserId(i); user.setUserName(String.format("古时的风筝 %s 号", i)); user.setAge(random.nextInt(100)); user.setGender(i % 2); user.setPhone("18812021111"); user.setAddress("无"); users.add(user); } return users; }filter用于条件筛选过滤,筛选出符合条件的数据。例如下面这个方法,筛选出性别为 0,年龄大于 50 的记录。 private static void filter(){ List<User> users = getUserData(); Stream<User> stream = users.stream(); stream.filter(user -> user.getGender().equals(0) && user.getAge()>50).forEach(e->System.out.println(e)); /** *等同于下面这种形式 匿名内部类 */ // stream.filter(new Predicate() {// @Override// public boolean test(User user) {// return user.getGender().equals(0) && user.getAge()>50;// }// }).forEach(e->System.out.println(e));}mapmap方法的接口方法声明如下,接受一个 Function函数式接口,把它翻译成映射最合适了,通过原始数据元素,映射出新的类型。 Stream map(Function<? super T, ? extends R> mapper);而 Function的声明是这样的,观察 apply方法,接受一个 T 型参数,返回一个 R 型参数。用于将一个类型转换成另外一个类型正合适,这也是 map的初衷所在,用于改变当前元素的类型,例如将 Integer 转为 String类型,将 DAO 实体类型,转换为 DTO 实例类型。 当然了,T 和 R 的类型也可以一样,这样的话,就和 peek方法没什么不同了。 @FunctionalInterfacepublic interface Function { /** * Applies this function to the given argument. * * @param t the function argument * @return the function result */ R apply(T t); }例如下面这个方法,应该是业务系统的常用需求,将 User 转换为 API 输出的数据格式。 private static void map(){ List<User> users = getUserData(); Stream<User> stream = users.stream(); List<UserDto> userDtos = stream.map(user -> dao2Dto(user)).collect(Collectors.toList()); } private static UserDto dao2Dto(User user){ UserDto dto = new UserDto(); BeanUtils.copyProperties(user, dto); //其他额外处理 return dto; }mapToInt将元素转换成 int 类型,在 map方法的基础上进行封装。 mapToLong将元素转换成 Long 类型,在 map方法的基础上进行封装。 mapToDouble将元素转换成 Double 类型,在 map方法的基础上进行封装。 flatMap这是用在一些比较特别的场景下,当你的 Stream 是以下这几种结构的时候,需要用到 flatMap方法,用于将原有二维结构扁平化。 StreamStream>Stream>以上这三类结构,通过 flatMap方法,可以将结果转化为 Stream这种形式,方便之后的其他操作。 比如下面这个方法,将List>扁平处理,然后再使用 map或其他方法进行操作。 private static void flatMap(){ List<User> users = getUserData(); List<User> users1 = getUserData(); List<List<User>> userList = new ArrayList<>(); userList.add(users); userList.add(users1); Stream<List<User>> stream = userList.stream(); List<UserDto> userDtos = stream.flatMap(subUserList->subUserList.stream()).map(user -> dao2Dto(user)).collect(Collectors.toList()); }flatMapToInt用法参考 flatMap,将元素扁平为 int 类型,在 flatMap方法的基础上进行封装。 flatMapToLong用法参考 flatMap,将元素扁平为 Long 类型,在 flatMap方法的基础上进行封装。 flatMapToDouble用法参考 flatMap,将元素扁平为 Double 类型,在 flatMap方法的基础上进行封装。 collection在进行了一系列操作之后,我们最终的结果大多数时候并不是为了获取 Stream 类型的数据,而是要把结果变为 List、Map 这样的常用数据结构,而 collection就是为了实现这个目的。 就拿 map 方法的那个例子说明,将对象类型进行转换后,最终我们需要的结果集是一个 List类型的,使用 collect方法将 Stream 转换为我们需要的类型。 下面是 collect接口方法的定义: R collect(Collector<? super T, A, R> collector);下面这个例子演示了将一个简单的 Integer Stream 过滤出大于 7 的值,然后转换成 List集合,用的是 Collectors.toList()这个收集器。 private static void collect(){ Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33); List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(Collectors.toList()); }很多同学表示看不太懂这个 Collector是怎么一个意思,来,我们看下面这段代码,这是 collect的另一个重载方法,你可以理解为它的参数是按顺序执行的,这样就清楚了,这就是个 ArrayList 从创建到调用 addAll方法的一个过程。 private static void collect(){ Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33); List<Integer> list = integerStream.filter(s -> s.intValue()>7).collect(ArrayList::new, ArrayList::add, ArrayList::addAll); }我们在自定义 Collector的时候其实也是这个逻辑,不过我们根本不用自定义, Collectors已经为我们提供了很多拿来即用的收集器。比如我们经常用到Collectors.toList()、Collectors.toSet()、Collectors.toMap()。另外还有比如Collectors.groupingBy()用来分组,比如下面这个例子,按照 userId 字段分组,返回以 userId 为key,List 为value 的 Map,或者返回每个 key 的个数。 // 返回 userId:ListMap> map = user.stream().collect(Collectors.groupingBy(User::getUserId)); // 返回 userId:每组个数Map map = user.stream().collect(Collectors.groupingBy(User::getUserId,Collectors.counting()));toArraycollection是返回列表、map 等,toArray是返回数组,有两个重载,一个空参数,返回的是 Object[]。 另一个接收一个 IntFunction类型参数。 @FunctionalInterfacepublic interface IntFunction { /** * Applies this function to the given argument. * * @param value the function argument * @return the function result */ R apply(int value); }比如像下面这样使用,参数是 User[]::new也就是new 一个 User 数组,长度为最后的 Stream 长度。 private static void toArray() { List<User> users = getUserData(); Stream<User> stream = users.stream(); User[] userArray = stream.filter(user -> user.getGender().equals(0) && user.getAge() > 50).toArray(User[]::new); }reduce它的作用是每次计算的时候都用到上一次的计算结果,比如求和操作,前两个数的和加上第三个数的和,再加上第四个数,一直加到最后一个数位置,最后返回结果,就是 reduce的工作过程。 private static void reduce(){ Stream<Integer> integerStream = Stream.of(1,2,5,7,8,12,33); Integer sum = integerStream.reduce(0,(x,y)->x+y); System.out.println(sum); }另外 Collectors好多方法都用到了 reduce,比如 groupingBy、minBy、maxBy等等。 并行 StreamStream 本质上来说就是用来做数据处理的,为了加快处理速度,Stream API 提供了并行处理 Stream 的方式。通过 users.parallelStream()或者users.stream().parallel() 的方式来创建并行 Stream 对象,支持的 API 和普通 Stream 几乎是一致的。 并行 Stream 默认使用 ForkJoinPool线程池,当然也支持自定义,不过一般情况下没有必要。ForkJoin 框架的分治策略与并行流处理正好契合。 虽然并行这个词听上去很厉害,但并不是所有情况使用并行流都是正确的,很多时候完全没这个必要。 什么情况下使用或不应使用并行流操作呢? 必须在多核 CPU 下才使用并行 Stream,听上去好像是废话。在数据量不大的情况下使用普通串行 Stream 就可以了,使用并行 Stream 对性能影响不大。CPU 密集型计算适合使用并行 Stream,而 IO 密集型使用并行 Stream 反而会更慢。虽然计算是并行的可能很快,但最后大多数时候还是要使用 collect合并的,如果合并代价很大,也不适合用并行 Stream。有些操作,比如 limit、 findFirst、forEachOrdered 等依赖于元素顺序的操作,都不适合用并行 Stream。最后Java 25 周岁了,有多少同学跟我一样在用 Java 8,还有多少同学再用更早的版本,请说出你的故事。 原文地址https://my.oschina.net/u/4519772/blog/4305992
关于C#委托三种调用的分享 一、同步调用1、同步调用会按照代码顺序来执行2、同步调用会阻塞线程,如果是要调用一项繁重的工作(如大量IO操作),可能会让程序停顿很长时间,造成糟糕的用户体验,这时候异步调用就很有必要了。 举个栗子: using System;using System.Collections.Generic;using System.Linq;using System.Runtime.Remoting.Messaging;using System.Text;using System.Threading; namespace Test{ public delegate int AddHandler(int a, int b); public class Calc { public static int Add(int a, int b) { Console.WriteLine("开始计算:" + a + "+" + b); Thread.Sleep(3000); //模拟该方法运行三秒 Console.WriteLine("计算完成!"); return a + b; } } class Program { static void Main(string[] args) { Console.WriteLine("===== 同步调用 SyncInvokeTest ====="); AddHandler handler = new AddHandler(Calc.Add); int result = handler.Invoke(1, 2); Console.WriteLine("继续做别的事情。。。"); Console.WriteLine(result); Console.ReadKey(); } } } 问:为什么Invoke的参数和返回值和AddHandler委托是一样的呢? 答:Invoke方法的参数很简单,一个委托,一个参数表(可选),而Invoke方法的主要功能就是帮助你在UI线程上调用委托所指定的方法。 Invoke方法首先检查发出调用的线程(即当前线程)是不是UI线程,如果是,直接执行委托指向的方法,如果不是,它将切换到UI线程,然后执行委托指向的方法。不管当前线程是不是UI线程,Invoke都阻塞直到委托指向的方法执行完毕,然后切换回发出调用的线程(如果需要的话),返回。所以Invoke方法的参数和返回值和调用他的委托应该是一致的。 二、异步调用1、异步调用不阻塞线程,而是把调用塞到线程池中,2、程序主线程或UI线程可以继续执行。3、委托的异步调用通过BeginInvoke和EndInvoke来实现。 举个栗子: using System;using System.Collections.Generic;using System.Linq;using System.Runtime.Remoting.Messaging;using System.Text;using System.Threading; namespace Test{ public delegate int AddHandler(int a, int b); public class Calc { public static int Add(int a, int b) { Console.WriteLine("开始计算:" + a + "+" + b); Thread.Sleep(3000); //模拟该方法运行三秒 Console.WriteLine("计算完成!"); return a + b; } } class Program { static void Main(string[] args) { Console.WriteLine("===== 异步调用 AsyncInvokeTest ====="); AddHandler handler1 = new AddHandler(Calc.Add); //IAsyncResult: 异步操作接口(interface) //BeginInvoke: 委托(delegate)的一个异步方法的开始 IAsyncResult result1 = handler1.BeginInvoke(1, 2, null, null); Console.WriteLine("继续做别的事情1。。。"); //异步操作返回 Console.WriteLine(handler1.EndInvoke(result1));//会等待加法类计算,如果没计算好就堵塞线程 Console.WriteLine("继续做别的事情2。。。"); Console.ReadKey(); } } } 注意: BeginInvoke : 开始一个异步的请求,调用线程池中一个线程来执行, 返回IAsyncResult 对象(异步的核心). IAsyncResult 简单的说, 它存储异步操作的状态信息的一个接口,也可以用他来结束当前异步。 注意: BeginInvoke和EndInvoke必须成对调用.即使不需要返回值, 但EndInvoke还是必须调用,否则可能会造成内存泄漏。 结果: 可以看到,主线程并没有等待,而是直接向下运行了。但是问题依然存在,当主线程运行到EndInvoke时,如果这时调用没有结束(这种情况很可能出现),这时为了等待调用结果,线程依旧会被阻塞。 三、异步回调用回调函数,当调用结束时会自动调用回调函数,解决了为等待调用结果,而让线程依旧被阻塞的局面。 举个栗子: using System;using System.Collections.Generic;using System.Linq;using System.Runtime.Remoting.Messaging;using System.Text;using System.Threading; namespace Test{ public delegate int AddHandler(int a, int b); public class Calc { public static int Add(int a, int b) { Console.WriteLine("开始计算:" + a + "+" + b); Thread.Sleep(3000); //模拟该方法运行三秒 Console.WriteLine("计算完成!"); return a + b; } } class Program { static void Main(string[] args) { Console.WriteLine("===== 异步回调 AsyncInvokeTest ====="); AddHandler handler2 = new AddHandler(Calc.Add); //异步操作接口(注意BeginInvoke方法的不同!) IAsyncResult result2 = handler2.BeginInvoke(1, 2, new AsyncCallback(MyCallBack), "AsycState:OK"); Console.WriteLine("继续做别的事情。。。"); Console.ReadKey(); } static void MyCallBack(IAsyncResult result) { //result 是“加法类.Add()方法”的返回值 //AsyncResult 是IAsyncResult接口的一个实现类,空间:System.Runtime.Remoting.Messaging //AsyncDelegate 属性可以强制转换为用户定义的委托的实际类。 AddHandler handler = (AddHandler)((AsyncResult)result).AsyncDelegate; Console.WriteLine(handler.EndInvoke(result)); Console.WriteLine(result.AsyncState); } } } 委托的类型为AddHandler,则为了访问 AddHandler.EndInvoke, result 是“加法calc.Add()方法”的返回值 AsyncResult 是IAsyncResult接口的一个实现类,空间:System.Runtime.Remoting.Messaging 必须将异步委托强制转换为 AddHandler。可以在异步回调函数(类型为 AsyncCallback)中调用 AddHandler.EndInvoke,以获取最初提交的 AddHandler.BeginInvoke 的结果。 ok,三种委托调用的分享就到这里了,有疑问的欢迎指正! 原文地址https://www.cnblogs.com/guhuazhen/p/13044003.html
实战| 配置DataDog监控Apache Hudi应用指标 可用性在Hudi最新master分支,由Hudi活跃贡献者Raymond Xu贡献了DataDog监控Hudi应用指标,该功能将在0.6.0 版本发布,也感谢Raymond的投稿。 简介Datadog是一个流行的监控服务。在即将发布的Apache Hudi 0.6.0版本中,除已有的报告者类型(Graphite和JMX)之外,我们将引入通过Datadog HTTP API报告Hudi指标的功能。 配置类似于其他支持的报告者,启用Datadog报告者需要以下两个属性。 hoodie.metrics.on=truehoodie.metrics.reporter.type=DATADOG下面的属性用来配置Datdog API站点。它会决定请求被发送给api.datadoghq.eu (EU) 还是 api.datadoghq.com (US)。根据你的Datadog账号作相应配置。 hoodie.metrics.datadog.api.site=EU # 或者 UShoodie.metrics.datadog.api.key可以让你配置API密匙。 hoodie.metrics.datadog.api.key=<你的API密匙>hoodie.metrics.datadog.api.key.supplier=<你的API密匙提供者>出于安全性考虑,你可能会选择在运行时返回API密匙。要使用这个方法,需要实现java.util.function.Supplier。并把实现类的完整类名设置到hoodie.metrics.datadog.api.key.supplier。由于hoodie.metrics.datadog.api.key有更高的优先级,也要确保它没有设置。 下面的属性用来配置指标前缀,从而区分不同job的指标。 hoodie.metrics.datadog.metric.prefix=<你的指标前缀>注意这里.会被用来隔离前缀和指标名。比如,如果前缀是foo,则foo.会被加在指标名称前。 其他的可选属性在配置参考页里有相关解释。 示例演示在这个示例中,我们运行了一个HoodieDeltaStreamer,启用了指标收集并做了相应的配置。 如图所示,我们能收集到Hudi操作相关的指标,比如 <前缀>.<表名>.commit.totalScanTime<前缀>.<表名>.clean.duration<前缀>.<表名>.index.lookup.duration以及HoodieDeltaStreamer相关的指标。 <前缀>.<表名>.deltastreamer.duration<前缀>.<表名>.deltastreamer.hiveSyncDuration. 总结Hudi提供了多种报告者,方便监控Hudi应用运行时的各项指标,及时发现系统中的问题。 PS:如果您觉得阅读本文对您有帮助,请点一下“推荐”按钮,您的“推荐”,将会是我不竭的动力!作者:leesf 掌控之中,才会成功;掌控之外,注定失败。出处:http://www.cnblogs.com/leesf456/
重学 Java 设计模式:实战单例模式 一、前言5个创建型模式的最后一个 在设计模式中按照不同的处理方式共包含三大类;创建型模式、结构型模式和行为模式,其中创建型模式目前已经介绍了其中的四个;工厂方法模式、抽象工厂模式、生成器模式和原型模式,除此之外还有最后一个单例模式。 掌握了的知识才是自己的 在本次编写的重学 Java 设计模式的编写中尽可能多的用各种场景案例还介绍设计的使用,包括我们已经使用过的场景;各种类型奖品发放、多套Redis缓存集群升级、装修公司报价清单和百份考卷题目与答案乱序,通过这些场景案例的实践感受设计模式的思想。但这些场景都是作者通过经验分离出来的,还并不是读者的知识,所以你如果希望可以融会贯通的掌握那么一定要亲力亲为的操作,事必躬亲的完成。 书不是看的是用的 在这里还是想强调一下学习方法,总有很多小伙伴对学习知识有疑惑,明明看了、看的时候也懂了,但到了实际使用的时候却用不上。或者有时候在想是不要是有更加生动的漫画或者什么对比会好些,当然这些方式可能会加快一个新人对知识的理解速度。但只要你把学习视频当电影看、学习书籍当故事看,就很难掌握这项技术栈。只有你把它用起来,逐字逐句的深挖,一点点的探求,把各项遇到的盲点全部扫清,才能让你真的掌握这项技能。 二、开发环境JDK 1.8Idea + Maven涉及工程1个,可以通过关注公众号:bugstack虫洞栈,回复源码下载获取(打开获取的链接,找到序号18)三、单例模式介绍 单例模式可以说是整个设计中最简单的模式之一,而且这种方式即使在没有看设计模式相关资料也会常用在编码开发中。 因为在编程开发中经常会遇到这样一种场景,那就是需要保证一个类只有一个实例哪怕多线程同时访问,并需要提供一个全局访问此实例的点。 综上以及我们平常的开发中,可以总结一条经验,单例模式主要解决的是,一个全局使用的类频繁的创建和消费,从而提升提升整体的代码的性能。 四、案例场景本章节的技术所出现的场景非常简单也是我们日常开发所能见到的,例如; 数据库的连接池不会反复创建spring中一个单例模式bean的生成和使用在我们平常的代码中需要设置全局的的一些属性保存在我们的日常开发中大致上会出现如上这些场景中使用到单例模式,虽然单例模式并不复杂但是使用面却比较广。 五、7种单例模式实现单例模式的实现方式比较多,主要在实现上是否支持懒汉模式、是否线程安全中运用各项技巧。当然也有一些场景不需要考虑懒加载也就是懒汉模式的情况,会直接使用static静态类或属性和方法的方式进行处理,供外部调用。 那么接下来我们就通过实现不同方式的实现进行讲解单例模式。 静态类使用 class Singleton_00 { public static Map<String,String> cache = new ConcurrentHashMap<String, String>(); }以上这种方式在我们平常的业务开发中非常场常见,这样静态类的方式可以在第一次运行的时候直接初始化Map类,同时这里我们也不需要到延迟加载在使用。在不需要维持任何状态下,仅仅用于全局访问,这个使用使用静态类的方式更加方便。但如果需要被继承以及需要维持一些特定状态的情况下,就适合使用单例模式。 懒汉模式(线程不安全) class Singleton_01 { private static Singleton_01 instance; private Singleton_01() { } public static Singleton_01 getInstance(){ if (null != instance) return instance; return new Singleton_01(); } }单例模式有一个特点就是不允许外部直接创建,也就是new Singleton_01(),因此这里在默认的构造函数上添加了私有属性 private。目前此种方式的单例确实满足了懒加载,但是如果有多个访问者同时去获取对象实例你可以想象成一堆人在抢厕所,就会造成多个同样的实例并存,从而没有达到单例的要求。 懒汉模式(线程安全) class Singleton_02 { private static Singleton_02 instance; private Singleton_02() { } public static synchronized Singleton_02 getInstance(){ if (null != instance) return instance; return new Singleton_02(); } }此种模式虽然是安全的,但由于把锁加到方法上后,所有的访问都因需要锁占用导致资源的浪费。如果不是特殊情况下,不建议此种方式实现单例模式。 饿汉模式(线程安全) class Singleton_03 { private static Singleton_03 instance = new Singleton_03(); private Singleton_03() { } public static Singleton_03 getInstance() { return instance; } }此种方式与我们开头的第一个实例化Map基本一致,在程序启动的时候直接运行加载,后续有外部需要使用的时候获取即可。但此种方式并不是懒加载,也就是说无论你程序中是否用到这样的类都会在程序启动之初进行创建。那么这种方式导致的问题就像你下载个游戏软件,可能你游戏地图还没有打开呢,但是程序已经将这些地图全部实例化。到你手机上最明显体验就一开游戏内存满了,手机卡了,需要换了。 使用类的内部类(线程安全) class Singleton_04 { private static class SingletonHolder { private static Singleton_04 instance = new Singleton_04(); } private Singleton_04() { } public static Singleton_04 getInstance() { return SingletonHolder.instance; } }使用类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,同时不会因为加锁的方式耗费性能。这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下可以被正确的加载。此种方式也是非常推荐使用的一种单例模式 双重锁校验(线程安全) class Singleton_05 { private volatile static Singleton_05 instance; private Singleton_05() { } public static Singleton_05 getInstance(){ if(null != instance) return instance; synchronized (Singleton_05.class){ if (null == instance){ instance = new Singleton_05(); } } return instance; } }双重锁的方式是方法级锁的优化,减少了部分获取实例的耗时。同时这种方式也满足了懒加载。volatile关键字会强制的保证线程的可见性,而不加这个关键字,JVM也会尽力去保证可见性,但如果CPU一直处于繁忙状态就不确定了。 CAS「AtomicReference」(线程安全) class Singleton_06 { private static final AtomicReference<Singleton_06> INSTANCE = new AtomicReference<Singleton_06>(); private static Singleton_06 instance; private Singleton_06() { } public static final Singleton_06 getInstance() { for (; ; ) { Singleton_06 instance = INSTANCE.get(); if (null != instance) return instance; INSTANCE.compareAndSet(null, new Singleton_06()); return INSTANCE.get(); } } public static void main(String[] args) { System.out.println(Singleton_06.getInstance()); // org.itstack.demo.design.Singleton_06@2b193f2d System.out.println(Singleton_06.getInstance()); // org.itstack.demo.design.Singleton_06@2b193f2d } }java并发库提供了很多原子类来支持并发访问的数据安全性;AtomicInteger、AtomicBoolean、AtomicLong、AtomicReference。AtomicReference 可以封装引用一个V实例,支持并发访问如上的单例方式就是使用了这样的一个特点。使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性。当然CAS也有一个缺点就是忙等,如果一直没有获取到将会处于死循环中。 Effective Java作者推荐的枚举单例(线程安全) enum Singleton_07 { INSTANCE; public void test(){ System.out.println("hi~"); } }约书亚·布洛克(英语:Joshua J. Bloch,1961年8月28日-),美国著名程序员。他为Java平台设计并实作了许多的功能,曾担任Google的首席Java架构师(Chief Java Architect)。 Effective Java 作者推荐使用枚举的方式解决单例模式,此种方式可能是平时最少用到的。这种方式解决了最主要的;线程安全、自由串行化、单一实例。调用方式 @Testpublic void test() { Singleton_07.INSTANCE.test(); 这种写法在功能上与共有域方法相近,但是它更简洁,无偿地提供了串行化机制,绝对防止对此实例化,即使是在面对复杂的串行化或者反射攻击的时候。虽然这中方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。 但也要知道此种方式在存在继承场景下是不可用的。 六、总结虽然只是一个很平常的单例模式,但在各种的实现上真的可以看到java的基本功的体现,这里包括了;懒汉、饿汉、线程是否安全、静态类、内部类、加锁、串行化等等。在平时的开发中如果可以确保此类是全局可用不需要做懒加载,那么直接创建并给外部调用即可。但如果是很多的类,有些需要在用户触发一定的条件后(游戏关卡)才显示,那么一定要用懒加载。线程的安全上可以按需选择。建议在学习的过程中一定要加以实践,否则很难完完整整的掌握一整套的知识体系。例如案例中的出现的Effective Java一书也非常建议大家阅读。另外推荐下这位大神的Github:https://github.com/jbloch七、推荐阅读重学 Java 设计模式:实战原型模式-模拟考试试卷乱序题目和答案Java开发架构篇:初识领域驱动设计DDD落地Java开发架构篇:DDD模型领域层决策规则树服务设计Java开发架构篇:领域驱动设计架构基于SpringCloud搭建微服务源码分析(面试常问题目) | Mybatis接口没有实现类为什么可以执行增删改查讲道理,只要你是一个爱折腾的程序员,毕业找工作真的不需要再花钱培训!作者:小傅哥 原文地址https://www.cnblogs.com/xiaofuge/p/13023749.html
关于sql String类型的日期数据如何进行比较查询我们运用date_format把String数据库中的String 类型的日期格式化成datetime. 上代码: SELECT SQL_CALC_FOUND_ROWS * FROM table_name WHERE DATE_FORMAT( expire_time, '%Y-%m-%d' ) BETWEEN STR_TO_DATE( '"+start_time+"', '%Y-%m-%d' ) AND STR_TO_DATE( '"+start_time+"', '%Y-%m-%d' );其中,expire_time是数据库字段,start_time,end_time是外部传过来的筛选条件。 tips:在sql语句中,我们需要先将数据库中的数据用函数 DATE_FORMAT 函数来格式化特定格式的日期 DATE_FORMAT(date,format);date:要格式化的有效日期值format:格式化后的特定格式使用 STR_TO_DATE 函数将字符串转变为 DATETIME 类型的值 STR_TO_DATE(str,format);str:时间格式的字符串format:时间显示格式原文地址https://my.oschina.net/mynotes/blog/4296437