Java 泛型深度学习笔记

泛型

描述泛型的优点
使用泛型类和接口
定义泛型类和接口
解释为什么泛型类型可以提高可靠性和可读性
定义并使用泛型方法和受限泛型类型
使用原始类型向后兼容
解释为什么有必须压迫必要有通配的泛型类型
描述泛型消除并列出一些由类型消除引起的泛型上的限制和局限性
设计并实现泛型矩阵类

21.1 引言

泛型(generic)是指参数化类型的能力。
使用泛型的主要优点是能够在编译时而不是运行时检测出错误。

21.2 动机和优点

package java.lang;
public interface Comparable<T> {
    pubic int compareTo(T o);
}

这里的<T>表示形式泛型类型(formal generic type)
随后可以用一个实际具体类型(actual concrete type)来替换它。
替换泛型类型称为泛型实例化(generic instantiation)
按照惯例,像E,T这类单个大写字母用于表示一个形式泛型类型

泛型类型必须是引用类型,
误:ArrayList<int> intList = new ArrayList<int>;
正:ArrayList<Integer> intList = new ArrayList<Integer>;

自动打包:intList.add(5),Java会自动把5包装为new Integer(5)
自动解包:int x = new Integer(5),

从泛型定义好的变量中取值时,不需要再做强制转化。

21.3 定义泛型类和接口

为了创建一个字符串堆栈,可以使用new GenericStack()
这个可能会会引起对构造方法的误解:
误:pubic GenericStack<E>()
正:pubic GenericStack()

有时候,泛型类可能会有多个参数。所有的参数应以其放在尖括号中,
并用逗号隔开。例如:<E1,E2,E3>

可以定义一个类或一个接口作为泛型或者接口的子类型。
譬如在Java API中,java.lang.String类被定义为实现Comparable接口

public class String implements Comparable<String>

21.4 泛型方法

可以定义泛型接口和泛型类,还可以使用泛型类型来定义泛型方法。
为了调用泛型方法,需要将实际类型放在尖括号内作为方法名的前缀。

public class GenericMethodDemo{
    public static void main(String[] args) {
        Integer[] integers = {1,2,3,4,5};
        String[] strings = {"London","Paris","NewYork","Austin"};

        GenericMethodDemo.<Integer>print(integers);
        GenericMethodDemo.<String>print(strings);
    }
    public static <E> void print(E[] list){

        Stream.of(list).forEach {

            x -> {System.out.println(x);}
        }
    }
}

可以将泛型指定为另一种类型的子类型。这样的泛型成为受限的(bounded)

非受限类型<E<E extends Object>是一样的。

  • 为了定义一个类为泛型类型,需要将泛型类型放在类名之后。
    例如GenericStack<E>
  • 为了定义一个方法为泛型类型,要将泛型类型放在方法返回类型之前。
    例如<E> void max <E o1, E o2>

21.5 原始类型和向后兼容

GenricStack stack = new GenericStack();
像GenericStack和ArrayList这样不使用类型参数的泛型类成为原始类型(raw type)

使用原始类型的Comparable

public class Max{
    public static Comparable max (Comparable o1, Comparable o2)  {
        if (o1.compareTo(o2) > 0)
            return o1;
        else
            return o2;
    }
}

这样的代码是不安全的, 因为Max.max("Welcome",23); 编译时不会出错,但运行时会出错。
更好的编程方法就是使用泛型类型。

public class Max{
    public static <E extends Comparable<E>> E max (E o1, E o2)  {
        if (o1.compareTo(o2) > 0)
            return o1;
        else
            return o2;
    }
}

E extends Comparable<E> 表示E是Comparable的子类型。
将其放在尖括号内<E extends Comparable<E>>放在函数的前面,表示定义这个方法为泛型类型。
E max (E, E),表示这个函数入力为两个E型,返回值也为E型。

21.6 通配泛型

public static double max(Generic<Number> stack){}
public static void main(String[] args) {
    GenericStack<Integer> intStack = new GenericStack<Integer>();
}

这样会出编译错误。因为GenericStack<Integer> 不是GenericStack<Number>的实例
通配泛型有3种形式,?,? extends T,? super T
第一种形式 ?称为非受限通配(unbounded wildcard)
它和? extends Object是一样的

第二种形式? extends T 成为受限通配(bounded wildcard),
表示T或者T的一个未知子类型。

第三种形式? super T 称为下限通配(lower-bounded wildcard)
表示T或者T的一个未知父类型。

使用下面的代码就可以修复上面的错误。

public static double max(Generic<E extends Number> stack){}

尽管Integer是Object的一个子类型,但是
GenericStack<Integer>并不是GenericStack<Object>的子类型

什么时候需要<? super T>通配符

public static void main(String[] args) {
    GenericStack<String> stack1 = new GenericStack<String>();
    GenericStack<Object> stack2 = new GenericStack<Object>();
    stack2.push("Java");
    stack2.push(2);
    stack1.push("Sun");
    add(stack1,stack2);
    print(stack2);
}
public static <T> void add(GenericStack<T> stack1, GenericStack<? super T> stack2){
    while (!stack1.isEmpty())
        stack2.push(stack1.pop());
}

如果用<T>代替<? super T> 那么在add处会产生编译错误。
因为stack1的类型为GenericStack而stack2的类型为GenericStack
<? super T>表示类型T或T的父类型,而Object是String的父类型。
2可以转换成Object而不能转换成String类型。

21.7 消除泛型和对泛型的限制

