《On Java 8》笔记 2

第十一章 内部类

Java 8 的 Lambda 表达式和方法引用减少了编写内部类的需求

外部类可以提供一个方法返回一个指向内部类的引用

链接外部类

内部类还拥有其外部类的所有元素的访问权

使用 .this 和 .new

外部类对象的引用:外部类类名.this
创建其某个内部类的对象:外部对象.new

非静态内部类情况下,在拥有外部类对象之前是不可能创建内部类对象的。这是因为内部类对象会暗暗地连接到建它的外部类对象上

内部类与向上转型

普通(非内部)类的访问权限不能被设为 private 或者 protected;他们只能设置为 public 或 package 访问权限,内部类都可以
非静态内部类创建方式:

  1. 内部类public方法返回内部类对象
  2. 外部对象.new 创建内部对象(构造器private就无法创建对象)(内部类Private也无法通过.new 创建对象)
interface Destination {
    String readLabel();
}

interface Contents {
    int value();
}

public class Parcel4 {
    // private 外部对象不能直接new
     private class PContents implements Contents {
        private int i = 11;

        @Override
        public int value() {
            return i;
        }
    }

    protected final class PDestination implements Destination {
        private String label;

        public PDestination(String whereTo) {
            label = whereTo;
        }

        public String readLabel() {
            return label;
        }
    }

    // 提供获取内部类的方法 用接口接对象 向上转型
    public Destination destination(String s) {
        return new PDestination(s);
    }

    // 提供获取内部类的方法 用接口接对象 向上转型
    public Contents contents() {
        return new PContents();
    }

}

class Test {
    public static void main(String[] args) {
        Parcel4 p = new Parcel4();
        Contents contents = p.contents();
        Destination destination = p.destination("test");
        
        // PContents 构造器私有
        // Parcel4.PContents pContents = p.new PContents();

    }
}

private 内部类给类的设计者提供了一种途径,通过这种方式可以完全阻止任何依赖于类型的编码,并且完全隐藏了实现的细节

内部类方法和作用域

在方法的作用域内的类:局部内部类
在对应方法之外不能访问

通过局部内部类创建一个接口的实现类,向上转型return 接口类型

public class Parcel5 {
    public Destination destination(String s) {
        final class PDestination implements Destination {
            private String label;
          
            private PDestination(String whereTo) {
                label = whereTo;
            }
          
            @Override
            public String readLabel() { return label; }
        }
        return new PDestination(s);
    }
  
    public static void main(String[] args) {
        Parcel5 p = new Parcel5();
        Destination d = p.destination("Tasmania");
    }
}

任意的作用域内嵌入一个内部类,超出作用域就失效

public class Parcel6 {
    private void internalTracking(boolean b) {
        if (b) {
            class TrackingSlip {
                private String id;

                public TrackingSlip(String id) {
                    this.id = id;
                }

                public String getSlip() {
                    return id;
                }
            }
            TrackingSlip ts = new TrackingSlip("slip");
            String s = ts.getSlip();
            System.out.println(s);
        }
        // !无效超出作用域
        // TrackingSlip ts = new TrackingSlip("slip");

    }

    public void track() {
        internalTracking(true);
    }

    public static void main(String[] args) {
        Parcel6 p = new Parcel6();
        p.track();
    }
}

匿名内部类

“创建一个继承自 Contents 的匿名类的对象。”通过 new 表达式返回的引用被自动向上转型为对 Contents 的引用

public class Parcel8 {
    public Wrapping wrapping(int x) {
        return new Wrapping(x) {
            @Override
            public int value() {
                return super.value() * 47;
            }
        };
    }

    public static void main(String[] args) {
        Parcel8 p = new Parcel8();
        Wrapping w = p.wrapping(10);

        System.out.println(w.value());

    }
}

class Wrapping {
    private int i;

    public Wrapping(int i) {
        this.i = i;
    }

    public int value() {
        return i;
    }
}

如果在定义一个匿名内部类时,它要使用一个外部环境(在本匿名内部类之外定义)对象,那么编译器会要求其(该对象)参数引用是 final 或者是 “effectively final”(也就是说,该参数在初始化后不能被重新赋值,所以可以当作 final)(如果不会在匿名类内部被直接使用,可以不定义为final,即使不加 final, Java 8 的编译器也会为我们自动加上 final,以保证数据的一致性)

