JAVA8新特性 函数式编程Lambda

JSR(Java Specification Requests) 335 = lambda表达式+接口改进(默认方法)+批量数据操作

一、函数式编程
以处理数据的方式处理代码,这意味着函数可以被赋值给变量,传递给函数等,函数是第一等级的值
入门例子:

[java] view plain copy

  1. import java.util.List;  
  2. public class Test1 {  
  3.     public String cleanNames(List<String> listOfNames) {  
  4.         StringBuffer sb = new StringBuffer();  
  5.         // 只能顺序处理List中的元素,每次都要执行一次筛选,转化和reduce(join)  
  6.         for(int i = 0; i < listOfNames.size(); i++) {  
  7.             if(listOfNames.get(i).length() > 1) {  
  8.                 sb.append(capitalizeString(listOfNames.get(i))).append(",");  
  9.             }  
  10.         }  
  11.         return sb.substring(0, sb.length() - 1).toString();  
  12.     }  
  13.   
  14.     public String capitalizeString(String s) {  
  15.         return s.substring(0, 1).toUpperCase() + s.substring(1, s.length());  
  16.     }  
  17. }  

Scala/Groovy/Clojure是JVM平台上的编程语言,都支持函数式编程,以下以Scala语言来函数式改写上面的代码:

[java] view plain copy

  1. val employees = List("dsds", "d", "gghh", "kmo");  
  2. val result = employees  
  3. .filter(_.length() > 1)  
  4. .map(_.capitalize)  
  5. .reduce(_ + "," + _)   

将过滤,转化和聚合三个行为分开,表面上看,上面的代码貌似先遍历列表挑出所有符合条件的元素传给map,map接着遍历并处理从filter传来的结果,最后将处理后的结果都传给reduce,reduce接着遍历处理。数了数貌似要经历过好多次遍历呢,但是其具体的实现并非如此,因为其有“惰性”,它的具体实现行为和上面JAVA for-loop实现基本一致,只是函数式更直观,更简洁而已。

静态语言 VS 动态语言:http://www.zhihu.com/question/19918532

JAVA的函数式编程(Lambda):
当我们用匿名内部类为某个接口定义不同的行为时,语法过于冗余,lambda提供了更简洁的语法。
- 函数式接口 Functional interfaces
每一个函数对象都对应一个接口类型。函数式接口只能拥有一个抽象方法,编译器会根据接口的结构自行判断(判断过程并不是简单的对接口方法计数,因为该接口可能还定义了静态或默认方法)。API作者也可以通过@FunctionalInterface注解来指定一个接口为函数式接口,之后编译器就会验证该接口是否满足函数式接口的要求。

JAVA SE 7中已经存在的函数式接口,例如:
- java.lang.Runnable
- java.util.concurrent.Callable
- java.util.Comparator
- java.io.FileFilter

JAVA SE 8中新增加了一个包 java.util.function,包含了常用的函数式接口,例如:
- Predicate<T> -- boolean test(T t);
- Consumer<T> -- void accept(T t);
- Function<T, R> -- R apply(T t);
- Supplier<T> -- T get();

- BiFunction<T, U, R> --  R apply(T t, U u);

lambda表达式例子:
x -> x+1 
() -> {System.out.println("hello world");}

(int x, int y) -> { x++; y--; return x+y;}

Function<Integer, Integer> func = (x, Integer y) -> x + y //wrong
Function<Integer, Integer> func = Integer x -> x + 1 //wrong


当且仅当下面所有条件均被满足时,lambda表达式才可以被赋给目标类型T:
- T是一个函数式接口
- lambda表达式的参数和T的方法参数在数量和类型上一一对应
- lambda表达式的返回值和T的方法返回值相兼容
- lambda表达式内所抛出的异常和T的方法throws类型想兼容
lambda表达式的参数类型可以从目标类型中得出的话就可以省略。
目标类型上下文:
- 变量声明
- 赋值
- 返回语句
public Runnable PrintSth() {
return () -> {
System.out.println("sth");
};

}

Comparator<String> c;

c = (String s1, String s2) -> s1.compareToIgnoreCase(s2);

- 数组初始化器
new FileFilter[] {
f -> f.exists(), f -> f.canRead(), f -> f.getName().startsWith("s")

}

