Java编程原理——动态与函数式编程

反射

  1. 反射是在类运行时动态获取类型的信息。比如接口信息、成员信息、方法信息、构造函数信息等。根据动态获取到的信息创建对象、访问/修改成员、调用方法等。
  2. 使用反射可以得到类的名称,调用方法getName()。其中还包括getSimpleName(),getCanonicalName()以及getPackage()。
  3. 类中定义的静态和实例变量称之为字段,使用Field类表示,可以获取字段的访问权限,设置访问权限,获得字段值以及设置字段值。除此之外,还可以获得字段的修饰符以及基本类型。需要注意的是使用反射返回的修饰符是一个整型。如果需要查看具体的返回值类型,可以使用Modifier进行再次判断。
  4. 通过反射也可以获取类中的方法信息:对于invoke方法而言,如果Method是静态方法,则obj被忽略为null,args可以为null;
  5. 创建对象和构造方法:public T newInstance() throws InstantiationException, IllegalAccessException
  6. 类型检查和转换:使用instanceof关键字可以用于判断变量指向的实际对象类型,instanceof后面的类型是在代码中确定的;
  7. 除此之外,Class方法中还有其他的一些方法可以用于获取类的声明信息、修饰符、父类、接口以及注解等;
  8. 对于数组类型可以使用public native Class<T> getComponentType()获取其元素类型;
  9. 反射与枚举:枚举类型也有一个专门的方法:public T[] getEnumConstants()
  10. 反射与泛型:可以获取泛型参数的信息:
  • Class类有如下方法:public TypeVariable<Class<T>>[] getTypeParameters()
  • Field有如下方法:public Type getGenericType()
  • Method有如下方法:public Type getGenericReturnType()public Type[] getGenericParameterTypes()public Type[] getGenericExceptionTypes()
  • Constructor有如下方法:public Type[] getGenericParameterTypes()
  1. Type实现了Class方法,其中还实现了下列方法:
  • TypeVariable类型参数可以有上界;
  • ParameterizedType参数化的类型,有原始类型和具体类型;
  • WildcardType通配符类型
  • 通过反射获取泛型示例
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 GenericDemo {
static class GenericTest<U extends Comparable<U, V> {
U u;
V v;
List<String> list;
public U test(List<? extends Number> numbers) {
return null;
}
}
public static void main(String[] args) {
Class<?> cls = GenericTest.class;
for (TypeVariable t : cls.getTypeParameters()) {
System.out.println(t.getName() + " extends " + Arrays.toString(t.getBounds()));
}
Field fu = cls.getDeclaredField("u");
System.out.println(fu.getGenericType());
Field first = cls.getDeclaredField("list");
Type listType = first.getGenericType();
if (!listType instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType)listType;
System.out.println("raw type: " + pType.getRawType() + " ,type arguments:" + Arrays.toString(pType.getActualTypeArguments()));
}
// 省略代码
}
}
  1. 总结:反射虽然灵活,但一般情况下我们并不优先建议,原因有如下:
  • 反射更容易出现运行时错误,使用显式的类和接口,编译器能帮我们做类型检查,减少错误,但使用反射,类型是运行时才知道的,编译器无能为力;
  • 反射会降低性能,在访问字段、调用方法前,反射要先查找对应的Field/Method,要慢一些;
  1. 如果能用接口实现同样的灵活性,就不要使用反射;

注解

  1. 注解是给程序添加一些信息,这些信息可以用于修饰它后面紧挨着的其他代码元素,比如类、接口、字段、方法、方法中的参数、构造方法等。注解可以被编译器、程序运行时和其他工具使用,用于增强或修改程序;
  2. Java内置@Override@Deprecated@SuppressWarning三个注解,这三个注解远远不能满足日常开发需求,因此自定义注解产生。
  3. 注解是声明式编程中的一种巧妙应用,在这种风格中,程序通常由下列三部分实现:
  • 声明的关键字和语法本身;
  • 系统/框架/库,它们负责解释,执行声明式的语句;
  • 应用程序,使用声明式编程风格编写程序;
  1. 创建注解:使用@interface关键字声明注解,另外是需要在声明的注解标明元注解。以@Override注解为例:
1
2
3
4
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