匿名内部类要么继承类,要么实现接口,但是不能两者兼备。而且如果是实现接口,也只能实现一个接口

嵌套类

不需要内部类对象与其外部类对象之间有联系,可以将内部类声明为 static,这通常称为嵌套类

  1. 创建嵌套类的对象时,不需要其外部类的对象。
  2. 不能从嵌套类的对象中访问非静态的外部类对象。

普通的内部类不能有 static 数据和 static 字段,也不能包含嵌套类

接口内部的类

接口中的任何类都自动地是 public 和 static,不写也是
创建某些公共代码,使得它们可以被某个接口的所有不同实现所共用,那么使用接口内部的嵌套类会显得很方便
可以使用嵌套类放置测试代码,会生成了一个独立的类 TestBed$Tester,打包产品之前删除即可

public class TestBed {
    public void f() {
        System.out.println("f()");
    }

    public static class Tester {
        public static void main(String[] args) {
            new TestBed().f();
        }
    }
}

从多层嵌套类中访问外部类的成员

多层嵌套类能透明地访问所有它所嵌入的外部类的所有成员

为什么需要内部类

一般说来,内部类继承自某个类或实现某个接口,内部类的代码操作创建它的外部类的对象。所以可以认为内部类提供了某种进入其外部类的窗口

使用内部类最吸引人的原因是:每个内部类都能独立地继承自一个(接口的)实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响

接口解决了部分问题,而内部类有效地实现了“多重继承”。也就是说,内部类允许继承多个非接口类型(类或抽象类)

class D {

}

abstract class E {

}

class Z extends D {
    E makeE() {
        return new E() {
        };
    }
}

public class MultiImplementation {
    static void takeD(D d) {
    }

    static void takeE(E e) {
    }

    public static void main(String[] args) {
        Z z = new Z();
        takeD(z);
        takeE(z.makeE());
    }
}

内部类的一些特性:

  1. 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其外部类对象的信息相互独立
  2. 在单个外部类中,可以让多个内部类以不同的方式实现同一个接口,或继承同一个类。 稍后就会展示一个这样的例子
  3. 创建内部类对象的时刻并不依赖于外部类对象的创建
  4. 内部类并没有令人迷惑的"is-a”关系,它就是一个独立的实体

闭包与回调

闭包(closure)是一个可调用的对象,它记录了一些信息,这些信息来自于创建它的作用域
在 Java 8 之前,内部类是实现闭包的唯一方式。在 Java 8 中,我们可以使用 lambda 表达式来实现闭包行为

interface Incrementable {
    void increment();
}

// Callee别召唤者
class Callee1 implements Incrementable {
    private int i = 0;

    @Override
    public void increment() {
        i++;
        System.out.println(i);
    }
}

class MyIncrement {
    public void increment() {
        System.out.println("Outer operation");
    }

    static void f(MyIncrement mi) {
        mi.increment();
    }
}

class Callee2 extends MyIncrement {
    private int i = 0;

    @Override
    public void increment() {
        super.increment();
        i++;
        System.out.println(i);
    }

    // 内部类 private
    private class Closure implements Incrementable {
        @Override
        public void increment() {
            Callee2.this.increment();
        }
    }

    Incrementable getCallbackReference() {
        return new Closure();
    }
}

class Caller {
    private Incrementable callbackReference;

    public Caller(Incrementable cbr) {
        this.callbackReference = cbr;
    }

    void go() {
        callbackReference.increment();
    }
}

public class Callbacks {
    public static void main(String[] args) {
        Callee1 c1 = new Callee1();
        Callee2 c2 = new Callee2();
        MyIncrement.f(c2);
        
        System.out.println("==============");

        Caller caller1 = new Caller(c1);
        Caller caller2 = new Caller(c2.getCallbackReference());
        
        caller1.go();
        caller1.go();
        
        caller2.go();
        caller2.go();
    }
}

Callee1直接实现Incrementable接口,所以可以直接重写调用increment方法
Callee2用内部类Closure实现了Incrementable,以提供一个返回Callee2的“钩子”(hook)-而且是一个安全的钩子。无论谁获得此 Incrementable 的引用,都只能调用 increment()

内部类与控制框架