泛型是使用一种称为类型消除(type erasure)的方法来实现的。
编译器使用泛型类型信息来编译代码,但是随后会消除它。
因此泛型信息在运行时是不可用的。
这种方法可以使泛型代码向后兼容使用原始类型的遗留代码。

尽管在编译时ArrayListArrayList是两种类型,
但运行时只有ArrayList类会被加载到JVM中。
所以下面的结果都是true。

ArrayList<String> list1 = new ArrayList<String>();
ArrayList<String> list2 = new ArrayList<String>();
System.out.println(list1 instanceof ArrayList);
System.out.println(list2 instanceof ArrayList);

由于泛型类型在运行时会被消除,因此会有一些限制。

限制1 不能使用new E()

出错的原因是运行时执行new E(),但E在运行时已经是不存在的了。

限制2 不能使用new E[]

不能使用泛型类型创建数组。譬如下面的代码就是错误的。
E[] elements = new E[capacity];
可以通过创建一个Object类型的数组,然后将它的类型转换成E[]来规避这个限制。
E[] elements = (E[])new Object[capacity];
会导致下面的编译警告:

型の安全性: Object[] から E[] への未検査キャスト

需要加上 @SuppressWarnings("unchecked")

不允许使用泛型类创建泛型数组,譬如下面代码是错误的。
ArrayList<String>[] list = new ArrayList<String>[10];
会导致下面的编译错误:

ArrayList<String> の総称配列を作成できません

可以使用下面的代码来规避这种限制
ArrayList<String>[] list = (ArrayList<String>[])new ArrayList[10];

限制3 在静态环境下不允许蕾的参数是泛型类型

由于泛型类的所有实例都有相同的运行时类,所以泛型类的静态变量和方法是被它的所有实例所共享的。
因此在静态方法,数据域或者初始化语句中,为了类而引用泛型类型参数是非法的。

public static void main(E 01) { //非法
}

public static E o1; //非法

static {
    E o1; //非法
}
限制4 异常类不能是泛型
public class MyException<T> extends Excpetion{
}
try {
    ...
} catch (MyException<T> ex){
    ...
}

因为如果允许这么做,就应为MyException添加一个catch子句。
JVM必须检查这个从try子句中抛出的异常以确定它是否与catch子句中指定的类型匹配。
但这是不可能的,运行时类型信息是不出现的。

21.8 实例学习:泛型矩阵类

《Java8函数式编程》学习笔记 - 第8章

第八章 设计和架构的原则

软件开发最重要的设计工具不是什么技术,而是一颗在设计原则方面训练有素的头脑。
—— Craig Larman

如何使用Lambda表达式实现SOLID原则,
该原则是开发良好面向对象程序的准备。

设计模式可以参考下面这个连接 设计模式

在这里主要是要去理解,Lambda表达式对设计模式的影响,以及跟设计模式的关系。

8.1 Lambda表达式改变了设计模式

设计模式是软件架构中解决通用问题的模板。
如果碰到一个问题,恰好有一个与之适应的模式,就能直接应用该模式来解决问题。
从某种程度上来说,设计模式将解决特定问题的最佳实践途径固定了下来。

8.1.1 命令者模式
  • 命令接收者:执行实际任务
  • 命令者:封装了所有调用命令执行者的信息
  • 发起者:控制一个或多个命令的顺序和执行
  • 客户端:创建具体命令实例

假设有一个GUI Editor 组件,在上面可以执行open、save 等一系列操作,
现在我们想实现宏功能——也就是说,可以将一系列操作录制下来,
日后作为一个操作执行,这就是我们的命令接收者

8.1.2 策略模式
8.1.3 观察者模式
8.1.4 模板方法模式

8.2 使用Lambda表达式的领域专用语言

8.2.3 评估

流畅性的一方面表现在DSL 是否是IDE 友好的。
用代码自动补全功能补齐代码。这就是使用Description 和Expect 对象的原因。
当然也可以导入静态方法it 或expect,一些DSL 中就使用了这种方式。
如果选择向Lambda 表达式传入对象,而不是导入一个静态方法,
就能让IDE 的使用者轻松补全代码。

唯一要记住的是调用describe方法

大多数测试框架提供了大量注释,或者很多外部“魔法”,或者借助于反射。
我们不需要这些技巧,就能直接使用Lambda 表达式在DSL 中表达行为,就和使用普通的Java 方法一样。

8.3 使用Lambda表达式的SOLID原则

SOLID 原则是设计面向对象程序时的一些基本原则。
原则的名字是个简写,分别代表了下面五个词的首字母

  • Single responsibility、
  • Open/closed、
  • Liskov substitution、
  • Interface segregation
  • Dependency inversion。

这些原则能指导你开发出易于维护和扩展的代码。

8.3.1 单一功能原则

8.4 进阶阅读

8.5 要点回顾

《Java8函数式编程》学习笔记 - 第7章

第七章 测试,调试,和重构

重构,测试驱动开发(TDD)和持续继承(CI)越来越流行。 <=这三个概念不是很明白

本章主要探讨如何在代码中使用Lambda表达式的技术,
也会说明什么情况下不应该(直接)使用lambda表达式。
如何调试大量使用Lambda表达式和流的程序。

如何使用Lambda表达式提高非集合类代码的质量。

7.1 重构候选项

在选择内部设计模型时,想想以何种形式向外展示API是大有裨益的。

什么时候应该Lambda化自己的应用或类库的要点:
其中每一条都可看作一个局部的反模式或者代码异味,借助于Lambda来修复。