4.1 ElementType是一个枚举,可以取下列值:

  • TYPE:表示类、接口(包括注解),或者是枚举类型
  • FIELD:字段,包括枚举常量
  • METHOD:方法
  • PARAMETER:方法中的参数
  • CONSTRUCTOR:构造方法
  • LOCAL_VARIABLE:本地变量
  • MODULE:模块(Java 9引入)

4.2 如果没有声明@Target,默认适用于所有类型;

4.3 @Retention表示注解信息保留到什么时候,取值只有一个,类型为RetentionPolicy,它是一个枚举,有三个值:

  • SOURCE:只在源代码中保留,编译器将代码编译为字节码文件后去掉;
  • CLASS:保留到字节码文件中,但Java虚拟机将class文件加载到内存时不一定会在内存中保留(默认值);
  • RUNTIME:一直保留到运行时

4.4 @Documented注解表示注解信息包含到生成文档中;

4.5 注解不能继承,但是使用了注解的父类一旦被子类继承,那么子类就可以继承父类的注解,如果要实现这样的效果,需要在父类的注解中加上@Inherited

  1. 总结:注解提升了Java语言的表达能力,有效地实现了应用功能和底层功能的分离。框架/库的程序员可以专注于底层实现,借助反射实现通用功能,提供注解给应用程序员使用,应用程序员可以专注于应用功能,通过简单的声明式注解与框架/库进行协作;

动态代理

  1. 静态代理
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
public class SImpleStaticProxy {
interface IService {
void sayHello();
}

// 真实对象
static class RealService implements IService {
@Override
public void sayHello() {
System.out.println("RealService Say Hello!");
}
}

// 代理对象
static class TraceProxy implements IService {
private IService realService;

TraceProxy(IService realService) {
this.realService = realService;
}

@Override
public void sayHello() {
System.out.println("entering say hello...");
this.realService.sayHello();
System.out.println("exited say hello...");
}
}

public static void main(String[] args) {
IService realService = new RealService();
IService proxyService = new TraceProxy(realService);
proxyService.sayHello();
}
}
  1. 动态代理
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
public class SimpleDynamicProxy {
interface IService {
void sayHello();
}

// 真实对象
static class RealService implements IService {
@Override
public void sayHello() {
System.out.println("Real Service Say Hello");
}
}

// 代理对象实现JDK中的InvocationHandler接口
static class SimpleInvocationHandle implements InvocationHandler {
private Object realObj;

SimpleInvocationHandle(Object realObj) {
this.realObj = realObj;
}

// 重写invoke方法
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// proxy:表示代理对象本身,不是被代理对象,一般用处不大
// method:表示正在被调用的方法
// args:表示方法的参数
System.out.println("entering " + method.getName());
Object result = method.invoke(realObj, args);
System.out.println("leaving " + method.getName());
return result;
}
}

public static void main(String[] args) {
IService realService = new RealService();
IService proxyService = (IService) Proxy.newProxyInstance(IService.class.getClassLoader(), new Class<?>[]{IService.class},
new SimpleInvocationHandle(realService)); // 使用Proxy.newProxyInstance生成一个代理对象
proxyService.sayHello();
}
}
  • newProxyInstance的声明如下:
1
2
3
4
5
public static Object newProxyInstance(ClassLoader classLoader, Class<?>[] interfaces, InvocationHandler h);
// ClassLoader:表示类加载器;
// Class<?>[] interfaces:表示代理类要实现的接口列表,元素类型只能是接口,不能是普通类;
// InvocationHandler:是一个接口,对代理接口所有方法的调用都会转给该方法;
// newProxyInstance是Object类型,它不能强制转换为某个类型,只能转换为接口类型;
  1. 类定义本身与被代理的对象没有关系,与InvocationHandler的具体实现也没有关系,而主要与接口数组有关,给定的接口数组会动态地创建每个接口的实现代码,实现就是转发给InvocationHandler,与被代理对象的关系以及对它的调用由InvocationHandler实现管理。
  2. 总结:使用动态代理,可以编写通用的代理逻辑,用于各种类型的被代理对象,而不需要为每个被代理的类型都是创建一个静态代理类。
  3. 局限性:JDK中的动态代理只能为接口创建代理,返回的代理对象也只能转换到某个接口类型;如果一个类没有接口,或者希望代理非接口中定义的方法,就无能为力了。
  4. 使用cglib的动态代理类可以解决上述的痛点。示例如下:
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 SimpleCGLibDemo {
static class RealService {
public void sayHello() {
System.out.println("hello");
}
}
static class SimpleInterceptor implements MethodInterceptor {
@Override
public Object intercept(Object object, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("entering " + method.getName());
Object result = proxy.invokeSuper(object, args);
System.out.println("leaving " + method.getName());
}
}
private static <T> T getProxy(Class<T> cls) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(cls);
enhancer.setCallback(new SimpleInterceptor());
return (T)enhancer.create();
}

