JDK8中包含了许多内建的Java中常用到函数接口,比如Comparator或者Runnable接口,这些接口都增加了@FunctionalInterface注解以便能用在lambda上。

name
type
description
Consumer
Consumer< T >
接收T对象,不返回值
Predicate
Predicate< T >
接收T对象并返回boolean
Function
Function< T, R >
接收T对象,返回R对象
Supplier
Supplier< T >
提供T对象(例如工厂),不接收值
UnaryOperator
UnaryOperator
接收T对象,返回T对象
BinaryOperator
BinaryOperator
接收两个T对象,返回T对象

标注为@FunctionalInterface的接口是函数式接口,该接口只有一个自定义方法。注意,只要接口只包含一个抽象方法,编译器就默认该接口为函数式接口。lambda表示是一个命名方法,将行为像数据一样进行传递。

Collection中的新方法

List.forEach()

该方法的签名为void forEach(Consumer<? super E> action),作用是对容器中的每个元素执行action指定的动作,其中Consumer是个函数接口,里面只有一个待实现方法void accept(T t)。注意,这里的Consumer不重要,只需要知道它是一个函数式接口即可,一般使用不会看见Consumer的身影。

list.forEach(item -> System.out.println(item));
List.removeIf()

该方法签名为boolean removeIf(Predicate<? super E> filter),作用是删除容器中所有满足filter指定条件的元素,其中Predicate是一个函数接口,里面只有一个待实现方法boolean test(T t),同样的这个方法的名字根本不重要,因为用的时候不需要书写这个名字。

// list中元素类型String
list.removeIf(item -> item.length() < 2);

List.replaceAll()

该方法签名为void replaceAll(UnaryOperator<E> operator),作用是对每个元素执行operator指定的操作,并用操作结果来替换原来的元素。

// list中元素类型String
list.replaceAll(item -> item.toUpperCase());

List.sort()

该方法定义在List接口中,方法签名为void sort(Comparator<? super E> c),该方法根据c指定的比较规则对容器元素进行排序。Comparator接口我们并不陌生,其中有一个方法int compare(T o1, T o2)需要实现,显然该接口是个函数接口。

// List.sort()方法结合Lambda表达式
ArrayList<String> list = new ArrayList<>(Arrays.asList("I", "love", "you", "too"));
list.sort((str1, str2) -> str1.length()-str2.length());

Map.forEach()

该方法签名为void forEach(BiConsumer<? super K,? super V> action),作用是对Map中的每个映射执行action指定的操作,其中BiConsumer是一个函数接口,里面有一个待实现方法void accept(T t, U u)。

map.forEach((key, value) -> System.out.println(key + ": " + value));

Stream API

认识了几个Java8 Collection新增的几个方法,在了解下Stream API,你会发现它在集合数据处理方面的强大作用。常见的Stream接口继承关系图如下:

Stream是数据源的一种视图,这里的数据源可以是数组、集合类型等。得到一个stream一般不会手动创建,而是调用对应的工具方法:
  • 调用Collection.stream()或者Collection.parallelStream()方法
  • 调用Arrays.stream(T[] array)方法
Stream的特性
  • 无存储。stream不是一种数据结构,它只是某种数据源的一个视图。本质上stream只是存储数据源中元素引用的一种数据结构,注意stream中对元素的更新动作会反映到其数据源上的。
  • 为函数式编程而生。对stream的任何修改都不会修改背后的数据源,比如对stream执行过滤操作并不会删除被过滤的元素,而是会产生一个不包含被过滤元素的新stream。
  • 惰式执行。stream上的操作并不会立即执行,只有等到用户真正需要结果的时候才会执行。
  • 可消费性。stream只能被“消费”一次,一旦遍历过就会失效,就像容器的迭代器那样,想要再次遍历必须重新生成。

对Stream的操作分为2种,中间操作与结束操作,二者的区别是,前者是惰性执行,调用中间操作只会生成一个标记了该操作的新的stream而已;后者会把所有中间操作积攒的操作以pipeline的方式执行,这样可以减少迭代次数。计算完成之后stream就会失效。

