深入volatile关键字

在Java多线程中,有一个特殊的关键字volatile,这个通常成为一个“轻量级锁”,下面我们就来深入的了解这个关键的作用和原理。

线程的内存备份

首先看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class VolatileThread extends Thread {
private boolean isRuning=true;
private void setRuning(boolean runing){
this.isRuning=runing;
}

public void run(){
System.out.println("进入Run方法");
while (isRuning){

}
System.out.println("线程结束");
}

public static void main(String[] args) throws InterruptedException {
VolatileThread volatileThread = new VolatileThread();
volatileThread.start();
Thread.sleep(3000);
volatileThread.setRuning(false);
System.out.println("runing设置成false,让线程停止");
Thread.sleep(1000);
System.out.println(volatileThread.isRuning);
}
}

在上面的代码并没有打印出“线程结束”的信息,因为我在主线程更改了isRuning 的值,并没有影响到线程中的数据。

产生这个的原因是因为JDK在创建线程的时候,都会从主内存中拷贝一份数据,所以线程的读取的变量的具有一定延迟

深入volatile关键字

使用volatile

对上面的代码进行修改,把isRuning变量使用volatile 关键字修饰,这样我们就能看到线程能够正常的停止了。下面我们总结下volatile的作用

1
2
3

如果变量被volatile关键字修饰, 则当变量改变的时候强制线程从主内存中读取和写入变量

CPU的缓存怎么办

代码最终的是由CPU执行的,为了保证CPU的执行效率,在读取数据的时候,CPU是优先把数据缓存到自己的高速缓存中,高速缓存带来了效率上面的提高,也同样带来了数据一致性的问题。

深入volatile关键字

例如下面这一段简单的代码:

1
2
3

count++;

当程序运行的时候,count会被拷贝到CPU高速缓存中,知道执行结束才会重新刷到主内存中。

如果在多线程的环境中,就会出现数据不一致的问题。

解决这个问题的方法有两种:

  1. 在总线的位置加锁,一次只允许一个CPU访问内存。

  2. 使用缓存一致性协议,当CPU发现当前的变量是volatile变量,就会被告知通知其他CPU告诉该变量的缓存无效,这样CPU就会从内存中重新加载数据

volatile 不具备原子性

共享变量只是在读和写的时候具有原子性,但是复杂的count++运算不具备原子性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class AppTest {
private volatile int count = 0;

public static void main(String[] args) throws InterruptedException {

AppTest app = new AppTest();

for (int i = 0; i < 10000; i++) {
Thread thread = new Thread(() -> {
for (int m = 0; m < 1000; m++) {
app.count();
}
});
thread.start();
}
System.out.println(app.count);
for (int i=0;i<10;i++){
Thread.sleep(1000);
System.out.println(app.count);
}
}

private void count() {
count++;
}
}

打印结果:

1
2
3
4
9444777
9578523
9578523

看到最终结果不是10000000。

CPU是如何实现运算

CPU的构成

我们知道CPU是芯片的集合,主要成分是硅。CPU的最小构成单位是一个PN节点,也就是我们常说的二极管。下面我们就聊一聊什么是二极管

PN节点 (二极管)

PN节点是一个硅晶体进行掺杂,分别在两侧掺入硼和磷,这样的硅晶体具有单项导电性,这样就形成一个PN节点。具体如下图:

CPU运算

由于具有单项导电性的特点,我们就能根据收到的电压变化,来确定输出的结果,我们假设收到高电压是1 ,低电压是0,PN节点的具体表现:

CPU运算

实现基本运算

根据上面的分析,我们可以尝试实现一个与门的电路实现,首先我们要清楚与门的具体逻辑。

有两个输入参数,只有同时为1的时候,才输出1,具体表示如下:

输入输出 1 0
1 1 0
0 0 0

实现电路图如下:

CPU运算

从图上可以看出来,由于C端的高电压的作用和向导电性,无论A和B哪一个是低电压,输出端Z都会获得低电压。只有两边同时为高电压的时候,Z才会获得高电压

这样的话,CPU就可以根据电信号来进行与门的计算。

程序如何运行的

在写代码的时候,我们直接在没有编译报错的时候,直接点击运行后,ide会直接把程序的结果输出到控制台上,代码如下:

1
2
3
4
5
6
7
  public static void main(String[] args) { 
int i=17;
int j=5;
int sum=i+j;
System.out.println(sum);
}

这段代码最终的结果是在控制台上面打印出:22,但是这个结果到底是怎么被执行的呢?

CPU能做什么

