Java8 In Action-1.基础知识

Java8带来的最重要的改变是引入了函数式编程的思想,在多核和处理大型数据集的计算应用下,带来了如Lambda(匿名函数) 、流、默认方法等核心特性,比之前的命令式更适应新的体系架构.

  1. 新的编程概念
    1.1流处理
    流是一系列数据项,一次只生成一项.程序可以从输入流中一个一个读取数据项,然后以同样的方式将数据项写入输出流。一个程序的输出流很可能是另一个程序的输入流。
    Stream API:java.util.stream
    这里的概念可以类比于Linux中的管道流(|).
    如以下命令:
    cat file1 file2 | tr “[A-Z]” “[a-z]” | sort | tail -3
    处理流程如下:
    Java8 In Action-1.基础知识_第1张图片
    Stream就是一系列T类型的项目,Stream API的很多方法可以链接起来形成一个复杂的流水线,把这样的流变成那样的流,甚至可以是并行流,因为java8可以透明的把输入的不相关部分拿到几个cpu上去分别执行Stream操作流水线.

1.2用行为参数化把代码传递给方法
通过API来传递代码,Java 8增加了把方法(你的代码)作为参数传递给另一个方法的能力,Stream API就是构建在通过传递代码使操作行为实现参数化的思想上的.

1.3并行与共享的可变数据
Java 8的流实现并行比Java现有的线程API更容易.
没有共享的可变数据+将方法和函数即代码传递给其他方法的能力是我们平常所说的函数式编程范式的基石.与此相反,在命令式编程范式中,你写的程序则是一系列改变状态的指令。

2.流
几乎每个Java应用都会制造和处理集合。但集合用起来并不总是那么理想。
场景:从一个User列表中筛选年龄大于20,然后按性别分组.

public class User {

    private String name;
    private String sex;
    private Integer age;

    public User(String name, String sex, Integer age) {
        this.name = name;
        this.sex = sex;
        this.age = age;
    }
    
   getter/setter()....
 
    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", sex='" + sex + '\'' +
                ", age=" + age +
                '}';
    }
}

package com.h.java8;

import java.util.*;
import java.util.stream.Collectors;

/**
 * Created by John on 2018/9/23.
 */
public class TestMain {

    public static void main(String[] args) {
        List<User> userList = Arrays.asList(new User("1", "m", 18), new User("2", "f", 21), new User("3", "m", 27));
        /**
         *  Java8前常见的套路代码
         */
        Map<String, List<User>> map1 = new HashMap<>();
        List<User> list = null;
        for (User u : userList) {
            if (u.getAge() > 20) {
                list = map1.get(u.getSex());
                if (Objects.isNull(list)) {
                    list = new ArrayList<>();
                    list.add(u);
                    map1.put(u.getSex(), list);
                } else {
                    list.add(u);
                }
            }
        }
        for (Map.Entry<String, List<User>> entry : map1.entrySet()) {
            System.out.println(entry.getKey() + "=>" + entry.getValue());
        }
        System.out.println("=================================");
        /**
         * java8之后
         */
        Map<String, List<User>> map = userList.stream().filter(u -> u.getAge() > 20).collect(Collectors.groupingBy(User::getSex));
        map.forEach((k, v) -> System.out.println(k + "=>" + v));

    }
}

和Collection API相比,Stream API处理数据的方式非常不同.用集合的话,你得自己去做迭代的过程。你得用for-each循环一个个去迭代元素,然后再处理元素。我们把这种数据迭代的方法称为外部迭代。相反,有了Stream API,你根本用不着操心循环的事情。数据处理完全是在库内部进行的。我们把这种思想叫作内部迭代。 Collection主要是为了存储和访问数据,而Stream则主要用于描述对数据的计算。

