Java 8 实战 P3 Effective Java 8 programming
Chapter 8. Refactoring, testing, and debugging
8.1 为改善可读性和灵活性重构代码
1.从匿名类到 Lambda 表达式的转换
注意事项:在匿名类中, this代表的是类自身,但是在Lambda中,它代表的是包含类
匿名类可以屏蔽包含类的变量,而Lambda表达式不
能(它们会导致编译错误)
//下面会出错
int a = 10;
Runnable r1 = () -> {
int a = 2;
System.out.println(a);
};
当以某预设接口相同的签名声明函数接口时,Lambda要加上标注区分
//例如下面接口用了相同的函数描述符<T> -> void
public static void doSomething(Runnable r){ r.run(); }
public static void doSomething(Task a){ a.execute(); }
//Lambda要在前面表明是哪个
doSomething((Task)() -> System.out.println("Danger danger!!"));
2.从 Lambda 表达式到方法引用的转换
将之前的dishesByCaloricLevel方法进行修改。
把groupingBy里面的内容改为Dish里面的一个方法getCaloricLevel,这样就可以在groupingBy里面用方法引用了()Dish::getCaloricLevel
尽量考虑使用静态辅助方法,比如comparing、 maxBy。如inventory.sort(comparing(Apple::getWeight));
用内置的集合类而非map+reduce,如int totalCalories = menu.stream().collect(summingInt(Dish::getCalories));
3.从命令式的数据处理切换到 Stream
所有使用迭代器这种数据处理模式处理集合的代码都转换成Stream API的方式。因为Stream清晰,而且可以进行优化
但这是一个困难的任务,需要考虑控制流语句(一
些工具可以帮助我们完成)
//筛选和抽取的混合,不好并行
List<String> dishNames = new ArrayList<>();
for(Dish dish: menu){
if(dish.getCalories() > 300){
dishNames.add(dish.getName());
}
}
menu.parallelStream()
.filter(d -> d.getCalories() > 300)
.map(Dish::getName)
.collect(toList());
4.灵活性
有条件的延迟执行
//问题代码
if (logger.isLoggable(Log.FINER)){
logger.finer("Problem: " + generateDiagnostic());
}
//日志器的状态(它支持哪些日志等级)通过isLoggable方法暴露给了客户端代码
//每次输出一条日志之前都去查询日志器对象的状态
//改进
//Java 8替代版本的log方法的函数签名如下
public void log(Level level, Supplier<String> msgSupplier)
logger.log(Level.FINER, () -> "Problem: " + generateDiagnostic());//在检查完该对象的状态之后才调用原来的方法
如果你发现你需要频繁地从客户端代码去查询一个对象的状态(比如前文例子中的日志器的状态),只是为了传递参数、调用该对象的一个方法(比如输出一条日志),那么可以考虑实现一个新的方法,以Lambda或者方法表达式作为参数,新方法在检查完该对象的状态之后才调用原来的方法
环绕执行
同样的准备和清理阶段
上面有例子,搜索环绕执行
8.2 使用Lambda重构面向对象的设计模式
1.策略模式
算法接口, 算法实现,客户
思路:
策略的函数签名,判断是否有预设的接口
创建/修改类(含有实现某接口的构造器,调用该接口方法的方法)
//下面,Validator类接受实现了ValidationStrategy接口的对象为参数
public class Validator{
private final ValidationStrategy strategy;
public Validator(ValidationStrategy v){
this.strategy = v;
}
public boolean validate(String s){
return strategy.execute(s); }//execute为ValidationStrategy接口的方法,该接口签名为String -> boolean
//创建具有特定功能(通过符合签名的Lambda传入)的类
Validator v3 = new Validator((String s) -> s.matches("\\d+"));
//使用该类
v3.validate("aaaa");
2.模版方法
如果你需要采用某个算法的框架,同时又希望有一定的灵活度,能对它的某些部分进行改进。
例如上面的例子,希望只用一个Validator,且保留validate方法。为了保持策略的多样,需要对validate方法进行改进,这可以给方法引入第二个参数(函数接口),从而提高方法的灵活性。书中有一个类似的例子,构建一个在线银行应用,在保持只有一个银行类的情况下,让相同的方法给客户不同的反馈。如下:
public void processCustomer(int id, Consumer<Customer> makeCustomerHappy){
Customer c = Database.getCustomerWithId(id);
makeCustomerHappy.accept(c);
}
3.观察者模式(简单情况下可用Lambda)
某些事件发生时(如状态转变),一个对象(主题)需要自动通知多个对象(观察者)
简单来说,一个主题类有观察者名单,有一方法(包含通知参数)能遍历地调用观察者的方法(接受通知参数,并作相应行为)
例子:
//观察者实现的接口
interface Observer {
void notify(String tweet);
}
//其中一个观察者类
class NYTimes implements Observer{
public void notify(String tweet) {
if(tweet != null && tweet.contains("money")){
System.out.println("Breaking news in NY! " + tweet);
}
}
}
//主题接口
interface Subject{
void registerObserver(Observer o);
void notifyObservers(String tweet);
}
//主体类
class Feed implements Subject{
private final List<Observer> observers = new ArrayList<>();
public void registerObserver(Observer o) {
this.observers.add(o);
}
public void notifyObservers(String tweet) {
observers.forEach(o -> o.notify(tweet));
}
}
//上面的简单例子能用下面的Lambda实现,只需一个主题类即可
f.registerObserver((String tweet) -> {
if(tweet != null && tweet.contains("money")){
System.out.println("Breaking news in NY! " + tweet);
}
});
4.责任链模式
创建处理对象序列(比如操作序列)的通用方案
通常做法是构建一个代表处理对象的抽象类来实现。如下
//抽象类有一个同类的successor的protected变量,设置successor的方法,处理任务的抽象方法,整合任务处理以及传递的handle方法
public abstract class ProcessingObject<T> {
protected ProcessingObject<T> successor;
public void setSuccessor(ProcessingObject<T> successor){
this.successor = successor;
}
abstract protected T handleWork(T input);
public T handle(T input){
T r = handleWork(input);
if(successor != null){
return successor.handle(r);
}
return r;
}
}
//实现阶段,创建继承上面抽象类的类,并实现handleWork方法。这样,在实例化继承类后并通过setSuccessor构成处理链。当第一个实例调用handle就能实现链式处理了。
//运用Lambda方式,构建实现UnaryOperator接口的不同处理对象,然后通过Function的andThen把处理对象连接起来,构成pipeline。
UnaryOperator<String> headerProcessing =
(String text) -> "From Raoul, Mario and Alan: " + text;
UnaryOperator<String> spellCheckerProcessing =
(String text) -> text.replaceAll("labda", "lambda");
Function<String, String> pipeline =
headerProcessing.andThen(spellCheckerProcessing);
//直接调用pipeline
String result = pipeline.apply("Aren't labdas really sexy?!!")
5.工厂模式(不适合Lambda)
无需向客户暴露实例化的逻辑就能完成对象的创建
public class ProductFactory {
public static Product createProduct(String name){
switch(name){
case "loan": return new Loan();
case "stock": return new Stock();
case "bond": return new Bond();
default: throw new RuntimeException("No such product " + name);
}
}
}
8.3 测试Lambda表达式
一般的测试例子
@Test
public void testMoveRightBy() throws Exception {
Point p1 = new Point(5, 5);
Point p2 = p1.moveRightBy(10);
assertEquals(15, p2.getX());
assertEquals(5, p2.getY());
}
1.对于Lambda,由于没有名字,而需要借用某个字段访问Lambda。如point类中增加了如下字段
public final static Comparator<Point> compareByXAndThenY =
comparing(Point::getX).thenComparing(Point::getY);
//测试时
@Test
public void testComparingTwoPoints() throws Exception {
Point p1 = new Point(10, 15);
Point p2 = new Point(10, 20);
int result = Point.compareByXAndThenY.compare(p1 , p2);
assertEquals(-1, result);
}
2.如果Lambda是包含在一个方法里面,就直接测试该方法的最终结果即可。
3.对于复杂的Lambda,将其分到不同的方法引用(这时你往往需要声明一个新的常规方法)。之后,你可以用常规的方式对新的方法进行测试。
可参照笔记的8.1.2或书的8.1.3例子
4.高阶函数测试
直接根据接口签名写不同的Lambda测试
8.4 调试
peek对stream调试
Chapter 9. Default methods
辅助类的意义已经不大?
兼容性:二进制、源代码和函数行为
public interface Sized {
int size();
default boolean isEmpty() {
return size() == 0;
}
}
1.设计接口时,保持接口minimal and orthogonal
2.函数签名冲突:类方法优先,底层接口优先,显式覆盖
- 如果父类的方法是“继承”“默认”的(非重写),则不算
- 需要显式覆盖的情况:
B.super.hello();
B为接口名,hello为重名方法 - 菱形问题中,A有默认方法,B,C接口继承A(没重写),D实现B,C,此时D回调用A的方法。如果B,C其中一个
Chapter 10. Using Optional as a better alternative to null
10.1 null与Optional入门
1.null带来的问题
NullPointerException
代码膨胀(null检查)
在Java类型系统的漏洞(null不属于任何类型)
2.Optional类
设置为Optional<Object>
的变量,表面它的null值在实际业务中是可能的。而非Optional类的null则在现实中是不正常的。
下面的例子中,人可能没车,车也可能没有保险,但是没有公司的保险是不可能的。
public class Person {
private Optional<Car> car;
public Optional<Car> getCar() { return car; }
}
public class Car {
private Optional<Insurance> insurance;
public Optional<Insurance> getInsurance() { return insurance; }
}
public class Insurance {
private String name;
public String getName() { return name; }
}
上面Optional类的存在让我们不需要在遇到NullPointerException时(来自Insurance的name缺失)单纯地添加null检查。因为这个异常的出现代表数据出了问题(保险不可能没有相应的公司),需要检查数据。
所以,一直正确使用Optional能够让我们在遇到异常时,知道问题是语法上还是数据上。
10.2 应用Optional
1.创建
- 声明空的:
Optional<Car> optCar = Optional.empty();
- 从现有的构建:
Optional.of(car)
如果car为null会直接抛异常,而非等到访问car时才说。 - 接受null的Optional
Optional.ofNullable(car)
2.使用map从Optional对象中提取和转换值
Optional对象中的map是只针对一个对象的(与Stream对比)
map操作保持Optional的封装,所以,如果某方法的返回值是Optional<Object>
,则一般会用下面的flatMap
3.使用flatMap来链接Optional
//下面代码,第一个map返回的是Optional<Optional<Car>>,这样第二个map中的变量就是Optional<Car>而非Car,故不能调用getCar
optPerson.map(Person::getCar)
.map(Car::getInsurance)
public String getCarInsuranceName(Optional<Person> person) {
return person.filter(p -> p.getAge() >= minAge)//后面API处介绍
.flatMap(Person::getCar)
.flatMap(Car::getInsurance)
.map(Insurance::getName)
.orElse("Unknown");
}
Optional的序列化,通过方法返回Optional变量
public class Person {
private Car car;
public Optional<Car> getCarAsOptional() {
return Optional.ofNullable(car);
}
}
4.多个Optional的组合
下面函数是一个nullSafe版的findCheapestInsurance,它接受Optional<Person>
和Optional<Car>
并返回一个合适的Optional<Insurance>
public Optional<Insurance> nullSafeFindCheapestInsurance(
Optional<Person> person, Optional<Car> car) {
return person.flatMap(p -> car.map(c -> findCheapestInsurance(p, c))); //很好地处理各种null的情况
}
5.API
.get
只有确保有值采用
.orElse(T other)
orElseGet(Supplier<? extends T> other)
如果创建默认值consuming时用
orElseThrow(Supplier<? extends X> exceptionSupplier)
ifPresent(Consumer<? super T>)
isPresent
filter
符合条件pass,否则返回空Optional
10.3 Optional的实战示例
//希望得到一个Optional封装的值
Optional<Object> value = Optional.ofNullable(map.get("key"));//即使可能为null也要取得的值
//用Optional.empty()代替异常。建议将多个类似下面的代码封装到一个工具类中
public static Optional<Integer> s2i(String s) {
try {
return of(Integer.parseInt(s));
} catch (NumberFormatException e) {
return empty();
}
}
//暂时避开基本类Optional,因为他们没有map、filter方法。
//下面针对Properties进行转换,如果K(name)对应的V是正整数,则返回该V的int,其他情况返回0
public int readDuration(Properties props, String name) {
return Optional.ofNullable(props.getProperty(name))//提取V,允许null。如果null,则只有orElse需要执行
.flatMap(OptionalUtility::stringToInt)//上例中提到的方法
.filter(i -> i > 0)//是否为正数
.orElse(0);
}
Chapter 11. CompletableFuture: composable asynchronous programming
11.1 Future接口
Future接口提供了方法来检测异步计算是否已经结束(使用isDone方法),等待异步操作结束,以及获取计算的结果。CompletableFuture在此基础上增加了不同功能。
同步API与异步API:异步是需要新开线程的
11.2 实现异步API
1.getPrice
下面代码是异步获取价格的方法。首先新建一个CompletableFuture,然后是一个新线程,这个线程的任务是calculatePrice(该方法添加1秒延迟来模拟网络延迟)。这个方法的返回变量futurePrice会马上得出,但是里面的结果要等到另外一个线程计算后才能取得,即完成.complete。
public Future<Double> getPriceAsync(String product) {
CompletableFuture<Double> futurePrice = new CompletableFuture<>();
new Thread( () -> {
try {
double price = calculatePrice(product);
futurePrice.complete(price);//计算正常的话设置结果,此时原线程的futurePrice就可以get到结果了。但一般不用普通get方法,重制get能设置等待时间
} catch (Exception ex) {
futurePrice.completeExceptionally(ex);//将异常返回给原线程
}
}).start();
return futurePrice;
}
return CompletableFuture.supplyAsync(() -> calculatePrice(product)); }
supplyAsync的函数描述符() -> CompletableFuture<T>
2.findPrices(查询某product在一列shops的价格)
这里的计算是一条线的,collect的执行需要所有getPrice执行完才可以执行,所以没有必要开异步。如果collect在getPrice执行完之前还有其他事情可以做,此时才用异步
//shops是List<Shop>
//通过并行实现
public List<String> findPricesParallel(String product) {
return shops.parallelStream()
.map(shop -> shop.getName() + " price is " + shop.getPrice(product))
.collect(Collectors.toList());
}
//异步实现
//一个stream只能同步顺序执行,但取值不需要等所有值都得出才取,所以join分在另一个stream里
public List<String> findPrices(String product) {
List<CompletableFuture<String>> priceFutures =
shops.stream()
.map(shop -> CompletableFuture.supplyAsync(() -> shop.getName() + " price is "
+ shop.getPrice(product), executor))//返回CompletableFuture<String>。这里使用了异步,可提供自定义executor
.collect(Collectors.toList());
List<String> prices = priceFutures.stream()
.map(CompletableFuture::join)//join相当于get,但不会抛出检测到的异常,不需要try/catch
.collect(Collectors.toList());
return prices;
}
假设默认有4个线程(Runtime. getRuntime().availableProcessors()
可查看),那么在4个shops的情况下,并行需要1s多点的时间(getPrice设置了1s的延迟),异步需要2s多点。如果5个shops,并行还是要2s。其实可以大致理解为异步有一个主线程,三个支线程。
然而异步的优势在于可配置Executor
定制执行器的建议
Nthreads = NCPU * UCPU * (1 + W/C)
N为数量,U为使用率,W/C为等待时间和计算时间比例
上面例子在4核,CPU100%使用率,每次等待时间1s占据绝大部分运行时间的情况下,建议设置线程池容量为400。当然,线程不应该多于shops,而且要考虑机器的负荷来调整线程数。下面是设置执行器的代码。设置好后,只要shop数没超过阈值,程序都能1s内完成。
private final Executor executor = Executors.newFixedThreadPool(Math.min(shops.size(), 100), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);//设置为保护线程,程序退出时线程会被回收
return t;
}
});
并行与异步的选择
并行:计算密集型的操作,并且没有I/O(就没有必要创建比处理器核数更多的线程)
异步:涉及等待I/O的操作(包括网络连接等待)
11.3 对多个异步任务进行流水线操作
1.连续异步(第一个CompletableFuture需要第二个CompletableFuture的结果)
此处getPrice的返回格式为Name:price:DiscountCode
Quote::parse
对接受的String进行split,并返回一个new Quote(shopName, price, discountCode)
第二个map没有涉及I/O和远程服务等,不会有太多延迟,所以可以采用同步。
第三个map涉及异步,因为计算Discount需要时间(设置的1s)。此时可以用thenCompose方法,它允许你对两个异步操作进行流水线,第一个操作完成时,将其结果作为参数传递给第二个操作
public List<String> findPrices(String product) {
List<CompletableFuture<String>> priceFutures =
shops.stream()
.map(shop -> CompletableFuture.supplyAsync(
() -> shop.getPrice(product), executor))
.map(future -> future.thenApply(Quote::parse))
.map(future -> future.thenCompose(quote ->
CompletableFuture.supplyAsync(
() -> Discount.applyDiscount(quote), executor)))
.collect(toList());
return priceFutures.stream()
.map(CompletableFuture::join)
.collect(toList());
}
由于Discount.applyDiscount消耗1s时间,所以总时间比之前多了1s
2.整合异步(两个不相关的CompletableFuture整合起来)
下面代码的combine操作只是相乘,不会耗费太多时间,所以不需要调用thenCombineAsync进行进一步的异步
Future<Double> futurePriceInUSD =
CompletableFuture.supplyAsync(() -> shop.getPrice(product))
.thenCombine(//这里和上一个同样是在新1线程
CompletableFuture.supplyAsync(//这个是新2线程
() -> exchangeService.getRate(Money.EUR, Money.USD)),
(price, rate) -> price * rate
);
11.4 响应CompletableFuture的completion事件
.thenAccept接收CompletableFuture<T>
,返回CompletableFuture<Void>
下面的findPricesStream是连续异步中去掉的三个map外的代码
CompletableFuture[] futures = findPricesStream("myPhone")
.map(f -> f.thenAccept(System.out::println))
.toArray(size -> new CompletableFuture[size]);
CompletableFuture.allOf(futures).join();//allOf接收CompletableFuture数组并返回所有CompletableFuture。也有anyOf
Chapter 12. New Date and Time API(未完)
12.1 LocalDate、LocalTime、Instant、Duration以及Period
下面都没有时区之分,不能修改
时点
//LocalDate
LocalDate date = LocalDate.of(2014, 3, 18);
int year = date.getYear();
Month month = date.getMonth();
int day = date.getDayOfMonth();
DayOfWeek dow = date.getDayOfWeek();
int len = date.lengthOfMonth();
boolean leap = date.isLeapYear();
LocalDate today = LocalDate.now();
int year = date.get(ChronoField.YEAR);//get接收一个ChronoField枚举,也是获得当前时间
date.atTime(time);
date.atTime(13, 45, 20);
//LocalTime
LocalTime time = LocalTime.of(13, 45, 20);//可以只设置时分
//同样是getXXX
time.atDate(date);
//通用方法
.parse()
//LocalDateTime
LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45, 20); LocalDateTime.of(date, time);
toLocalDate();
toLocalTime();
//机器时间
Instant.ofEpochSecond(3);
Instant.ofEpochSecond(4, -1_000_000_000);//4秒之前的100万纳秒(1秒)
Instant.now()
//Instant的设计初衷是为了便于机器使用。它包含的是由秒及纳秒所构成的数字。所以,它 无法处理那些我们非常容易理解的时间单位,不要与上面的方法混用。
时段
//Duration
Duration d1 = Duration.between(time1, time2);//也可以是Instant,LocalDateTimes,但LocalDate不行
//Period
Period tenDays = Period.between(LocalDate1, LocalDate2)
//通用方法
Duration.ofMinutes(3);
Duration.of(3, ChronoUnit.MINUTES);
Period.ofDays(10);
twoYearsSixMonthsOneDay = Period.of(2, 6, 1);
//还有很多方法