Java 8 (7) 重构、测试和调试

为改善可读性和灵活性重构代码

  看到这里我们已经可以使用lambda和stream API来使代码更简洁,用在新项目上。但大多数并不是全新的项目,而是对现有代码的重构,让它变的更简洁可读,更灵活。

改善代码的可读性

  别人理解这段代码的难易程度,改善可读性意味着你要确保你的代码能非常容易的被别人理解和维护。为了确保这点,有几个步骤可以尝试:

    1.使用Java 8,你可以减少冗长的代码,让代码更易于理解

    2.通过方法引用和Stream API,你的代码会变得更直观

    3.重构代码,用Lambda表达式取代匿名类

    4.用方法引用重构Lambda表达式

    5.用Stream API重构命令式的数据处理

 

从匿名类到Lambda表达式的转换

比如前面的Runnable例子:

        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("hh");
            }
        };

        //使用Lambda
        Runnable r1 = ()->System.out.println("hh");

但是,在某些情况下,匿名类转换为Lambda表达式可能是一个比较复杂的过程,因为在匿名类中和Lambda表达式中的this和super的含义是不同的,在匿名类中this是类的自身,Lambda表达式中,this代表的是包含类。还有在匿名类中可以屏蔽包含类的变量,而Lambda表达式不能:

        int a = 10;
        Runnable r2 = new Runnable() {
            @Override
            public void run() {
                int a = 2; //正常
                System.out.println(a);
            }
        };

        Runnable r3 = ()->{
            int a = 2; //编译报错
            System.out.println(a);
        };

现在大多数编辑器,比如InteliJ可以帮助我们自动检查是否可以转换为Lambda表达式。

 

从Lambda表达式到方法引用的转换

Lambda表达式非常适用于需要传递代码片段的场景。为了改善代码可读性,也请尽量使用方法引用,如:

        Map<String, List<Dish>> group1 = menu.stream().collect(groupingBy(c -> {
            if (c.getCalories() <= 400) {
                return "low";
            } else if (c.getCalories() > 400) {
                return "higt";
            } else {
                return "other";
            }
        }));

将Lambda表达式抽取到一个单独的方法中,将其作为参数传递给groupingBy方法,在Dish类中添加方法:

    public String getCaloricLevel(){
        if (Calories <= 400) {
            return "low";
        } else if (Calories > 400) {
            return "higt";
        } else {
            return "other";
        }
    }

然后通过方法引用调用这个方法:

Map<String, List<Dish>> group2 = menu.stream().collect(groupingBy(Dish::getCaloricLevel));

除此之外,我们还应该尽量考虑使用静态辅助方法,比如comparing、maxBy。这些方法设计时就考虑了会结合方法引用一起使用。

apples.sort((Apple a1,Apple a2) -> a1.getWeight().compareTo(a2.getWeight()));
  //使用方法引用
  apples.sort(Comparator.comparing(Apple::getWeight));

还有很多通用的归约操作,比如sum、max,都有内建的辅助方法可以和方法引用结合使用。

int totalCalories = menu.stream().map(Dish::getCalories).reduce(0,(c1,c2)->c1+c2);
  //归约
  int totalCalories2 = menu.stream().mapToInt(Dish::getCalories).sum();
  int totalCalories3 = menu.stream().collect(summingInt(Dish::getCalories));

 

从命令式的数据处理切换到Stream

  建议将所有迭代器这种数据处理模式处理集合的代码都转换为Stream API的方式,因为Stream API有短路和延迟载入的方式,并且流可以很清楚的表达意图。

比如下面的命令式代码使用了两种模式:筛选和抽取,这两种模式混合在了一起,这样的代码结构迫使程序员必须彻底搞清楚程序的每个细节才能理解代码的功能。此外,实现并行运行的程序所面对的困难也多的多:

List<String> dishNames = new ArrayList<>();
        for(Dish dish : menu){
            if(dish.getCalories() > 300){
                dishNames.add(dish.getName());
            }
        }

        //Stream API
        List<String> dishNames2 = menu.stream().filter(c->c.getCalories()>300).map(Dish::getName).collect(toList());

 