3.默认方法,新增default关键字
Java 8中加入默认方法主要是为了支持库设计师,让他们能够写出更容易改进的接口。例如,在Java 8之前,需要使用Collections.sort()对集合排序,而现在可以直接对List调用sort方法。它是用Java 8 List接口中如下所示的默认方法实现的,它会调用List.sort静态方法:

 default void sort(Comparator<? super E> c) {
        Object[] a = this.toArray();
        Arrays.sort(a, (Comparator) c);
        ListIterator<E> i = this.listIterator();
        for (Object e : a) {
            i.next();
            i.set((E) e);
        }
    }

这意味着List的任何实体类都不需要显式实现sort,而在以前的Java版本中,除非提供了sort的实现,否则这些实体类在重新编译时都会失败。不过慢着,一个类可以实现多个接口,不是吗?那么,如果在好几个接口里有多个默认实现,是否意味着Java中有了某种形式的多重继承?是的,在某种程度上是这样。
Java8从函数式编程中引入的两个核心思想:将方法和Lambda作为一等值,以及在没有可变共享状态时,函数或方法可以有效、安全地并行执行。前面说到的新的Stream API把这两种思想都用到了。

二.通过行为参数化传递代码
行为参数化就是可以帮助你处理频繁变更的需求的一种软件开发模式。一言以蔽之,它意味着拿出一个代码块,把它准备好却不去执行它。这个代码块以后可以被你程序的其他部分调用,这意味着你可以推迟这块代码的执行。例如,你可以将代码块作为参数传递给另一个方法,稍后再去执行它。这样,这个方法的行为就基于那块代码被参数化了。
场景:根据Apple的某些属性(比如它是绿色的吗?重量超过150克吗?)来返回一个boolean值.我们把它称为谓词(即一个返回boolean值的函数)。
2.1策略设计模式.
定义一个接口来对选择标准建模:

public interface ApplePredicate{
boolean test (Apple apple);
}

定义多个实现代表不同的选择标准:

public class AppleHeavyWeightPredicate implements ApplePredicate{
public boolean test(Apple apple){
	return apple.getWeight() > 150;//仅仅选出重的苹果
}
}
public class AppleGreenColorPredicate implements ApplePredicate{
public boolean test(Apple apple){
	return "green".equals(apple.getColor());//仅仅选出绿苹果
}
}

Java8 In Action-1.基础知识_第2张图片
根据抽象条件筛选苹果:

public static List<Apple> filterApples(List<Apple> inventory,
ApplePredicate p){
	List<Apple> result = new ArrayList<>();
	for(Apple apple: inventory){
		if(p.test(apple)){  //谓词对象封装了测试苹果的条件
			result.add(apple);
		}
	}
return result;
}

Java8 In Action-1.基础知识_第3张图片

2.2使用匿名内部类:进一步改进,减少啰嗦的代码
匿名类和 局部类(块中定义的类)差不多,但匿名类没有名字。它允许你同时
声明并实例化一个类。换句话说,它允许你随用随建。

List<Apple> redApples = filterApples(inventory, new ApplePredicate() {
	public boolean test(Apple apple){
		//直接内联参数化filterapples方法的行为
		return "red".equals(apple.getColor());
	}
});
public class MeaningOfThis{
    public final int value = 4;

    public void doIt(){
        int value = 6;
        Runnable r = new Runnable() {
            public final int value = 5;
            @Override
            public void run() {
                int value = 10;
                System.out.println(this.value);
            }
        };
        r.run();
    }

    public static void main(String[] args) {
        MeaningOfThis testMain = new MeaningOfThis();
        testMain.doIt();
    }
}
/***输出5,因为this指的是包含它的Runnable,而不是外面的类MeaningOfThis***/

2.3使用Lambda 表达式

List<Apple> result = filterApples(inventory, (Apple apple) -> "red".equals(apple.getColor()));

2.4将List类型抽象化

public interface Predicate<T>{
	boolean test(T t);
}
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;
}

行为参数化,就是一个方法接受多个不同的行为作为参数,并在内部使用它们,完成不同行为的能力。它是一个很有用的模式, 类似于策略设计模式,它能够轻松地适应不断变化的需求。这
种模式可以把一个行为(一段代码)封装起来,并通过传递和使用创建的行为(例如对Apple的不同谓词)将方法的行为参数化。