在硬件的世界里面,只有0和1,就是这么简单的0和1,到底是怎么做加法的呢?

我们知道CPU的功能是执行指令,有三个简单的基本操作:与,非,或三种运算。在加上位的运算一种有5种:&,|,~,<<,>>. 利用这个几个运算如何实现代码中的15+5的运算?

首先,把加法拆解,分成两个部分: 把个位和个位相加,如果有进1的话,就用进1的值十位与另一个十位相加。得到的和在进行相加。

  1. 把15+5进行拆解就是 7+5=12,发现5+5有进位10;
  2. 利用进位的十位与10+10 =20
  3. 再把两个的和相加,20+2=22 ,没有再进位,运算结束。

根据上面的分析,我们可以使用递归的方法,写出加法的位运算代码如下:

1
2
3
4
5
6
7
static int add(int i, int j){
if(j == 0)
return i;
int sum = i ^ j;//得到个位相加
int carry = (i & j) << 1;//得到进位相加
return add(sum, carry);
}

对这个算法进行封装成一个CPU指令,我们就可以利用二进制进行进行运算。

Java代码最终的编译结果

我们知道Java的代码最终是经过编译器,转换成字节码最终由JVM解释执行,具体过程如下:

Java过程

当Java代码最终转换成字节码的时候,JVM虚拟机执行对应的字节指令,最终传递给CPU来执行代码,CPU计算的过程我们已经分析过,最终会调用位运算来实现加法。

并发问题

并发问题

在编程的时候我们经常会碰到并发的问题,如果处理不好很有可能造成业务数据的错误。我们思考,到底什么是并发问题?
简单的来说我们可以把并发问题归纳为:未写入而先读取 带来的问题。

我们用最简单的取钱的模型来描述这个问题:

compare

在①②③④ 这个几个步骤中,①②和③④分别是两个独立的过程,如果执行的顺序是 ①③②④,这样就会带来最终余额为负的现象,这个就是一个简单的并发问题。

我们可以用代码简单的模拟这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

public class AppTest {
private int count = 0;
public static void main(String[] args) {
long ts = System.currentTimeMillis();
AppTest app = new AppTest();
List<Thread> tList = new ArrayList<>(500);

for (int i = 0; i < 10000; i++) {
Thread thread = new Thread(() -> {
for (int m = 0; m < 1000; m++) {
app.count();
}
});
tList.add(thread);
}
//启动线程
for (Thread t : tList) {
t.start();
}
//等待所有线程执行完
for (Thread t : tList) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

System.out.println(app.count);
long tend = System.currentTimeMillis();
System.out.println(tend-ts);
}

private void count() {
count++;
}
}

上述代码执行结果: 9997990(结果具有不确定性,此次结果就偶然一次结果),耗时:3378

造成这个结果的原因就是,在多线程执行的过程中,count的值还没有来得及写入内存,另一个线程就已经把count的读取,就导致count少一次count++运算。

解决并发

既然我们已经知道并发问题,如何解决? 对于并发的解决思路是:保证读取的时候,写入已经完成。具体方法有两种,分别是锁和CAS操作。

使用锁 synchronized

修改count()方法:

1
2
3
4
5
6

private synchronized void count() {
count++;
}


运行结果:10000000 耗时:4176

锁的方法导致性能下降很多。

使用CAS操作

把int类型的count换成AtomicInteger类型

1
2
3
4
5
6
7
private AtomicInteger count = new AtomicInteger();

private void count() {
count.addAndGet(1);
}


修改上述两出代码得到运行结果:10000000 耗时:3305

Java对象头

对象是什么

Java是一个面向对象的语言,在Java中可以使用new关键字来产生一个对象,但这个对象到底是什么,应该具有哪些属性?

首先跟我们自己先想下,Java对象已经有以下几个属性:

  1. 属于哪个类
  2. 有哪些字段和类型
  3. 对象的值
    ….

