zl程序教程

您现在的位置是:首页 >  其他

当前栏目

函数式接口, Collection等

2023-03-07 09:40:12 时间

Lambda

函数式接口

lambda 表达式的使用需要借助于 函数式接口, 也就是说只有函数式接口才可以将其用 lambda 表达式进行简化. 函数式接口定义为仅含有一个抽象方法的接口. 按照这个定义, 一个接口如果声明了两个或两个以上的方法就不叫函数式接口.

JDK1.8为接口的定义引入了默认方法, 可以用default关键字在接口中直接定义方法的实现. 如果一个接口存在多个默认方法, 但是仅含有一个抽象方法, 这个接口也符合函数式接口的定义.

@FunctionalInterface注解用于标记该接口是一个函数式接口, 这也是JDK1.8之后新增的. 添加了该注解之后, 编译器会限制接口只允许有一个抽象方法, 否则报错. 建议为函数式接口添加该注解. 在JDK中添加了这个注解的典型接口有 Function, Consumer, Predicate等.

  • 该注解只能标记在”有且仅有一个抽象方法”的接口上
  • JDK8接口中的静态方法和默认方法,都不算是抽象方法
  • 接口默认继承java.lang.Object,所以如果接口显示声明覆盖了Object中方法,那么也不算抽象方法
  • 该注解不是必须的,如果一个接口符合”函数式接口”定义,那么加不加该注解都没有影响。加上该注解能够更好地让编译器进行检查。如果编写的不是函数式接口,但是加上了@FunctionInterface,那么编译器会报错
  • 在一个接口中定义两个自定义的方法,就会产生Invalid ‘@FunctionalInterface’ annotation; FunctionalInterfaceTest is not a functional interface错误.

Consumer Consumer<T> 接收T对象,不返回值

Predicate Predicate<T> 接收T对象并返回boolean

Function Function<T, R> 接收T对象,返回R对象

Supplier Supplier<T> 提供T对象(例如工厂),不接收值

UnaryOperator UnaryOperator 接收T类型参数, 并返回同一类型的结果. 例如

    public static void main(String[] args) {
       List<Integer> list = Arrays.asList(10,20,30,40,50);
       UnaryOperator<Integer> unaryOpt = i->i*i; 
       unaryOperatorFun(unaryOpt, list).forEach(x->System.out.println(x));       
    }
    private static List<Integer> unaryOperatorFun(UnaryOperator<Integer> unaryOpt, List<Integer> list){
       List<Integer> uniList = new ArrayList<>();
       list.forEach(i->uniList.add(unaryOpt.apply(i))); 
       return uniList;
    }

BinaryOperator BinaryOperator 接收两个T类型的参数, 并返回同一类型的结果, 例如

    public static void main(String[] args) {
        Map<String,String> map = new HashMap<>();
        map.put("X", "A");
        map.put("Y", "B");
        map.put("Z", "C");
        BinaryOperator<String> binaryOpt = (s1,s2)-> s1+"-"+s2;
        binaryOperatorFun(binaryOpt, map).forEach(x->System.out.println(x));
    }
    private static List<String> binaryOperatorFun(BinaryOperator<String> binaryOpt, Map<String,String> map){
        List<String> biList = new ArrayList<>();
        map.forEach((s1,s2)->biList.add(binaryOpt.apply(s1,s2)));
        return biList;
    }

Lambda

行为参数化 行为参数化简单的说就是将方法的逻辑以参数的形式传递到方法中, 方法主体仅包含模板类通用代码, 而一些会随着业务场景而变化的逻辑则以参数的形式传递到方法之中, 采用行为参数化可以让程序更加的通用, 以应对频繁变更的需求. 例如对于一个Apple对象

public class Apple {
    private Color color;
    private Float weight;
    
    public Apple() {}
    public Apple(Color color, Float weight) {
        this.color = color;
        this.weight = weight;
    }
    ...
}

最初是需要筛选颜色, 可以使用颜色作为参数

public static List<Apple> filterApplesByCOlor(Lis<Apple> apples, Color color) {
    List<Apple> filtered = new ArrayList<>();
    for (final Apple apple : apples) {
        if (color.equals(apple.getColor())) {
            filtered.add(apple);
        }
    }
    return filtered;
}

