Java 8 实战 P3 Effective Java 8 programming

时间:2023-03-09 16:20:26
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的OptionalOptional.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);
//还有很多方法