增加代码的灵活性

  我们曾经介绍过,Lambda表达式有利于行为参数化。你可以使用不同的Lambda表示不同的行为,并将它们作为参数传递给函数去处理执行。

1.采用函数接口

  没有函数接口,就无法使用Lambda表达式。因此,需要在代码中引入函数接口。这里介绍两种通用的模式,可以按照这两种模式重构代码,它们分别是:有条件的延迟执行和环绕执行。

2.有条件的延迟执行

        if (logger.isLoggable(Level.FINER)){
            logger.finer("Problem: " + generateDiagnostic());
        }

比如Logger类中的finer方法,每次使用时都要去判断一下日志级别。比较好的做法是使用log方法,该方法在输出日志消息之前,会在内部检查日志对象是否已经为恰当的等级:

logger.log(Level.FINER , "Problem:" + generateDiagnostic());

这种代码的好处是,你不需要在代码中插入那些条件判断,但这段代码还是要每次都要判断。

Lambda可以施展拳脚了,你需要做的仅仅是延迟消息构造,如此一来,日志就只会在某些特定的情况下才开启,log有一个重载版本,接受一个Supplier作为参数。

logger.log(Level.FINER , () -> "Problem:" + generateDiagnostic());

此时,如果日志的级别设置恰当,log则会在内部执行作为参数传递进来的Lambda表达式。

3.环绕执行

环绕执行就是拥有很多同样的准备和清理代码,这时,完全可以讲这部分代码用Lambda实现。这种方式的好处是可以重用准备和清理阶段的逻辑,减少重复冗余的代码。

System.out.println(processFile((BufferedReader br) -> br.readLine() + br.readLine()));
    
public static String processFile(BufferedReaderProcessor p) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader("/Users/baidawei/Desktop/test.txt"))) {
            //处理BufferedReader对象
            return p.process(br);
        }
    }

@FunctionalInterface
public interface BufferedReaderProcessor {
    String process(BufferedReader b) throws IOException;

}

这是第二章的例子,凭借函数式接口BufferedReaderProcessor达成的,通过这个接口可以传递各种Lambda表达式对BufferedReader对象进行处理。

 

使用Lambda重构面向对象的设计模式

  新的语言特性常常让现存的变成模式或设计黯然失色。比如,java 5中引入了 for-each循环,由于它的稳健性和简介性,已经替代了很多迭代器的使用。Java 7中推出的菱形<>操作符,让大家无需显示使用泛型。

  对设计经验的归纳总结称为设计模式。Lambda表达式为解决传统设计模式的问题提供了新的解决方案。主要讨论五个设计模式:

策略模式

  策略模式代表了解决一类算法的通用解决方案,你可以在运行时选择使用哪种方案。比如之前的筛选苹果例子:

public interface ApplePredicate {
    boolean test(Apple apple);
}
public class AppleHeavyWeightPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return apple.getWeight() > 32;
    }
}
public class AppleGreenColorPredicate implements ApplePredicate {
    @Override
    public boolean test(Apple apple) {
        return "green".equals(apple.getColor());
    }
}

        List<Apple> greenApple = filterApples(apples,new AppleGreenColorPredicate());
        List<Apple> bigApple = filterApples(apples,new AppleHeavyWeightPredicate());


    //根据抽象条件筛选
    public static List<Apple> filterApples(List<Apple> apples,ApplePredicate p){
        List<Apple> result = new ArrayList<>();
        for(Apple apple : apples){
            if(p.test(apple)){
                result.add(apple);
            }
        }
        return result;
    }

将这种啰嗦的模板代码使用Lambda表达式来替换:

List<Apple> greenApple = filter(apples, (Apple apple) -> "green".equals(apple.getColor()));

使用Lambda表达式避免了采用策略设计模式时的模板代码。

 

模板方法

  如果你需要采用某个算法的框架,同时又希望有一定的灵活度,能对它的某些部分进行改进,那么采用模板方法设计模式是比较通用的方案。换句话说,你希望使用这个算法,但是需要对其中的某些行为进行改进,才能达到希望的效果。

  例如,你需要编写一个简单的在线银行应用。实现功能如下:输入账号,不同的银行可以给你不同的奖励,可以定义如下抽象类:

public abstract class OnlineBanking {
    public void processCustomer(int id){
        Customer c = Database.getCustomerById(id);
        makeCustomerHappy(c);
    }
    abstract  void makeCustomerHappy(Customer c);
}

  processCustomer方法通过 客户id 为客户提供服务,不同的分行可以通过继承OnlineBanking类,对方法进行实现。

  使用Lambda表达式同样可以解决这些问题,并且无需继承OnlineBanking类。但是要改造一下processCustomer方法,增加一个参数。Consumer<Customer>类型的参数,消费它。

public class OnlineBanking {
    public void processCustomer(int id,Consumer<Customer> makeCustomerHappy){
        Customer c = Database.getCustomerById(id);
        makeCustomerHappy.accept(c);
    }
}

现在就可以传递lambda表达式了。

new OnlineBanking().processCustomer(1333,(Customer c) -> System.out.println(c.getName()));

 

观察者模式

  如果一个对象(称为主题)需要自动的通知其他多个对象(称为观察者),就是观察者模式。比如通知系统,cctv 1 2 3 4 5 都订阅了新闻,如果新闻中存在感兴趣的关键词时就得到特别通知。

定义一个观察者接口,它将不同的观察者聚合在一起。它仅有一个notify的方法,一旦接收到一条新的新闻,该方法就会被调用:

public interface Observer {
    void notify(String news);
}

这里举例cctv3 音乐频道和 cctv5 体育频道 

public class CCTV5 implements Observer {
    @Override
    public void notify(String news) {
        if(news != null && news.contains("体育")){
            System.out.println("cctv5 播放体育:" + news);
        }
    }
}
public class CCTV3 implements Observer {
    @Override
    public void notify(String news) {
        if(news != null && news.contains("音乐")){
            System.out.println("cctv3 播放音乐:" + news);
        }
    }
}

最后 主题接口

public interface Subject {
    //注册观察者
    void registerObserver(Observer o);
    //通知它的观察者一个新闻到来
    void notifyObservers(String news);
}

实现Feed类,注册一个观察者列表,一条新闻到达时,他就会进行通知。

        Feed feed = new Feed();
        feed.registerObserver(new CCTV3());
        feed.registerObserver(new CCTV5());
        feed.notifyObservers("一个音乐节目,周杰伦的龙卷风!");

CCTV3 会关注这条新闻,这里也可以使用Lambda表达式,可以不用继承Observer类,写那些模板代码。如:

        feed.registerObserver((String news)->{
            if(news != null && news.contains("音乐")){
                System.out.println("cctv3 播放着音乐:" + news);
            }
        });

观察者中的代码有可能逻辑十分复杂,还有可能定义了多个方法,这时最好依旧使用类的方式。

 

责任链模式

  一个处理对象可能需要在完成一些工作后,将结果传递给另一个对象,这个对象接着做一些工作,再转交给下一个对象,以此类推。例如:

public abstract class ProcessingObjet<T> {
    protected ProcessingObjet<T> successor;

    public void setSuccessor(ProcessingObjet<T> successor) {
        this.successor = successor;
    }

    public T handle(T input) {
        T r = handleWork(input);
        if (successor != null) {
            return successor.handle(r);
        }
        return r;
    }

    abstract protected T handleWork(T input);
}

可以通过不同的类继承ProcessingObject类,提供handleWork方法来进行创建。

public class HandlerTextProcssing extends ProcessingObjet<String> {
    @Override
    protected String handleWork(String input) {
        return "哈喽大家好啊" + input;
    }
}
public class SpellCheckerProcessing extends ProcessingObjet<String> {
    @Override
    protected String handleWork(String input) {
        return input.replace("哈喽","hello,");
    }
}

 现在就可以把这两个对象结合起来,构造一个操作序列!

        ProcessingObjet<String> p1 = new HandlerTextProcssing();
        ProcessingObjet<String> p2 = new SpellCheckerProcessing();
        p1.setSuccessor(p2);
        String result = p1.handle("你说啥?");
        System.out.println(result);
//hello,大家好啊你说啥?