如果以重量为参数, 也可以仿照上门的格式再写一个方法 如果筛选的条件不止一种, 需要灵活组合, 那就有必要将filter作为一个参数, 将筛选行为抽象化

public interface AppleFilter {
    boolean accept(Apple apple);
}

public static class List<Apple> filterApplesByFilter(List<Apple> apples, AppleFilter filter) {
    List<Apple> filtered = new ArrayList<Apple>();
    for (final Apple apple : apples) {
        if (filter.accept(apple)) {
            filtered.add(apple);
        }
    }
    return filtered;
}

public static void main(String[] args) {
    //...
    AppleFilter filter = new AppleFilter() {
        @Override
        public boolean accept(Apple apple) {
            //...
        }
    }
    //...
}

上面的行为参数化方式采用匿名类实现, 可以在具体调用的地方用匿名类指定函数的具体执行逻辑, 但是还不够简洁, 在 JDK1.8中可以通过 lambda 表达式进行简化

List<Apple> filtered = filterApplesByFilter(apples, 
        (apple) -> Color.RED.equals(apple.getColor()));

Lambda 表达式的定义与形式 Lambda 表达式

  • 本质上是一个函数, 虽然它不属于某个特定的类, 但具备参数列表、函数主体、返回类型, 甚至能够抛出异常
  • 其次它是匿名的, lambda 表达式没有具体的函数名称

Lambda 表达式可以像参数一样进行传递, 从而简化代码的编写,其格式定义如下

参数列表 -> 表达式参数列表 -> {表达式集合}

注意

  • lambda 表达式隐含了 return 关键字, 所以在单个的表达式中, 我们无需显式的写 return语句, 但是当表达式是一个语句集合的时候则需要显式添加 return语句并用花括号{ } 将多个表达式包围起来.
  • lambda中, this不是指向lambda表达式产生的那个SAM对象, 而是声明它的外部对象

方法引用

  • objectName::instanceMethod
  • ClassName::staticMethod
  • ClassName::instanceMethod

前两种方式类似, 等同于把lambda表达式的参数直接当成instanceMethod|staticMethod的参数来调用. 比如System.out::println等同于x->System.out.println(x), Math::max等同于(x, y)->Math.max(x,y), 例如 execStrs.forEach(System.out::println).

最后一种方式等同于把lambda表达式的第一个参数当成instanceMethod的目标对象, 其他剩余参数当成该方法的参数. 比如String::toLowerCase等同于x->x.toLowerCase(). 可以这么理解,前两种是将传入对象当参数执行方法, 后一种是调用传入对象的方法.

List<BigDecimal> bdList = new ArrayList<>();
BigDecimal result = bdList.stream()
        .reduce(BigDecimal.ZERO, BigDecimal::add);

上面的代码,

  • 创建一个BigDecimal列表

  • 转换为 Stream<BigDecimal>

  • 调用 reduce 方法

    • 提供了一个定义好的加数, 这里是BigDecimal.ZERO
    • 使用BinaryOperator<BigDecimal>, 通过方法BigDecimal::add将两个BigDecimal的值相加
List<Invoice> invoiceList = new ArrayList<>();
//populate
Function<Invoice, BigDecimal> totalMapper = invoice -> invoice.getUnit_price().multiply(invoice.getQuantity());
BigDecimal result = invoiceList.stream()
        .map(totalMapper)
        .reduce(BigDecimal.ZERO, BigDecimal::add);

上面的代码, 使用了一个Function做stream对象的map.

构造器引用 构造器引用语法: ClassName::new 把lambda表达式的参数当成ClassName构造器的参数. 例如BigDecimal::new等同于x->new BigDecimal(x).

Iterable

JDK1.8中, Iterable接口增加了两个带default实现的方法, 一个是

    default void forEach(Consumer<? super T> action) {
        Objects.requireNonNull(action);
        for (T t : this) {
            action.accept(t);
        }
    }

另一个是

    default Spliterator<T> spliterator() {
        return Spliterators.spliteratorUnknownSize(iterator(), 0);
    }

Spliterator

Spliterator这个类型是JDK1.8新增的接口方法

boolean tryAdvance(Consumer<? super T> action);

Spliterator<T> trySplit();

