终于来新同事了,没想到竟是我噩梦的开始

简介: 终于来新同事了,没想到竟是我噩梦的开始

Java 8 入门使用

哈喽,大家好,我是Java小面。

       今天主管老大静悄悄地把我叫了过去,跟我说他之前招的三年工作经验的后端来了,让我带一下.....Excuses me?为什么三年了还要我带?起初我以为只是说笑,想我帮新同事熟悉一下部门和自家产品,所以才这么说。

       结果相处了两天发现,新同事在记忆力方面不错,讲起理论来朗朗上口,跟背歌词一样。但是实操方面,却是个只会捡螺丝的人,连拧螺丝都不是很顺手.....

       一份数据过滤+转化+提取的过程,他硬生生的用了17行代码,三次for循环,我明明记得他的简历上写着擅长使用Java8特性来着....

       等我写完这篇文章,我就甩他脸上去,顺便甩一份给主管老大,怪不得让我带他,还偷偷摸摸的吩咐我。

前言

       Java8是目前最常用的JDK版本,相比Java 7,增加了很多功能,帮助开发者们简化了很多代码。比如:Lambda,Stream流操作。而Stream流操作是Java8版本针对数据集合做开发出来的特有的抽象概念。它可以按照我们编写的方式对集合进行处理,对数据进行复杂的查询、过滤、映射提取数据等操作。

       说到Stream,我们往往会第一个想到I/O Stream,但是在Java8中,通过Lambda为核心的函数式编程,使得Java8有了一个新的Stream概念,用于解决当前集合库已有的弊端。

1.浅谈Lambda表达式

       Lambda 表达式,也可称为闭包,我们也能把 Lambda理解为是一段可以传递的代码,与以前的method方法传参不一样的是,它所传递的是代码,是逻辑。通过函数式接口将代码像形参一样进行传递,是一种紧凑的代码风格,让我们写出的代码变得更加灵活,简洁,使Java的语言表达能力得到了提升。

Lambda表达式核心:函数式接口

       什么是函数式接口呢?只有单一抽象方法的接口,使用 @FunctionalInterface 来描述,可转换成 Lambda 表达式,便称之为函数式接口。

使用举例:

public class LambdaTest {
     //我们定义一个函数式编程
     @FunctionalInterface
     interface MathOperation {
        //定义一个未知逻辑的抽象方法
        int operation(int a, int b);
     }
    //定义一个内部私有、使用这个函数式的方法
    private int operate(int a, int b, MathOperation mathOperation)
    {
        return mathOperation.operation(a, b);
    }
}

接下来我们拿MathOperation实现加法和减法

public class LambdaTest {
    //我们定义一个函数式编程
     @FunctionalInterface
     interface MathOperation {
        //定义一个未知逻辑的抽象方法
        int operation(int a, int b);
     }
    //定义一个内部私有、使用这个函数式的方法
    private int operate(int a, int b, MathOperation mathOperation)
    {
        return mathOperation.operation(a, b);
    }
    public static void main(String[] args)
    {
        LambdaTest test = new LambdaTest();
        //在内部 *实现* 对应的函数逻辑
        //有类型声明的加法
        MathOperation add = (int a, int b) -> a + b;
        //无类型声明的减法
        MathOperation sub = (a, b) -> a - b;
        //使用时传入参数和对应实现的方法
        //add:传入加法的实现逻辑(int a, int b) -> a + b
        System.out.println("使用add传参:10+5="+test.operate(10, 5, add));
        //sub:传入减法的实现逻辑(int a, int b) -> a - b
        System.out.println("使用sub传参:10-5="+test.operate(10, 5, sub));
        //控制台打印
        //使用add传参:10+5=15
        //使用sub传参:10-5=5
    }
}
它是怎么做到的呢?

       是因为一种新的操作符”->“,该操作符被称之为箭头操作符,操作符将表达式分成了两部分:

(int a, int b) -> a + b

       左侧:int a,int b 是Lambda表达式的参数,它对应到时候使用的MathOperation接口中抽象方法operation(int a, int b)的参数列表,而且必须顺序一致。

       右侧:a+b 是 Lambda表达式中所需要执行的功能,也就是operation(int a, int b)抽象方法的内部逻辑。

       如果你不清楚自己定义的是否是函数式编程,可以用@FunctionalInterface来判断。

       接下来,为了更好的了解Lambda表达式的实战效果,我们进入Stream操作环节。

2.Java8三个重要方面

使用Stream简化集合操作

对于开发的好处:

       一、方便自己:通过使用函数式编程来对数据集合进行处理,简洁且意图明确的代码方便你后续回忆,并且使用Stream接口还能让你从此告别复杂的for循环。

       二、方便他人:规范的存在拉近了开发人员之间的距离,统一使用Java8的stream,不仅大家都能理解到你的用意,还减少了双方对代码编写的分歧。

       如果要拿Stream与MySQL做对比的话,以下这个表可以帮助你清晰的认识Stream。

方法 中文 操作类型 对比SQL 作用
filter 筛选/过滤 中间步骤 where 对数据流进行过滤,过滤掉不符合传入条件
map 转换/投影 中间步骤 select 根据传入的函数、对流中的每个元素进行转换
flatMap 扁平化 中间步骤 -- 相当于map+flat,先通过map把每个元素转换为流,再通过把所有流链接在一起扁平化展开
sorted 排序 中间步骤 order by 使用传入的比较器,对流中的元素进行比较
distinct 去重 中间步骤 distinct 对流中的元素进行去重,原理是Object.equals判重
skip&limit 分页 中间步骤 limit 跳过部分元素以及限制元素数量
collect 收集 最后步骤 -- 对流进行最后的收集操作,把流转换成我们想要的数据格式
forEach 遍历 最后步骤 -- 对流中每一个元素进行遍历
anyMatch 是否有元素匹配 最后步骤 -- 判断流中是否有一个元素符合我们要的判断
allMatch 是否所有元素匹配 最后步骤 -- 判断流中所有元素是否符合我们要的判断

接下来针对每个方法,我们都举个简单的例子进行使用

@Data
public class Order {
    private Long id;
    private Long customerId;//顾客ID 
    private String name;//顾客姓名 
    private List<OrderItem> otherList;//订单商品明细 
    private Double totalPrice;//总价格 
    private LocalDateTime placedAt;//下单时间
}
@Data
public class OrderItem { 
    private Long productId;//商品ID 
    private String productName;//商品名称 
    private Double productPrice;//商品价格 
    private Integer productQuantity;//商品数量
}
filter方法

       .filter()方法可以实现对数据的过滤操作,相当于SQL中的where。

       它可以代替我们日常java开发中的for循环+equals匹配方法。

       有个List集合,如果我们要获取顾客姓名叫Tony的,总金额大于100的订单时,我们可以这样写:

@Test
public void testFilter(){
    List<Order> list = new ArrayList<>();
    list.add(Order.builder().name("Tony").id(1L).totalPrice(101.0).build());
    list.add(Order.builder().name("Sam").id(2L).totalPrice(91.0).build());
    list.add(Order.builder().name("Tony").id(3L).totalPrice(21.0).build());
    list.add(Order.builder().name("Sam").id(4L).totalPrice(131.0).build());
    //先过滤name叫Tony的,再过滤总价格大于100的,然后打印出来
    //以前的做法
    for (Order order : list) {
          Double totalPrice = order.getTotalPrice();
          String name = order.getName();
          if (totalPrice>100.0 && "Tony".equals(name)) {
              System.out.println(order);
          }
    }
    //Java 8的做法
    list.stream().filter(e -> "Tony".equals(e.getName()))
      .filter(e -> e.getTotalPrice()>100.0)
      .forEach(System.out::println);
}

       Java8的写法相对于以前的写法,在实现这个功能需求上,在写法上会相对的简洁很多,并且使用filter一看就知道,我打算过滤什么。而以前的做法把所有判断条件堆积在一起,if中判断条件一旦过多就会非常不美观,代码冗长且可阅读性很差。

       而且使用Java 8 的话,我可以直接一行就可以完成这项功能,不像以前那样需要7行才可以解决。

