Java 8 基础知识

来源:互联网 发布:海信电视mac是什么 编辑:程序博客网 时间:2024/06/04 18:07

关于行为参数化

行为参数化概念:意味着拿出一个代码块(一般是接口中,定义了某个方法),你准备好却不去执行它。这个代码以后可以被你的程序的其它部分调用,这意味着你可以推迟这块代码的执行。这个方法的行为就是基于那块代码的参数化。

举个栗子:

有一堆苹果,苹果的属性字段中有颜色和重量,你可能需要根据颜色或者重量来筛选这堆苹果。

不推荐的写法:

public class AppleSelect {public static List<Apple> filterAppleByColor(List<Apple>apples, String color){        List<Apple>nowApples = new ArrayList<>();for(Apple apple:apples){if(apple.getColor().equals(color)){nowApples.add(apple);            }        }return nowApples;    }public static List<Apple> filterAppleByWeight(List<Apple>apples, int weight){        List<Apple>nowApples = new ArrayList<>();for(Apple apple:apples){if(apple.getWeight() >weight){nowApples.add(apple);            }        }return nowApples;    }public static void main(String[] args) {        List<Apple> apples = Arrays.asList(new Apple("red",15), new Apple("green", 10));        List<Apple> nowApples1 = filterAppleByColor(apples, "green");        List<Apple> nowApples2 = filterAppleByWeight(apples, 12);        System.out.println(nowApples1);        System.out.println(nowApples2);    }}

此处省略了Apple类,输出:

[Apple [color=green, weight=10]]

[Apple [color=red, weight=15]]

推荐的写法:

需要明确一点,我们只是需要从一堆苹果中筛选出符合我们条件的东西,至于筛选条件如何,我们可能还不太确定。上述的写法,需要确定每种筛选行为后才能表达出筛选的行为。这里,推荐用接口的方式,先把筛选的行为参数化,至于具体的行为可以通过类来继承该接口,写法如下:

interface AppleFilter {boolean filter(Apple apple);}public class AppleSelect2 {    public static List<Apple> filterApples(List<Apple>apples, AppleFilter appleFilter){//准备好一段代码但是不去执行它        List<Apple>newApples = new ArrayList<>();for(Apple apple:apples){if(appleFilter.filter(apple)){newApples.add(apple);            }        }return newApples;    }    public stati void main(String[] args) {//业务代码    }}

根据业务,实现接口interface AppleFilter,获取绿色的苹果,或者苹果重量大于12。

添加两个业务筛选类:

class AppleWeightFilter implements AppleFilter{private intweight; //用户传入需要筛选大于某个重量的值public AppleWeightFilter(intweight) {this.weight = weight;    }@Overridepublic boolean filter(Apple apple) {return apple.getWeight() >weight;    }}class AppleColorFilter implements AppleFilter{private String color; //用来传入需要筛选的颜色public AppleColorFilter(String color) {this.color = color;    }@Overridepublic boolean filter(Apple apple) {return apple.getColor.equals(color);    }}

具体调用如下:

public static void main(String[] args) {List<Apple>apples = Arrays.asList(new Apple("red",15), new Apple("green", 10));List<Apple>nowApples1 = filterApples(apples, new AppleColorFilter("green"));List<Apple>nowApples2 = filterApples(apples, new AppleWeightFilter(12));System.out.println(nowApples1);System.out.println(nowApples2);}

输出结果为:

[Apple [color=green, weight=10]]

[Apple[color=red, weight=15]]

上述中的new AppleColorFilter(“green”)以及new AppleWeightFilter(12)即是将行为参数化。

不难从上面的方法中,可以看到,我们需要定义一个接口,接口中声明了一个方法,可能会有返回类型,但是无具体的实现函数。具体的实现函数,需要我们编写一个相应的实现类来实现该方法。这样就会有一个问题:当我们有多少个筛选行为时,就需要编写多少个实现类,通过观察,发现编写的实现类中,类名是无关紧要的,而且类中的方法以及返回类型都是接口中已经有的,唯一有用的信息就是,方法的具体实现内容。

因此,我们用一个匿名函数来改造上面的代码,删除AppleColorFilter类,AppleWeightFilter类,程序的代码如下(此处省略Apple简单Java bean):
interface AppleFilter{    boolean filter(Apple apple);}public class AppleSelect {        public static List<Apple> filterApples(List<Apple> apples, AppleFilter appleFilter){        List<Apple> newApples = new ArrayList<Apple>();        for(Apple apple:apples){            if(appleFilter.filter(apple)){                newApples.add(apple);            }        }        return newApples;    }        public static void main(String[] args) {        List<Apple> apples = Arrays.asList(new Apple("green", 10), new Apple("red", 20));        //获取颜色是红色的苹果        List<Apple> filterRedApples = filterApples(apples, new AppleFilter(){            @Override            public boolean filter(Apple apple) {                return ("green").equals(apple.getColor());            }        });        System.out.println(filterRedApples);        List<Apple> filterWeightApples = filterApples(apples, new AppleFilter(){            @Override            public boolean filter(Apple apple) {                return apple.getWeight()>15;            }        });        System.out.println(filterWeightApples);            }}

输出:

[Apple [color=green, weight=10]]

[Apple [color=red, weight=20]]

上面的代码,在逻辑上可能没有之前的清楚,但是代码的开发量确实减少了很多,开始更进一步的将行为参数化。通过观察我们发现,对于接口:interface AppleFilter是已知的,我们在用匿名函数时,每次都要写这些相同的代码,对比上面的两个行为,相同的有:new AppleFilter(){ public Booleanfilter(Apple apple)},唯一不同的是其具体的实现方法,而实现方法中需要用到方法中的参数。

总结:有用的参数或者方法有:Appleapple, 具体的实现方法,比如:(“green”).equals(apple.getColor()),在Java 8中充分利用这些参数,所以写法如下:
List<Apple> filterRedApples = filterApples(apples, new AppleFilter(){            @Override            public boolean filter(Apple apple) {                return ("green").equals(apple.getColor());            }        });

改为:

List<Apple> filterRedApples

= filterApples(apples, (Apple apple)->"green".equals(apple.getColor()));

不难看出:通过箭头->进行区分,箭头前(Apple apple)是方法中的参数,而箭头后"green".equals(apple.getColor()))是方法中的具体实现。

所以上述代码改为:

interface AppleFilter{    boolean filter(Apple apple);}public class AppleSelect {        public static List<Apple> filterApples(List<Apple> apples, AppleFilter appleFilter){        List<Apple> newApples = new ArrayList<Apple>();        for(Apple apple:apples){            if(appleFilter.filter(apple)){                newApples.add(apple);            }        }        return newApples;    }        public static void main(String[] args) {        List<Apple> apples = Arrays.asList(new Apple("green", 10), new Apple("red", 20));        //获取颜色是红色的苹果        List<Apple> filterRedApples = filterApples(apples, (Apple apple) -> "green".equals(apple.getColor()));        List<Apple> filterWeightApples = filterApples(apples, (Apple apple) -> apple.getWeight()>15);            }}

这种写法:(Apple apple) -> "green".equals(apple.getColor())即为lamda表达式。

补充Lamda表达式的基本语法:

(parameters) -> expression   //expression表达式,默认会有一个return

(parameters)->{statements;}  

上述:

List<Apple> filterRedApples = filterApples(apples, (Apple apple) -> "green".equals(apple.getColor()));也可以更改为:List<Apple> filterRedApples = filterApples(apples, (Apple apple) -> {return "green".equals(apple.getColor()));}

总结Lamda表达式如何用在哪里用?

1、 需要一个函数式接口:顾名思义,必须是一个接口,接口中要有函数,而且函数只能有一个。

2、 根据接口中获取的条件,再写一个我们实际处理的业务函数。业务函数中有一个参数是上面的函数式接口。

3、 在实际调用业务函数时,才会用到Lamda表达式。Lamda表达式箭头前的参数是上面函数式接口中定义的函数的参数,可以为空,函数体为函数式接口中的函数体,如果有复杂逻辑简单的表达式(expression)可能不能满足,需要用花括号{statement}来描述。

4、 在定义函数式接口时,显示的说明,即加入注解:@FunctionalInterface。当然,这个不是必须的,只是让代码更清晰。

5、 关于异常

举个栗子(关于文件中文本的读取):

传统的写法:

public static void main(String[] args) throws IOException {FileReader fr = new FileReader("d:\\tim.txt");BufferedReader br1 = new BufferedReader(fr);System.out.println(br1.readLine());}

上述代码的作用:是从d盘的tim.txt文件中加载文件,然后通过BufferedReader来读取第一行(br1.readLine())。抛出的异常是IOException,这个是综合了FileReader以及BufferedReader的异常。

改写为Lamda表达式:

1、声明函数式接口

interface ReadFile{    String read(BufferedReader br);g}

2、业务处理函数

public static String processFile(BufferedReader br, ReadFile readFle){        return readFle.read(br);    }

3、实际调用:

public static void main(String[] args) throws IOException {        FileReader fr = new FileReader("d:\\tim.txt");        BufferedReader br1 = new BufferedReader(fr);        String data = processFile(br1, (BufferedReader br)->br.readLine());         System.out.println(data);    }

此时br.readLine()会抛出一个IOException异常,如果直接try/catch捕获,捕获完后还有异常在。此时需要在相应的函数声明式接口以及业务处理函数中添加throws IOException。所以,正确的代码如下:

@FunctionalInterface interface ReadFile{    String read(BufferedReader br) throws IOException;}public class FileRead {    public static String processFile(BufferedReader br, ReadFile readFle) throws IOException{        return readFle.read(br);    }    public static void main(String[] args) throws IOException {        FileReader fr = new FileReader("d:\\tim.txt");        BufferedReader br1 = new BufferedReader(fr);        String data = processFile(br1, (BufferedReader br)->br.readLine());         System.out.println(data);    }}

将上述代码中的Lamda表达式改为方法引用:

String data = processFile(br1, (BufferedReader br) -> br.readLine());

改为方法引用:

String data = processFile(br1, BufferedReader::readLine);

进一步发现,Lambda表达式实际上是对函数式接口的的具体的具体实现类,比如上述的代码:

String data = processFile(br1, (BufferedReader br) -> br.readLine());

改为:

ReadFile readFile = (BufferedReaderbr)->br.readLine();

Stirng data = processFile(br1, readFile);

相应的:String data = processFile(br1,BufferedReader::readLine);

改为:

ReadFile readFile =BufferedReader::readLine;

String data = processFile(br1, readFile);


关于方法引用

方法引用:可以看做是某些Lambda表达式的快捷方式。

(注意:这里强调的是某些,指的的是如果一个Lambda 代表的只是“直接调用这个方法”)

方法引用主要有三类:

(1)指向静态的方法引用

例如:Integer的parseInt方法,写作:Integer::paseInt

(2)指向任意类型的实例方法的方法引用

例如:String的length方法,写作:String::length

(3)在Lamda调用一个已经存在的外部对象中的方法

例如:Lambda表达式()->expensiveTransaction.getValue()可以写作:expensiveTransaction::getValue

Java 8中方法引用的其他一些例子:

Lambda

等效的方法引用

(Apple a) -> a.getWeight()

Apple::getWeight

()->Thread.currentThread().dumpStack()

Thread.currentThread()::dumpStack

(str, i)->str.substring(i)

String::substring

(String, s)->System.out.println(s)

System.out::println

总之,为什么可以这样用方法引用来替代Lambda,是因为:

编译器会进行一种与Lambda表达式类似的类型检查过程,来确定对于给定的函数式接口,这个方法引用是否有效:方法引用的签名必须和上下文类型匹配。

构造函数引用

@FunctionalInterfaceinterface Supplier<T>{    T get();}@FunctionalInterfaceinterface Function<A, B>{    B apply(A a);}public class Factory {    public static void main(String[] args) {        Supplier<Apple> c1 = Apple::new;         //相应的Lambda表达式:Supplier<Apple> c1 = ()-> new Apple();        Apple a1 = c1.get();        System.out.println(a1);                        Function<Integer, Apple> c2 = Apple::new;         //相应的Lambda表达式:Function<Integer, Apple> c2 = (Integer weight) -> new Apple(weight);//        Function<Integer, Apple> c2 = (Integer weight) -> new Apple();        Apple a2 = c2.apply(110);        System.out.println(a2);    }}  

。。。

具体解释,后面在补充:

什么是流?

1、流(Stream):允许你以声明性方式处理数据集合,定义:从支持数据处理操作的源生成的元素序列(Java 8中的集合支持stream方法)。

2、关于流的几个概念:

(1)元素序列:与集合一样,流也提供了一个接口,可以访问特定元素类型的一组有序值。但是,集合是数据结构,主要目的是以特定的时间/空间复杂度存储和访问元素(不存在说 使用过就不能再使用),而流是用于计算(只能遍历一次,即流只能消费一次)。

(2)流的数据源:可以是集合、文件或者生成函数。注意:从有序集合生成的流会保留原有的顺序,有列表生成的流,仍然保持其原有的顺序。

(3)数据处理操作:流的数据处理功能支持类似数据库的操作,以及函数式编程语言中常用的操作,如:filter、map、reduce、find、match、sort等。流操作可以顺序执行,也可以并行执行。

3、关于流操作的特点

(1)流水线:流的中间操作会返回一个流,这些操作可以链接起来,形成一个流水线。流的停止是通过终端操作,比如:limit或者collect等。

(2)内部迭代:与使用迭代器显示操作的集合不同,流的迭代操作是在背后进行的。

 

关于流式处理的一个流程图:


为什么使用流呢?举个栗子

栗子:有一个菜单中有很多到菜,需要筛选出卡路里小于400的菜,并将其按热量从小到大排序,最后输出的结果,只需要该菜单中的菜名。

重点强调:

不要过度的相信流式处理(包括并行流)一定会快,在大多数情况下传统的for循环的迭代版本执行起来要快的多。所以,只能把java 8中的stream作为一个备选的优化策略

再强调一点,你应用始终遵循三个黄金原则:测量,测量,再测量

通常测量一段代码所需要的时间,代码如下:
long beginTime = System.currentTimeMillis();Thread.sleep(1000);long endTime = System.currentTimeMillis();System.out.println("Cost time is:" + (endTime-beginTime) + "ms");

这种代码的性能不错。不过语义上和代码量使得代码没有那么容易阅读。

在很多工具包中会提供一个类:StopWatch(其内部机制只是包装了上述的System.current…),这个类在apache.common中有提供,spring中也有提供,以及guava也有。为了项目的通用性,用org.apache.commons.lang.time.StopWatch

在StopWatch中提供了:start()、stop()、reset()、split()等等,以及获取值的getTime()和getSplitTime()(注意:这个方法要和split配合使用),这里,仅用简单的计算每段程序的运行时间,因此没有用到split。

为了开发更便捷,自己继承StopWatch类实现自己的StopWatch类,如下:

public class StopWatch extends org.apache.commons.lang.time.StopWatch{    /**     * 简化操作:reset和start放一起     */    public void restart(){        reset();        start();    }        /**     * 获取任务花的时间     */    public String taskCostTime(String taskName){        String taskCostTime = "Task:" + taskName + ", cost time is:" + getTime()/1000 + "s";        return taskCostTime;    }}
上面添加了一个restart()方法用于组合reset()和start()方法,以及添加一个方法描述当前任务花的时间,避免每次手写相同的代码。

因此,基于上述的StopWatch,可以用如下代码:

public static void main(String[] args) throws InterruptedException {        StopWatch stopWatch = new StopWatch();        stopWatch.start();        Thread.sleep(2000);        stopWatch.stop();        System.out.println(stopWatch.taskCostTime("task0"));                stopWatch.restart();        Thread.sleep(3000);        stopWatch.stop();        System.out.println(stopWatch.taskCostTime("task1"));    }

输出:

Task:task0, cost time is:2s

Task:task1,cost time is:3s

注意:StopWatch不要设置成单例模式,最好在对应的调用方法处设置一个。(因为:秒表只能供一个人使用,多个人使用一个秒表计时肯定会出问题)。

举个栗子如下:

数据源如下:

StopWatch stopWatch = new StopWatch();        List<Dish> menu = Arrays.asList(                new Dish("pork", false, 800, Dish.Type.MEAT),                new Dish("beef", false, 700, Dish.Type.MEAT),                new Dish("chicken", false, 400, Dish.Type.MEAT),                new Dish("french fries", true, 530, Dish.Type.OTHER),                new Dish("rice", true, 350, Dish.Type.OTHER),                new Dish("season fruit", true, 120, Dish.Type.OTHER),                new Dish("pizza", true, 550, Dish.Type.OTHER),                new Dish("prawns", false, 300, Dish.Type.FISH),                new Dish("salmon", false, 450, Dish.Type.FISH)                );

传统的思路:

采用流的写法如下:

stopWatch.start();        long beginTime = System.currentTimeMillis();        List<Dish> lowCaloricDish = new ArrayList<>();        //1、筛选出卡路里小于400的Dish        for(Dish d:menu){            if(d.getCalories() < 400){                lowCaloricDish.add(d);            }        }        //2、进行排序操作        Collections.sort(lowCaloricDish, new Comparator<Dish>(){            @Override            public int compare(Dish d1, Dish d2) {                return Integer.compare(d1.getCalories(), d2.getCalories());            }        });        //3、查找出相应的菜单名        List<String> lowCaloricDishName = new ArrayList<>();        for(Dish d:lowCaloricDish){            lowCaloricDishName.add(d.getName());        }        System.out.println(lowCaloricDishName);        stopWatch.stop();        System.out.println(stopWatch.taskCostTime("非Java 8"));

Java8 的写法:

stopWatch.restart();        List<String> lowCaloricDishName2 = menu.stream()                .filter(d->d.getCalories()<400)                .sorted(comparing(Dish::getCalories))                .map(d->d.getName())                .collect(toList());         System.out.println(lowCaloricDishName2);        stopWatch.stop();        System.out.println(stopWatch.taskCostTime("Java 8"));

传统的写法和Java8 lambda流式写法的输出与时间如下:

[season fruit, prawns, rice]

Task:非Java 8, cost time is:2ms

[season fruit, prawns, rice]

Task:Java8, cost time is:147ms

问题:java 8耗时比传统的多了好多毫秒???

答:

(1)流式处理建立中间操作是需要耗费时间的,集合转成流(即使对几十万的集合转换成流大概几毫秒(new 一个对象的操作),可以忽略不计),建立中间操作等也需要时间,因此对于数据量小的话,流的性能也可能不怎么好。

(2)上面的业务中需要先从菜单中筛选出热量为小于400热量的Dish,当全部筛选完成之后,才能进行sorted排序操作,而只有sorted操作全部完成之后,才能进行获取其菜单的名字,显然,这里流的每一步都需要依赖于上一步的操作。因此,性能会比原生的慢。

 

再举个栗子:

栗子:对一个List<String> list中的元素进行判断,如果字符串是数字则将字符串转成数字加上1000,再转为数字。

代码如下:

public static void main(String[] args) {        StopWatch stopWatch = new StopWatch();        List<String> list = new ArrayList<>();        for(int i=0;i<1_000;i++){            list.add("123");list.add("adf");list.add("h12");list.add("345345");list.add("12334");        }        //將list中字符串如果是数字则加上1000        //传统的写法        stopWatch.start();        List<String> list2 = new ArrayList<>();        for(String str:list){            if(NumberUtils.isDigits(str)){                int sum = NumberUtils.toInt(str) + 1000;                list2.add(String.valueOf(sum));            }else{                list2.add(str);            }        }        stopWatch.stop();        System.out.println(stopWatch.taskCostTime("非java 8"));                //java 8的写法        stopWatch.restart();        List<String> list3 = list.stream().map(str->{            if(NumberUtils.isDigits(str)){                int sum = NumberUtils.toInt(str) + 1000;                return String.valueOf(sum);            }else{                return str;            }        }).collect(Collectors.toList());        stopWatch.stop();        System.out.println(stopWatch.taskCostTime("java 8"));    }

一个统计的表格如下:


上述性能中横坐标是测试的数据集个数,纵坐标是耗费的毫秒数。所有数据都是循环20次求平均值。

可以看到在数据集小于四百多万时,Java 8 并行流性能稍微好于Java 8串行流,好于非Java 8,d当大于六百多万时,反而仍是非Java 8 性能最好。

 

原因分析:

(1)由于上述字符串转数字加上1000再转回字符串,耗时极短(不到1ms)。

(2)且上述的处理环节只有一个,相对于流式根本没有什么优势。

(3)使用并行流反而效率更低,那是因为并行流的本质上是开启多个线程,而线程的创建以及数据的合并的代价已经超过了(1)中数据处理的代价,因此在数据量一大,这个缺点被极具放大。

 

为了证明这个观点,增加数字转字符串加上1000花费的时间(Thread.sleep(2)),即该操作会慢2ms。得到的统计数据表如下:


这里非Java 8的性能和 Java8串行的性能几乎一致,所以重合。

而Java 8 并行的性能已经远远好于其他两种。

原因分析:为什么Java8串行的性能与非java的性能一致,因为,这里的流的中间操作只有一个,因此没有利用流的特性(至少需要两个),而Java 8的并行流,在处理第一个中间操作开启多线程处理,因此能够显著提高性能。

 

中间操作与终端操作

流的使用一般包括三件事:

(1)一个数据源(如集合)来执行一个查询

(2)一个中间操作练,形成一个流的流水线

(3)一个终端操作,执行流水线,并能生成结果

原创粉丝点击