这个模式看起来像是在连接函数,我们可以使用Lambda来链接这些函数:

        UnaryOperator<String> headerProcessing = (String text) -> "哈喽大家好啊" + text;
        UnaryOperator<String> spellCheckerProcessing = (String text) -> text.replace("哈喽","hello,");
        Function<String,String> pipeline = headerProcessing.andThen(spellCheckerProcessing);
        String result1 = pipeline.apply("你说啥?");

 

工厂模式

  使用工厂模式,你无需像客户暴露实例化的逻辑就能完成对象的创建。比如,需要一种方式创建不同的金融方式:贷款、期权、股票:

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: return null;
        }
    }
}

这里的Loan、Stock、Bond都是Product的子类。 在前台创建时可以这样:

Product p = ProductFactory.createProduct("loan");

使用Lambda表达式,我们可以使用::new的方式引用构造函数:

Supplier<Product> p = Loan::new;
Loan loan = p.get();

通过这种方式,你可以重构之前的代码,创建一个Map,将产品名映射到对应的构造函数:

        final static Map<String,Supplier<Product>> map = new HashMap<>();
        static{
            map.put("loan",Loan::new);
            map.put("stock",Stock::new);
            map.put("bond",Bond::new);
        }

这样就可以使用工厂设计模式那样,利用这个Map来实例化不同的产品。

public class ProductFactory{
    public static Product createProduct(String name){
        Supplier<Product> p =map.get(name);
        if(p!=null) return p.get();
        return null;
    }
}

如果createProduct需要接受多个参数传递给构造方法,这种方法扩展性不是很好。还需要自己创建函数接口来支持更多的参数。

 

测试Lambda表达式

  好的软件工程实践一定少不了单元测试。编写测试用例,通过这些测试用例确保代码中的每个组成部分都实现预期的结果。比如简单的Point类:

public class Point {
    int x;
    int y;

    public Point() {

    }

    private Point(int x, int y) {
        this.x = x;
        this.y = y;
    }


    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public Point moveRightBy(int x) {
        return new Point(this.x + x, this.y);
    }

    @Test
    public void testMoveRightBy() {
        Point p1 = new Point(5, 5);
        Point p2 = p1.moveRightBy(10);

        assertEquals(15, p2.getX());
        assertEquals(5, p2.getY());
    }
}

测试 testMoveRightBy 方法比较容易,但是Lambda并没有函数名,因此要对Lambda函数进行测试可以借助某个字段访问Lambda函数。比如在Point类中添加了静态字段compareByXAndThenY,通过该字段使用方法引用可以访问Comparator对象:

public class Point {
    public final  static Comparator<Point> compareByXAndThenY = Comparator.comparing(Point::getX).thenComparing(Point::getY);
    ...

Lambda表达式会生成函数式接口的一个实例,因此可以测试该实例的行为。 对Comparator对象类型实例compareByXAndThenY的compare方法进行调用,验证他们是否正确:

    @Test
    public void testComparingTwoPoints(){
        Point p1 = new Point(10,15);
        Point p2 = new Point(10,20);
        int result = Point.compareByXAndThenY.compare(p1,p2);
        assertEquals(-1,result);
    }

 

但是,Lambda的初衷是将一部分逻辑封装起来给另一个方法使用。不应该将Lambda表达式声明为public,他们仅是具体的实现细节。我们需要对使用Lambda表达式的方法进行测试:

    public static List<Point> moveAllPointsRightBy(List<Point> points, int x){
        return points.stream()
                .map(p->new Point(p.getX() + x , p.getY()))
                .collect(toList());
    }
    @Test
    public void testMoveAllPointsRightBy(){
        List<Point> points = Arrays.asList(new Point(5,5),new Point(10,5));
        List<Point> expectedPoints = Arrays.asList(new Point(15,5),new Point(20,5));
        List<Point> newPoints = Point.moveAllPointsRightBy(points,10);
        assertEquals(expectedPoints,newPoints);
    }

运行后发生异常:

java.lang.AssertionError: expected: java.util.Arrays$ArrayList<[Point{x=15, y=5}, Point{x=20, y=5}]> but was: java.util.ArrayList<[Point{x=15, y=5}, Point{x=20, y=5}]>
Expected :java.util.Arrays$ArrayList<[Point{x=15, y=5}, Point{x=20, y=5}]> 
Actual   :java.util.ArrayList<[Point{x=15, y=5}, Point{x=20, y=5}]>

这是因为Arrays.asList返回的是一个不可变的集合。没有add方法的。

 

复杂的Lambda表达式