在HotSpot虚拟机中,真实的Java对象是分成三个部分:

  1. 对象头
  2. 对象的值
  3. 对象的填充字节 (在JVM中,要求对象占用内存的大小应该是8bit的倍数,这个信息是用来补齐8bit的,无其他作用

对象头

对象头是Java中对象都具有的属性,是jvm在编译和运行阶段读取的信息。对象头包含三个部分:

  1. mark word
  2. 指针向类的指针
  3. 数组的长度(只有数组的对象用到)

这3个中最复杂的是MarkWordMarkWord用于存储对象自身的运行时数据,
如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等。在32bit的环境中,Java头存储的信息如下

Java对象头

对象头的应用



偏向锁

在HotSpot的虚拟机中,当一个线程访问同步块并尝试获取锁时,会在对象头和栈帧中的记录里存储锁的偏向的线程ID,以后该线程进入和退出同步块的代码时候,不需要再进行加锁 ,只需要检测下MarkWord中的是否是当前线程的偏向锁。如果成功,则说明已经获得了锁,如果检测不成功,就再进行加锁。

轻量级锁

线程在执行同步块之前,JVM会现在当前的线程的栈帧中创建用于存储锁记录的空间,并肩对象头的MarkWord复制到锁的记录中,然后线程尝试将对象头MarkWord替换为指向锁记录的指针,如果不成功,说明锁存在竞争,当前线程开始进行盲等来获取锁。

Java中的字符和流

字节

用于计量存储容量的一种计量单位,通常情况下一字节等于八位, 也表示一些计算机编程语言中的数据类型和语言字符。

在计算机中,我们知道,所有的一切都是归根到底都是二进制的字节形式,包括文件,字符串等等。所以在写程序的时候,尝尝需要和字节打交道。

例如:我们需要把一个字符串写入到文本文件中,我们就需要借助字节这个中间的单位来进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

@Test
public void stringToFile() throws Exception {
String name = "Test";
byte[] bytes = name.getBytes();
FileOutputStream fileOutputStream = new FileOutputStream("D:/test.txt");
fileOutputStream.write(bytes);
fileOutputStream.close();
}

```
当然同样的,从文件到字符串也是离不了字节这个单位:

``` Java

@Test
public void fileToString() throws Exception {
FileInputStream fileInputStream = new FileInputStream("D:/test.txt");
byte[] inByte = new byte[100];
fileInputStream.read(inByte);
String str=new String(inByte);
System.out.println(str);
}

以上我们可以用下面的模型图来表示:

Java Stream模型

流是什么?

从上面的代码中,我们可以看到,有一个 FileInputStreamFileOutputStream这两个流的对象,对于这个对象我们怎么理解?

流是一个字节转移的介质,如同用吸管喝水,水是字节的集合的文件,吸管就是流。

输入流:顾名思义,就是从外界到程序中的介质。

输出流:对外界进行输出的介质

Java 中常用的输入流和输出流:

Java Stream模型

字符流

什么是字符流?用什么用?

还是用喝水来说,我们怎么获得吸管里面水的属性?我们比如获得吸管中某一段水的重量怎么获得?

字符流是流的一种工具,是能够获得流中的单个字节单位的工具。

对应的是Reader和Writer.

我们可以把流转成Reader和Writer工具,来进行处理我们的文件和字节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Test
public void streamWriteTest() throws Exception {
FileInputStream fileInputStream = new FileInputStream("D:/test.txt");
InputStreamReader reader = new InputStreamReader(fileInputStream);
char[] ch =new char[100];
reader.read(ch);
System.out.println(ch);

}

```

``` Java

@Test
public void streamTest() throws Exception {
String str = "test";
FileOutputStream fileOutputStream = new FileOutputStream("D:/Test.txt");
OutputStreamWriter writer = new OutputStreamWriter(fileOutputStream);
writer.write(str.toCharArray());
writer.close();

}

整个的过程入如下:

Java Stream模型

Java反射超越泛型

BeanUtils.copyProperties引发的血案

在一次使用BeanUtils.copyProperties的方法是,莫名其妙的报错,产生的代码分解如下:

Java反射超越泛型

1
2
3
4
5
6
7
8
9
10
11
12

EntityA entityA = new EntityA();
List<UniteA> uniteAList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
UniteA uniteA = new UniteA();
uniteA.setAge(i);
uniteA.setName("name" + i);
uniteAList.add(uniteA);
}
entityA.setKey(10);
entityA.setUniteList(uniteAList);

把EntityA的值赋值给EntityB:

1
2
3
4
5

EntityB entityB = new EntityB();
BeanUtils.copyProperties(entityA, entityB);
System.out.println(entityB.getUniteList().get(0).getClass());

在运行这段代码的时候,发生了异常的信息:

Java反射超越泛型

对于这个异常,我观察了很久,在调试的时候发现,EntityA中的UniteA中的字段竟然能够赋值给 EntityB中UnitB中的字段!! 类型不一样,竟能够存储!!

Java反射超越泛型


泛型仅仅适用于编译期


对于这个问题,归根于Java的泛型的特点,泛型仅仅适用于编译期,下面我们写几个代码来验证这个问题:

1
2
3
List<String> strList=new ArrayList<>();
strList.add("1234");//正常的编译
strList.add(1234);//编译不能通过