7.1.1 进进出出,摇摇晃晃
//例7-1 logger 对象使用isDebugEnabled 属性避免不必要的性能开销
Logger logger = new Logger();
if (logger.isDebugEnabled()) {
    logger.debug("Look at this: " + expensiveOperation());
}

这种反模式通过传入代码即数据的方式很容易解决。
isDebugEnabled 方法暴露了内部状态。
如果使用Lambda 表达式,外面的代码根本不需要检查日志级别。

//例7-2 使用Lambda 表达式简化记录日志代码
Logger logger = new Logger();
logger.debug(() -> "Look at this: " + expensiveOperation());

当程序处于调试级别,并且检查是否使用Lambda表达式的逻辑被封装在Logger对象中时,才会调用Lambda 表达式。

7.1.2 孤独的覆盖

这个代码异味是使用继承,其目的只是为了覆盖一个方法。

ThreadLocal 就是一个很好的例子。ThreadLocal能创建一个工厂,为每个线程最多只产生一个值。 这是确保非线程安全的类在并发环境下安全使用的一种简单方式。

//例7-3 在数据库中查找艺术家
ThreadLocal<Album> thisAlbum = new ThreadLocal<Album> () {
    @Override protected Album initialValue() {
        return database.lookupCurrentAlbum();
    }
};
//例7-4 使用工厂方法
//为工厂方法withInitial 传入一个Supplier 对象的实例来创建对象
ThreadLocal<Album> thisAlbum
    = ThreadLocal.withInitial(() -> database.lookupCurrentAlbum());

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

优点:

  1. 任何已有的Supplier实例不需要重新封装就可以在此使用,鼓励了重用和组合。
  2. 代码更加清晰。
  3. JVM会少加载一个类
7.1.3 同样的东西写两遍

不要重复你劳动(Don’t Repeat Yourself,DRY) 同样的东西写两遍(Write Everything Twice,WET)

//例7-7 使用领域方法重构Order 类
public long countFeature(ToLongFunction<Album> function) {
    return albums.stream()
    .mapToLong(function)
    .sum();
}
public long countTracks() {
    return countFeature(album -> album.getTracks().count());
}
public long countRunningTime() {
    return countFeature(album -> album.getTracks()
            .mapToLong(track -> track.getLength())
            .sum());
}
public long countMusicians() {
    return countFeature(album -> album.getMusicians().count());
}

传入一个代表了领域知识的Lambda 表达式

7.2 Lambda表达式的单元测试

单元测试是测试一段代码的行为是否符合预期的方式。 Lambda表达式没有名字,

7.3 在测试替身时使用Lambda表达式

测试替身也常被称为模拟,事实上测试存根和模拟都属于测试替身。 区别是模拟可以验证代码的行为。读者若想了解更多有关这方面的信息, 请阅读Martin Fowler的相关文章

//例7-14 使用Lambda 表达式编写测试替身,传给countFeature 方法
@Test
public void canCountFeatures() {
    OrderDomain order = new OrderDomain(asList(
        newAlbum("Exile on Main St."),
        newAlbum("Beggars Banquet"),
        newAlbum("Aftermath"),
        newAlbum("Let it Bleed")));
assertEquals(8, order.countFeature(album -> 2));
}

OrderDomain是Order类,其中有上面例7-7里面的4个方法。
现在测试的是countFeature方法。其参数为Funtion接口。
入力为Album集合,出力为固定整数2,最后求和为8

//例7-15 结合Mockito 框架使用Lambda 表达式
List<String> list = mock(List.class);
when(list.size()).thenAnswer(inv -> otherList.size());
assertEquals(3, list.size());

Mokito会在别的博客中介绍。

7.4 惰性求值和调试

因为惰性求值的存在,没法简单的调试。

7.5 日志和打印消息

因为是惰性求值,是无法看中间值的。
一旦调用了forEach操作,因为是及早求值,使得无法继续流操作。

7.6 解决方案:peek

流有一个方法让你能查看每个值,同时能继续操作流。这就是peek 方法。
使用peek 方法还能以同样的方式,将输出定向到现有的日志系统中,
比如log4j、java.util.logging 或者slf4j。

7.7 在流中间设置断点

记录日志这是peek方法的用途之一。 为了像调试循环那样一步一步跟踪,可在peek方法中加入断点,这样就能逐个调试流中的元素了。
peek方法可知包含一个空的方法体,只要能设置断点就行。
有一些调试器不允许在空的方法体中设置断点时,将值简单地映射为其本身,这样就有地方设置断点了,

7.8 要点回顾

  • Lambda表达式,有一些通用的模式。
  • 如果想要对复杂一点的Lambda表达式编写单元测试,将其抽取成一个常规的方法。
  • peek方法能记录中间值,在调试时非常有用。

《Java8函数式编程》学习笔记 - 第5章

第五章 高级集合器和收集器

介绍一些高级主题,包括新引入的Collectors类,方法引用。

5.1 方法引用

artist -> artist.getName()
Artist :: getName
标准语法为ClassName::MethodName
虽然这是一个方法,但不需要在后面加括号,
因为这里并不调用该方法。
我们只是提供了和Lambda表达式等价的一种结构,在需要时才会调用。
凡是使用Lambda表达式的地方,就可以用使用方法引用。

例如对构造函数改造成方法引用
(name, nationality) -> new Artist(name, nationality)
Artist::new
创建一个字符串型数组:
String[]::new

5.2 元素顺序

Set<Integer> numbers = new HashSet<>(asList(4, 3, 2, 1));
List<Integer> sameOrder = numbers.stream()
                                 .sorted()
                                 .collect(toList());
assertEquals(asList(1, 2, 3, 4), sameOrder);