map方法

       .map()方法可以做转化提取,类似于SQL的select。

       在以前的java开发中,我们需要先for循环遍历,然后再把需要的字段打印出来,但是使用map就可以完全替换掉它。

       有个List集合,如果我们要获取总金额大于100的订单的顾客姓名时,我们可以这样写:

//先过滤、总价格大于100的,再把name转化出来,然后打印出来
    //以前的做法
    for (Order order : list) {
          Double totalPrice = order.getTotalPrice();
          String name = order.getName();
          if (totalPrice>100.0 ) {
              System.out.println(name);
          }
    } 
    //Java 8的做法
    list.stream().filter(e -> e.getTotalPrice()>100)
        .map(Order::getName)
        .forEach(System.out::println);

       Java8抽取name,也只需要在使用filter过滤完后再用map(ClassName::getXxx)抽取即可,不需要像以前一样,for循环对每个类的金额进行判断后再get出来。

flatMap方法

       .flatMpa()方法是一种扁平化操作,相当于map+flat操作,常用于获取一个list中嵌套的所有orderList,然后进行流操作。

       有个List集合,它嵌套了一个订单详情的otherList,我需要获取这个List集合里所有订单的总价格。

       我们来分析一下这个做法,正常情况下,我们需要遍历这个List,然后拿到每个Order对象里的otherList集合对象,然后遍历这个集合对象,使用商品单价productPrice 乘以 商品数量productQuantity,把得到的值放到一个list里,加法后打印出来。

如果使用flatMap方法,我们可以这样写:

List<Order> list = data;
    //先拿到每个Order对象里的otherList集合对象的流,然后让每个对象的商品单价productPrice 乘以 商品数量productQuantity,
    //以前的做法
    double sum1 = 0.0f;
    for (Order order : list) {
        List<OrderItem> otherList = order.getOtherList();
        for (OrderItem item : otherList) {
            double i = item.getProductQuantity() * item.getProductPrice();
            sum1 +=i;
        }
    }
    //现在的做法
    double sum = list.stream().flatMap(order -> order.getOtherList().stream())
                    .mapToDouble(item -> item.getProductQuantity() * item.getProductPrice())
                    .sum();
    System.out.println(sum);
       
       这个方法可以用于计算集合内的集合的属性。通过以前的做法可以看到,我们需要经过两次for循环才能计算内嵌的集合的值,之后加起来才是最后的总数。但是通过flatMap,我们可以把所有的内嵌集合都汇聚在一起,然后再通过mapToDouble来计算它们的总值。这样是不是就简单多了。
sorted方法、skip方法、limit方法

       .sorted() 方法可以用于集合内排序,相当于SQL中的order by。

       .skip()方法可以用于跳过数据。

       .limit()方法可以约束集合里的数量。

假设有个List集合

       我们要让它按照totalPrice总价格倒序排序,只要前五个时,我们就可以这样写:

List<Order> list = data;
// sorted(排序的标准) 倒序:reversed() 只要五条:limit
list.stream().sorted(comparing(Order::getTotalPrice).reversed()).limit(5).forEach(System.out::println);

       我们要让它按照totalPrice总价格倒序排序,只要第三个和第四个时,我们可以这样写:

List<Order> list = data;
//使用skip(跳过多少条),limit(取多少条)
list.stream().sorted(Comparator.comparing(Order::getTotalPrice)
             .reversed())
       .skip(2).limit(2)
       .forEach(System.out::println);
distinct方法

       .distinct()方法是数据去重,类似于SQL中的distinct。

       有一个List集合,我们要获取总金额大于100的订单的顾客姓名,且对它进行去重时,我们可以这样写:

List<Order> list = new ArrayList<>();
list.add(Order.builder().name("Tony").id(1L).totalPrice(101.0).build());
list.add(Order.builder().name("Sam").id(2L).totalPrice(91.0).build());
list.add(Order.builder().name("Tony").id(3L).totalPrice(21.0).build());
list.add(Order.builder().name("Sam").id(4L).totalPrice(131.0).build());//先过滤、总价格大于100的,再把name转化出来,然后打印出来
list.stream().filter(e -> e.getTotalPrice()>100)
    .map(Order::getName).distinct()
    .forEach(System.out::println);
collect方法

       collect方法是收集操作,对流进行终止的操作,然后把流转换成我们需要的数据格式。也可以理解成要把数据装到一个对象里的操作,不再进行数据的其他操作了,所以使用后就没办法再使用上面提到的那些方法了,除非再使用一次.stream()方法。

       collect中比较重要的就是Collectors类,以下是它的静态方法:

image.png

举几个collect常用案例:

一、使用collect实现字符串拼接,随机生成一定位数的字符串

Random random = new Random();
//实现逻辑是,获取随机48到122中的数字,过滤掉转成字符后是特殊字符的数字,然后取30位,使用collect方法用StringBuilder拼接。
String id = random.ints(48, 122)
                  .filter(i -> (i < 57 || i > 65) && (i < 90 || i > 97))
                  .mapToObj(i -> (char)i).limit(30)
                  .collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
System.out.println(id);

二、使用collect实现转List、LinkedList、Set....

//1使用list装数据
List<String> list1 = list.stream().map(Order::getName)
                                  .collect(Collectors.toList());
//1使用list装数据
List<String> list2 = list.stream().map(Order::getName)
             .collect(Collectors.toCollection(ArrayList::new));
//2使用Linkedlist装数据
LinkedList<String> linkedList = list.stream().map(Order::getName)
               .collect(Collectors.toCollection(LinkedList::new));
//3使用set来装数据
Set<String> set = list.stream().map(Order::getName)
             .collect(Collectors.toSet());

     其源码类似,都是定义对应T的数据类型集合,然后addAll进去,return就结束了,这个T可以自定义传入,也可以使用定义好的比如toList:

//1
public static <T>Collector<T, ?, List<T>> toList() {
        return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                                   (left, right) -> { left.addAll(right); return left; },CH_ID);
}
//2
public static <T, C extends Collection<T>> Collector<T, ?, C> toCollection(Supplier<C> collectionFactory) {
        return new CollectorImpl<>(collectionFactory, Collection<T>::add,
                                   (r1, r2) -> { r1.addAll(r2); return r1; },CH_ID);
}
//3
public static <T> Collector<T, ?, Set<T>> toSet() {
        return new CollectorImpl<>((Supplier<Set<T>>) HashSet::new, Set::add,
                                   (left, right) -> { left.addAll(right); return left; },CH_UNORDERED_ID);
}

三、使用collect计算平均购买的商品数量

//1是平均,2是购买数量,但是需要先求购买数量后求平均值
Double collect = list.stream().
      collect(
          Collectors.averagingInt(order -> order.getOtherList().stream()
                .collect(Collectors.summingInt(OrderItem::getProductQuantity)))
);


解析:先通过summingInt(OrderItem::getProductQuantity)获取集合里各自的购买数量,再通过averagingInt来算平均值来达成效果


四、使用collect分组

//使用groupingby以购买人名字为key,用Collectors.counting方法统计每个人下单的数量,然后comparingByValue().reversed()倒序排序
List<Map.Entry<String, Long>> collect1 = list.stream().collect(
           Collectors.groupingBy(Order::getName,Collectors.counting())
                ).entrySet().stream()
        .sorted(Map.Entry.<String, Long>comparingByValue().reversed()).collect(Collectors.toList());

五、使用collect+partitionBy给数据分区