Spliterator用于对一个数据源进行遍历和分区, 这个数据源可以是一个数组, 一个Collection,一个IO通道, 或者一个生成函数. Spliterator可以单个或成批地处理元素, 也可以将部分元素划分为单独的Spliterator, 不能分区的Spliterator将不能从并行处理中获益. Spliterator用characteristics()方法汇报结构特性. 调用trySplit()的线程可以将返回的Spliterator传递给另一个线程, 这个线程可以遍历或进一步拆分这个Spliterator.

Consumer

Consumer是一个操作, 用于接受单个输入并且不返回结果. 在stream里主要是用于forEach内部迭代的时候, 对传入的参数做一系列的业务操作. Consumer有相关的原始类型实现: IntConsumer,LongConsumer,DoubleConsumer, 是Consumer的特例

    void accept(T t);

    default Consumer<T> andThen(Consumer<? super T> after) {
        Objects.requireNonNull(after);
        return (T t) -> { accept(t); after.accept(t); };
    }

JDK8中有双冒号的用法,就是把方法当做参数传到stream内部,使stream的每个元素都传入到该方法里面执行一下

public class MyTest {
    public static void  printValur(String str){
        System.out.println("print value : "+str);
    }
 
    public static void main(String[] args) {
        List<String> al = Arrays.asList("a", "b", "c", "d");
        al.forEach(AcceptMethod::printValur);
        //下面的方法和上面等价的
        Consumer<String> methodParam = AcceptMethod::printValur;
        al.forEach(x -> methodParam.accept(x));//方法执行accept
    }
}

Collection

Collection是集合类型对象的根接口. collection代表了一组对象集合, 这些对象就是集合的元素. 一些集合允许重复的元素, 另一些则不允许. 一些集合是有序的, 另一些则是无序的. JDK并没有提供这个接口的直接实现, 但是提供了其派生接口的实现, 例如Set和List. 这个接口用于传递和操作集合类型的数据并保持最大的通用特性.

打包类型或多集合类型(包含重复对象的无序集合)应该直接实现这个接口.

所有通用的Collection实现类(通常是实现某个派生接口)都应该提高两个标准的构造方法: 一个 void (无参数) 构造方法和一个单参数构造方法, 前者创建一个空集合, 后者创建一个包含一个元素的集合. 实际上, 后者允许用户用任何集合创建包含相同元素的新类型集合. 虽然不能对这种便利进行强制(因为接口不能包含构造方法)但是Java平台上所有的通用Collection实现都遵守这种约定.

在操作集合时, 如果调用了此集合类型不支持的方法, 应当抛出UnsupportedOperationException. 在特殊情况下, 例如调用对集合并不产生影响时, 可以抛出UnsupportedOperationException, 但这不是强制的. 例如在一个不可修改的collection上调用addAll(Collection)方法时, 如果参数的集合为空时, 不强制抛出异常.

有些集合实现会对元素有限制. 例如一些实现禁止null元素, 另一些则对元素的类型有要求. 添加不合法的元素时会抛出异常, 例如NullPointerException 或 ClassCastException. 而查询一个不合法元素时可能会抛出异常, 也可能会返回false; 这些异常在接口的规范中是可选的.

同步的策略由各个集合实现自己来决定. 由于不是强约束, 被其他线程转换的集合上调用任何接口方法都可能导致未定义的行为, 包含直接调用, 传参后调用, 或者用迭代器对集合进行操作.

Collections框架中的许多方法是根据equals方法定义的. 例如, contains(Object o)方法的定义是: "仅当这个集合包含至少一个元素e满足(o==null ? e==null : o.equals(e))时, 返回true." 这个定义不应该被解释为说明使用非null参数调用Collection.contains时将导致o.equals(e)对每一个e元素进行调用. Collection的实现可以自行优化避免使用equals, 例如首先比较两个元素的hash code. (Object.hashCode() 的定义保证了hash code不相等的两个对象肯定不相等.) 更进一步, Collections 框架的不同实现可以自行利用对象方法的特定行为进行优化..

一些操作中, 对直接或间接包含自己的集合进行递归遍历可能会抛出异常, 例如clone(), equals(), hashCode() 和 toString() 方法. 具体的实现类可能会对这种情况进行处理, 不过大多数现在的实现并不会这样做.