sorted方法使得原本无序的流映射后成了有序的。
unordered方法可以消除顺序。
但大多数操作都是在有序的流上效率更高。

5.3 使用收集器

从流中生成List,是最自然的数据结构。
但有时候,需要从流生成Map或者Set,或者定制一个类,把需要的结果抽象出来。

仅凭流上方法的签名,就能判断出这是否是一个及早求值的操作。
⇒及早求值的方法,其返回值是流以外的类型。
reduce操作就是一个很好地例子。
⇒从一个流中返回一个值,因此是一个及早求值的操作

有时人们希望做的更多,这就是收集器
一种通用的,从流生成复杂值的结构。只要将它传给collect方法,
所有的流就都可以使用它。

标准类库已经提供了一些有用的收集器,例如本章使用的收集器
都是java.util.stream.Collectors类中静态导入的。

5.3.1 转换成其他集合(toSet,toCollection)

有一些收集器可以生成其他集合。
比如前面已经见过的toList,生成了java.util.List类的实例。
还有toSet和toCollection,分别生成Set和Collection类的实例

如何使用Stream 类库并行处理数据,收集并行操作
的结果需要的Set,和对线程安全没有要求的Set 类是完全不同的。
具体内容在p?

使用一个特定的集合收集值,而且你可以稍后指定该集合的类型。
比如,使用TreeSet的情况,可以使用toCollection

//例5-5 使用toCollection,用定制的集合收集元素
stream.collect(toCollection(TreeSet::new));
5.3.2 转换成值(maxBy...)
//成员最多的乐队
public Optional<Artist> biggestGroup(Stream<Artist> artists) {
    Function<Artist,Integer> getCount =
         artist -> artist.getMembers().size();
    return artists.collect(Collectors.maxBy(Comparator.comparing(getCount)));
}

这里主要学习maxBy的用法。

double d = this.albums.stream()
                .collect(Collectors.averagingInt(
                        album -> album.getTrackList().size()));
int i = this.albums.stream()
                .collect(Collectors.summingInt(
                        album -> album.getTrackList().size()));

这里主要学习averagingInt以及summingInt的用法。

5.3.3 数据分块(partitioningBy)
Map<Boolean, List<Artist>> x =
    allArtists.stream().collect(Collectors.partitioningBy(
        artist -> artist.isFrom("London")));

这里主要学习partitioningBy的用法
返回一个map,true那边是来自伦敦,false那边是不来自伦敦

5.3.4 数据分组(groupingBy)
public Map<Artist, List<Album>> albumsByArtist(Stream<Album> albums) {
    return albums.collect(groupingBy(album -> album.getMainMusician()));
}

groupingBy跟数据分块类似,只是不再限制true,false,可以使用任意值对数据分组

5.3.5 字符串(joining)
allArtists.stream()
        .map(Artist::getName)
        .collect(Collectors.joining(", ", "[", "]"));

joining的三个参数分别为,分隔符,前缀,后缀

5.3.6 组合收集器
public Map<Artist, Long> numberOfAlbums(Stream<Album> albums) {
    return albums.collect(
        groupingBy(album -> album.getMainMusician(),counting()));
}

groupingBy的第二个参数为第二个收集器

5.3.6 重构和定制收集器
StringBuilder reduced =
                allArtists.stream()
                .map(Artist::getName)
                .reduce(new StringBuilder(), (builder, name) -> {
                    if (builder.length() > 0)
                        builder.append(", ");
                    builder.append(name);
                    return builder;
                    }, (left, right) -> left.append(right));
        reduced.insert(0, "[");
        reduced.append("]");
        String result = reduced.toString();
StringCombiner combined =
    artists.stream()
        .map(Artist::getName)
        .reduce(new StringCombiner(", ", "[", "]"),
            StringCombiner::add,
            StringCombiner::merge);
String result = combined.toString();

如何理解上述代码,首先要理解reduce的三种override方法

  • Optional<T> reduce(BinaryOperator<T> accumulator);
  • T reduce(T identity, BinaryOperator<T> accumulator);
  • <U> U reduce(U identity,BiFunction<U, ? super T, U> accumulator,BinaryOperator<U> combiner);

第一个变形,接受一个函数接口BinaryOperator

  • 变形1,未定义初始值,从而第一次执行的时候第一个参数的值是Stream的第一个元素,
    第二个参数是Stream的第二个元素
  • 变形2,定义了初始值,从而第一次执行的时候第一个参数的值是初始值,第二个参数是Stream的第一个元素

  • 变形3,如果使用了parallelStream,reduce操作是并发进行的,
    为了避免竞争,每个reduce线程都会有独立的result
    combiner的作用在于合并每个线程的result得到最终结果

ArrayList<Integer> accResult_ = Stream.of(1, 2, 3, 4)
    .reduce(new ArrayList<Integer>(),
        new BiFunction<ArrayList<Integer>, Integer, ArrayList<Integer>>() {
            @Override
            public ArrayList<Integer> apply(ArrayList<Integer> acc, Integer item) {
                acc.add(item);
                System.out.println("item: " + item);
                System.out.println("acc+ : " + acc);
                System.out.println("BiFunction");
                return acc;
            }
        }, new BinaryOperator<ArrayList<Integer>>() {
            @Override
            public ArrayList<Integer> apply(ArrayList<Integer> acc, 
                ArrayList<Integer> item) {
                System.out.println("BinaryOperator");
                acc.addAll(item);
                System.out.println("item: " + item);
                System.out.println("acc+ : " + acc);
                System.out.println("--------");
                return acc;
            }
        });