三.Lambda表达式
3.1什么是Lambda表达式
简洁地表示可传递的匿名函数的一种方式:它没有名称,但它有参数列表、函数主体、返回类型,可能还有一个可以抛出的异常列表.
匿名-我们说匿名,是因为它不像普通的方法那样有一个明确的名称:写得少而想得多!
函数-我们说它是函数,是因为Lambda函数不像方法那样属于某个特定的类。但和方法一样,Lambda有参数列表、函数主体、返回类型,还可能有可以抛出的异常列表。
传递-Lambda表达式可以作为参数传递给方法或存储在变量中
简洁-无需像匿名类那样写很多模板代码
Java8 In Action-1.基础知识_第4张图片
Lambda的基本语法:

  • (parameters) -> expression
  • (parameters) -> { statements; }

Java8 In Action-1.基础知识_第5张图片

3.2在哪里以及如何使用Lambda?
Lambda表达式需要配合函数式接口使用.函数式接口就是只定义一个抽象方法的接口.这里要注意:接口现在还可以拥有默认方法(即在类没有对方法进行实现时,其主体为方法提供默认实现的方法)。哪怕有很多默认方法,只要接口只定义了一个抽象方法,它就仍然是一个函数式接口.用函数式接口可以干什么呢?Lambda表达式允许你直接以内联的形式为函数式接口的抽象方法提供实现,并把整个表达式作为函数式接口的实例(具体说来,是函数式接口一个具实现的实例).@FunctionalInterface标注用于表示该接口会设计成一个函数式接口.

3.3函数描述符
函数式接口的抽象方法的签名基本上就是Lambda表达式的签名。我们将这种抽象方法叫作函数描述符.

3.4把Lambda付诸实践:环绕执行模式
场景:资源处理(例如处理文件或数据库)时一个常见的模式就是打开一个资源,做一些理,然后关闭资源。这个设置和清理阶段总是很类似,并且会围绕着执行处理的那些重要代码。就是所谓的环绕执行(execute around)模式.

public static String processFile() throws IOException {
    //Java 7中带资源的try语句,它已经简化了代码,因为你不需要显式地关闭资源
	try (BufferedReader br = new BufferedReader(new FileReader("data.txt"))) {
    	return br.readLine();//这就是做有用工作的那行代码
   }
}
@FunctionalInterface
public interface BufferedReaderProcessor {
    String process(BufferedReader b) throws IOException;
}
public class TestMain {

    public static void main(String[] args) throws IOException {
        /**
         * 向函数式接口传递Lambda表达式,灵活应对需求
         */
        //获取第一行数据
        String s0 = processFile(br -> br.readLine());
        System.out.println(s0);
        //获取前两行数据
        String s1 = processFile(br -> br.readLine() + br.readLine());
        System.out.println(s1);
    }

    public static String processFile(BufferedReaderProcessor p) throws IOException {
        try (BufferedReader br = new BufferedReader(new FileReader("D://data.txt"))) {
            return p.process(br);//处理BufferedReader对象
        }
    }
}

3.5Java API中的函数式接口(java.util.function)
Java8 In Action-1.基础知识_第6张图片
Java8 In Action-1.基础知识_第7张图片

原始类型特化
Java类型要么是引用类型(比如Byte、Integer、Object、List),要么是原始类型(比如int、double、byte、char)。但是泛型(比如Consumer中的T)只能绑定到引用类型。这是由泛型内部的实现方式造成的。因此,在Java里有一个将原始类型转换为对应的引用类型的自动拆装箱机制。但这在性能方面是要付出代价的。装箱后的值本质上就是把原始类型包裹起来,并保存在堆里。因此,装箱后的值需要更多的内存,并需要额外的内存搜索来获取被包裹的原始值。
Java 8为我们前面所说的函数式接口带来了一个专门的版本,以便在输入和输出都是原始类型时避免自动装箱的操作。比如,在下面的代码中,使用IntPredicate就避免了对值1000进行装箱操作,但要是用Predicate就会把参数1000装箱到一个Integer对象中:

public interface IntPredicate{
	boolean test(int t);
}
IntPredicate evenNumbers = (int i) -> i % 2 == 0;
evenNumbers.test(1000);//true,无装箱
Predicate<Integer> oddNumbers = (Integer i) -> i % 2 == 1;
oddNumbers.test(1000);//false,装箱

一般来说,针对专门的输入参数类型的函数式接口的名称都要加上对应的原始类型前缀,比如DoublePredicate、IntConsumer、LongBinaryOperator、IntFunction等。Function接口还有针对输出参数类型的变种:ToIntFunction、IntToDoubleFunction等。
Java8 In Action-1.基础知识_第8张图片
任何函数式接口都不允许抛出受检异常(checked exception)。如果你需要Lambda表达式来抛出异常,有两种办法:定义一个自己的函数式接口,并声明受检异常,或者把Lambda
包在一个try/catch块中,显示捕获受检异常.

3.6类型检查、类型推断以及限制
当我们第一次提到Lambda表达式时,说它可以为函数式接口生成一个实例。然而,Lambda表达式本身并不包含它在实现哪个函数式接口的信息.Lambda的类型是从使用Lambda的上下文推断出来的。上下文(比如,接受它传递的方法的参数,或接受它的值的局部变量)中Lambda表达式需要的类型称为目标类型,利用目标类型来检查一个Lambda是否可以用于某个特定的上下文.
Java8 In Action-1.基础知识_第9张图片

同样的Lambda,不同的函数式接口
特殊的void兼容规则
如果一个Lambda的主体是一个语句表达式, 它就和一个返回void的函数描述符兼容(当然需要参数列表也兼容)。例如,以下两行都是合法的,尽管List的add方法返回了一个boolean,而不是Consumer上下文(T -> void)所要求的void:

// Predicate返回了一个boolean
Predicate<String> p = s -> list.add(s);
// Consumer返回了一个void
Consumer<String> b = s -> list.add(s);

3.7类型推断
Java编译器会从上下文(目标类型)推断出用什么函数式接口来配合Lambda表达式,这意味着它也可以推断出适合Lambda的签名,因为函数描述符可以通过目标类型来得到。这样做的好处在于,编译器可以了解Lambda表达式的参数类型,这样就可以在Lambda语法中省去标注参数类型。换句话说,Java编译器会像下面这样推断Lambda的参数类型:

//没有类型推断
Comparator<Apple> c = (Apple a1, Apple a2) -> a1.getWeight().compareTo(a2.getWeight());
//有类型推断
Comparator<Apple> c =(a1, a2) -> a1.getWeight().compareTo(a2.getWeight());
//当Lambda仅有一个类型需要推断的参数时,参数名称两边的括号也可以省略

3.8使用局部变量
Lambda表达式允许使用自由变量(不是参数,而是在外层作用域中定义的变量),就像匿名类一样。 它们被称作捕获Lambda。例如:下面的Lambda捕获了portNumber变量.

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);

Lambda可以没有限制地捕获(也就是在其主体中引用)实例变量和静态变量。但局部变量必须显式声明为final,或事实上是final。换句话说,Lambda表达式只能捕获指派给它们的局部变量一次。(注:捕获实例变量可以被看作捕获最终局部变量this),
下面的代码无法编译,因为portNumber变量被赋值两次:

int portNumber = 1337;
Runnable r = () -> System.out.println(portNumber);
portNumber = 31337;

对局部变量的限制
为什么局部变量有这些限制。第一,实例变量和局部变量背后的实现有一个关键不同。实例变量都存储在堆中,而局部变量则保存在栈上。如果Lambda可以直接访问局部变量,而且Lambda是在一个线程中使用的,则使用Lambda的线程,可能会在分配该变量的线程将这个变量收回之后,去访问该变量。因此,Java在访问自由局部变量时,实际上是在访问它的副本,而不是访问原始变量。如果局部变量仅仅赋值一次那就没有什么区别了——因此就有了这个限制。
第二,这一限制不鼓励你使用改变外部变量的典型命令式编程模式(我们会在以后的各章中
解释,这种模式会阻碍很容易做到的并行处理)。

