Java8 9 lambda 学习笔记
/** * 引用类型的赋值是将原来的地址给了另一个引用,它们指向的地址是同一个,如果修改了其中一个,那么就会影响另一个。 * 如果我们需要切断两者的联系,可以采用 “复制构造函数的”的办法 * */public static void main(String[] args) { Person before = new Person("fcy"); Listpeople = Stream.of(before).collect(Collectors.toList()); Person after = people.get(0); if (after==before){ System.out.println("after==before"); } before.setName("abc"); if (after.getName().equals("abc")){ System.out.println("equal"); }}
/** * 复制构造函数 * 这里由于调用了map(Person::new),相当于返回了一个新的Person对象,所以和上面完全相反 * */public static void main(String[] args) { Person before = new Person("fcy"); Listpeople = Stream.of(before).map(Person::new).collect(Collectors.toList()); Person after = people.get(0); if (after==before){ System.out.println("after==before"); } if (after.equals(before)){ System.out.println("after.equals(before)"); } before.setName("aaa"); if (after.equals(before)){ System.out.println("after.equals(before)"); }}
Java8在接口中提供了默认方法和静态方法的实现方法,默认方法的实现只需要给出关键在default,然后提供相应的实现即可;静态方法的实现只需给出关键字static,然后提供相应实现。
为什么Java8提供默认方法?为了向后兼容,不破坏之前的这个接口。需要注意,接口的静态方法是不需要实现接口的,直接拿来用即可,同静态类一样。 3.java.util.function包中的接口分为四类,Consumer(消费型接口)、Supplier(供给型接口)、Predicate(谓词型接口)、Function(功能型接口)。
- Conusmer接口
// 传入一个参数,不返回任何值void accept(T t);default ConsumerandThen(Consumer after)
最常见的forach方,ifPresent法,这里传入的是函数,但在java中函数是对象
default void forEach(Consumer action)public void ifPresent(Consumer consumer)
应用举例:
string.forEach(System.out::println);
其他Consumer接口如下:
| 接口 | 抽象方法 | |--|--| |IntConsumer |void accept(int x) | | DoubleConsumer | void accept(double x) | | LongConsumer |void accept(long x) | | BiConsumer | void accept(T t, U u)| 这里的BiConsumer还有三种变体 | 接口 |抽象方法 | |--|--| | ObjIntConsumer | void accept(T t, int value); | |ObjDoubleConsumer | void accept(T t, double value); | | ObjLongConsumer | void accept(T t, long value); |- Supplier接口
// 不传入参数,返回一个类型T get();
Math.random方法,不传入参数就能返回double类型数据
// java8提供的一个supplier接口public interface DoubleSupplier { double getAsDouble();}
这么使用
DoubleSupplier randomSupplier=new DoubleSupplier();randomSupplier=()->Math::random;
应用举例:
public static void main(String[] args) { Liststrings = Arrays.asList("a", "b", "c", "d", "e", "f", "g"); Optional first = strings.stream().filter(s -> s.startsWith("a")).findFirst(); System.out.println(first); System.out.println(first.orElse("none")); System.out.println(first.orElse(get("s1"))); System.out.println(first.orElseGet(()->get("s2")));}private static String get(String name){ System.out.println(name+"执行了"); return "any";}
分析:这里重点关注orElse和orElseGet两个方法,对于orElse方法,无论Optional中是否有值,它都会执行get方法,而orElseGet方法只有在Optional的值为空时才会去执行get方法。虽然最后返回的结果是一样的,但是这两个方法内部执行是不同的,需要注意!
- Predicate接口
// 传入一个参数,返回true or falseboolean test(T t);default Predicateand(Predicate other)default Predicate negate()default Predicate or(Predicate other)static Predicate isEqual(Object targetRef)
常见的如Stream下的filter方法
Streamfilter(Predicate predicate);
应用举例
public static void main(String[] args) { String[] names = {"java","kotlin","python"}; int length=6; //String nameOfLength = getNameOfLength(length, names); String nameOfLength = getNameSatisfyingCondition(s->s.length()==length, names); System.out.println(nameOfLength);}public static String getNameOfLength(int length, String... names){ return Arrays.stream(names).filter(s->s.length()==length).collect(Collectors.joining(", "));}public static String getNameSatisfyingCondition(Predicatecondition, String... names){ return Arrays.stream(names).filter(condition).collect(Collectors.joining(", "));}
getNameOfLength方法已经将lambda表达式封装在了函数体内,这样重用性不好。getNameSatisfyingCondition方法可以通过传入不同的谓词(lambda表达式)实现不同的功能。如果谓词过长,可以使用常量替代。
我对谓语的理解就是:谓语就是一种行为,行为就是一种动作,行为的重点在于做什么,而不在于怎么做。
- Function接口
// 传入一个参数,返回一个参数R apply(T t);defaultFunction compose(Function before)default Function andThen(Function after)static Function identity()
常见的如map方法
Stream map(Function mapper);
应用举例:
public static void main(String[] args) { Liststrings = Arrays.asList("a", "ab", "abc", "abcd"); List collect = strings.stream().map(String::length).collect(Collectors.toList()); collect.forEach(System.out::println);}
4.
java8引入新的流式隐喻,流是一种元素序列,它不存储元素,也不会修改原始源。java的函数编程通过一系列流水线的中间操作(intermediate operation)传递元素,并利用终止表达式(terminal expression)完成这一过程。
流只使用一次,流在经过零个或多个中间操作并达到终止操作后就会结束。 流是惰性的,只有终止操作才会处理数据。
- 创建流的方法:
- Stream.of(T t)或Stream.of(T... values)
- Arrays.stream(T[] array)
- Stream.iterate(T seed, UnaryOperator f)
- Stream.generate(Supplier s)
- Collection.Steam() 举几个例子:
// 该方法返回一个无限顺序的有序流,由函数f产生,UnaryOperator是一个输入输出参数类型相同的函数接口public staticStream iterate(final T seed, final UnaryOperator f)
下面的例子打印当前日期开始的后面10天
Stream.iterate(LocalDate.now(),ld->ld.plusDays(1L)).limit(10).forEach(System.out::println);
// generate方法通过多次调用Supplier产生一个顺序的无序流public staticStream generate(Supplier s)
下面的例子创建随机流
Stream.generate(Math::random).limit(10).forEach(System.out::println);
注意上面的例子都是无限流,需要通过limit来限制
6.对于基本类型流,无法像使用字符串流一样直接转化成集合。但是可以通过以下几种方式解决:
- 使用boxed方法
Listlist = IntStream.of(3, 1, 4, 1, 5, 9).boxed().collect(Collectors.toList());
- 使用mapToObj方法
IntStream.of(3, 1, 4, 1, 5, 9).mapToObj(Integer::valueOf).collect(Collectors.toList());
- 使用collect方法的三参数形式
R collect(Supplier supplier, ObjIntConsumer accumulator, BiConsumer combiner);
IntStream.of(3, 1, 4, 1, 5, 9).collect(ArrayList::new,ArrayList::add,ArrayList::addAll);
java的函数式范式经常采用“映射-筛选-规约”(map-filter-reduce)的过程处理数据。首先,map操作将一种类型的流转换为另一种类型(如通过调用length方法将String流转换成int流)。接着,filter操作产生一个新的流,它仅包含所需的元素。最后,通过终止操作从流中生成单个值。
常见规约操作: average、count、max、min、sum、sunmaryStatistics、collect、reduce 如果流中没有元素,结果为空,以上提到的方法将返回Optional
使用reduce方法求和:
int sum = IntStream.rangeClosed(1, 10).reduce((x, y) -> x + y).orElse(0);
请看下面这段代码:
IntStream.rangeClosed(1,10).reduce((x, y)->{ System.out.printf("x=%d, y=%d%n", x, y); return x+y;}).orElse(0);打印:x=1, y=2x=3, y=3x=6, y=4x=10, y=5x=15, y=6x=21, y=7x=28, y=8x=36, y=9x=45, y=10
在lambda表达式中,可以将二元运算符的第一个参数看做累加器,第二个参数看做流中每个元素的值,从上面的打印就能看出。
但是如果现在需要将每个数字增加一倍后再求和,该怎么写? 下面这种写法是错误的:int sum = IntStream.rangeClosed(1, 10).reduce((x, y) -> x + 2 * y).orElse(0);System.out.println(sum); // 109
为什么打印出来是109,而不是110呢?因为x和y的初始值是1和2,y是从2开始的,而不是1,所以结果不对。
改成下面的形式就可以了:int sum = IntStream.rangeClosed(1, 10).reduce(0,(x, y) -> x + 2 * y);System.out.println(sum); // 110
这种形式的reduce的方法签名是这样的:
int reduce(int identity, IntBinaryOperator op);
第一个identity是标识值,也就是累加器内部的初始值。加法操作是0,乘法操作是1,字符串操作是""
因此也能这样使用reduce方法求和:Integer sum = Stream.of(1, 2, 3, 4, 5).reduce(0, Integer::sum);
连接字符串的方法有以下3种:
- reduce方法
String s = Stream.of("a", "b", "c").reduce("", String::concat);
- StringBuilder方法
String s = Stream.of("a", "b", "c").collect(StringBuilder::new, StringBuilder::append, StringBuilder::append).toString();
- Collections的joining方法
String s = Stream.of("a", "b", "c").collect(Collectors.joining());
- 根据长度对字符串排序
Listlist = Arrays.asList("python", "java", "kotlin");List collect = list.stream().sorted(Comparator.comparingInt(String::length)).collect(Collectors.toList());collect.forEach(System.out::println);打印:javapythonkotlin
8.字符串转换成流的方法如下:
public default IntStream codePoints()public default IntStream chars()
这两个方法是String类实现的CharSequence接口中的默认方法,可以将字符串转换成流。
public static boolean isPalindrome(String s) { String before = s.toLowerCase().codePoints() .filter(Character::isLetterOrDigit) .collect(StringBuffer::new, StringBuffer::appendCodePoint, StringBuffer::append) .toString(); String after = new StringBuilder(before).reverse().toString(); return before.equals(after);}
这里我比较疑惑的是codePoints返回的是int类型,后面是Character::isLetterOrDigit,但是查了一下源码,是有这个重载方法的,入参也是int类型的。
public static boolean isLetterOrDigit(int codePoint)
9.计数由两种方法:
- Stream的count方法
- Collections的counting()方法
10.map和flatmap的方法签名如下:
Stream map(Function mapper); Stream flatMap(Function > mapper);
map返回的是Stream<Stream>,flatmap返回的是Stream,将相当于去掉一层Stream
如果需要将每个元素转换成一个值,则使用map方法;如果需要将每个元素转换成多个值,且需要将生成的流“展平”,则使用flatmap方法。
应用举例:
Listlist1 = Arrays.asList("ab", "b", "c");List list2 = Arrays.asList("abc", "bc", "cd");List
> lists = Arrays.asList(list1, list2);List collect = lists.stream().flatMap(list -> list.stream()) // 返回Stream .filter(s->s.length()>=2) .collect(Collectors.toList());System.out.println(collect);
11.流的拼接有两种方法:
- Stream的concat方法 当流中有一个是并行流时,则结果为并行流。 应用举例:
Streamfirst = Stream.of("a", "b", "c");Stream second = Stream.of("d", "e", "f");List collect = Stream.concat(first, second).collect(Collectors.toList());System.out.println(collect);
这只用于两个的流的拼接,多个流的拼接不建议使用concat方法,因为存在栈溢出问题。
- flatmap方法拼接 多个流拼接使用flatmap方法。 应用举例:
Streamfirst = Stream.of("a", "b", "c").parallel();Stream second = Stream.of("d", "e", "f");Stream
12.将List转换成Map的方法:
应用举例Listpeople = Arrays.asList( new Person(1, "Mike", 10), new Person(2, "David", 12), new Person(3, "Smith", 20) ); // 将List转换成Map// Map collect = people.stream().collect(Collectors.toMap(Person::getId, b -> b)); Map collect = people.stream().collect(Collectors.toMap(Person::getId, Function.identity()));// Map collect = people.stream().collect(Collectors.toMap(Person::getId, UnaryOperator.identity())); System.out.println(collect);
注意到toMap方法的第二个参数有三种写法
String[] animals = {"cat", "dog", "pig", "duck", "fish", "horse"};Mapmap = Arrays.stream(animals).collect(Collectors.groupingBy(String::length, Collectors.counting()));map.entrySet().stream() .sorted(Map.Entry.comparingByKey(Comparator.reverseOrder())) .forEach(e -> System.out.printf("Length %d: %d words%n", e.getKey(), e.getValue())); 输出: Length 5: 1 words Length 4: 2 wordsLength 3: 3 words
上面这段代码根据给定单词将长度作为key,符合该长度的单词个数作为value,存储在Map中,并且Map根据key倒序排列。
注意groupingBy可以只传入一个参数String::length,这样的话返回的就不是Map<Integer, Long>,而是Map<Integer, List>,如果传入第二个参数Collectors.counting(),那么就是所谓的“下游收集器(downstream)”,会返回符合长度的单词个数,所以变成了Map<Integer, Long>14.创建不可变集合的方法
- Collections中有大量以Unmodifiable开头的静态方法,可以生成不可变的列表、集合、映射等
- Collectors中的方法collectingAndThen
// 第一个参数是下游收集器,第二个参数是终止器,用于读取每个输入元素,将他们应用到下游收集器中public staticCollector collectingAndThen(Collector downstream,Function finisher)
应用举例
public static void main(String[] args) { Listlist = createImmutableList(1, 2, 3); // 无法使用add修改list,否则报错 UnsupportedOperationException // list.add(1); System.out.println(list);}@SafeVarargspublic static final List createImmutableList(T... elements){ return Arrays.stream(elements) .collect(Collectors.collectingAndThen(Collectors.toList(),Collections::unmodifiableList));}
@SafeVarargs注解只能用在参数长度可变的方法或构造方法上,且方法必须声明为static或final,否则会出现编译错误。一个方法使用@SafeVarargs注解的前提是,开发人员必须确保这个方法的实现中对泛型类型参数的处理不会引发类型安全问题。
Java7引入的Objects类,包含了很多有用的静态方法,值得研究。
Java8在Objects中新增了两个方法isNull和nonNull
public static boolean isNull(Object obj)public static boolean nonNull(Object obj)
从泛型列表中滤掉空元素
public static void main(String[] args) { Listlist = Arrays.asList("a", null, "b", "c", null); System.out.println(getNonNullElements(list));}public static List getNonNullElements(List list){ return list.stream() .filter(Objects::nonNull) .collect(Collectors.toList());}
在lambda表达式内部访问外部的变量,这在Java8之前需要在外部变量前加上final修饰,但在Java8中不需要加final修饰,变量会自动拥有被final修饰的效果。
在Java8之前,为什么需要加final修饰的原因如下: 内部类对象的生命周期与局部变量的生命周期不一致 (1)这里所说的“匿名内部类”主要是指在其外部类的成员方法内定义的同时完成实例化的类,若其访问该成员方法中的局部变量,局部变量必须要被final修饰。原因是编译器实现上的困难:内部类对象的生命周期很有可能会超过局部变量的生命周期。 (2) 局部变量的生命周期:当该方法被调用时,该方法中的局部变量在栈中被创建,当方法调用结束时,退栈,这些局部变量全部死亡。而内部类对象生命周期与其它类对象一样:自创建一个匿名内部类对象,系统为该对象分配内存,直到没有引用变量指向分配给该对象的内存,它才有可能会死亡(被JVM垃圾回收)。所以完全可能出现的一种情况是:成员方法已调用结束,局部变量已死亡,但匿名内部类的对象仍然活着。 (3)如果匿名内部类的对象访问了同一个方法中的局部变量,就要求只要匿名内部类对象还活着,那么栈中的那些它要所访问的局部变量就不能“死亡”。 (4)解决方法:匿名内部类对象可以访问同一个方法中被定义为final类型的局部变量。定义为final后,编译器会把匿名内部类对象要访问的所有final类型局部变量,都拷贝一份作为该对象的成员变量。这样,即使栈中局部变量已经死亡,匿名内部类对象照样可以拿到该局部变量的值,因为它自己拷贝了一份,且与原局部变量的值始终保持一致(final类型不可变)。 闭包:函数以及在其环境中定义的可访问的变量。
17.Java8中Map接口新增了很多默认方法,比如computeIfAbsent、computeIfPresent等
default V computeIfAbsent(K key, Function mappingFunction)
default V computeIfPresent(K key, BiFunction remappingFunction)
以计算斐波那契数列为例,介绍computeIfAbsent的用法
public class MapDefaultMethods { private Mapcache = new HashMap<>(); public static void main(String[] args) { MapDefaultMethods test = new MapDefaultMethods(); long before = System.currentTimeMillis(); BigInteger bigInteger = test.fib(40); long after = System.currentTimeMillis(); System.out.println(bigInteger); System.out.println("用时"+(after-before)+"ms"); } // java8之前的写法 private BigInteger fibo(long i){ if (i == 0) return BigInteger.ZERO; if (i == 1) return BigInteger.ONE; return fibo(i - 2).add(fibo(i - 1)); } // java8的写法 private BigInteger fib(long i) { if (i == 0) return BigInteger.ZERO; if (i == 1) return BigInteger.ONE; return cache.computeIfAbsent(i, n -> fib(n - 2).add(fib(n - 1))); }}//使用java8之前写法,传入fibo(40),用时2000ms//使用java8的写法,传入fib(40),用时60ms
由此可见,computeIfAbsent极大提高了效率。因为computeIfAbsent会利用缓存计算,只要执行一次,之前的键值就会留下,以后用的时候直接去缓存中取就行,不需要每次都重复执行。
computeIfPresent的用法// 计算给定文本passage中出现的符合给定strings的单词的个数,用map统计public MapcountWords(String passage, String... strings) { Map wordCounts = new HashMap<>(); // 将特定单词放入映射中,并将计数器置0 Arrays.stream(strings).forEach(s -> wordCounts.put(s, 0)); Arrays.stream(passage.split(" ")) .forEach(word -> wordCounts.computeIfPresent(word, (key, val) -> val + 1)); return wordCounts;}
merge的用法
default V merge(K key, V value, BiFunction remappingFunction)
如果我们希望统计所有单词的个数,那么需要考虑两种情况:如果单词已经在映射中,那么直接更新计数器;如果单词不在映射中,则将其置于映射中,并使计数器加1
Arrays.stream(passage).forEach(word->wordCounts.merge(word,1,Integer::sum));
Optional实例是不可变的,但它包装的对象却不一定是不可变的。如果创建一个包含可变对象实例的Optional,则仍然可以对实例进行修改。
创建Optional的方法有3个:
public staticOptional empty()public static Optional of(T value)public static Optional ofNullable(T value)
在使用optional中get方法前,先判断optional中是否包含值,用isPresent方法判断。
19.Optional的映射map 假定有如下方法:public OptionalfindEmployeeById(int id)
- 根据Id查找Employee(使用Stream.map方法)
// 使用Stream.map方法public ListfindEmployeesByIds(List ids){ return ids.stream() .map(this::findEmployeeById) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toList());}
如上所示,第一个map操作的结果是一个由Optional构成的流,每个Optional要么包含一个员工,要么为空。为提取包含的值,我们采用fiter方法删除所有空Optional,然后第二个map将每个Optional映射到它们的所包含的值。
Optional的map方法定义如下:
public Optional map(Function mapper) { Objects.requireNonNull(mapper); if (!isPresent()) return empty(); else { return Optional.ofNullable(mapper.apply(value)); }}
Optional.map方法传入Function作为参数,如果Optional不为空,map方法将提取包含的值并应用给定的函数,函数返回一个包含结果的Optional;如果结果为空,返回一个空的Optional
- 根据Id查找Employee(使用Optional.map方法)
// 使用Optional.map方法public ListfindEmployeesByIds(List ids){ return ids.stream() .map(this::findEmployeeById) //Stream > .flatMap(optional->optional.map(Stream::of) //Stream >> .orElseGet(Stream::empty)) //Stream > // flatMap又将其转成Stream .collect(Collectors.toList()); //List }
20.Java9新增了一些处理流的方法
(1)新增的Stream方法public staticStream ofNullable(T t)public static Stream iterate(T seed, Predicate hasNext, UnaryOperator next)default Stream takeWhile(Predicate predicate)default Stream dropWhile(Predicate predicate)
- ofNullable方法的出现使得Stream创建流时可以不考虑参数是否为空。
- iterate方法的比较(Java8和Java9)
在Java8中,iterate创建流时,生成的是无限流,需要用limit或其他短路操作来控制返回流的大小。在Java9中,iterate方法新增了重载形式,传入Predicate作为第二个参数,只要不满足谓词,就退出。
应用举例:
// Java8实现ListbigDecimals = Stream.iterate(BigDecimal.ZERO, bd -> bd.add(BigDecimal.ONE)) .limit(10) .collect(Collectors.toList()); // Java9实现List collect = Stream.iterate(BigDecimal.ZERO, bd -> bd.longValue() < 10L, bd -> bd.add(BigDecimal.ONE)) .collect(Collectors.toList());
- takeWhile和dropWhile方法
takewhile方法从流的起始位置开始,返回“匹配给定谓词的元素的最长前缀”。dropWhile方法的作用刚好相反,丢弃最长前缀后,返回其余元素。
两个方法都是在同一个位置将流拆分,不过takeWhile返回拆分位置前的元素(不包括首先不匹配的那个元素),而dropWhile返回拆分位置后的元素(包括首先不匹配的那个元素)。
(2)下游收集器filtering和flatMapping
(3)Optional新增stream、or、ifPresentOrElse方法- stream方法
如果值存在,stream方法返回一个包含该值得流,如果值不存在,stream方法返回一个空流,因此可以改造之前的写法。
之前的写法
public ListfindEmployeesByIds(List ids){ return ids.stream() .map(this::findEmployeeById) .filter(Optional::isPresent) .map(Optional::get) .collect(Collectors.toList());}// 或者public List findEmployeesByIds(List ids){ return ids.stream() .map(this::findEmployeeById) //Stream > .flatMap(optional->optional.map(Stream::of) //Stream >> .orElseGet(Stream::empty)) //Stream > // flatMap又将其转成Stream .collect(Collectors.toList()); //List }
现在的写法
public ListfindEmployeesByIds(List ids){ return ids.stream() .map(this::findEmployeeById) .flatMap(Optional::stream) .collect(Collectors.toList());}
- or方法
or方法和orElse方法的区别是,or方法返回的是Optional类型,而orElse方法返回的是T类型
- ifPresentOrElse方法
ifPresentOrElse方法与ifPresent的区别在于,ifPresent方法只有当Optional不为空才会执行,而ifPresentOrElse方法可以支持Optional为空时,执行其他操作。
应用举例
public void printUser(Integer id){ findUserById(id).ifPresentOrElse(System.out::println, ()->System.out.println("User not found"));}
User存在就打印,否则打印User not found