上面的例子我们可以看到List的类型的集合无法直接添加一个整型数据,但这个仅仅在编译的才校验,我们可以是有反射绕过这个验证。

1
2
3
4
5
6
7
8
9
10
11

List<String> strList=new ArrayList<>();
strList.add("1234");//正常的编译
try {
Method method=strList.getClass().getDeclaredMethod("add",Object.class);
method.invoke(strList,1234);
System.out.println(strList);
} catch (Exception e) {
e.printStackTrace();
}

调试信息如下:

Java反射超越泛型

同样,我们也可以使用如下代码验证上面的结论:


List<String> strList=new ArrayList<>();
List<Integer> intList=new ArrayList<>();
System.out.println(strList.getClass().equals(intList.getClass()));

打印结果为:true

本文代码文件:下载

Java中lambda表达式详解

为什么使用lambda

在Java中我们很容易将一个变量赋值,比如int a =0;int b=a;

但是我们如何将一段代码和一个函数赋值给一个变量?这个变量应该是什么的类型?

lambda

在Javascript中,可以用一个对象来存储。

1
2
3
4
5
6
7
var t=function()
{
int a=1;
a=a+1;
alert(a);
}

在Java中,直到Java8的lambda的特性问世,才有办法解决这个问题



什么是lambda

什么是lambda? lambda在程序中到底是怎样的一个存在? 首先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

interface eat
{
void eatFood();
}

public static void main(String[] args)
{

eat e = () -> System.out.printf("hello\n");
e.eatFood();

eat e1 = new eat()
{
@Override
public void eatFood()
{
System.out.printf("anoymous class\n");
}
};
e1.eatFood();
}

上面的代码中,e是一个lambda的对象,根据Java的继承的特性,我们可以说e对象的类型是继承自eat接口。而e1是一个正常的匿名类的对象.

通过对比, 可以说 lambda的表达式其实是接口的实现的“另一种方式”。这种方式更加简洁,更容易阅读。除了代码层面的简洁外,在编译的结果时候lambda也不会产生一个多余的匿名类。

对于eat这个特殊的接口,称之为:函数式接口



lamda的优点


  • 代码缩减

compare

  • Option的使用简化代码

    假如我们有个方法,能够产生一个Option对象std

    1
    2
    3

    Option<Person> std=getStudent();

  1. 是否为空的判断

    lambda

  2. 返回不为空的对象

    compare

  3. 多重if else的简化

    lambda



函数式接口

什么是函数式接口?这个是我们理解Lambda表达式的重点,也是产生lambda表达式的**”母体”**,这里我们引用一个比较容易理解的说法:

函数式接口是 一个只有一个抽象方法(不包含object中的方法)的接口。

这个需要说明一点,就是在Java中任何一个对象都来自Object 所有接口中自然会继承自Object中的方法,但在判断是否是函数式接口的时候要排除Object中的方法,下面举几个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//这个是函数式接口
interface eat
{
void eatFood();
}


//这个不是是函数式接口
interface eat
{
default void eatFood()
{
System.out.println("hello");
};
}



//这个是一个函数式接口
interface eat
{
void eatFood();
String toString();
}

对于是否是函数式接口,Java8中也提供了一个专用的注解:@FunctionalInterface。通过这个注解,可以确定是否是函数式接口:

1
2
3
4
5
6
7
8
9
10
11
12

//此处会报编译错误
@FunctionalInterface
interface eat
{
default void eatFood()
{
System.out.println("hello");
};
}


下面我们写一段lambda简单的代码,找出指定的身份证号码,并打印出来。

lambda


最终的调用:

lambda


对于上面的代码实现,在我们调用excutor方法前,并不知道findName的实现方法,直到在最后把一个方法作为参数传入到excutor方法中。

反思:函数式接口NameCheckInterface,是不是可以用来表示所有返回值为bool类型的,有两个形参(类型是passager 和String类型)的lambda表达式?

如果我们再配合泛型的话,是不是我们只需要定义一个通用的函数式接口?下面我们改写下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

@FunctionalInterface
public interface NameCheckInterface<T,T1,T2>
{
T2 findName(T passager,T1 name);
}

@FunctionalInterface
public interface PrintInterface<T>
{
void printName(T name);
}

private void excutor(List<passager> passagerList, NameCheckInterface<Boolean,passager,String> checker, PrintInterface<String> printer)
{
for (passager p : passagerList) {
if (checker.findName(p,"李四")){
printer.printName(p.getPassagerNo());
}
}
}

