lambda从入门到精通
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中的新方法
该方法的签名为void forEach(Consumer<? super E> action),作用是对容器中的每个元素执行action指定的动作,其中Consumer是个函数接口,里面只有一个待实现方法void accept(T t)。注意,这里的Consumer不重要,只需要知道它是一个函数式接口即可,一般使用不会看见Consumer的身影。
list.forEach(item -> System.out.println(item));
该方法签名为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接口继承关系图如下:
- 调用Collection.stream()或者Collection.parallelStream()方法
- 调用Arrays.stream(T[] array)方法
- 无存储。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、ASP.NET MVC入门到精通——新语法
本系列目录:ASP.NET MVC4入门到精通系列目录汇总 在学习ASP.NET MVC之前,有必要先了解一下C#3.0所带来的新的语法特性,这一点尤为重要,因为在MVC项目中我们利用C#3.0的新特 ...
- 12、ASP.NET MVC入门到精通——HtmlHelper
本系列目录:ASP.NET MVC4入门到精通系列目录汇总 HtmlHelper:是为了方便View的开发而产生 HtmlHelper的演变 普通首页超级链接为:<a href="/h ...
- 23、ASP.NET MVC入门到精通——业务层和数据层父类及接口-T4模板
本系列目录:ASP.NET MVC4入门到精通系列目录汇总 在上一篇中,我们已经把项目的基本框架搭起来了,这一篇我们就来实现业务层和数据层的父接口及父类. 1.我们先来定义一个业务层父接口IBaseB ...
- Python运算符,python入门到精通[五]
运算符用于执行程序代码运算,会针对一个以上操作数项目来进行运算.例如:2+3,其操作数是2和3,而运算符则是“+”.在计算器语言中运算符大致可以分为5种类型:算术运算符.连接运算符.关系运算符.赋值运 ...
- <程序员从入门到精通> -- How
定位 自己才是职业生涯的管理者,想清楚自己的发展路径: 远期的理想是什么?近期的规划是什么?今日的任务和功课又是什么? 今日之任务或功课哪些有助于近期之规划的实现,而近期之规划是否有利于远期之理想? ...
- 【无私分享:从入门到精通ASP.NET MVC】从0开始,一起搭框架、做项目 目录索引
索引 [无私分享:从入门到精通ASP.NET MVC]从0开始,一起搭框架.做项目(1)搭建MVC环境 注册区域 [无私分享:从入门到精通ASP.NET MVC]从0开始,一起搭框架.做项目(2)创建 ...
- ASP.NET MVC4入门到精通系列目录汇总
序言 最近公司在招.NET程序员,我发现好多来公司面试的.NET程序员居然都没有 ASP.NET MVC项目经验,其中包括一些工作4.5年了,甚至8年10年的,许多人给我的感觉是:工作了4.5年,We ...
- Web jquery表格组件 JQGrid 的使用 - 从入门到精通 开篇及索引
因为内容比较多,所以每篇讲解一些内容,最后会放出全部代码,可以参考.操作中总会遇到各式各样的问题,个人对部分问题的研究在最后一篇 问题研究 里.欢迎大家探讨学习. 代码都经过个人测试,但仍可能有各种未 ...
- 5、ASP.NET MVC入门到精通——NHibernate代码映射
本系列目录:ASP.NET MVC4入门到精通系列目录汇总 上一篇NHibernate学习笔记—使用 NHibernate构建一个ASP.NET MVC应用程序 使用的是xml进行orm映射,那么这一 ...
随机推荐
- eclipse安装中java环境的搭建
转自博客园:amandaj 做了小小改动. 一.java 开发环境的搭建 这里主要说的是在windows 环境下怎么配置环境. 1.首先安装JDK java的sdk简称JDK ,去其官方网站下载最近 ...
- Kotlin 或将取代 Java——《Java 编程思想》作者 Bruce Eckel [转]
Bruce Eckel 是<Java 编程思想>.<C++编程思想>的作者,同时也是 MindView 公司的总裁,该公司向客户提供软件咨询和培训.他是 C++ 标准委员会拥有 ...
- 验证对Random的两个猜想
猜想1:Random.Next()产生的随机数不会有重复. 猜想2:大量级执行Random.Next(int i)分布在各个数值上的概率是均匀的. 验证猜想1 /*如果Random.Next()产生的 ...
- ASP.NET -- WebForm -- 缓存Cache的使用
ASP.NET -- WebForm -- 缓存Cache的使用 把数据从数据库或文件中读取出来,放在内存中,后面的用户直接从内存中取数据,速度快.适用于经常被查询.但不经常变动的数据. 1. Te ...
- Spring的通知类型,切入表达式写法
转载自 https://www.cnblogs.com/ltfxy/p/9882697.html Spring中通知类型: 前置通知:目标方法执行之前进行操作,可以获得切入点信息 后置通知: 目标方 ...
- 【字符串】ZSC-字符串编辑
Description 从键盘输入一个字符串(长度<=40个字符),对字符串进行编辑.例如,输入"This is a book." 现对该字符串进行编辑,编辑功能有:(1) ...
- mysql 数据可视化操作---Navicat安装及简单使用
,一.安装 下载地址:https://pan.baidu.com/s/1bpo5mqj 安装方法:https://www.cnblogs.com/clschao/articles/10022040.h ...
- 简单理解Vue中的nextTick
Vue中的nextTick涉及到Vue中DOM的异步更新,感觉很有意思,特意了解了一下.其中关于nextTick的源码涉及到不少知识,很多不太理解,暂且根据自己的一些感悟介绍下nextTick. 一. ...
- MySQL高级知识(四)——Explain
前言:explain(执行计划),使用explain关键字可以模拟优化器执行sql查询语句,从而知道MySQL是如何处理sql语句.explain主要用于分析查询语句或表结构的性能瓶颈. 注:本系列随 ...
- 设计模式のNullObjectPattern(空对象模式)----行为模式
一.产生背景 在空对象模式(Null Object Pattern)中,一个空对象取代 NULL 对象实例的检查.Null 对象不是检查空值,而是反应一个不做任何动作的关系.这样的 Null 对象也可 ...