public static void main(String[] args) {
RealService proxyService = getProxy(RealService.class);
proxyService.sayHello();
}
}
  1. cglib代理面向的一个具体的类,创建对象只有一个;而SDK代理面向的是接口;

类加载机制

  1. 类加载机制可以应用于下列场景:
  • 热部署:不重启程序的情况下动态替换
  • 应用模块化和互相隔离:不同的ClassLoader可以加载相同的类,但是互相隔离、互不影响。例如:Tomcat管理多个Web应用程序;
  • 从不同地方灵活加载:系统默认从本地.class文件或者.jar包中加载字节码文件,通过自定义ClassLoader可以从共享服务器、数据库、缓存服务器等其他地方加载字节码文件;
  1. 类加载机制组成部分:
  • 启动类型加载器(Bootstrap Classloader):一般由C++实现的,它负责加载Java的基础类,主要是/lib/rt.jar;
  • 扩展类加载器(Extension Classloader):加载器的实现类sun.misc.Launcher$ExtClassLoader,它负责加载Java的一些扩展类;
  • 应用程序类加载器(Application ClassLoader):实现类是sun.misc.Launcher$AppClassLoader。负责加载应用程序的类,包括自己写的和引入的第三方类库,即所有在类路径中指定的类;
  1. 类加载的全过程:
  • 1)判断是否已经加载过,如果加载过直接返回Class对象,一个类只会被一个ClassLoader加载过一次;
  • 2)如果没有被加载,先让父ClassLoader去加载,如果加载成功,返回得到的Class对象;
  • 3)在父ClassLoader没有加载成功的前提下,自己尝试加载类;
  1. 这个过程称之为“双亲委派”模型。优先让父ClassLoader去加载。
  2. 类加载都按照“双亲委派”模型,但是也有例外:
  • 自定义加载顺序:即自定义的类加载器可以不遵循“双亲委派”模型,但一般上是不推荐的;
  • 网状加载顺序:在OSGI和Java9中存在网状加载的情况。
  • 父加载器委派给子加载器:典型应用有JNDI。
  1. 类加载器的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
public class ClassLoaderDemo {
public static void main(String[] args) {
ClassLoader c1 = ClassLoaderDemo.class.getClassLoader();
while (c1 != null) {
System.out.println(c1.getClass().getName());
c1 = c1.getParent();
}
System.out.println(String.class.getClassLoader());
}
}
// 输出结果为:
// sun.misc.Launcher.$AppClassLoader
// sun.misc.Launcher.$ExtClassLoader
  1. ClassLoader中有一个默认方法public static ClassLoader getSystemClassLoader(),还有一个默认方法用于加载类: public class<?> loadClass(String name) throws ClassNotFoundException。示例如下:
1
2
3
4
5
6
7
8
ClassLoader c1 = ClassLoader.getSystemClassLoader();
try {
Class<?> cls = c1.loadClass("java.util.ArrayList");
ClassLoader actualLoader = cls.getClassLoader();
System.out.println(actualloader);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
  1. 类加载器是一个抽象类。Application ClassLoader和Extension ClassLoader的具体实现类分别是sun.misc.Launcher$AppClassLoader以及sun.misc.Launcher$ExtClassLoader。Bootstrap ClassLoader不是由Java实现的。因此没有实现类。需要说明的是,由于是委派机制。使用getClassLoader()方法返回的不一定调用load-Class.
  2. ClassLoader中的loadClass方法只会加载类但是不会执行。示例如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class CLInitDemo {
public static class Hello {
static {
System.out.println("hello");
}
}

public static void main(String[] args) {
ClassLoader c1 = ClassLoader.getSystemClassLoader();
String className = CLInitDemo.class.getName() + "$Hello";
try {
Class<?> cls = c1.loadClass(className); // 不会输出"hello"
// Class<?> cls = Class.forName(className); // 会输出"hello"
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
  1. 通过更改配置,不用改变代码,就可以改变程序的行为,在设计模式中,也是一种策略模式。

正则表达式

单个字符

  1. 用字符本身表示
  2. 特殊字符:例如制表符’\t’,换行符’\n’以及回车符’\r’;
  3. 八进制表示的字符:以’\0’开头,后面加上1~3位数字,例如\0141(8) -> 97(10)
  4. 十六进制表示的字符:以’\x’开头,后面跟两位字符,比如\x6A(16) -> 106(10)
  5. Unicode表示字符,以’\u’开头,后面跟4位字符
  6. 斜杠本身就是特殊字符,如果需要使用斜杠需要用一个斜杠进行转义;
  7. 元字符:例如“.、*、?、+”等,如果要皮匹配自身,需要在前面加’’进行转义

字符祖

  1. ‘.’字符匹配除了换行符以外的任意字符,也可以使用“单行匹配模式”或者“点号匹配模式”。可以以(?s)开头,s表示single line;
  2. 中括号[]表示匹配字符组中的任意一个字符,例如[abcd]匹配任意一个字母;
  3. ‘^’表示排除符号;
  4. \d:匹配一个数字字符,等同于[0-9];
  5. \w:匹配一个单词字符,等同于[a-zA-Z_0-9];
  6. \s:匹配一个空白字符,等同于[\t\n\x0B\f\r]
  7. \D:匹配一个非数字字符,即\d
  8. \W:匹配一个非单词字符,即\w
  9. \S:匹配一个非空白字符,即\s

量词

  1. 量词是指定出现次数的元字符,常见的有三种+、*、?
  2. ‘+’表示前面字符的一次或多次出现。比如正则表达式ab+c,既能匹配abc,也能匹配abbc:
  3. ‘表示前面的字符的零次或多次出现,例如:abc。既能匹配abc,也能匹配ac或abbbc;
  4. ‘?’表示前面字符可能出现,也可能不出现。例如正则表达式ab?c,既能匹配abc,也能匹配ac;
  5. 通用表示出现次数的语法为{m,n},出现次数从m到n,包括m和n。如果n没有限制可以省略。如果m和n一样,可以省略一个写为{m}。例如:
  • ab{1,10}c:b可以出现1次到10次
  • ab{3}c:b必须出现三次
  • ab{1,}c:等同于ab+c
  • ab{0,}c:等同于ab*c
  • ab{0,1}c:等同于ab?c
  1. 量词默认是贪婪的,也就是说会从正则开始匹配到结束内的所有字符,如果需要在匹配到第一个就终止,需要使用懒惰量词。即在两次后面加上一个符号’?’;

分组

  1. 可以用括号将表达式括起来,表示一个分组。分组匹配的子字符串可以在后续访问,好像被被捕获了一样,所以默认分组称之为捕获分组;
  2. 在正则表达式中,可以使用斜杠\加分组编号引用之前匹配的分组,称之为回溯引用;

特殊边界匹配

  1. 常见的特殊边界的元字符有^、$、\A、\Z、\z和\b。
  2. ‘’匹配整个字符串的开始,此外‘’也表示排除,默认单行匹配;
  3. ‘$’匹配整个字符串的结束,默认单行匹配;
  4. 多行匹配的方式如下:以(?m)开头。例如:(?m)^abc$
  5. \A和^类似;
  6. \Z和$类似;\z匹配的总是结束的边界;
  7. \b匹配的是单词的边界;
  8. 边界匹配不同于字符匹配,可以认为,在一个字符串中,每个字符的两边都是边界;

环视边界匹配

  1. 此部分内容比较晦涩,暂不做了解;

函数式编程

函数是接口:

  1. Java中预定义了大量的函数式接口:
函数接口 方法定义 说明
Predicate boolean test(T t) 谓词,测试输入条件是否满足要求
Function R apply(T t) 函数转换,输入类型T,输出类型R
Consumer void accept(T t) 消费者,输入类型T
Supplier T get() 工厂方法
UnaryOperator T apply(T t) 函数转换的特例,输入和输出类型一样
BiFunction R apply(T t, U u) 函数转换,接受两个参数,输出R
BinaryOperator T apply(T t, T u) BiFunction的特例,输入和输出类型一样
BiConsumer void accept(T t, U u) 消费者,接受两个参数
BiPredicate boolean test(T t, U u) 谓词,接受两个参数
  1. Predicate示例:
1
2
3
4
5
6
7
8
static class Student {
String name;
double score;
// 省略getter/setter
}

// 借助Predicate撰写一个filter的逻辑,例如:
students = filter(students, t -> t.getScore() > 90);
  1. Function示例:数据转换
1
2
3
4
5
// 根据学生列表返回名称列表的代码:
List<Student> names = map(students, t -> t.getName());

// 将学生名称转换为大写:
students = map(students, t -> new Student(t.getName().toUpperCase(), t.getScore()))
  1. Consumer示例:直接对原值进行修改
1
foreach(students, t -> t.setName(t.getName(),.toUpperCase()))

方法引用

  1. 示例代码中Student::getName称之为方法引用,使用::分隔开。分隔符前面是类型或变量名,后者是方法名。方法可以是实例方法,也可以是静态方法。例如:
1
2
3
4
5
6
public static String getColleageName() {
return "Test String";
}
// 下面两条句子的等价的
Supplier<String> s = Student::getColleageName;
Supplier<String> s = () -> Student.getColleageName();

函数的复合

  1. 函数式接口和Lambda表达式可以用作方法的返回值,传递代码回调者,将上述两种方法结合起来,可以构造复合的函数,使程序简洁易读;
  2. 复合Comparator中的复合方法:
1
2
3
Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));
// 进一步简化
Arrays.sort(files, Comparator.comparing(File::getName()));

Stream API

  1. 基本过滤
1
2
3
4
5
6
7
8
9
10
11
// 使用Stream API之前
List<Student> above90List = new ArrayList<>();
for (Student t : students) {
if (t.getScore() > 90) {
above90List.add(t);
}
}

// 使用Stream API之后
List<Student> above90List = students.stream()
.filter(t -> t.getScore() > 90).collect(Collectors.toList());
  • 没有显式的迭代循环,循环过程被Stream隐藏;
  • 提供了声明式的处理函数;
  • 流畅式接口;
  1. 基本转换
1
2
3
4
5
6
7
8
9
// 使用Stream API之前
List<String> nameList = new ArrayList<>();
for (Student t : students) {
nameList.add(t.getName());
}

// 使用Stream API之后
List<String> nameList = students.stream()
.map(Student::getName).collect(Collectors.toList());
  1. 基本过滤和转换组合
1
2
3
4
List<String> above90List = students.stream()
.filter(t -> t.getScore() > 90)
.map(Student::getName)
.collect(Collectors.toList());
  1. distinct运算
  • 用于过滤重复元素,只留下唯一元素,是否重复根据equals()方法来比较。
1
2
3
4
List<String> list = Arrays.asList(new String[]{"abc", "def", "hello", "Abc"});
List<String> retList = list.stream()
.filter(s -> s.length() <= 3).map(String::toLowerCase).distinct()
.collect(Collectors.toList());
  1. sorted方法
  • 对流中的元素进行排序,返回一个排序后的stream。例如:
1
2
3
4
List<Student> list = students.stream().filter(t -> t.getScore() > 0)
.sorted(Comparator.comparing(Student::getScore)
.reversed().thenComparing(Student::getName))
.collect(Collectors.toList());
  1. skip/limit
  • 跳过流中的n个元素,如果流的长度不足n个,返回一个空流。例如:
1
2
3
List<Student> list = students.stream()
.sorted(Comparator.comparing(Student::getScore).reversed()
.skip(2).limit(3).collect(Coolectors.toList()));
  1. peek
  • 返回一个与之前一样的流,没有变化。但是提供一个Consumer,会将流中的每一个元素传给Consumer。这个方法主要目的用于支持调试,可以使用该方法观察在流水线中流转的元素。例如:
1
2
3
4
List<String> above90Names = students.stream()
.filter(t -> t.getScore() > 90)
.peek(System.out::println).map(Student::getName)
.collect(Collectors.toList());
  1. mapToLong/mapToInt/mapToDouble
  • map函数接受函数参数是一个Function<T, R>,为避免拆箱,装箱提高性能。例如:
1
2
double sum = students.stream()
.mapToDouble(Student::getScore).sum();
  1. flatMap
  • 接受一个函数mapper,对流中的每一个元素,mapper会将该元素转换成一个流Stream。然后将新生成流的每一个元素传递给下一个操作。比如:
1
2
3
4
5
List<String> lines = Arrays.asList(new String[]{"hello abc", "laoma biancheng"});
List<String> words = lines.stream()
.flatMap(line -> Arrays.stream(line.split("\\s+")))
.collect(Collectors.toList());
System.out.println(words); // [hello, abc. laoma, biancheng]

终端操作

  1. 中间操作不触发实际执行,返回值是Stream,而终端操作触发执行,返回一个具体的值。除了collect之外还有:max, min, count, allMatch, anyMatch, noneMatch, findFirst, findAny, forEach, toArray, reduce等。
  2. max/min: 返回流中的最大值/最小值。其返回类型为Optional而非T。Optional表明可能为null,程序应当进行适当的处理。例子:
1
2
Student student = students.stream()
.max(Comparator.comparing(Student::getScore).reversed()).get();
  1. count:返回流中的元素个数,例如:
1
2
long above90Count = students.stream()
.filter(t -> t.getScore() > 90).count();
  1. allMatch/anyMatch/noneMatch:用于判定流中额元素是否满足一定的条件,区别在于:
  • allMatch:流中所有元素都满足条件的情况下返回true;
  • anyMatch:流中的元素只要有一个元素满足条件即返回true;
  • noneMatch:只有流中的所有元素都不满足条件才返回true;
  • 示例如下:
1
2
boolean allPass = students.stream()
.allMatch(t -> t.getScore() >= 60);
  1. findFirst/findAny:返回类型均为Optional,如果流为空则返回Optional.empty()。findFirst返回第一个元素,而findAny返回任一元素。均为短路操作。示例:
1
2
3
4
5
6
Optional<Student> student = students.stream()
.filter(t -> t.getScore() < 60)
.findAny();
if (student.isPresent()) {
// 处理不符合要求的数据
}
  1. forEach/forEachOrdered:并行流中forEach不保证顺序,forEachOrdered能保证顺序。例如:
1
2
3
students.stream()
.filter(t -> t.getScore() > 90)
.forEach(System.out::println);
  1. toArray:将流转换为数组,包含两个方法。
1
2
Object[] toArray() // 返回值为Object[]
<A> A[] toArray(IntFunction<A[]> generator) // 得到指定类型的数组
  • 示例为:
1
2
3
Student[] above90Arr = students.stream()
.filter(t -> t.getScore() > 90)
.toArray(Student[]::new); // Student[]::new就是一个IntFunction类型的ggenerator
  1. reduce:归约或折叠,将流中的元素归约为一个值,有三种reduce函数:
1
2
3
Optional<T> reduce(BinaryOperator<T> accumulator);
T reduce(T identity, BinaryOperator<T> accumulator);
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
  • 第一个reduce函数的使用
1
2
3
4
5
6
7
8
Student topStudent = students.stream()
.reduce((acc, t) -> {
if(acc.getScore() >= t.getScore()) {
return acc;
} else {
return t;
}
}).get();
  • 第二个reduce函数比第一个多了identity参数,表示初始值
  • 第三个reduce函数更为通用,可以自定义归约类型,另外还多了一个combiner参数,在并行流中用于合并子线程结果。还有示例:
1
2
3
4
double sumScore = students.stream()
.reduce(0d, (sum, t) -> sum += t.getScore(),
(sum1, sum2) -> sum1 += sum2)
);
  1. 迭代Map的过程删除元素,直接使用forEach会报ConcurrentModificationException
1
2
Map<String, Object> map = other.getHashMap<>();
map.entrySet().removeIf(entryElement -> "some condition".equals(entryElement.getKey()))

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!