对应的调用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void simpTest()
{
List<passager> passagerList = new ArrayList<>();
passagerList.add(new passager("李四", "123456789"));
passagerList.add(new passager("张三", "123456789"));
passagerList.add(new passager("王二", "123456789"));

excutor(passagerList,(p,str)->p.getName().equals(str),str-> System.out.println(str));
}



对于这段代码,可以得出lambda中的函数式接口是可以公用的,而jdk中也已经定义了很多通用的函数式接口。



常用的函数式接口

在jdk中通用的函数式接口如下(都在Java.util.function包中):

1
2
3
4
5
6
7
8

Runnable r = () -> System.out.printf("say hello");//没有输入参数,也没有输出
Supplier<String> sp = () -> "hello";//只有输出消息,没有输入参数
Consumer<String> cp = r -> System.out.printf(r);//有一个输入参数,没有输出
Function<Integer, String> func = r -> String.valueOf(r);//有一个输入参数 有一个输出参数
BiFunction<Integer, Integer, String> biFunc = (a, b) -> String.valueOf(a + b);//有两个输入参数 有一个输出参数
BiConsumer<Integer, Integer> biCp = (a, b) -> System.out.printf(String.valueOf(a + b));//有两个输入参数 没有输出参数

PS:上面是基本的方法,其他的都是基于这几个扩展而来

如果上面的代码使用jdk中的函数式接口的话,就不用额外的定义NameCheckInterface和PrintInterface 接口了。根据上面的参数和返回值的形式,可以使用BiFunction和Consumer直接改写excutor方法:

1
2
3
4
5
6
7
8
9
10

private void excutor(List<passager> passagerList, BiFunction<passager,String,Boolean> checker, Consumer<String> printer) {
for (passager p : passagerList) {
if (checker.apply(p,"李四")){
printer.accept(p.getPassagerNo());
}
}

}



函数的引用

从上面的demo中,使用通用的函数表达式能够减少自定义函数式接口,为了进一步简化代码,lambda表达式可以改写成函数的引用的形式

函数的引用是lambda表达式的更简洁的一种写法,也是更能体现出函数式编程的一种形式,让我们更能理解lambda终归也是一个**”函数的对象”**。 下面我们改写一个例子:

1
2
3
4
5
6

Consumer<String> c1 = r -> System.out.printf(r);
c1.accept("1");
Consumer<String> c2 =System.out::printf;
c1.accept("2");

在上面的demo中lambda表达式被我们改写成System.out::printf这个形式,等于我们把一个函数直接赋值给了一个c2对象,这里我们可以俗称(非官方)c2为Java函数的一个对象,这个也结局填补了Java中一个空白。



函数引用的规则

对于Java中lambda改成函数的引用要遵循一定的规则,具体可以分为下面的四种形式:

  1. 静态方法的引用

    如果函数式接口的实现恰好可以通过调用一个静态方法来实现,那么就可以使用静态方法引用

    StaticMethod

    1
    2
    3
    4
    5
    6
    	
    Consumer<String> c1 = r -> Integer.parseInt(r);
    c1.accept("1");
    Consumer<String> c2 =Integer::parseInt;
    c1.accept("2");

  2. 实例方法引用

    如果函数式接口的实现恰好可以通过调用一个实例方法来实现,那么就可以使用实例方法引用

    StaticMethod

    1
    2
    3
    4
    5
    6

    Consumer<String> ins1 = r -> System.out.print(r);
    c1.accept("1");
    Consumer<String> ins2 =System.out::print;
    c1.accept("2");

  3. 对象方法引用

    抽象方法的第一个参数类型刚好是实例方法的类型,抽象方法剩余的参数恰好可以当做实例方法的参数。如果函数式接口的实现能由上面说的实例方法调用来实现的话,那么就可以使用对象方法的引用。

    instMethod

    1
    2
    3
    4
    5

    Function<BigDecimal,Double> fuc1=t->t.doubleValue();
    fuc1.apply(new BigDecimal("1.025"));
    Function<BigDecimal,Double> fuc2=BigDecimal::doubleValue;
    fuc2.apply(new BigDecimal("1.025"));
    1
    2
    3
    4
    BiFunction<BigDecimal, BigDecimal, BigDecimal> func3 = (x, y) -> x.add(y);
    func3.apply(new BigDecimal("1.025"), new BigDecimal("1.254"));
    BiFunction<BigDecimal, BigDecimal, BigDecimal> func4 = BigDecimal::add;
    func4.apply(new BigDecimal("1.025"), new BigDecimal("1.254"));
  4. 构造方法引用

    如果函数式接口的实现恰好可以通过调用一个类的构造方法来实现,那么就可以使用构造方法引用。

    instMethod ]

    1
    2
    3
    4
    Consumer<String> n1 = r ->new BigDecimal(r);
    c1.accept("1");
    Consumer<String> n2 =BigDecimal::new;
    c1.accept("2");