//下了单的订单
List<Order> list = new ArrayList<>();
list.add(Order.builder().name("Tony").id(1L).totalPrice(101.0).build());
list.add(Order.builder().name("Sam").id(2L).totalPrice(91.0).build());
list.add(Order.builder().name("Tony").id(3L).totalPrice(21.0).build());
list.add(Order.builder().name("Sam").id(4L).totalPrice(131.0).build());//付了款的订单
List<Order> list2 = new ArrayList<>();
list2.add(Order.builder().name("Tony").id(1L).totalPrice(101.0).build());
//分组,把list分成true和false,true为已经付了款的,false为没付款的订单
//逻辑:list2.stream().map(Order::getId)获取付了款的id,partitioningBy用anyMatch匹配下了单的订单,形成的分组。
 Map<Boolean, List<Order>> data = list.stream().collect(
                Collectors.partitioningBy(order -> list2.stream().map(Order::getId).anyMatch(e -> e.equals(order.getId())))
                );
System.out.println(data);
使用 Optional 简化判空逻辑

       除了Optional以外,还有OptionalInt,OptionalDouble...等,各种不同基本类型的可空对象。此外Java8还定义了用于引用类型的Optional类,使用Optional,不仅可以避免数据联级内的空指针问题,它还给我们开发者提供了实用的方法避免判空逻辑。减少了我们对数据判断的代码编写,提升效率。

       以下是一些例子,演示了如何使用 Optional 来避免空指针,以及如何使用它的 fluent API 简化冗长的 if-else 判空逻辑。

@Test(expected = IllegalArgumentException.class)
 public void testOptional() {
     //通过get方法获取Optional中的值
     System.out.println(Optional.of(100).get());
     //通过ofNullable来初始化一个空字符串对象,通过orElse方法实现Optional中无数据的时候返回一个默认值
     System.out.println(Optional.ofNullable(null).orElse(""));
     //double的Optional对象,isPresent可判断OptionalDouble有无数据
     System.out.println(OptionalDouble.empty().isPresent());
     //通过map方法可以对Optional对象进行级联转换,不会出现空指针
     System.out.println(Optional.of(100).map(Math::incrementExact).get());
     //使用filter实现Optional中数据的过滤,得到一个Optional,使用orElse提供默认值
     System.out.println(Optional.of(1).filter(e -> e % 2 == 0).orElse(null));
     //通过orElseThrow在无数据时抛出异常
     System.out.println(Optional.empty().orElseThrow(IllegalArgumentException::new));
 }

下面是关于Optional的方法整理:

方法 作用
.empty() 返回一个空的Optional
.orElse() 有值则返回,否则返回默认值
.orElseGet() 有值则返回,否则返回Supplier函数提供的值
.orElseThrow() 有值则返回,否则返回Supplier函数生成的异常
.of() 将值进行Optional包装,值为null则抛出NullPointerException异常
.ofNullable() 将值进行Optional包装,值为null则生成空的Optional
.ifPresent() 有值则使用Consumer函数消费值
.ifPresent() 判断是否有值
.get() 有值则获取值,否则抛出NoSuchElementException异常
.map() 如果有值,则应用传入的Function函数
.filter() 如果有值且匹配传入的Predicate函数,则返回包含值的Optional,否则返回空的Optional
.stream() 如果有值则返回包含值的steam,否则返回空的stream
Java 8 类对于函数式 API 的增强

       除了Stream以外,Java 8中有很多类都实现了函数式的功能,比如ConcurrentHashMap。ConcurrentHashMap一般常用于系统缓存,接下来我们通过ConcurrentHashMap来看看Java 8 的增强。

