Shepard's Blog

Nothing is true everything is permitted


  • 首页

  • 标签

  • 分类

  • 归档

  • 公益404

  • 搜索

stream中去除相同的元素

发表于 2020-06-24 | 分类于 Java |

给一个集合中的元素去重算是一个比较常见的任务需求了。这里记录一下通过stream来对元素去重操作的两种方法。

distinct方法

stream本身提供了distinct()这样的方法来对集合中的元素进行去重操作。不过默认情况下对于集合中的自定义类是无能为力的。
看了下关于distinct()中的注释,他是利用equals()方法来判断集合中的对象是否完全相等来去重的。
因此如果是自定义类,可以通过重写equals()方法来达到目的:

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
class Student {
private String name;
private int age;

public Student(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

@Override
public String toString() {
return "{ name: \"" + name + "\", age: " + age + " }";
}

@Override
public boolean equals(Object obj) {
if (this == obj) { return true; }
if (obj == null || getClass() != obj.getClass()) { return false; }
Student s = (Student)obj;
if (this.name == null) {
if (s.name != null) { return false; }
} else if (!this.name.equals(s.name)) { return false; }
if (this.age != s.age) { return false; }
return true;
}

@Override
public int hashCode() {
return Objects.hash(name, age);
}
}

这里定义了一个Student类,然后通过判断name与age属性来判断对象是否相等。
需要注意的是,点开equals()方法会看到以下注释:

请注意,通常每当重写此方法时,都必须重写{@code hashCode}方法,以便维护{@code hashCode}方法的常规协定,该协定规定相等的对象必须具有相等的哈希码。

因此也不要忘记了重写hashCode()方法。接下来测试一下。

1
2
3
4
5
List<Student> students = new Random().ints(10, 10, 16).boxed()
.map(i -> new Student(String.valueOf((char)(55+i)), i)).collect(Collectors.toList());
students.forEach(System.out::println);
System.out.println("-----------------------------------");
students.stream().distinct().forEach(System.out::println);

输出的结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{ name: "C", age: 12"}
{ name: "E", age: 14"}
{ name: "A", age: 10"}
{ name: "A", age: 10"}
{ name: "C", age: 12"}
{ name: "A", age: 10"}
{ name: "C", age: 12"}
{ name: "A", age: 10"}
{ name: "F", age: 15"}
{ name: "B", age: 11"}
-----------------------------------
{ name: "C", age: 12"}
{ name: "E", age: 14"}
{ name: "A", age: 10"}
{ name: "F", age: 15"}
{ name: "B", age: 11"}

可以看到distinct()方法已经生效了。

filter方法

如果不想修改自定义类的equals()与hashCode()方法,就可以利用filter方法来过滤掉重复的类。
filter方法接收的是一个Predicate,他可以筛选出在stream中与该Predicate匹配的元素,
因此可以写个去重的Predicate来达到目的:

1
2
3
4
5
6
7
public static <T> Predicate<T> distinctByKeys(Function<? super T, ?>... keyExtractors) {
Map<Object, Boolean> map = new ConcurrentHashMap<>();
return t -> {
List<?> keys = Arrays.stream(keyExtractors).map(k -> k.apply(t)).collect(Collectors.toList());
return map.putIfAbsent(keys, Boolean.TRUE) == null;
};
}

Function有点类似匿名函数,他通过传入一个参数,然后可以返回一个结果。
这里通过将这些Function的执行结果组成一个列表当作Map的key存入到指定的Map中,
putIfAbset方法与put方法不同点在于它并不会覆盖掉Map中已存在的键值对,并且还会返回该键所对应的值。
所以如果Map中没有该键值对时,必然返回的值为null。
因此第一次放入Map中的元素都能成功放入,返回的都是null,如果Map本身已包含了该元素,则不会返回null,说明该元素已经重复了。
这里主要是通过键来过滤元素,对值的要求不高,使用Boolean.True对象是因为它占用的字节数较少。
因为stream有可能通过parallel()方法来变成一个parallelStream,因此需要使用ConcurrentHashMap来确保多线程下的访问。
最终配合filter()方法可以过滤掉重复的元素。

1
students.stream().filter(distinctByKeys(Student::getName, Student::getAge)).forEach(System.out::println);

最终输出结果与上边过滤结果一致。

如果只是想要利用类的单个属性来过滤,则直接将Function的执行结果作为key即可:

1
2
3
4
5
6
public static <T> Predicate<T> distinctByKey(Function<? super T, Object> keyExtractor) {
Map<Object, Boolean> map = new ConcurrentHashMap<>();
return t -> map.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
}

students.stream().filter(distinctByKey(Student::getName)).forEach(System.out::println);

Optional.orElse与Optional.orElseGet的区别

发表于 2020-06-22 | 分类于 Java |

Java8加入了Optional这一利器用来对付null。它里边就包含了orElse与orElseGet方法。
单看方法及注释的话,它俩的区别就是接收的参数不一样,以及一个返回null,另一个则不允许。
不过最近在使用过程中发现它俩还有一个区别,就是无论Optional中的值为不为null,
orElse都会有执行,而orElseGet则不会执行。

下面通过一个简单的代码段来测试一下:

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
public static String other() {
System.out.println("call other method");
return "nothing";
}

public static String orElseMethod(String s) {
return Optional.ofNullable(s).orElse(other());
}

public static String orElseGetMethod(String s) {
return Optional.ofNullable(s).orElseGet(() -> other());
}

public static void main(String[] args) throws Exception {
String s = null;
System.out.println("----------orElse---------");
System.out.println(orElseMethod(s));
System.out.println("----------orElseGet---------");
System.out.println(orElseGetMethod(s));
}

output:
----------orElse---------
call other method
nothing
----------orElseGet---------
call other method
nothing

查看输出信息可以得知,当Optional值为null的时候,orElse及orElseGet都执行了对应的方法并且返回了候补值。
当传入的值不为null时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) throws Exception {
String s = "a";
System.out.println("----------orElse---------");
System.out.println(orElseMethod(s));
System.out.println("----------orElseGet---------");
System.out.println(orElseGetMethod(s));
}

output:
----------orElse---------
call other method
a
----------orElseGet---------
a

这时orElseGet里的方法并不会执行,而orElse里的方法还是会执行。

乍看之下,它俩有点像单例模式中的饿汉模式与懒汉模式,一个是事先准备好返回值,
另一个则是需要用到的时候才去构造对应的返回值。因此这样看某些情况下orElseGet比orElse性能会好一些?

跑个分看看:

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
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
@State(Scope.Benchmark)
public class OptionalTest {

@Param({"sss"})
public String s = null;

public String other() {
return "nothing";
}

@Benchmark
public String orElseMethod() {
return Optional.ofNullable(s).orElse(other());
}

@Benchmark
public String orElseGetMethod() {
return Optional.ofNullable(s).orElseGet(() -> other());
}

public static void main(String[] args) throws Exception {
Options options = new OptionsBuilder().include(OptionalTest.class.getSimpleName())
.forks(2)
.warmupIterations(4).warmupTime(TimeValue.seconds(1))
.measurementIterations(10).measurementTime(TimeValue.seconds(1))
.threads(4)
.build();
new Runner(options).run();
}
}

output:
Benchmark (s) Mode Cnt Score Error Units
OptionalTest.orElseGetMethod sss thrpt 40 494.108 ± 16.033 ops/us
OptionalTest.orElseMethod sss thrpt 40 461.837 ± 36.228 ops/us

这里的评测指标为吞吐量Throughput,表示指定时间内能执行多少次调用。那自然分数是越高越好。
从这个测试结果来看,orElseGet是比orElse分数高的,当然测试用例的不同分数自然不一样,此次跑分结果仅供娱乐。

Spring Security ACL的基本使用

发表于 2019-03-29 | 分类于 Java , Spring |

访问控制列表(Access Control List,即 ACL)是用以对指定对象权限进行管理的一组列表。Spring Security ACL可以在单个域对象上定义特定的用户/角色权限。例如,一个拥有管理员角色的用户可以读取(READ)与删除(DELETE)所有的资源,而普通用户只能查看自己的资源。可以认为是不同的用户/角色对不同的指定对象有着不同的权限。接下来我就来试试Spring Security ACL是如何实现这一基本功能的。

阅读全文 »

Spring WebFlux Security与JWT整合

发表于 2019-03-08 | 分类于 Java , Spring |

现在的Web项目基本上都是前后端分离,后端专注于提供API接口即可,使用Spring Boot来开发RESTful API十分方便,如果需要保护这些API接口搭配Spring全家桶套餐中的Spring Security算是不错的选择。之前在Spring官网看Spring Security相关的示例是包含了网页访问相关的内容,并不是只提供接口访问的Web后端。接下来我就试试用JWT来保护这些后端提供的接口。

阅读全文 »

使用Tensorflow识别图片验证码

发表于 2018-10-16 | 分类于 Python , TensorFlow |

验证码是我们在网络浏览中经常能见到的元素之一。它主要是用来区分用户是计算机还是人的公共全自动程序。之前使用jmeter来测试登录页面的功能时就被这个验证码给挡住了,最终手动输入了事。由于这个验证码是比较简单的,由字母与数字组成,之前看过TensorFlow的一篇教程,是用卷积神经网络(CNN)来识别手写数字的,后来想想,不如使用TensroFlow来搭建一个卷积神经网络模型来识别一下验证码。

阅读全文 »

使用Tensorflow建立一个卷积神经网络

发表于 2018-05-29 | 分类于 Python , TensorFlow |

Tensorflow的layers模块提供了一些高等API用于更加容易的构建神经网络。它提供了方便创建密集(全连接)层/卷积层,添加激励函数,及使用正则化解决过拟合等一系列方法。在这个教程中,你将学习到如何使用layers创建一个用于识别MNIST手写数字集合的卷积神经网络。

阅读全文 »

初探istio

发表于 2018-05-04 | 分类于 Kubernetes |

istio项目是Service Mesh概念的最新实现,旨在所有主流集群管理平台上提供Service Mesh层,初期以实现Kubernetes上的服务治理层为目标。它由控制平面和数据平面组成:控制平面由Go语言实现,包括pilot、mixer、auth三个组件;数据平面功能由Envoy在pod中以Sidecar的部署形式提供。

阅读全文 »

Kubernetes初体验:部署无状态服务Redis

发表于 2018-04-13 | 分类于 Kubernetes |

最近看微服务相关的文章得知下一代微服务的王牌项目貌似是istio,而它现阶段又是构建于Kubernetes上的,那就想着来简单体验一下Kubernetes。

阅读全文 »

Spring Boot security with database

发表于 2017-09-06 | 分类于 Java , Spring |

在Spring Boot中加入Spring Security功能的话,官方给出了一个很好的例子。例子中给出的验证用户是放在内存中的,不过我想试试常规一点的方法,将用户存储到数据库中。

阅读全文 »

Linphone通话录音功能实现

发表于 2017-06-26 | 分类于 iOS , Objective-C |

最近使用Linphone要实现一个通话录音的功能,Linphone倒是给了相关的方法,不过组合在一起还不知道怎么用。好在有安卓方面成功的代码:

1
2
3
4
5
LinphoneCall call = linphoneCore.getCurrentCall();
LinphoneCallParams params = call.getCurrentParamsCopy();
params.setRecordFile(recordingPath);
linphoneCore.updateCall(myCall, params);
call.startRecording();

然后照着这部分代码试着写了一个iOS版本的:

1
2
3
4
5
6
LinphoneCall *call = linphone_core_get_current_call(LC);
LinphoneCallParams *params = linphone_call_params_copy(linphoen_call_get_current_params(call));
const char *file_path = [recordingPath cStringUsingEncoding:[NSString defaultCStringEncoding]];
linphone_call_params_set_record_file(params, file_path);
linphone_core_update_call(LC, call, params);
linphone_call_params_destroy(params);

这样算是设置好了录音文件的路径,接下来只需要调用对应的方法就开始录音了:

1
linphone_call_start_recording(call);

结束录音的时候,尝试看看能不能获取到对应的路径:

1
2
3
4
5
6
linphone_call_stop_recording(call);
const LinphoneCallParams *params = linphone_call_get_current_params(call);
const char *record_file = linphone_call_params_get_record_file(params);
if (record_file) {

}

结果却是这里的路径总是为NULL,看样子这样的写法是不成功了。

查了相关资料,看了那些方法的注释,才发现要在通话开始之前就要设置好录音文件的路径。
如果是在打电话的过程中,则需要在拨打之前进行设置:

1
2
3
4
5
LinphoneAddress *address = linphone_address_new(linphone_core_get_identity(LC));
LinphoneCallParams *params = linphone_core_create_call_params(LC, NULL);
linphone_call_params_set_record_file(params, file_path);
linphone_core_invite_address_with_params(LC, address, params);
linphone_call_params_destroy(params);

如果是在接听电话的时候需要录音,那则在收到对应的状态时就要设置:

1
2
3
4
5
6
if (state == LinphoneCallIncomingReceived) {
LinphoneCallParams *params = linphone_core_create_call_params(LC, NULL);
linphone_call_params_set_record_file(params, file_path);
linphone_core_accept_call_with_params(LC, call, params);
linphone_call_params_destroy(params);
}

接下来在通话的过程中使用linphone_call_start_recording开始录音,使用linphone_call_stop_recording方法结束录音。然后调用linphone_call_params_get_record_file就可以获取到对应的录音文件路径了。

12…6
Shepard

Shepard

59 日志
25 分类
67 标签
GitHub
© 2015 — 2020 Shepard
由 Hexo 强力驱动
|
主题 — NexT.Mist v5.1.4