- 方法和构造方法的参数
List<Person> ps = ...;
Stream<String> names = ps.stream().map(p -> p.getName());
map的参数是Function<T, R>,p可以从List<Person>推导出T为Person类型,而通过Stream<String>可以知道R的类型为String,当编译器无法解析出正确lambda类型的时候,可以:
1. 为lambda参数提供显示类型
2. 将lambda表达式转型为Function<Person, String>

3. 为泛型参数R提供一个实际类型,比如 .<String>map(p->p.getName())

- lambda表达式函数体
- 条件表达式(?:)
- 转型表达式
Supplier<Runnable> c = () -> () -> {System.out.println("hi");};
Callable<Integer> c = flag ?  (() -> 23) : (() -> 42);
Object o = () -> {System.out.println("hi");}; //非法
Object o = (Runnable) () -> {System.out.println("hi");};

词法作用域:
lambda不会引入一个新的作用域。
public class Hello {
  Runnable r1 = () -> { System.out.println(this); };
  Runnable r2 = () -> { System.out.println(toString()); };
  public String toString() {  return "Hello, world"; }
  public static void main(String... args) {
    new Hello().r1.run();
    new Hello().r2.run();
  }

}

Output: 

Hello, world

Hello, world


lambda不可以掩盖其所在上下文中的局部变量,类似:
int i = 0;
int sum = 0;
for (int i = 1; i < 10; i += 1) { //这里会出现编译错误,因为i已经在for循环外部声明过了
  sum += i;
}
 

变量捕获:

[java] view plain copy

  1. import java.util.function.Function;  
  2.   
  3. public class Test3 {  
  4.     int sum = 0;  
  5.     public Function m() {  
  6.         //int sum = 0;  
  7.         Function<Integer, Integer> func= i -> {sum ++; return sum + i;};  
  8.         sum++;  
  9.         return func;  
  10.     }  
  11.       
  12.     public static void main(String[] args) {  
  13.         Test3 t = new Test3();  
  14.         Function<Integer, Integer> func = t.m();  
  15.         //t = null;  
  16.         System.out.println(func.apply(9));  
  17.         System.out.println(t.sum);  
  18.     }  
  19. }  

Output: 11, 2

Function<Integer, Integer> func = t.m(); //++this.sum;

System.out.println(func.apply(9)); //++this.sum; this.sum+i;

lambda只能捕获effectively final的局部变量,就是捕获时或捕获后不能修改相关变量。如果sum是m方法的局部变量,func不能修改该局部变量,因为m执行完就退出栈帧。同时这样也可以避免引起race condition,in a nutshell,lambda表达式对值封闭,对变量开放。

int sum = 0;
list.forEach(e -> { sum += e.size(); }); // Illegal, close over values
List<Integer> aList = new List<>();
list.forEach(e -> { aList.add(e); }); // Legal, open over variables

A race condition occurs when two or more threads can access shared data and they try to change it at the same time. Because the thread scheduling algorithm can swap between threads at any time, you don't know the order in which the threads will attempt to access the shared data.

list.parallelStream().forEach(e -> {sum += e;}); //此时多个线程同时处理,如果sum不是不可变的话,就会引起race condition,那样的话我们就需要提供synchronization或者利用volatile来避免读取stale data。

http://www.lambdafaq.org/what-are-the-reasons-for-the-restriction-to-effective-immutability/

在普通内部类中,实例会一直保留一个对其外部类实例的强引用,往往可能会造成内存泄漏。而lambda表达式只有在有用到类实例变量时才保持一个this引用,如上面func会保存一个引用指向Test3的实例。

lambda简写形式 - 方法引用:
如果我们想要调用的方法拥有一个名字,我们就可以直接调用它。
- 静态方法引用 className::methodName
- 实例上的实例方法引用 instanceReference::methodName
- 超类上的实例方法引用 super::methodName
- 类型上的实例方法引用 className::methodName,比如String::toUpperCase,语法和静态方法引用相同,编译器会根据实际情况作出决定
- 构造方法引用 className::new
- 数组构造方法引用 TypeName[]::new