stream API的引用

Stream是处理数组和集合的API,Stream具有以下特点:

  • 不是数据结构,没有内部存储
  • 不支持索引访问
  • 延迟计算
  • 支持过滤,查找,转换,汇总等操作

对于StreamAPI的学习,首先需要弄清楚lambda的两个操作类型:中间操作和终止操作。 下面通过一个demo来认识下这个过程。

1
2
3
4
Stream st=Arrays.asList(1,2,3,4,5).stream().filter(x->{
System.out.print(x);
return x>3;
});

当我们执行这段代码的时候,发现并没有任何输出,这是因为lambda表达式需要一个终止操作来完成最后的动作。 我们修改代码:

1
2
3
4
5
6
Stream st=Arrays.asList(1,2,3,4,5).stream().filter(x->{
System.out.print(x);
return x>3;
});

st.forEach(t-> System.out.print(t));

对应的输出结果是:

1
2
3

1234455

为什么会有这个输出呢?因为在filter函数的时候并没有真正的执行,在forEach的时候才开始执行整个lambda表达式,所以当执行到4的时候,filter输出之后,forEach也执行了,最终结果是1234455


对于Java中的lambda表达式的操作,可以归类和整理如下:

中间操作:

  • 过滤 filter
  • 去重 distinct
  • 排序 sorted
  • 截取 limit、skip
  • 转换 map/flatMap
  • 其他 peek

终止操作

  • 循环 forEach
  • 计算 min、max、count、 average
  • 匹配 anyMatch、 allMatch、 noneMatch、 findFirst、 findAny
  • 汇聚 reduce
  • 收集器 toArray collect


常用的几个lambda