操作类型
接口方法
中间操作
concat() distinct() filter() flatMap() limit() map() peek() skip() sorted() parallel() sequential() unordered()
结束操作
allMatch() anyMatch() collect() count() findAny() findFirst() forEach() forEachOrdered() max() min() noneMatch() reduce() toArray()

stream方法

forEach()

stream的遍历操作。

filter()

函数原型为Stream<T> filter(Predicate<? super T> predicate),作用是返回一个只包含满足predicate条件元素的Stream。

distinct()

函数原型为Stream<T> distinct(),作用是返回一个去除重复元素之后的Stream。

sorted()

排序函数有两个,一个是用自然顺序排序,一个是使用自定义比较器排序,函数原型分别为Stream<T> sorted()和Stream<T> sorted(Comparator<? super T> comparator)。

map()

函数原型为<R> Stream<R> map(Function<? super T,? extends R> mapper),作用是返回一个对当前所有元素执行执行mapper之后的结果组成的Stream。直观的说,就是对每个元素按照某种操作进行转换,转换前后Stream中元素的个数不会改变,但元素的类型取决于转换之后的类型。

List<Integer> list = CollectionUtil.newArrayList(1, 2, 3, 4);
list.stream().map(item -> String.valueOf(item)).forEach(System.out::println);
flapmap()

和map类似,不同的是每个元素转换得到的是stream对象,会把子stream对象压缩到父集合中。

List<List<String>> list3 = Arrays.asList(
Arrays.asList("aaa", "bb", "ccc"),
Arrays.asList("aa", "bbb", "ccc"));
list3.stream().flatMap(Collection::stream).collect(Collectors.toList());

reduce 和 collect

reduce的作用是从stream中生成一个值,sum()、max()、min()、count()等都是reduce操作,将他们单独设为函数只是因为常用。

// 找出最长的单词
Stream<String> stream = Stream.of("I", "love", "you", "too");
Optional<String> longest = stream.reduce((s1, s2) -> s1.length()>=s2.length() ? s1 : s2);

collect方法是stream中重要的方法,如果某个功能没有在Stream接口中找到,则可以通过collect方法实现。

// 将Stream转换成容器或Map
Stream<String> stream = Stream.of("I", "love", "you", "too");
List<String> list = stream.collect(Collectors.toList());
// Set<String> set = stream.collect(Collectors.toSet());
// Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length));

诸如String::length的语法形式称为方法引用,这种语法用来替代某些特定形式Lambda表达式。如果Lambda表达式的全部内容就是调用一个已有的方法,那么可以用方法引用来替代Lambda表达式。方法引用可以细分为四类。引用静态方法 Integer::sum,引用某个对象的方法 list::add,引用某个类的方法 String::length,引用构造方法 HashMap::new。

Stream Pipelines原理

ArrayList<String> list = CollectionUtil.newArrayList("I", "love", "you");
list.stream()
.filter(s -> s.length() > 1)
.map(String::toUpperCase)
.sorted()
.forEach(System.out::println);

上面的代码和下面的功能一样,不过下面的代码便于打断点调试。

ArrayList<String> list = CollectionUtil.newArrayList("I", "love", "you");
list.stream()
.filter(s -> {
return s.length() > 1;
})
.map(s -> {
return s.toUpperCase();
})
.sorted()
.forEach(s -> {
System.out.println(s);
});

首先filter方法了解一下:

// ReferencePipeline
@Override
public final Stream<P_OUT> filter(Predicate<? super P_OUT> predicate) {
Objects.requireNonNull(predicate);
return new StatelessOp<P_OUT, P_OUT>(this, StreamShape.REFERENCE,
StreamOpFlag.NOT_SIZED) {
// 生成state对应的Sink实现
@Override
Sink<P_OUT> opWrapSink(int flags, Sink<P_OUT> sink) {
return new Sink.ChainedReference<P_OUT, P_OUT>(sink) {
@Override
public void begin(long size) {
downstream.begin(-1);
} @Override
public void accept(P_OUT u) {
if (predicate.test(u))
downstream.accept(u);
}
};
}
};
}