[java] view plain copy

  1. import java.util.function.Function;  
  2. import java.util.function.IntFunction;  
  3.   
  4. public class Test3 {  
  5. public static void typeRef() {  
  6. // public String toUpperCase()  
  7. Function<String, String> func = String::toUpperCase;  
  8. System.out.println(func.apply("aaa"));  
  9. }  
  10.   
  11. public static void arrayRef() {  
  12. IntFunction<int[]> arrayInt = int[]::new;  
  13. int[] array = arrayInt.apply(10);  
  14. System.out.println(array);  
  15. }  
  16.   
  17. public static void main(String[] args) {  
  18. typeRef();  
  19. arrayRef();  
  20. }  
  21. }  


二、默认方法和静态接口方法
如何给一个单抽象函数式接口添加其他的方法呢?接口方法可以是抽象和默认的,默认方法拥有默认的实现,实现接口的类型可以通过继承得到该默认实现。除此之外,接口还允许定义静态方法,使得我们可以从接口直接调用和它相关的辅助方法。默认方法可以被继承,当父类和接口的超类拥有多个具有相同签名的方法时,比如菱形继承,我们就需要遵照一些规则:
1. 类的方法优先于默认方法,无论该方法是具体还是抽象的
2. 被其他类型所覆盖的方法会被忽略,当超类共享一个公共祖先的时候
class LinkedList<E> implements List<E>, Queue<E> { ... }
3. 当两个独立的默认方法或者默认方法和抽象方法相冲突时会发生编译错误,此时我们需要显式覆盖超类方法
interface Robot implements Artist, Gun {
  default void draw() { Artist.super.draw(); }
}

三、批量处理

- 传递行为而不是值

enhanced for loop VS forEach

[java] view plain copy

  1. import java.util.*;  
  2. import java.util.function.Consumer;  
  3.   
  4. public class Test2 {  
  5.     static List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);  
  6.   
  7.     public static void outer() {  
  8.     //只能顺序处理集合中的元素  
  9.         for(int number : numbers) {  
  10.             System.out.println(number);  
  11.         }  
  12.     }  
  13.   
  14. // interface Consumer<T>:  
  15. // void accept(T t);  
  16.   
  17. //  interface Iterable<T>:  
  18. // default void forEach(Consumer<? super T> action) {  
  19. //        Objects.requireNonNull(action);  
  20. //        for (T t : this) {  
  21. //            action.accept(t);  
  22. //        }  
  23. //    }  
  24.   
  25. //  Array.asList -> Array$ArrayList  
  26. //  Array$ArrayList extends AbstractList, AbstractList implements List<E>  
  27. //  List extends Collection, Collection extends Iterable  
  28. // public void forEach(Consumer<? super T> action) {  
  29. //   Objects.requireNonNull(action);  
  30. //   for (T e : a) {  
  31. //       action.accept(e);  
  32. //     }  
  33. // }  
  34.    
  35.     public static void inner() {  
  36.         numbers.forEach(value -> System.out.println(value));  
  37.     }  
  38.   
  39.     public static void main(String[] args) {  
  40.     inner();  
  41.     }  
  42. }  

个人见解,这个例子中,forEach并没有比for loop更高效,一般其在parallel stream下被调用的时候才发挥作用。


- 可以并行处理,批量处理
流是java8类库中新增的关键抽象,被定义于java.util.stream中。流的操作可以被组合成流水线pipeline。

Stream特性:
1. stream不会存储值,流的元素来自数据源(数据结构,生成函数或I/O通道)
2. 对stream的操作并不会修改数据源
3. 惰性求值,每一步只要一找到满足条件的数字,马上传递给下一步去处理并且暂停当前步骤
4. 可选上界,因为流可以是无限的,用户可以不断的读取直到设置的上界

我们可以把集合作为流的数据源(Collection拥有stream和parallelStream方法),也可以通过流产生一个集合(collect方法)。流的数据源可能是一个可变集合,如果在遍历时修改数据源,会产生interference,所以只要该集合只属于当前线程并且lambda表达式不修改数据源就可以。
1. 不要干扰数据源
2. 不要干扰其他lambda表达式,当一个lambda在修改某个可变状态而另一个lambda在读取状态时就会产生这种干扰

惰性:
急性求值,即在方法返回前完成对所有元素的过滤,而惰性求值则在需要的时候才进行过滤,一次性遍历。