下面我们对这几个常用的lambda表达式写几个demo,首先定义公共的Student类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class Student {

public Student(String stuName, int age, BigDecimal score, int clazz) {
this.stuName = stuName;
this.age = age;
this.score = score;
this.clazz = clazz;
}

private String stuName;
private int age;
private BigDecimal score;
private int clazz;

public String getStuName() {
return stuName;
}
public void setStuName(String stuName) {
this.stuName = stuName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public BigDecimal getScore() {
return score;
}
public void setScore(BigDecimal score) {
this.score = score;
}
public int getClazz() {
return clazz;
}
public void setClazz(int clazz) {
this.clazz = clazz;
}
}

List<Student> studentList = new ArrayList<>();
studentList.add(new Student("黎 明", 20, new BigDecimal(80), 1));
studentList.add(new Student("郭敬明", 22, new BigDecimal(90), 2));
studentList.add(new Student("明 道", 21, new BigDecimal(65.5), 3));
studentList.add(new Student("郭富城", 30, new BigDecimal(90.5), 4));
studentList.add(new Student("刘诗诗", 20, new BigDecimal(75), 1));
studentList.add(new Student("成 龙", 60, new BigDecimal(88), 5));
studentList.add(new Student("郑伊健", 60, new BigDecimal(86), 1));
studentList.add(new Student("刘德华", 40, new BigDecimal(81), 1));
studentList.add(new Student("古天乐", 50, new BigDecimal(83), 2));
studentList.add(new Student("赵文卓", 40, new BigDecimal(84), 2));
studentList.add(new Student("吴奇隆", 30, new BigDecimal(86), 4));
studentList.add(new Student("言承旭", 50, new BigDecimal(68), 1));
studentList.add(new Student("郑伊健", 60, new BigDecimal(86), 1));
studentList.add(new Student("黎 明", 20, new BigDecimal(80), 1));
studentList.add(new Student("李连杰", 65, new BigDecimal(86), 4));
studentList.add(new Student("周润发", 69, new BigDecimal(58), 1));
studentList.add(new Student("徐若萱", 28, new BigDecimal(88), 6));
studentList.add(new Student("许慧欣", 26, new BigDecimal(86), 8));
studentList.add(new Student("陈慧琳", 35, new BigDecimal(64), 1));
studentList.add(new Student("关之琳", 45, new BigDecimal(50), 9));
studentList.add(new Student("温碧霞", 67, new BigDecimal(53), 2));
studentList.add(new Student("林青霞", 22, new BigDecimal(56), 3));
studentList.add(new Student("李嘉欣", 25, new BigDecimal(84), 1));
studentList.add(new Student("彭佳慧", 26, new BigDecimal(82), 5));
studentList.add(new Student("陈紫涵", 39, new BigDecimal(88), 1));
studentList.add(new Student("张韶涵", 41, new BigDecimal(90), 6));
studentList.add(new Student("梁朝伟", 58, new BigDecimal(74), 1));
studentList.add(new Student("梁咏琪", 65, new BigDecimal(82), 7));
studentList.add(new Student("范玮琪", 22, new BigDecimal(83), 1));


forEach


forEach:代表循环当前的list ,下面的例子是循环打印出student的名字

1
2
3

studentList.stream().forEach(x -> System.out.println(x.getStuName()));


filter


根据条件过滤当前的数据,获得分数大于80的学生名称

1
2
3

studentList.stream().filter(t -> t.getScore().compareTo(new BigDecimal(80)) > 0).forEach(x -> System.out.println(x.getStuName()));


distinct、sorted 、group


  1. 去除重复数据

    1
    2
    3

    studentList.stream().distinct().forEach(x -> System.out.println(x.getStuName()));

  2. 单条件排序和多条件排序

    1
    2
    3
    4
    5
    6
       
    studentList.stream().sorted(Comparator.comparing(Student::getScore)).forEach(x -> System.out.println(x.getStuName()));

    //多条件排序
    studentList.stream().sorted(Comparator.comparing(Student::getScore).thenComparing(Student::getStuName)).forEach(x -> System.out.println(x.getStuName()));

  3. group 的使用

    1
    2
    3
        
    System.out.println(studentList.stream().collect(Collectors.groupingBy(x->x.getAge(),Collectors.counting())));


limit、skip

跳过多少,取多少个元素,可以根据当前的数据进行分页

1
2
3
4
5
6
7
8

studentList.stream().skip(10).limit(5).forEach(x -> System.out.println(x.getStuName()));

//具体的分页
int pageIndex=1;
int pageSize=5;
studentList.stream().skip((pageIndex-1)*pageSize).limit(pageSize).forEach(x -> System.out.println(x.getStuName()));


map/flatMap

map是一个转换的工具,提供很多转换的方法,mapToInt,mapToDouble

1
2
3

studentList.stream().map(Student::getScore).forEach(x -> System.out.println(x));

map

上面的结果是输出当前的所有同学的得分。

flatMap是一个可以把子数组的值放到数组里面, 下面的实例是把所有的名字都拆开成一个新的数组

1
2
3
4

studentList.stream().flatMap(x-> Arrays.stream(x.getStuName().split(""))).forEach(x -> System.out.println(x));


flatmap


min、max、count、 average

一组常用的统计函数:

1
2
3
4
5
6

studentList.stream().max(Comparator.comparing(x -> x.getAge())).ifPresent(x-> System.out.println(x.getAge()));
studentList.stream().min(Comparator.comparing(x -> x.getAge())).ifPresent(x-> System.out.println(x.getAge()));
System.out.println(studentList.stream().count());
studentList.stream().mapToDouble(x -> x.getScore().doubleValue()).average().ifPresent(x-> System.out.println(x));

flatmap


anyMatch、noneMatch、 allMatch、 findFirst、 findAny

  • anyMatch: 操作用于判断Stream中的是否有满足指定条件的元素。如果最少有一个满足条件返回true,否则返回false。

  • noneMatch: 与anyMatch相反。allMatch是判断所有元素是不是都满足表达式。

  • findFirst: 操作用于获取含有Stream中的第一个元素的Optional,如果Stream为空,则返回一个空的Optional。若Stream并未排序,可能返回含有Stream中任意元素的Optional。

  • findAny: 操作用于获取含有Stream中的某个元素的Optional,如果Stream为空,则返回一个空的Optional。由于此操作的行动是不确定的,其会自由的选择Stream中的任何元素。在并行操作中,在同一个Stram中多次调用,可能会不同的结果。在串行调用时,都是获取的第一个元素, 默认的是获取第一个元素,并行是随机的返回。

1
2
3
4
5
6
7
8
9
10
11
12

System.out.println(studentList.stream().anyMatch(r -> r.getStuName().contains("伟")));
System.out.println(studentList.stream().allMatch(r -> r.getStuName().contains("伟")));
System.out.println(studentList.stream().noneMatch(r -> r.getStuName().contains("伟")));
System.out.println(studentList.stream().findFirst());
System.out.println(studentList.stream().findAny());

for (int i=0;i<10;i++)
{
System.out.println(studentList.stream().parallel().findAny().get().getStuName());
}


reduce

对于reduce的使用,应该在js中也有接触到,但也是比较小众的功能,但使用起来功能却非常的强大,先看一个正常的demo:

1
2
3
4
5
6
7
8
9
10

Stream.of(1, 5, 10, 8).reduce((x, y) -> {
System.out.println("x : " + x);
System.out.println("y : " + y);
System.out.println("x+y : " +x);

System.out.println("--------");
return x + y;
});

打印结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
x : 1
y : 5
x+y : 1
--------
x : 6
y : 10
x+y : 6
--------
x : 16
y : 8
x+y : 16
--------

可以看出:

  1. reduce是一个循环,有两个参数
  2. 第一次执行的时候x是第一个值,y是第二个值。
  3. 在第二次执行的时候,x是上次返回的值,y是第三个值
    ….
    直到循环结束为止。

再修改代码如下:

1
2
3
4
5
6
7
8
9
10
11

//指定了初始值

Stream.of(1, 5, 10, 8).reduce(100,(x, y) -> {
System.out.println("x : " + x);
System.out.println("y : " + y);
System.out.println("x+y : " +x);
System.out.println("--------");
return x + y;
});

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
x : 100
y : 1
x+y : 100
--------
x : 101
y : 5
x+y : 101
--------
x : 106
y : 10
x+y : 106
--------
x : 116
y : 8
x+y : 116
--------


toArray、collect

toArray和collect是两个收集器,toArray是把数据转换成数组,collect是转成其他的类型。这里就不在讨论了。

1
2
3
 
System.out.println(studentList.stream().collect(Collectors.groupingBy(x->x.getAge(),Collectors.counting())));

《王阳明度阴山》--读后感

在高中开始学习哲学的时候,就知道哲学有两大阵营,唯物主义和唯心主义。唯物主义认为世界的本源是物质,物质是客观存在的,物质决定意识,中国古代代表的是朱熹的格物致知。唯心主义认为世界的本质是意识,心外无物,像佛学,道教和王阳明的心学都是唯心主义的代表。

在《度阴山》这本书上,说了一个很有趣的例子 :

王阳明游南镇,一友指岩中花树问曰:“天下无心外之物,如此花树在深山中自开自落,于我心亦何相关?”先生曰:“你未看此花时,此花与汝心同归于寂;你来看此花时,则此花颜色一时明白起来;便知此花不在你的心外。”

翻译过来就是: 王阳明于朋友游南镇,看到岩石上面友一朵花,他的朋友指着岩石花说:“你常说心外无物,这个花在这个深山中花开花落,和我的心有什么关心呢?” 王阳明说:“你没有看到这个花的时候,这个花与你的心一同沉寂,当你看到此花时,花的颜色在你的心中明白了起来,看来这个花并没有在你的心外。”

王阳明朋友的理解是唯物主义的角度去理解,我想大多数人也都会认为第一种说法更为合理,因为花确实是客观存在,不受任何人的影响,即使你人的意识没了,花依然存在,所以是先有了花,才能谈生出人们对花的意识。

从王阳明的心学角度考虑,当你看到花的时候,花的颜色,花的形状,”花”的这个名字,都已经在你的心中了,只不过在你看到的那个时刻,产生了一个碰撞,在你看到花之前就已经有了花的意识,从反面角度来说,如果一个人失去了意识,就不可能感受到物质的存在,如果所有的人和生物都失去了意识,那么整个宇宙就没有了意义。

对于上面的两种结识,我更喜欢王阳明的心学,这个也是我写这篇博客的目的。唯物主义固然是对的,也是现代科学的基础,但唯物主义只能教会我们更好认识世界,确没有告诉我们认识自己,而再完美的科学也不能脱离人的意识而存在。而王阳明的心学追求的是”知行合一”,认为我们的良知无所不能,良知能做到一切,让我们跟随自己的良知去做事,良知认为什么是对的就做什么,不对的就想办法把他克服掉。

有人会问,王阳明的心学和佛学有很多类似的地方,都认为心外无物,这个书中也给了我们的答案,心学于佛学最大的不同是佛学是灭了人的七情六欲,所以心经里面就有照见五蕴皆空等等的句子。心学认为人的七情六欲是正常的,我们只要管理好他就行了。虽然说起来很简单,这个正是心学高明的地方。

奉唯物观点为经典,存天理去人欲,人生在宇宙,犹如沧海一粟,生命的脆弱带来的只是意志的脆弱和人性的虚伪,所以我们不仅需要唯物观点认识世界,也需要心学来充实内心,社会的浮躁,恰恰是只在乎了前者,而忽略缺乏了内心的修养。

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×