5万字长文:Stream和Lambda表达式最佳实践-附PDF下载(二)

简介: 5万字长文:Stream和Lambda表达式最佳实践-附PDF下载(二)

3.3 在Functional Interfaces中不要滥用Default Methods


Functional Interface是指只有一个未实现的抽象方法的接口。


如果该Interface中有多个方法,则可以使用default关键字为其提供一个默认的实现。


但是我们知道Interface是可以多继承的,一个class可以实现多个Interface。 如果多个Interface中定义了相同的default方法,则会报错。


通常来说default关键字一般用在升级项目中,避免代码报错。


3.4 使用Lambda 表达式来实例化Functional Interface


还是上面的例子:


@FunctionalInterface
public interface Usage {
    String method(String string);
}


要实例化Usage,我们可以使用new关键词:


Usage usage = new Usage() {
    @Override
    public String method(String string) {
        return string;
    }
};


但是最好的办法就是用lambda表达式:


Usage usage = parameter -> parameter;


3.5 不要重写Functional Interface作为参数的方法


怎么理解呢? 我们看下面两个方法:


public class ProcessorImpl implements Processor {
    @Override
    public String process(Callable<String> c) throws Exception {
        // implementation details
    }
    @Override
    public String process(Supplier<String> s) {
        // implementation details
    }
}


两个方法的方法名是一样的,只有传入的参数不同。但是两个参数都是Functional Interface,都可以用同样的lambda表达式来表示。


在调用的时候:


String result = processor.process(() -> "test");


因为区别不了到底调用的哪个方法,则会报错。


最好的办法就是将两个方法的名字修改为不同的。


3.6 Lambda表达式和内部类是不同的


虽然我们之前讲到使用lambda表达式可以替换内部类。但是两者的作用域范围是不同的。


在内部类中,会创建一个新的作用域范围,在这个作用域范围之内,你可以定义新的变量,并且可以用this引用它。


但是在Lambda表达式中,并没有定义新的作用域范围,如果在Lambda表达式中使用this,则指向的是外部类。


我们举个例子:


private String value = "Outer scope value";
public String scopeExperiment() {
    Usage usage = new Usage() {
        String value = "Inner class value";
        @Override
        public String method(String string) {
            return this.value;
        }
    };
    String result = usage.method("");
    Usage usageLambda = parameter -> {
        String value = "Lambda value";
        return this.value;
    };
    String resultLambda = usageLambda.method("");
    return "Results: result = " + result + 
      ", resultLambda = " + resultLambda;
}


上面的例子将会输出“Results: result = Inner class value, resultLambda = Outer scope value”


3.7 Lambda Expression尽可能简洁


通常来说一行代码即可。如果你有非常多的逻辑,可以将这些逻辑封装成一个方法,在lambda表达式中调用该方法即可。


因为lambda表达式说到底还是一个表达式,表达式当然越短越好。


java通过类型推断来判断传入的参数类型,所以我们在lambda表达式的参数中尽量不传参数类型,像下面这样:


(a, b) -> a.toLowerCase() + b.toLowerCase();


而不是:


(String a, String b) -> a.toLowerCase() + b.toLowerCase();


如果只有一个参数的时候,不需要带括号:


a -> a.toLowerCase();


而不是:


(a) -> a.toLowerCase();


返回值不需要带return:


a -> a.toLowerCase();


而不是:


a -> {return a.toLowerCase()};


3.8 使用方法引用


为了让lambda表达式更加简洁,在可以使用方法引用的时候,我们可以使用方法引用:


a -> a.toLowerCase();


可以被替换为:


String::toLowerCase;


3.9 Effectively Final 变量


如果在lambda表达式中引用了non-final变量,则会报错。


effectively final是什么意思呢?这个是一个近似final的意思。只要一个变量只被赋值一次,那么编译器将会把这个变量看作是effectively final的。


String localVariable = "Local";
    Usage usage = parameter -> {
         localVariable = parameter;
        return localVariable;
    };


上面的例子中localVariable被赋值了两次,从而不是一个Effectively Final 变量,会编译报错。


为什么要这样设置呢?因为lambda表达式通常会用在并行计算中,当有多个线程同时访问变量的时候Effectively Final 变量可以防止不可以预料的修改。


4. stream表达式中实现if/else逻辑


在Stream处理中,我们通常会遇到if/else的判断情况,对于这样的问题我们怎么处理呢?