filter方法返回一个StatelessOp实例,并实现了其opWrapSink方法,可以肯定的是opWrapSink方法在之后某个时间点会被调用,进行Sink实例的创建。从代码中可以看出,filter方法不会进行真正的filter动作(也就是遍历列表进行filter操作)。

filter方法中出现了2个新面孔,StatelessOp和Sink,既然是新面孔,那就先认识下:

abstract class AbstractPipeline<E_IN, E_OUT, S extends BaseStream<E_OUT, S>>
extends PipelineHelper<E_OUT> implements BaseStream<E_OUT, S>

StatelessOp继承自AbstractPipeline,lambda的流处理可以分为多个stage,每个stage对应一个AbstractPileline和一个Sink。

Stream流水线组织结构示意图如下:

图中通过Collection.stream()方法得到Head也就是stage0,紧接着调用一系列的中间操作,不断产生新的Stream。这些Stream对象以双向链表的形式组织在一起,构成整个流水线,由于每个Stage都记录了前一个Stage和本次的操作以及回调函数,依靠这种结构就能建立起对数据源的所有操作。这就是Stream记录操作的方式。

Stream上的所有操作分为两类:中间操作和结束操作,中间操作只是一种标记,只有结束操作才会触发实际计算。中间操作又可以分为无状态的(Stateless)和有状态的(Stateful),无状态中间操作是指元素的处理不受前面元素的影响,而有状态的中间操作必须等到所有元素处理之后才知道最终结果,比如排序是有状态操作,在读取所有元素之前并不能确定排序结果。

有了AbstractPileline,就可以把整个stream上的多个处理操作(filter/map/...)串起来,但是这只解决了多个处理操作记录的问题,还需要一种将所有操作叠加到一起的方案。你可能会觉得这很简单,只需要从流水线的head开始依次执行每一步的操作(包括回调函数)就行了。这听起来似乎是可行的,但是你忽略了前面的Stage并不知道后面Stage到底执行了哪种操作,以及回调函数是哪种形式。换句话说,只有当前Stage本身才知道该如何执行自己包含的动作。这就需要有某种协议来协调相邻Stage之间的调用关系。这就需要Sink接口了,Sink包含的方法如下:

方法名
作用
void begin(long size)
开始遍历元素之前调用该方法,通知Sink做好准备。
void end()
所有元素遍历完成之后调用,通知Sink没有更多的元素了。
boolean cancellationRequested()
是否可以结束操作,可以让短路操作尽早结束。
void accept(T t)
遍历元素时调用,接受一个待处理元素,并对元素进行处理。Stage把自己包含的操作和回调方法封装到该方法里,前一个Stage只需要调用当前Stage.accept(T t)方法就行了。

有了上面的协议,相邻Stage之间调用就很方便了,每个Stage都会将自己的操作封装到一个Sink里,前一个Stage只需调用后一个Stage的accept()方法即可,并不需要知道其内部是如何处理的。当然对于有状态的操作,Sink的begin()和end()方法也是必须实现的。比如Stream.sorted()是一个有状态的中间操作,其对应的Sink.begin()方法可能创建一个存放结果的容器,而accept()方法负责将元素添加到该容器,最后end()负责对容器进行排序。Sink的四个接口方法常常相互协作,共同完成计算任务。实际上Stream API内部实现的的本质,就是如何重载Sink的这四个接口方法。

回到最开始地方的代码示例,map/sorted方法流程大致和filter类似,这些操作都是中间操作。重点关注下forEach方法:

// ReferencePipeline
@Override
public void forEach(Consumer<? super P_OUT> action) {
evaluate(ForEachOps.makeRef(action, false));
} // ... -> // AbstractPipeline
@Override
final <P_IN, S extends Sink<E_OUT>> S wrapAndCopyInto(S sink, Spliterator<P_IN> spliterator) {
copyInto(wrapSink(Objects.requireNonNull(sink)), spliterator);
return sink;
}
@Override
final <P_IN> Sink<P_IN> wrapSink(Sink<E_OUT> sink) {
// 各个pipeline的opWrapSink方法回调
for ( @SuppressWarnings("rawtypes") AbstractPipeline p=AbstractPipeline.this; p.depth > 0; p=p.previousStage) {
sink = p.opWrapSink(p.previousStage.combinedFlags, sink);
}
return (Sink<P_IN>) sink;
}
@Override
final <P_IN> void copyInto(Sink<P_IN> wrappedSink, Spliterator<P_IN> spliterator) {
if (!StreamOpFlag.SHORT_CIRCUIT.isKnown(getStreamAndOpFlags())) {
// sink各个方法的回调
wrappedSink.begin(spliterator.getExactSizeIfKnown());
spliterator.forEachRemaining(wrappedSink);
wrappedSink.end();
}
else {
copyIntoWithCancel(wrappedSink, spliterator);
}
}

