《OnJava8》精读(六)lambda、流式编程、异常处理及代码校验
@
介绍
《On Java 8》是什么?
它是《Thinking In Java》的作者Bruce Eckel基于Java8写的新书。里面包含了对Java深入的理解及思想维度的理念。可以比作Java界的“武学秘籍”。任何Java语言的使用者,甚至是非Java使用者但是对面向对象思想有兴趣的程序员都该一读的经典书籍。目前豆瓣评分9.5,是公认的编程经典。
为什么要写这个系列的精读博文?
由于书籍读起来时间久,过程漫长,因此产生了写本精读系列的最初想法。除此之外,由于中文版是译版,读起来还是有较大的生硬感(这种差异并非译者的翻译问题,类似英文无法译出唐诗的原因),这导致我们理解作者意图需要一点推敲。再加上原书的内容很长,只第一章就多达一万多字(不含代码),读起来就需要大量时间。
所以,如果现在有一个人能替我们先仔细读一遍,筛选出其中的精华,让我们可以在地铁上或者路上不用花太多时间就可以了解这边经典书籍的思想那就最好不过了。于是这个系列诞生了。
一些建议
推荐读本书的英文版原著。此外,也可以参考本书的中文译版。我在写这个系列的时候,会尽量的保证以“陈述”的方式表达原著的内容,也会写出自己的部分观点,但是这种观点会保持理性并尽量少而精。本系列中对于原著的内容会以引用的方式体现。
最重要的一点,大家可以通过博客平台的评论功能多加交流,这也是学习的一个重要环节。
第十三章 函数式编程
本章总字数:12800
关键词:
- Lambda
- 方法引用
- 高阶函数
本篇是系列《On Java 8》精读的第六篇。相比较前几篇,本篇更多讲解编程的方法及技巧。在原著中,十三至十六章多以编程经验理论为主。但是文中依然有不少值得学习的知识,比如——Lambda。
比起C#,Java支持Lambda较晚——在Java8中新增了该功能。在前几篇讲Java内部类的时候我们曾经提到过Lambda,作者也同样认为Lambda的出现使得内部类的一些写法被取代。
Lambda
首先,Lambda表达式所产生的是函数(方法)而不是类。
public interface Print {
String outPrint();
}
void test() {
Print print = () -> "hello!";
System.out.println(print.outPrint());
}
结果:
hello!
Lambda表达式由 ->符号及参数、方法体组成。 ->符号前是参数部分,之后是方法体。
上文示例是没有参数的情况,也可以写出如下代码:
当有一个参数时:
public interface Print {
String outPrint(String s);
}
void test() {
Print print = (s) -> "Hi "+s;
System.out.println(print.outPrint("Jimmy!"));
}
结果:
Hi Jimmy!
当有多个参数时:
public interface Print {
String outPrint(String s, int l);
}
void test() {
Print print = (s,l) -> "Hi "+s.substring(0,l);
System.out.println(print.outPrint("Jimmy!!!!!!!!!!!!!!!!!",6));
}
结果:
Hi Jimmy!
[1] 当只用一个参数,可以不需要括号 ()。 然而,这是一个特例。
[2] 正常情况使用括号 () 包裹参数。 为了保持一致性,也可以使用括号 () 包裹单个参数,虽然这种情况并不常见。
[3] 如果没有参数,则必须使用括号 () 表示空参数列表。
[4] 对于多个参数,将参数列表放在括号 () 中。
方法引用
除了Lambda,方法引用也是一个很有用的编程技巧。但事实上,它在实战中很少被用到,大家对它的了解也少于lambda。
方法引用使用 ::符号, ::符号前是类名或对象名称,符号后是引用的方法名。
public interface Print {
void outPrint(String s);
}
public class PrintHi {
void outPrint(String s) {
System.out.println("Hi " + s);
}
}
@Test
void test() {
PrintHi pH = new PrintHi();
Print p = pH::outPrint;
p.outPrint("Jimmy!");
}
结果:
Hi Jimmy!
除了使用对象方式,也可以引用static方法。由于是静态方法,直接使用类名即可,不需要实例化。
public interface Print {
void outPrint(String s);
}
public static class PrintHi {
static void outPrint(String s) {
System.out.println("Hi " + s);
}
}
@Test
void test() {
Print p = PrintHi::outPrint;
p.outPrint("Jimmy!");
}
结果:
Hi Jimmy!
高阶函数
高阶函数是一个产生或消费函数(方法)的函数(方法)。
import java.util.function.Function;
...
static Function<String, String> outPrint() {
return s -> s + " Jimmy!!";
}
@Test
void test() {
Function<String, String> f = outPrint();
System.out.println(f.apply("Hi"));
}
结果:
Hi Jimmy!!
高阶函数没有听起来那么吓人。上个示例中outPrint() 内部提供了一个方法,这个方法在后面通过apply() 执行。
值得一提的是, Function<String, String> 中,第一个String是参数,第二个是返回值。如果要使用Function,需要引入java.util.function.Function 接口。
上文示例,高阶函数是作为函数生产者的身份,接下来我们试试让他作为消费者:
import java.util.function.Function;
...
static String outPrint(Function<String, String> method) {
return method.apply("Jimmy!!!");
}
@Test
void test() {
System.out.println(outPrint((s) -> "Hi " + s));
}
结果:
Hi Jimmy!!!
第十四章 流式编程
本章总字数:17000
关键词:
- 什么是流式编程
- 流创建
- 中间操作
- 终端操作
什么是流式编程
流式编程是Java8新加入的编程概念。提起它,我们更容易想起另外一种编程语言——SQL。SQL的编程方式很有特点,往往你只需要告诉编译器你想要什么就可以了。比如查询A表,直接就是:
select * from A
具体A表是怎么查的,是怎么从数据库遍历取出的,每个数据类型是怎么处理的,我们都不需要关注。“我就要A的数据”——就这么简单。
流式编程的特点与之类似。我们对一个数据集合进行处理而不需要考虑操作细节,这就是流式编程。
先看一则原著中的示例:
// streams/Randoms.java
import java.util.*;
public class Randoms {
public static void main(String[] args) {
new Random(47)
.ints(5, 20)
.distinct()
.limit(7)
.sorted()
.forEach(System.out::println);
}
}
结果:
6
10
13
16
17
18
19
我们给 Random 对象一个种子(以便程序再次运行时产生相同的输出)。ints() 方法产生一个流并且 ints() 方法有多种方式的重载 — 两个参数限定了数值产生的边界。这将生成一个整数流。我们可以使用中间流操作(intermediate stream operation) distinct() 来获取它们的非重复值,然后使用 limit() 方法获取前 7 个元素。接下来,我们使用 sorted() 方法排序。最终使用 forEach() 方法遍历输出,它根据传递给它的函数对每个流对象执行操作。在这里,我们传递了一个可以在控制台显示每个元素的方法引用。System.out::println 。
流创建
除了上个示例中的Random (随机数流),在Java8中我们还可以使用很多其他类型的流。
比如,IntStream (整数流):
// streams/Ranges.java
import static java.util.stream.IntStream.*;
public class Ranges {
public static void main(String[] args) {
// 传统方法:
int result = 0;
for (int i = 10; i < 20; i++)
result += i;
System.out.println(result);
// for-in 循环:
result = 0;
for (int i : range(10, 20).toArray())
result += i;
System.out.println(result);
// 使用流:
System.out.println(range(10, 20).sum());
}
}
结果:
145
145
145
以及 Arrays.stream (数组流):
// streams/ArrayStreams.java
import java.util.*;
import java.util.stream.*;
public class ArrayStreams {
public static void main(String[] args) {
Arrays.stream(new double[] { 3.14159, 2.718, 1.618 })
.forEach(n -> System.out.format("%f ", n));
System.out.println();
Arrays.stream(new int[] { 1, 3, 5 })
.forEach(n -> System.out.format("%d ", n));
System.out.println();
Arrays.stream(new long[] { 11, 22, 44, 66 })
.forEach(n -> System.out.format("%d ", n));
System.out.println();
// 选择一个子域:
Arrays.stream(new int[] { 1, 3, 5, 7, 15, 28, 37 }, 3, 6)
.forEach(n -> System.out.format("%d ", n));
}
}
结果:
3.141590 2.718000 1.618000
1 3 5
11 22 44 66
7 15 28
最后一次 stream() 的调用有两个额外的参数。第一个参数告诉 stream() 从数组的哪个位置开始选择元素,第二个参数用于告知在哪里停止。每种不同类型的 stream() 都有类似的操作。
中间操作
流对象在创建之后要进行一系列操作,而这些操作流的中间操作会在处理之后将对象作为一个新流输出。
可能有点绕口。举个简单的例子就明白了:
select * from A order by a
这个SQL语句前半部分只是查出数据,而后半部分对数据进行排序,排序之后的对象作为一个新数据流输出,就是我们最终看到的排过序的A表数据。
在本章第一个例子中我们就已经见过几个中间操作,比如:sorted() 。
sorted() 除了使用无参的形式默认排序,还可以传入Comparator参数,或者传入Lambda表达式。
其他较常用的中间操作:
- distinct():用于消除流中的重复元素。
- filter(Predicate):过滤操作会保留与传递进去的过滤器函数计算结果为 true 元素。
- map(Function):将函数操作应用在输入流的元素中,并将返回值传递到输出流中。
- mapToInt(ToIntFunction):操作同上,但结果是 IntStream。
- mapToLong(ToLongFunction):操作同上,但结果是 LongStream。
- mapToDouble(ToDoubleFunction):操作同上,但结果是 DoubleStream。
- sorted():元素排序。
终端操作
终端操作可以认为是流处理的最后一步——将流转换为对象。
常用的终端操作,引用原著内容:
- toArray():将流转换成适当类型的数组。
- toArray(generator):在特殊情况下,生成自定义类型的数组。
- forEach(Consumer)常见如 System.out::println 作为 Consumer 函数。
- forEachOrdered(Consumer): 保证 forEach 按照原始流顺序操作。
- collect(Collector):使用 Collector 收集流元素到结果集合中。
- collect(Supplier, BiConsumer, BiConsumer):同上,第一个参数 Supplier 创建了一个新结果集合,第二个参数 BiConsumer 将下一个元素包含到结果中,第三个参数 BiConsumer 用于将两个值组合起来。
- allMatch(Predicate) :如果流的每个元素根据提供的 Predicate 都返回 true 时,结果返回为 true。在 - 第一个 false 时,则停止执行计算。
- anyMatch(Predicate):如果流中的任意一个元素根据提供的 Predicate 返回 true 时,结果返回为 true。在第一个 false 是停止执行计算。
- noneMatch(Predicate):如果流的每个元素根据提供的 Predicate 都返回 false 时,结果返回为 true。在第一个 true 时停止执行计算。
- findFirst():返回第一个流元素的 Optional,如果流为空返回 Optional.empty。
- findAny(:返回含有任意流元素的 Optional,如果流为空返回 Optional.empty。
- count():流中的元素个数。
- max(Comparator):根据所传入的 Comparator 所决定的“最大”元素。
- min(Comparator):根据所传入的 Comparator 所决定的“最小”元素。
- average() :求取流元素平均值。
第十五章 异常
本章总字数:26800
关键词:
- 异常的概念
- 异常捕获
- 自定义异常
- 捕获所有异常
- finally清理
异常的概念
人总会犯错,程序也是。任何未知的因素都可能导致发生异常。有些异常比较容易捕捉,甚至我们自己就能看出来,比如:1/0,因为除数不能为0会导致异常;有些异常则难以预料,这时候就需要对可能出现的异常预先设置捕获。
作者提了一个例子:
if(t == null)
throw new NullPointerException();
如果对象t 是 null,而被直接使用,就会出错,所以进行一个if 判断,如果为null 则抛出一个NullPointerException 异常。
异常也可以带参数:
throw new NullPointerException("t = null");
关键字 throw 将产生许多有趣的结果。在使用 new 创建了异常对象之后,此对象的引用将传给 throw。尽管异常对象的类型通常与方法设计的返回类型不同,但从效果上看,它就像是从方法“返回”的。可以简单地把异常处理看成一种不同的返回机制,当然若过分强调这种类比的话,就会有麻烦了。另外还能用抛出异常的方式从当前的作用域退出。在这两种情况下,将会返回一个异常对象,然后退出方法或作用域。
抛出异常与方法正常返回的相似之处到此为止。因为异常返回的“地点”与普通方法调用返回的“地点”完全不同。(异常将在一个恰当的异常处理程序中得到解决,它的位置可能离异常被抛出的地方很远,也可能会跨越方法调用栈的许多层级。)
异常捕获
如果需要针对某一个代码块进行异常捕获,可以使用try 关键词,对指定异常进行的特殊处理则可以用 catch关键词。
try {
// Code that might generate exceptions
} catch(Type1 id1) {
// Handle exceptions of Type1
} catch(Type2 id2) {
// Handle exceptions of Type2
} catch(Type3 id3) {
// Handle exceptions of Type3
}
// etc.
异常处理程序必须紧跟在 try 块之后。当异常被抛出时,异常处理机制将负责搜寻参数与异常类型相匹配的第一个处理程序。然后进入 catch 子句执行,此时认为异常得到了处理。一旦 catch 子句结束,则处理程序的查找过程结束
自定义异常
在Java中可以自定义异常类型。针对不同的使用场景,可以使用继承的方式创建新的异常。
比如:
class SimpleException extends Exception {}
我们也可以创造出接受参数的自定义异常:
// exceptions/FullConstructors.java
class MyException extends Exception {
MyException() {}
MyException(String msg) { super(msg); }
}
public class FullConstructors {
public static void f() throws MyException {
System.out.println("Throwing MyException from f()");
throw new MyException();
}
public static void g() throws MyException {
System.out.println("Throwing MyException from g()");
throw new MyException("Originated in g()");
}
public static void main(String[] args) {
try {
f();
} catch(MyException e) {
e.printStackTrace(System.out);
}
try {
g();
} catch(MyException e) {
e.printStackTrace(System.out);
}
}
}
结果:
Throwing MyException from f()
MyException
at FullConstructors.f(FullConstructors.java:11)
at
FullConstructors.main(FullConstructors.java:19)
Throwing MyException from g()
MyException: Originated in g()
at FullConstructors.g(FullConstructors.java:15)
at
FullConstructors.main(FullConstructors.java:24)
方法声明处显式的列出异常,可以让使用的程序员知道需要做哪些异常处理:
void f() throws TooBig, TooSmall, DivZero { // ...
Java 鼓励人们把方法可能会抛出的异常告知使用此方法的客户端程序员。这是种优雅的做法,它使得调用者能确切知道写什么样的代码可以捕获所有潜在的异常。
捕获所有异常
Exception 是所有异常类的基类,也因此可以使用它来捕获所有异常。
catch(Exception e) {
System.out.println("Caught an exception");
}
对于捕获的异常,我们可以使用其自带的方法来获取关于异常的信息:
// exceptions/ExceptionMethods.java
// Demonstrating the Exception Methods
public class ExceptionMethods {
public static void main(String[] args) {
try {
throw new Exception("My Exception");
} catch(Exception e) {
System.out.println("Caught Exception");
System.out.println(
"getMessage():" + e.getMessage());
System.out.println("getLocalizedMessage():" +
e.getLocalizedMessage());
System.out.println("toString():" + e);
System.out.println("printStackTrace():");
e.printStackTrace(System.out);
}
}
}
结果:
Caught Exception
getMessage():My Exception
getLocalizedMessage():My Exception
toString():java.lang.Exception: My Exception
printStackTrace():
java.lang.Exception: My Exception
at
ExceptionMethods.main(ExceptionMethods.java:7)
当有多个异常捕获且作出统一处理时,可以使用| 符号:
// exceptions/MultiCatch.java
public class MultiCatch {
void x() throws Except1, Except2, Except3, Except4 {}
void process() {}
void f() {
try {
x();
} catch(Except1 | Except2 | Except3 | Except4 e) {
process();
}
}
}
通过 throw 关键词可以已经捕获的异常重新抛出。
catch(Exception e) {
throw e;
}
重抛异常会把异常抛给上一级环境中的异常处理程序,同一个 try 块的后续 catch 子句将被忽略。此外,异常对象的所有信息都得以保持,所以高一级环境中捕获此异常的处理程序可以从这个异常对象中得到所有信息。
finally清理
在一个代码块中,如果我们希望无论是否有异常发生都执行某一个固定的操作,就可以使用finally 关键词。
比如:
try {
// The guarded region: Dangerous activities
// that might throw A, B, or C
} catch(A a1) {
// Handler for situation A
} catch(B b1) {
// Handler for situation B
} catch(C c1) {
// Handler for situation C
} finally {
// Activities that happen every time
}
那什么时候需要用到finally 关键词呢?有以下几个场景:
- 操作外部文件或应用程序,在最终需要关闭时使用。
- 对图形界面的绘制,在最终需要清除对象时使用。
- 在打开网络连接,最终需要断开时使用。
- 其他各种需要释放的对象或需要重置的内容。
finally 子句在任何时候都会被执行。即便在与 return关键词同在一个代码块中时,finally 依然会被执行。
// exceptions/MultipleReturns.java
public class MultipleReturns {
public static void f(int i) {
System.out.println(
"Initialization that requires cleanup");
try {
System.out.println("Point 1");
if(i == 1) return;
System.out.println("Point 2");
if(i == 2) return;
System.out.println("Point 3");
if(i == 3) return;
System.out.println("End");
return;
} finally {
System.out.println("Performing cleanup");
}
}
public static void main(String[] args) {
for(int i = 1; i <= 4; i++)
f(i);
}
}
结果:
Initialization that requires cleanup
Point 1
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Point 3
Performing cleanup
Initialization that requires cleanup
Point 1
Point 2
Point 3
End
Performing cleanup
如果使用finally 不恰当会出现异常丢失的现象。
比如这个示例中,即便出现了异常也不会被处理。
// exceptions/ExceptionSilencer.java
public class ExceptionSilencer {
public static void main(String[] args) {
try {
throw new RuntimeException();
} finally {
// Using 'return' inside the finally block
// will silence any thrown exception.
return;
}
}
}
第十六章 代码校验
本章总字数:22800
关键词:
- 单元测试
- JUnit
- 断言
上一章我们讲到了异常,也说过一句话“人总会犯错,程序也是”。这句话反过来还有另外一个含义——“程序会犯错,人也是”。
如果说异常捕获是可以捕获未知的程序错误,那程序测试就是针对人犯错的良药。
单元测试与JUnit
“单元”是指测试一小部分代码 。通常,每个类都有测试来检查它所有方法的行为。“系统”测试则是不同的,它检查的是整个程序是否满足要求。
JUnit是一个Java语言的单元测试框架。目前最新的版本是JUnit5。在Java8中,JUnit甚至支持Lambda表达式。
原著中的示例:
创建 CountedList 类,并继承 ArrayList:
// validating/CountedList.java
// Keeps track of how many of itself are created.
package validating;
import java.util.*;
public class CountedList extends ArrayList<String> {
private static int counter = 0;
private int id = counter++;
public CountedList() {
System.out.println("CountedList #" + id);
}
public int getId() { return id; }
}
创建一个测试类,来测试 CountedList 类。
// validating/tests/CountedListTest.java
// Simple use of JUnit to test CountedList.
package validating;
import java.util.*;
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;
public class CountedListTest {
private CountedList list;
@BeforeAll
static void beforeAllMsg() {
System.out.println(">>> Starting CountedListTest");
}
@AfterAll
static void afterAllMsg() {
System.out.println(">>> Finished CountedListTest");
}
@BeforeEach
public void initialize() {
list = new CountedList();
System.out.println("Set up for " + list.getId());
for(int i = 0; i < 3; i++)
list.add(Integer.toString(i));
}
@AfterEach
public void cleanup() {
System.out.println("Cleaning up " + list.getId());
}
@Test
public void insert() {
System.out.println("Running testInsert()");
assertEquals(list.size(), 3);
list.add(1, "Insert");
assertEquals(list.size(), 4);
assertEquals(list.get(1), "Insert");
}
@Test
public void replace() {
System.out.println("Running testReplace()");
assertEquals(list.size(), 3);
list.set(1, "Replace");
assertEquals(list.size(), 3);
assertEquals(list.get(1), "Replace");
}
// A helper method to simplify the code. As
// long as it's not annotated with @Test, it will
// not be automatically executed by JUnit.
private void compare(List<String> lst, String[] strs) {
assertArrayEquals(lst.toArray(new String[0]), strs);
}
@Test
public void order() {
System.out.println("Running testOrder()");
compare(list, new String[] { "0", "1", "2" });
}
@Test
public void remove() {
System.out.println("Running testRemove()");
assertEquals(list.size(), 3);
list.remove(1);
assertEquals(list.size(), 2);
compare(list, new String[] { "0", "2" });
}
@Test
public void addAll() {
System.out.println("Running testAddAll()");
list.addAll(Arrays.asList(new String[] {
"An", "African", "Swallow"}));
assertEquals(list.size(), 6);
compare(list, new String[] { "0", "1", "2",
"An", "African", "Swallow" });
}
}
结果:
>>> Starting CountedListTest
CountedList #0
Set up for 0
Running testRemove()
Cleaning up 0
CountedList #1
Set up for 1
Running testReplace()
Cleaning up 1
CountedList #2
Set up for 2
Running testAddAll()
Cleaning up 2
CountedList #3
Set up for 3
Running testInsert()
Cleaning up 3
CountedList #4
Set up for 4
Running testOrder()
Cleaning up 4
>>> Finished CountedListTest
- @BeforeAll 注解是在任何其他测试操作之前运行一次的方法。必须是静态的。
- @AfterAll 是所有其他测试操作之后只运行一次的方法。必须是静态的。
- @BeforeEach注解是通常用于创建和初始化公共对象的方法,并在每次测试前运行。
- @AfterEach,测试后执行清理(如果修改了需要恢复的静态文件,打开文件需要关闭,打开数据库或者网络连接,etc)。
- @Test使JUnit发现需要测试的方法,并将每个方法作为测试运行。
断言
验证一段程序是否满足某个条件,这就是——断言。
常用的断言方式:
assert boolean-expression: information-expression;
原著示例:
public class Assert2 {
public static void main(String[] args) {
assert false:
"Here's a message saying what happened";
}
}
结果:
___[ Error Output ]___
Exception in thread "main" java.lang.AssertionError:
Here's a message saying what happened
at Assert2.main(Assert2.java:8)
断言默认是关闭的,如果要启用则需要:
ClassLoader.getSystemClassLoader().setDefaultAssertionStatus(true);
断言的启用,还需要分包开启与类开启。
因为启用 Java 本地断言很麻烦,Guava 团队添加一个始终启用的用来替换断言的 Verify 类。他们建议静态导入 Verify 方法。
示例:
// validating/GuavaAssertions.java
// Assertions that are always enabled.
import com.google.common.base.*;
import static com.google.common.base.Verify.*;
public class GuavaAssertions {
public static void main(String[] args) {
verify(2 + 2 == 4);
try {
verify(1 + 2 == 4);
} catch(VerifyException e) {
System.out.println(e);
}
try {
verify(1 + 2 == 4, "Bad math");
} catch(VerifyException e) {
System.out.println(e.getMessage());
}
try {
verify(1 + 2 == 4, "Bad math: %s", "not 4");
} catch(VerifyException e) {
System.out.println(e.getMessage());
}
String s = "";
s = verifyNotNull(s);
s = null;
try {
verifyNotNull(s);
} catch(VerifyException e) {
System.out.println(e.getMessage());
}
try {
verifyNotNull(
s, "Shouldn't be null: %s", "arg s");
} catch(VerifyException e) {
System.out.println(e.getMessage());
}
}
}
结果:
com.google.common.base.VerifyException
Bad math
Bad math: not 4
expected a non-null reference
Shouldn't be null: arg s
提高代码质量
原著中本章后面的内容主要讲解使用日志及JDB调试来检验代码。但是在实际的项目开发中,我们往往有更强大的IDE辅助我们进行调试和代码追踪。所以这里不再详述。
在最后,作者提到了重构及结对编程对检验代码的重要性。重构可以使代码更加健壮,而结对编程可以review彼此的代码同样可以提高代码质量。这些都是很好的建议。
总结
本篇共四章内容,主要讲解了函数式编程、流式编程及异常处理和代码测试相关的知识。本篇内容主要以编程技巧的讲解为主。了解了本篇的内容可以使你在日常编程中的技巧得以提升。特别是Lambda及流式编程的技巧是Java8新加入的功能,可以多用一用就会发现它的强大。
作者:Mr.Jimmy
出处:https://www.cnblogs.com/JHelius
联系:yanyangzhihuo@foxmail.com
如有疑问欢迎讨论,转载请注明出处