System.out.println("accResult_: " + accResult_);

传递给第一个参数是ArrayList,在第二个函数参数中打印了“BiFunction”,
而在第三个参数接口中打印了函数接口中打印了”BinaryOperator“.
可是,看打印结果,只是打印了“BiFunction”,而没有打印”BinaryOperator“,
说明第三个函数参数并没有执行。这里我们知道了该变形可以返回任意类型的数据。
对于第三个函数参数,为什么没有执行,刚开始的时候也是没有看懂到底是啥意思呢,
而且其参数必须为返回的数据类型?看了好几遍文档也是一头雾水。
在 java8 reduce方法中的第三个参数combiner有什么作用?这里找到了答案,
Stream是支持并发操作的,为了避免竞争,对于reduce线程都会有独立的result,
combiner的作用在于合并每个线程的result得到最终结果。
这也说明了了第三个函数参数的数据类型必须为返回数据类型了。

5.3.8 对收集器的归一化处理

如果想为自己领域内的类定制一个收集器,不妨考虑一下其他替代方案。
最容易想到的方案是构建若干个集合对象,作为参数传给领域内类的构造函数
如果领域内的类包含多种集合,这种方式又简单又适用。

另外可以使用reducing收集器,它为流上的归一操作提供了统一实现。

5.4 一些细节

Java8对Map类的一个改变。
引入了一个新方法,computeIfAbsent,
该方法接收一个Lambda表达式,值不存在时使用该Lambda表达式计算新值。

//例5-32使用computeIfAbsent 缓存
public Artist getArtist(String name) {
    return artistCache.computeIfAbsent(name, this::readArtistFromDB);
}
//例5-33 一种丑陋的迭代Map 的方式
Map<Artist, Integer> countOfAlbums = new HashMap<>();
for(Map.Entry<Artist, List<Album>> entry : albumsByArtist.entrySet()) {
    Artist artist = entry.getKey();
    List<Album> albums = entry.getValue();
    countOfAlbums.put(artist, albums.size());
}

统计每一个艺术家专辑的数量,从Map<Artist,List> -> Map<Artist, Integer>
Java8中的forEach,接受一个BiConsumer对象作为参数,
用这个重写的代码如下

5-34 使用内部迭代遍历Map 里的值
Map<Artist, Integer> countOfAlbums = new HashMap<>();
albumsByArtist.forEach((artist, albums) -> {
    countOfAlbums.put(artist, albums.size());});

5.5 要点回顾

  • 方法引用是一种引用方法的轻量级语法,形如:ClassName::methodName。
  • 收集器可用来计算流的最终值,是 reduce 方法的模拟。
  • Java 8 提供了收集多种容器类型的方式,同时允许用户自定义收集器。

《Java8函数式编程》学习笔记 - 4.1

类库

4.1 在代码中使用Lambda表达式

前面讨论了如何编写Lambda表达式,接下来阐述的是如何使用Lambda表达式

//例4-1 使用isDebugEnabled方法降低日志性能开销
Logger logger = new Logger();
if (logger.isDebugEnabled()) {
    logger.debug("Look at this: " + expensiveOperation());
}

直接调用debug方法能够省去记录文本信息,<-省去记录文本信息这句话怎么理解?
但还是会调用expensiveOperation方法,因此用if语句显式判断,可以让程序跑的更快。

如果用lambda表达式,生成一条用作日志信息的字符串。
只有日志级别在调试或以上级别时,才会执行该lambda表达式。

// 例4-2 使用Lambda表达式简化日志代码
Logger logger = new Logger();
logger.debug(() -> "Look at this: " + expensiveOperation());

问:相应的在Logger类中该debug方法应该如何实现?
答:从类库的角度看(从类库的角度看这句话本身怎么理解?)
使用内置的Supplier(生产者)函数接口,它只有一个get方法。
然后通过调用isDebugEnabled判断是否需要记录日志,是否需要调用get方法。

//启用Lambda表达式实现的日志记录器
public void debug(Supplier<String> message) {
    if (isDebugEnabled()) {
        debug(message.get());
    }
}

调用get方法,相当于调用传入的Lambda表达式。
- 这种方式也能和匿名内部类一起工作。
- 如果用户暂时无法升级到Java 8,这种方式可以实现向后兼容。

4.2 基本类型

  • 如果只有一个可能的目标类􀅖 型,由相应函数接口里的参数类型推导得出;
  • 如果有多个可能的目标类型,由最具体的类型推导得出;
  • 如果有多个可能的目标类型且最具体的类型不明确,则需人为指定类型。

int和Integer,前者是基本类型,后者是装箱类型。
Java 的泛型是基于对泛型参数类型的擦除
↓针对这句话的理解,可以看下面这篇博客。
Java 泛型,你了解类型擦除吗?
装箱类型是对象,因此在内存中存在额外开销。
比如整型在内存中占四个字节,整型对象却要占用16字节。
将基本类型转换成装箱类型,称为装箱,反之则成为拆箱。两者也都需要额外的开销

为了减小这些性能开销,Stream类的某些方法对基本类型和装箱类型做了区分。
Java8中,仅对整型,长整型,和双浮点型做了特殊处理。

顺便理解了一下,byte类型(一个字节)的范围是-128~127,而不是-127~127的原因。
Java中,为什么byte类型的取值范围为-128~127?
⇒大学里的基础知识,真的是忘得一干二净。

对基本类型做特殊处理的方法,在命名上有明确的规范。

  • 如果方法返回类型为基本类型,则在基本类型前加To,如ToLongFunction
  • 如果参数是基本类型,则不加前缀只需类型名即可,如LongFuntion
  • 如果高阶函数使用基本类型,则在操作后加后缀To在加基本类型,如mapToLong