设计模式总是将变化的事物与保持不变的事物分离开,在 模板方法 设计模式中,模板方法是保持不变的事物,而可重写的方法就是变化的事物

内部类允许:

  • 控制框架的完整实现是由单个的类创建的,从而使得实现的细节被封装了起来。内部类用来表示解决问题所必需的各种不同的 action()
  • 内部类能够很容易地访问外部类的任意成员,所以可以避免这种实现变得笨拙

继承内部类

继承内部类的子类构造器必须使用:父类对象.super(); 才能通过编译

内部类可以被重写么?

一个类继承了某个外部类的时候,内部类并没有发生什么特别神奇的变化。这两个内部类是完全独立的两个实体,各自在自己的命名空间内
一个内部类明确继承另一个内部类时,并且重写其内部类方法,这个时候就是重写,可以使用多态的方式调用

public class BigEgg2 extends Egg2 {
    public class Yolk extends Egg2.Yolk {

局部内部类

局部内部类不能有访问说明符,因为它不是外部类的一部分。但是它可以访问当前代码块内的常量、此外部类的所有成员
使用局部内部类而不是匿名内部类呢?理由是:

  • 需要一个已命名的构造器,或者需要重载构造器,而匿名内部类只能使用实例初始化
  • 需要不止一个该内部类的对象

内部类标识符

内部类也必须生成一个 .class 文件以包含它们的 Class 对象信息
类文件命名规则:外部类的名字,加上 "$" ,再加上内部类的名字
如果内部类是匿名的,编译器会简单地产生一个数字作为其标识符

Counter.class
LocalInnerClass$1.class
LocalInnerClass$1LocalCounter.class
LocalInnerClass.class

第十二章 集合

泛型和类型安全的集合

没有使用泛型,Java编译器会给出警告,使用特定的注解来抑制警告信息。 @SuppressWarning 注解及其参数表示只抑制“unchecked”类型的警告
通过使用泛型,就可以在编译期防止将错误类型的对象放置到集合中

// new ArrayList<>() 。这有时被称为“菱形语法”(diamond syntax)。在 Java 7 之前,必须要在两端都进行类型声明
ArrayList<Apple> apples = new ArrayList<Apple>();

当指定了某个类型为泛型参数时,并不仅限于只能将确切类型的对象放入集合中。向上转型也可以像作用于其他类型一样作用于泛型

基本概念

Java集合类库采用“持有对象”(holding objects)的思想,并将其分为两个不同的概念

  1. 集合(Collection) :一个独立元素的序列,这些元素都服从一条或多条规则
  2. 映射(Map) : 一组成对的“键值对”对象,允许使用键来查找值

Collection 的构造器可以配合Arrays.asList() 作为输入,但是 Collections.addAll() 运行得更快
Collection.addAll() 方法只能接受另一个 Collection 作为参数,因此它没有 Arrays.asList() 或 Collections.addAll() 灵活。这两个方法都使用可变参数列表

注意!Arrays.asList() 的输出作为一个 List ,但是这里的底层实现是数组,可以修改,但没法调整大小

Arrays.toString() 集合的打印
subList() 方法可以轻松地从更大的列表中创建切片
Collections.sort() 和 Collections.shuffle() 方法,不会影响 containsAll() 结果
retainAll() 交集,依赖于equals()
List有一个重载的 addAll(int index, Collection<? extends E> c) 可以将新列表插入到原始列表的中间位置
Collection:Object[] toArray(); 无参版本返回一个 Object 数组

Object[] objects = collection.toArray();
Integer[] integers = collection.toArray(new Integer[0]); // 如果参数数组太小而无法容纳 List 中的所有元素(就像本例一样),则 toArray() 会创建一个具有合适尺寸的新数组

迭代器

迭代器(也是一种设计模式)通常被称为轻量级对象(lightweight object)
能够将遍历序列的操作与该序列的底层结构分离,迭代器统一了对集合的访问方式

  1. iterator() 方法要求集合返回一个 Iterator。 Iterator 将准备好返回序列中的第一个元素
  2. next() 方法获得序列中的下一个元素
  3. hasNext() 方法检查序列中是否还有元素
  4. remove() 方法将迭代器最近返回的那个元素删除,调用 remove() 之前必须先调用 next()

写方法迭代集合 入参为Iterator就需要手动传入Collection的iterator对象
入参使用 Iterable 接口更简洁,该接口描述了“可以产生 Iterator 的任何东西

// Collection 继承了 Iterable
public interface Collection<E> extends Iterable<E> {

ListIterator

只能用于List

