zoukankan      html  css  js  c++  java
  • 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);
    //还有很多方法
    
  • 相关阅读:
    南阳ACM 题目811:变态最大值 Java版
    南阳ACM 题目811:变态最大值 Java版
    南阳ACM 题目517:最小公倍数 Java版
    南阳ACM 题目517:最小公倍数 Java版
    Jquery学习总结(4)——高效Web开发的10个jQuery代码片段
    Mysql学习总结(34)——Mysql 彻底解决中文乱码的问题
    MyBatis学习总结(19)——Mybatis传多个参数(三种解决方案)
    Git学习总结(10)——git 常用命令汇总
    Spring MVC学习总结(7)——Spring MVC整合Ehcache缓存框架
    Mysql学习总结(33)——阿里云centos配置MySQL主从复制
  • 原文地址:https://www.cnblogs.com/code2one/p/9871595.html
Copyright © 2011-2022 走看看