Collectors:
利用collect()方法把流中的元素聚合到集合中,如List或者Set。collect()接收一个类型Collectors的参数,这个参数决定了如何把流中的元素聚合到其他数据结构中。Collectors类包含了大量常用收集器的工厂方法,toList()和toSet()就是其中最常见的两个。

并行:
为了实现高效的并行计算,引入了fork/join模型:
- 把问题分解为子问题
- 串行解决子问题从而得到部分结果
- 合并部分结果为最终结果


并行流:将流抽象为Spliterator,允许把输入元素的一部分转移到另一个新的Spliterator中,而剩下的数据会保存在原来的Spliterator里面。之后还可以进一步分割这两个Spliterator。分割迭代器还可以提供源的元数据(比如元素的数量)和其他一系列布尔值特征(比如元素是否被排序)。如果想要自己实现一个集合或者流,就可能需要手动实现Spliterator接口。stream在JAVA SE8中非常重要,我们为collection提供了stream和parallelStream把集合转化成流,此外数组也可以通过Arrays.stream()被转化为流。stream中还有一些静态工厂方法来创建流,例如Stream.of(), Stream.generate()等。

- 创建stream
1.Stream接口的静态方法
Stream<Integer> integerStream = Stream.of(1, 2, 3, 5);
Stream<String> stringStream = Stream.of("taobao");

2.Collection接口的默认方法

- 转换stream,每次转换会返回一个新的stream对象(把一个stream通过某些行为转换成一个新的stream)
1. distinct,stream中包含的元素不重复
2. filter,使用给定的过滤函数进行过滤操作
3. map,对于stream中包含的元素使用给定的转换函数进行转换操作,新生成的Stream只包含转换生成的元素。这个方法有三个对于原始类型的变种方法,分别是mapToInt, mapToLong和mapToDouble。
4. peek,生成包含原stream的各个元素的新stream,同时提供一个消费函数,新stream每个元素被消费的时候都会执行给定的消费函数
5. limit,对一个stream进行截断

6. skip,返回一个丢弃原stream的前N个元素后剩下元素组成新的stream

- 对stream进行聚合,获取想要的结果
接受一个元素序列为输入,反复使用某个合并操作,把序列中的元素合并成一个汇总的结果

http://docs.oracle.com/javase/tutorial/essential/concurrency/forkjoin.html
 

四、其他

代码加载顺序:

非静态是在new申请内存空间并初始化其值。java必须先加载字节码,然后通过new根据字节码信息申请类的内存空间。静态是在类的字节码加载到jvm中,但仅且只执行一次。静态变量和静态代码块跟声明顺序有关,如果今天太代码块中调用静态变量,那么静态变量必须在静态代码块前面声明;如果静态代码块没有调用静态变量,那么就先声明谁被加载。
父类静态属性-》父类静态代码块-》子类静态变量-》子类静态代码块-》父类非静态变量-》父类非静态变量-》父类非静态代码块-》父类构造函数-》子类非静态变量-》子类非静态代码块-》子类构造函数
 

1. 初始化构造时,先父后子
2. 静态,非静态
3. java中的类只有被用到的时候才会被加载
4. java类只有在类字节码被加载后才可以被构造成对象实例

递归+组合以后再慢慢研究:

http://www.cnblogs.com/ldp615/archive/2013/04/09/recursive-lambda-expressions-1.html

http://www.kuqin.com/shuoit/20140913/342127.html

目前(2016-8)像JAD,JD-GUI都还不支持Lambda特性,所以发现了一个新反编译工具CFR,只要去官网下载CFR的jar包,然后CMD命令进入到放置该包的目录,运行:

java -jar cfr**.jar *.class (*号的地方自己写入符合的名字) 

Reference:

1. Java 下一代:函数式编码风格(Groovy, Scala和Clojure共享的函数结构及其优势)
2. http://www.cnblogs.com/figure9/archive/2014/10/24/4048421.html
3. http://zh.lucida.me/blog/java-8-lambdas-insideout-library-features/

4. http://ifeve.com/stream/

5. http://www.oschina.net/question/2273217_217864

猜你喜欢

转载自my.oschina.net/u/3787897/blog/1631324