  • 双向移动hasPrevious() previous()
  • previousIndex() nextIndex()
  • set()
  • add()

链表LinkedList

中间执行插入和删除操作时比 ArrayList 更高效,随机访问操作效率方面却要逊色一些
LinkedList 还添加了一些方法,使其可以被用作栈、队列或双端队列(deque)

  • getFirst() 和 element() 是相同的,它们都返回列表的头部(第一个元素)而并不删除它,如果 List 为空,则抛出 NoSuchElementException 异常。 peek- () 方法与这两个方法只是稍有差异,它在列表为空时返回 null
  • removeFirst() 和 remove() 也是相同的,它们删除并返回列表的头部元素,并在列表为空时抛出 NoSuchElementException 异常。 poll() 稍有差异,它在- 列表为空时返回 null
  • addFirst() 在列表的开头插入一个元素
  • offer() 与 add() 和 addLast() 相同。 它们都在列表的尾部(末尾)添加一个元素
  • removeLast() 删除并返回列表的最后一个元素

Queue 接口在 LinkedList 的基础上添加了 element() , offer() , peek() , poll() 和 remove() 方法,以使其可以成为一个 Queue 的实现

堆栈Stack

Java 1.0 中附带了一个 Stack 类,使用了继承而非组合进行实现

class Stack<E> extends Vector<E> {

使用 ArrayDeque 可以产生更好的 Stack

public class Stack<T> {
  private Deque<T> storage = new ArrayDeque<>();
  public void push(T v) { storage.push(v); }
  public T peek() { return storage.peek(); }
  public T pop() { return storage.pop(); }
  public boolean isEmpty() { return storage.isEmpty(); }
  @Override
  public String toString() {
    return storage.toString();
  }
}

集合Set

TreeSet 将元素存储在红-黑树数据结构中
HashSet 使用散列函数
LinkedHashSet 也使用散列来提高查询速度,但是似乎使用了链表来维护元素的插入顺序

映射Map

通过使用 containsKey() 和 containsValue() 方法去测试一个 Map ,以查看它是否包含某个键或某个值
Map 可以返回由其键组成的 Set ,由其值组成的 Collection ,或者其键值对的 Set

队列Queue

LinkedList 实现了 Queue 接口,通过将 LinkedList 向上转换为 Queue
offer() 是 Queue 的特有方法之一,它在允许的情况下,在队列的尾部插入一个元素,或者返回 false

peek() 和 element() 都返回队头元素而不删除它,但如果队列为空,则 peek() 返回 null , 而 element() 抛出 NoSuchElementException
poll() 和 remove() 都删除并返回队头元素,但如果队列为空,则 poll() 返回 null ,而 remove() 抛出 NoSuchElementException

优先级队列PriorityQueue

PriorityQueue 确保在调用 peek() , poll() 或 remove() 方法时,获得的元素将是队列中优先级最高的元素
Integer , String 和 Character 可以与 PriorityQueue 一起使用,因为这些类已经内置了自然排序

    public static void main(String[] args) {
        List<Integer> ints = Arrays.asList(25, 22, 20,
                18, 14, 9, 3, 1, 1, 2, 3, 9, 14, 18, 21, 23, 25);
        PriorityQueue<Integer> priorityQueue = new PriorityQueue<>(ints);
        QueueDemo.printQ(priorityQueue);// 1 1 2 3 3 9 9 14 14 18 18 20 21 22 23 25 25 

        priorityQueue = new PriorityQueue<>(Collections.reverseOrder());
        priorityQueue.addAll(ints);
        System.out.println(priorityQueue);// [25, 25, 21, 23, 14, 14, 20, 22, 1, 2, 3, 9, 9, 3, 18, 1, 18]
        QueueDemo.printQ(priorityQueue); // 25 25 23 22 21 20 18 18 14 14 9 9 3 3 2 1 1 
    }

for-in和迭代器

也适用于任何 Collection 对象
Java 5 引入了一个名为 Iterable 的接口,该接口包含一个能够生成 Iterator 的 iterator() 方法。for-in 使用此 Iterable 接口来遍历序列

适配器方法惯用法

如果已经有一个接口并且需要另一个接口时,则编写适配器就可以解决这个问题

添加方法返回Iterable对象,用匿名内部类重写其iterator方法返回Iterator对象

注意

不要在新代码中使用遗留类 Vector ,Hashtable 和 Stack