3.9方法引用
方法引用让你可以重复使用现有的方法定义,并像Lambda一样传递它们。在一些情况下,比起使用Lambda表达式,它们似乎更易读,感觉也更自然。方法引用可以被看作仅仅调用特定方法的Lambda的一种快捷写法。它的基本思想是,如果一个Lambda代表的只是“直接调用这个方法”,那最好还是用名称来调用它,而不是去描述如何调用它。事实上,方法引用就是让你根据已有的方法实现来创建Lambda表达式。
Java8 In Action-1.基础知识_第10张图片
你可以把方法引用看作针对仅仅涉及单一方法的Lambda的语法糖,因为你表达同样的事情时要写的代码更少了.

如何构建方法引用
方法引用主要有三类:

  • 指向静态方法的方法引用(例如Integer的parseInt方法,写作Integer::parseInt)
  • 指向任意类型实例方法的方法引用( 例如String 的length 方法, 写作String::length)
  • 指向现有对象的实例方法的方法引用(假设你有一个局部变量expensiveTransaction用于存放Transaction类型的对象,它支持实例方法getValue,那么你就可以写expensiveTransaction::getValue)
    Java8 In Action-1.基础知识_第11张图片

请注意,还有针对构造函数、数组构造函数和父类调用(super-call)的一些特殊形式的方法引用.

构造函数引用
对于一个现有构造函数,你可以利用它的名称和关键字new来创建它的一个引用:ClassName::new。它的功能与指向静态方法的引用类似。

//构造函数引用指向默认的Apple()构造函数,等价于 Supplier c1 = () -> new Apple();
Supplier<Apple> c1 = Apple::new;
调用Supplier的get方法将产生一个新的Apple
Apple a1 = c1.get();

//指向Apple(Integer weight)的构造函数引用,等价于 Function c2 = (weight) -> new Apple(weight);
Function<Integer, Apple> c2 = Apple::new;
Apple a2 = c2.apply(110);

//指向Apple(String color,Integer weight)的构造函数引用
BiFunction<String, Integer, Apple> c3 = Apple::new;
Apple c3 = c3.apply("green", 110);
public class Fruit {
    protected String color;
    protected Integer weight;
    public Fruit(Integer weight) {
        this.weight = weight;
    }
    @Override
    public String toString() {
        return "Fruit{" +
                "color='" + color + '\'' +
                ", weight=" + weight +
                '}';
    }
}

public class Apple extends Fruit {
    public Apple(Integer weight) {
        super(weight);
    }
}

public class Orange extends Fruit{
    public Orange(Integer weight) {
        super(weight);
    }
}

public class TestMain {

    public static Map<String,Function<Integer,Fruit>> map = new HashMap<>();

    static {
        map.put("apple", Apple::new);
        map.put("orange",Orange::new);
    }

    public static void main(String[] args) throws IOException {
        Fruit apple = getFruit("apple", 123);
        System.out.println(apple);
    }

    public static Fruit getFruit(String fruit,Integer weight){
        return map.get(fruit).apply(weight);
    }
}

3.10复合Lambda 表达式的有用方法
Java 8的好几个函数式接口都有为方便而设计的方法。具体而言,许多函数式接口,比如用于传递Lambda表达式的Comparator、Function和Predicate都提供了允许你进行复合的方法。这是什么意思呢?在实践中,这意味着你可以把多个简单的Lambda复合成复杂的表达式。比如,你可以让两个谓词之间做一个or操作,组合成一个更大的谓词。而且,你还可以让一个函数的结果成为另一个函数的输入。你可能会想,函数式接口中怎么可能有更多的方法呢?(毕竟,这违背了函数式接口的定义啊!)窍门在于,我们即将介绍的方法都是默认方法,也就是说它们不是抽象方法。
比较器复合