这些基本类型都有与之对应的Stream,以基本类型名为前缀,如LongStream。←没找到这个类型
mapToLong的方法返回的不是一个一般的Stream,是一个特殊处理的Stream。
在这个特殊的Stream中,map方法的实现方式也不同。
它接受一个LongUnaryOperator函数,将一个长整型值映射成另一个长整型值。
通过一些高阶函数装箱方法,如mapToObj,也可以从一个基本类型的Stream得到一个装箱后的Stream,如Stream←这段话还是要好好理解。

//例4-4使用summaryStatistics方法统计曲目长度
public static void printTrackLengthStatistics(Album album) {
    IntSummaryStatistics trackLengthStats
        = album.getTracks()
            .mapToInt(track -> track.getLength())
            .summaryStatistics();
    System.out.printf("Max: %d, Min: %d, Ave: %f, Sum: %d",
        trackLengthStats.getMax(),
        trackLengthStats.getMin(),
        trackLengthStats.getAverage(),
        trackLengthStats.getSum());
}

mapToInt,返回每首曲目的长度。
因为该方法返回一个IntStream对象,包含一个summaryStatistics方法,
这个方法能计算出各种各样的统计数据,返回成IntSummaryStatistics类型。

4.3 重载解析

BinaryOperator 是一种特殊的BiFunction 类型,参数的类型和返回值的类型相同。比如, 两个整数相加就是一个BinaryOperator。

java的重载,会自动挑出最具体的类型。 Lambda表达式的类型就是对应的函数接口类型,因此Lambda表达式作为参数传递时,情况也依然如此。

操作时可以重载一个方法,分别接受BianaryOperatorhe该接口的一个子类型作为参数。