  • Vector:所有get()、set()方法都是synchronized方法,不能对同步进行细粒度控制
  • Hashtable:线程安全造成的效率低下(而且没有遵循驼峰命名法),继承了被弃用的父类Dictionary

除 TreeSet 之外的所有 Set 都具有与 Collection 完全相同的接口。List 和 Collection 存在着明显的不同,尽管 List 所要求的方法都在 Collection 中。另一方面,在 Queue 接口中的方法是独立的,在创建具有 Queue 功能的实现时,不需要使用 Collection 方法。最后, Map 和 Collection 之间唯一的交集是 Map 可以使用 entrySet() 和 values() 方法来产生 Collection

第十三章 函数式编程

函数式编程语言操纵代码片段就像操作数据一样容易。 虽然 Java 不是函数式语言,但 Java 8 Lambda 表达式和方法引用 (Method References) 允许你以函数式编程

函数式编程(FP):通过合并现有代码来生成新功能而不是从头开始编写所有内容,我们可以更快地获得更可靠的代码。至少在某些情况下,这套理论似乎很有用。在这一过程中,函数式语言已经产生了优雅的语法,这些语法对于非函数式语言也适用

OO(object oriented,面向对象)是抽象数据,FP(functional programming,函数式编程)是抽象行为
纯粹的函数式语言在安全性方面更进一步。它强加了额外的约束,即所有数据必须是不可变的:设置一次,永不改变。将值传递给函数,该函数然后生成新值但从不修改自身外部的任何东西(包括其参数或该函数范围之外的元素)

“不可变对象和无副作用”范式解决了并发编程中最基本和最棘手的问题之一(当程序的某些部分同时在多个处理器上运行时)。这是可变共享状态的问题

Lambda表达式

是使用最小可能语法编写的函数定义

interface Description {
    String brief();
}

interface Body {
    String detailed(String head);
}

interface Multi {
    String twoArg(String head, Double d);
}

public class LambdaExpressions {
    static Body bod = h -> h + "No Parens!";
    static Body bod2 = (h) -> h + "More details";
    static Description desc = () -> "Short info";
    static Multi mult = (h, n) -> h + n;

    static Description moreLines = () -> {
        System.out.println("moreLines()");
        return "from moreLines()";
    };

    public static void main(String[] args) {
        System.out.println(bod.detailed("Oh!"));
        System.out.println(bod2.detailed("Hi"));
        System.out.println(desc.brief());
        System.out.println(mult.twoArg("Pi!", 3.14159));
        System.out.println(moreLines.brief());
    }
}

表达式的基本语法是:

  1. 参数
  2. 接着 ->,可视为“产出”
  3. -> 之后的内容都是方法体

注意:

  • ()内放参数,一个参数可省略
  • Lambda 表达式方法体都是单行的,结果自动成为返回值,在这里 return非法
  • Lambda 表达式中确实需要多行,则必须将放在花括号中,这时需要使用 return

递归

可以编写递归的 Lambda 表达式,递归方法必须是实例变量或静态变量,否则会出现编译时错误

整数 n 的阶乘

interface IntCall {
    int call(int arg);
}

public class RecursiveFactorial {
    static IntCall fact;

    public static void main(String[] args) {
        fact = n -> n == 0 ? 1 : n * fact.call(n - 1);
        for (int i = 0; i < 10; i++) {
            System.out.println(fact.call(i));
        }
    }
}

斐波那契

interface IntCall {
    int call(int arg);
}

public class RecursiveFibonacci {
    IntCall fib;

    public RecursiveFibonacci() {
        // fib = n -> {
        //     if (n == 0) return 0;
        //     if (n == 1) return 1;
        //     return fib.call(n - 1) + fib.call(n - 2);
        // };
        fib = n -> n == 0 ? 0 :
                n == 1 ? 1 :
                        fib.call(n - 1) + fib.call(n - 2);
    }