  如果碰到大量的业务逻辑,无法在测试程序中引用lambda表达式时,一种策略是将Lambda表达式转换为方法引用,然后用常规的方式对新的方法进行测试。

 

高阶函数的测试

  接受函数作为参数的方法或者返回一个函数的方法更难测试。如果一个方法接受Lambda表达式作为参数,你可以采用的方案是使用不同的Lambda表达式对它进行测试。

public static <T> List<T> filter(List<T> list, Predicate<T> p) {
        List<T> result = new ArrayList<>();
        for (T e : list) {
            if (p.test(e)) {
                result.add(e);
            }
        }
        return result;
    }

    @Test
    public void testFilter() {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
        List<Integer> even = filter(numbers, i -> i % 2 == 0);
        List<Integer> smallerThanThree = filter(numbers, i -> i < 3);

        assertEquals(Arrays.asList(2,4),even);
        assertEquals(Arrays.asList(1,2),smallerThanThree);
    }

 

调试

1.查看栈跟踪

  当程序突然停止时,比如抛出一个异常。你会得到它的栈跟踪,通过一个又一个栈帧,你可以了解程序失败时的概略信息。换句话说,通过这些你可以得到程序失败时的方法调用列表。

public class Debugging {
    public static void main(String[] args) {
        List<Point> points = Arrays.asList(new Point(12, 2), null);
        points.stream().map(p -> p.getX()).forEach(System.out::println); }
}

这段代码 故意传入了一个null,查看下面的栈跟踪:

Exception in thread "main" java.lang.NullPointerException
    at Java8_7.Debugging.lambda$main$0(Debugging.java:9)
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
    at java.util.Spliterators$ArraySpliterator.forEachRemaining(Spliterators.java:948)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:481)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
    at java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151)
    at java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174)
    at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
    at java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:418)
    at Java8_7.Debugging.main(Debugging.java:9)

首先异常说了是 空引用,然后第二行是告诉你 项目名 类名 是lambda表达式 $ main方法 中的 debugging.java 9行。 因为lambda没有名字 ,所以编译器为他起了一个名字。 lambda$main$0

 

2.使用日志调试

  加入对操作的流水线进行调试,可以使用forEach将流操作的结果日志输出。

        List<Integer> nums = Arrays.asList(1,2,3,3,4,4,4,4,3,3,2,2);
        nums.stream()
                .map(c->c+17)
                .filter(c->c%2==0)
                .limit(3)
                .forEach(System.out::println); //18 20 20

但是,如果用这种方式就会终止流。peek方法可以不终止流又输出当前结果。

        nums.stream()
                .peek(c->System.out.println("调用stream后:" + c))
                .map(c->c+17)
                .peek(c->System.out.println("调用map后" + c))
                .filter(c->c%2==0)
                .peek(c->System.out.println("调用filter后" + c))
                .limit(3)
                .forEach(System.out::println); //18 20 20
调用stream后:1
调用map后18
调用filter后18
18
调用stream后:2
调用map后19
调用stream后:3
调用map后20
调用filter后20
20
调用stream后:3
调用map后20
调用filter后20
20

 

小结:

  1.Lambda表达式能提升代码的可读性和灵活性。

  2.尽量使用Lambda替代匿名类,但是要注意this、局部变量问题。

  3.尽量使用方法引用来替代Lambda表达式。

  4.尽量使用Stream API替代迭代式集合处理。

  5.Lambda表达式有助于避免使用面向对象设计模式时容易出现的模板代码。

  6.Lambda表达式也可以单元测试

  7.尽量将复杂的Lambda表达式抽象到普通方法中。

  8.Lambda调试 可以通过日志进行分析

  9.流提供的peek方法可以分析Stream每一步。

 

posted @ 2018-08-03 15:23  海盗船长  阅读(660)  评论(0编辑  收藏  举报