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

2023-03-07 10:05:50 浏览数 (2)

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 表达式,便称之为函数式接口。

使用举例:

代码语言:javascript复制
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实现加法和减法

代码语言:javascript复制
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
  
    }

}
它是怎么做到的呢?

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

代码语言:javascript复制
(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

是否所有元素匹配

最后步骤

--

判断流中所有元素是否符合我们要的判断

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

代码语言:javascript复制
@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的订单时,我们可以这样写:

代码语言:javascript复制
@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的订单的顾客姓名时,我们可以这样写:

代码语言:javascript复制
        
    //先过滤、总价格大于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方法,我们可以这样写:

代码语言:javascript复制

    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总价格倒序排序,只要前五个时,我们就可以这样写:

代码语言:javascript复制

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

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

代码语言:javascript复制

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的订单的顾客姓名,且对它进行去重时,我们可以这样写:

代码语言:javascript复制

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类,以下是它的静态方法:

方法

返回类型

作用

toList

List

把流中的元素收集成为一个List

toSet

Set

把流中的元素收集成为一个Set,去重

toCollection

Collection

把流中的元素收集成为指定的集合

counting

Long

计算元素中的个数

summingInt

Integer

对元素中某个整形属性进行求和

averagingInt

Double

对元素中的某个整形属性进行求平均值

joining

String

连接流中元素toString后的字符串

minBy

Optional

选出最小值

maxBy

Optional

选出最大值

collectionAndThen

根据收集器返回

传入一个收集器,对其进行转化

groupBy

Map<K,List>

根据指定的属性进行分组,指定的属性为key

partitionBy

Map<Boolean,List>

对元素中的某个值进行判断,true为一组,false为一组

举几个collect常用案例:

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

代码语言:javascript复制

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....

代码语言:javascript复制
//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:

代码语言:javascript复制
//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计算平均购买的商品数量

代码语言:javascript复制

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

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

四、使用collect分组

代码语言:javascript复制

//使用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给数据分区

代码语言:javascript复制

//下了单的订单
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 判空逻辑。

代码语言:javascript复制
 @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 的增强。

代码语言:javascript复制
 //系统缓存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则替代掉繁杂的逻辑,以下是它具体的实现源码:

代码语言:javascript复制

    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.结语

0 人点赞