//逆序,比较器链
inventory.sort(Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getCountry));

谓词复合:谓词接口包括三个方法:negate、and和or,让你可以重用已有的Predicate来创建更复杂的谓词.

Predicate<Apple> redAndHeavyAppleOrGreen =
redApple.and(a -> a.getWeight() > 150)
.or(a -> "green".equals(a.getColor()));
 public static void main(String[] args) throws IOException {
        BiFunction<String,Integer,Apple> biFunction = Apple::new;
        List<Apple> list = Arrays.asList(biFunction.apply("red",200),biFunction.apply("green",200),
                biFunction.apply("green",50),biFunction.apply("red",160),biFunction.apply("blue",200));
        //按重量逆序排序,然后再按照颜色排序(逆序+比较器链)
        list.sort(Comparator.comparing(Apple::getWeight).reversed().thenComparing(Apple::getColor));
        list.forEach(a -> System.out.println(a));
        System.out.println("=======================");
        //筛选出所有颜色不是红色的苹果
        Predicate<Apple> redPredict = a -> Objects.equals("red",a.getColor());
        Predicate<Apple> notRedPredict = redPredict.negate();
        List<Apple> appleList = list.stream().filter(notRedPredict).collect(Collectors.toList());
        appleList.forEach(a -> System.out.println(a));
        System.out.println("=======================");
        //进一步组合谓词,表达要么是重(150克以上)的红苹果,要么是绿苹果
        Predicate<Apple> redAndHeavyAppleOrGreen = redPredict.and(a -> a.getWeight() > 150).or(a -> "green".equalsIgnoreCase(a.getColor()));
        appleList = list.stream().filter(redAndHeavyAppleOrGreen).collect(Collectors.toList());
        appleList.forEach(a -> System.out.println(a));
    }

这一点为什么很好呢?从简单Lambda表达式出发,你可以构建更复杂的表达式,但读起来仍然和问题的陈述差不多!请注意,and和or方法是按照在表达式链中的位置,从左向右确定优先级的。因此,a.or(b).and©可以看作(a || b) && c。

3.11函数复合
最后,你还可以把Function接口所代表的Lambda表达式复合起来。Function接口为此配了andThen和compose两个默认方法,它们都会返回Function的一个实例。

public static void main(String[] args) throws IOException {
        //g(f(x))
        Function<Integer, Integer> f = x -> x + 1;
        Function<Integer, Integer> g = x -> x * 2;
        Function<Integer, Integer> h = f.andThen(g);
        int result = h.apply(1);
        System.out.println(result);//4
        System.out.println("=================");
        //f(g(x))
        Function<Integer, Integer> h1 = f.compose(g);
        result = h1.apply(1);
        System.out.println(result);//3
    }

小结:

  • Lambda表达式可以理解为一种匿名函数:它没有名称,但有参数列表、函数主体、返回 类型,可能还有一个可以抛出的异常的列表。
  • Lambda表达式让你可以简洁地传递代码。
  • 函数式接口就是仅仅声明了一个抽象方法的接口。
  • 只有在接受函数式接口的地方才可以使用Lambda表达式。
  • Lambda表达式允许你直接内联,为函数式接口的抽象方法提供实现,并且将整个表达式作为函数式接口的一个实例。
  • Java 8自带一些常用的函数式接口,放在java.util.function包里,包括Predicate、Function、Supplier、Consumer和BinaryOperator
  • 为了避免装箱操作,对Predicate和Function等通用函数式接口的原始类型特化:IntPredicate、IntToLongFunction等。
  • 环绕执行模式(即在方法所必需的代码中间,你需要执行点儿什么操作,比如资源分配和清理)可以配合Lambda提高灵活性和可重用性。
  • Lambda表达式所需要代表的类型称为目标类型。
  • 方法引用让你重复使用现有的方法实现并直接传递它们。
  • Comparator、Predicate和Function等函数式接口都有几个可以用来结合Lambda表达式的默认方法。

你可能感兴趣的:(JavaSE)