forEach()流程中会触发各个Sink的操作,也就是执行各个lambda表达式里的逻辑了。到这里整个lambda流程也就完成了。

Java lambda 原理

Java lambda 一眼看上去有点像匿名内部类的简化形式,但是二者确有着本质的差别。匿名内部类经编译后会生成对应的class文件,格式为XXX$n.class;而lambda代码经过编译后生成一个private方法,方法名格式为lambda$main$n。

// Application.main 方法中代码
ArrayList<String> list = CollectionUtil.newArrayList("I", "love", "you");
list.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
list.forEach(System.out::println);

以上代码就会产生一个Application$1.class文件和一个lambda$main$0的方法。既然lambda实现不是内部类,那么在lambda中this就代表的当前所在类实例。

// Application.main 方法中代码
ArrayList<String> list = CollectionUtil.newArrayList("I", "love", "you");
list.forEach(item -> {
System.out.println(item);
});

通过javap -c -p Application.class查看以上代码对应的字节码:

Constant pool:
#1 = Methodref #12.#36 // java/lang/Object."<init>":()V
#2 = Class #37 // java/lang/String
#3 = String #38 // I
#4 = String #39 // love
#5 = String #40 // you
#6 = Methodref #41.#42 // cn/hutool/core/collection/CollectionUtil.newArrayList:([Ljava/lang/Object;)Ljava/util/ArrayList;
#7 = InvokeDynamic #0:#48 // #0:accept:()Ljava/util/function/Consumer;
#8 = Methodref #49.#50 // java/util/ArrayList.forEach:(Ljava/util/function/Consumer;)V
#9 = Fieldref #51.#52 // java/lang/System.out:Ljava/io/PrintStream;
#10 = Methodref #53.#54 // java/io/PrintStream.println:(Ljava/lang/String;)V
#11 = Class #55 // com/luo/demo/Application
#12 = Class #56 // java/lang/Object
#13 = Utf8 <init>
#14 = Utf8 ()V
#15 = Utf8 Code
#16 = Utf8 LineNumberTable
#17 = Utf8 LocalVariableTable
#18 = Utf8 this
#19 = Utf8 Lcom/luo/demo/Application;
#20 = Utf8 main
#21 = Utf8 ([Ljava/lang/String;)V
#22 = Utf8 args
#23 = Utf8 [Ljava/lang/String;
#24 = Utf8 list
#25 = Utf8 Ljava/util/ArrayList;
#26 = Utf8 LocalVariableTypeTable
#27 = Utf8 Ljava/util/ArrayList<Ljava/lang/String;>;
#28 = Utf8 lambda$main$0
#29 = Utf8 (Ljava/lang/String;)V
#30 = Utf8 item
#31 = Utf8 Ljava/lang/String;
#32 = Utf8 SourceFile
#33 = Utf8 Application.java
#34 = Utf8 RuntimeVisibleAnnotations
#35 = Utf8 Lorg/springframework/boot/autoconfigure/SpringBootApplication;
#36 = NameAndType #13:#14 // "<init>":()V
#37 = Utf8 java/lang/String
#38 = Utf8 I
#39 = Utf8 love
#40 = Utf8 you
#41 = Class #57 // cn/hutool/core/collection/CollectionUtil
#42 = NameAndType #58:#59 // newArrayList:([Ljava/lang/Object;)Ljava/util/ArrayList;
#43 = Utf8 BootstrapMethods
#44 = MethodHandle #6:#60 // invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#45 = MethodType #61 // (Ljava/lang/Object;)V
#46 = MethodHandle #6:#62 // invokestatic com/luo/demo/Application.lambda$main$0:(Ljava/lang/String;)V
#47 = MethodType #29 // (Ljava/lang/String;)V
#48 = NameAndType #63:#64 // accept:()Ljava/util/function/Consumer;
#49 = Class #65 // java/util/ArrayList
#50 = NameAndType #66:#67 // forEach:(Ljava/util/function/Consumer;)V
#51 = Class #68 // java/lang/System
#52 = NameAndType #69:#70 // out:Ljava/io/PrintStream;
#53 = Class #71 // java/io/PrintStream
#54 = NameAndType #72:#29 // println:(Ljava/lang/String;)V
#55 = Utf8 com/luo/demo/Application
#56 = Utf8 java/lang/Object
#57 = Utf8 cn/hutool/core/collection/CollectionUtil
#58 = Utf8 newArrayList
#59 = Utf8 ([Ljava/lang/Object;)Ljava/util/ArrayList;
#60 = Methodref #73.#74 // java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#61 = Utf8 (Ljava/lang/Object;)V
#62 = Methodref #11.#75 // com/luo/demo/Application.lambda$main$0:(Ljava/lang/String;)V
#63 = Utf8 accept
#64 = Utf8 ()Ljava/util/function/Consumer;
#65 = Utf8 java/util/ArrayList
#66 = Utf8 forEach
#67 = Utf8 (Ljava/util/function/Consumer;)V
#68 = Utf8 java/lang/System
#69 = Utf8 out
#70 = Utf8 Ljava/io/PrintStream;
#71 = Utf8 java/io/PrintStream
#72 = Utf8 println
#73 = Class #76 // java/lang/invoke/LambdaMetafactory
#74 = NameAndType #77:#81 // metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#75 = NameAndType #28:#29 // lambda$main$0:(Ljava/lang/String;)V
#76 = Utf8 java/lang/invoke/LambdaMetafactory
#77 = Utf8 metafactory
#78 = Class #83 // java/lang/invoke/MethodHandles$Lookup
#79 = Utf8 Lookup
#80 = Utf8 InnerClasses
#81 = Utf8 (Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
#82 = Class #84 // java/lang/invoke/MethodHandles
#83 = Utf8 java/lang/invoke/MethodHandles$Lookup
#84 = Utf8 java/lang/invoke/MethodHandles
{
public com.luo.demo.Application();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 12: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/luo/demo/Application; public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=2, args_size=1
0: iconst_3
1: anewarray #2 // class java/lang/String
4: dup
5: iconst_0
6: ldc #3 // String I
8: aastore
9: dup
10: iconst_1
11: ldc #4 // String love
13: aastore
14: dup
15: iconst_2
16: ldc #5 // String you
18: aastore
19: invokestatic #6 // Method cn/hutool/core/collection/CollectionUtil.newArrayList:([Ljava/lang/Object;)Ljava/util/ArrayList;
22: astore_1
23: aload_1
24: invokedynamic #7, 0 // InvokeDynamic #0:accept:()Ljava/util/function/Consumer;
29: invokevirtual #8 // Method java/util/ArrayList.forEach:(Ljava/util/function/Consumer;)V
32: return
LineNumberTable:
line 15: 0
line 16: 23
line 19: 32
LocalVariableTable:
Start Length Slot Name Signature
0 33 0 args [Ljava/lang/String;
23 10 1 list Ljava/util/ArrayList;
LocalVariableTypeTable:
Start Length Slot Name Signature
23 10 1 list Ljava/util/ArrayList<Ljava/lang/String;>; private static void lambda$main$0(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #9 // Field java/lang/System.out:Ljava/io/PrintStream;
3: aload_0
4: invokevirtual #10 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
7: return
LineNumberTable:
line 17: 0
line 18: 7
LocalVariableTable:
Start Length Slot Name Signature
0 8 0 item Ljava/lang/String;
}

通过字节码可以看出,调用lambda方法时使用了invokedynamic,该字节码命令是为了支持动态语言特性而在Java7中新增的。Java的lambda表达式实现上也就借助于invokedynamic命令。

字节码中每一处含有invokeDynamic指令的位置都称为“动态调用点”,这条指令的第一个参数不再是代表方法调用符号引用的CONSTANT_Methodref_info常亮,而是变成为JDK7新加入的CONSTANT_InvokeDynamic_info常量,从这个新常量中可得到3项信息:引导方法(Bootstrap Method,此方法存放在新增的BootstrapMethods属性中)、方法类型和名称。引导方法是有固定的参数,并且返回值是java.lang.invoke.CallSite对象,这个代表真正要执行的目标方法调用。根据CONSTANT_InvokeDynamic_info常量中提供的信息,虚拟机可以找到并执行引导方法,从而获得一个CallSite对象,最终调用要执行的目标方法。

从上述mian方法的字节码可见,有一个invokeDynamic指令,他的参数为第7项常量(第二个值为0的参数HotSpot中用不到,占位符):

invokedynamic #7,  0              // InvokeDynamic #0:accept ()Ljava/util/function/Consumer;

常量池中第7项是#7 = InvokeDynamic #0:#48 // #0:accept:()Ljava/util/function/Consumer;,说明它是一项CONSTANT_InvokeDynamic_info常量,常量值中前面的#0表示引导方法取BootstrapMethods属性表的第0项,而后面的#48表示引用第48项类型为CONSTANAT_NameAndType_info的常量,从这个常量中可以获取方法名称和描述符,即accept方法。

BootstrapMethods:
0: #44 invokestatic java/lang/invoke/LambdaMetafactory.metafactory:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodType;Ljava/lang/invoke/MethodHandle;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;
Method arguments:
#45 (Ljava/lang/Object;)V
#46 invokestatic com/luo/demo/Application.lambda$main$0:(Ljava/lang/String;)V
#47 (Ljava/lang/String;)V

上图是在lambda代码中打断点时的调用栈信息,如果在这里的lambda中打印当前所属class,就是Application类,也印证了前面分析的lambda代码会生成一个private方法。

从调用栈的信息来看,是在accept方法中调用lambda对应的private方法(ambda$main$0)的,但是这里的accept方法是属于什么对象呢?从图中看是一串数字字符串,这里可以理解成一个Consumer接口的实现类即可,每个lambda表达式可以理解成在一个新的Consumer实现类中调用的即可。使用命令jmap -histo查看JVM进程类和对象信息可以看到这一行信息:

600: 1 16 com.luo.demo.Application$$Lambda$5/1615039080

参考资料

1、https://github.com/CarpenterLee/JavaLambdaInternals

2、https://blog.csdn.net/zxhoo/article/details/38387141

lambda从入门到精通的更多相关文章

  1. 1、ASP.NET MVC入门到精通——新语法

    本系列目录:ASP.NET MVC4入门到精通系列目录汇总 在学习ASP.NET MVC之前,有必要先了解一下C#3.0所带来的新的语法特性,这一点尤为重要,因为在MVC项目中我们利用C#3.0的新特 ...

  2. 12、ASP.NET MVC入门到精通——HtmlHelper

    本系列目录:ASP.NET MVC4入门到精通系列目录汇总 HtmlHelper:是为了方便View的开发而产生 HtmlHelper的演变 普通首页超级链接为:<a href="/h ...

  3. 23、ASP.NET MVC入门到精通——业务层和数据层父类及接口-T4模板

    本系列目录:ASP.NET MVC4入门到精通系列目录汇总 在上一篇中,我们已经把项目的基本框架搭起来了,这一篇我们就来实现业务层和数据层的父接口及父类. 1.我们先来定义一个业务层父接口IBaseB ...

  4. Python运算符,python入门到精通[五]

    运算符用于执行程序代码运算,会针对一个以上操作数项目来进行运算.例如:2+3,其操作数是2和3,而运算符则是“+”.在计算器语言中运算符大致可以分为5种类型:算术运算符.连接运算符.关系运算符.赋值运 ...

  5. <程序员从入门到精通> -- How

    定位 自己才是职业生涯的管理者,想清楚自己的发展路径: 远期的理想是什么?近期的规划是什么?今日的任务和功课又是什么? 今日之任务或功课哪些有助于近期之规划的实现,而近期之规划是否有利于远期之理想? ...

  6. 【无私分享:从入门到精通ASP.NET MVC】从0开始,一起搭框架、做项目 目录索引

    索引 [无私分享:从入门到精通ASP.NET MVC]从0开始,一起搭框架.做项目(1)搭建MVC环境 注册区域 [无私分享:从入门到精通ASP.NET MVC]从0开始,一起搭框架.做项目(2)创建 ...

  7. ASP.NET MVC4入门到精通系列目录汇总

    序言 最近公司在招.NET程序员,我发现好多来公司面试的.NET程序员居然都没有 ASP.NET MVC项目经验,其中包括一些工作4.5年了,甚至8年10年的,许多人给我的感觉是:工作了4.5年,We ...

  8. Web jquery表格组件 JQGrid 的使用 - 从入门到精通 开篇及索引

    因为内容比较多,所以每篇讲解一些内容,最后会放出全部代码,可以参考.操作中总会遇到各式各样的问题,个人对部分问题的研究在最后一篇 问题研究 里.欢迎大家探讨学习. 代码都经过个人测试,但仍可能有各种未 ...

  9. 5、ASP.NET MVC入门到精通——NHibernate代码映射

    本系列目录:ASP.NET MVC4入门到精通系列目录汇总 上一篇NHibernate学习笔记—使用 NHibernate构建一个ASP.NET MVC应用程序 使用的是xml进行orm映射,那么这一 ...

随机推荐

  1. eclipse安装中java环境的搭建

    转自博客园:amandaj  做了小小改动. 一.java 开发环境的搭建 这里主要说的是在windows 环境下怎么配置环境. 1.首先安装JDK java的sdk简称JDK ,去其官方网站下载最近 ...

  2. Kotlin 或将取代 Java——《Java 编程思想》作者 Bruce Eckel [转]

    Bruce Eckel 是<Java 编程思想>.<C++编程思想>的作者,同时也是 MindView 公司的总裁,该公司向客户提供软件咨询和培训.他是 C++ 标准委员会拥有 ...

  3. 验证对Random的两个猜想

    猜想1:Random.Next()产生的随机数不会有重复. 猜想2:大量级执行Random.Next(int i)分布在各个数值上的概率是均匀的. 验证猜想1 /*如果Random.Next()产生的 ...

  4. ASP.NET -- WebForm -- 缓存Cache的使用

    ASP.NET -- WebForm --  缓存Cache的使用 把数据从数据库或文件中读取出来,放在内存中,后面的用户直接从内存中取数据,速度快.适用于经常被查询.但不经常变动的数据. 1. Te ...

  5. Spring的通知类型,切入表达式写法

    转载自  https://www.cnblogs.com/ltfxy/p/9882697.html Spring中通知类型: 前置通知:目标方法执行之前进行操作,可以获得切入点信息 后置通知: 目标方 ...

  6. 【字符串】ZSC-字符串编辑

    Description 从键盘输入一个字符串(长度<=40个字符),对字符串进行编辑.例如,输入"This is a book." 现对该字符串进行编辑,编辑功能有:(1) ...

  7. mysql 数据可视化操作---Navicat安装及简单使用

    ,一.安装 下载地址:https://pan.baidu.com/s/1bpo5mqj 安装方法:https://www.cnblogs.com/clschao/articles/10022040.h ...

  8. 简单理解Vue中的nextTick

    Vue中的nextTick涉及到Vue中DOM的异步更新,感觉很有意思,特意了解了一下.其中关于nextTick的源码涉及到不少知识,很多不太理解,暂且根据自己的一些感悟介绍下nextTick. 一. ...

  9. MySQL高级知识(四)——Explain

    前言:explain(执行计划),使用explain关键字可以模拟优化器执行sql查询语句,从而知道MySQL是如何处理sql语句.explain主要用于分析查询语句或表结构的性能瓶颈. 注:本系列随 ...

  10. 设计模式のNullObjectPattern(空对象模式)----行为模式

    一.产生背景 在空对象模式(Null Object Pattern)中,一个空对象取代 NULL 对象实例的检查.Null 对象不是检查空值,而是反应一个不做任何动作的关系.这样的 Null 对象也可 ...