    int fibonacci(int n) {
        return fib.call(n);
    }

    public static void main(String[] args) {
        RecursiveFibonacci rf = new RecursiveFibonacci();
        for (int i = 0; i <= 10; i++) {
            System.out.println(rf.fibonacci(i));
        }
    }
}

方法引用

方法引用组成: 类名或对象名::方法名称

interface Callable {
    void call(String s);
}

class Describe {
    void show(String msg) {
        System.out.println(msg);
    }
}

public class MethodReferences {
    static void hello(String name) {
        System.out.println("hello," + name);
    }

    static class Description {
        String about;

        public Description(String about) {
            this.about = about;
        }

        void help(String msg) {
            System.out.println(about + " " + msg);
        }
    }

    static class Helper {
        static void assist(String msg) {
            System.out.println(msg);
        }
    }

    public static void main(String[] args) {
        Describe d = new Describe();
        // 1. 方法引用
        // 因为方法引用符合 Callable 的 call() 方法的签名
        Callable c = d::show;
        // Java 将 call() 映射到 show()
        c.call("call()");

        // 2. 静态方法引用
        c = MethodReferences::hello;
        c.call("Bob");

        // 3. 内部静态类非静态方法 已实例化对象的方法的引用,有时称为绑定方法引用
        c = new Description("valuable")::help;
        c.call("infomation");

        // 4. 静态内部类中的静态方法
        c = Helper::assist;
        c.call("Help!");
    }
}

Runnable接口

class Go {
    static void go() {
        System.out.println("Go::go");
    }
}

public class RunnableMethodReference {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Anonymous");
            }
        }).start();

        new Thread(() -> System.out.println("lambda")).start();

        new Thread(Go::go).start();
    }
}

未绑定的方法引用

没有关联对象的普通(非静态)方法

class X {
    String f() {
        return "X::f()";
    }
}

interface MakeString {
    String make();
}

interface TransformX {
    String transform(X x);
}

public class UnboundMethodReference {
    public static void main(String[] args) {
        // MakeString ms = X::f; // invalid method reference
        TransformX sp = X::f;

        X x = new X();
        // 效果相同
        System.out.println(sp.transform(x)); // [1]
        System.out.println(x.f());// 对象调用方法
    }
}

第一种方式:

  1. 拿到未绑定的方法引用
  2. 调用它的transform()方法,将一个X类的对象传递给它
  3. 最后使得 x.f() 以某种方式被调用
    Java知道它必须拿第一个参数,该参数实际就是this 对象,然后对此调用方法

构造函数引用

每种情况下赋值给不同的接口,编译器可以从中知道具体使用哪个构造函数

class Dog {
    String name;
    int age = 1;

    public Dog() {
        name = "stray";
    }

    public Dog(String name) {
        this.name = name;
    }

    public Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

interface MakeNoArgs {
    Dog make();
}

interface Make1Args {
    Dog make(String nm);
}

interface Make2Args {
    Dog make(String nm, int age);
}

public class CtorReference {

    public static void main(String[] args) {
        MakeNoArgs mna = Dog::new;
        Make1Args m1a = Dog::new;
        Make2Args m2a = Dog::new;
        Dog make = mna.make();
        Dog comet = m1a.make("Comet");
        m2a.make("Ralph", 14);
    }
}

函数式接口

Java 8 引入了 java.util.function 包。它包含一组接口,这些接口是 Lambda 表达式和方法引用的目标类型。 每个接口只包含一个抽象方法,称为 函数式方法
可以使用 @FunctionalInterface 注解强制执行此“函数式方法”模式,当接口中抽象方法多于一个时产生编译期错误
如果将方法引用或 Lambda 表达式赋值给函数式接口(类型需要匹配),Java 会适配你的赋值到目标接口。 编译器会在后台把方法引用或 Lambda 表达式包装进实现目标接口的类的实例中

java.util.function 包旨在创建一组完整的目标接口,使得我们一般情况下不需再定义自己的接口

以下是基本命名准则:

  • 如果只处理对象而非基本类型,名称则为 Function,Consumer,Predicate 等。参数类型通过泛型添加
  • 如果接收的参数是基本类型,则由名称的第一部分表示,如 LongConsumer,DoubleFunction,IntPredicate 等,但返回基本类型的 Supplier 接口例外
  • 如果返回值为基本类型,则用 To 表示,如 ToLongFunction 和 IntToLongFunction
  • 如果返回值类型与参数类型相同,则是一个 Operator :单个参数使用 UnaryOperator,两个参数使用 BinaryOperator
  • 如果接收参数并返回一个布尔值,则是一个 谓词 (Predicate)
  • 如果接收的两个参数类型不同,则名称中有一个 Bi

在使用函数接口时,名称无关紧要——只要参数类型和返回类型相同。 Java 会将你的方法映射到接口方法。 要调用方法,可以调用接口的函数式方法名,而不是你的方法名

可以使用Function <Integer,Double> 并产生正常的结果,所以用基本类型(IntToDoubleFunction)的唯一理由是可以避免传递参数和返回结果过程中的自动拆装箱,进而提升性能

高阶函数

高阶函数(Higher-order Function)只是一个消费或产生函数的函数

// 使用继承为专用接口起别名
interface FuncSS extends Function<String, String> {
}

public class ProduceFunction {
    static FuncSS produce() {
        return s -> s.toLowerCase();
    }

    public static void main(String[] args) {
        // 这里 produce() 是高阶函数
        FuncSS f = produce();
        System.out.println(f.apply("YELLING"));
    }
}

闭包

考虑一个更复杂的 Lambda,它使用函数作用域之外的变量。 返回该函数会发生什么? 也就是说,当你调用函数时,它对那些 “外部 ”变量引用了什么? 如果语言不能自动解决,那问题将变得非常棘手
Java 8 提供了有限但合理的闭包支持

被 Lambda 表达式引用的局部变量必须是 final 或者是等同 final 效果的
等同 final 效果(Effectively Final)。这个术语是在 Java 8 才开始出现的,表示虽然没有明确地声明变量是 final 的,但是因变量值没被改变过而实际有了 final 同等的效果。 如果局部变量的初始值永远不会改变,那么它实际上就是 final 的

在闭包中,在使用 x 和 i 之前,通过将它们赋值给 final 修饰的变量解决问题
每个闭包都有自己独立的 ArrayList,它们之间互不干扰

public class Closure8 {
    Supplier<List<Integer>> makeFun() {
        // final 可以去掉
        final List<Integer> ai = new ArrayList<>();
        ai.add(1);
        return () -> ai;
    }

    public static void main(String[] args) {
        Closure8 c7 = new Closure8();
        List<Integer> l1 = c7.makeFun().get(),
                l2 = c7.makeFun().get();
        System.out.println(l1);
        System.out.println(l2);

        l1.add(42);
        l2.add(96);
        System.out.println(l1);
        System.out.println(l2);
    }
}

规则并非只是 “在 Lambda 之外定义的任何变量必须是 final 的或等同 final 效果” 那么简单。相反,你必须考虑捕获的变量是否是等同 final 效果的。 如果它是对象中的字段(实例变量),那么它有独立的生命周期,不需要任何特殊的捕获以便稍后在调用 Lambda 时存在。(注:结论是——Lambda 可以没有限制地引用 实例变量和静态变量。但 局部变量必须显式声明为final,或事实上是final 。)

作为闭包的内部类

public class AnonymousClosure {
    IntSupplier makeFun(int x) {
        int i = 0;
        return new IntSupplier() {
            @Override
            public int getAsInt() {
                return x + i;
            }
        };
    }
}

实际上只要有内部类,就会有闭包(Java 8 只是简化了闭包操作)
在 Java 8 之前,变量 x 和 i 必须被明确声明为 final。在 Java 8 中,内部类的规则放宽,包括等同 final 效果

函数组合

  • andThen(argument) 执行原操作,再执行参数操作
  • compose(argument) 执行参数操作,再执行原操作
  • and(argument) 原谓词(Predicate)和参数谓词的短路逻辑与 Predicate
  • or(argument) 原谓词和参数谓词的短路逻辑或 Predicate
  • negate() 该谓词的逻辑非

柯里化和部分求值

柯里化意为:将一个多参数的函数,转换为一系列单参数函数

参考资料

《On Java 8》

posted @ 2021-03-10 20:25  AaronLin  阅读(99)  评论(0编辑  收藏  举报