//例4-7 另外一个重载方法调用
overloadedMethod((x, y) -> x + y);
//例4-8 两个重载方法可供选择
private interface IntegerBiFunction extends BinaryOperator<Integer> {
}
private void overloadedMethod(BinaryOperator<Integer> Lambda) {
    System.out.print("BinaryOperator");
}
private void overloadedMethod(IntegerBiFunction Lambda) {
    System.out.print("IntegerBinaryOperator");
}
//例4-9重载方法导致的编译错误
overloadedMethod((x) -> true);
private interface IntPredicate {
    public boolean test(int value);
}
private void overloadedMethod(Predicate<Integer> predicate) {
    System.out.print("Predicate");
}
private void overloadedMethod(IntPredicate predicate) {

什么叫“代码异味”:需要调查一下

4.4 @FunctionalInterface

每个用作函数接口的接口都应该添加这个注释。

Java中有一些接口,虽然只有一个方法,但并不是为了使用Lambda表达式来实现的。
比如有些对象内部可能保存着某种状态,使用带有一个方法的接口可能纯属巧合。
java.lang.Comparable 和java.io.Closeable 就属于这样的情况。

如果一个类是可比较,意味着在该类的实例之间存在着某种顺序。
Lambda表达式应该是既没有属性,也没有状态的一个闭包。
如果一个东西既没有属性也没有状态,拿什么比较呢?

同理一个可关闭的独享必须持有某种打开的资源,譬如一个需要关闭的文件句柄。
同样,该接口也不能是一个纯函数。因为关闭资源是更改状态的另一种形式。

和Closeable和Comparable接口

4.5 二进制接口的兼容性

4.6 默认方法

4.7 多重继承

4.8 权衡

4.9 接口的静态方法

4.10 Optional

4.11 要点回顾

《Java8函数式编程》学习笔记 - 第三章 流

第三章 流

Java8中对核心类库的改进主要包括集合类的API和新引入的流(Stream)

3.1 从外部迭代到内部迭代

样板代码的理解:是为了完成了某种操作的固定需要写的代码,譬如写循环时的for,bean的getter和setter

外部迭代

for循环起始就是一个外部迭代的过程。
首先调用Iterator方法产生一个新的Iterator对象,进而控制整个迭代过程,这就是外部迭代。

int count = 0;
Iterator<Artist> iterator = allArtists.iterator();
while(iterator.hasNext()) {
    Artist artist = iterator.next();
    if (artist.isFrom("London")) {
        count++;
    }
}

外部迭代的问题:
1. 很难受喜爱能够出不同的操作 2. 本质上是一种串行化操作。 3. 使用for循环会将行为和方法混为一谈。

内部迭代
long count = allArtists.stream()
    .filter(artist -> artist.isFrom("London"))
    .count();

Stream 是用函数式编程方式在集合类上进行复杂操作的工具。
分解为2步操作:

  1. 找出所有来自伦敦的艺术家
  2. 计算出他们的人数

每种操作都对应Stream接口的一个方法。
filter:这里是指“只保留通过某项测试的对象”,测试由一个函数完成。
由于StreamAPI的函数式编程风格,我们并没有改变集合,只是对Stream里的元素增加了一个描述。
count:count()方法计算给定的Stream里包含多少个对象。

3.2 实现机制

通常Java中调用一个方法,计算机会立即执行操作。
Stream里的一些方法略有不同,他们不是返回一个新的集合,而是创建新的集合的描述

long count = allArtists.stream()
    .filter(artist -> artist.isFrom("London"));

这行代码并不做什么实际工作,只是对本来的Stream增加了描述。
像filter这样只描述Stream,最终不产生新集合的方法,叫做惰性求值方法。
而像count这样最终会从Stream产生值的方法叫做及早求值方法。
判断是惰性求值方法还是及早求值方法很简单:只需要看返回值是不是Stream类型。

使用这些操作的理想方式就是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果,这正是它的合理之处。

整个过程和建造者模式有共通之处。建造者模式使用一系列操作设置属性和配置,
最后调用一个build 方法,这时,对象才被真正创建。

3.3 常用的流操作

下面流操作都是针对集合的操作,所以缺省可以看作是遍历了集合中的每个元素。

3.3.1 collect

collect(toList()) 方法由Stream 里的值生成一个列表,是一个及早求值操作。

List<String> collected = Stream.of("a", "b", "c") .collect(Collectors.toList()); 
assertEquals(Arrays.asList("a", "b", "c"), collected); 
3.3.2 map

如果有一个函数可以将一种类型的值转换成另外一种类型,
map操作就可以使用该函数,将一个流中的值转换成一个新的流。
使用for 循环将字符串转成大写。

List<String> collected = new ArrayList<>();
for (String string : asList("a", "b", "hello")) {
    String uppercaseString = string.toUpperCase();
    collected.add(uppercaseString);
}

使用map 操作将字符串转换为大写形式

List<String> collected = 
    Stream.of("a", "b", "hello")
        //Stream里的每个元素遍历,并转成大写。
        //传给map的Lambda表达式只接受一个String类型的参数,返回一个新的String
        .map(string -> string.toUpperCase())
        .collect(toList());
assertEquals(asList("A", "B", "HELLO"), collected);

给map的Lambda表达式的参数和返回值不必属于同一种类型,但是Lambda表达式必须是Function接口的一个实例
map的函数接口是Function

3.3.3 filter

遍历数据并检查其中的元素时,可尝试使用Stream 中提供的新方法filter

List<String> beginningWithNumbers =
    Stream.of("a", "1abc", "abc1")
        .filter(value -> isDigit(value.charAt(0)))
        .collect(toList());

跟map一样,接受一个返回值肯定是true或者false的函数作为参数,
经过过滤,Stream中符合条件的,即Lambda表达式值为true的元素被保留下来。
filter的函数接口是Predicate

3.3.4 flatMap

flatMap 方法可用Stream替换值,然后将多个Stream 连接成一个Stream

3.3.5 max和min
List<Track> tracks = asList(new Track("Bakai", 524),
                            new Track("Violets for Your Furs", 378),
                            new Track("Time Was", 451));
Track shortestTrack = tracks.stream()
                            //Stream里的每个元素遍历,并比较length
                            .min(Comparator.comparing(track -> track.getLength()))
                            //取了最短的Track之后,把整个Track取出来
                            .get();
assertEquals(tracks.get(1), shortestTrack);

为了让Stream 对象按照曲目长度进行排序,需要传给它一个Comparator 对象。
Java 8 提供了一个新的静态方法comparing,使用它可以方便地实现一个比较器。
放在以前,我们需要比较两个对象的某项属性的值,现在只需要提供一个存取方法就够了。
本例中使用getLength方法。

3.3.6 通用模式

max,min,以及上述找最短曲目,都用得是通用模式。下面的伪代码表示的就是通用模式

//reduce模式
Object accumulator = initialValue;
for(Object element : collection) {
    accumulator = combine(accumulator, element);
}
3.3.7 reduce

reduce操作可以实现从一组值中生成一个值。 使用reduce方法的求和如下

int count = Stream.of(1, 2, 3)
    .reduce(0, (acc, element) -> acc + element);
System.out.println(count);

但是感觉很繁琐,一般就是一个sum就可以解决了的事情。

BinaryOperator<Integer> accumulator = (acc, element) -> acc + element;
int count = accumulator.apply(
accumulator.apply(accumulator.apply(0, 1),2),3);
3.3.8整合操作

是否需要对外暴露一个List或者Set对象?可能一个Stream工厂才是最好的选择。
通过Stream暴露集合最大的优点在于,很好地封装了内部实现的数据结构。
仅暴露一个Stream接口,用户在实际操作中无论如何使用,都不会影响内部List或Set。

3.4 重构遗留代码

重构时可能遇到返回List或者Set类型,只要调用.stream()方法就可以转成Stream类型继续操作。

3.5 多次调用流操作

Stream的使用习惯,以及其长处,就是其链式调用。

3.6 高阶函数

高阶函数是指接受另外一个函数作为参数,或返回一个函数函数。 高阶函数不难辨认:看函数签名就够了。如果函数的参数列表里包含函数接口,或该函数返回一个函数接口,那么该函数就是高阶函数

3.7 正确使用Lambda表达式

明确了要达成什么转化,而不是说明如何转化。
这种方式写出来的代码,潜在的缺陷更少,更直接表达出了程序员的意图。

明确要达成什么转化,而不是说明如何转化的另外一层含义在于写出的函数没有副作用。
这点非常重要,这样只通过函数的返回值就能充分理解函数的全部作用。

3.8 要点回顾

  • 内部迭代将更多控制权交给了集合类。
  • Iterator类似,Stream是一种内部迭代方式。
  • 将Lambda表达式和Stream上的方法结合起来,可以完成很多常见的集合操作。

《Java8函数式编程》学习笔记 - 第一章,第二章

第一章 简介

1.1 为什么需要再次修改Java

开发类库的程序员使用Java时,发现抽象级别还不够,
处理大型数据集合,Java还欠缺高效的并行操作。
使用Java8可以编写复杂的集合处理算法, 只需要简单修改一个方法,就能让代码在多核CPU上高效运行。

为了编写这类处理批量数据的并行类库,
需要在语言层面上修改现有的Java:增加Lambda表达式。

  • 面向对象编程是对数据进行抽象,
  • 函数式编程是对行为进行抽象。

函数式编程并不是性能优先的代码,但带来的好处是比较明显。
代码更容易阅读,更多地表达了业务逻辑的意图,而不是实现机制。
特别是在写回调函数事件处理程序时,不必再纠缠于内部类的冗繁和可读性

1.2 什么是函数式编程

使用不可变值函数函数对一个值进行处理,映射成另一个值。

1.3 示例

创建了,Artist类,Track类,Album类

第二章 Lambda表达式

Java8的最大变化就是引入了Lambda表达式,一种紧凑的,传递行为的方式。

2.1 第一个Lambda表达式

//例2-1 使用匿名内部类将Action和按钮点击进行关联  
button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        System.out.println("button clicked");
    }
});