还记得我们在上一篇文章lambda最佳实践中提到,lambda表达式应该越简洁越好,不要在其中写臃肿的业务逻辑。


接下来我们看一个具体的例子。


4.1 传统写法


假如我们有一个1 to 10的list,我们想要分别挑选出奇数和偶数出来,传统的写法,我们会这样使用:


public void inForEach(){
        List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        ints.stream()
                .forEach(i -> {
                    if (i.intValue() % 2 == 0) {
                        System.out.println("i is even");
                    } else {
                        System.out.println("i is old");
                    }
                });
    }

上面的例子中,我们把if/else的逻辑放到了forEach中,虽然没有任何问题,但是代码显得非常臃肿。


接下来看看怎么对其进行改写。


4.2 使用filter


我们可以把if/else的逻辑改写为两个filter:


List<Integer> ints = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
        Stream<Integer> evenIntegers = ints.stream()
                .filter(i -> i.intValue() % 2 == 0);
        Stream<Integer> oddIntegers = ints.stream()
                .filter(i -> i.intValue() % 2 != 0);


有了这两个filter,再在filter过后的stream中使用for each:


evenIntegers.forEach(i -> System.out.println("i is even"));
        oddIntegers.forEach(i -> System.out.println("i is old"));

怎么样,代码是不是非常简洁明了。


5. 在map中使用stream


Map是java中非常常用的一个集合类型,我们通常也需要去遍历Map去获取某些值,java 8引入了Stream的概念,那么我们怎么在Map中使用Stream呢?


5.1 基本概念


Map有key,value还有表示key,value整体的Entry。


创建一个Map:


Map<String, String> someMap = new HashMap<>();


获取Map的entrySet:


Set<Map.Entry<String, String>> entries = someMap.entrySet();


获取map的key:


Set<String> keySet = someMap.keySet();


获取map的value:


Collection<String> values = someMap.values();


上面我们可以看到有这样几个集合:Map,Set,Collection。


除了Map没有stream,其他两个都有stream方法:


Stream<Map.Entry<String, String>> entriesStream = entries.stream();
        Stream<String> valuesStream = values.stream();
        Stream<String> keysStream = keySet.stream();


我们可以通过其他几个stream来遍历map。


5.2 使用Stream获取map的key


我们先给map添加几个值:


someMap.put("jack","20");
someMap.put("bill","35");


上面我们添加了name和age字段。


如果我们想查找age=20的key,则可以这样做:


Optional<String> optionalName = someMap.entrySet().stream()
                .filter(e -> "20".equals(e.getValue()))
                .map(Map.Entry::getKey)
                .findFirst();
        log.info(optionalName.get());


因为返回的是Optional,如果值不存在的情况下,我们也可以处理:


optionalName = someMap.entrySet().stream()
                .filter(e -> "Non ages".equals(e.getValue()))
                .map(Map.Entry::getKey).findFirst();
        log.info("{}",optionalName.isPresent());


上面的例子我们通过调用isPresent来判断age是否存在。


如果有多个值,我们可以这样写:


someMap.put("alice","20");
        List<String> listnames = someMap.entrySet().stream()
                .filter(e -> e.getValue().equals("20"))
                .map(Map.Entry::getKey)
                .collect(Collectors.toList());
        log.info("{}",listnames);


上面我们调用了collect(Collectors.toList())将值转成了List。


5.3 使用stream获取map的value


上面我们获取的map的key,同样的我们也可以获取map的value:


List<String> listAges = someMap.entrySet().stream()
                .filter(e -> e.getKey().equals("alice"))
                .map(Map.Entry::getValue)
                .collect(Collectors.toList());
        log.info("{}",listAges);


上面我们匹配了key值是alice的value。


6. Stream中的操作类型和peek的使用


java 8 stream作为流式操作有两种操作类型,中间操作和终止操作。这两种有什么区别呢?


我们看一个peek的例子:


Stream<String> stream = Stream.of("one", "two", "three","four");
        stream.peek(System.out::println);


上面的例子中,我们的本意是打印出Stream的值,但实际上没有任何输出。


为什么呢?


6.1 中间操作和终止操作


一个java 8的stream是由三部分组成的。数据源,零个或一个或多个中间操作,一个或零个终止操作。


中间操作是对数据的加工,注意,中间操作是lazy操作,并不会立马启动,需要等待终止操作才会执行。


终止操作是stream的启动操作,只有加上终止操作,stream才会真正的开始执行。


