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);
//还有很多方法
posted @ 2018-10-29 16:55  justcodeit  阅读(145)  评论(0编辑  收藏  举报