这实际上是一个代码即数据的例子--我们给按钮传递了一个代表某种行为的对象
设计匿名内部类的目的,就是为了方便将代码作为数据传递。

匿名内部类的问题:

  1. 不够简单,上述就有4行样板代码。(打印以外的4行)
  2. 难读,不想传入对象,只想传入代码。

在java8中可以改写成下面的lambda表达式

//例2-2 使用Lambda表达式将行为和按钮单击进行关联
button.addActionListener(event->
        System.out.println("button clicked"););

和传入一个实现某接口的对象不同,我们传入了一段代码块,一个没有名字的函数

  • event是参数名,和上面匿名内部类是同一个参数
  • “->”,将参数和lambda表达式的主体分开,
  • 主体,是用户点击按钮时会运行的一些代码。

和使用匿名内部类的另一处不同在于声明event 参数的方式。
内部类需要显式声明,而lambda表达式通过类型推断,无需显式声明。

尽管与之前相比,Lambda表达式中的参数需要的样板代很少,
但是Java8仍然是一种静态类型语言

2.2如何辨别Lambda表达式

Runnable noArguments = () -> System.out.println("Hello World");

该Lambda表达式实现了Runnable接口。 Runnable接口也只有一个run方法,没有参数,且返回类型为void。

ActionListener oneArgument = event -> System.out.println("button clicked");

该Lambda表达式实现了ActionListener接口 ActionListener接口只有一个actionPerformed方法,而actionPerformed方法只有ActionEvent一个参数。 因此这里只需要"event"一个参数,并且通过系统的类型推断,无需显式声明。

Runnable multiStatement = () -> {
    System.out.print("Hello");
    System.out.println(" World");
};

该Lambda表达式同样实现了Runnable接口。
Lambda表达式的主体不仅可以是一个表达式,也可以是一个闭包。

BinaryOperator<Long> add = (x, y) -> x + y;

这行代码并不是将两个数字相加,而是创建了一个函数,用来计算两个数字相加的结果。

目标类型是指Lambda 表达式所在上下文环境的类型。
比如,将Lambda 表达式赋值给一个局部变量,或传递给一个方法作为参数,
局部变量或方法参数的类型就是Lambda 表达式的目标类型。

2.3 引用值,而不是变量

//例2-5 匿名内部类中使用final 局部变量
final String name = getUserName(); 
button.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent event) {
        System.out.println("hi " + name);
    }
});

如上例所示,匿名内部类中用到变量name时,需要将name声明为final。
Lambda表达式虽然可以引用非final变量,但该变量在既成事实上必须是final。
既成事实上的final是指只能给该变量赋值一次。
换句话说,Lambda表达式引用的是值,而不是变量

例如

String name = getUserName();
button.addActionListener(event -> System.out.println("hi " + name));

给变量多次赋值,然后在Lambda表达式中引用它,编译器就会报错。

String name = getUserName();
name = formatUserName(name);
button.addActionListener(event -> System.out.println("hi " + name));

显示出错信息:local variables referenced from a Lambda expression must be final or effectively final
总结:Lambda表达式都是静态类型

接口单一方法的命名并不重要,只要方法签名和Lambda表达式的类型匹配即可。

2.4 函数接口

函数接口是只有一个抽象方法的接口,用作Lambda表达式的类型

public interface ActionListener extends EventListner {
    public void actionPerformed(ActionEvent event);
}

Java1.8提供了不少重要的函数接口 Consumer,Supplier,Function<T,R>,Predicate

2.5 类型推断

Java 7 中的菱形操作符

Map<String, Integer> oldWordCounts = new HashMap<String, Integer>();
Map<String, Integer> diamondWordCounts = new HashMap<>();

Java 7中我们可以省略构造函数的泛型类型。
Java 8中更进一步,可以省略Lambda表达式中的所有参数类型。

Java 8 中对类型推断系统的改善值得一提。上面的例子将new HashMap<>()传给useHashmap方法, 即使编译器拥有足够的信息,也无法在Java 7 中通过编译。

正:BinaryOperator<Long> addLongs = (x, y) -> x + y;
误:BinaryOperator add = (x, y) -> x + y; 编译器给出的报错信息如下:
Operator '& #x002B;' cannot be applied to java.lang.Object, java.lang.Object.
其实就是浏览器认不出x,y的类型,x,y被默认为Object,而Object对象是没有+操作符的,因此报错了。

要点

  • Lambda 表达式是一个匿名方法,将行为像数据一样进行传递。
  • Lambda表达式的常见结构:BinaryOperator add = (x, y) → x + y。
  • 函数接口指仅具有单个抽象方法的接口,用来表示 Lambda表达式的类型。