所以,问题解决了,peek是一个中间操作,所以上面的例子没有任何输出。


6.2 peek


我们看下peek的文档说明:peek主要被用在debug用途。


我们看下debug用途的使用:


Stream.of("one", "two", "three","four").filter(e -> e.length() > 3)
                .peek(e -> System.out.println("Filtered value: " + e))
                .map(String::toUpperCase)
                .peek(e -> System.out.println("Mapped value: " + e))
                .collect(Collectors.toList());


上面的例子输出:


Filtered value: three
Mapped value: THREE
Filtered value: four
Mapped value: FOUR


上面的例子我们输出了stream的中间值,方便我们的调试。


为什么只作为debug使用呢?我们再看一个例子:


Stream.of("one", "two", "three","four").peek(u -> u.toUpperCase())
                .forEach(System.out::println);


上面的例子我们使用peek将element转换成为upper case。然后输出:


one
two
three
four


可以看到stream中的元素并没有被转换成大写格式。


再看一个map的对比:


Stream.of("one", "two", "three","four").map(u -> u.toUpperCase())
                .forEach(System.out::println);


输出:


ONE
TWO
THREE
FOUR


可以看到map是真正的对元素进行了转换。


当然peek也有例外,假如我们Stream里面是一个对象会怎么样?


@Data
    @AllArgsConstructor
    static class User{
        private String name;
    }


List<User> userList=Stream.of(new User("a"),new User("b"),new User("c")).peek(u->u.setName("kkk")).collect(Collectors.toList());
        log.info("{}",userList);


输出结果:


10:25:59.784 [main] INFO com.flydean.PeekUsage - [PeekUsage.User(name=kkk), PeekUsage.User(name=kkk), PeekUsage.User(name=kkk)]


我们看到如果是对象的话,实际的结果会被改变。


为什么peek和map有这样的区别呢?


我们看下peek和map的定义:


Stream<T> peek(Consumer<? super T> action)
<R> Stream<R> map(Function<? super T, ? extends R> mapper);


peek接收一个Consumer,而map接收一个Function。


Consumer是没有返回值的,它只是对Stream中的元素进行某些操作,但是操作之后的数据并不返回到Stream中,所以Stream中的元素还是原来的元素。


而Function是有返回值的,这意味着对于Stream的元素的所有操作都会作为新的结果返回到Stream中。


这就是为什么peek String不会发生变化而peek Object会发送变化的原因。


7. lambda表达式中的异常处理


java 8中引入了lambda表达式,lambda表达式可以让我们的代码更加简介,业务逻辑更加清晰,但是在lambda表达式中使用的Functional Interface并没有很好的处理异常,因为JDK提供的这些Functional Interface通常都是没有抛出异常的,这意味着需要我们自己手动来处理异常。


因为异常分为Unchecked Exception和checked Exception,我们分别来讨论。


7.1 处理Unchecked Exception


Unchecked exception也叫做RuntimeException,出现RuntimeException通常是因为我们的代码有问题。RuntimeException是不需要被捕获的。也就是说如果有RuntimeException,没有捕获也可以通过编译。


我们看一个例子:


List<Integer> integers = Arrays.asList(1,2,3,4,5);
        integers.forEach(i -> System.out.println(1 / i));


这个例子是可以编译成功的,但是上面有一个问题,如果list中有一个0的话,就会抛出ArithmeticException。


虽然这个是一个Unchecked Exception,但是我们还是想处理一下:


integers.forEach(i -> {
            try {
                System.out.println(1 / i);
            } catch (ArithmeticException e) {
                System.err.println(
                        "Arithmetic Exception occured : " + e.getMessage());
            }
        });


上面的例子我们使用了try,catch来处理异常,简单但是破坏了lambda表达式的最佳实践。代码变得臃肿。


我们将try,catch移到一个wrapper方法中:


static Consumer<Integer> lambdaWrapper(Consumer<Integer> consumer) {
        return i -> {
            try {
                consumer.accept(i);
            } catch (ArithmeticException e) {
                System.err.println(
                        "Arithmetic Exception occured : " + e.getMessage());
            }
        };
    }


则原来的调用变成这样:


integers.forEach(lambdaWrapper(i -> System.out.println(1 / i)));


但是上面的wrapper固定了捕获ArithmeticException,我们再将其改编成一个更通用的类:


static <T, E extends Exception> Consumer<T>
    consumerWrapperWithExceptionClass(Consumer<T> consumer, Class<E> clazz) {
        return i -> {
            try {
                consumer.accept(i);
            } catch (Exception ex) {
                try {
                    E exCast = clazz.cast(ex);
                    System.err.println(
                            "Exception occured : " + exCast.getMessage());
                } catch (ClassCastException ccEx) {
                    throw ex;
                }
            }
        };
    }


上面的类传入一个class,并将其cast到异常,如果能cast,则处理,否则抛出异常。


这样处理之后,我们这样调用:


integers.forEach(
                consumerWrapperWithExceptionClass(
                        i -> System.out.println(1 / i),
                        ArithmeticException.class));


7.2 处理checked Exception


checked Exception是必须要处理的异常,我们还是看个例子:


static void throwIOException(Integer integer) throws IOException {
    }


List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
        integers.forEach(i -> throwIOException(i));


上面我们定义了一个方法抛出IOException,这是一个checked Exception,需要被处理,所以在下面的forEach中,程序会编译失败,因为没有处理相应的异常。


最简单的办法就是try,catch住,如下所示:


integers.forEach(i -> {
            try {
                throwIOException(i);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        });


当然,这样的做法的坏处我们在上面已经讲过了,同样的,我们可以定义一个新的wrapper方法:


static <T> Consumer<T> consumerWrapper(
            ThrowingConsumer<T, Exception> throwingConsumer) {
        return i -> {
            try {
                throwingConsumer.accept(i);
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        };
    }


我们这样调用:


integers.forEach(consumerWrapper(i -> throwIOException(i)));


我们也可以封装一下异常:


static <T, E extends Exception> Consumer<T> consumerWrapperWithExceptionClass(
            ThrowingConsumer<T, E> throwingConsumer, Class<E> exceptionClass) {
        return i -> {
            try {
                throwingConsumer.accept(i);
            } catch (Exception ex) {
                try {
                    E exCast = exceptionClass.cast(ex);
                    System.err.println(
                            "Exception occured : " + exCast.getMessage());
                } catch (ClassCastException ccEx) {
                    throw new RuntimeException(ex);
                }
            }
        };
    }


然后这样调用:


integers.forEach(consumerWrapperWithExceptionClass(
                i -> throwIOException(i), IOException.class));


8. stream中throw Exception


之前的文章我们讲到,在stream中处理异常,需要将checked exception转换为unchecked exception来处理。


我们是这样做的:


static <T> Consumer<T> consumerWrapper(
            ThrowingConsumer<T, Exception> throwingConsumer) {
        return i -> {
            try {
                throwingConsumer.accept(i);
            } catch (Exception ex) {
                throw new RuntimeException(ex);
            }
        };
    }


将异常捕获,然后封装成为RuntimeException。


封装成RuntimeException感觉总是有那么一点点问题,那么有没有什么更好的办法?


8.1 throw小诀窍


java的类型推断大家应该都知道,如果是 这样的形式,那么T将会被认为是RuntimeException!


我们看下例子:


public class RethrowException {
    public static <T extends Exception, R> R throwException(Exception t) throws T {
        throw (T) t; // just throw it, convert checked exception to unchecked exception
    }
}


上面的类中,我们定义了一个throwException方法,接收一个Exception参数,将其转换为T,这里的T就是unchecked exception。


接下来看下具体的使用:


@Slf4j
public class RethrowUsage {
    public static void main(String[] args) {
        try {
            throwIOException();
        } catch (IOException e) {
           log.error(e.getMessage(),e);
            RethrowException.throwException(e);
        }
    }
    static void throwIOException() throws IOException{
        throw new IOException("io exception");
    }
}


上面的例子中,我们将一个IOException转换成了一个unchecked exception。


相关文章
|
1月前
|
XML 缓存 JSON
为什么浏览器中有些图片、PDF等文件点击后有些是预览,有些是下载
为什么浏览器中有些图片、PDF等文件点击后有些是预览,有些是下载
124 0
|
2月前
|
安全 算法 文件存储
共享资料下载,自动转PDF并添加隐形水印
云盒子企业网盘增强文件安全,支持下载时自动转PDF并加水印。管理员可配置目录规则,选择明水印、隐形水印或点阵水印。明水印直观防复制,隐形水印用于隐蔽追踪,点阵水印不影响阅读。文件格式支持度和水印类型取决于设置。此功能适用于文档安全、版权保护等场景。欲知详情或测试,访问[云盒子官网](yhz66.com)咨询客服。
|
1月前
|
数据采集 前端开发 开发者
解决PuppeteerSharp生成PDF颜色问题的最佳实践
使用PuppeteerSharp生成PDF时颜色丢失是个常见问题。本文介绍如何通过正确配置PdfOptions与CSS规则(如设置`PrintBackground`为`true`及使用`@media print`确保颜色准确显示),结合爬虫代理IP、User-Agent和Cookie设置等技巧来解决此问题,并提供了完整的代码示例。这些方法不仅有助于保持PDF的颜色准确性,还能增强爬虫的稳定性和效率。
解决PuppeteerSharp生成PDF颜色问题的最佳实践
|
2月前
|
JavaScript 前端开发 程序员
《JavaScript权威指南第7版》中文PDF+英文PDF+源代码 +JavaScript权威指南(第6版)(附源码)PDF下载阅读分享推荐
JavaScript是Web标准语言,广泛应用于各类浏览器,造就了其最广泛部署的地位。Node.js的兴起扩展了JavaScript的使用场景,使其成为开发者首选语言。无论新手还是经验丰富的程序员,都能受益于学习JavaScript。[《JavaScript权威指南第7版》资源链接](https://zhangfeidezhu.com/?p=224)
127 5
《JavaScript权威指南第7版》中文PDF+英文PDF+源代码 +JavaScript权威指南(第6版)(附源码)PDF下载阅读分享推荐
|
2月前
|
C++ Python
《从零开始学Python》(第二版) PDF下载读书分享
Python,由Guido van Rossum创造(1989),是1991年发布的面向对象、解释型编程语言,以其简洁清晰的语法和强大的库著称,昵称“胶水语言”。它连接不同模块,强调代码的优雅、明确和简单。《从零开始学Python》(第二版)是本风趣、实践导向的教材,提供PDF下载,是学习Python的宝贵资源。![书封](https://ucc.alicdn.com/pic/developer-ecology/nrw3f3oqlpmag_40f357729aac4defa97fb1e0f66a2501.png)
44 1
《从零开始学Python》(第二版) PDF下载读书分享
|
2月前
ChatGPT提问获取高质量答案的艺术PDF下载书籍推荐分享
**掌握ChatGPT高质量Prompt技巧的指南,教你艺术性提问以获取卓越答案。适用于各层次用户,提升内容创作效率。了解Prompt工程,作为对话模式触发器,有效引导ChatGPT生成人类般文本。点击获取PDF资源:[ChatGPT提问艺术](https://zhangfeidezhu.com/?p=334)**
38 0
ChatGPT提问获取高质量答案的艺术PDF下载书籍推荐分享
|
2月前
ChatGPT提问提示指南PDF下载经典分享推荐书籍,让你做好prompt工程
**掌握ChatGPT提问艺术:本书提供有效互动策略,教你构造精准提示获取专业答案。适用于各层次用户,通过实例解析提示工程,驱动模型生成定制化文本。[PDF下载](https://zhangfeidezhu.com/?p=335)**
82 0
ChatGPT提问提示指南PDF下载经典分享推荐书籍,让你做好prompt工程
|
2月前
|
数据挖掘 Python
利用Python进行数据分析PDF下载经典数据分享推荐
**Python数据分析大师作,Wes McKinney亲著,详述数据操作、清洗与分析。第2版面向Python 3.6,涵盖pandas、NumPy、IPython和Jupyter更新,实战案例丰富;第3版已升级至Python 3.10和pandas 1.4,继续引领数据科学潮流。[PDF下载](https://zhangfeidezhu.com/?p=337)**
50 0
利用Python进行数据分析PDF下载经典数据分享推荐
|
1月前
|
Linux Python Windows
Python PDF文件转Word格式,只需要3秒(附打包)
Python PDF文件转Word格式,只需要3秒(附打包)
54 3
Python PDF文件转Word格式,只需要3秒(附打包)
|
23天前
|
移动开发 资源调度 JavaScript
Vue移动端网页(H5)预览pdf文件(pdfh5和vue-pdf)
这篇文章介绍了在Vue移动端网页中使用`pdfh5`和`vue-pdf`两个插件来实现PDF文件的预览,包括滚动查看、缩放、添加水印、分页加载、跳转指定页数等功能。
Vue移动端网页(H5)预览pdf文件(pdfh5和vue-pdf)

热门文章

最新文章