//系统缓存map
 private Map<Long, Order> cacheMap = new ConcurrentHashMap<>();
    //创建一个数据集合
    List<Order> list = new ArrayList<>();
    //原本获取缓存的写法
    private Order beforeGetCacheMethod(Long id) {
        Order order = null;
        //Key存在,返回Value
        if (cache.containsKey(id)) {
            order = cacheMap.get(id);//获取后直接赋值,跳过else直接return
        } else {
            //不存在,则获取Value
            //需要遍历数据源查询获得order
            for (Order o : list) {
                if (o.getId().equals(id)) {//判断是否存在这个id
                    order = o;//存在则直接赋值
                    break;
                }
            }
            //判断order是否为null
            if (order != null)
                cacheMap.put(id, order);//加入map中去
            }
        return order;//返回对象
    }
    //Java 8 中,我们利用 ConcurrentHashMap 用可以这样实现繁琐的操作
    //现在获取缓存的写法
    private Order nowGetCacheMethod(Long id) { 
    //当Key不存在的时候提供一个Function来代表根据Key获取Value过程 
    return cacheMap.computeIfAbsent(id, item -> 
                    list.stream()
                        .filter(p -> p.getId().equals(item)) //通过对象里的id属性过滤数据
                        .findFirst() //只找第一个,其他的不理会。得到Optional<Order>
                        .orElse(null)); //如果找不到符合要求的,则返回一个默认值,这里我们设置为null
    }

computeIfAbsent则替代掉繁杂的逻辑,以下是它具体的实现源码:

default V computeIfAbsent(K key,Function<? super K, ? extends V> mappingFunction) {
        //判断传入的Function是否是null
        Objects.requireNonNull(mappingFunction);
        V v;
        //给v赋值的同时顺便判断是否为null
        if ((v = get(key)) == null) {
            V newValue;
            //执行传入的Funcation函数后判断所得值是否为null
            if ((newValue = mappingFunction.apply(key)) != null) {
             //不为null则存入map里,然后再返回值
                put(key, newValue);
                return newValue;
            }
        }
        return v;
    }

computeIfAbsent方法的逻辑其实也是很简单,跟我们原本的写法逻辑一样,只是它的代码非常的简洁,不局限于任何一个实现逻辑。它能通过Function函数对应的方法来控制缓存的条件。不需要像以前一样,一种缓存方式需要写一个方法。

3.结语

       我们来回顾一下,这次我给你简单的介绍了一下Java 8 中最为重要和常用的几个功能,其中Lambda、Stream、Optional操作,这些特性可以帮助我们写出简单易懂、可读性强的代码。有些案例可能不太好理解,我建议你对着代码逐一到源码中查看这些操作的方法定义,以及 JDK 中的代码注释。然后亲手多敲几遍,修改几次参数,加深对方法的印象。代码之道,在于多写多测多练习,以强化记忆、加深理解。

相关文章
|
运维 监控 数据可视化
当开发同事辞职,接手到垃圾代码怎么办?
当开发同事辞职,接手到垃圾代码怎么办?
|
Web App开发 安全 中间件
学会这招,技术问题再也难不倒你
学会这招,技术问题再也难不倒你
学会这招,技术问题再也难不倒你
|
消息中间件 存储 JavaScript
如何写出一手让同事膜拜的漂亮代码?
如何写出一手让同事膜拜的漂亮代码?
|
编解码 前端开发 程序员
刚入职的程序员做不好哪些事情容易被开除?
刚入职的程序员做不好哪些事情容易被开除?
835 0
|
IDE Java 程序员
疯了!同事又问我为什么不能用 isXXX
最近在做Code Review,写下了这篇文章:代码写成这样,老夫无可奈何!,说多了都是泪啊。。
|
芯片
瞧!公务员的工作还可以这样干
盼啊盼,第六届世界互联网大会在乌镇如期而至。 在今天的大会上,小云带来了帮助公务员提升工作效率的“神器”,平头哥压箱底的“宝贝”...... 一起来深入了解下。
9253 0
|
物联网 大数据 数据库
产品:“嘘,这事千万别让开发知道”
作为2019年首场最受瞩目的云计算开发者大会,阿里云火力全开。本次开发者大会聚焦开源大数据、IT基础设施云化、数据库、云原生、物联网五大主力方向。
2199 0
|
程序员 开发者
如何写出让同事膜拜的漂亮代码?
“代码千万行,注释第一行;编程不规范,同事两行泪”;"道路千万条,安全第一条。代码不规范,亲人两行泪。"在技术圈广为盛传,可见代码不规范让程序员们是多么的头痛。
1457 0

相关实验场景

更多